Driver side (was a stub):
- In-app driver onboarding: a driver-role user creates their own linked
drivers profile (driver/profile+api GET/POST/PATCH).
- Driver dashboard: online/offline toggle, today's earnings, incoming
request cards (accept/decline), active ride panel (start/complete trip).
Polls /driver/rides every 4s while online.
- Location heartbeat (use-driver-location): watchPositionAsync pings
/driver/location every ~5s; restarts the watch on app foreground so a
backgrounded driver doesn't go permanently stale and miss requests.
Dispatch (auto-match nearest, Uber-style):
- Ride state machine: requested -> accepted -> en_route -> completed/cancelled
with a nullable driver_id until matched (lib/dispatch.matchNextDriver).
- matchNextDriver locks the ride (SELECT FOR UPDATE), expires 15s-stale
offers, picks the nearest eligible driver of the matching service by
haversine, offers one at a time. Called from ride/create, ride/[id] GET
(lazy match on the rider's poll), and ride/[id]/respond (on decline).
- ride/create is now a request endpoint (driver_id NULL, status=requested,
service); drops the pre-match driver_id payment reconciliation.
- ride/[id] GET returns status/service/nullable driver; PATCH handles rider
cancel + driver en_route/completed. ride/list backs the history tabs.
Rider flow (best experience):
- confirm-ride is now a request screen: single trip fare + nearest-driver
ETA + cash/card + Request Ride -> live status. Periodically polls online
drivers of the selected service and disables Request when none are
online (prevents the "stuck searching forever" state).
- book-ride is the live ride-status screen (searching -> accepted ->
en_route -> completed/cancelled + Cancel), polling every 3s.
- lib/request-ride unifies the Areeba card flow + cash path.
- Map reads /driver/nearby (real positions, service-filtered); lib/map
adds calculateTripFare + service-aware fares.
POI suggestions:
- lib/places (Google Nearby Search) + nearby-suggestions chips for
mall/hospital/pharmacy/restaurant on the home screen.
Service categories now drive both matching and a per-service fare
multiplier (car 1.0 / moto 0.7 / courier 0.85 / chauffeur 1.5).
Map tiles: react-native-maps rendered blank on Android because no Google
Maps key was set. Switched app.json -> app.config.js so
android.config.googleMaps.apiKey is injected from
EXPO_PUBLIC_GOOGLE_API_KEY at build time (keeps the key out of git).
Requires a native rebuild (expo run:android) to take effect.
Also includes the prior payment/auth hardening (server-authoritative
payment_orders ledger with double-spend guards, peppered OTP, register
TOCTOU fix, stats cents fix) that was left uncommitted.
Co-Authored-By: Claude <noreply@anthropic.com>
The home screen passed its FlatList header as `ListHeaderComponent={() =>
(...)}`. VirtualizedList renders a function-valued header prop as
`<HeaderComponent />`, so a fresh arrow function on each render is a fresh
element type: React unmounted the entire header subtree — MapView included —
and mounted a new one. Home re-renders several times on mount (useFetch
loading->data, useUserLocation pending->granted, session resolve), and
recreating the Android GoogleMap surface each time left it grey with the
Google logo and tiles that never finished loading.
Pass the header as an element instead so the type stays stable and MapView
mounts once. The same `() => (...)` pattern in ListEmptyComponent here, and in
rides.tsx and confirm-ride.tsx, cost needless remounts of list chrome and
DriverCards; fixed alongside.
Also pin expo-crypto to ~13.0.2. It was ^57.0.1 — a wrong-major native module
for SDK 51 that nothing in the app imports. npm hoisted it to the top level
and gave expo-auth-session a nested 13.0.2 to satisfy its ~13.0.0 constraint,
leaving two copies of one native module in the tree for autolinking to choose
between.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>