OdontoX Mobile App — Audit & Findings
Date: 2026-05-01 Scope:mobile/odontox-mobile (Expo SDK 55, RN 0.83) vs docs/design.md, web app onboarding, and current platform best practices.
Status: Mobile build verified — installs and launches on iPhone 17 Pro simulator.
TL;DR
The mobile app is functional but is a patient-only MVP that diverges materially from the OdontoX design system, ships an outdated/single logo, has a thin (4-slide) onboarding that doesn’t actually showcase features, has zero AI/Ruby presence, and ignores the multi-role permission tree the webapp depends on. To bring it to product quality we should:- Re-scope as a multi-role staff + patient app (or split into two flows under one binary).
- Adopt design.md tokens via NativeWind so web and mobile share one source of truth.
- Rebuild onboarding as a proper first-run flow: brand → value props → role pick → permission priming → auth → biometric setup → in-app tour.
- Refresh logo / icon / splash with light + dark variants and iOS 18/26 tinted icons.
- Add HIPAA mobile defenses: FLAG_SECURE on PHI, biometric re-auth on resume, idle timeout, expo-secure-store.
1. Current state of the mobile app
1.1 Routes (expo-router)
1.2 Onboarding today (4 slides)
app/(auth)/onboarding.tsx — scheduling, health security, direct chat, transparent billing. Ends at sign-in. Misses:
- Welcome / value-prop framing
- Persona/role selection
- Permissions priming (notifications, biometrics) before native dialogs
- Account creation path (signin only — assumes user was provisioned)
- In-app feature tour after auth (lands on a blank dashboard if no data)
- Empty-state coaching per tab
1.3 Logo & splash
| Asset | File | Status |
|---|---|---|
| App icon | assets/images/icon.png (1024²) | Outdated — single static PNG, no iOS dark/tinted, no Android monochrome wired |
| Splash logo | assets/images/logo.png (1393×317) | Light variant only |
logo-dark.png | exists in assets/ | Unused — code never imports it |
logo-light.png | exists in assets/ | Unused |
| Adaptive Android | foreground/background/monochrome PNGs exist | Background #E6F4FE is light-blue tint — not on brand |
require('../../assets/images/logo.png') in welcome.tsx:21 and onboarding.tsx:62 — no theme switching. Violates design.md §1: “Always swap based on the current theme — never force a single variant on both backgrounds.”
1.4 Design system divergence vs docs/design.md
| Rule (design.md) | Mobile reality | Severity |
|---|---|---|
Primary indigo #5048E5 (light) / #7C5CFA (dark) | #630ed4 violet (AtelierTheme.ts:14) | Medium — wrong brand hue |
| Poppins primary, Inter fallback | Manrope only (@expo-google-fonts/manrope) | Medium |
| Status colors as bg/fg pairs (green/amber/sky/violet/red) | Hardcoded singletons (#7c3aed, #630ed4 in onboarding slides) | Medium — no Badge primitive |
| Banknote icon for money UI | Billing uses doc.text, checkmark.circle | Low |
| Both light and dark first-class | Dark palette absent in AtelierTheme.ts | High |
| Semantic tokens, never raw hex | Raw hex in onboarding.tsx:18,24,30 | Low |
| Ruby AI mark + animations | Zero AI surfaces anywhere | High (product-level gap) |
| Form primitives (FormField, FormLabel…) | None — raw Pressable + TextInput | Medium |
Status Badge / Chip component | None | Medium |
| Skeleton loading | None | Low |
| ResponsiveTable | None | N/A on mobile |
1.5 Component library
components/atelier/ ships a small primitive set: AtelierView, AtelierText, AtelierKPICard, AtelierActivityList, AtelierFAB, AtelierHero. It’s a parallel, mobile-only design system — none of the design.md tokens flow through. Each tab is a monolithic screen, not composed from primitives.
1.6 Roles & permissions
Searching forrole, permission, isAdmin, doctor, receptionist in mobile/odontox-mobile: no role-gating anywhere. Auth context stores user.firstName/lastName/email only. The mobile app silently assumes one persona (patient).
1.7 Module status
| Module | State |
|---|---|
| Auth (signin, MFA, passkey) | Functional |
| Appointments (list, book, detail, reschedule) | Partial — wired |
| Messages (list + thread) | Partial |
| Records (files, prescriptions) | Partial |
| Billing (invoices/receipts) | Partial — uses Banknote-less icons |
| Reminders / Notes tab | Stub (empty) |
| Profile (edit, history, security, help) | Stub — handlers () => {} |
| Ruby / AI | Absent |
2. Web app: source-of-truth onboarding & roles
2.1 Onboarding flows in production webapp
| Path | Component | Audience |
|---|---|---|
/onboarding/trial | ui/src/components/onboarding/TrialOnboarding.tsx | Prospect signing up for trial — manual superadmin approval (CLAUDE memory: anti-competitor gate) |
/invitations/:token | ui/src/pages/InvitationAccept.tsx | Invited staff or patient — collects DOB/gender/phone (patient), password, 2FA (passkey/TOTP/email) |
/onboarding/complete/:token | ui/src/components/auth/CompleteOnboarding.tsx | Pre-provisioned account → email OTP → password |
Post-login: /select-clinic → /dashboard | SelectClinic.tsx + role-based dashboards | All authed users |
2.2 Canonical role model
Source:server/src/lib/permissions.ts (lines 226–334). Roles stored in user_clinic_assignments.role (varchar 50). Four clinic roles + one platform role:
admin— Practice Owner / Clinic Admin (sees everything in their clinic)doctor— Clinicalreceptionist— Front deskpatient— Self-service portalsuperadmin— Platform operator (not a clinic role; should not be on mobile)
2.3 Plan-based gates
Receptionist inventory access depends on plan tier (Default → view-only, Pro → full, Pro+ → manage). Radiology Workstation requiresdicom_imaging feature flag. The mobile app must consult the same plan / feature flag service the web uses.
3. Recommended role → mobile module map
The phone is for on-the-go work: short tasks, contextual reads, glanceable status. Heavy admin (config, analytics, payroll, user management) stays on desktop. Per-role mobile feature set:3.1 Admin (Practice Owner) — 5 tabs
- Today — KPI strip (revenue, appointments, no-shows, outstanding), Ruby daily brief
- Calendar — clinic-wide schedule, block time, see all chairs/rooms
- Patients — search, recent, quick actions (call, message)
- Approvals — pending invoices to write off, expense approvals, refund requests, lab acceptance
- More — Reports (top-line only), Notifications, Switch clinic, Settings
3.2 Doctor — 5 tabs
- Today — my schedule, current patient card, Ruby clinical assist
- Patients — my list, search, quick chart preview, prescriptions
- Charting — view dental chart, add findings, sign clinical note (mobile-friendly chart only — full editing stays desktop)
- Files — X-rays, lab results, Bridge inbox (review + approve)
- More — Treatment plans, Prescriptions, Notifications, Switch clinic
3.3 Receptionist — 5 tabs
- Today — appointment ribbon, walk-ins queue, no-show list, recall list
- Calendar — book / reschedule / move blocks
- Patients — create / edit / call / SMS / email
- Billing — create invoice, record payment, send receipt (Banknote icon!), insurance claim status
- More — Lab cases, Inventory (plan-gated), Notifications
3.4 Patient — 5 tabs (current app’s audience)
- Home — next appointment card, treatment plan to accept, prescription refills, balance due
- Appointments — my schedule, book, reschedule
- Records — clinical notes, prescriptions, X-rays/files, dental chart (read-only)
- Billing — invoices, pay (Stripe / local gateway), installment plan
- Messages — clinic chat, Ruby self-care answers (gated to non-clinical)
3.5 Mode selection
On first launch (or post-auth role detection), the app picks the right tab set. If the user has multiple roles across clinics, surface a clinic switcher in the header (matches the recent web change in commite256035c — feat(ui): in-app clinic switcher).
4. Onboarding redesign (proper, feature-showcasing flow)
Per Apple HIG (short, skippable, no rote memorization) and Material 3 (priming-then-permission, self-select for implicit education):Critical onboarding rules
- Skippable: every slide except auth.
- Permission priming card before the system dialog — never let iOS/Android dialogs be the first thing the user sees.
- Persona implicit, not explicit: detect role from server, don’t make user choose. Only ask if the same email has multiple roles.
- Re-show the tour when a user gets a new role assigned (pull from
user_clinic_assignmentschange events).
5. Logo, icon, splash refresh
5.1 What to fix
- Add the design.md-compliant indigo wordmark in light + dark variants under
assets/brand/and consume them via a<Logo>component that readsuseColorScheme(). - iOS app icon: produce a
.iconApple Icon Composer bundle so we get default + dark + tinted automatically. Wire viaexpo.ios.icon(SDK 54+). - Android adaptive icon: ensure foreground / background / monochrome are all set so themed icons (Material You) work. Replace the
#E6F4FEbaby-blue background with the on-brand indigo (#5048E5) or a neutralcardtoken. - Splash: switch to the new
expo-splash-screenconfig (single 1024² brand mark + brand background + Ruby pulse viaSplashScreen.setOptions({ duration, fade })).
5.2 References
- Expo splash & app icon docs (SDK 54): https://docs.expo.dev/develop/user-interface/splash-screen-and-app-icon/
- Apple Icon Composer (
.iconbundles): https://developer.apple.com/documentation/TechnologyOverviews/adopting-liquid-glass
6. Best-practices alignment (Expo + iOS HIG + Material 3)
6.1 Architecture changes
| Area | Current | Target |
|---|---|---|
| Routing | mixed groups | (auth)/, (onboarding)/, (tabs-admin)/, (tabs-doctor)/, (tabs-receptionist)/, (tabs-patient)/ with Stack.Protected auth gate in root _layout.tsx |
| Tabs | custom FloatingTabBar | Expo Router native tabs (gets iOS 26 Liquid Glass tab bar automatically when built with Xcode 26) |
| iPad | not addressed | sidebarAdaptable tab pattern → tab bar on phone, sidebar on iPad |
| Design tokens | parallel AtelierTheme.ts | NativeWind v4 consuming the same theme.ts Tailwind config the webapp uses |
| Fonts | runtime useFonts (Manrope) | expo-font config plugin embed (Poppins per design.md), system font for chrome on iOS for Dynamic Type |
| Auth | email + passkey + MFA | Add Sign in with Apple only if we add Google/Facebook SSO (Guideline 4.8) — not needed today |
| Tokens at rest | unclear | expo-secure-store for refresh token, never AsyncStorage |
6.2 HIPAA mobile hardening (mandatory before clinical roles ship)
- FLAG_SECURE on Android,
UIScreen.capturedDidChangeNotification+ privacy overlay on iOS for any PHI screen (records, prescriptions, charting). - Idle timeout: 1–2 min for clinical roles, 30–60 s for prescriptions/financials. Re-prompt biometric on
AppState→activeafter > 30 s in background. - Privacy overlay on the app preview snapshot when backgrounded.
- Disable copy/paste on PHI fields (
contextMenuHiddenonTextInput). - Privacy Manifest declaring PHI usage (App Review 5.1.x).
6.3 Sign-in UX
- Passkey first, magic link / 6-digit OTP fallback. SMS OTP discouraged (CISA/FBI 2025).
- Biometric priming before
LocalAuthentication.authenticateAsync— system prompt only fires once per cold launch.
7. Findings summary table
| # | Finding | Impact | Effort |
|---|---|---|---|
| F1 | Patient-only app; no role awareness | Blocks the user’s stated goal of “all roles” | XL — re-architect |
| F2 | Logo single variant, no theme swap, outdated icon strategy | Brand inconsistency in dark mode + iOS 18/26 | S |
| F3 | Onboarding doesn’t showcase features, no permission/biometric priming | First-run conversion + perceived polish | M |
| F4 | Wrong primary color (#630ed4 vs #5048E5) | Brand consistency | S |
| F5 | Manrope vs Poppins | Brand consistency | S |
| F6 | No Banknote icon for money UI | design.md §2.3 violation, low risk | XS |
| F7 | No dark palette | Half the app is unbuilt for dark mode | M |
| F8 | No Ruby/AI surfaces — design.md §18 entirely missing | Product positioning | L |
| F9 | No HIPAA mitigations (FLAG_SECURE, idle timeout, privacy overlay) | Compliance blocker before staff/clinical roles ship | M |
| F10 | Design tokens duplicated across AtelierTheme and webapp | Maintenance debt | M (one-time NativeWind migration) |
| F11 | No form primitives (RHF + Zod), Skeleton, Badge, Sheet | Re-implements design.md primitives | M |
| F12 | Profile/Reminders tabs are stubs | UX gap | S |
| F13 | Custom FloatingTabBar instead of native tabs | Loses iOS 26 Liquid Glass | S |
| F14 | No iPad layout | App will run but not feel iPad-native | M |
| F15 | Adaptive icon background #E6F4FE not on brand | Visual | XS |
8. Recommended sequencing
Phase 1 — Foundations (1 sprint)- Migrate to NativeWind, share
theme.tswith web (fixes F4, F5, F7, F10) - Refresh logo/icon/splash with light+dark variants and
.iconbundle (F2, F15) - Switch to native tabs (F13)
- Per-role tab groups + role detection + clinic switcher (F1)
- New onboarding flow with feature showcase + priming + tour (F3)
- Form primitives, Badge, Skeleton, Sheet (F11)
- FLAG_SECURE, idle timeout, biometric re-auth, privacy overlay, Privacy Manifest (F9)
- Ruby surfaces: daily brief on Today, AI assist on patient detail, AI shimmer + glow per design.md §18 (F8)
- Profile/Reminders real implementations (F12)
- iPad sidebar-adaptable layout (F14)
- Banknote icon swap (F6)
- Per-tab empty states + Skeletons
9. Open questions for the user
- Scope confirmation: do you want one binary serving all four roles (admin/doctor/receptionist/patient) with role-aware tabs, or two binaries — a clinic staff app and a separate patient app? (The OS app stores prefer the latter for healthcare.)
- Sign in with Apple — do we plan to add Google / Facebook SSO? If yes, Apple is mandatory; if no, we can skip.
- Biometric policy — do you want hard biometric on every cold launch, or grace period (e.g. 8h) for non-PHI screens?
- Patient pay — confirm payment provider on mobile (Stripe vs local Pakistani gateway like JazzCash/Easypaisa). This affects the Billing tab design.
- Bridge inbox on mobile — for doctors, do we want full DICOM viewing on phone, or just a “review and approve” surface that defers viewing to desktop?
10. Sources
Mobile app:mobile/odontox-mobile/{app,components,assets,constants,app.json}
Webapp: ui/src/{components/onboarding,components/auth,pages,hooks/useNavItems.ts}, server/src/lib/permissions.ts, server/src/schema/user_clinic_assignments.ts
Design system: docs/design.md
External (best practices):
- Expo Router: https://docs.expo.dev/router/basics/core-concepts/
- Expo Splash & App Icon (SDK 54): https://docs.expo.dev/develop/user-interface/splash-screen-and-app-icon/
- Apple HIG Onboarding: https://developer.apple.com/design/human-interface-guidelines/onboarding
- Apple Adopting Liquid Glass: https://developer.apple.com/documentation/TechnologyOverviews/adopting-liquid-glass
- App Review Guideline 4.8 (Sign in with Apple): https://developer.apple.com/app-store/review/guidelines/
- Material onboarding: https://m1.material.io/growth-communications/onboarding.html
- HIPAA mobile checklist: https://www.accountablehq.com/post/hipaa-requirements-for-mobile-health-apps-a-practical-compliance-checklist
- HIPAA screen lock: https://www.accountablehq.com/post/hipaa-screen-lock-requirements-explained-auto-lock-timeouts-and-best-practices
- NativeWind vs Tamagui vs Unistyles 2026: https://medium.com/react-native-journal/nativewind-vs-tamagui-vs-unistyles-which-styling-library-should-you-use-in-2026-cf4f4d78b76f

