Skip to main content

Mobile Patient Redesign — Spec

Date: 2026-05-01 Owner: ssh Audit reference: docs/qa/2026-05-01-mobile-app-audit.md Target binary: mobile/odontox-mobile (Expo SDK 55, RN 0.83)

1. Goal

Ship a polished, end-to-end patient mobile app that matches the webapp’s patient feature set, follows OdontoX’s design system (docs/design.md), and adopts modern Expo + iOS HIG + Material 3 patterns. One binary, role-aware architecture, but only the patient surface is built in v1.

2. Non-goals (v1)

  • Multi-role tab groups (admin, doctor, receptionist) — architecturally enabled but unbuilt
  • Ruby AI surfaces — deferred
  • iPad sidebar-adaptable layout — deferred
  • Payment UX innovation — match webapp exactly
  • DICOM viewer on mobile — patient doesn’t need it
  • Appointment reschedule endpoint — web doesn’t have it; we cancel + rebook
  • Prescription refill request endpoint — web doesn’t have it; patients message clinic
  • Sign in with Apple — not required (no third-party SSO offered)

3. Scope: patient features

Confirmed against webapp permissions tree (server/src/lib/permissions.ts:319-333) and PatientPortal.tsx. Five tabs + Profile sheet:

3.1 Home tab

  • Next visit card (date, time, doctor, clinic, action: view/cancel)
  • Pending treatment plan alert with “Review & accept” CTA when one exists
  • Balance due card (outstanding amount, “View invoices” CTA)
  • Recent activity feed (last 5 events: appointments, files, payments, messages)
  • Notifications feed entry-point (bell icon in header → full-screen feed)

3.2 Appointments tab

  • List (upcoming + past, segmented control)
  • Detail (doctor, time, clinic, status, treatment plan link if any)
  • Book new (request — server creates with request_status='pending')
  • Cancel (≥48h notice enforced server-side)

3.3 Records tab (clinical hub)

  • Treatment Plans — list, detail with cost breakdown, accept-and-generate-invoice flow
  • Clinical Notes — read-only list per appointment
  • Prescriptions — list, detail (medication, dosage, refill state)
  • Consent Forms — read-only viewer
  • X-Rays & Files (extension of webapp module) — list with category filter, viewer (image/PDF), file metadata
  • Dental Chart — read-only FDI numbering with condition palette per design.md §16

3.4 Billing tab

  • Invoices — list, detail, download PDF
  • Receipts — list, download
  • Quotations — list, detail (separate from invoices)
  • Payment History — list, status

3.5 Messages tab

  • Clinic chat thread (one thread per clinic)
  • Send text + attachment

3.6 Profile sheet (avatar tap in Home header)

  • Personal info (name, DOB, phone, email, gender, address)
  • Medical history (allergies, meds, conditions, surgical history, blood type, smoking, alcohol, notes)
  • Security (change mPIN, toggle Face ID/Touch ID, sign out)
  • Notification preferences
  • Switch clinic (if user has ≥2 clinic assignments)
  • Help & Support (clinic contact + FAQ)
  • Public blog (external link)
  • App version + legal

4. Auth model

Different from webapp — designed for mobile-grade re-auth.

4.1 First mobile login (re-onboarding existing webapp users)

  1. Splash (≤1s)
  2. Welcome — brand mark + tagline
  3. Three value-prop slides (skippable):
    • “Your clinic in your pocket”
    • “Stay informed in real time” (showcases push notifications)
    • “Secure by default” (showcases biometric/mPIN)
  4. Auth choice — Sign in (existing) or I have an invitation (paste token / scan QR)
  5. Sign in: email + password OR magic-link OTP
  6. Force mPIN setup (6 digits, numeric, confirm)
  7. Force biometric enrollment (Face ID / Touch ID / Class 3 Android biometric)
  8. Permission priming card → native notifications dialog
  9. Three-card patient tour (Home / Records / Messages)
  10. → Tabs

4.2 Cold launch unlock (every subsequent launch)

  • App killed → JWT and refresh token are gone (no persistence)
  • Splash → biometric prompt
  • Success → server exchanges device trust token for fresh JWT → tabs
  • Biometric fail/dismiss/3 retries → mPIN entry screen → on success same exchange happens
  • mPIN fail 5 times → soft lock 60s, then 5 min, then sign-out

4.3 Storage

  • expo-secure-store (iOS Keychain / Android Keystore):
    • device_trust_token — device-bound, server-issued at mPIN setup, rotated on every cold-launch exchange
    • biometric_enabled flag
  • No JWT in storage, ever. JWT lives in memory only.
  • mPIN hash stored server-side (Neon, argon2). Client never persists raw or hashed mPIN.

4.4 Session flush trigger

  • App killed (cold launch detected via AppState initial state)
  • Sign out (revokes trust token + Expo push token server-side)
  • mPIN changed (forces re-enrollment)
  • Trust token rejected by server (e.g. revoked by another device)

5. Push notifications

5.1 Stack

  • expo-notifications on client
  • Expo Push Service (handles APNs + FCM uniformly)
  • Server adds: device_tokens table + push_helper.ts calling Expo Push REST API

5.2 Schema

device_tokens (
  id uuid pk,
  user_id uuid fk,
  expo_push_token text not null,
  platform text not null check (platform in ('ios','android')),
  device_name text,
  created_at timestamptz,
  last_seen_at timestamptz,
  revoked_at timestamptz null
)
  • Unique on (user_id, expo_push_token). Revoke on sign-out.

5.3 Client lifecycle

  • After successful sign-in OR mPIN unlock, request notification permission (if not yet primed)
  • Get Expo push token via Notifications.getExpoPushTokenAsync()
  • POST to /api/v1/protected/me/devices
  • On sign-out: DELETE /api/v1/protected/me/devices/:tokenId

5.4 Server fanout triggers (v1)

EventTitleBodyDeep link
attachment.created (where patient_id == recipient)“New file in your records”category-derived (“X-ray added”, “Document uploaded”) — no clinical jargonRecords → file detail
appointment.reminder_24h”Appointment tomorrow"" at Appointments → detail
appointment.reminder_1h”Appointment in 1 hour""Head to the clinic”Appointments → detail
treatment_plan.created”New treatment plan""Tap to review and accept”Records → treatment plan detail
payment.received”Payment received""Rs on invoice #Billing → receipt
message.received sent a message”first 80 charsMessages → thread
recall.due”It’s time for your check-up"" recommends an appointment”Appointments → book
Copy must NEVER include clinical jargon (“lab case”, “DICOM”, “treatment phase 2”). Default phrasing always patient-friendly.

5.5 In-app notification feed

  • /api/v1/protected/notifications — paginated list of all events delivered (whether pushed or not)
  • Server stores notification rows in DB; push is just a fanout layer
  • Mobile shows feed in a full-screen sheet from Home header bell

6. HIPAA hardening

MitigationMobile implementation
Screenshot block on PHIiOS: UIScreen.capturedDidChangeNotification + privacy overlay. Android: FLAG_SECURE on activity. Applied to Records, Billing, Messages screens.
App preview snapshotOn AppStateinactive/background, render full-screen privacy overlay with logo
Idle timeout8h (patient) — AppState foreground timer; on expiry → return to lock screen (biometric/mPIN)
Background re-authAfter ≥30s in background, require biometric on resume
Token storageexpo-secure-store only, never AsyncStorage
Privacy ManifestDeclare PHI usage (App Review 5.1.x) in ios/PrivacyInfo.xcprivacy
Copy/paste on PHIcontextMenuHidden on PHI fields (DOB, medical history)

7. Design system migration

  • Adopt design.md tokens. Replace constants/AtelierTheme.ts with NativeWind v4 + a shared theme.ts module also consumed by webapp Tailwind config. Single source of truth.
  • Primary: indigo (#5048E5 light / #7C5CFA dark) — replaces #630ed4 violet.
  • Font: Poppins (per design.md §3) via expo-font config plugin (build-time embed). Drops Manrope.
  • Status colors: pair tokens (green/amber/sky/violet/red) per design.md §2.
  • Money UI: Banknote icon (lucide-react-native) — replaces doc.text/checkmark.circle in Billing.
  • Ruby: not built in v1; reserve token namespace for future.

8. Branding refresh

  • Logo: ship logo-light.svg + logo-dark.svg in assets/brand/. New <Logo> component reads useColorScheme().
  • iOS app icon: produce a .icon Apple Icon Composer bundle (default + dark + tinted) and wire via expo.ios.icon.
  • Android adaptive icon: replace baby-blue #E6F4FE background with on-brand neutral; ensure foreground / background / monochrome are all set so Material You themed icons work.
  • Splash: expo-splash-screen with setOptions({ duration: 800, fade: true }).

9. Architecture changes

9.1 Routing (expo-router)

app/
├── _layout.tsx                     # font loading, theme, auth gate, splash
├── index.tsx                       # redirect: locked → onboarding/auth/tabs
├── (auth)/
│   ├── _layout.tsx
│   ├── welcome.tsx
│   ├── value-props.tsx             # 3 swipeable slides
│   ├── sign-in.tsx
│   └── invitation.tsx
├── (onboarding)/                   # post-auth one-time setup
│   ├── _layout.tsx
│   ├── mpin-setup.tsx
│   ├── biometric-setup.tsx
│   ├── notifications-priming.tsx
│   └── tour.tsx
├── (locked)/                       # cold-launch unlock
│   ├── _layout.tsx
│   ├── biometric.tsx
│   └── mpin.tsx
├── (tabs)/                         # native tabs
│   ├── _layout.tsx
│   ├── home.tsx
│   ├── appointments/
│   │   ├── index.tsx
│   │   ├── [id].tsx
│   │   └── book.tsx
│   ├── records/
│   │   ├── index.tsx
│   │   ├── treatment-plans.tsx
│   │   ├── clinical-notes.tsx
│   │   ├── prescriptions.tsx
│   │   ├── consent.tsx
│   │   ├── files.tsx
│   │   └── chart.tsx
│   ├── billing/
│   │   ├── index.tsx               # tabbed: invoices | receipts | quotations | payments
│   │   └── [type]/[id].tsx
│   └── messages/
│       ├── index.tsx
│       └── [threadId].tsx
└── profile/                        # modal sheet
    ├── _layout.tsx
    ├── index.tsx
    ├── medical-history.tsx
    ├── security.tsx
    ├── notifications.tsx
    ├── switch-clinic.tsx
    └── help.tsx

9.2 Auth gate logic (in app/_layout.tsx)

if (!hasTrustToken) → redirect (auth)/welcome
if (hasTrustToken && !mPinConfigured) → (onboarding)/mpin-setup
if (hasTrustToken && needsBiometricSetup) → (onboarding)/biometric-setup
if (coldLaunch && needsUnlock) → (locked)/biometric
else → (tabs)/home

9.3 Server additions

  • New table: device_tokens (see §5.2)
  • New table: mpin_credentials (user_id, mpin_hash, attempts, locked_until, updated_at)
  • New table: device_trust_tokens (id, user_id, token_hash, device_fingerprint, created_at, last_used_at, revoked_at)
  • New routes:
    • POST /api/v1/protected/me/mpin — set/update mPIN
    • POST /api/v1/auth/mpin/verify — verify mPIN (returns JWT + new trust token)
    • POST /api/v1/auth/trust-token/exchange — exchange trust token for JWT (cold launch)
    • POST /api/v1/protected/me/devices — register Expo push token
    • DELETE /api/v1/protected/me/devices/:id — revoke
    • GET /api/v1/protected/notifications — feed
  • New helper: server/src/lib/push.ts — wraps Expo Push API with batching + retry

10. Sequencing

Each phase is its own PR + deploy. Phases are sequenced because each builds on the previous.
PhaseTitleDeliverable
P1FoundationsNativeWind + shared theme.ts + Poppins + logo refresh + splash + native tabs. No behavioral change.
P2Auth runtimemPIN + biometric + session-flush model + onboarding redesign + SecureStore migration + new server tables/routes
P3Patient parityAll five tabs rebuilt on new primitives + Treatment Plans tab + Quotations + Clinical Notes + Consent + Profile sheet + HIPAA hardening
P4Notificationsdevice_tokens table + push helper + Expo client + deep links + in-app feed + Bridge attachment hook

11. Acceptance criteria (per phase)

P1

  • App opens to design.md-compliant indigo primary; Poppins renders; light/dark logo swaps; native tab bar visible; no functional regression on existing screens
  • npx expo run:ios builds and launches; existing flows (signin, appointments, records, billing) still work

P2

  • New user signing in for first time is forced through mPIN + biometric setup
  • App killed → reopened → biometric prompt appears; success returns to last-known tab
  • expo-secure-store only; AsyncStorage no longer stores tokens
  • Settings → change mPIN works; toggle biometric works
  • Server tables migrated; existing webapp users unaffected (mPIN setup happens lazily on first mobile login)

P3

  • All web patient permissions have a UI surface on mobile (audit checklist passes)
  • Treatment plan accept flow works end-to-end (server generates invoice on accept)
  • Quotations + clinical notes + consent forms display
  • Profile sheet covers personal info + medical history + security + notification prefs + switch clinic + help
  • FLAG_SECURE / iOS screenshot block active on Records + Billing + Messages
  • Privacy overlay shown when app backgrounded
  • Idle timeout (8h) returns to lock screen

P4

  • Device registers Expo push token after auth; row visible in device_tokens
  • Uploading an attachment for a patient triggers a push notification within 5s
  • Tapping a push notification deep-links to the right tab + record
  • In-app feed shows the same events as push history
  • Sign-out revokes the device token (server-side delete) and stops further pushes

12. Risks

RiskMitigation
Existing webapp users locked out of mobile due to mPIN requirementmPIN setup is lazy — happens at first mobile login, not as a webapp migration
Trust-token theftTokens are device-bound (fingerprint), short-lived (rotate on each exchange), revocable, and never leave SecureStore unlocked except by biometric/mPIN
Expo Push outageFall back to in-app feed (rendered from server-stored notifications) — push is best-effort, source of truth is the DB
Privacy Manifest review failureDeclare PHI usage explicitly; reference HIPAA mobile checklist; submit with 5.1.x intent strings
Native tabs breaking layout on older RNTest on RN 0.83 / Expo SDK 55 simulator before release. Fall back to custom tab bar if blocking.

13. Open questions

None — user has confirmed all five questions from §9 of the audit + clarified Xray/Files extension, no clinical-jargon notification copy, and Expo Push Service for fanout.

14. References

  • Audit: docs/qa/2026-05-01-mobile-app-audit.md
  • Design system: docs/design.md
  • Permissions source-of-truth: server/src/lib/permissions.ts
  • Patient portal reference: ui/src/components/dashboards/PatientPortal.tsx
  • Memory: project_mobile_v2_decisions.md, project_mobile_app_state.md