Skip to content

Mobile Role Separation — Detailed Changes

Date: 2026-02-19 Repo: Okta-Mobile Branch: development Commit: f2ad105 Scope: 57 files changed, 1,567 insertions, 1,322 deletions

Architecture and design decisions are documented in docs/mobile-role-separation.md. This document covers the file-by-file migration.


Summary

Migrated the Okta-Mobile app from a monolithic dashboard with inline role checks to a route-group-based architecture where patient and doctor screens live in separate Expo Router groups ((patient)/ and (doctor)/), guarded by a RoleGuard component. Simultaneously extracted all duplicated HTTP/auth boilerplate from four service files into a shared httpClient singleton.


New Files

Route Guards & Auth

File Lines Purpose
components/auth/RoleGuard.tsx 30 Wraps route groups; redirects if user role doesn't match allow prop
__tests__/roleGuard.test.tsx 142 7 test cases: matching role, wrong role redirect, loading state, unauthenticated redirect, custom redirectTo

Role-Specific Layouts

File Lines Purpose
app/(patient)/_layout.tsx 16 RoleGuard allow="PATIENT" → Stack with (workout) and plan-management screens
app/(doctor)/_layout.tsx 12 RoleGuard allow="DOCTOR" → Stack with (clinical-assessment) screens

Role-Specific Dashboards

File Lines Purpose
components/patient/PatientDashboard.tsx 156 Workout cards, empty state, plan management card, pull-to-refresh, timezone auto-sync
components/doctor/DoctorDashboard.tsx 253 "Start In-Clinic Test" button, recent assessments list (last 5), view-all-tests link, timezone auto-sync
File Lines Purpose
components/navigation/CustomTabBar.tsx 294 Floating pill-shaped tab bar with animated indicators and per-tab accent colors
components/navigation/StreakHeaderWidget.tsx 184 Compact streak display in dashboard header (daily count + weekly calendar dots)
components/navigation/tabBarConfig.ts 80 Tab definitions: icons, colors, labels, role visibility rules

HTTP Client

File Lines Purpose
lib/httpClient.ts 91 Singleton with get<T>, post<T>, put<T>, patch<T>, delete<T> — auto-injects Bearer token and Origin header

Styling & Config

File Lines Purpose
components/workout/styles.ts Moved from app/(workout)/styles.ts (zero changes to content)
.eslintrc.js 41 Import boundary enforcement: patient routes can't import components/doctor/* and vice versa

Deleted Files

All deleted files were moved, not removed. Content was relocated into role-specific route groups.

Original Path New Path Notes
app/(clinical-assessment)/_layout.tsx app/(doctor)/(clinical-assessment)/_layout.tsx Identical
app/(clinical-assessment)/index.tsx app/(doctor)/(clinical-assessment)/index.tsx Import paths updated to @/ aliases
app/(clinical-assessment)/processing.tsx app/(doctor)/(clinical-assessment)/processing.tsx Import paths updated
app/(clinical-assessment)/recent-tests.tsx app/(doctor)/(clinical-assessment)/recent-tests.tsx Import paths updated
app/(clinical-assessment)/record.tsx app/(doctor)/(clinical-assessment)/record.tsx Import paths updated
app/(clinical-assessment)/results.tsx app/(doctor)/(clinical-assessment)/results.tsx Import paths updated
app/(workout)/[id].tsx app/(patient)/(workout)/[id].tsx Import paths updated, style import changed
app/(workout)/styles.ts components/workout/styles.ts Moved to live alongside consumer components
app/(workout)/video/[videoUrl].tsx app/(patient)/(workout)/video/[videoUrl].tsx Identical
app/plan-management/_layout.tsx app/(patient)/plan-management/_layout.tsx Identical
app/plan-management/follow-up-questions.tsx app/(patient)/plan-management/follow-up-questions.tsx Import paths updated
app/plan-management/index.tsx app/(patient)/plan-management/index.tsx Import paths updated
app/plan-management/new-plan-preview.tsx app/(patient)/plan-management/new-plan-preview.tsx Import paths updated
app/plan-management/view-plan.tsx app/(patient)/plan-management/view-plan.tsx Import paths updated
components/CompactStreakCard.tsx Replaced by components/navigation/StreakHeaderWidget.tsx

Modified Files

Core Layout & Routing

app/_layout.tsx (+13 −6) - Added (patient) and (doctor) as Stack.Screen entries in the root navigator - Added Sentry initialization with DSN

app/(tabs)/_layout.tsx (+44 −existing) - Replaced default tab bar with CustomTabBar component - Added role-aware tab visibility: History tab hidden for doctors (href: null) - Tab config now driven by tabBarConfig.ts

app/(tabs)/dashboard.tsx (544 → ~9 lines) - Gutted from ~544-line monolith to 9-line dispatcher:

const { isDoctor } = useAuth();
return isDoctor ? <DoctorDashboard /> : <PatientDashboard />;
- All state, API calls, and UI moved to PatientDashboard.tsx and DoctorDashboard.tsx

app/(tabs)/history.tsx (+7 −existing) - Minor: added role-aware header tint color

app/(tabs)/messages.tsx (+6 −existing) - Minor: added role-aware header tint color

app/(tabs)/profile.tsx (+3) - Minor: added header styling

Auth Context

context/AuthContext.tsx (+128 −existing, net refactor) - Added UserType type alias: "PATIENT" | "DOCTOR" - Added typed User interface with userType: UserType - Added isDoctor and isPatient convenience booleans to context value - Login flow now extracts userType from API response (data.type) and persists to AsyncStorage - Signup flow stores userType from API response - Logout clears userType from AsyncStorage - Replaced raw fetch() calls in autoUpdateUserTimezone with httpClient.post() - Note: Debug console.log statements remain on login response (lines ~104-106)

Service Layer — httpClient Migration

All four services followed the same pattern: remove private getAuthHeaders() + request() methods, delegate to httpClient.

services/api.ts (−102 lines net) - Removed: getAuthHeaders(), fetchWithAuth(), API_URL constant - All methods now use httpClient.get<T>() / httpClient.post<T>() / etc. - Example: this.fetchWithAuth("/v2/patient/workouts-today")httpClient.get<...>("/v2/workouts/patient-upcoming")

services/clinicalAssessmentService.ts (−57 lines net) - Removed: getAuthHeaders(), request(), API_URL - All methods delegate to httpClient - Example: this.request(/v2/clinical-assessment/${id})httpClient.get<Assessment>(/v2/clinical-assessment/${id})

services/messagingService.ts (−94 lines net) - Removed: getAuthHeaders(), request(), API_URL - All methods delegate to httpClient - getAvailableDoctors() and getAvailablePatients() now return typed responses

services/videoUploadService.ts (−35 lines net) - Removed: getAuthHeaders(), API_URL - Pre-upload API call uses httpClient; actual upload still uses XMLHttpRequest (needed for progress tracking)

Component Updates

components/PlanManagementCard.tsx (+2 −2) - Route path: "/plan-management""/(patient)/plan-management"

components/WorkoutCard.tsx (+45 −existing) - Route path: "/(workout)/[id]""/(patient)/(workout)/[id]" - Minor layout/styling updates

components/messaging/NewConversationSheet.tsx (+14 −existing) - Added useAuth() import for isDoctor flag - Conditionally fetches contacts: isDoctor ? getAvailablePatients() : getAvailableDoctors() - Contact name display: doctors see patient full names, patients see "Dr. " prefix

Workout components (10 files, +2 each) All changed a single import path:

- import { workoutStyles as styles } from "../../app/(workout)/styles";
+ import { workoutStyles as styles } from "./styles";
Files: ExercisesListModal, ExerciseThumbnailRow, InitialWorkoutSetupScreen, PatientFeedbackModal, SetCompletionModal, WorkoutCompletionScreen, WorkoutExecutionScreen, WorkoutProgressBar, WorkoutSetupScreen, WorkoutTimer, WorkoutVideoPlayer

Auth & Onboarding Screens

app/email-entry.tsx (+2 −2) - Minor import path update

app/welcome.tsx (+4 −2) - Minor import path update

Localization

locales/en.json (+12 −existing) - Added: messages.noPatients ("No patients connected") - Minor key adjustments for role-aware empty states

locales/ru.json (+12 −existing) - Russian translations for same new keys

Configuration

package.json (+5 −existing) - Added Jest moduleNameMapper for @/ path alias support in tests

Removed

.github/workflows/eas-update.yml (−68 lines) - Removed EAS Update GitHub Actions workflow


Migration Patterns

Pattern 1: Moving a route into a role group

# Before
app/(workout)/[id].tsx
  imports from "../../services/api"
  imports from "../../app/(workout)/styles"

# After
app/(patient)/(workout)/[id].tsx
  imports from "@/services/api"
  imports from "@/components/workout/styles"

Steps: 1. Move file to app/(role)/ subdirectory 2. Update relative imports to @/ aliases (avoids depth-dependent paths) 3. Update any router.push() / router.replace() calls elsewhere that reference the old path

Pattern 2: Extracting a service to httpClient

// Before (in every service)
private async getAuthHeaders() {
  const token = await AsyncStorage.getItem("accessToken");
  const origin = getTenantOrigin();
  return { Authorization: `Bearer ${token}`, Origin: origin };
}

private async request<T>(path: string, options?: RequestInit): Promise<T> {
  const headers = await this.getAuthHeaders();
  const res = await fetch(`${API_URL}${path}`, { ...options, headers: { ...headers, ...options?.headers } });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

// After (one-liner per call)
async getAssessment(id: number) {
  return httpClient.get<Assessment>(`/v2/clinical-assessment/${id}`);
}

Pattern 3: Role-aware component dispatch

// Before: 544-line dashboard.tsx with inline if (userType === "DOCTOR") branches
// After: 9-line dispatcher
export default function Dashboard() {
  const { isDoctor } = useAuth();
  return isDoctor ? <DoctorDashboard /> : <PatientDashboard />;
}

What Was NOT Changed

  • Public/unauthenticated screens (welcome, login, signup, email-entry) — remain at root level
  • Conversation/messaging screens — shared by both roles, no group needed
  • AI workout builder (onboarding) — unchanged
  • Video upload — still uses XMLHttpRequest for progress tracking (can't use fetch)
  • Login/signup fetch calls in AuthContext — remain as raw fetch() since they run before any token exists