import { MaterialCommunityIcons } from "@expo/vector-icons"; import { router } from "expo-router"; import { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, Alert, Image, ScrollView, Text, TextInput, TouchableOpacity, View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { CustomButton } from "@/components/custom-button"; import { icons, images } from "@/constants"; import { SERVICES, type ServiceId } from "@/constants/services"; import { ApiError, fetchAPI } from "@/lib/fetch"; import { useSession } from "@/lib/session"; import { useDriverLocation } from "@/lib/use-driver-location"; import { formatTime } from "@/lib/utils"; // Poll cadence for the driver dashboard (offers / active ride / earnings). const POLL_MS = 4000; type Profile = { id: number; first_name: string; last_name: string; profile_image_url: string | null; car_image_url: string | null; car_seats: number; rating: number; service: ServiceId; online: boolean; car_model: string | null; }; type Offer = { offer_id: number; offered_at: string; ride_id: number; origin_address: string; destination_address: string; ride_time: number; fare_price: number; payment_status: string; service: string; }; type ActiveRide = { ride_id: number; status: string; service: string; payment_status: string; origin_address: string; destination_address: string; ride_time: number; fare_price: number; rider_name: string | null; rider_phone: string | null; }; type Dashboard = { offers: Offer[]; active: ActiveRide | null; recent: { ride_id: number; fare_price: number; service: string }[]; earnings: number; }; const DriverHome = () => { const { signOut, user } = useSession(); const [loading, setLoading] = useState(true); const [profile, setProfile] = useState(null); const [online, setOnline] = useState(false); const [dashboard, setDashboard] = useState(null); const [busy, setBusy] = useState(false); const loadProfile = useCallback(async () => { try { const res = await fetchAPI("/(api)/driver/profile"); const p = res.data as Profile; setProfile(p); setOnline(p.online); } catch (err) { // 403 with code ONBOARD means no profile yet — show the onboarding form. if (err instanceof ApiError && err.status === 403) { setProfile(null); } else { console.log("[DRIVER_PROFILE_LOAD]: ", err); } } finally { setLoading(false); } }, []); useEffect(() => { void loadProfile(); }, [loadProfile]); // Keep the location heartbeat running only while the driver is online and // has completed onboarding. useDriverLocation(online && profile !== null); // Poll the dashboard while online. useCallback keeps the fetcher stable so the // interval effect doesn't re-subscribe on every render. const fetchDashboard = useCallback(async () => { try { const res = await fetchAPI("/(api)/driver/rides"); setDashboard(res.data as Dashboard); } catch (err) { console.log("[DRIVER_DASHBOARD_POLL]: ", err); } }, []); useEffect(() => { if (!online || !profile) return; void fetchDashboard(); const timer = setInterval(() => void fetchDashboard(), POLL_MS); return () => clearInterval(timer); }, [online, profile, fetchDashboard]); const toggleOnline = async () => { if (!profile) return; const next = !online; setBusy(true); try { await fetchAPI("/(api)/driver/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ online: next }), }); setOnline(next); setProfile({ ...profile, online: next }); if (!next) setDashboard(null); } catch (err) { console.log("[DRIVER_TOGGLE]: ", err); Alert.alert("Error", "Could not change your status. Please try again."); } finally { setBusy(false); } }; const respond = async (offer: Offer, action: "accept" | "decline") => { setBusy(true); try { await fetchAPI(`/(api)/ride/${offer.ride_id}/respond`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }), }); await fetchDashboard(); } catch (err) { console.log("[DRIVER_RESPOND]: ", err); Alert.alert( "Error", action === "accept" ? "Could not accept this ride. It may have been taken or expired." : "Could not decline this ride. Please try again.", ); } finally { setBusy(false); } }; const advance = async (rideId: number, status: "en_route" | "completed") => { setBusy(true); try { await fetchAPI(`/(api)/ride/${rideId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }); await fetchDashboard(); } catch (err) { console.log("[DRIVER_ADVANCE]: ", err); Alert.alert("Error", "Could not update the ride. Please try again."); } finally { setBusy(false); } }; if (loading) { return ( ); } if (!profile) { return ( ); } const earnings = dashboard?.earnings ?? 0; const rideCount = dashboard?.recent.length ?? 0; return ( Driver mode Sign out {/* Online / offline toggle */} {online ? "● Online — receiving ride requests" : "○ Go online to drive"} {/* Earnings summary */} Today's earnings ${(earnings / 100).toFixed(2)} Completed today {rideCount} {/* Active ride */} {dashboard?.active ? ( ) : null} {/* Incoming offers */} Incoming requests {online ? "" : "(offline)"} {!online ? null : dashboard?.offers.length ? ( dashboard.offers.map((offer) => ( respond(offer, "accept")} onDecline={() => respond(offer, "decline")} /> )) ) : ( {online ? "Waiting for ride requests…" : "Go online to start driving."} )} ); }; // --- Onboarding form ------------------------------------------------------ const Onboarding = ({ onCreated, signOut, userName, }: { onCreated: () => Promise; signOut: () => Promise; userName?: string | null; }) => { const [service, setService] = useState("car"); const [carModel, setCarModel] = useState(""); const [carSeats, setCarSeats] = useState("4"); const [submitting, setSubmitting] = useState(false); const submit = async () => { const seats = Number(carSeats); if (!Number.isInteger(seats) || seats < 1 || seats > 8) { Alert.alert("Invalid seats", "Car seats must be a whole number 1–8."); return; } setSubmitting(true); try { await fetchAPI("/(api)/driver/profile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ service, car_model: carModel.trim() || null, car_seats: seats, }), }); await onCreated(); } catch (err) { console.log("[DRIVER_ONBOARD]: ", err); Alert.alert("Error", "Could not create your driver profile. Please try again."); } finally { setSubmitting(false); } }; return ( Welcome, {userName?.split(" ")[0] || "driver"} Sign out Set up your driver profile to start receiving ride requests. What will you drive? {SERVICES.map((item) => { const active = item.id === service; return ( setService(item.id)} className={`flex-1 items-center rounded-2xl border py-3 ${ active ? "border-primary-500 bg-primary-500/10" : "border-neutral-100 bg-neutral-100" }`} > {item.label} ); })} Car model Car seats ); }; // --- Offer card ----------------------------------------------------------- const OfferCard = ({ offer, busy, onAccept, onDecline, }: { offer: Offer; busy: boolean; onAccept: () => void; onDecline: () => void; }) => ( New request · {offer.service} {offer.payment_status === "cash" ? "💵 Cash" : "💳 Card"} From {offer.origin_address} To {offer.destination_address} Trip time {formatTime(offer.ride_time)} Fare ${(offer.fare_price / 100).toFixed(2)} Decline {busy ? "…" : "Accept"} ); // --- Active ride card ----------------------------------------------------- const ActiveRideCard = ({ ride, busy, onAdvance, }: { ride: ActiveRide; busy: boolean; onAdvance: (rideId: number, status: "en_route" | "completed") => void; }) => { const statusLabel = ride.status === "accepted" ? "Head to pickup" : ride.status === "en_route" ? "Trip in progress" : ride.status; return ( ● {statusLabel} {ride.service} {ride.rider_name ? ( {ride.rider_name} ) : null} From {ride.origin_address} To {ride.destination_address} Fare ${(ride.fare_price / 100).toFixed(2)} {ride.status === "accepted" ? ( onAdvance(ride.ride_id, "en_route")} className="mb-2" /> ) : null} {ride.status === "en_route" ? ( onAdvance(ride.ride_id, "completed")} /> ) : null} ); }; export default DriverHome;