Add 'apps/mobile/' from commit '5fa30f365f21531094cd4d2045042bb4f1370ac3'
git-subtree-dir: apps/mobile git-subtree-mainline:86f8987dffgit-subtree-split:5fa30f365f
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
# App Store Connect — beenvoice iOS
|
||||
|
||||
Copy-paste reference for submitting **beenvoice** (`com.beenvoice.app`, v1.0.0). Production web host: `beenvoice.app`.
|
||||
|
||||
---
|
||||
|
||||
## App Information
|
||||
|
||||
| Field | Value |
|
||||
|-------|--------|
|
||||
| **Name** | beenvoice |
|
||||
| **Subtitle** (30 chars max) | Invoices & time tracking |
|
||||
| **Bundle ID** | `com.beenvoice.app` |
|
||||
| **SKU** | `beenvoice-ios` (your choice; immutable) |
|
||||
| **Primary language** | English (U.S.) |
|
||||
| **Primary category** | Business |
|
||||
| **Secondary category** | Productivity |
|
||||
| **Content rights** | Does not contain third-party content |
|
||||
| **Age rating** | 4+ (no restricted content; business/finance utility) |
|
||||
|
||||
### Copyright
|
||||
|
||||
```
|
||||
© 2026 beenvoice
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URLs
|
||||
|
||||
Deploy the Next.js legal pages before submission. Privacy Policy and Terms must load without login.
|
||||
|
||||
| Field | URL |
|
||||
|-------|-----|
|
||||
| **Privacy Policy URL** | `https://beenvoice.app/privacy` |
|
||||
| **Terms of Use (EULA)** | Use Apple Standard EULA *or* link `https://beenvoice.app/terms` |
|
||||
| **Support URL** | `https://beenvoice.app` (or a dedicated `/support` page when available) |
|
||||
| **Marketing URL** (optional) | `https://beenvoice.app` |
|
||||
|
||||
---
|
||||
|
||||
## Promotional Text (170 chars max)
|
||||
|
||||
Optional; can be changed without a new build.
|
||||
|
||||
```
|
||||
Track billable hours, manage clients, and send invoices from your phone. Syncs with your beenvoice account. Lock the app with Face ID.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Description (4000 chars max)
|
||||
|
||||
```
|
||||
beenvoice is the mobile companion for freelancers and small teams who invoice clients and track billable time.
|
||||
|
||||
DASHBOARD AT A GLANCE
|
||||
See revenue, pending and overdue invoices, and your running timer without opening multiple tools.
|
||||
|
||||
TIME CLOCK
|
||||
Clock in and out with an optional description, client, invoice, and hourly rate. On iPhone, a Live Activity on the Lock Screen and Dynamic Island shows elapsed time while you work.
|
||||
|
||||
INVOICES
|
||||
Browse, filter, create, and edit invoices. Update status and keep billing moving from anywhere.
|
||||
|
||||
CLIENTS & BUSINESSES
|
||||
Maintain client records and business profiles so invoices stay consistent across web and mobile.
|
||||
|
||||
MULTI-ACCOUNT
|
||||
Switch between beenvoice accounts (e.g. work and personal) with separate sessions, similar to a password manager.
|
||||
|
||||
SECURITY
|
||||
Optional per-account app lock with PIN and Face ID / Touch ID when returning to the app.
|
||||
|
||||
OFFICIAL OR SELF-HOSTED
|
||||
Sign in to the official beenvoice cloud or point the app at your own beenvoice server URL.
|
||||
|
||||
REQUIREMENTS
|
||||
A beenvoice account and network access to your beenvoice server. The mobile app is not a standalone product—it connects to the same API as the beenvoice web app.
|
||||
|
||||
Questions or feedback: support via your beenvoice administrator or the contact on beenvoice.app.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keywords (100 chars max, comma-separated, no spaces after commas)
|
||||
|
||||
```
|
||||
invoice,time tracking,freelance,billing,clients,timer,accounting,small business,hours,beenvoice
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What’s New (Version 1.0.0)
|
||||
|
||||
```
|
||||
Initial App Store release.
|
||||
|
||||
• Dashboard with revenue and invoice summaries
|
||||
• Time clock with optional client, invoice, and rate
|
||||
• iOS Live Activity for running timers
|
||||
• Invoice list, create, and edit
|
||||
• Clients and businesses management
|
||||
• Multi-account support with secure sign-in
|
||||
• Per-account app lock (PIN and Face ID)
|
||||
• Light and dark appearance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## App Review Information
|
||||
|
||||
### Sign-in required
|
||||
|
||||
**Yes** — the app requires a beenvoice account.
|
||||
|
||||
### Demo account (production server)
|
||||
|
||||
Ensure migrations through `0028_enable_public_demo_password` have run on the server reviewers will hit.
|
||||
|
||||
| Field | Value |
|
||||
|-------|--------|
|
||||
| **Username** | `demo@example.com` |
|
||||
| **Password** | `demo123` |
|
||||
|
||||
### Notes for Review
|
||||
|
||||
```
|
||||
beenvoice is a client for the beenvoice invoicing and time-tracking platform (web + API).
|
||||
|
||||
SIGN IN
|
||||
1. Open the app.
|
||||
2. Leave "Official" server selected (https://beenvoice.app) unless we specify otherwise in this note.
|
||||
3. Sign in with the demo account above.
|
||||
|
||||
WHAT TO TEST
|
||||
• Dashboard — sample invoices and stats are pre-seeded.
|
||||
• Timer tab — clock in, optionally pick client/description; on a physical device, Lock Screen Live Activity appears while a timer runs.
|
||||
• Invoices — list includes draft, sent, and paid examples.
|
||||
• Settings — profile, theme, optional app lock (PIN / Face ID).
|
||||
|
||||
ACCOUNT DELETION
|
||||
Settings → Delete account offers permanent in-app account deletion without contacting support.
|
||||
After a destructive confirmation, the server removes the account record, sessions, access keys,
|
||||
invoices, clients, businesses, recurring invoices, expenses and receipts, time entries, templates,
|
||||
audit records, and uploaded files. The app then removes the local account and returns to sign-in.
|
||||
The supplied demo account is shared, so please test deletion last if deletion verification is required.
|
||||
|
||||
APP LOCK
|
||||
Optional. Enable in Settings → App Lock. Face ID uses on-device biometrics only; no biometric data is sent to our servers.
|
||||
|
||||
LIVE ACTIVITY
|
||||
Requires a physical iPhone (not available in Simulator). Start a timer, lock the device, and check the Lock Screen / Dynamic Island.
|
||||
|
||||
SELF-HOSTED SERVERS
|
||||
Users may enter a custom server URL on sign-in. Review uses the official server only.
|
||||
|
||||
No in-app purchases. No ads.
|
||||
```
|
||||
|
||||
Update the official server URL in the note if you change `DEFAULT_API_URL` in `lib/config.ts`.
|
||||
|
||||
---
|
||||
|
||||
## App Privacy (Privacy Nutrition Labels)
|
||||
|
||||
Answer in App Store Connect → App Privacy. Adjust if you add analytics later.
|
||||
|
||||
### Data linked to the user
|
||||
|
||||
| Data type | Purpose | Collected | Linked | Tracking |
|
||||
|-----------|---------|-----------|--------|----------|
|
||||
| **Email address** | App functionality, account | Yes | Yes | No |
|
||||
| **Name** | App functionality, account | Yes | Yes | No |
|
||||
| **Other user content** (clients, invoices, time entries, business details) | App functionality | Yes | Yes | No |
|
||||
| **User ID** | App functionality | Yes | Yes | No |
|
||||
|
||||
### Data not collected for tracking
|
||||
|
||||
The app does **not** use data for tracking across apps/websites. No third-party analytics SDKs in the current build.
|
||||
|
||||
### Data collected but not linked (typically none)
|
||||
|
||||
If you only use on-device Face ID via `expo-local-authentication`, Apple treats biometrics as **not** collected by the developer—do **not** declare Face ID templates as collected data.
|
||||
|
||||
### Practice to select
|
||||
|
||||
- **Data Used to Track You:** None
|
||||
- **Data Linked to You:** Contact info, identifiers, user content (as above)
|
||||
- **Data Not Linked to You:** None (unless you add crash logs without account linkage)
|
||||
|
||||
---
|
||||
|
||||
## Age Rating Questionnaire (typical answers)
|
||||
|
||||
| Topic | Answer |
|
||||
|-------|--------|
|
||||
| Cartoon / fantasy violence | None |
|
||||
| Realistic violence | None |
|
||||
| Sexual content | None |
|
||||
| Profanity | None |
|
||||
| Drugs, alcohol, tobacco | None |
|
||||
| Gambling | None |
|
||||
| Horror | None |
|
||||
| Mature / suggestive themes | None |
|
||||
| Unrestricted web access | No (in-app browser not used for open web) |
|
||||
| User-generated content broadly distributed | No (invoice data is private to the account) |
|
||||
|
||||
Expected result: **4+**.
|
||||
|
||||
---
|
||||
|
||||
## Export Compliance
|
||||
|
||||
In App Store Connect encryption questions:
|
||||
|
||||
- **Uses encryption:** Yes (HTTPS/TLS for API)
|
||||
- **Exempt:** Yes — standard HTTPS only, qualify for exemption under mass-market encryption rules (same as most apps using TLS)
|
||||
|
||||
Confirm annually in Connect; no separate ERN needed for standard TLS-only apps in most cases.
|
||||
|
||||
---
|
||||
|
||||
## Screenshots (required sizes)
|
||||
|
||||
Capture from **iPhone 6.7"** (e.g. iPhone 15 Pro Max) and **6.5"** if you support older requirements. Xcode Simulator → Save Screenshot, or physical device.
|
||||
|
||||
Suggested screens (portrait):
|
||||
|
||||
1. **Sign-in** — brand, clean auth (optional; some teams skip)
|
||||
2. **Dashboard** — stats + recent invoices (demo account)
|
||||
3. **Timer** — running or ready to clock in
|
||||
4. **Invoices** — list with statuses
|
||||
5. **Invoice detail / edit** — line items
|
||||
6. **Settings** — theme + app lock (shows polish)
|
||||
|
||||
Minimum: **3 screenshots** per required device size.
|
||||
|
||||
Optional: iPad 12.9" if `supportsTablet: true` — use iPad simulator or “Run on iPad” with scaled iPhone UI.
|
||||
|
||||
---
|
||||
|
||||
## Build & submit
|
||||
|
||||
### Option A — Local Xcode (no EAS)
|
||||
|
||||
See **[IOS_LOCAL_RELEASE.md](./IOS_LOCAL_RELEASE.md)** for the full guide.
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
cp .ios-release.env.example .ios-release.env # once — add Team ID + API key
|
||||
bun run ios:release:upload # archive + upload to TestFlight
|
||||
```
|
||||
|
||||
Requires Xcode on macOS, Apple Developer membership, and an App Store Connect API key.
|
||||
|
||||
### Option B — EAS (Expo cloud build)
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
|
||||
# Production iOS build (auto-increments build number)
|
||||
eas build --platform ios --profile production
|
||||
|
||||
# Submit latest build to App Store Connect
|
||||
eas submit --platform ios --profile production
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Apple Developer Program membership
|
||||
- App record created in App Store Connect with bundle ID `com.beenvoice.app`
|
||||
- EAS credentials configured (`eas credentials`) — Option B only
|
||||
- Privacy Policy URL live and reachable
|
||||
|
||||
---
|
||||
|
||||
## Pre-submission checklist
|
||||
|
||||
- [ ] Legal pages live at Privacy Policy URL (HTTP 200, no auth wall)
|
||||
- [ ] Demo account works on production API (`demo@example.com` / `demo123`)
|
||||
- [ ] `eas build --profile production` succeeds
|
||||
- [ ] TestFlight smoke test on device (login, timer, invoices, app lock)
|
||||
- [ ] Live Activity tested on physical iPhone
|
||||
- [ ] App Privacy answers match actual data flows
|
||||
- [ ] In-app account deletion succeeds from Settings and returns to sign-in
|
||||
- [ ] Screenshots uploaded for required device sizes
|
||||
- [ ] Review notes include demo credentials and server URL
|
||||
- [ ] Export compliance answered
|
||||
- [ ] Version `1.0.0` matches `app.json` / Connect version field
|
||||
@@ -0,0 +1,276 @@
|
||||
# beenvoice-app architecture
|
||||
|
||||
Dense reference for the Expo 57 mobile companion. Talks to **beenvoice** over tRPC + better-auth. Requires a **development build** (not Expo Go) for widgets, SecureStore auth, and biometrics.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Framework | Expo 57, expo-router 57 (file-based routes) |
|
||||
| UI | React Native 0.85, `@expo/ui` (SwiftUI widgets) |
|
||||
| API | tRPC 11 + TanStack Query, SuperJSON |
|
||||
| Auth | better-auth + `@better-auth/expo` → `expo-secure-store` |
|
||||
| Types | `AppRouter` imported from `../beenvoice-web/src/server/api/root` |
|
||||
|
||||
## Boot sequence
|
||||
|
||||
```
|
||||
app/_layout.tsx
|
||||
SafeAreaProvider
|
||||
ThemeProvider ← AsyncStorage color mode
|
||||
BrandBackground + StatusBar
|
||||
AccountsProvider ← load accounts, active id, apiUrl
|
||||
AppServices ← key={activeAccountId:apiUrl}
|
||||
AuthProvider ← better-auth client (storagePrefix)
|
||||
TRPCProvider ← cookie header on /api/trpc
|
||||
RootNavigator
|
||||
session? → (app) | (auth)
|
||||
```
|
||||
|
||||
`AccountsProvider` blocks on `LoadingScreen` until AsyncStorage hydrates. `AuthProvider` + `TRPCProvider` **remount** when `activeAccountId` or `apiUrl` changes so each account uses isolated SecureStore session keys.
|
||||
|
||||
## Routing
|
||||
|
||||
### Auth group `app/(auth)/`
|
||||
|
||||
| Screen | File | Notes |
|
||||
|--------|------|-------|
|
||||
| redirect | `index.tsx` | → sign-in |
|
||||
| Sign in | `sign-in.tsx` | Server picker, deferred validation |
|
||||
| Register | `register.tsx` | REST register + sign-in + finalize account |
|
||||
| Forgot password | `forgot-password.tsx` | REST |
|
||||
| Reset password | `reset-password.tsx` | Deep link `beenvoice://reset-password?token=` |
|
||||
|
||||
### App group `app/(app)/`
|
||||
|
||||
`NativeTabs` (5 tabs) in `_layout.tsx`. Wrapped in `AppLockProvider` + `AppLockOverlay`.
|
||||
|
||||
| Tab | Screen | Features |
|
||||
|-----|--------|----------|
|
||||
| Dashboard | `index.tsx` | Stats, running timer chip, recent invoices |
|
||||
| Timer | `timer.tsx` | `TimeClockPanel` |
|
||||
| Entities | `entities/*` | Clients + businesses CRUD stacks |
|
||||
| Invoices | `invoices/*` | List, create, edit, status |
|
||||
| More | `more/*` | Expenses, reports, recurring invoices, time entries, settings |
|
||||
|
||||
Nested stacks: `entities/_layout.tsx`, `invoices/_layout.tsx`, `more/_layout.tsx`.
|
||||
|
||||
## Multi-account model
|
||||
|
||||
**Storage** — `lib/accounts.ts` (AsyncStorage):
|
||||
|
||||
| Key | Content |
|
||||
|-----|---------|
|
||||
| `beenvoice:accounts` | `SavedAccount[]` |
|
||||
| `beenvoice:active-account-id` | current account id or absent |
|
||||
| `beenvoice:draft-instance-url` | server URL before first login |
|
||||
|
||||
**Account id**: `{hostname}::{userId}` — host from instance URL without protocol/trailing slash.
|
||||
|
||||
**Auth storage prefix** (SecureStore, better-auth):
|
||||
|
||||
| Mode | Prefix |
|
||||
|------|--------|
|
||||
| Guest (signed out / adding account) | `beenvoice:guest` |
|
||||
| Per account | `beenvoice:auth:{accountId}` |
|
||||
|
||||
Keys written by `@better-auth/expo`: `{prefix}_cookie`, `{prefix}_session_data`, `{prefix}_last_login_method` (colons normalized to `_` in SecureStore). Large cookies are chunked (`\x01ba-chunks:N` + `.{i}` keys).
|
||||
|
||||
### Sign-in flow (critical)
|
||||
|
||||
1. User signs in while `AuthProvider` uses **guest** (or current) prefix.
|
||||
2. Session lands in that prefix's SecureStore.
|
||||
3. `finalizeAuthenticatedAccount()` (`lib/auth-storage.ts`):
|
||||
- `migrateAuthStorage(sourcePrefix → targetPrefix)` — copy session keys
|
||||
- `registerAccount()` — set active account, persist metadata
|
||||
4. `AuthProvider` remounts with account prefix; session already migrated → `RootNavigator` shows `(app)`.
|
||||
|
||||
Without migration, remounting loses the session and forces a second login.
|
||||
|
||||
### Account switcher
|
||||
|
||||
`components/AccountSwitcher.tsx` (header):
|
||||
|
||||
- **Switch**: `switchAccount(id)` → remount auth/tRPC with that account's URL + prefix (session must already exist in target prefix).
|
||||
- **Add account**: `signOut()` + `clearActiveAccount()` → auth stack on guest prefix.
|
||||
|
||||
## Server picker
|
||||
|
||||
`components/AuthServerPicker.tsx` + `lib/server-mode.ts`:
|
||||
|
||||
- **Official** — `DEFAULT_API_URL` (`https://beenvoice.app` in `lib/config.ts`)
|
||||
- **Self-hosted** — user URL, normalized via `lib/instance-url.ts` (adds `http://` for localhost/LAN)
|
||||
|
||||
`setInstanceUrl()` updates runtime API (`lib/config.ts` `setRuntimeApiUrl`) and draft or active account URL.
|
||||
|
||||
## API URL resolution
|
||||
|
||||
`lib/config.ts` priority:
|
||||
|
||||
1. Runtime override (`setRuntimeApiUrl` from AccountsContext)
|
||||
2. `EXPO_PUBLIC_API_URL` from `.env`
|
||||
3. Dev: Metro host IP + `:3000`
|
||||
4. `DEFAULT_API_URL`
|
||||
|
||||
## tRPC client
|
||||
|
||||
`lib/trpc.tsx`:
|
||||
|
||||
```ts
|
||||
httpBatchLink({
|
||||
url: `${apiUrl}/api/trpc`,
|
||||
transformer: SuperJSON,
|
||||
headers: () => ({ cookie: authClient.getCookie() }),
|
||||
})
|
||||
```
|
||||
|
||||
Query defaults: `staleTime: 30_000`, `retry: 1`. Usage: `import { api } from "@/lib/trpc"`.
|
||||
|
||||
## Offline mode
|
||||
|
||||
Read queries are cached per account + API URL in AsyncStorage (`lib/offline-cache.ts`). `TRPCProvider` restores that cache before mounting app screens, then persists successful query data for up to 7 days. SuperJSON is used for persistence so API payloads keep Date values and other transformed types.
|
||||
|
||||
`lib/network-status.ts` connects `expo-network` to TanStack Query's `onlineManager`. When the device is offline, queries pause instead of repeatedly failing; when connectivity returns, React Query reconnect behavior refreshes stale data. The better-auth Expo client also keeps its SecureStore session cache enabled so a previously signed-in account can get through app boot while offline, then reconcile with the server when connectivity returns.
|
||||
|
||||
Account removal clears that account's persisted query cache alongside SecureStore auth and time-clock preferences.
|
||||
|
||||
Current offline scope: previously loaded dashboard, invoices, clients, businesses, expenses, reports, recurring invoices, and time entries can be viewed offline from cache. Mutations still require the API; timer actions, invoice edits/status changes, receipt uploads, reminders, emails, and onboarding writes are not persisted as an offline queue yet because they need conflict and side-effect rules.
|
||||
|
||||
## App lock (per account)
|
||||
|
||||
`lib/app-lock.ts` — SecureStore keys scoped by `activeAccountId`:
|
||||
|
||||
- `beenvoice:app-lock:{id}:enabled|pin|biometric`
|
||||
- One-time migration from legacy global keys `beenvoice_app_lock_*`
|
||||
|
||||
`contexts/AppLockContext.tsx`:
|
||||
|
||||
- Hydrates on account change
|
||||
- Locks when returning from background (if enabled)
|
||||
- PIN 4–6 digits; Face ID / Touch ID via `expo-local-authentication`
|
||||
- Only active inside `(app)/_layout.tsx` — auth screens never locked
|
||||
|
||||
UI: `AppLockOverlay.tsx`, `PinPrompt.tsx`, settings toggles.
|
||||
|
||||
## Time clock
|
||||
|
||||
`components/time-clock/TimeClockPanel.tsx`:
|
||||
|
||||
- Client required; description optional (defaults to **"Clock In"** via `lib/time-clock.ts`)
|
||||
- Optional invoice, hourly rate, backdated start
|
||||
- `clockOut` sends optional description update
|
||||
- Syncs iOS Live Activity when timer metadata changes (client, invoice, description); timer uses native `timerInterval` on the lock screen
|
||||
|
||||
### Live Activity
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `widgets/TimeClockActivity.tsx` | SwiftUI widget (`expo-widgets`); must keep all UI **inside** the `"widget"` function (babel preset serializes only that) |
|
||||
| `lib/time-clock-live-activity.ts` | `syncTimeClockLiveActivity`, `endTimeClockLiveActivity` |
|
||||
| `app.json` | Plugin `expo-widgets`, app group `group.com.beenvoice.app` |
|
||||
|
||||
Does not work in Expo Go — use `bun run ios` dev build.
|
||||
|
||||
## Theming
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `lib/beenvoice-theme.ts` | Light tokens mirrored from web `globals.css` |
|
||||
| `lib/theme-palette.ts` | Light + dark `ThemeColors` |
|
||||
| `constants/theme.ts` | spacing, radii, fonts (Playfair / Inter / SpaceMono) |
|
||||
| `contexts/ThemeContext.tsx` | system / light / dark → AsyncStorage `beenvoice:color-mode` |
|
||||
| `components/BrandBackground.tsx` | Grid + animated blob |
|
||||
| `lib/use-themed-styles.ts` | Memoized StyleSheet factory |
|
||||
|
||||
## Contexts summary
|
||||
|
||||
| Context | File |
|
||||
|---------|------|
|
||||
| Auth | `contexts/AuthContext.tsx` |
|
||||
| Accounts | `contexts/AccountsContext.tsx` |
|
||||
| App lock | `contexts/AppLockContext.tsx` |
|
||||
| Theme | `contexts/ThemeContext.tsx` |
|
||||
|
||||
## `lib/` module index
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `accounts.ts` | Account registry types + AsyncStorage |
|
||||
| `auth-storage.ts` | Session migration, `finalizeAuthenticatedAccount` |
|
||||
| `auth-api.ts` | REST register / forgot / reset |
|
||||
| `config.ts` | API URL |
|
||||
| `instance-url.ts` | URL normalization + persistence |
|
||||
| `server-mode.ts` | Official vs self-hosted |
|
||||
| `trpc.tsx` | tRPC provider |
|
||||
| `app-lock.ts` | Per-account PIN storage |
|
||||
| `form-validation.ts` | Validators + `useFieldVisibility` |
|
||||
| `format.ts` | Currency, dates |
|
||||
| `invoice-status.ts` | Status colors |
|
||||
| `invoice-number.ts` | Number generation |
|
||||
| `time-clock.ts` | Timer formatting, clock-out copy |
|
||||
| `time-clock-live-activity*.ts` | Live Activity bridge |
|
||||
| `tab-layout.ts`, `tab-bar-insets.ts`, `top-chrome-insets.ts` | Layout metrics |
|
||||
|
||||
## Components layout
|
||||
|
||||
```
|
||||
components/
|
||||
├── ui/ # Button, Card, Input, SelectField, DateTimeField
|
||||
├── time-clock/ # TimeClockPanel
|
||||
├── clients/ # ClientForm
|
||||
├── businesses/ # BusinessForm
|
||||
├── invoices/ # LineItemEditor
|
||||
├── AuthServerPicker, AccountSwitcher, TopChrome, AppLockOverlay, Logo, …
|
||||
```
|
||||
|
||||
## Native config
|
||||
|
||||
**`app.json`**
|
||||
|
||||
- `scheme`: `beenvoice`
|
||||
- `bundleIdentifier`: `com.beenvoice.app`
|
||||
- Plugins: dev-client, router, secure-store, widgets, local-authentication
|
||||
- iOS Face ID usage strings; custom `beenvoice.icon`
|
||||
|
||||
**`eas.json`**
|
||||
|
||||
- Profiles: development, preview, production
|
||||
- `cli.appVersionSource`: remote
|
||||
- User does not require EAS for local `expo run:ios`
|
||||
|
||||
**Metro**: port **8082** (`package.json` scripts) to avoid collisions.
|
||||
|
||||
## Deep links
|
||||
|
||||
| URL | Handler |
|
||||
|-----|---------|
|
||||
| `beenvoice://reset-password?token=…` | `reset-password.tsx` |
|
||||
| `beenvoice://timer` | Live Activity tap → timer tab |
|
||||
|
||||
## Development commands
|
||||
|
||||
```bash
|
||||
bun run ios # build + run simulator, Metro :8082
|
||||
bun run start # Metro only (--dev-client --port 8082)
|
||||
bunx expo prebuild --platform ios --clean # after native dep / icon changes
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause |
|
||||
|---------|----------------|
|
||||
| `PlatformConstants` / runtime not ready | Wrong Metro port, stale native build, or Expo Go instead of dev client |
|
||||
| Login twice | Session not migrated — see `finalizeAuthenticatedAccount` |
|
||||
| Live Activity blank | Widget UI outside `"widget"` function; rebuild native |
|
||||
| API unreachable on device | Use LAN IP in `EXPO_PUBLIC_API_URL`, not `localhost` |
|
||||
| Auth fails on device | `BETTER_AUTH_URL` on server must match reachable host |
|
||||
|
||||
## Server dependency
|
||||
|
||||
Requires beenvoice with:
|
||||
|
||||
- `@better-auth/expo` in `src/lib/auth.ts`
|
||||
- `trustedOrigins` including `beenvoice://` and `exp://`
|
||||
- Postgres running (`docker compose -f docker-compose.dev.yml up -d db`)
|
||||
|
||||
See [beenvoice-web/docs/ARCHITECTURE.md](../../beenvoice-web/docs/ARCHITECTURE.md).
|
||||
@@ -0,0 +1,88 @@
|
||||
# Local iOS release (no EAS)
|
||||
|
||||
Archive and upload **beenvoice** to App Store Connect using Xcode on your Mac — no Expo Application Services (EAS) subscription required.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS with **Xcode** (same major version you use for development)
|
||||
- **Apple Developer Program** membership
|
||||
- App record in [App Store Connect](https://appstoreconnect.apple.com) with bundle ID `com.beenvoice.app`
|
||||
- **Distribution** signing set up in Xcode (automatic signing + team is enough for most cases)
|
||||
- [App Store Connect API key](https://appstoreconnect.apple.com/access/integrations/api) (for upload only)
|
||||
|
||||
## One-time setup
|
||||
|
||||
```bash
|
||||
cd beenvoice-app
|
||||
cp .ios-release.env.example .ios-release.env
|
||||
```
|
||||
|
||||
Edit `.ios-release.env`:
|
||||
|
||||
| Variable | Where to find it |
|
||||
|----------|------------------|
|
||||
| `APPLE_TEAM_ID` | [developer.apple.com/account](https://developer.apple.com/account) → Membership → Team ID |
|
||||
| `APP_STORE_CONNECT_API_KEY_ID` | App Store Connect → Users and Access → Integrations → Keys |
|
||||
| `APP_STORE_CONNECT_API_ISSUER_ID` | Same page (Issuer ID at top) |
|
||||
| `APP_STORE_CONNECT_API_KEY_PATH` | Path to downloaded `AuthKey_XXXXXX.p8` |
|
||||
| `EXPO_PUBLIC_API_URL` | Production API URL baked into the release bundle |
|
||||
|
||||
Optional: store the `.p8` in `~/.appstoreconnect/private_keys/` (never commit it).
|
||||
|
||||
Open the iOS project once in Xcode and confirm **Signing & Capabilities** succeeds for targets **beenvoice** and **ExpoWidgetsTarget**.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Archive + export signed IPA to dist/ios-release/export/
|
||||
bun run ios:release
|
||||
|
||||
# Archive + export + upload to App Store Connect (TestFlight)
|
||||
bun run ios:release:upload
|
||||
```
|
||||
|
||||
### Flags (pass through to the script)
|
||||
|
||||
```bash
|
||||
bash scripts/ios-release.sh --archive-only # .xcarchive only
|
||||
bash scripts/ios-release.sh --export-only --upload # re-upload existing archive
|
||||
bash scripts/ios-release.sh --no-prebuild # skip expo prebuild
|
||||
bash scripts/ios-release.sh --no-bump # don't increment build number
|
||||
```
|
||||
|
||||
With `IOS_BUMP_BUILD=1` in `.ios-release.env`, each run bumps `CFBundleVersion` via `agvtool` (recommended for repeated TestFlight uploads).
|
||||
|
||||
## What the script does
|
||||
|
||||
1. `expo prebuild --platform ios` (unless `--no-prebuild`)
|
||||
2. `pod install`
|
||||
3. Optional build-number bump (`agvtool`)
|
||||
4. `xcodebuild archive` (Release, generic iOS device)
|
||||
5. `xcodebuild -exportArchive` → App Store IPA
|
||||
6. `xcrun altool --upload-app` (with `--upload` only)
|
||||
|
||||
Artifacts land in `dist/ios-release/` (gitignored).
|
||||
|
||||
## After upload
|
||||
|
||||
1. App Store Connect → **TestFlight** — wait for “Processing” to finish
|
||||
2. Smoke-test on device
|
||||
3. Submit for App Store review when ready
|
||||
|
||||
See also [APP_STORE_CONNECT.md](./APP_STORE_CONNECT.md) for metadata, screenshots, and review notes.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Dev client:** `expo-dev-client` is in the native project today. Store builds still work, but the binary includes the dev client shell. For a slimmer production binary, remove that plugin and re-run prebuild before release (or maintain a separate `app.config` variant).
|
||||
- **Manual upload:** After `bun run ios:release`, drag the IPA into Apple’s [Transporter](https://apps.apple.com/app/transporter/id1450874784) app instead of using `--upload`.
|
||||
- **CI:** Run the same script on a Mac runner (GitHub `macos-latest`, etc.) with secrets injected as env vars instead of `.ios-release.env`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| No signing certificate | Xcode → Settings → Accounts → Download Manual Profiles; or open project and enable automatic signing |
|
||||
| `pod install` fails | `cd ios && pod repo update && pod install` |
|
||||
| Upload auth error | Verify API key has **Developer** access; check Key ID, Issuer ID, and `.p8` path |
|
||||
| Duplicate build number | Enable `IOS_BUMP_BUILD=1` or bump `CURRENT_PROJECT_VERSION` in Xcode |
|
||||
| Widget extension signing | Both **beenvoice** and **ExpoWidgetsTarget** need the same team |
|
||||
@@ -0,0 +1,12 @@
|
||||
# beenvoice-app documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [ARCHITECTURE.md](./ARCHITECTURE.md) | Routing, contexts, auth/accounts, tRPC, app lock, Live Activity, theming |
|
||||
| [../README.md](../README.md) | Setup, run, troubleshooting |
|
||||
| [../AGENTS.md](../AGENTS.md) | Agent conventions |
|
||||
|
||||
## Related
|
||||
|
||||
- [beenvoice-web docs](../../beenvoice-web/docs/README.md) — server API and web app
|
||||
- [Workspace README](../../README.md) — full-stack layout
|
||||
Reference in New Issue
Block a user