Launching the app¶
There are several ways to run this app, and one command that reaches all of them:
That opens a menu. Every entry is documented below, and every entry is also a
direct argument, so pnpm launch wifi skips the menu. The script lives at
scripts/launch.sh
and runs the same commands this page describes, with the preflight checks
already built in.
Which mode do I want?¶
| I want to... | Mode | Command |
|---|---|---|
| See a change now, no phone involved | iOS Simulator | pnpm launch sim |
| Check labels, traits, and hit targets | Simulator with Inspector | pnpm launch a11y |
| Test on real hardware, real VoiceOver | Wi-Fi to an iPhone | pnpm launch wifi |
| Put a build in a tester's hands | TestFlight | pnpm launch testflight |
| Reinstall after a native change | USB install | pnpm launch device |
| Check Android | Android emulator | pnpm launch android |
| Reconnect a dev client that is already on | Metro only | pnpm launch metro |
| Know whether a build will pass before it runs | Preflight | pnpm launch check |
This app does not run in Expo Go. It uses native modules (audio, video,
reanimated), so it needs a custom dev client. That client gets built into the
simulator by pnpm launch sim, and onto a phone by pnpm launch device. Once
it is installed, JavaScript changes hot reload and no rebuild is needed.
iOS Simulator¶
This builds the dev client into the simulator, installs it, and starts Metro in the same terminal. The first run takes a few minutes because it compiles the native project. Later runs are warm and take seconds.
While Metro runs:
| Key | What it does |
|---|---|
r |
Reload the app |
j |
Open React Native DevTools (console, network, element inspector) |
m |
Toggle the in-app dev menu |
i |
Open on iOS |
a |
Open on Android |
The backend is optional. Without it the app runs and the Settings screen shows "Backend status" as unreachable. To run it alongside, open a second terminal:
The simulator shares the Mac's network, so localhost resolves and no address
configuration is needed.
Picking a specific simulator¶
pnpm ios uses the last booted device. To choose another:
xcrun simctl list devices available # see what is installed
cd apps/mobile && pnpm exec expo run:ios --device "iPhone SE (3rd generation)"
The iPhone SE is worth keeping around. It is the smallest supported display (375x667) and the reference device for the one-screen rule, so it is where a layout breaks first.
Simulator with Accessibility Inspector¶
Same as the simulator mode, and it also opens Xcode's Accessibility Inspector and the Simulator ahead of the build.
In the Inspector, set the target dropdown at the top left to Simulator, then to the running app. Two tools matter:
- The pointer icon inspects one element at a time and shows its label, value, traits, and frame. This is the fastest way to answer "does this control have a real label?"
- Audit runs a static pass over the current screen and flags missing labels, small hit targets, and contrast problems.
The Inspector reports what an element exposes. It does not report reading order, focus behavior, or whether an announcement actually fires. Those need the manual VoiceOver pass in Accessibility. Treat this mode as the fast check between releases, not a replacement for it.
Wi-Fi to a physical iPhone¶
This is the everyday loop for real-device work: install the dev client once over USB, then iterate over Wi-Fi with no cable and no rebuilds.
Once per phone¶
Connect the iPhone by USB, unlock it, and tap Trust. Select the phone when
prompted. If the build asks for a signing team, open
apps/mobile/ios/SeniorHealthApp.xcworkspace in Xcode, select the
SeniorHealthApp target, and set your team under Signing & Capabilities.
Then, in Xcode, open Window > Devices and Simulators, select the iPhone, and check Connect via network. After that even native rebuilds run without the cable.
Every session after that¶
This runs the preflight checks below, prints the Mac's LAN IP, then starts two servers together:
- the FastAPI backend on
0.0.0.0:8000, so the phone can reach it (the default127.0.0.1binding is reachable only from the Mac) - Metro bound to the LAN with
--dev-client, with a QR code
Open the installed Senior Health App on the phone. It reconnects on its own;
scan the QR code if it does not. The app derives the backend address from the
Metro URL, so there is nothing to configure. Edit a file, save, and the change
appears on the phone. Ctrl+C stops both servers.
Why the Wi-Fi path fails, and how to tell¶
The connection needs the phone to reach two ports on the Mac, 8081 for Metro and
8000 for the backend. Almost every failure is one of these, in rough order of how
often it happens. pnpm launch wifi checks the first four before starting
anything.
| Symptom | Cause | Fix |
|---|---|---|
| App opens, spins, "Could not connect to the server" | macOS firewall is blocking incoming connections to node |
Allow node when prompted, or System Settings > Network > Firewall > Options |
| Nothing connects, no prompt appears | A VPN is up on the Mac. The phone's packets never reach it | Disconnect the VPN for LAN work |
| Metro starts on port 8082 and the phone looks for 8081 | Another Metro is still running from an earlier session | lsof -nP -iTCP:8081 -sTCP:LISTEN, then kill the old process |
| Backend refuses to start | Port 8000 is taken, usually by a local docs server | Stop it, or run docs on 8001 with pnpm docs:serve |
| App connects to Metro, backend shows unreachable | The backend is bound to 127.0.0.1 instead of 0.0.0.0 |
Use pnpm dev:backend:lan, which pnpm launch wifi does for you |
| Phone and Mac are both "on Wi-Fi" but cannot see each other | A guest SSID, or client isolation on the router | Put both on the main network, not the guest one |
| Same, on a corporate or hotel network | Client isolation again, and it is usually not something you can turn off | Use USB, or a phone hotspot with both devices joined to it |
| Worked yesterday, not today | DHCP handed the Mac a new LAN IP | Restart pnpm launch wifi; it prints the current IP and Metro re-advertises |
| Mac is on Ethernet, phone on Wi-Fi | Fine if both are the same subnet, broken if the router keeps them apart | Check the printed IP is on the phone's subnet, or move the Mac to Wi-Fi |
| App is stale after a native change | The dev client is a native binary. New native modules, plugin changes, permissions, icons, and splash all need a rebuild | pnpm launch device |
When nothing else explains it, open Safari on the phone and go to
http://<the printed IP>:8000/health. A JSON response means the network is fine
and the problem is the app. No response means the problem is the network, the
firewall, or the binding, and the table above applies.
Falling back to USB always works, because it does not use the network at all.
Testers who are not on your Wi-Fi¶
A LAN address is useless to anyone outside the house. Give the backend a stable
public hostname with Backend over Cloudflare Tunnel, and put
that hostname in EXPO_PUBLIC_API_URL before building.
TestFlight¶
The script runs the checks first, then the two commands, and asks before it submits. What it does, in order:
- Confirms
easis installed and signed in. - Warns if
EXPO_PUBLIC_API_URLis unset inapps/mobile/.env. Testers are not on your Wi-Fi, so a build without it ships pointing at a laptop nobody can reach, and the only fix is another build. - Warns if
EXPO_PUBLIC_DEV_AUTO_LOGINis still set, which would sign testers in as the seeded dev account. - Runs
pnpm expo:doctor. Ten seconds here beats finding dependency drift ten minutes into a cloud build. - Runs the build, from
apps/mobile. - Offers to submit.
By hand it is:
cd apps/mobile
eas build --profile production --platform ios # store-signed, auto-increments the build number
eas submit --platform ios --latest # uploads to App Store Connect
Processing on Apple's side takes 5 to 20 minutes. eas.json assigns the
Tester group on submit, so testers are notified without any App Store Connect
clicking. Every build is universal, so the same build installs on iPad.
The profiles in apps/mobile/eas.json:
| Profile | What it is for |
|---|---|
development |
Dev client, internal distribution, includes an iOS simulator build |
preview |
Production-like build, internal distribution, no store signing |
production |
Store-signed, auto-incrementing build number, what TestFlight uses |
Never run EAS from the root¶
Every eas command runs from apps/mobile. Never from the repo root.
Running one at the root makes Expo scaffold a second, empty app there: a root
app.json, eas.json, tsconfig.json, and ios/, pointing at a different EAS
project under a personal account. That phantom app has native files and no
JavaScript entry point, so the build gets all the way to the end and then fails
with "Unknown error. See logs of the Bundle JavaScript build phase". It reads
like an app bug. It is not one, and diagnosing it costs a full cloud build.
pnpm launch testflight and pnpm launch check both detect those stray root
files and offer to delete them.
To tell a real app bug from this trap, run npx expo export --platform ios in
apps/mobile. If it produces a bundle, the app is fine and the build was simply
started in the wrong directory.
Android emulator¶
Start an emulator in Android Studio first, or the build has nothing to install
to. adb devices should list it. Everything else matches the iOS simulator flow.
Metro only¶
Starts Metro with --dev-client and nothing else. Use it when the dev client is
already installed and only JavaScript changed, which is the common case. It skips
the native build entirely, so it starts in seconds.
Preflight checks¶
Runs pnpm expo:doctor and pnpm verify (types, lint, tests, formatting), and
cleans up a phantom root scaffold if it finds one. Worth running before any EAS
build, since the same failures cost minutes in the cloud instead of seconds here.
Where the commands live¶
The launcher wraps scripts that all live in the root package.json, so they can
be run directly from the repo root, or from anywhere with pnpm -w.
| Command | What it does |
|---|---|
pnpm launch |
The menu described on this page |
pnpm ios |
Build and run in the iOS simulator |
pnpm android |
Build and run in the Android emulator |
pnpm ios:device |
Build and install on a USB-connected iPhone |
pnpm deploy:iphone |
LAN backend plus LAN Metro, prints the Mac's IP |
pnpm dev:backend |
Backend only, on 127.0.0.1:8000 |
pnpm dev:backend:lan |
Backend only, on 0.0.0.0:8000 so a phone can reach it |
pnpm expo:doctor |
SDK 57 dependency and config health |
Running one of these from apps/mobile fails with
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL: Command "deploy:iphone" not found, because
they are root scripts. Use pnpm -w <script> from a subdirectory.
Related pages¶
- Deploy to iPhone: device signing detail, iPad, and TestFlight account troubleshooting
- Backend over Cloudflare Tunnel: a stable public backend hostname for builds that leave your network
- Accessibility: the manual assistive-technology pass the Inspector does not replace