Waseel: driver capture, chat/calls, dispatch, and session fixes

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>
This commit is contained in:
Krikorios
2026-08-26 02:17:55 +03:00
co-authored by Claude Opus 5
parent 1d84003e0a
commit 8807ff41c5
111 changed files with 14568 additions and 1411 deletions
+137 -7
View File
@@ -12,6 +12,10 @@ declare interface Driver {
online?: boolean;
latitude?: number | null;
longitude?: number | null;
/** Degrees clockwise from north; null when the device couldn't determine it. */
heading?: number | null;
/** km/h at the last fix; null when unknown, 0 when stationary. */
speed_kph?: number | null;
user_id?: string | null;
car_model?: string | null;
last_seen?: string | null;
@@ -31,6 +35,8 @@ declare interface MarkerData {
service?: string;
online?: boolean;
car_model?: string | null;
heading?: number | null;
speed_kph?: number | null;
time?: number;
price?: string;
}
@@ -43,6 +49,34 @@ declare interface MapProps {
onMapReady?: () => void;
}
/**
* Ride lifecycle (see lib/ride-lifecycle.ts, which owns the server-side sets):
* requested → accepted → arrived → en_route → completed, with cancelled from
* any pre-trip state and expired when nobody offered in time. A request is
* broadcast to every driver nearby and gets its driver at one moment only:
* when the rider picks one of the offers that came back.
*/
declare type RideStatus =
| "requested"
| "accepted"
| "arrived"
| "en_route"
| "completed"
| "cancelled"
| "expired";
/**
* `cash` is a fare still owed to the driver; `cash_collected` is one they've
* confirmed taking at drop-off. Both are "settled" only in the second case —
* the distinction is what makes the cash ledger reconcilable.
*/
declare type PaymentStatus =
/** Requested but not yet assigned — the rider pays when they pick a driver. */
| "pending"
| "paid"
| "cash"
| "cash_collected";
declare interface Ride {
ride_id?: number;
origin_address: string;
@@ -53,15 +87,32 @@ declare interface Ride {
destination_longitude: number;
ride_time: number;
fare_price: number;
payment_status: string;
status: string;
payment_status: PaymentStatus | string;
status: RideStatus | string;
service: string;
driver_id: number | null;
user_id?: string;
payment_order_id?: string | null;
created_at: string;
accepted_at?: string | null;
arrived_at?: string | null;
started_at?: string | null;
completed_at?: string | null;
cancelled_at?: string | null;
cancelled_by?: "rider" | "driver" | "system" | null;
cancellation_reason?: string | null;
cash_collected_at?: string | null;
/** Shown to the rider only while the code is still live (pre-trip). */
pickup_code?: string | null;
/** The rating this rider left, or null if they haven't rated yet. */
my_rating?: number | null;
/** Drivers who have volunteered, while the request is still open. */
offers?: RideOffer[];
/** Server clock at read time — elapsed time is measured against this, not
* the phone's own clock, which can be seconds out either way. */
now?: string;
/** How long a request stays open for offers, in seconds. */
request_ttl_seconds?: number;
driver: {
id: number | null;
first_name: string | null;
@@ -70,21 +121,91 @@ declare interface Ride {
profile_image_url: string | null;
car_image_url: string | null;
rating: number | null;
rating_count?: number | null;
service: string | null;
car_model: string | null;
latitude: number | null;
longitude: number | null;
};
}
/**
* A driver volunteering for an open request, as the rider's screen sees it.
*
* Deliberately carries no coordinates: choosing between drivers needs how far
* away each one is, not where they are, and only the driver who is actually
* picked has a position the rider is entitled to watch.
*/
declare interface RideOffer {
offer_id: number;
driver_id: number;
offered_at: string;
/** Metres from the driver to the pickup, as of when they offered. */
pickup_distance_m: number | null;
first_name: string | null;
last_name: string | null;
profile_image_url: string | null;
car_image_url: string | null;
car_model: string | null;
car_seats: number | null;
rating: number | null;
rating_count: number | null;
service: string | null;
}
// In-app chat message between a rider and their assigned driver, scoped to a
// ride. `sender_id` is the users.id (UUID) or drivers.id (INT) depending on
// `sender_type`; the API stringifies it so the client treats both uniformly.
declare interface Message {
id: number;
ride_id: number;
driver_id: number;
status: string;
offered_at: string;
responded_at: string | null;
ride?: Ride;
sender_type: "rider" | "driver";
sender_id: string;
body: string;
created_at: string;
sender_name?: string;
sender_avatar?: string | null;
}
// The active ride that has an open chat conversation (the other party is
// assigned), returned by GET /(api)/chat/active. `peer` is the other party.
declare interface ChatActiveRide {
ride_id: number;
status: string;
role: "rider" | "driver";
peer: {
name: string;
avatar: string | null;
service?: string | null;
car_model?: string | null;
} | null;
}
// A WebRTC audio call's signaling + lifecycle row, returned by the call
// endpoints. SDP offer/answer are JSON-stringified RTCSessionDescriptions with
// gathered ICE candidates embedded (non-trickle).
declare interface CallRecord {
id: number;
ride_id: number;
caller_type: "rider" | "driver";
status: "ringing" | "answered" | "ended" | "declined" | "missed";
is_caller: boolean;
sdp_offer?: string | null;
sdp_answer?: string | null;
started_at?: string | null;
ended_at?: string | null;
created_at: string;
}
// The client-side call state machine driven by useCall.
declare type CallStatus =
| "idle"
| "outgoing"
| "incoming"
| "connecting"
| "in-call"
| "ended";
declare interface NearbyPlace {
name: string;
address: string;
@@ -105,6 +226,13 @@ declare interface ButtonProps extends TouchableOpacityProps {
iconLeft?: React.ComponentType<any>;
iconRight?: React.ComponentType<any>;
className?: string;
/**
* Touchable to build the button on. Defaults to react-native's, which is
* wrong in exactly one place: inside a @gorhom/bottom-sheet on Android it
* swallows the first press. Screens living in a sheet pass the touchable
* the sheet exports instead.
*/
Touchable?: React.ComponentType<TouchableOpacityProps>;
}
declare interface GoogleInputProps {
@@ -167,6 +295,8 @@ declare interface LocationStore {
longitude: number;
address: string;
}) => void;
/** Forget the destination — a finished trip must not keep drawing a route. */
clearDestination: () => void;
}
declare interface DriverStore {