Architecture
Navigation tree
Navigation is React Navigation, driven by route constants in
src/navigation/constants.js (SCREENS, STACK_NAMES, TAB_NAMES). The root
is a native stack (RootNavigator) that switches between the auth flow, the
main tab app, and a set of blocking “required action” screens:
RootNavigator (stack, initial = AuthStack)
├─ AuthStack login / signup / recover / onboarding
├─ PaymentRequiredScreen blocks app until membership payment is resolved
├─ CurrencyInactiveScreen blocks app until flyer currency is restored
├─ TwoFaRequiredScreen staff who must enable 2FA (defers to web portal)
├─ ForceUpdateScreen minimum supported build gate
└─ HomeScreen → TabNavigator
├─ Home ├─ Chart ├─ Logbook ├─ Profile └─ SupportThe required-action screens are full-screen gates shown instead of the tab app. Which one (if any) appears is decided once, post-login, by the auth context (see below).
Auth context & session lifecycle
src/context/AuthContext.js owns session state and is the single source of truth
for “is the user signed in, and can they use the app”.
- Token storage — the JWT is persisted via
AsyncData(AsyncStorage). On cold start,checkAuthStatus()loads it and, if present, callsloadUserData(). loadUserData()fetches the member profile and runsvalidateUserStatus(), which returns arequiredAction:paymentRequired→currencyInactive→twoFaRequired→ otherwisenull(full access).useLogin.getTargetScreen()maps that action to the matching required-action screen.- Inactivity timeout — an in-app timer (default 30 min, via
INACTIVITY_TIMEOUT_MINUTES) logs the user out after inactivity; it pauses in the background and resets on resume. - Logout has two modes —
explicit: true(user-initiated) clears biometric credentials;explicit: false(inactivity or a 401) preserves them so the user can re-auth with Face/Touch ID / fingerprint.
Services layer
src/services/ wraps all backend and device access:
ApiServices— the axios client. Response interceptor refreshes the JWT from atokenresponse header, triggers a full logout on401(viaauthService), retries on network/5xx errors (not 401/403), and keeps expected4xxs out of Sentry.authService— a tiny indirection soApiServicescan callAuthContext.logout()on a 401 without a circular import.biometricService—react-native-keychainwrapper (save / retrieve / clear credentials underBIOMETRY_ANY). Self-heals when the stored ciphertext can’t be decrypted (Keystore key invalidated) by clearing the entry and falling back to password. See Authentication.PushNotificationService— FCM token + permissions + handlers. See Push notifications.userService,notificationService,logbookService, … — typed wrappers over API endpoints.
Observability
src/utils/logger.js is the logging façade and the only thing that should
talk to Sentry:
logger.debug— dev console only.logger.info/logger.warn— Sentry breadcrumbs (no event).logger.error— a Sentry exception (captureException).
So the rule of thumb is: an expected failure (a cancelled biometric prompt, a
transient network error, a session-expiry 401) should be warn/debug, and
only genuinely unexpected failures should be error. config/sentry.js
additionally drops transient/connectivity noise in beforeSend. See
Build & release → Source maps.