Skip to content

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, use http://10.0.2.2:8080 instead of localhost — the emulator can't see your host's localhost directly.
  • 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 platforms
  • EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID — iOS bundle ID-registered
  • EXPO_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:

  1. Get the SHA-1 of your debug or release keystore.
  2. Add it to the OAuth client in Google Cloud Console → APIs & Services → Credentials.
  3. Rebuild the app — the change is read at runtime so a rebuild isn't strictly necessary, but expo prebuild artifacts 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