import * as Location from "expo-location"; import { AppState } from "react-native"; import { useEffect, useRef } from "react"; import { fetchAPI } from "@/lib/fetch"; // While the driver is online, watch their position and POST it to the server // as a heartbeat. Each ping both updates the driver's lat/lng and refreshes // last_seen/online, which is what keeps the driver eligible for matching. The // watch is started when `online` flips true and torn down on false/unmount. // // The watch must also restart when the app returns to the foreground: the OS // suspends location updates in the background, and the subscription we hold // does not auto-revive. Without this, a driver who briefly backgrounds the app // goes permanently stale (last_seen older than the dispatch freshness window) // and stops receiving ride requests until they toggle offline→online again. // // Pings are throttled to every PING_INTERVAL_MS so a fast-moving driver // doesn't hammer the server, and the location permission is only requested // once the driver actually intends to go online. const PING_INTERVAL_MS = 5000; export const useDriverLocation = (online: boolean) => { const subscriptionRef = useRef(null); const onlineRef = useRef(online); onlineRef.current = online; useEffect(() => { if (!online) return; let cancelled = false; const ping = async (latitude: number, longitude: number) => { try { await fetchAPI("/(api)/driver/location", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ latitude, longitude }), }); } catch (error) { // A failed ping is non-fatal — the next one will retry. last_seen // going stale is what takes a driver out of the match pool, not a 500. console.log("[DRIVER_LOCATION_PING]: ", error); } }; const start = async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (cancelled || status !== "granted") return; if (!(await Location.hasServicesEnabledAsync())) return; // Seed the server with the last known position immediately, so the // driver is matchable without waiting for the first watch callback. const cached = await Location.getLastKnownPositionAsync({ maxAge: 5 * 60 * 1000, }); if (!cancelled && cached) { void ping(cached.coords.latitude, cached.coords.longitude); } const subscription = await Location.watchPositionAsync( { accuracy: Location.Accuracy.Balanced, timeInterval: PING_INTERVAL_MS, distanceInterval: 20, }, ({ coords }) => { if (!cancelled) void ping(coords.latitude, coords.longitude); }, ); if (cancelled) { await subscription.remove(); return; } subscriptionRef.current = subscription; }; const stop = () => { const sub = subscriptionRef.current; subscriptionRef.current = null; void sub?.remove(); }; void start(); // Restart the watch whenever the app comes back to the foreground. While // backgrounded the OS pauses location updates and the old subscription is // dead; without re-starting it the driver never pings again. const onAppStateChange = (state: string) => { if (state !== "active") return; if (!onlineRef.current) return; stop(); if (!cancelled) void start(); }; const subscription = AppState.addEventListener("change", onAppStateChange); return () => { cancelled = true; stop(); subscription.remove(); }; }, [online]); };