Fixes App Store Connect warnings when the widget extension CFBundle version does not match the containing application. Co-authored-by: Cursor <cursoragent@cursor.com>
beenvoice Mobile
Expo companion for beenvoice — dashboard, time clock, invoices, clients, businesses, and settings. Shares the same tRPC API and better-auth sessions as the web app.
Architecture (dense): docs/ARCHITECTURE.md
Prerequisites
- Bun 1.3+
- beenvoice API running (setup)
- Xcode + iOS Simulator (or device) for native dev build
- Not Expo Go — widgets, SecureStore auth, and biometrics need
expo-dev-client
Setup
cd beenvoice-app
bun install
cp .env.example .env
.env:
# Simulator
EXPO_PUBLIC_API_URL=http://localhost:3000
# Physical iPhone — Mac LAN IP
EXPO_PUBLIC_API_URL=http://192.168.1.42:3000
Omit EXPO_PUBLIC_API_URL in production builds to default to https://beenvoice.soconnor.dev.
Server must enable @better-auth/expo in beenvoice/src/lib/auth.ts with beenvoice:// in trustedOrigins.
Run
# Terminal 1 — API
cd ../beenvoice && bun run dev
# Terminal 2 — mobile (builds native app if needed)
cd beenvoice-app && bun run ios
Metro uses port 8082 (avoids other Expo projects on 8081).
Metro only (app already installed):
bun run start -- --clear
Open the beenvoice dev build on the simulator — not Expo Go.
After native changes
Icon (assets/beenvoice.icon), widgets, or new native modules:
bunx expo prebuild --platform ios --clean
bun run ios
Features
| Area | Details |
|---|---|
| Auth | Sign in, register, forgot/reset password; official or self-hosted server |
| Multi-account | Bitwarden-style switcher; per-account session in SecureStore |
| Dashboard | Revenue, pending, overdue, running timer, recent invoices |
| Timer | Clock in/out, client + invoice + rate; optional description (default "Clock In"); iOS Live Activity |
| Entities | Clients and businesses — list, create, edit |
| Invoices | List, filter, create, edit, status updates |
| Settings | Profile, accounts, theme, per-account app lock (PIN + Face ID), sign out |
| App lock | Per-account; locks on background return |
Auth & accounts (summary)
- Guest auth storage:
beenvoice:guestuntil first successful login - Per account:
beenvoice:auth:{host::userId}in SecureStore - After login,
finalizeAuthenticatedAccount()migrates session keys before activating the account (avoids double login) - Server picker: Official (
beenvoice.soconnor.dev) or custom URL on auth screens
Full flow: docs/ARCHITECTURE.md#multi-account-model
Deep links & Shortcuts
| URL | Action |
|---|---|
beenvoice://reset-password?token=… |
Reset password |
beenvoice://timer |
Open time clock |
beenvoice://shortcuts/clock-in |
Clock in (last client) |
beenvoice://shortcuts/clock-in?title=… |
Clock in with title |
beenvoice://shortcuts/clock-out |
Clock out running timer |
iOS Shortcuts / Siri (requires native rebuild: bunx expo prebuild --platform ios && bun run ios):
- Clock In — starts the timer with your last client
- Clock Out — stops the running timer
- Open Time Clock — opens the timer tab
- Install a fresh build on a physical iPhone (iOS 16+).
- Open the app once while signed in (registers shortcuts with the system).
- Shortcuts app → search beenvoice → add actions, or say “Hey Siri, clock in with beenvoice”.
- Pick a client once on the Timer tab before the first clock-in shortcut.
Test deep links:
xcrun simctl openurl booted "beenvoice://shortcuts/clock-in"
xcrun simctl openurl booted "beenvoice://shortcuts/clock-out"
xcrun simctl openurl booted "beenvoice://timer"
Project layout
app/
_layout.tsx # Providers, auth guard
(auth)/ # sign-in, register, password flows
(app)/ # tab shell + nested stacks
components/ # UI, forms, chrome, time clock
contexts/ # Auth, Accounts, AppLock, Theme
lib/ # tRPC, auth storage, config, theming
widgets/ # iOS Live Activity (TimeClockActivity)
Troubleshooting
| Issue | Fix |
|---|---|
PlatformConstants / runtime not ready |
Stop other Metro on 8081/8082; rebuild with prebuild --clean |
| Expo Go | Use bun run ios dev build |
| API errors on device | EXPO_PUBLIC_API_URL = Mac LAN IP; server BETTER_AUTH_URL must match |
| Live Activity empty | Rebuild iOS; widget UI must live inside "widget" function |
| Login twice | Server + app versions with session migration (lib/auth-storage.ts) |