# ScanReceipts — iOS App

Native SwiftUI client for the same backend the web app (`../..`) already
runs. There is no separate mobile backend and no separate database — this
app authenticates against the same Next.js API (`/api/**`) and the same
Postgres `users`/`receipts`/`projects` tables. A Pro account bought on the
web is a Pro account in the app the moment you log in with the same
credentials, because both clients read `users.plan` / `users.expiresAt` from
the identical row.

Written from the [architecture plan](../../docs/ios-app-plan.html) (also
published as a Claude artifact during planning) — read that first for the
*why*; this file is the *how*.

## Status

This scaffold was generated without access to Xcode/macOS (built on Windows),
so nothing here has been compiled yet. What exists:

- **Backend (already shipped, in the main repo, not this folder):** a
  Bearer-token auth path alongside the existing cookie/session auth, so a
  native client without a cookie jar can authenticate. See
  `src/lib/auth/session.ts` (`getCurrentUser(request)`), `src/lib/auth/csrf.ts`
  (`isCsrfExempt`), and the `X-Client: ios` header handling in
  `src/app/api/auth/login/route.ts`. Verified end-to-end against a live dev
  server (login → Bearer-authenticated `/api/receipts` round-trip → logout).
- **Sign in with Apple, backend + app, fully wired** — `POST /api/auth/apple`
  (`src/app/api/auth/apple/route.ts`) verifies the identity token
  (`src/lib/auth/apple.ts`, hand-rolled RS256/JWK verification against
  `node:crypto`, no new dependency) and issues a session exactly like
  `/api/auth/login`; `AppleSignInButton.swift` calls it via
  `AppState.signInWithApple`. The token-verification logic itself (signature,
  issuer, audience, expiry, tamper/forged-key rejection) was proven correct
  with a 9-case self-signed-JWT test — there's no way to get a REAL Apple
  identity token without a physical device and an enrolled Apple Developer
  account, so that's the strongest verification possible from here. The route
  wiring (HTTP status codes, CSRF exemption, rate limiting) was NOT verified
  live end-to-end: the local dev server broke (every route, including
  pre-existing ones, started 404ing) partway through testing because another
  process was concurrently editing files in this repo (`src/lib/schema/receipt.ts`
  changed mid-session) — not something caused by this change. `npx tsc
  --noEmit` is clean. Re-run a live check (`curl -X POST .../api/auth/apple`)
  once the repo is quiet before relying on this in production.
- **Apple In-App Purchase, backend + app, real (not stubbed) purchase
  engine** — `Features/Settings/StoreKitPurchaseService.swift` is a real
  StoreKit 2 implementation (product loading with actual App-Store-localized
  prices, purchase, transaction verification, restore) wired into
  `PaywallView`; `Configuration.storekit` lets it be tested in the Simulator
  with zero Apple Developer account (see setup below). Server-side,
  `POST /api/webhooks/apple` (`src/app/api/webhooks/apple/route.ts`,
  `src/lib/billing/appleIAP.ts`) verifies App Store Server Notifications V2
  using Apple's own official `@apple/app-store-server-library` (full x5c
  certificate-chain verification against `src/lib/billing/certs/AppleRootCA-G3.cer`
  — downloaded from apple.com, NOT hand-rolled crypto, deliberately unlike
  the Sign-in-with-Apple JWKS check above, because chain-of-trust validation
  is a much easier place to get subtly wrong) and grants/revokes
  `users.plan` exactly like the existing Stripe webhook does. A purchase is
  tied back to the account via `User.appleAccountToken` (iOS) /
  `userIdFromAppAccountToken` (backend) — a losslessly-reversible UUID
  built from the user's own id, no extra stored mapping needed.
  **Verified:** the full certificate-chain verification path was proven
  against a self-built 3-tier test CA (root → intermediate → leaf, signed
  with the same Apple-specific X.509 extensions the library requires) —
  valid chain accepted, tampered payload/untrusted root/wrong bundle
  ID/wrong environment all correctly rejected (11/11 checks), and the
  appAccountToken round-trip was verified byte-for-byte. **Not verified:**
  an actual end-to-end purchase, since that needs a real device, an
  enrolled Apple Developer account, and real App Store Connect products —
  none of which exist yet. `npx tsc --noEmit` is clean.
- **Sign in with Apple, backend + app, fully wired** — `POST /api/auth/apple`
  (`src/app/api/auth/apple/route.ts`) verifies the identity token
  (`src/lib/auth/apple.ts`, hand-rolled RS256/JWK verification against
  `node:crypto`, no new dependency) and issues a session exactly like
  `/api/auth/login`; `AppleSignInButton.swift` calls it via
  `AppState.signInWithApple`. The token-verification logic itself (signature,
  issuer, audience, expiry, tamper/forged-key rejection) was proven correct
  with a 9-case self-signed-JWT test — there's no way to get a REAL Apple
  identity token without a physical device and an enrolled Apple Developer
  account, so that's the strongest verification possible from here. The route
  wiring (HTTP status codes, CSRF exemption, rate limiting) was NOT verified
  live end-to-end: the local dev server broke (every route, including
  pre-existing ones, started 404ing) partway through testing because another
  process was concurrently editing files in this repo (`src/lib/schema/receipt.ts`
  changed mid-session) — not something caused by this change. `npx tsc
  --noEmit` is clean. Re-run a live check (`curl -X POST .../api/auth/apple`)
  once the repo is quiet before relying on this in production.
- **This app's networking/model foundation** (`ScanReceipts/Networking`,
  `ScanReceipts/Models`, `ScanReceipts/App`): API client, Keychain token
  storage, and Codable models matching the backend's JSON exactly.
- **Visual design** (`ScanReceipts/Design/`): the web app's actual "Zenith
  Silver" design system (`DESIGN (1).md`, `tailwind.config.ts`), ported —
  same palette, same 4px spacing scale, 0px corner radius everywhere, no
  shadows, and the SAME font files (`Resources/Fonts/*.ttf` — real Hanken
  Grotesk / Inter / JetBrains Mono variable fonts pulled from the canonical
  google/fonts repo, registered via `UIAppFonts`, not a system-font
  approximation). `ZenithStatusBadge` ports the web dashboard's 3-tier
  receipt-status badge (`StatusBadge.tsx`) color-for-color. Every screen in
  `Features/` consumes this — see `Design/*.swift`'s doc comments for the
  full component list (`ZenithButtonStyle`, `ZenithTextField`, `ZenithCard`,
  `ZenithDivider`, `ZenithStatusBadge`, `ZenithChip`). The two native
  controls Apple doesn't allow restyling (`SignInWithAppleButton`, the
  VisionKit camera / `UIActivityViewController` share sheet) keep their own
  required system appearance — that's an App Store requirement, not a gap.
- **Feature screens** (`ScanReceipts/Features/*`): all four areas are built
  against that foundation — Auth (login/signup/email-verification/Sign in
  with Apple UI), Scan (VisionKit document camera + Photos fallback + review
  form), Receipts (list/search/detail-edit/delete/export + share sheet), and
  Settings (profile/Pro status/projects/change password/delete
  account/paywall). Cross-checked for naming collisions and that every type
  `MainTabView.swift`/`RootView.swift` depend on (`AuthFlowView`,
  `ScanTabView`, `ReceiptsListView`, `SettingsView`) exists with a working
  zero-arg initializer. Not yet opened in Xcode. Expect the first
  `xcodegen generate` + build on a Mac to surface small issues (an unused
  import, a SwiftUI modifier from a newer/older SDK than assumed) — normal
  for ~4,100 lines of Swift written without a compiler in the loop; nothing
  here should need a structural rewrite.

## First-time setup (macOS)

```bash
brew install xcodegen
cd app/ios
xcodegen generate
open ScanReceipts.xcodeproj
```

In Xcode: **Signing & Capabilities** → set your own Team. `project.yml`
leaves `DEVELOPMENT_TEAM` blank on purpose — set it locally, don't commit it.

**To test in-app purchases without an Apple Developer account or real
money:** `Configuration.storekit` defines the three Pro products locally.
Enable it once per scheme: **Product → Scheme → Edit Scheme → Run →
Options → StoreKit Configuration → `Configuration.storekit`**. Purchases in
the Simulator then run against this local file — no App Store Connect
product configuration or real payment is involved. This is a genuinely
separate thing from the *real* products a shipped build needs registered in
App Store Connect with the exact same product identifiers (see
`PurchaseService.swift`'s `PurchaseProductID`).

Point the app at a real backend to test against by editing
`ScanReceipts/Networking/APIEnvironment.swift`: `.local` targets
`http://localhost:3901` (the web repo's `dev-verify` launch config — start it
with the web app's own tooling, a plain `npm run dev` on port 3000 also
works if you change the port here), `.production` is a placeholder domain to
replace before shipping.

## Architecture

```
ScanReceipts/
  App/              Entry point, AppState (auth), root/tab routing
  Networking/        APIClient, per-resource API namespaces, Keychain
  Models/            Codable structs mirroring src/lib/schema/*.ts
  Support/           Small standalone helpers (ISO8601 date parsing, ...)
  Features/
    Auth/            Login, signup, email-verification-pending, Sign in with Apple (fully wired — see below)
    Scan/            VisionKit camera capture → AI extraction → review → save
    Receipts/        List, detail/edit, delete, export (CSV/XLSX/PDF)
    Settings/         Profile, plan/Pro status, projects (folders), logout, delete account
```

One rule that keeps this tree maintainable: **feature code never calls
`URLSession` or reads the Keychain directly.** Everything goes through
`Networking/*API.swift` (`AuthAPI`, `ReceiptsAPI`, `ProjectsAPI`, `ExportAPI`)
and `AppState`. If a screen needs a new backend call, add it to the relevant
`*API.swift` file rather than reaching around it.

## What's deliberately NOT done yet

- **An actual App Store Connect listing.** The purchase engine and webhook
  are both real, but there is still no Apple Developer Program enrollment,
  no App Store Connect app record, and no real product configuration — so
  `APPLE_APP_APPLE_ID` stays unset (see `.env.example`) and real-money
  purchases genuinely cannot happen yet. `Configuration.storekit` covers
  local Simulator testing in the meantime.
- **Push notifications** for "scan finished" — not built; a nice-to-have per
  the plan, not required for a working v1.
- **iPad layout** — `project.yml` targets iPhone only (`TARGETED_DEVICE_FAMILY: "1"`).

## Testing against the shared database

Because this app and the web dashboard hit the same API, the fastest way to
sanity-check a screen while developing is: create/verify a user via the web
app's normal signup flow (or `node scripts/create-admin.ts` in the main repo
for an instant Pro account), then log into the iOS app (Simulator, `.local`
environment) with the same credentials. Anything synced from one side shows
up on the other on next refresh — there is no separate mobile dataset to
seed.
