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
+322 -3
View File
@@ -136,6 +136,9 @@ export const en = {
recentRides: "Recent Rides",
noRecent: "No recent rides found.",
noRecentAlt: "No recent rides found",
activeRideWithDriver: "{name} is on the way",
rateLastRide: "How was your last ride?",
rate: "Rate",
},
rides: {
@@ -156,7 +159,38 @@ export const en = {
title: "Chat",
messageAlt: "message",
noMessages: "No Messages Yet",
startConversation: "Start a conversation with your friends and family",
startConversation: "Messages open with your driver once a ride is matched.",
inputPlaceholder: "Message…",
send: "Send",
loadError: "Couldn't load messages. Pull to retry.",
sendError: "Couldn't send message. Try again.",
cannotMessage: "This ride is no longer active.",
call: "Call",
},
call: {
incoming: "Incoming call",
outgoing: "Calling…",
connecting: "Connecting…",
inCall: "In call",
ended: "Call ended",
missed: "Missed call",
declined: "Call declined",
failed: "Call failed",
unavailable: "No active ride to call.",
accept: "Accept",
decline: "Decline",
end: "End call",
mute: "Mute",
unmute: "Unmute",
speaker: "Speaker",
speakerOff: "Speaker off",
cancel: "Cancel",
connectingWith: "Connecting with {name}…",
micDeniedTitle: "Microphone blocked",
micDeniedBody:
"Waseel needs microphone access to make calls. Enable it in Settings.",
audioFailed: "Couldn't start audio. Please try again.",
},
profile: {
@@ -170,14 +204,34 @@ export const en = {
emailPlaceholder: "Your Email address",
},
adjustPin: {
pickupLabel: "Pickup point",
destinationLabel: "Drop-off point",
locating: "Finding this place…",
hint: "Drag the map to move the pin exactly where you want it.",
confirmPickup: "Confirm pickup point",
confirmDestination: "Confirm drop-off point",
recenter: "Go to my location",
},
findRide: {
adjustOnMap: "Set it on the map",
title: "Ride",
from: "From",
to: "To",
findNow: "Find now",
service: "Ride type",
nAvailable: "{n} nearby",
estimatedFare: "Estimated fare",
setBothPoints: "Set a pickup and destination",
payLaterHint:
"Drivers nearby will see your request. You choose who takes it, and pay after.",
sending: "Sending your request…",
},
confirmRide: {
noDriversInRadius: "No {service} drivers within {km} km",
tryInstead: "Available near you right now:",
title: "Request Ride",
yourTrip: "Your trip",
pickup: "Pickup",
@@ -190,6 +244,13 @@ export const en = {
lbpEstimate: "≈ {lbp}",
noDrivers: "No {service} drivers online right now",
findingDrivers: "Finding drivers nearby…",
driversNearby: {
one: "1 driver nearby",
other: "{n} drivers nearby",
zero: "No drivers nearby",
},
withinRadius: "Within {km} km of you",
searchingRadius: "Searching within {km} km…",
nearestDriver: "Nearest driver ≈ {eta} min away",
requesting: "Requesting…",
noDriversOnline: "No drivers online",
@@ -204,20 +265,61 @@ export const en = {
"Something went wrong while booking your ride. Please try again.",
alertPayCardTitle: "Pay by card",
alertPayCardBody: "Your card will be charged ${fare}.",
alertInProgressTitle: "Ride already in progress",
alertInProgressBody:
"You have a ride in progress. Finish or cancel it before booking another.",
viewRide: "View ride",
},
bookRide: {
status: {
requested: "Finding your driver…",
choosing: "Choose your driver",
accepted: "Driver assigned — heading to you",
enRoute: "On your trip",
completed: "You've arrived!",
cancelled: "Ride cancelled",
arrived: "Your driver is here",
expired: "No driver available",
},
rideNotFound: "Ride not found.",
couldNotLoad: "Could not load this ride.",
backHome: "Back Home",
matchingDriver: "We're matching you with the nearest {service} driver.",
searchingFor: "Searching for {seconds}s",
match: {
driverFallback: "Your driver",
alertBody: "Something went wrong. Please try again.",
},
offers: {
title: "Drivers available",
count: {
one: "1 offer",
other: "{n} offers",
zero: "No offers yet",
},
away: "{eta} min away · {distance}",
seats: {
one: "1 seat",
other: "{n} seats",
zero: "",
},
pick: "Choose",
goneTitle: "That driver is gone",
goneBody: "They took another ride. Pick someone else from the list.",
goneBodyPaid:
"They took another ride. Your payment hasn't been used — pick someone else and it will go to them.",
},
payment: {
title: "How would you like to pay?",
titleNamed: "Ride with {name}",
subtitle: "Fare ${fare}",
cash: "Pay cash",
cashHint: "Hand the fare to your driver at drop-off.",
card: "Pay by card",
cardHint: "Charged now, before your driver sets off.",
working: "Working…",
},
ratingFallback: "—",
paymentCash: "💵 Cash to driver",
paymentCard: "💳 Paid by card",
@@ -228,10 +330,26 @@ export const en = {
cancelling: "Cancelling…",
alertErrorTitle: "Error",
alertErrorBody: "Could not cancel this ride. Please try again.",
pickupCodeLabel: "Your pickup code",
pickupCodeHint: "Give this to your driver to start the trip.",
driverHere: "Your driver is outside",
cashDue: "Pay ${amount} in cash to your driver.",
youRated: "You rated this ride {n}★",
rateDriver: "Rate your driver",
noDriversFound:
"No driver picked up your request. Nothing was charged — try again in a moment.",
cancelledByDriver: "Your driver cancelled this ride.",
enRouteNotice:
"Enjoy your ride — your driver will end the trip on arrival.",
},
driver: {
home: {
owesCompany: "Commission you owe",
owesCompanyHint: "Your cash rides — hand this in at the office.",
owedToDriver: "The company owes you",
owedToDriverHint: "Card rides, paid out to you.",
afterFee: "after ${fee} platform fee",
signOutAlt: "Sign out",
driverMode: "Driver mode",
online: "● Online — receiving ride requests",
@@ -240,6 +358,7 @@ export const en = {
completedToday: "Completed today",
incomingRequests: "Incoming requests",
incomingRequestsOffline: "Incoming requests (offline)",
finishCurrentRide: "Finish your current ride to see new requests.",
waitingRequests: "Waiting for ride requests…",
goOnlineStart: "Go online to start driving.",
welcome: "Welcome, {name}",
@@ -260,36 +379,223 @@ export const en = {
"Could not create your driver profile. Please try again.",
alertToggleBody: "Could not change your status. Please try again.",
noRequestsAlt: "No recent rides found",
cashInHand: "Cash collected today",
uncollected: "Uncollected fares today",
ratingCount: "{n} ratings",
ratingNew: "New driver",
alertOfflineBlocked:
"Finish or cancel your current ride before going offline.",
},
credentials: {
title: "Your credentials",
intro:
"We check every driver before they can take a ride. These details are reviewed by our team.",
licenseNumber: "Driving licence number",
licenseNumberPlaceholder: "As printed on your licence",
licenseExpiry: "Licence expiry",
nationalId: "National ID number",
nationalIdPlaceholder: "Your ID card number",
plateNumber: "Plate number",
plateNumberPlaceholder: "e.g. 123456/B",
reviewNote:
"Your account stays offline until a reviewer approves it. This usually takes less than a day.",
submit: "Submit for review",
errorTitle: "Check your details",
errorMissing:
"Licence number, national ID and plate number are all required.",
errorExpiryFormat: "Enter the licence expiry as YYYY-MM-DD.",
errorExpired: "That licence has already expired.",
errorScanRequired: "Scan your driving licence before submitting.",
alertResubmitBody: "Could not resubmit your details. Please try again.",
},
captureUnavailable:
"Photo capture isn't available in this version of the app. Please update to the latest version and try again.",
photo: {
title: "Your photo",
hint: "Taken now with the camera, not from your gallery. Riders see it next to your name when they pick a driver, and use it to check it's you at pickup. Face the camera in good light.",
take: "Take photo",
retake: "Retake",
required: "Take a profile photo before submitting.",
permissionTitle: "Permission needed",
permissionCamera:
"Waseel needs the camera to take your photo. Allow camera access to continue.",
permissionCameraBlocked:
"Camera access is turned off for Waseel, and Android won't ask again from here. Open Settings, then turn on Camera under Permissions.",
errorTitle: "Couldn't save that photo",
errorBody: "Something went wrong. Please try again.",
errorTooLarge: "That photo is too large. Try taking a new one.",
errorRateLimit: "Too many uploads. Wait a few minutes and try again.",
errorUnsupported: "Use a JPEG, PNG or WebP photo.",
},
scan: {
licenseLabel: "Driving licence",
licenseHint:
"Lay it flat and fill the frame. We read the number and expiry off it.",
idLabel: "ID card",
idHint: "The side showing your ID number.",
vehicle_regLabel: "Vehicle registration",
vehicle_regHint: "The page showing the plate number and the car model.",
optional: "Optional",
take: "Take photo",
retake: "Rescan",
choose: "Choose photo",
reading: "Reading your document…",
filled: {
one: "Filled in 1 detail — check it below.",
other: "Filled in {n} details — check them below.",
},
savedNoFields:
"Photo saved, but we couldn't read the details. Type them in below.",
savedUnreadable:
"Photo saved. Scanning is unavailable right now — type the details in below.",
alreadyOnFile: "A scan is already on file. Rescan only if you need to.",
allRead: "Read from your documents",
missingPrompt:
"We couldn't read these off your documents. Please add them and you're done.",
edit: "Check or edit details",
done: "Done",
checkPrompt: "Correct anything that was read wrongly, then tap Done.",
permissionTitle: "Permission needed",
permissionCamera:
"Allow camera access to photograph your documents, or choose a photo instead.",
permissionCameraBlocked:
"Camera access is turned off for Waseel, and Android won't ask again from here. Open Settings and turn on Camera under Permissions — or choose an existing photo instead.",
permissionLibrary:
"Allow photo access to pick a picture of your documents.",
permissionLibraryBlocked:
"Photo access is turned off for Waseel, and Android won't ask again from here. Open Settings and turn on Photos under Permissions — or take a photo with the camera instead.",
errorTitle: "Couldn't scan that",
errorBody:
"Something went wrong. Try again, or type the details in below.",
errorTooLarge: "That photo is too large. Try taking a new one.",
errorRateLimit: "Too many scans. Wait a few minutes and try again.",
errorUnsupported: "Use a JPEG, PNG or WebP photo.",
errorRetry: "Not uploaded. Try again, or type the details in below.",
},
review: {
pendingTitle: "Under review",
pendingBody:
"We're checking your details. You'll be able to go online as soon as you're approved.",
rejectedTitle: "Not approved",
rejectedBody:
"Your details didn't pass review. Correct them below and submit again.",
suspendedTitle: "Account suspended",
suspendedBody:
"Your driver account has been suspended. Contact support to sort this out.",
approvedTitle: "Approved",
approvedBody: "You're cleared to drive.",
reasonLabel: "Reason",
checkAgain: "Check again",
resubmitTitle: "Correct your details",
resubmitIntro: "Fix what's wrong and we'll review it again.",
resubmit: "Submit again",
},
offerCard: {
youEarn: "You earn",
newRequest: "New request · {service}",
openFor: "On the board for",
seconds: "{n}s",
awayFromPickup: "{km} km from pickup",
firstIn: "You'd be first",
rivals: {
one: "1 other driver offered",
other: "{n} other drivers offered",
zero: "You'd be first",
},
cash: "💵 Cash",
card: "💳 Card",
fromAlt: "From",
toAlt: "To",
tripTime: "Trip time",
fare: "Fare",
decline: "Decline",
accept: "Accept",
offer: "Offer this ride",
withdraw: "Withdraw my offer",
waitingOnRider: "Offered — waiting for the rider to choose",
lostTitle: "Request closed",
lostBody:
"The rider went with another driver, or the request timed out. You're free for the next one.",
alertOfferBody: "Could not send your offer. Please try again.",
alertWithdrawBody: "Could not withdraw your offer. Please try again.",
},
activeRide: {
youEarn: "You earn",
headToPickup: "Head to pickup",
tripInProgress: "Trip in progress",
rider: "{name}",
pickupPin: "Rider pickup",
dropoffPin: "Drop-off",
navigateToPickup: "Navigate to pickup",
navigateToDropoff: "Navigate to drop-off",
alertNavigateBody: "Could not open a navigation app on this phone.",
fromAlt: "From",
toAlt: "To",
fare: "Fare",
message: "Message",
call: "Call",
startTrip: "Start trip",
completeTrip: "Complete trip",
cancelRide: "Cancel ride",
cancelConfirmTitle: "Cancel this ride?",
cancelConfirmBody:
"The rider will be notified and the ride will be marked as cancelled.",
cancelConfirmDismiss: "Keep ride",
cancelConfirmConfirm: "Cancel ride",
alertErrorTitle: "Error",
alertAcceptBody:
"Could not accept this ride. It may have been taken or expired.",
alertDeclineBody: "Could not decline this ride. Please try again.",
alertUpdateBody: "Could not update the ride. Please try again.",
alertCancelBody: "Could not cancel the ride. Please try again.",
imHere: "I've arrived",
atPickup: "At pickup — waiting for rider",
askForCode: "Ask the rider for their 4-digit pickup code.",
cashConfirmTitle: "Collect the fare",
cashConfirmBody: "Did you collect ${amount} in cash from the rider?",
cashCollected: "Yes, collected",
cashNotCollected: "Not collected",
},
},
rating: {
rateDriverTitle: "How was your ride with {name}?",
rateRiderTitle: "How was {name} as a passenger?",
subtitle: "Your rating stays private to the other person.",
starLabel: "{n} stars",
commentPlaceholder: "Add a comment (optional)",
submit: "Submit rating",
notNow: "Not now",
error: "Could not submit your rating. Please try again.",
},
cancelSheet: {
title: "Cancel this ride?",
subtitleRider: "Tell us why so we can improve matching.",
subtitleDriver: "The rider will be notified and the ride marked cancelled.",
confirm: "Cancel ride",
keepRide: "Keep ride",
cancelling: "Cancelling…",
reasons: {
changed_mind: "I changed my mind",
wait_too_long: "The wait is too long",
wrong_address: "Wrong pickup address",
driver_no_show: "The driver never arrived",
rider_no_show: "The rider never showed up",
unreachable: "I couldn't reach them",
vehicle_issue: "Vehicle problem",
other: "Another reason",
},
},
pickupCode: {
title: "Start the trip",
subtitle: "Enter the 4-digit code from the rider's screen.",
startTrip: "Start trip",
wrongCode: "That code doesn't match. Check with the rider.",
},
services: {
nearbyCount: "{n} nearby",
noneNearby: "None nearby",
car: { label: "Car", tagline: "An everyday ride, up to 4 seats." },
moto: {
label: "Moto",
@@ -385,6 +691,16 @@ export const en = {
"Map is not available on web.\nRun on Android/iOS for the full experience.",
},
rideCard: {
outcomeCompleted: "Completed",
outcomeCancelled: "Cancelled",
outcomeExpired: "No driver found",
cancelledByYou: "You cancelled",
cancelledByDriver: "Driver cancelled",
cancelledBySystem: "No driver available",
noDriver: "No driver assigned",
paymentNotCharged: "Not charged",
paymentRefundDue: "Refund due",
paymentCashCollected: "Cash paid",
mapAlt: "Map",
originAlt: "Origin",
destinationAlt: "Destination",
@@ -485,6 +801,9 @@ export const en = {
rtlRestartBody:
"Arabic layout will apply fully the next time you open the app.",
},
general: {
title: "General",
},
keepAwake: {
title: "Do not lock screen",
description: "Keep the display on while the app is open.",