Replaced every mock in the passenger app and wrapped it for iOS and Android.
27 files swept to light mode. The ride request flow now inserts directly into Supabase and subscribes to postgres_changes for real-time driver updates. Eight files had mock arrays stripped and replaced with live queries. Capacitor wrapped the Vite bundle into native iOS and Android projects.
The passenger app had four layers of fake data. Every layer needed to come out before the app could connect to a real driver, a real vendor, or a real rental car.
The constraint
The codebase had three categories of problems.
First, 27 files contained hardcoded dark hex values (#0B0F1A, #141929, #1C2333) and the old teal accent (#00D4AA). The design system had moved to light mode with Lime Green (#10B981), but the components had not followed.
Second, RideRequestSheet.tsx was not talking to the database at all. handleConfirm called fetch("/api/rides/request") — a route that does not exist in production — and a set of useRef timers auto-advanced trip status from searching to assigned to en_route without any driver ever accepting. Five mock drivers lived in MOCK_DRIVERS as a fallback when no WebSocket data arrived.
Third, HubPage.tsx, rental-cars.ts, TravelTicketsPage.tsx, StorePage.tsx, PropertyPage.tsx, MyOrdersPage.tsx, and two others held hardcoded arrays — MOCK_VENDORS, MOCK_CARS, MOCK_SCHEDULES, MOCK_ORDERS. Each returned fake data if Supabase was unreachable, or fake data unconditionally.
The proof
Phase 1 — light mode sweep
The sweep ran in two rounds. Round 1 touched the four core components: RideRequestSheet.tsx, BottomNav.tsx, HubSwitcher.tsx, and BuyAgainCarousel.tsx. Round 2 ran 22 files across four parallel agents. After both rounds, a pattern scan returned:
| Pattern | Result |
|---|---|
Dark hex colors (#0B0F1A, #141929, #1C2333) | Zero matches |
Old teal accent (#00D4AA) | Zero matches |
bg-white/[, border-white/[, hover:bg-white/[ | Zero matches |
dark-shimmer in components | Zero matches |
rgba(255,255,255,...) | 8 intentional — frosted headers, image overlays |
27 files changed. Every dark background replaced with #FFFFFF or #F9FAFB. Every TEAL constant replaced with LIME (#10B981). Every text-white on a light surface replaced with text-slate-900 or text-slate-500.
Phase 2 — live ride insertion
handleConfirm in RideRequestSheet.tsx previously called:
fetch(`${API_BASE}/api/rides/request`, {
method: "POST",
body: JSON.stringify({ pickup, destination, vehicle, payment_method }),
})
That route does not exist. I replaced it with a direct Supabase insert:
const { data, error } = await supabase
.from("orders")
.insert({
status: "searching",
payment_method: paymentMethod,
fare: fareResult?.fare ?? 0,
pickup: { address: pickup, lat: pickupCoords![0], lng: pickupCoords![1] },
destination: { address: destination, lat: dropoffCoords![0], lng: dropoffCoords![1] },
customer_id: userData?.user?.id,
})
.select("id")
.single();
I removed fallbackTimerRef, fallbackChainRef, isDemoFallbackRef, fallbackClearedRef, and the entire useEffect that auto-advanced trip status on a clock. The MOCK_DRIVERS array — five hardcoded Ugandan names, plates, and ratings — came out entirely. liveDriver now resolves to wsDriver or null.
In place of the timers, a postgres_changes subscription activates the moment orderId is set:
supabase
.channel(`order-${orderId}`)
.on("postgres_changes", {
event: "UPDATE",
schema: "public",
table: "orders",
filter: `id=eq.${orderId}`,
}, (payload) => {
const status = payload.new.status;
if (status === "assigned") setTripStatus("assigned");
if (status === "en_route") setTripStatus("en_route");
if (status === "completed") setTripStatus("arrived");
if (status === "cancelled") setTripStatus(null);
})
.subscribe();
When the subscription unmounts or orderId changes, supabase.removeChannel(channel) cleans up.
Phase 3 — live hub queries
Eight files had mock arrays removed and replaced with live Supabase fetches.
HubPage.tsx previously fell back to MOCK_VENDORS when hasSupabase() returned false. The guard came out. The table name changed from vendors to partners — the correct table in the production schema:
const { data, error } = await supabase
.from("partners")
.select("*");
if (error || !data) {
setVendors([]);
return;
}
rental-cars.ts removed MOCK_CARS and the usedMock return flag. fetchRentalCars now returns { cars, error }. An empty response renders an empty state, not fake cars.
TravelTicketsPage.tsx removed MOCK_SCHEDULES — 10 hardcoded bus and flight rows — and added a useEffect fetching from supabase.from("travel_schedules").select("*").
StorePage.tsx removed MOCK_PRODUCTS and fetches from supabase.from("products").select("*").eq("vendor_id", id).
MyOrdersPage.tsx removed the entire localStorage mock branch — courier orders, broker requests, travel tickets, and ride orders all returned as fake data if Supabase was unreachable. All six data sources now fetch exclusively from live tables. The catch block resets every array to [].
Phase 4 — Capacitor native shell
I installed the four Capacitor packages:
pnpm install @capacitor/core @capacitor/ios @capacitor/android
pnpm install -D @capacitor/cli
capacitor.config.ts in the project root:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.fikalo.passenger',
appName: 'Fikalo',
webDir: 'dist'
};
export default config;
pnpm run build compiled 2,278 modules in 20.12 seconds. Then npx cap sync returned this:
[fatal] The Capacitor CLI requires NodeJS >=22.0.0
The machine was on Node v20.19.6. I upgraded with nvm:
nvm install 22
nvm use 22.23.2
cap sync ran clean:
√ copy web in 324.83ms
√ update web in 211.52ms
[info] Sync finished in 0.975s
The ios/ and android/ native project folders now exist in the repo.
Where this sits
| Item | Status |
|---|---|
| 27-file light mode sweep — zero dark hex matches | Done |
handleConfirm — direct Supabase insert into orders | Done |
| Demo fallback timers — removed | Done |
MOCK_DRIVERS — removed | Done |
postgres_changes subscription for real-time driver updates | Done |
HubPage.tsx — MOCK_VENDORS removed, live partners fetch | Done |
rental-cars.ts — MOCK_CARS removed | Done |
TravelTicketsPage.tsx — MOCK_SCHEDULES removed | Done |
StorePage.tsx — MOCK_PRODUCTS removed | Done |
MyOrdersPage.tsx — mock localStorage branch removed | Done |
Capacitor installed, cap sync completed | Done |
| iOS and Android native projects generated | Done |
| Google Maps API key — not yet wired | Open |
| PesaPal production credentials — not yet wired | Open |
The passenger app no longer simulates a ride. It books one.