# {{capProjectName}} — mobile (Expo + Voltro)

An [Expo](https://expo.dev) (React Native) app that is the **third consumer** of
your Voltro api — the same typed hooks you use on the web (`useSubscription` /
`useMutation`), offline-first by default, with typed deep links and device
registration for push.

> **Expo owns the dev loop, not `voltro dev`.** This app is started with
> `expo start` / `expo run:ios`, not `voltro dev`. The sibling api still runs
> under `voltro dev`; this app connects to it over the network.

## Run it

```sh
# 1. In one shell — the api (scaffold it with --api=api-backend):
voltro dev

# 2. In another shell — the app. `pnpm ios` runs `voltro codegen` first, which
#    writes this app's api binding from voltro.mobile.ts.
pnpm ios        # or: pnpm android · pnpm start (Expo Go)

# The ws URL is derived from the host running Metro, so a device on the same
# wifi reaches your machine. Override it for a deployed api:
#   export EXPO_PUBLIC_API_WS_URL="wss://api.example.com/ws"
#   export EXPO_PUBLIC_API_PORT=4000   # if your api is not on 4000
```

## What's here

| Path | What |
|---|---|
| `src/lib/api.ts` | the **typed hook binding** — `createHooks<AppProcedures>('app')`, imported by every screen |
| `src/lib/deeplinks.ts` | the typed deep-link table + pure resolver (**tested**) |
| `src/lib/sync.ts` | the background-sync decision layer (**tested**) |
| `src/lib/notifications.ts` | build a `_voltro_devices` row from a push token (**tested**) |
| `src/client.ts` | the Voltro client: generated api binding + supervisor + persistence |
| `src/persistence.ts` | RN key-value with the secure/plain split |
| `src/app/*` | Expo Router screens (list, detail, settings) |
| `voltro.mobile.ts` | which api(s) this app consumes |

## Honest status (read before you expect the loop to work on a device)

The client is wired: `voltro codegen` writes this app's api binding from
`voltro.mobile.ts`, and `src/client.ts` connects it through the SAME reconnect
supervisor the web client uses, with persisted stores hydrated before the first
render. The start scripts run codegen, so `pnpm ios` needs no separate step.

What is NOT verified in this repository is the one thing only a device can tell
you: that the loop runs under Metro on a simulator or a phone. There is no
iOS/Android toolchain in the framework's sandbox, so booting the app is your
Expo/EAS CI step. The unit tests here run without one and deliberately do not
pretend otherwise.

Two things to check first if a device shows loading forever:

- **`localhost` on a phone is the phone.** The ws URL is derived from the host
  running Metro (`resolveDevWsUrl`), which works on a device on the same wifi.
  Set `EXPO_PUBLIC_API_WS_URL` to point somewhere else.
- **The api must be running.** `voltro dev` in the project root boots it; this
  app is deliberately not part of that orchestration, because Expo owns its own
  dev server.

## Tests

```sh
pnpm test        # the pure lib — no simulator needed
pnpm typecheck   # tsc over the app (needs Expo/RN types installed)
```

### Why this app's `tsconfig.json` overrides Expo's base

Three overrides, each for a reason that shows up as a hard error without it:

- **`moduleResolution: "bundler"`** — Expo's base sets `node` (node10), which
  TypeScript 6 rejects outright. `bundler` is also the honest description of
  Metro.
- **`types: ["node", "react"]`** — `process.env.EXPO_PUBLIC_*` is Expo's own
  idiom for build-time config, and `process` needs a type. Listing the types
  explicitly keeps everything else out.
- **`@types/node` as a devDependency** — the other half of the same line.

Run `pnpm typecheck` after `voltro codegen`, in that order: the generated api
binding is a real import and the app does not typecheck without it.
