Skip to content

Mobile App

The OpenInsure mobile app gives policyholders native access to their coverage, claims, payments, and documents on iOS and Android. Built with Expo SDK 55 and React Native’s New Architecture.

Expo SDK 55

React Native 0.84, New Architecture, Expo Router, NativeWind 5

OTP Auth

2-step OTP login + biometric (Face ID / Touch ID) + SecureStore JWT

Offline First

useOfflineQuery caches API responses (7-day TTL) + PDF cache (30-day)

EAS Build

Fingerprint → OTA update or native build + store submit


apps/mobile/
├── app/
│ ├── _layout.tsx # Root: SplashScreen, AuthGate, providers
│ ├── login.tsx # 2-step OTP login (policy number → 6-digit code)
│ ├── auth/verify.tsx # Magic link deep-link handler
│ └── (tabs)/
│ ├── _layout.tsx # NativeTabs (5 tabs)
│ ├── (home,claims)/
│ │ ├── home.tsx # Dashboard: KPIs, quick actions, recent claims
│ │ ├── claims.tsx # Claims list (FlatList, offline cached)
│ │ ├── file-claim.tsx # FNOL form (Zod validated)
│ │ ├── i/[id].tsx # Claim detail + error boundary
│ │ ├── support.tsx # AI chat support
│ │ └── members/ # Policy member management (admin)
│ ├── (policy)/
│ │ ├── index.tsx # Policy detail + coverages + endorsements
│ │ ├── id-card.tsx # Digital ID card with QR code
│ │ ├── renewal.tsx # Renewal intent form
│ │ ├── request-coi.tsx # COI request (Zod validated)
│ │ └── request-endorsement.tsx # Endorsement request (Zod validated)
│ ├── (documents)/
│ │ └── index.tsx # Document vault (authenticated download)
│ ├── (payments)/
│ │ └── index.tsx # Invoices + Stripe payment initiation
│ └── (settings)/
│ ├── index.tsx # Account, biometric, notifications, sign-out
│ └── profile.tsx # Edit profile (Zod validated)
├── components/
│ └── ui/ # NativeWind primitives: Button, Input, Card, etc.
├── lib/
│ ├── api-provider.tsx # MobileApiProvider: QueryClient + token injection
│ ├── store/auth-mobile.ts # TanStack Store: OTP auth + biometric + SecureStore
│ ├── use-offline-query.ts # Cache wrapper for TanStack Query
│ ├── use-reduced-motion.ts # Accessibility: respect system reduce-motion
│ ├── validation.ts # Zod v4 schemas (FNOL, COI, endorsement, profile, member)
│ ├── offline-cache.ts # AsyncStorage (JSON) + FileSystem (PDF) caching
│ ├── document-download.ts # PDF download → share sheet
│ ├── notifications.ts # Push token registration
│ ├── sentry.ts # Error tracking (PII-filtered)
│ └── brand.ts # White-label config
├── test/
│ ├── setup.ts # Vitest mocks for RN modules
│ └── mocks/factories.ts # Realistic insurance mock data
└── eas.json # Build profiles + App Store submit config

EAS project: 2c00cf79-cdc7-40e0-a4bd-6e4866336b22 Account / slug: mhcis / openinsure Bundle IDs: dev.openinsure.mobile (iOS + Android) Apple Team ID: 2D65GH64H3 | ASC App ID: 6761020503


Auth state lives in a TanStack Store at lib/store/auth-mobile.ts. There is no React Context — all screens subscribe directly with individual selectors.

const user = useAuthStore((s) => s.user);
const requestOtp = useAuthStore((s) => s.requestOtp);
const verifyOtp = useAuthStore((s) => s.verifyOtp);
  1. User enters their policy number on the login screen 2. App calls POST /auth/policyholder-otp-request with 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 calls POST /auth/policyholder-token with policy number + OTP 6. API returns a JWT (8-hour expiry) — stored in SecureStore (Keychain) 7. Push notification token is registered (best-effort)

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.

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.


The mobile app calls the API directly with a Bearer JWT — no Next.js proxy layer.

Mobile ScreenAPI EndpointMethod
Home (policy)/v1/portal/policies/:idGET
Home (claims)/v1/claims?orgId=...&policyId=...GET
Home (invoices)/v1/portal/invoicesGET
Claims list/v1/claimsGET
File claim/v1/claims/fnolPOST
Policy detail/v1/portal/policies/:idGET
Coverages/v1/policies/:id/jacket/coveragesGET
Endorsements/v1/policies/:id/endorsementsGET
Request COI/v1/coi/generatePOST
Request endorsement/v1/policies/:id/endorsementsPOST
Documents/v1/portal/documentsGET
Document download/v1/portal/documents/:id/downloadGET
Invoices/v1/portal/invoicesGET
Pay invoice/v1/invoices/:id/payment-intentPOST
Profile/v1/portal/profileGET / PUT
Push token/v1/portal/push-tokensPOST
Support chat/v1/chatPOST
Renewal intent/v1/policies/renewal/intentPOST

The 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.


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

Used on: Home dashboard, policy detail, and claims list.

CacheTTLStorage
API JSON responses7 daysAsyncStorage
PDF documents30 daysFileSystem

pruneExpired() runs on app start to clean up stale entries.


All 6 form screens use Zod v4 schemas from lib/validation.ts:

FormSchemaFields Validated
File ClaimfnolClaimSchemalossDate, lossDescription (min 10 chars), estimatedLoss (optional, positive)
Request COIcoiRequestSchemaholderName, holderAddress (required)
Request EndorsementendorsementRequestSchemachangeType, description (min 10 chars)
Edit ProfileprofileSchemaemail, phone, ZIP (format validated)
Add MembermemberSchemaentityName, email, EIN (XX-XXXXXXX), memberType

The <Input error={}> component displays inline validation errors with red border + helper text.


42 tests across 3 test files, run with Vitest:

Terminal window
pnpm --filter @openinsure/mobile test
Test FileTestsCoverage
lib/__tests__/validation.test.ts15All 5 Zod schemas, edge cases
lib/__tests__/offline-cache.test.ts8TTL expiry, pruning, JSON parse errors
lib/store/__tests__/auth-mobile.test.ts19OTP request/verify, init, logout, biometric, 401

  • 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

The app uses phosphor-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.

import { Bell, FileText, CreditCard } from 'phosphor-react-native';

Workflows live in apps/mobile/.eas/workflows/.

Triggered by CircleCI deploy-mobile job on merge to master.

fingerprint → get-build (check existing)
├─ JS-only change → OTA update to production channel (minutes)
└─ Native change → full build + App Store / Play Store submit (hours)

Delivers OTA update to preview channel — testers get changes instantly.

Manual trigger. Builds dev client for local debugging with expo start --dev-client.


The deploy-mobile job in .circleci/config.yml:

  1. Gates on full CI suite (test, typecheck, lint, slo-check)
  2. Maps EXPO_CIRCLECI_ACCESS_TOKENEXPO_TOKEN
  3. Fails fast if token is missing
  4. Calls eas workflow:run .eas/workflows/production.yml

Terminal window
cd apps/mobile
# Start Metro bundler
npx expo start
# Run on iOS simulator
npx expo run:ios
# Run on Android emulator
npx expo run:android
# Point at local API worker
EXPO_PUBLIC_API_URL=http://localhost:8787 npx expo start
# Run tests
pnpm test

Terminal window
cd apps/mobile
# iOS production build (interactive first time for Apple credentials)
eas build --platform ios --profile production
# Android production build
eas build --platform android --profile production
# Submit to stores
eas submit --platform ios --profile production
eas submit --platform android --profile production