From 32ffe782eafbbe4716d277810830d1a6d447699b Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 18 Jun 2026 01:23:36 -0400 Subject: [PATCH] Fix Live Activity lock screen rendering and polish multi-account auth. Flatten widget layouts and use system colors so banner and expanded regions render on vibrant lock screens; migrate auth sessions per account to prevent double sign-in; scope app lock PIN to accounts; default clock description to "Clock In"; add architecture docs and deferred form validation on auth screens. Co-authored-by: Cursor --- AGENTS.md | 34 ++- README.md | 104 ++++++--- app.json | 3 +- app/(app)/index.tsx | 4 +- app/(app)/invoices/edit/[id].tsx | 30 ++- app/(app)/invoices/new.tsx | 63 +++--- app/(auth)/forgot-password.tsx | 25 ++- app/(auth)/register.tsx | 60 ++++- app/(auth)/reset-password.tsx | 44 +++- app/(auth)/sign-in.tsx | 84 ++++--- app/_layout.tsx | 10 +- components/AccountSwitcher.tsx | 264 ++++++++++++++++++++++ components/AppLockOverlay.tsx | 74 ++++--- components/AuthServerPicker.tsx | 227 +++++++++++++++++++ components/PinPrompt.tsx | 1 - components/TopChrome.tsx | 6 +- components/TopChromeBar.tsx | 2 +- components/businesses/BusinessForm.tsx | 17 +- components/clients/ClientForm.tsx | 23 +- components/time-clock/TimeClockPanel.tsx | 49 +++-- components/ui/Input.tsx | 8 +- components/ui/SelectField.tsx | 18 +- contexts/AppLockContext.tsx | 175 +++++++++------ docs/ARCHITECTURE.md | 266 +++++++++++++++++++++++ docs/README.md | 12 + lib/app-lock.ts | 70 ++++-- lib/auth-storage.ts | 71 ++++++ lib/form-validation.ts | 66 ++++++ lib/secure-store-keys.ts | 7 + lib/server-mode.ts | 35 +++ lib/time-clock-live-activity.ts | 18 +- lib/time-clock-live-activity.types.ts | 8 +- lib/time-clock.ts | 7 + lib/widget-brand-assets.ts | 55 ----- widgets/TimeClockActivity.tsx | 161 +++++++------- 35 files changed, 1659 insertions(+), 442 deletions(-) create mode 100644 components/AccountSwitcher.tsx create mode 100644 components/AuthServerPicker.tsx create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/README.md create mode 100644 lib/auth-storage.ts create mode 100644 lib/form-validation.ts create mode 100644 lib/secure-store-keys.ts create mode 100644 lib/server-mode.ts delete mode 100644 lib/widget-brand-assets.ts diff --git a/AGENTS.md b/AGENTS.md index a26b4bb..9e0c4ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,33 @@ -# Expo HAS CHANGED +# beenvoice-app — agent notes -Read the exact versioned docs at https://docs.expo.dev/versions/v56.0.0/ before writing any code. +Expo SDK **56**. Read [Expo v56 docs](https://docs.expo.dev/versions/v56.0.0/) before changing native config. + +## Read first + +- [README.md](./README.md) — setup and run +- [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) — routing, auth, accounts, tRPC, widgets + +## Conventions + +- **Package manager**: Bun only +- **API types**: import `AppRouter` from `beenvoice/server/api/root` (tsconfig path `../beenvoice/src/*`) +- **Styling**: `useAppTheme()` + `useThemedStyles()`; tokens in `lib/theme-palette.ts` +- **Forms**: `lib/form-validation.ts`; show errors only after blur/submit (`useFieldVisibility`) +- **Auth**: never remount account without migrating SecureStore session (`lib/auth-storage.ts`) +- **Widgets**: all Live Activity UI must be inside the `"widget"` function in `widgets/TimeClockActivity.tsx` +- **Metro**: port 8082; dev client required (not Expo Go) + +## Key files + +| Concern | Path | +|---------|------| +| Root providers | `app/_layout.tsx` | +| Multi-account | `contexts/AccountsContext.tsx`, `lib/accounts.ts` | +| Session migration | `lib/auth-storage.ts` | +| tRPC | `lib/trpc.tsx` | +| App lock | `lib/app-lock.ts`, `contexts/AppLockContext.tsx` | +| Time clock | `components/time-clock/TimeClockPanel.tsx` | + +## Server repo + +Sibling `../beenvoice` — run `bun run dev` on :3000 before mobile dev. diff --git a/README.md b/README.md index c2e0b5e..05d1ab6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # beenvoice Mobile -Expo companion app for [beenvoice](../beenvoice) — dashboard, time clock, invoices, and account settings. +Expo companion for [beenvoice](../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](./docs/ARCHITECTURE.md) ## Prerequisites - [Bun](https://bun.sh) 1.3+ -- beenvoice server running (see `../beenvoice/README.md`) -- iOS development build for Live Activities (`expo-widgets`) +- beenvoice API running ([setup](../beenvoice/README.md)) +- Xcode + iOS Simulator (or device) for native dev build +- **Not Expo Go** — widgets, SecureStore auth, and biometrics need `expo-dev-client` ## Setup @@ -16,68 +19,103 @@ bun install cp .env.example .env ``` -Edit `.env` and set your API URL: +`.env`: ```env # Simulator EXPO_PUBLIC_API_URL=http://localhost:3000 -# Physical iPhone (use your Mac's LAN IP) +# Physical iPhone — Mac LAN IP EXPO_PUBLIC_API_URL=http://192.168.1.42:3000 ``` -The beenvoice server must have the Expo auth plugin enabled (`@better-auth/expo` in `beenvoice/src/lib/auth.ts`). +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 ```bash -# Terminal 1 — API server +# Terminal 1 — API cd ../beenvoice && bun run dev -# Terminal 2 — mobile app (development build) -cd beenvoice-app -bun run ios +# Terminal 2 — mobile (builds native app if needed) +cd beenvoice-app && bun run ios ``` -This uses port **8082** for Metro so it does not collide with other Expo projects on 8081. +Metro uses port **8082** (avoids other Expo projects on 8081). -If you already built the app and only need Metro: +Metro only (app already installed): ```bash bun run start -- --clear ``` -Then open the **beenvoice** app on the simulator (not Expo Go). +Open the **beenvoice** dev build on the simulator — not Expo Go. -Live Activities require a native build (`bun run ios`). They do not work in Expo Go. +### After native changes -After changing `assets/beenvoice.icon`, rebuild iOS: +Icon (`assets/beenvoice.icon`), widgets, or new native modules: ```bash bunx expo prebuild --platform ios --clean bun run ios ``` -### Troubleshooting `PlatformConstants` / `[runtime not ready]` - -Usually one of: - -1. **Wrong Metro bundler** — another project's dev server is on the same port. Stop it or use `--port 8082`. -2. **Stale native build** — after adding native modules, rebuild: - ```bash - bunx expo prebuild --platform ios --clean - bun run ios - ``` -3. **Expo Go** — native modules like widgets need the custom dev build from `bun run ios`, not Expo Go. - ## Features -- **Auth** — sign in, register, forgot password, reset password; multiple saved accounts -- **Dashboard** — revenue, pending, overdue, recent invoices -- **Timer** — clock in/out with client, invoice, and hourly rate; iOS Live Activity (dev build) -- **Invoices** — list, filter by status, tap to update status -- **Settings** — profile, accounts, theme (system/light/dark), server URL, sign out +| 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:guest` until 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](./docs/ARCHITECTURE.md#multi-account-model) ## Deep links -- `beenvoice://reset-password?token=...` — open reset password screen with token prefilled +| Scheme | Screen | +|--------|--------| +| `beenvoice://reset-password?token=…` | Reset password | +| `beenvoice://timer` | Timer tab (from Live Activity) | + +## 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`) | + +## Related + +- [beenvoice README](../beenvoice/README.md) +- [beenvoice ARCHITECTURE](../beenvoice/docs/ARCHITECTURE.md) +- [Workspace root README](../README.md) diff --git a/app.json b/app.json index 6a1f64f..c7601e4 100644 --- a/app.json +++ b/app.json @@ -48,7 +48,8 @@ [ "expo-widgets", { - "groupIdentifier": "group.com.beenvoice.app" + "groupIdentifier": "group.com.beenvoice.app", + "bundleIdentifier": "com.beenvoice.app.ExpoWidgetsTarget" } ], [ diff --git a/app/(app)/index.tsx b/app/(app)/index.tsx index 912ba4a..bba5587 100644 --- a/app/(app)/index.tsx +++ b/app/(app)/index.tsx @@ -18,7 +18,7 @@ import { formatCurrency, formatDate } from "@/lib/format"; import type { ThemeColors } from "@/lib/theme-palette"; import { useThemedStyles } from "@/lib/use-themed-styles"; import { getInvoiceStatus } from "@/lib/invoice-status"; -import { formatElapsedHoursMinutes } from "@/lib/time-clock"; +import { formatElapsedHoursMinutes, resolveClockDescription } from "@/lib/time-clock"; import { useRunningElapsed } from "@/lib/use-running-elapsed"; import { api } from "@/lib/trpc"; @@ -88,7 +88,7 @@ export default function DashboardScreen() { - {running.description || "Timer running"} + {resolveClockDescription(running.description)} {running.client?.name ?? "No client"} diff --git a/app/(app)/invoices/edit/[id].tsx b/app/(app)/invoices/edit/[id].tsx index 1007263..90b3ebf 100644 --- a/app/(app)/invoices/edit/[id].tsx +++ b/app/(app)/invoices/edit/[id].tsx @@ -22,6 +22,7 @@ import { fonts, spacing } from "@/constants/theme"; import { useAppTheme } from "@/contexts/ThemeContext"; import { formatCurrency } from "@/lib/format"; import { getInvoiceStatus } from "@/lib/invoice-status"; +import { validateLineItems } from "@/lib/form-validation"; import { useTabBarScrollPadding } from "@/lib/tab-bar-insets"; import type { ThemeColors } from "@/lib/theme-palette"; import { useThemedStyles } from "@/lib/use-themed-styles"; @@ -100,6 +101,8 @@ export default function InvoiceEditScreen() { const taxAmount = subtotal * (taxRate / 100); const total = subtotal + taxAmount; const currency = invoice?.currency ?? "USD"; + const lineItemsError = validateLineItems(items); + const canSave = !lineItemsError; if (!id) { return ; @@ -170,6 +173,7 @@ export default function InvoiceEditScreen() { } function handleSave() { + if (!canSave) return; setError(null); const parsedItems: Array<{ @@ -180,25 +184,11 @@ export default function InvoiceEditScreen() { }> = []; for (const item of items) { - const hours = Number(item.hours); - const rate = Number(item.rate); - if (!item.description.trim()) { - setError("Each line needs a description"); - return; - } - if (Number.isNaN(hours) || hours < 0) { - setError("Hours must be a valid number"); - return; - } - if (Number.isNaN(rate) || rate < 0) { - setError("Rate must be a valid number"); - return; - } parsedItems.push({ date: item.date, description: item.description.trim(), - hours, - rate, + hours: Number(item.hours), + rate: Number(item.rate), }); } @@ -274,10 +264,16 @@ export default function InvoiceEditScreen() { + {lineItemsError ? {lineItemsError} : null} {error ? {error} : null} -