Native Testing¶
How to QA the native iOS and Android apps — simulators, real devices, OAuth caveats, and the failures you'll see most often.
For the release workflow see Native Release. For local Android dev environment (Android Studio, keystores, SHA-1) see Local development.
Local development modes¶
There are three ways to run the app locally, in increasing order of fidelity to a production build:
1. Expo Go (web / quick-look only)¶
task dev:frontend starts the Expo dev server on :8081. You can scan the QR with the Expo Go app for the fastest possible loop, but this has hard limits:
- No native modules outside the Expo Go SDK (rules out Google Sign-In, certain camera/payment features).
- No custom URL schemes / deep links.
Useful for UI-only changes. For everything else, use a dev client.
2. iOS Simulator¶
# First run on the Simulator (or after a native dep change)
task dev:ios # = npx expo run:ios
# Day-to-day after that
task dev:frontend # starts Metro; then press "i"
The first run installs the dev client into the Simulator. Subsequent runs reuse it; JS edits hot-reload via Fast Refresh without rebuilding the binary.
The same pattern applies to Android with task dev:android then task dev:frontend + a.
If you need a build that more closely matches production (signed, release config), build the store artifact locally with task native:beta:ios and install the resulting .ipa on a device via Xcode or TestFlight. See Native Release.
3. Real device via a local dev-client build¶
task dev:ios # = npx expo run:ios, builds + installs onto a tethered device
# or task dev:android for Android
Once installed, point the dev client at your local Metro server (task dev:frontend); the dev client landing page has a field to paste the URL.
This is the only way to test features that need a real device — native push notifications, biometric auth, camera, payments, deep links from third-party apps.
Pointing at different backends¶
Native builds bake in the EXPO_PUBLIC_API_URL and EXPO_PUBLIC_WS_URL at build time. To test against a non-production backend:
- Local backend: in a dev-client build with Metro pointing at
http://localhost:8081, the JS bundle is built on the fly and you can override env vars in your shell before running Metro. On Android emulator, usehttp://10.0.2.2:8080instead oflocalhost— the emulator can't see your host'slocalhostdirectly. - Dev backend: build locally with the dev API URL exported in your shell (or in
frontend/.env) before running the build.
OAuth client IDs¶
Google Sign-In needs a different OAuth client ID per platform. The native app reads three:
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID— needed for the ID token exchange on both platformsEXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID— iOS bundle ID-registeredEXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID— Android package + SHA-1 registered
For production builds these come from frontend/.env at build time (native binaries are built locally). For local dev builds these come from your shell environment when running Metro / npx expo run:*. If you're testing OAuth on a dev build, make sure you've got the right values exported and they match the keystore SHA-1 of the build you're running. See Local Development for the Android SHA-1 details.
Common issues¶
Metro cache weirdness¶
If a JS change isn't showing up, or you see errors referencing modules that no longer exist:
task dev:frontend:clear
That clears Metro's transform cache. If it's still wrong, also cd frontend && rm -rf node_modules && npm install.
Android emulator can't see localhost¶
Symptom: backend requests fail immediately with a network error on the Android emulator, but iOS Simulator works fine.
Fix: use 10.0.2.2 instead of localhost for the API URL. The Android emulator runs its own NAT and localhost resolves to the emulator itself, not your host.
Google Sign-In: DEVELOPER_ERROR on Android¶
Symptom: Google Sign-In on Android fails immediately with DEVELOPER_ERROR (code 10).
Cause: the SHA-1 fingerprint of the keystore used to sign your current build is not registered with the Android OAuth client.
Fix:
- Get the SHA-1 of your debug or release keystore.
- Add it to the OAuth client in Google Cloud Console → APIs & Services → Credentials.
- Rebuild the app — the change is read at runtime so a rebuild isn't strictly necessary, but
expo prebuildartifacts may need refreshing.
Details and the keystore commands in Local Development.
Push notifications not arriving on iOS¶
Push only works on a real device with a build that includes the push entitlement and is signed against an APNs key. The iOS Simulator can't receive remote push at all (only foreground simulated ones).
Real-time updates stop arriving¶
Likely the WebSocket connection dropped and isn't reconnecting (or backed off too far). Check the device's network state, then check whether the backend Pod is healthy (see Getting Started → Investigate WebSocket flapping). The native WS client has the same idle-timeout sensitivity as the web client.
Focus / keyboard verification (shared-component refactor)¶
The shared-component refactor (field-rows, sheets, VisibilityPicker, ChatSettings) has so far been verified on web only. TextInput focus, keyboard-avoidance, and TrueSheet present/dismiss behave differently on native, so every input + sheet below needs a native pass. tsc / eslint / jest do NOT catch these; they are hand-QA on a device or emulator.
Keyboard avoidance is standardized on react-native-keyboard-controller: KeyboardProvider at the app root, useKeyboard/useKeyboardVisible, KeyboardAwareScroll for normal screens, and KeyboardStickyView for docked bars. Sheets keep TrueSheet's native handling (never nest a keyboard-controller view inside a TrueSheet). The Android side of this was validated separately: see Keyboard Android Handoff.
Quick Android emulator (Linux-doable; no Mac needed)¶
export ANDROID_HOME=$HOME/Android/Sdk
SDKM=$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager
# One-time: emulator + a system image (needs /dev/kvm for acceleration)
yes | "$SDKM" --install "emulator" "system-images;android-34;google_apis;x86_64" "platforms;android-34"
echo no | "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" create avd -n tomoda -k "system-images;android-34;google_apis;x86_64" -d pixel_6
"$ANDROID_HOME/emulator/emulator" -avd tomoda -gpu swiftshader_indirect & # add -no-window for headless
# Build + install the dev client, then Metro hot-reloads JS:
task dev:android # = npx expo run:android
iOS has no Linux path; use a Mac simulator (task dev:ios) or a real device.
The checklist (walk each on Android + iOS)¶
| Surface | What to verify | Risk |
|---|---|---|
| ChatSettings nickname sheet | bottom Sheet lifts above the keyboard; input never hidden |
high (was a top overlay; now bottom-anchored) |
| ChatSettings nickname input | autoFocus actually focuses inside TrueSheet (can race the present animation on native) |
high |
| ChatSettings nickname | focus border toggles (primary↔surface); Save disabled on empty/whitespace + during save; dismiss via X, backdrop, swipe-down | med |
| ChatSettings group-rename inline edit | autofocus, returnKeyType=done, submit renames, disabled during save |
med |
| ChatSettings disappearing-messages | SelectionSheet tap-to-select updates + auto-closes; selected row highlighted |
low |
InlineEditField (profile-edit name / bio) |
always-editable; dirty reveals revert/confirm; keyboard doesn't cover the confirm buttons; save flashes ✓ | high |
change-username / change-email / change-password |
FocusField input focus + keyboard-avoid; submit; error surfacing |
med |
QuickAddSheet / ItemSheet inputs |
sheet input not clipped by keyboard; docked footer rides the keyboard (default keyboardOffset) |
high |
| compose-moment caption + Publish | caption scrolls clear; the absolute Publish footer (KeyboardStickyView) rides the keyboard, not covered |
high |
| QuickReplyBar (moment reply) | reply bar rests just above the keyboard; no double-lift; dismiss via backdrop | high |
| MomentEditSheet form | caption scrolls above the keyboard so lifetime / companions / delete stay reachable | high |
Chat composer (MessageInput) |
composer + send stay above the keyboard; picker toggle doesn't double-pad | high |
VisibilityPicker pills |
tap selects; wraps correctly; no keyboard involvement | low |
Any Sheet with a SearchField (People add-friends, message search) |
autofocus + keyboard-avoid | med |
| Safe-area insets | notch/gesture-bar devices: headers + docked footers respect insets | med |
Log results against this table per platform; file a bug for any row that fails and reference the surface name.
See also¶
- Local Development — Android Studio, SDK, emulator, signing
- Native Release — local build + manual store upload flow
- Secrets — what
EXPO_PUBLIC_*actually means