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:
co-authored by
Claude Opus 5
parent
1d84003e0a
commit
8807ff41c5
Vendored
+137
-7
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user