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>
315 lines
8.7 KiB
TypeScript
315 lines
8.7 KiB
TypeScript
import { TextInputProps, TouchableOpacityProps } from "react-native";
|
|
|
|
declare interface Driver {
|
|
id: number;
|
|
first_name: string;
|
|
last_name: string;
|
|
profile_image_url: string;
|
|
car_image_url: string;
|
|
car_seats: number;
|
|
rating: number;
|
|
service: string;
|
|
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;
|
|
}
|
|
|
|
declare interface MarkerData {
|
|
latitude: number;
|
|
longitude: number;
|
|
id: number;
|
|
title?: string;
|
|
profile_image_url: string;
|
|
car_image_url: string;
|
|
car_seats: number;
|
|
rating: number;
|
|
first_name: string;
|
|
last_name: string;
|
|
service?: string;
|
|
online?: boolean;
|
|
car_model?: string | null;
|
|
heading?: number | null;
|
|
speed_kph?: number | null;
|
|
time?: number;
|
|
price?: string;
|
|
}
|
|
|
|
declare interface MapProps {
|
|
destinationLatitude?: number;
|
|
destinationLongitude?: number;
|
|
onDriverTimesCalculated?: (driversWithTimes: MarkerData[]) => void;
|
|
selectedDriver?: number | null;
|
|
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;
|
|
destination_address: string;
|
|
origin_latitude: number;
|
|
origin_longitude: number;
|
|
destination_latitude: number;
|
|
destination_longitude: number;
|
|
ride_time: number;
|
|
fare_price: number;
|
|
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;
|
|
last_name: string | null;
|
|
car_seats: number | null;
|
|
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;
|
|
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;
|
|
latitude: number;
|
|
longitude: number;
|
|
/** Straight-line distance from the rider. */
|
|
distanceMeters?: number;
|
|
/** Distance and time along the actual driving route, when one was found. */
|
|
routeDistanceMeters?: number;
|
|
routeDurationSeconds?: number;
|
|
category?: string;
|
|
}
|
|
|
|
declare interface ButtonProps extends TouchableOpacityProps {
|
|
title: string;
|
|
bgVariant?: "primary" | "secondary" | "danger" | "outline" | "success";
|
|
textVariant?: "primary" | "default" | "secondary" | "danger" | "success";
|
|
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 {
|
|
icon?: string;
|
|
initialLocation?: string;
|
|
containerStyles?: string;
|
|
textInputBackgroundColor?: string;
|
|
handlePress: ({
|
|
latitude,
|
|
longitude,
|
|
address,
|
|
}: {
|
|
latitude: number;
|
|
longitude: number;
|
|
address: string;
|
|
}) => void;
|
|
}
|
|
|
|
declare interface InputFieldProps extends TextInputProps {
|
|
label: string;
|
|
icon?: any;
|
|
secureTextEntry?: boolean;
|
|
labelStyles?: string;
|
|
containerStyles?: string;
|
|
inputStyles?: string;
|
|
iconStyles?: string;
|
|
className?: string;
|
|
}
|
|
|
|
declare interface PaymentProps {
|
|
fullName: string;
|
|
email: string;
|
|
amount: string;
|
|
driverId: number;
|
|
rideTime: number;
|
|
}
|
|
|
|
declare interface LocationStore {
|
|
userLatitude: number | null;
|
|
userLongitude: number | null;
|
|
userAddress: string | null;
|
|
destinationLatitude: number | null;
|
|
destinationLongitude: number | null;
|
|
destinationAddress: string | null;
|
|
setUserLocation: ({
|
|
latitude,
|
|
longitude,
|
|
address,
|
|
}: {
|
|
latitude: number;
|
|
longitude: number;
|
|
address: string;
|
|
}) => void;
|
|
setDestinationLocation: ({
|
|
latitude,
|
|
longitude,
|
|
address,
|
|
}: {
|
|
latitude: number;
|
|
longitude: number;
|
|
address: string;
|
|
}) => void;
|
|
/** Forget the destination — a finished trip must not keep drawing a route. */
|
|
clearDestination: () => void;
|
|
}
|
|
|
|
declare interface DriverStore {
|
|
drivers: MarkerData[];
|
|
selectedDriver: number | null;
|
|
setSelectedDriver: (driverId: number) => void;
|
|
setDrivers: (drivers: MarkerData[]) => void;
|
|
clearSelectedDriver: () => void;
|
|
}
|
|
|
|
declare interface DriverCardProps {
|
|
item: MarkerData;
|
|
selected: number;
|
|
setSelected: () => void;
|
|
}
|