Skip to main content

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:
  1. Re-scope as a multi-role staff + patient app (or split into two flows under one binary).
  2. Adopt design.md tokens via NativeWind so web and mobile share one source of truth.
  3. Rebuild onboarding as a proper first-run flow: brand → value props → role pick → permission priming → auth → biometric setup → in-app tour.
  4. Refresh logo / icon / splash with light + dark variants and iOS 18/26 tinted icons.
  5. 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)

app/
├── (auth)/
│   ├── index, welcome, onboarding (4 slides), signin, mfa, passkey, notifications
├── (tabs)/
│   ├── index (home), messages, appointments/(index|book|[id]),
│   ├── records, billing, reminders (stub), profile (stub)
├── chat/[id], modal, viewer, passkey-setup, notification-permission, reschedule, +not-found
It’s a patient-only experience: book appointment, see records, see invoices, message a doctor. There is no staff-side, no admin, no doctor-as-clinician view.

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

AssetFileStatus
App iconassets/images/icon.png (1024²)Outdated — single static PNG, no iOS dark/tinted, no Android monochrome wired
Splash logoassets/images/logo.png (1393×317)Light variant only
logo-dark.pngexists in assets/Unused — code never imports it
logo-light.pngexists in assets/Unused
Adaptive Androidforeground/background/monochrome PNGs existBackground #E6F4FE is light-blue tint — not on brand
Imported only as 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 realitySeverity
Primary indigo #5048E5 (light) / #7C5CFA (dark)#630ed4 violet (AtelierTheme.ts:14)Medium — wrong brand hue
Poppins primary, Inter fallbackManrope 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 UIBilling uses doc.text, checkmark.circleLow
Both light and dark first-classDark palette absent in AtelierTheme.tsHigh
Semantic tokens, never raw hexRaw hex in onboarding.tsx:18,24,30Low
Ruby AI mark + animationsZero AI surfaces anywhereHigh (product-level gap)
Form primitives (FormField, FormLabel…)None — raw Pressable + TextInputMedium
Status Badge / Chip componentNoneMedium
Skeleton loadingNoneLow
ResponsiveTableNoneN/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 for role, 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

ModuleState
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 tabStub (empty)
Profile (edit, history, security, help)Stub — handlers () => {}
Ruby / AIAbsent

2. Web app: source-of-truth onboarding & roles

2.1 Onboarding flows in production webapp

PathComponentAudience
/onboarding/trialui/src/components/onboarding/TrialOnboarding.tsxProspect signing up for trial — manual superadmin approval (CLAUDE memory: anti-competitor gate)
/invitations/:tokenui/src/pages/InvitationAccept.tsxInvited staff or patient — collects DOB/gender/phone (patient), password, 2FA (passkey/TOTP/email)
/onboarding/complete/:tokenui/src/components/auth/CompleteOnboarding.tsxPre-provisioned account → email OTP → password
Post-login: /select-clinic/dashboardSelectClinic.tsx + role-based dashboardsAll authed users
The webapp itself doesn’t ship a guided in-app tour — it relies on the sidebar + role-specific dashboard for discovery. So the mobile app should arguably do better than the web here, not match it.

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 — Clinical
  • receptionist — Front desk
  • patient — Self-service portal
  • superadmin — Platform operator (not a clinic role; should not be on mobile)
202 granular permissions across 17 modules: Appointments, Patients, Clinical, Billing, Inventory, Lab, Communications, Reports, AI Insights, Settings, Notifications, Audit, IPD, Radiology, Referrals, Bridge, Files.

2.3 Plan-based gates

Receptionist inventory access depends on plan tier (Default → view-only, Pro → full, Pro+ → manage). Radiology Workstation requires dicom_imaging feature flag. The mobile app must consult the same plan / feature flag service the web uses.
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

  1. Today — KPI strip (revenue, appointments, no-shows, outstanding), Ruby daily brief
  2. Calendar — clinic-wide schedule, block time, see all chairs/rooms
  3. Patients — search, recent, quick actions (call, message)
  4. Approvals — pending invoices to write off, expense approvals, refund requests, lab acceptance
  5. More — Reports (top-line only), Notifications, Switch clinic, Settings
Off mobile: payroll, staff management, deep analytics, service catalog editing, plan/billing config.

3.2 Doctor — 5 tabs

  1. Today — my schedule, current patient card, Ruby clinical assist
  2. Patients — my list, search, quick chart preview, prescriptions
  3. Charting — view dental chart, add findings, sign clinical note (mobile-friendly chart only — full editing stays desktop)
  4. Files — X-rays, lab results, Bridge inbox (review + approve)
  5. More — Treatment plans, Prescriptions, Notifications, Switch clinic
Off mobile: treatment plan authoring (review + sign yes), advanced charting, financial settings.

3.3 Receptionist — 5 tabs

  1. Today — appointment ribbon, walk-ins queue, no-show list, recall list
  2. Calendar — book / reschedule / move blocks
  3. Patients — create / edit / call / SMS / email
  4. Billing — create invoice, record payment, send receipt (Banknote icon!), insurance claim status
  5. More — Lab cases, Inventory (plan-gated), Notifications
Off mobile: reports, expenses, deep config.

3.4 Patient — 5 tabs (current app’s audience)

  1. Home — next appointment card, treatment plan to accept, prescription refills, balance due
  2. Appointments — my schedule, book, reschedule
  3. Records — clinical notes, prescriptions, X-rays/files, dental chart (read-only)
  4. Billing — invoices, pay (Stripe / local gateway), installment plan
  5. 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 commit e256035c — 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):
Cold launch

[Splash] OdontoX logo + Ruby pulse (animated, ≤ 1s)

[Welcome] One brand screen: logo + "Pakistan's AI-first dental clinic OS"

[Value props — 3 slides, swipeable, skippable]
  1. "Run your day from your pocket" → today's schedule mockup
  2. "Ruby, your AI co-pilot" → Ruby suggesting next action
  3. "Trusted by clinics nationwide" → trust signals (HIPAA, +92, PKR)

[Auth choice] Sign in (default) ⟷ I have an invitation (paste token / scan QR)

[Sign in] Email → Passkey (preferred) → fallback magic link / OTP
       (Sign in with Apple required if we add Google — currently not, so skipable)

[Role detection] Server returns roles[] + clinics[]
  ↓ (multi-clinic)        ↓ (single)
[Clinic picker]           [skip]

[Permission priming card] "We'll ask for notifications next — used only for your appointments and approvals" → [Continue] → native prompt

[Biometric priming] "Use Face ID to unlock OdontoX" → opt-in → enrolls in expo-secure-store

[Role-tailored 3-card tour]
  - Admin: Today / Approvals / Calendar
  - Doctor: Today / Patient quick chart / Bridge files
  - Receptionist: Calendar / Quick book / Take payment
  - Patient: Next visit / Treatment plan / Messages

[Tabs] (the right set for the detected role)

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_assignments change 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 reads useColorScheme().
  • iOS app icon: produce a .icon Apple Icon Composer bundle so we get default + dark + tinted automatically. Wire via expo.ios.icon (SDK 54+).
  • Android adaptive icon: ensure foreground / background / monochrome are all set so themed icons (Material You) work. Replace the #E6F4FE baby-blue background with the on-brand indigo (#5048E5) or a neutral card token.
  • Splash: switch to the new expo-splash-screen config (single 1024² brand mark + brand background + Ruby pulse via SplashScreen.setOptions({ duration, fade })).

5.2 References


6. Best-practices alignment (Expo + iOS HIG + Material 3)

6.1 Architecture changes

AreaCurrentTarget
Routingmixed groups(auth)/, (onboarding)/, (tabs-admin)/, (tabs-doctor)/, (tabs-receptionist)/, (tabs-patient)/ with Stack.Protected auth gate in root _layout.tsx
Tabscustom FloatingTabBarExpo Router native tabs (gets iOS 26 Liquid Glass tab bar automatically when built with Xcode 26)
iPadnot addressedsidebarAdaptable tab pattern → tab bar on phone, sidebar on iPad
Design tokensparallel AtelierTheme.tsNativeWind v4 consuming the same theme.ts Tailwind config the webapp uses
Fontsruntime useFonts (Manrope)expo-font config plugin embed (Poppins per design.md), system font for chrome on iOS for Dynamic Type
Authemail + passkey + MFAAdd Sign in with Apple only if we add Google/Facebook SSO (Guideline 4.8) — not needed today
Tokens at restunclearexpo-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 AppStateactive after > 30 s in background.
  • Privacy overlay on the app preview snapshot when backgrounded.
  • Disable copy/paste on PHI fields (contextMenuHidden on TextInput).
  • 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

#FindingImpactEffort
F1Patient-only app; no role awarenessBlocks the user’s stated goal of “all roles”XL — re-architect
F2Logo single variant, no theme swap, outdated icon strategyBrand inconsistency in dark mode + iOS 18/26S
F3Onboarding doesn’t showcase features, no permission/biometric primingFirst-run conversion + perceived polishM
F4Wrong primary color (#630ed4 vs #5048E5)Brand consistencyS
F5Manrope vs PoppinsBrand consistencyS
F6No Banknote icon for money UIdesign.md §2.3 violation, low riskXS
F7No dark paletteHalf the app is unbuilt for dark modeM
F8No Ruby/AI surfaces — design.md §18 entirely missingProduct positioningL
F9No HIPAA mitigations (FLAG_SECURE, idle timeout, privacy overlay)Compliance blocker before staff/clinical roles shipM
F10Design tokens duplicated across AtelierTheme and webappMaintenance debtM (one-time NativeWind migration)
F11No form primitives (RHF + Zod), Skeleton, Badge, SheetRe-implements design.md primitivesM
F12Profile/Reminders tabs are stubsUX gapS
F13Custom FloatingTabBar instead of native tabsLoses iOS 26 Liquid GlassS
F14No iPad layoutApp will run but not feel iPad-nativeM
F15Adaptive icon background #E6F4FE not on brandVisualXS

Phase 1 — Foundations (1 sprint)
  • Migrate to NativeWind, share theme.ts with web (fixes F4, F5, F7, F10)
  • Refresh logo/icon/splash with light+dark variants and .icon bundle (F2, F15)
  • Switch to native tabs (F13)
Phase 2 — Onboarding & multi-role (2 sprints)
  • Per-role tab groups + role detection + clinic switcher (F1)
  • New onboarding flow with feature showcase + priming + tour (F3)
  • Form primitives, Badge, Skeleton, Sheet (F11)
Phase 3 — HIPAA + AI surfaces (1 sprint)
  • 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)
Phase 4 — Polish (1 sprint)
  • 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

  1. 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.)
  2. Sign in with Apple — do we plan to add Google / Facebook SSO? If yes, Apple is mandatory; if no, we can skip.
  3. Biometric policy — do you want hard biometric on every cold launch, or grace period (e.g. 8h) for non-PHI screens?
  4. Patient pay — confirm payment provider on mobile (Stripe vs local Pakistani gateway like JazzCash/Easypaisa). This affects the Billing tab design.
  5. 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):