← back to Costa Rica App
app/book/[slug].tsx
108 lines
import { useEffect, useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import * as WebBrowser from 'expo-web-browser';
import { api, money, isAuthed } from '../../src/api';
export default function BookScreen() {
const { slug } = useLocalSearchParams<{ slug: string }>();
const [l, setL] = useState<any>(null);
const [checkIn, setCheckIn] = useState('2026-09-10');
const [checkOut, setCheckOut] = useState('2026-09-13');
const [guests, setGuests] = useState('2');
const [booking, setBooking] = useState<any>(null);
const [busy, setBusy] = useState(false);
useEffect(() => { api.listing(slug!).then(r => setL(r.listing)).catch(() => {}); }, [slug]);
if (!l) return <ActivityIndicator style={{ marginTop: 40 }} />;
async function quote() {
if (!isAuthed()) { router.push('/login'); return; }
setBusy(true);
try {
const r = await api.createBooking({ place_slug: slug, check_in: checkIn, check_out: checkOut, guests: +guests });
setBooking({ ...r.booking, split: r.split });
} catch (e: any) { Alert.alert('Could not create booking', e.message); } finally { setBusy(false); }
}
async function pay(method: 'card' | 'sinpe') {
setBusy(true);
try {
const r = await api.pay(booking.code, method);
if (r.client_action?.type === 'redirect') {
// Tilopay 3-D Secure / hosted checkout — return via the app scheme.
await WebBrowser.openAuthSessionAsync(r.client_action.url, 'crmarketplace://pay/return');
} else if (r.client_action?.type === 'sinpe_instructions') {
Alert.alert('SINPE Móvil', `${r.client_action.note}\n\nSINPE: ${r.client_action.sinpe_phone}`);
}
// Poll for confirmation
for (let i = 0; i < 10; i++) {
const p = await api.payment(String(r.payment_id));
if (p.payment.status === 'succeeded') {
Alert.alert('Confirmed! 🎉', `Booking ${booking.code} is confirmed.`);
router.replace('/bookings'); return;
}
if (p.payment.status === 'failed') { Alert.alert('Payment failed'); return; }
await new Promise(r => setTimeout(r, 1500));
}
router.replace('/bookings');
} catch (e: any) { Alert.alert('Payment error', e.message); } finally { setBusy(false); }
}
return (
<View style={s.wrap}>
<Text style={s.name}>{l.name}</Text>
{!booking ? (
<>
<Field label="Check-in (YYYY-MM-DD)" value={checkIn} onChange={setCheckIn} />
<Field label="Check-out (YYYY-MM-DD)" value={checkOut} onChange={setCheckOut} />
<Field label="Guests" value={guests} onChange={setGuests} keyboard="number-pad" />
<TouchableOpacity style={s.cta} onPress={quote} disabled={busy}>
<Text style={s.ctaTxt}>{busy ? '…' : 'Review price'}</Text>
</TouchableOpacity>
</>
) : (
<>
<View style={s.box}>
<Row k="Subtotal" v={money(booking.split.subtotal, booking.currency)} />
{booking.split.cleaningFee ? <Row k="Cleaning" v={money(booking.split.cleaningFee, booking.currency)} /> : null}
<Row k="Service fee" v={money(booking.split.platformFee, booking.currency)} />
<View style={s.hr} />
<Row k="Total" v={money(booking.split.total, booking.currency)} bold />
</View>
<Text style={s.pick}>Pay with</Text>
<TouchableOpacity style={s.cta} onPress={() => pay('card')} disabled={busy}>
<Text style={s.ctaTxt}>{busy ? '…' : '💳 Card (USD/CRC)'}</Text>
</TouchableOpacity>
<TouchableOpacity style={[s.cta, s.ctaAlt]} onPress={() => pay('sinpe')} disabled={busy}>
<Text style={[s.ctaTxt, { color: '#0a7d55' }]}>📱 SINPE Móvil</Text>
</TouchableOpacity>
</>
)}
</View>
);
}
const Field = ({ label, value, onChange, keyboard }: any) => (
<View style={{ marginTop: 12 }}>
<Text style={s.lbl}>{label}</Text>
<TextInput style={s.inp} value={value} onChangeText={onChange} keyboardType={keyboard || 'default'} autoCapitalize="none" />
</View>
);
const Row = ({ k, v, bold }: any) => (
<View style={s.row}><Text style={[s.rk, bold && s.b]}>{k}</Text><Text style={[s.rv, bold && s.b]}>{v}</Text></View>
);
const s = StyleSheet.create({
wrap: { flex: 1, padding: 16, backgroundColor: '#f6f7f5' },
name: { fontSize: 20, fontWeight: '800', color: '#1a2b22' },
lbl: { color: '#6b756e', marginBottom: 4, fontSize: 13 },
inp: { backgroundColor: '#fff', borderRadius: 10, height: 46, paddingHorizontal: 14, borderWidth: 1, borderColor: '#e2e5e0' },
cta: { backgroundColor: '#0a7d55', height: 52, borderRadius: 12, alignItems: 'center', justifyContent: 'center', marginTop: 14 },
ctaAlt: { backgroundColor: '#fff', borderWidth: 1.5, borderColor: '#0a7d55' },
ctaTxt: { color: '#fff', fontSize: 16, fontWeight: '700' },
box: { backgroundColor: '#fff', borderRadius: 12, padding: 16, marginTop: 16, borderWidth: 1, borderColor: '#e8ebe6' },
row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
rk: { color: '#6b756e' }, rv: { color: '#1a2b22' }, b: { fontWeight: '800', fontSize: 16 },
hr: { height: 1, backgroundColor: '#eee', marginVertical: 8 },
pick: { marginTop: 20, fontWeight: '700', color: '#1a2b22' },
});