RideKit
Docs/Reference/Polish audit — how RideKit feels

Polish audit — how RideKit feels

Scope: rider app, driver app, admin console. Evidence is source code plus the 164 screenshots from the end-to-end pass (<scratchpad>/e2e/). Nothing here is a guess about behaviour I could not read or see.

Date: 2026-09-05


The one-minute version

The product is functionally further along than it looks, and the polish is unevenly distributed. A handful of moments are genuinely well made — the OTP screen, the toast system, the driver's ringing offer alert, the driver's "you're online" pulse, and most of the admin console. Those should be left alone.

But the polish stops at the edges of those few moments. Three patterns explain almost every complaint:

  1. The rider's side of the trip got less love than the driver's side. The driver has a pulsing online state, a looping ring, a countdown bar and a route line on the map. The rider — at the two moments that decide whether they cancel or stay — gets an ActivityIndicator and a car that teleports across a map with no route drawn on it. The asymmetry is not a design decision; in one case the route data is already fetched on the rider's screen and thrown away.

  2. Emoji are the icon system. 306 emoji glyphs across the two apps, and neither declares an icon library. The rider's tab bar is 🏠 📅 👛 ⚙️. This has already caused a shipped bug (the "Go offline" tofu box). It is the largest single reason the apps read as a template rather than a product — and the fix needs no new dependency, because @expo/vector-icons is already sitting in both apps' node_modules via Expo.

  3. The "in-between" states were never designed. Zero skeleton loaders in either app (105 bare spinners). Zero list/layout transitions. Pull-to-refresh on 6 screens out of ~50. No connectivity awareness anywhere — a dropped connection just freezes the map while the sheet keeps saying "Driver on the way". The admin console, by contrast, has a proper shimmer skeleton on nearly every page and carefully distinguishes "nobody is waiting" from "we cannot see who is waiting". The apps should borrow from the admin, not the other way round.

Do these five first (detail below): the dead "Finding your driver" screen · the missing route line on the rider's map · the teleporting car marker · replace the emoji icon set · make a backgrounded driver's phone actually ring.

None of the five is architectural. All five are visible in the first 90 seconds of a demo.


First: what is already good (do not rebuild these)

Padding a list helps nobody. These are done, and done well.

Moment Why it's good Where
OTP entry Auto-advances per digit, auto-submits on the 6th, handles paste across boxes, and on a wrong code: shakes the row, flashes the boxes red, buzzes with an error haptic, announces to the screen reader, clears and refocuses. autoComplete="sms-otp" + textContentType="oneTimeCode" for OS autofill. mobile/rider/app/(auth)/otp.tsx:54-107, same in driver
Toasts Reanimated slide-up with a deliberate overshoot, per-kind haptic (error / success / light), announceForAccessibility, errors linger longer. Replaced Alert.alert app-wide — only 5 rider / 6 driver alerts remain, all genuinely destructive confirms. mobile/rider/src/components/Toast.tsx:74-95
Driver offer alert Rings offer.wav on a loop every 1.6s with a repeating haptic until accept / decline / expire, and plays through silent mode. Degrades to haptic-only if audio is unavailable. This is the right design. mobile/driver/src/lib/sound.ts
Driver "You're online" An expanding pulse ring (scale 1 → 2.4 with a fade) plus a breathing dot, and the toggle springs on press. The online state is alive. mobile/driver/app/(app)/home.tsx:85-110, 268 · screenshot d27-online.png
Rider status ding playDing() fires on every real ride-status change, on parcel status changes, and on a new bid. Not silent. mobile/rider/app/(app)/tracking.tsx:190
Primary CTA Gradient fill, brand glow shadow, press scale to 0.97, integrated loading spinner, full a11y state. mobile/rider/src/components/ui.tsx:129-158 · screenshot r13-quote.png
Admin loading + liveness A real .skeleton shimmer used on ~all 35 dashboard pages; socket-backed dispatch driver feed; 5s dashboard refresh with an "updated 4s ago" freshness stamp. admin/src/app/globals.css:352-374, admin/src/lib/useDispatchFeed.ts · screenshot a04-after-login.png
Admin stale-vs-empty Dispatch explicitly refuses to render a failed poll as an empty queue — "'nobody is waiting' and 'we cannot see who is waiting' are opposite instructions to an operator" — and keeps the last good rows while shouting that they're stale. This is better reasoning than most production consoles have. admin/src/app/(console)/dispatch/page.tsx:90-140
Welcome screen Full-bleed photography, gradient scrim, confident type, language switcher in the corner. Genuinely premium. screenshot r01-launch.png
Trip resume Both apps return to the live ride at the correct stage after a hard kill and after a device reboot. Foreground-service notification during trips. verified on device in the e2e pass

Do these five first

1. "Finding your driver" is a dead screen — and it's the highest-anxiety moment in the product

Todayr14-booked.png. An almost-empty map showing one small teal dot, a tiny ActivityIndicator, the words "Finding your driver…", four flat grey progress segments, and "Cancel ride". Nothing moves. Nothing counts. Nothing suggests the request is alive or that drivers exist. The map is not even fitted to show the pickup and destination together. mobile/rider/app/(app)/tracking.tsx:436-441, 519-527

Should be — the wait must look like work is happening: a pulsing radar ring expanding from the pickup pin (the driver app already has exactly this animation — home.tsx:100-107 — it just was never given to the rider), nearby car markers, an elapsed or expected-wait counter, and honest narration that changes ("Asking drivers nearby…" → "Still looking — drivers in your area are busy"). If the search is going to fail, say so before it fails.

Why it matters — this is where riders cancel. A rider staring at a frozen screen for 40 seconds assumes the app is broken and opens a competitor. It is also the moment a buyer evaluating the demo forms their opinion of "does this feel real".

Effort: M (2–3 days). The pulse animation is already written in the driver app and can be lifted directly.


2. The rider's live map has no route line — and the data is already there

Todaytracking.tsx imports Polyline on line 3 and never renders it. The rider watches a car dot drift across bare streets with no indication of the path it's taking or where it's going. Confirmed visually in v16-rider-TRACKING-FIX.png and r23-tracking-moving.png.

The screen already calls geoApi.route(...) at line 322 — it uses the result only to extract a number of minutes and discards the geometry.

Both other maps in the product draw the route properly, with a straight-line fallback if the routing call fails:

  • mobile/rider/app/(app)/confirm.tsx:313{route && <Polyline coordinates={route} .../>}
  • mobile/driver/app/(app)/trip.tsx:396 — same, with .catch(() => setRoute([from, target]))

Should be — keep the geometry from the call already being made and render it, with the same straight-line fallback the driver screen uses. Also fit the camera to include both the car and the pickup rather than centring on the car alone.

Why it matters — this is the cheapest large win in the document. The rider's trip screen is the screen they stare at longest, and it is currently the only map in the product missing the thing that makes a map useful.

Effort: S (half a day).


3. The car marker teleports

Today — every driver:location socket event does setDriverPos(pos) and the marker re-renders at the new coordinate. The camera animates smoothly (animateToRegion(..., 600)), but the marker itself snaps. There is also no bearing, so the car icon never faces the direction of travel. mobile/rider/app/(app)/tracking.tsx:201-208, 364-370

Should be — interpolate the marker between successive positions over the update interval (react-native-maps supports AnimatedRegion + animateMarkerToCoordinate, or animate a plain shared value and drive the coordinate from it), and rotate the marker to the computed bearing. Same treatment on the driver's own marker.

Why it matters — a smoothly gliding, correctly oriented car is the signature detail of a ride-hailing app. A snapping dot is the detail people point at when they say "it feels cheap".

Effort: M (2 days including the bearing maths and the driver-side marker).


4. Emoji are the icon system — 306 of them, and no icon library is installed

Todaygrep counts 198 emoji glyphs in the rider source and 108 in the driver source. Neither package.json declares an icon package — no react-native-svg, no lucide, nothing. So:

  • the rider tab bar is 🏠 📅 👛 ⚙️ (r05-home.png)
  • the driver's Account menu is 📄 💰 🎯 🔥 🏁 👛 👤 📄 🌐 ⚙️ ❓ (d37-earnings.png)
  • empty and error states are ⚠️ and 🧾 at fontSize: 40 (history.tsx:93-101)
  • the trip-complete success mark is a ✅ emoji in a tinted circle (r24-completed.png)
  • the map's driver marker is 🚗 inside a coloured circle (tracking.tsx:367)

This has already shipped a bug: the driver's "Go offline" button rendered as a tofu box because U+23FB has no glyph on Android (e2e findings #4). That is not bad luck — it is the predictable failure mode of relying on the platform's font for iconography. Emoji also render differently on every Android vendor and OS version, are multicolour so they fight the brand palette, and cannot be recoloured for dark mode.

There is a second, related inconsistency: the rider home shows photo-realistic vehicle cut-outs in the SERVICES row (r05-home.png) while the ride-picker two taps later shows emoji vehicles 🚗 🚙 🚐 (r13-quote.png). Two different visual languages for the same objects, in one flow.

Should be — one consistent line-icon set, themable, monochrome, sized on a scale. Keep emoji only where an emoji is genuinely the right character (a flag in a country picker). Pick one vehicle-art system and use it everywhere.

The good news: no new dependency is needed. @expo/vector-icons is already present in both apps' node_modules as a transitive dependency of Expo — it ships with the SDK. Ionicons, Feather and MaterialCommunityIcons are importable today. The work is a mechanical swap, not an integration.

Why it matters — this is the highest-leverage visual change available. It is what separates "a template someone bought" from "a product someone built", and it is the first thing a buyer's designer will comment on.

Effort: M–L (roughly a week across both apps, all of it mechanical). Splitable: tab bar + menus + map markers first (about 2 days) captures most of the perceived gain.


5. A backgrounded driver's phone does not ring — it dings once

Today — the looping offer.wav ring only runs from home.tsx while the app is foregrounded. When the driver is backgrounded, the offer arrives as a push notification whose Android channel is configured sound: 'default' — the generic system tone, once. mobile/driver/src/lib/push.ts:42-48

There is no notification action pair (Accept / Decline on the notification itself), and no full-screen intent, so a driver whose phone is in their pocket gets one anonymous chirp indistinguishable from a WhatsApp message, for an offer that expires in 15 seconds.

Should be — ship offer.wav as the channel's custom sound (Android notification channels support a bundled raw resource; iOS supports a custom .caf), keep AndroidImportance.MAX, add Accept/Decline notification actions, and consider a full-screen intent for the offer so the phone behaves like an incoming call. The vibration pattern is already correct ([0, 300, 200, 300]).

Why it matters — a missed offer is directly lost revenue for the driver and a longer wait for the rider. Drivers do not sit staring at the app; this is the single most consequential unreliability in the product.

Effort: M (2–3 days; custom channel sounds need a native config plugin and a rebuild, and the channel must be recreated because Android channel settings are immutable after creation).


The full findings, by moment

App launch and splash

No splash-screen control at all. Neither app depends on expo-splash-screen. RootLayout does if (!loaded) return null while nine Google Font faces load (_layout.tsx:82), then index.tsx renders a bare centred ActivityIndicator on the canvas while it awaits a network round trip (ridesApi.active()) to decide where to land.

The cold-start sequence is therefore: native splash → blank frame → spinner on an empty screen → content. On a slow connection the third stage is open-ended, and if the call fails it silently routes home (a rider mid-trip with bad signal lands on the home screen instead of their ride).

Should be: add expo-splash-screen, preventAutoHideAsync() at module scope, and hideAsync() only once fonts and the landing decision are ready — so the branded splash covers the whole gap. Give ridesApi.active() a short timeout and a retry, and never route a logged-in user to home on a transport error. Effort: S. mobile/rider/app/_layout.tsx:68-82, mobile/rider/app/index.tsx


OTP entry

Covered above under "already good", with one gap:

"Resend code" has no cooldown. It is a live link from the first frame, with no "Resend in 0:29" countdown and no disabled state (otp.tsx:220-229, visible in r03-otp.png). A rider who doesn't get the SMS taps it repeatedly, each tap costing an SMS and none of them telling them to wait. Should be: a 30–60s countdown, disabled until it elapses, with the remaining seconds in the label. Effort: S (an hour).


The rider's home screen

The map behind the home screen is empty. home.tsx:98-110 renders a MapView with showsUserLocation and no markers of any kind. A rider opening the app for the first time sees their own blue dot on empty streets (r05-home.png). Every competitor scatters nearby car icons here, because it is the cheapest possible proof that the service exists. Should be: plot nearby available drivers (approximate, jittered — never a real driver's exact position). The backend already tracks last_location per driver and already has a demand-heatmap endpoint to build on.

⚠️ This one must be market-gated. French law (code des transports Art L.3120-2 III 1°, upheld by the Conseil constitutionnel) bans informing a customer, before booking, of both the location and the availability of a vehicle on a public road. Showing only availability, or only location, or the expected wait time, remains lawful. RideKit is compliant today purely because this feature does not exist — so build it behind a per-market flag, with a wait-time-only variant for France. See docs/country-gaps.md FR-1.

Effort: M (a nearby-drivers endpoint with deliberate coordinate fuzzing, plus the market gate).


Choosing a ride / the fare quote

The screen itself is good — route drawn, three tiers, a "FASTEST" badge, a fixed-fare reassurance line, Now/Schedule, payment method, promo (r13-quote.png). Two problems:

All three tiers show the same "14 min · 6.0 km". That is the trip duration and distance — identical by definition across tiers. Riders read the leading number as "how long until my car arrives", so the screen simultaneously answers the wrong question and looks broken (three rows, one number). The pickup ETA — the number that actually differs per tier and that riders decide on — is not shown anywhere. Should be: lead each row with the pickup ETA ("3 min away"), and put trip duration/distance in the secondary line. Effort: M (needs a per-tier nearest-driver ETA).

No surge or price-explanation surface. There is no indicator when a fare is elevated and no "why this price" breakdown before booking — only a receipt afterwards. Several markets in the preset list cap or regulate surge, so this is also a compliance-adjacent surface. Effort: M.


The moment a driver is found

Today — the ding plays (good), and then the sheet's contents swap instantly: the "Finding your driver" block is replaced by the driver card, and the progress bar jumps from 0 to 1 segment. No transition, no emphasis, no map re-fit to show the newly-appeared car alongside the pickup. v16-rider-TRACKING-FIX.png shows the result — a correct but flat "Driver on the way".

Should be — this is the product's single best emotional beat and it currently passes unmarked. The driver card should animate in, the progress bar should fill rather than jump, the map should ease out to frame car + pickup, and the ETA chip should be prominent. A success haptic alongside the ding.

Why it matters — relief is the emotion to design for here. A rider who feels the moment their driver was found is a rider who stops watching the screen anxiously.

Effort: M.

Related: the ETA chip only renders when status === 'accepted' and the route call has returned (tracking.tsx:317-327); on failure the code deliberately keeps the last value and logs. In v16 no ETA is present at all — so at the moment captured, the rider had no idea how far away their driver was. There is no fallback (not even a crow-flies estimate) and no placeholder. Effort: S.


Arriving, trip start, trip end (driver side)

The driver's most consequential buttons confirm with nothing. arrivestartcomplete are plain API calls with a busy state and no haptic, no sound, no animation (trip.tsx:331-350). The driver app has haptics available and a sound module already wired for offers; neither is used here. Should be: a success haptic on each stage advance, and a distinct short tone on trip completion. Effort: S (an hour).

The driver's trip sheet has no distance or time remaining. v18-ontrip.png shows "On trip" + the destination address + the rider card + "Complete trip". A driver mid-trip cannot see how far they have left or what the meter is doing. (Rentals do get a live meter panel — city trips get nothing.) Effort: M.

Observed once: the route line was absent on the driver's trip map (v18-ontrip.png — driver arrow, drop-off dot, no line), even though trip.tsx:203-205 sets a route with a straight-line fallback on failure. Worth reproducing: it suggests the effect did not re-run for that phase transition. Flagged as an observation, not a confirmed defect. Effort: S to investigate.


The fare reveal, rating and tip

r24-completed.png — the layout is sound (success mark, fare, payment method, driver avatar, stars, tip presets, comment, submit, skip). The feel is flat:

  • The fare appears instantly, with no count-up and no emphasis. This is the number the whole trip was about.
  • The success mark is a ✅ emoji. It reads as clip-art. A drawn checkmark that strokes on in ~400ms, with a success haptic, costs almost nothing.
  • The stars are emoji ⭐ at opacity: 0.25 unselected, 1 selected (rate.tsx:87). No fill animation, no haptic per star, no label per rating ("Terrible" → "Great"), and at 25% opacity they read as disabled rather than as an invitation.
  • No haptic on tip selection.

Should be: animate the fare in, draw the checkmark, give the stars a real component with per-star haptic, a scale bounce and a rating label. Effort: S–M (1–2 days for all of it).

Known, and functional rather than cosmetic: the tip is recorded on ride_ratings.tip_minor but never posted to the ledger, so the driver's wallet and earnings never see it (e2e findings #7). Flagged here only so it is not mistaken for a polish item — it is a money bug.


The driver's offer card

d28-offer.png. The mechanics are right: a Modal with a native slide, an animated countdown bar, the ringing loop, a live region for screen readers, and loud full-bleed banners for parcel/rental/intercity so a driver is never surprised by the kind of job. Gaps:

No distance or ETA to the pickup, and no trip distance. The fare is the only number on the card. A driver's first question is "how far do I have to drive to get there" and the card cannot answer it. This is the most commercially significant omission in the driver app after the push sound. Effort: S if dispatch already computes the driver→pickup distance for ranking (likely); M if not.

The countdown bar never changes colour. It runs brand-teal from full to zero (OfferSheet.tsx:154, 200, 209-211). Colour is the fastest channel a human has; with 3 seconds left the bar should be red. Effort: S (an hour).

The countdown animates with useNativeDriver: false, so the width animation runs on the JS thread — the same thread handling socket traffic and the map. It will stutter exactly when the app is busiest. Animating scaleX with the native driver fixes it. Effort: S.


Going online / offline

Already good (see above). One note: d27-online.png shows a garbled glyph box on the "Go offline" label — that is the pre-fix build of e2e findings #4, since corrected. Retained here only as evidence for why the emoji-as-icons finding matters.


Earnings updating

The number changes silently. The driver home earnings strip reloads via useFocusEffect on return from a trip (home.tsx:128-149), so ₹0.00 becomes ₹106.39 between one frame and the next, with no count-up and no highlight. The one moment in the driver's day that is pure reward passes without acknowledgement. Should be: animate the value up and briefly highlight the strip when it increases. Effort: S.


Empty states

They exist and they are structurally correct — icon, title, sub-line, and a retry action where one applies (history.tsx:91-104). Two issues:

  • They are all emoji-illustrated and identically shaped, so every empty screen in the app looks like the same screen.
  • One promises an action it does not offer. "No drivers available" reads "No drivers are free right now. Try again or pick another ride type" — and then presents only Try again and Back to home (r16-nodrivers.png, tracking.tsx:537-552). Either add a ride-type switcher, or fix the copy. Also worth offering: "notify me when a driver is free". Effort: S for the copy/action mismatch; M for a proper illustrated empty-state component.

The admin's empty states are better — see "No activity in the last 30 days / Trips appear here as riders request them" in a04-after-login.png. Same standard should apply in the apps.


The rider's Settings screen is a near-empty shell

Todaymobile/rider/app/(app)/settings.tsx is 90 lines and contains exactly two things: Language and Appearance. The driver's equivalent is 465 lines — auto-accept, destination mode, trip filters (max pickup distance, minimum fare, long trips), service types (parcel, intercity), plus language and appearance.

Should be — the rider's Settings is where a user goes looking for notification preferences, a default payment method, privacy controls, support, and the legal documents. Right now none of that exists anywhere in the app. This is also where account deletion and the Terms / Privacy Policy links have to live (both are app-store requirements and both are currently missing — see docs/country-gaps.md U1 and U2).

Why it matters — a settings screen with two rows reads as unfinished, and it is the screen a reviewer opens when checking store-policy compliance.

Effort: S for the legal links and notification toggles; M once account deletion is included.


Loading states

There is not one skeleton loader in either mobile app. 105 ActivityIndicator instances (60 rider, 45 driver) and zero shimmer placeholders. Every list, every detail screen, every wallet resolves through a bare spinner on an empty background.

The admin already solved this: a proper .skeleton shimmer with a token-driven sweep colour that adapts to light/dark/ops themes (admin/src/app/globals.css:352-374), plus Skeleton and SkeletonRows primitives used across essentially every dashboard page.

Should be: port the same idea to the apps — a <Skeleton> primitive plus per-screen shapes for the history list, wallet, earnings and trip detail. Perceived speed improves even when actual speed does not. Effort: M (3–4 days for a primitive plus the ten screens that matter).


Pull-to-refresh

Present on 6 screens out of roughly 50:

  • rider: history, scheduled, business, parcels
  • driver: earnings, quests

Missing where users will absolutely try it: rider wallet, driver wallet, driver history, rider trip detail, driver documents, driver profile. A wallet balance that can change from an external top-up is the canonical pull-to-refresh surface, and pulling there does nothing. Effort: S (a day for all of them).


List transitions and press feedback

No list or layout transitions anywhere. Zero uses of LayoutAnimation or reanimated's entering / layout props in either app. Rows, sheets, banners and status blocks appear and disappear instantaneously. Effort: M.

Most tappable things do not respond to touch. Out of 181 rider and 119 driver Pressables:

  • android_ripple: 0 in either app
  • a ({ pressed }) style function: 17 rider, 4 driver

The shared Button is fine (press scale 0.97 + glow). But icon buttons, list rows, tip chips, star buttons, tag chips, service tiles, the location pill, menu rows and map controls are all completely inert to the touch. On Android especially, a control with no ripple reads as broken.

Should be: one shared Touchable wrapper applying ripple on Android, opacity/scale on iOS, plus an optional light haptic — then swap it in. It is a mechanical change with a disproportionate effect on perceived quality. Effort: M (a day for the wrapper, 2–3 days to migrate).

No haptic on any primary CTA. Haptics exist only in Toast and the OTP screen. "Book ride", "Accept", "Go online", "Complete trip", star taps and tip taps all buzz nothing. Effort: S once the wrapper above exists.


Slow or dropped connection

Neither app has any connectivity awareness. No NetInfo, no offline banner, no queued-action handling. Consequences:

  • Mid-trip, if the connection drops: the driver marker stops moving, the ETA freezes, and the status sheet keeps confidently saying "Driver on the way". The rider has no way to distinguish "the driver is stuck in traffic" from "my phone lost signal". Socket.io reconnects silently underneath (socket.ts:23-24, reconnectionDelay: 800) with nothing surfaced.
  • Actions taken while offline fail one at a time into individual error toasts.
  • On cold start, index.tsx blocks on a network call with no timeout.

The one good exception is the tracking screen's non-blocking stale banner — a tap-to-retry pill with accessibilityLiveRegion that appears when the ride-detail fetch fails (tracking.tsx:373-387). That is exactly the right pattern; it just needs to be app-wide and driven by real connectivity rather than by one endpoint's failure.

Should be: add @react-native-community/netinfo, a global offline banner in both apps, "last updated Xs ago" freshness on live screens (the admin already does this), and a socket disconnect/reconnect listener that drives it. Effort: M (2–3 days).


What an operator sees when nothing is moving

Mostly good. a04-after-login.png: a LIVE chip with "updated 4s ago", four live counters, a "Needs attention" panel surfacing 2 open safety incidents, and a genuinely well-written empty state on the revenue chart. The stale-vs-empty discipline in dispatch (quoted above) is better than most production consoles.

Three gaps:

The dispatch queue and driver list poll every 30 seconds (dispatch/page.tsx:175), while driver positions come over a socket. So a driver going online, or a new ride entering the queue, can take up to half a minute to appear on the operator's board. For a console whose whole job is "react now", 30s is too slow. Should be: push queue changes over the existing socket, or drop the poll to ~5s to match the dashboard. Effort: S–M.

Zero-baseline deltas render as alarming red. With no trips in the window, Performance shows "↓ −100%" in red against ₹0 revenue and 0 trips. A fresh install therefore greets its new operator with two failure indicators. A "no comparison data yet" state is more honest and less frightening. Effort: S.

The admin's HTML title is hardcoded. admin/src/app/layout.tsx:28 sets title: 'Taxi Platform — Admin' — a literal, not the branding value. Every other surface reads the brand from settings (the sidebar showing "Taxi Platform" in the screenshot is the seeded DB default, which is correct behaviour), so the browser tab is the one place a buyer's rebrand will not take. Effort: S.


Ranked list

Do first — these five change how the product feels

# Finding Effort
1 "Finding your driver" is a dead screen — add the radar pulse, nearby cars, live narration M
2 Draw the route line on the rider's live map (data is already fetched and discarded) S
3 Interpolate + rotate the car marker instead of teleporting it M
4 Replace the 306 emoji with a real icon set — @expo/vector-icons is already installed transitively, so this is a swap, not an integration (start with tab bars, menus, map markers) M–L
5 Make a backgrounded driver's phone actually ring (custom channel sound + notification actions) M

High value, next

Finding Effort
Connectivity awareness — offline banner + freshness stamps in both apps M
Skeleton loaders — port the admin's shimmer to the apps' ten busiest screens M
Distance + ETA to pickup on the driver's offer card S–M
A shared Touchable with ripple/scale/haptic, applied across ~300 pressables M
Design the "driver found" beat — animate the card in, fill the bar, re-fit the map M
Pickup ETA per tier on the ride picker (instead of three identical trip times) M
Splash held until fonts and the landing decision are ready S
Nearby driver markers on the rider's home map — must be market-gated; illegal in France M

The long tail — small, cheap, worth doing

Finding Effort
Resend-OTP cooldown with a visible countdown S
Success haptic on driver arrive / start / complete S
Fare count-up + drawn checkmark + star haptics and labels on the rating screen S–M
Earnings strip animates and highlights when it increases S
Offer countdown bar escalates to red; switch it to the native driver S
Pull-to-refresh on wallet, driver history, driver wallet, trip detail S
"No drivers available" — offer the ride-type switch the copy promises S
ETA fallback on the tracking screen when the routing call fails S
Distance/time remaining on the driver's trip sheet M
Rider Settings is 2 rows vs the driver's 9 — add notification prefs, legal links, support S–M
Admin: dispatch queue over socket or a 5s poll S–M
Admin: no "−100%" against a zero baseline S
Admin: HTML title from branding, not a literal S
Investigate the missing route line on the driver's trip map (v18-ontrip.png) S
One vehicle-art system (photos or icons — not both) S–M
List/layout transitions M
Illustrated empty states that differ from one another M

Explicitly not on this list

The OTP screen, the toast system, the driver's offer ring, the driver's online pulse, the primary button, the welcome screen, the admin's skeletons, and the admin's stale-vs-empty discipline. These are done.


Method and honesty notes

  • Every code claim carries a file and, where useful, a line number; every screenshot claim names the file in <scratchpad>/e2e/.
  • Counts (306 emoji, 105 spinners, 181/119 pressables, 6 refresh screens, 0 skeletons, 0 ripples) are grep results over mobile/rider and mobile/driver, excluding node_modules.
  • Some screenshots predate fixes made during the e2e pass (the "Go offline" tofu box, the truncated home tiles, the empty "Pickup —" on the offer card). Where a screenshot shows an already-fixed defect I have said so rather than counting it twice.
  • Effort estimates are single-developer working days: S ≈ under a day, M ≈ 2–4 days, L ≈ 1–2 weeks.
  • One observation (the driver trip map's missing route line) is labelled as unreproduced rather than presented as a defect.

Companion document: docs/country-gaps.md covers what is missing market by market — regulatory, payment and localisation gaps across the 37 preset countries. Two items appear in both because they are simultaneously polish problems and store-policy blockers: the rider's near-empty Settings screen, and the fact that "Terms & Privacy Policy" on the welcome screen is unlinked text.

source: docs/polish-audit.md (ships identically in the product zip)