Comma
Architecture

Navigation

The phone app uses Expo Router: the folder tree under `app/` maps directly to routes, wrapped in a custom shell that provides a left drawer, a hidden tab navigator, and a right-slide Reports panel.

left drawerhand-built, swipe from the edge
tabsExpo Router tabs, bar hidden
reports panelright-slide overlay
File-based routes wrapped in a hand-built shell. Android back closes the Reports panel, then the drawer, then exits.

Route tree

app/
├── _layout.tsx # Root layout — providers, global init, error handling
├── notifications.tsx # Notification permission setup

├── (tabs)/ # Main shell (drawer + tabs + Reports overlay)
│ ├── _layout.tsx # Custom drawer, hidden Tabs, Reports panel
│ ├── index.tsx # Dashboard — also the onboarding gate
│ ├── shifts/index.tsx # Shifts list
│ ├── analytics.tsx # Advanced analytics (flag-gated)
│ ├── expenses/index.tsx # Expenses list
│ ├── tax/index.tsx # Tax center (hidden from the tab bar; reached via drawer)
│ └── more.tsx # More menu

├── setup/ # Activation-checklist destinations
│ ├── platforms.tsx # Choose platforms
│ ├── vehicle.tsx # Add a real vehicle
│ └── goal.tsx # Set an earnings goal

├── shift/add.tsx # Create / log a shift
├── shifts/[id].tsx # Shift detail / edit
├── expense/add.tsx # Log an expense
├── expense/[id].tsx # Expense detail / edit
├── vehicles/index.tsx # Vehicle list
├── vehicles/[id].tsx # Vehicle detail / edit
├── goals/index.tsx # Goals + gamification (flag-gated)
├── tax/center.tsx # Tax center detail
├── reports/index.tsx # Reports panel content (rendered as an overlay)
├── schedule/index.tsx # Weekly schedule (flag-gated)

├── settings/
│ ├── index.tsx # Settings root
│ ├── backup.tsx # Google Drive backup & sync
│ ├── profile.tsx # Edit profile
│ └── import.tsx # CSV import

├── about/index.tsx # About (version, licenses, links)
└── docs/ # Internal design notes (Markdown, not routes)

Routes to files

RouteFileNotes
/app/(tabs)/index.tsxDashboard; renders the onboarding wizard until setup is complete
/shiftsapp/(tabs)/shifts/index.tsxShifts list
/shifts/[id]app/shifts/[id].tsxShift detail / edit
/shift/addapp/shift/add.tsxLog or create a shift
/analyticsapp/(tabs)/analytics.tsxGated on analytics_advanced
/expensesapp/(tabs)/expenses/index.tsxExpenses list
/expense/add, /expense/[id]app/expense/…Create and edit an expense
/taxapp/(tabs)/tax/index.tsxGated on tax_workspace; hidden from the tab bar
/tax/centerapp/tax/center.tsxTax center detail
/goalsapp/goals/index.tsxGated on goals
/vehicles, /vehicles/[id]app/vehicles/…Vehicle list and detail
/reportsapp/reports/index.tsxOpened as a right-slide overlay, not a pushed screen
/scheduleapp/schedule/index.tsxGated on schedule
/settings, /settings/backup, /settings/profile, /settings/importapp/settings/…Settings screens
/setup/platforms, /setup/vehicle, /setup/goalapp/setup/…Activation-checklist destinations
/aboutapp/about/index.tsxAbout
/notificationsapp/notifications.tsxNotification permission setup

Root layout

app/_layout.tsx wraps the app in the React Query provider and the gesture root, sets up global error handling and notification listeners, and hydrates the Zustand stores from storage on launch.


Onboarding gate

There is no separate onboarding route. The Dashboard (app/(tabs)/index.tsx) checks isOnboardingCompleted from the settings store, and while it is false it renders <OnboardingWizard /> in place of the dashboard.

The wizard opens on a welcome gate with three choices — start fresh, try the demo, or restore existing data — then runs two steps: country and region, then the driver's last shift. It ends in an hourly-rate reveal computed from that shift. Everything the wizard no longer asks for is deferred to the dashboard's activation checklist (components/ActivationChecklist.tsx), whose items deep-link into /setup/platforms, /setup/vehicle, and /setup/goal. See Onboarding for the driver-facing flow.


The shell: drawer, tabs, and Reports

app/(tabs)/_layout.tsx is a custom shell rather than a stock navigator.

  • Left drawer. A hand-built Animated drawer, opened by the header's menu button or a left-edge swipe (a PanResponder). It is the primary navigation. Its items are built per render and respect feature flags.
  • Tabs. An Expo Router Tabs navigator hosts the main screens (index, shifts, analytics, expenses, tax, more), but its bar is hidden (tabBarStyle: { display: "none" }) — navigation happens through the drawer and the More screen. The tax tab sets href: null, so it is reachable only from the drawer.
  • Reports panel. A full-screen Animated overlay that slides in from the right, holding ReportsScreen. The drawer's Reports item opens it rather than navigating. The Android back button closes the Reports panel, then the drawer, before exiting.

Drawer items

ItemRouteShown when
Dashboard/Always
Shifts/shiftsAlways
Analytics/analyticsanalytics_advanced enabled
Expenses/expensesAlways
Goals/goalsgoals enabled
Tax/taxtax_workspace enabled and the country has self-assessment tax
Reports/reportsAlways (opens the overlay)
Schedule/scheduleschedule enabled
Vehicles/vehiclesAlways
Settings/settingsAlways
About/aboutAlways (drawer footer)

Feature-gated routes

Gated screens are resolved with useFeatureEnabled(flag), which reads a user override first, then the country default. The gated flags are analytics_advanced, goals, tax_workspace, and schedule. A gated screen stays in the route tree even when hidden — it is simply not linked from the drawer, and can still be reached by its route.

On this page