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 |
Navigation System¶
| 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:
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";
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
XMLHttpRequestfor progress tracking (can't use fetch) - Login/signup fetch calls in AuthContext — remain as raw
fetch()since they run before any token exists