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>
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>
Sign-in gains a "Keep me signed in" checkbox: checked issues a 30-day
token and prefills the address next launch, unchecked drops the session
to 12 hours and forgets the address. The TTL is chosen server-side in
the login route.
Emailed codes are now reachable without retyping. OtpField opts into the
iOS one-time-code keyboard suggestion and raises a paste chip when the
user returns from Gmail with a code on the clipboard. The mails put the
code first in the subject and body, which is what makes Gmail render its
"Copy code" notification action at all.
Fixes found along the way:
- Session was wiped on every launch. decodeJwtExp used atob, which
neither RN 0.74 nor Expo SDK 51 defines, so it threw, returned null,
and the caller read that as "expired" and deleted the token. Replaced
with a dependency-free base64url decoder, and restore now only
discards a session it can prove is expired.
- Verification and reset codes counted attempts but never enforced them,
leaving a 6-digit code open to unlimited guessing. Both routes now
charge the attempt before comparing so concurrent guesses can't race
past the cap of five, and compare in constant time.
- A wrong verification code showed the "Verified" success screen:
onModalHide fired unconditionally, so the failure state advanced the
flow. Only an explicit "verified" state does that now.
- fetchAPI discarded the server's error body, so the UI substring-matched
synthetic status strings and showed "Could not sign in" for everything.
It now throws ApiError carrying status and the server's message.
- Login answered a missing account faster than a wrong password; it now
runs the same scrypt work either way.
- Blank email or password is caught client-side instead of surfacing as
an opaque 400, and a failed attempt only clears the password on a 401.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SMTP:
- Add connection/greeting/socket timeouts so a stalled Gmail
connection no longer hangs sign-up
- Wrap sendMail in try/catch and fall back to logging the code
- Derive secure from port (465 implicit TLS vs 587 STARTTLS)
- Strip whitespace from the Gmail app password
- Document SMTP_HOST/SMTP_PORT in .env.example and environment.d.ts
Password reset (new):
- POST /(api)/auth/forgot-password emails a 6-digit code and does
not reveal whether the address is registered
- POST /(api)/auth/reset-password validates the code, sets the new
password, verifies the email, and signs the user in
- password_reset_codes table added to seed-db.mjs
- "Forgot password?" flow on the mobile sign-in screen
User deletion (new):
- DELETE /(api)/admin/users/[id], owner-only, blocks self-deletion
- Delete button with confirmation on the dashboard Users page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>