import { router, useLocalSearchParams } from "expo-router"; import { useEffect, useState } from "react"; import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native"; import { CustomButton } from "@/components/custom-button"; import { RideLayout } from "@/components/ride-layout"; import { SERVICES } from "@/constants/services"; import { ApiError, fetchAPI } from "@/lib/fetch"; import { useT } from "@/lib/i18n"; import { calculateTripFare } from "@/lib/map"; import { formatLBP } from "@/lib/pricing"; import { requestRide } from "@/lib/request-ride"; import { useSession } from "@/lib/session"; import { formatTime, haversine } from "@/lib/utils"; import { useLocationStore, useServiceStore } from "@/store"; type PaymentMethod = "cash" | "card"; type NearbyDriver = { id: number; first_name: string; latitude: number; longitude: number; }; // Confirm-ride is now the request screen: the rider no longer browses and // picks a driver. They see a single fare estimate + nearest-driver ETA, pick a // payment method, and tap Request — auto-match assigns the driver and they're // routed to the live status screen. const ConfirmRide = () => { const params = useLocalSearchParams<{ service?: string }>(); const { userAddress, userLatitude, userLongitude, destinationAddress, destinationLatitude, destinationLongitude, } = useLocationStore(); const { service: storeService, setService } = useServiceStore(); const { user } = useSession(); const t = useT(); const service = params.service ?? storeService; const selected = SERVICES.find((s) => s.id === service) ?? SERVICES[0]; const [method, setMethod] = useState("cash"); const [estimate, setEstimate] = useState<{ fare: string; durationSeconds: number; } | null>(null); const [nearestEta, setNearestEta] = useState(null); const [driversOnline, setDriversOnline] = useState(null); const [estimating, setEstimating] = useState(true); const [processing, setProcessing] = useState(false); // Trip fare estimate — one Directions call for the trip leg, recomputed when // the route or service changes. Independent of driver availability. useEffect(() => { if ( !userLatitude || !userLongitude || !destinationLatitude || !destinationLongitude ) return; let cancelled = false; setEstimating(true); const run = async () => { const trip = await calculateTripFare({ userLatitude, userLongitude, destinationLatitude, destinationLongitude, service: selected.id, }); if (cancelled) return; setEstimate( trip ? { fare: trip.fare, durationSeconds: trip.durationSeconds } : null, ); }; void run().finally(() => { if (!cancelled) setEstimating(false); }); return () => { cancelled = true; }; }, [ userLatitude, userLongitude, destinationLatitude, destinationLongitude, selected.id, ]); // Online-driver availability for the selected service, polled so the "no // drivers" state self-heals the moment a driver of this service comes // online. The nearest driver's pickup ETA is resolved alongside the count. useEffect(() => { if (!userLatitude || !userLongitude) return; let cancelled = false; const check = async () => { try { const res = await fetchAPI( `/(api)/driver/nearby?service=${selected.id}&lat=${userLatitude}&lng=${userLongitude}`, ); const drivers = (res.data ?? []) as NearbyDriver[]; if (cancelled) return; setDriversOnline(drivers.length); if (drivers.length === 0) { setNearestEta(null); return; } const nearest = drivers .map((d) => ({ d, dist: haversine( userLatitude, userLongitude, d.latitude, d.longitude, ), })) .sort((a, b) => a.dist - b.dist)[0].d; const directionsRes = await fetch( `https://maps.googleapis.com/maps/api/directions/json?origin=${nearest.latitude},${nearest.longitude}&destination=${userLatitude},${userLongitude}&key=${process.env.EXPO_PUBLIC_GOOGLE_API_KEY}`, ); const data = await directionsRes.json(); const leg = data.routes?.[0]?.legs?.[0]; if (!cancelled) setNearestEta(leg ? Math.round(leg.duration.value / 60) : null); } catch { if (!cancelled) { setDriversOnline(null); setNearestEta(null); } } }; void check(); const timer = setInterval(() => void check(), 10000); return () => { cancelled = true; clearInterval(timer); }; }, [userLatitude, userLongitude, selected.id]); const request = async () => { if (!userLatitude || !userLongitude || !destinationLatitude || !destinationLongitude) { Alert.alert( t("confirmRide.alertMissingRouteTitle"), t("confirmRide.alertMissingRouteBody"), ); return; } if (!estimate) { Alert.alert( t("confirmRide.alertNoEstimateTitle"), t("confirmRide.alertNoEstimateBody"), ); return; } // Nested so the guards above narrow userLatitude/estimate to non-null for // the card-confirm callback as well as the direct cash path. const doRequest = async () => { setProcessing(true); try { // Keep the store in sync with whatever service we resolved for this ride. setService(selected.id); const { ride } = await requestRide({ method, service: selected.id, user: { name: user?.name ?? "", email: user?.email ?? "" }, origin: { address: userAddress ?? "", latitude: userLatitude, longitude: userLongitude, }, destination: { address: destinationAddress ?? "", latitude: destinationLatitude, longitude: destinationLongitude, }, rideTimeSeconds: estimate.durationSeconds, fareCents: Math.round(parseFloat(estimate.fare) * 100), }); router.replace(`/(root)/book-ride?id=${ride.ride_id}`); } catch (err) { console.log("[REQUEST_RIDE]: ", err); const msg = err instanceof ApiError ? err.message : t("confirmRide.alertErrorFallback"); Alert.alert(t("confirmRide.alertErrorTitle"), msg); } finally { setProcessing(false); } }; if (method === "card") { Alert.alert( t("confirmRide.alertPayCardTitle"), t("confirmRide.alertPayCardBody", { fare: estimate.fare }), [ { text: t("common.cancel"), style: "cancel" }, { text: t("common.continue"), onPress: () => void doRequest() }, ], ); } else { void doRequest(); } }; return ( {t("confirmRide.yourTrip")} {t("confirmRide.pickup")} {userAddress} {t("confirmRide.destination")} {destinationAddress} {t(selected.labelKey)} · {t(selected.taglineKey)} {t("confirmRide.tripTime", { time: estimate ? formatTime(estimate.durationSeconds / 60) : "…", })} {estimating ? "…" : estimate ? t("confirmRide.fareDisplay", { fare: estimate.fare }) : "—"} {estimate ? ( {t("confirmRide.lbpEstimate", { lbp: formatLBP(parseFloat(estimate.fare)), })} ) : null} {driversOnline === 0 ? t("confirmRide.noDrivers", { service: t(selected.labelKey) }) : nearestEta == null ? t("confirmRide.findingDrivers") : t("confirmRide.nearestDriver", { eta: nearestEta })} {t("confirmRide.paymentMethod")} setMethod("cash")} className={`flex-1 items-center py-3 rounded-xl border ${ method === "cash" ? "bg-general-600 dark:bg-primary-500/20 border-primary-500" : "bg-white dark:bg-neutral-900 border-general-700 dark:border-neutral-700" }`} > {t("confirmRide.cash")} setMethod("card")} className={`flex-1 items-center py-3 rounded-xl border ${ method === "card" ? "bg-general-600 dark:bg-primary-500/20 border-primary-500" : "bg-white dark:bg-neutral-900 border-general-700 dark:border-neutral-700" }`} > {t("confirmRide.card")} ); }; export default ConfirmRide;