Driver onboarding now photographs the licence, ID card and vehicle
registration and reads the credential fields off them, plus a camera-only
profile selfie riders check the arriving driver against. Adds in-app chat
and WebRTC calls, push-backed ride offers, ratings, cancellation and
payment sheets, settlement, and the owner dashboard endpoints behind them.
Camera permission on Android:
- Declare CAMERA and READ_MEDIA_IMAGES in the manifest. expo-image-picker's
own plugin never declares CAMERA, and Android denies a request for an
undeclared permission instantly and silently — no dialog is ever shown,
which is indistinguishable from the app not asking at all.
- Handle canAskAgain: once Android stops showing the dialog, repeating why
we need it is a dead end, so offer Open Settings instead (lib/capture-
permission.ts), matching what the location flow already did.
Session: a 401 on a request that carried a token now ends the session
instead of being reinterpreted per-screen — driver-home had been reading it
as "this user has no driver profile" and showing an onboarding form to an
already-onboarded driver. Requests without a token are exempt so a failed
sign-in doesn't sign you out, and the notification is latched per token so
concurrent polls tear the session down once. (root) gains the auth guard
that turns that into the sign-in screen; app/index.tsx only guarded the way
in, leaving a session that ended mid-screen with nowhere to go.
Also ignore .uploads/ — it holds driver licence, ID and vehicle scans plus
profile photos, which are personal data and must not be committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The Android map never drew because mapType was "mutedStandard", an Apple
Maps value. Android's MapManager looks the name up in a fixed table and
unboxes the result into an int, so an unrecognised value threw a
NullPointerException in the native view manager before any tile
rendered. Android now gets "standard" plus a customMapStyle that mutes
POI and transit labels, since showsPointsOfInterest is iOS-only too.
Location hung indefinitely: getCurrentPositionAsync was called with no
accuracy and no timeout, so it waited for a GPS fix that never arrives
indoors or on an emulator with no mock location. useUserLocation now
takes a cached fix first for an immediate render, caps the precise
reading at 15 seconds, and checks device location services separately
from app permission. Reverse geocoding moved off the critical path so a
failed lookup costs the address label rather than the coordinates. The
single boolean became five states, each with its own notice and either a
retry or a settings shortcut, since retrying a hard denial does nothing.
Map also no longer deletes itself when the driver fetch fails or the
location is still pending: drivers are an overlay, and calculateRegion
already falls back to Beirut.
Adds a four-tile service selector above Recent Rides - Car, Moto,
Courier, My Car - with the choice held in useServiceStore for the
booking flow to read. English only for now; the intended Arabic names
are recorded in constants/services.ts for the language pass. Selection
styling matches the role picker on sign-up so the two read as one
control.
app.config.js layers the Google Maps key and expo-location permission
strings onto app.json. No effect under Expo Go, which ignores native
config, but required for the first EAS build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>