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
@@ -0,0 +1,89 @@
|
||||
// Who is holding the money, and who still has to hand it over.
|
||||
//
|
||||
// A completed ride splits into a driver payout and a platform fee, but the
|
||||
// split alone doesn't say whether anyone has actually been paid. That depends
|
||||
// on how the rider paid, because it decides who ends up holding the cash:
|
||||
//
|
||||
// Card — the rider pays the platform. The company already has its fee the
|
||||
// moment the card settles, and now OWES THE DRIVER their payout.
|
||||
//
|
||||
// Cash — the driver takes the whole fare at the kerb. They already have
|
||||
// their payout in their pocket, and now OWE THE COMPANY its fee.
|
||||
//
|
||||
// Unpaid — a cash ride the driver couldn't collect. Nobody has been paid and
|
||||
// nothing is owed between them; the fare itself is simply lost.
|
||||
//
|
||||
// So "has the company collected?" has no single answer per ride — it's one
|
||||
// question for card rides and the opposite question for cash ones. This module
|
||||
// is the single place that knows the difference, so the admin ledger, the
|
||||
// driver's balance and the settle endpoint can't drift apart.
|
||||
|
||||
/** Payment states in which a completed ride actually produced money. */
|
||||
export const SETTLED_PAYMENT_STATUSES = ["paid", "cash_collected"] as const;
|
||||
|
||||
export const isPaidRide = (paymentStatus: string): boolean =>
|
||||
(SETTLED_PAYMENT_STATUSES as readonly string[]).includes(paymentStatus);
|
||||
|
||||
/** Which side of a ride's money a settlement action refers to. */
|
||||
export const SETTLEMENT_SIDES = ["platform_fee", "driver_payout"] as const;
|
||||
|
||||
export type SettlementSide = (typeof SETTLEMENT_SIDES)[number];
|
||||
|
||||
export const isSettlementSide = (v: unknown): v is SettlementSide =>
|
||||
typeof v === "string" && (SETTLEMENT_SIDES as readonly string[]).includes(v);
|
||||
|
||||
export type RideMoney = {
|
||||
status: string;
|
||||
payment_status: string;
|
||||
platform_fee_cents: number | null;
|
||||
driver_payout_cents: number | null;
|
||||
platform_fee_settled_at: string | null;
|
||||
driver_payout_settled_at: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a single completed ride still owes, and to whom.
|
||||
*
|
||||
* `companyOwedCents` is money the company is waiting on — a cash ride whose
|
||||
* fee the driver hasn't remitted. `driverOwedCents` is money the company still
|
||||
* has to pay out — a card ride the driver hasn't been paid for. A ride that
|
||||
* never happened, or was never paid for, owes nothing in either direction.
|
||||
*/
|
||||
export const rideBalance = (
|
||||
ride: RideMoney,
|
||||
): { companyOwedCents: number; driverOwedCents: number } => {
|
||||
if (ride.status !== "completed" || !isPaidRide(ride.payment_status)) {
|
||||
return { companyOwedCents: 0, driverOwedCents: 0 };
|
||||
}
|
||||
|
||||
const fee = ride.platform_fee_cents ?? 0;
|
||||
const payout = ride.driver_payout_cents ?? 0;
|
||||
|
||||
return {
|
||||
companyOwedCents: ride.platform_fee_settled_at === null ? fee : 0,
|
||||
driverOwedCents: ride.driver_payout_settled_at === null ? payout : 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The settlement timestamps a ride should be born with, given how it was paid.
|
||||
*
|
||||
* Whoever physically ends up holding their own share is settled the instant
|
||||
* the ride completes — there is no transfer left to make. Only the other side
|
||||
* is left outstanding, and that's the one somebody has to act on.
|
||||
*/
|
||||
export const initialSettlement = (
|
||||
paymentStatus: string,
|
||||
): { platformFeeSettled: boolean; driverPayoutSettled: boolean } => {
|
||||
switch (paymentStatus) {
|
||||
// Company holds the fare: its fee is in hand, the driver is owed.
|
||||
case "paid":
|
||||
return { platformFeeSettled: true, driverPayoutSettled: false };
|
||||
// Driver holds the fare: their payout is in hand, the company is owed.
|
||||
case "cash_collected":
|
||||
return { platformFeeSettled: false, driverPayoutSettled: true };
|
||||
// Nobody was paid; there is nothing to settle between them.
|
||||
default:
|
||||
return { platformFeeSettled: false, driverPayoutSettled: false };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user