Expo SDK 55
React Native 0.84, New Architecture, Expo Router, NativeWind 5OTP Auth
2-step OTP login + biometric (Face ID / Touch ID) + SecureStore JWTOffline First
useOfflineQuery caches API responses (7-day TTL) + PDF cache (30-day)EAS Build
Fingerprint → OTA update or native build + store submitArchitecture
2c00cf79-cdc7-40e0-a4bd-6e4866336b22
Account / slug: mhcis / openinsure
Bundle IDs: dev.openinsure.mobile (iOS + Android)
Apple Team ID: 2D65GH64H3 | ASC App ID: 6761020503
Authentication
Auth state lives in a TanStack Store atlib/store/auth-mobile.ts. There is no React Context — all screens subscribe directly with individual selectors.
OTP Login Flow
- User enters their policy number on the login screen 2. App calls
POST /auth/policyholder-otp-requestwith the policy number 3. API sends a 6-digit OTP to the email on file (10-minute expiry) 4. User enters the OTP code 5. App callsPOST /auth/policyholder-tokenwith policy number + OTP 6. API returns a JWT (8-hour expiry) — stored in SecureStore (Keychain) 7. Push notification token is registered (best-effort)
Biometric (Face ID / Touch ID)
On app launch,app/_layout.tsx blocks routing behind a biometricPending gate. If biometric is enabled and the device supports it, a Face ID / Touch ID prompt fires before any screen renders.
Failed biometric clears the session and forces re-login via OTP.
Session Teardown
clearSession() clears: SecureStore JWT, biometric flag, offline caches (AsyncStorage + FileSystem PDFs), and the TanStack Query cache via the API provider’s token-change listener. No PII persists on device after logout.
API Integration
The mobile app calls the API directly with a Bearer JWT — no Next.js proxy layer.Endpoint Mapping
| Mobile Screen | API Endpoint | Method |
|---|---|---|
| Home (policy) | /v1/portal/policies/:id | GET |
| Home (claims) | /v1/claims?orgId=...&policyId=... | GET |
| Home (invoices) | /v1/portal/invoices | GET |
| Claims list | /v1/claims | GET |
| File claim | /v1/claims/fnol | POST |
| Policy detail | /v1/portal/policies/:id | GET |
| Coverages | /v1/policies/:id/jacket/coverages | GET |
| Endorsements | /v1/policies/:id/endorsements | GET |
| Request COI | /v1/coi/generate | POST |
| Request endorsement | /v1/policies/:id/endorsements | POST |
| Documents | /v1/portal/documents | GET |
| Document download | /v1/portal/documents/:id/download | GET |
| Invoices | /v1/portal/invoices | GET |
| Pay invoice | /v1/invoices/:id/payment-intent | POST |
| Profile | /v1/portal/profile | GET / PUT |
| Push token | /v1/portal/push-tokens | POST |
| Support chat | /v1/chat | POST |
| Renewal intent | /v1/policies/renewal/intent | POST |
MobileApiProvider creates a fetcher that prepends the API base URL and injects the Bearer token from the auth store. 401 responses auto-clear the session.
Offline Support
useOfflineQuery
lib/use-offline-query.ts wraps useApiQuery with offline cache fallback:
- On successful fetch → persists to AsyncStorage via
setCache(path, data) - On fetch failure when offline → returns cached data with
isStale: true - Screens show a “Showing cached data” banner when serving stale data
Cache TTLs
| Cache | TTL | Storage |
|---|---|---|
| API JSON responses | 7 days | AsyncStorage |
| PDF documents | 30 days | FileSystem |
pruneExpired() runs on app start to clean up stale entries.
Form Validation
All 6 form screens use Zod v4 schemas fromlib/validation.ts:
| Form | Schema | Fields Validated |
|---|---|---|
| File Claim | fnolClaimSchema | lossDate, lossDescription (min 10 chars), estimatedLoss (optional, positive) |
| Request COI | coiRequestSchema | holderName, holderAddress (required) |
| Request Endorsement | endorsementRequestSchema | changeType, description (min 10 chars) |
| Edit Profile | profileSchema | email, phone, ZIP (format validated) |
| Add Member | memberSchema | entityName, email, EIN (XX-XXXXXXX), memberType |
<Input error={}> component displays inline validation errors with red border + helper text.
Testing
42 tests across 3 test files, run with Vitest:| Test File | Tests | Coverage |
|---|---|---|
lib/__tests__/validation.test.ts | 15 | All 5 Zod schemas, edge cases |
lib/__tests__/offline-cache.test.ts | 8 | TTL expiry, pruning, JSON parse errors |
lib/store/__tests__/auth-mobile.test.ts | 19 | OTP request/verify, init, logout, biometric, 401 |
Accessibility
- Reduce Motion:
useReducedMotion()hook gates Reanimated entry animations on 3 key screens - Accessibility labels: Button (
accessibilityRole="button"), Input (accessibilityLabel,accessibilityState), ListItem (accessibilityLabel,accessibilityHint) - Privacy Manifest: Declares no tracking, email + crash data collection for app functionality
Icon System
The app usesphosphor-react-native@^3.0.3 (NOT @phosphor-icons/react — that’s for web). Tab bar icons use Expo’s NativeTabs.Trigger.Icon sf="..." SF Symbols.
EAS Build Workflows
Workflows live inapps/mobile/.eas/workflows/.
Production (production.yml)
Triggered by CircleCI deploy-mobile job on merge to master.
Preview (preview.yml)
Delivers OTA update to preview channel — testers get changes instantly.
Development (development.yml)
Manual trigger. Builds dev client for local debugging with expo start --dev-client.
CircleCI Integration
Thedeploy-mobile job in .circleci/config.yml:
- Gates on full CI suite (test, typecheck, lint, slo-check)
- Maps
EXPO_CIRCLECI_ACCESS_TOKEN→EXPO_TOKEN - Fails fast if token is missing
- Calls
eas workflow:run .eas/workflows/production.yml
Local Development
First Build Setup
Caution:
Apple credentials must be set up interactively the first time. Run eas build --profile production --platform ios and follow the prompts to generate a Distribution Certificate and provisioning
profile.