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)
- Splash (≤1s)
- Welcome — brand mark + tagline
- Three value-prop slides (skippable):
- “Your clinic in your pocket”
- “Stay informed in real time” (showcases push notifications)
- “Secure by default” (showcases biometric/mPIN)
- Auth choice — Sign in (existing) or I have an invitation (paste token / scan QR)
- Sign in: email + password OR magic-link OTP
- Force mPIN setup (6 digits, numeric, confirm)
- Force biometric enrollment (Face ID / Touch ID / Class 3 Android biometric)
- Permission priming card → native notifications dialog
- Three-card patient tour (Home / Records / Messages)
- → 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 exchangebiometric_enabledflag
- 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
AppStateinitial 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-notificationson client- Expo Push Service (handles APNs + FCM uniformly)
- Server adds:
device_tokenstable +push_helper.tscalling Expo Push REST API
5.2 Schema
- 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)
| Event | Title | Body | Deep link |
|---|---|---|---|
attachment.created (where patient_id == recipient) | “New file in your records” | category-derived (“X-ray added”, “Document uploaded”) — no clinical jargon | Records → 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 chars | Messages → thread |
recall.due | ”It’s time for your check-up" | " recommends an appointment” | Appointments → book |
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
| Mitigation | Mobile implementation |
|---|---|
| Screenshot block on PHI | iOS: UIScreen.capturedDidChangeNotification + privacy overlay. Android: FLAG_SECURE on activity. Applied to Records, Billing, Messages screens. |
| App preview snapshot | On AppState → inactive/background, render full-screen privacy overlay with logo |
| Idle timeout | 8h (patient) — AppState foreground timer; on expiry → return to lock screen (biometric/mPIN) |
| Background re-auth | After ≥30s in background, require biometric on resume |
| Token storage | expo-secure-store only, never AsyncStorage |
| Privacy Manifest | Declare PHI usage (App Review 5.1.x) in ios/PrivacyInfo.xcprivacy |
| Copy/paste on PHI | contextMenuHidden on PHI fields (DOB, medical history) |
7. Design system migration
- Adopt design.md tokens. Replace
constants/AtelierTheme.tswith NativeWind v4 + a sharedtheme.tsmodule also consumed by webapp Tailwind config. Single source of truth. - Primary: indigo (
#5048E5light /#7C5CFAdark) — replaces#630ed4violet. - Font: Poppins (per design.md §3) via
expo-fontconfig plugin (build-time embed). Drops Manrope. - Status colors: pair tokens (green/amber/sky/violet/red) per design.md §2.
- Money UI:
Banknoteicon (lucide-react-native) — replacesdoc.text/checkmark.circlein Billing. - Ruby: not built in v1; reserve token namespace for future.
8. Branding refresh
- Logo: ship
logo-light.svg+logo-dark.svginassets/brand/. New<Logo>component readsuseColorScheme(). - iOS app icon: produce a
.iconApple Icon Composer bundle (default + dark + tinted) and wire viaexpo.ios.icon. - Android adaptive icon: replace baby-blue
#E6F4FEbackground with on-brand neutral; ensure foreground / background / monochrome are all set so Material You themed icons work. - Splash:
expo-splash-screenwithsetOptions({ duration: 800, fade: true }).
9. Architecture changes
9.1 Routing (expo-router)
9.2 Auth gate logic (in app/_layout.tsx)
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 mPINPOST /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 tokenDELETE /api/v1/protected/me/devices/:id— revokeGET /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.| Phase | Title | Deliverable |
|---|---|---|
| P1 | Foundations | NativeWind + shared theme.ts + Poppins + logo refresh + splash + native tabs. No behavioral change. |
| P2 | Auth runtime | mPIN + biometric + session-flush model + onboarding redesign + SecureStore migration + new server tables/routes |
| P3 | Patient parity | All five tabs rebuilt on new primitives + Treatment Plans tab + Quotations + Clinical Notes + Consent + Profile sheet + HIPAA hardening |
| P4 | Notifications | device_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:iosbuilds 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-storeonly; 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
| Risk | Mitigation |
|---|---|
| Existing webapp users locked out of mobile due to mPIN requirement | mPIN setup is lazy — happens at first mobile login, not as a webapp migration |
| Trust-token theft | Tokens are device-bound (fingerprint), short-lived (rotate on each exchange), revocable, and never leave SecureStore unlocked except by biometric/mPIN |
| Expo Push outage | Fall back to in-app feed (rendered from server-stored notifications) — push is best-effort, source of truth is the DB |
| Privacy Manifest review failure | Declare PHI usage explicitly; reference HIPAA mobile checklist; submit with 5.1.x intent strings |
| Native tabs breaking layout on older RN | Test 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

