← 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' },
});