Date: 2026-06-07 Commits: ce1fb8b, bd61523, e8748de, 5482203, 0ccd385, 6953c69, 93f694b, 7d268d4, 3e2cf3e, 6fb8bdc, and the Profile/Character/Device follow-ups (fbab509, a7473f9, 683dda4, d669f87, 7b4320a, e03faf1, 0aba8a6, fc8646e, be0049b, 430e70c, bd19811, ec9b80f) Theme: Build the iOS companion app - the human face of the headless car device.

The device (Reports 01–02) is a headless appliance. This session built the Expo / React Native app the driver actually touches: to set the device up, watch its status, edit their profile, tune the voice, and review memory.

Stack: Expo 52 · React Native 0.76 · TypeScript (strict) · React Navigation v6 · Zustand · Reanimated 3 · GestureHandler v2 · Axios.


1. App scaffold + design system (ce1fb8b, bd61523)

What. An Expo 52 / RN 0.76 TypeScript app, plus a design system - colors.ts, spacing.ts, typography.ts.

Why. Every screen would otherwise reinvent its own colours and spacing, producing visual drift. A car app also wants a specific feel: dark, calm, high-contrast for glanceability while driving.

How. A dark theme built around background #09090F, card #1A1A26, an accent #00D4FF (cyan) for CTAs and an active #00FF88 (green) for the connection indicator - KITT-adjacent without being a costume. Spacing and typography are tokens, not magic numbers.

Purpose / goal. A single source of visual truth so the app stays coherent as screens multiply.


2. API client + typed device contract (e8748de)

What. An Axios client (api/client.ts) and typed request functions (api/device.ts) talking to the device's FastAPI server.

Why. The phone and the Pi are two processes on a flaky local link (WiFi or BT/USB tether). Requests must time out fast, be cancellable, and target a base URL the user can change.

How. A single Axios instance whose baseURL is cached in module memory and invalidated via invalidateBaseUrl() (called when the device IP changes). Every request carries a 3-second timeout; fetchStatus accepts an optional AbortSignal so polling can cancel in-flight requests on unmount. The contract: GET /status → { status, mode: 'cloud'|'local', internet, uptime_s }.

Purpose / goal. A resilient, typed bridge to the device that degrades gracefully when the link is slow or gone - the spine of every screen.


3. Navigation + state (5482203, 0ccd385)

What. A root navigator with an onboarding gate, bottom-tab main navigation, and three Zustand stores (useDeviceStore, useProfileStore, useConfigStore).

Why. The app has two modes - first-run onboarding and normal use - and shared state (connection, profile, config) that many screens read and write. That needs structure, not prop-drilling.

How. RootNavigator reads AsyncStorage['onboarding_complete'] on mount and renders either the onboarding stack or the main tabs. State lives in three small Zustand stores, each persisted to AsyncStorage: device connection/status, user profile (syncs via PATCH /profile), and device config (syncs via dot-notation PATCH /config).

Purpose / goal. A clean app skeleton where adding a screen or a piece of shared state is a local change.


4. Hooks, shared components, onboarding (6953c69, 93f694b, 7d268d4)

What. useDeviceConnection (polls /status) and useLocation hooks; shared UI components (StatusBadge, PulseIndicator, SettingRow); a 3-screen onboarding flow (Welcome → Location → Usage).

Why. Connection state should update on its own; common UI shouldn't be copy-pasted; and a first-time user needs guided setup, not a cold dashboard.

How. useDeviceConnection polls /status on an interval, one AbortController per tick, aborting on unmount or IP change. Onboarding collects the basics (location for personalisation, usage pattern) and flips the onboarding_complete flag.

Purpose / goal. Make the app feel alive (live status) and welcoming (guided first run) with reusable building blocks.


5. Core screens + offline-tolerant forms (3e2cf3e, 430e70c, be0049b,

bd19811)

What. Home, Profile, Character (Voice), and Memory screens - and the decision to let forms work even when the device is disconnected.

Why / the design problem. Early versions blanked the Profile/Character screens when the device was unreachable, so a user on the couch (phone, but device powered off in the car) saw nothing and couldn't prepare anything. That's a dead app most of the time.

How. Profile and Character render their forms with empty/default values even when offline, showing an "EXCAR not connected" banner instead of hiding (be0049b, 430e70c, bd19811). A deviceSynced flag distinguishes local edits from device-confirmed state, so edits made offline reconcile when the device comes back.

Purpose / goal. The app is useful any time you open it, not only when sitting in a running car - a basic but easy-to-miss requirement for an appliance companion.


6. Device setup screen + tab polish (fbab509, a7473f9, 683dda4,

d669f87, 7b4320a, fc8646e)

What. A DeviceSetupScreen for entering a custom device IP (save & test, reset to default), reachable from Profile and from a "Set up connection" button on a disconnected Home card; plus Ionicons tab bar (home / person / mic / time).

Why. excar.local mDNS resolution isn't reliable on every network, so users need a manual IP escape hatch. And the tab bar needed to read at a glance.

How. The setup screen writes the IP and calls invalidateBaseUrl() so the next request rebuilds the Axios instance against the new host (54a9ab0 later hardened this). Tabs moved from dots to labelled Ionicons.

Purpose / goal. Make first connection robust on real-world networks and the navigation instantly legible.


Bugs & fixes this session

  • Onboarding wrote the wrong profile fields - used location/hours where the

store expected city/daily_hours (e03faf1, 0aba8a6). Fixed by aligning field names and simplifying the updateProfile call.

  • @expo/vector-icons crashed without expo-font - added expo-font ~13.0.4

and pinned expo-asset ~11.0.5 (6fb8bdc, ec9b80f). Root cause: icon font loading is a transitive requirement that isn't auto-installed.


End-of-session state

A working iOS app: onboarding → connect to device (mDNS or manual IP) → live status on Home → edit Profile/Voice offline-tolerant → review Memory. The device finally has a face. Report 04 turns that face into a control room.