Mobile Workout Flow (Current Implementation)¶
This document describes what is currently implemented in Okta-Mobile for the patient workout flow.
High-Level Flow¶
Workout navigation is driven by useWorkoutFlow and WorkoutMode. The flow is optimized for the fewest taps: arriving at an exercise means it has started, and the feedback step is a single screen.
No recording: WORKOUT_SETUP -> EXERCISE_ACTIVE -> FEEDBACK -> EXERCISE_ACTIVE -> ... -> COMPLETION
Recording: WORKOUT_SETUP -> RECORD_SETUP -> RECORDING -> FEEDBACK -> RECORD_SETUP -> ... -> COMPLETION
Clinics with the workoutLogbook flag open a different screen from Start workout: app/(patient)/(workout)/log/[id].tsx, the whole session as one list logged set by set (see Workout Logbook). Everything below is the guided flow every other clinic uses.
WORKOUT_SETUP is skipped entirely when there is nothing to decide (saved progress exists, or the workout isn't circuit-eligible and the camera is permanently denied). Progress is persisted to the server at every exercise boundary, so any exit is resumable — see Patient Workout Completion, Resume & Quick-Finish.
Main router file: Okta-Mobile/app/(patient)/(workout)/[id].tsx
Screen Orchestration ([id].tsx)¶
- Uses
useWorkoutData(id)to fetch the workout (with any savedprogressState). The routeidparam is normalized (array-valued params are rejected). Start time is no longer patched on load — the first progress write is the authoritative session-start signal. - Uses
useWorkoutFlow(workout, { restart })for mode/state transitions, entry resolution (resume vs fresh vs skip-setup), and progress persistence. - The loading spinner is held until
entryResolvedso a resumed session never flashes the setup screen. - A workout with zero exercises renders a friendly empty state with a Close action instead of a blank flow.
- Renders one screen per mode:
InitialWorkoutSetupScreeninWORKOUT_SETUPExerciseActiveScreeninEXERCISE_ACTIVE(merged setup + execution; the exercise is already running)WorkoutSetupScreeninRECORD_SETUP(recording pre-screen, single "Start Recording" button)VideoExerciseExecutionScreeninRECORDINGPatientFeedbackModalinFEEDBACK(a first-class mode, single screen)WorkoutCompletionScreeninCOMPLETIONExercisesListModalas inline overlay- The screen container is a side-edges-only
SafeAreaView(react-native-safe-area-context): the navigation header owns the top inset and every footer/sheet appliesinsets.bottomitself — a bottom-padded container here would double the inset and float the buttons above the home indicator.
Header + Progress¶
- Header is configured with
Stack.Screenin[id].tsx. - On setup/execution modes, title shows sets x reps/seconds and subtitle shows exercise name.
- Left action (✕) exits via
router.back()— safe at any point because progress is already persisted; no confirmation dialog. - Right action (exercise list pill) is shown in
EXERCISE_ACTIVEandRECORD_SETUP(regular mode only). - A small status line under the progress bar surfaces the progress-save state (saving / offline / error) so patients know unsent progress will retry.
WorkoutProgressBaris shown in all modes exceptWORKOUT_SETUP.- Progress behavior (
WorkoutProgressBar.tsx): - 8px rounded progress track with Reanimated animated fill (
withTiming, 250ms) - Fill color:
#10B981(green) - Small right-aligned counter text (
X/Y, e.g.2/5) - Safe divisor guard (
Math.max(totalExercises, 1))
Initial Setup (InitialWorkoutSetupScreen.tsx)¶
Single-CTA layout: the screen leads with one Start workout PrimaryButton; camera and mode are compact secondary controls, both pre-filled from the last session.
- Header: "Ready to start?" title with the workout name and exercise count as subtitle.
- Start workout (
PrimaryButton, play icon): persists the current choices vialib/workoutSetupPrefs.ts, then callsonEnableCamera(mode)when the camera switch is on, otherwiseonStartWithoutRecording(mode). - Record with camera switch row (off by default): small card with
videocam-outlineicon, label, and caption. No permission logic in the component —enableCameraForWorkoutrequests permissions and falls back to a no-recording session on denial. - Mode pill (circuit-eligible workouts only, i.e. at least one exercise block with 2+ exercises): a two-segment
Regular | Circuitcontrol with the selected mode's one-line description beneath. Haptic feedback on change. - Remembered preferences (
lib/workoutSetupPrefs.ts, AsyncStorage keysworkout-setup-camera/workout-setup-mode, device-level): the last-used camera and mode choices pre-fill the screen on the next session. A rememberedCIRCUITmode is ignored on workouts that aren't circuit-eligible.
Permission behavior¶
Permission logic is centralized in useWorkoutFlow.enableCameraForWorkout():
- Requests both camera and microphone permissions.
- Enables recording mode only if both are granted.
- On denial the workout still starts — as a no-recording session with a dismissible notice on the first exercise (no dead end).
On load, useWorkoutFlow checks permission status with getCameraPermissionsAsync/getMicrophonePermissionsAsync (no permission prompt on mount).
Recording Pre-Screen (WorkoutSetupScreen.tsx)¶
Only shown on the recording path (RECORD_SETUP). The record-or-not decision was already made at workout setup, so the only action is starting the camera — the camera must not auto-start while the patient is still positioning the phone.
- Shows exercise title, block chip, and a green sets/reps pill badge.
- Shows video player (with loading skeleton), description, and the doctor guideline card via
DoctorPrescriptionDisplaywhen present. - One floating
PrimaryButton(record variant): Start Recording.
On the no-recording path there is no pre-screen at all — EXERCISE_ACTIVE starts immediately.
Video Player (WorkoutVideoPlayer.tsx)¶
Supports three video sources with priority: Cloudflare Stream > Kinescope > YouTube.
- Loading skeleton: While any video source loads, an overlay shows either the exercise thumbnail image (if available) or a dark fallback (
#1F2937), with a semi-transparent tint and centered spinner. Loading state resets when the exercise changes (sourceKey). - Kinescope/Cloudflare: Rendered via
WebViewinside a wrapperView, with the WebView usingabsoluteFillObject(fixes Android layout issues withflex:1on WebViews). - YouTube: Rendered inside a measured container (
onLayout). Player height is computed asMath.min(maxHeight, (width * 9) / 16)for true 16:9 aspect ratio fit. Player only renders whenwidth > 0to prevent zero-width flash.
Active Exercise (ExerciseActiveScreen.tsx)¶
The merged setup + execution screen for the no-recording path: arriving here means the exercise has started.
- Full-screen exercise video or fallback info block.
- Timer is inline (
useState+setInterval) and starts on arrival; time-based exercises show the per-set target next to the count-up (e.g.0:12 / 0:30). - Optional quick-rating affordance opens
QuickRatingSheet(pain/exertion 0–10) mid-exercise; values pre-fill the feedback screen and never block the flow. - Shows the dismissible camera-fallback notice when recording was requested but permissions were denied.
- The finish button reads Finish Set on circuit intermediate visits and Finish Exercise otherwise.
- Unified HUD overlay at top with three variants:
- Full HUD (frosted dark card): shown when prescription data exists. Displays sets/reps pills, doctor notes, and timer. Tappable to open
ExerciseInfoSheetif description exists. - Compact HUD (row layout): shown when no prescriptions but description exists. Shows info icon + "Exercise Info" label + divider + timer.
- Timer-only HUD: shown when neither prescriptions nor description exist.
- Floating green finish button at bottom with right-arrow icon and drop shadow.
ExerciseInfoSheetrendered inline for description/details.- Keeps device awake with
useKeepAwake().
Execution With Recording (VideoExerciseExecutionScreen.tsx)¶
- Split view with a thin white border separator between halves:
- Top: demo video
- Bottom:
CameraView(mode="video", fixedfacing="front") - Auto-starts recording ~300ms after camera is ready.
- On recording completion, queues upload via
videoUploadService.queueUpload(workoutId, exerciseId, uri). - Shows upload queued badge (green, bottom-right) when recording stopped.
- Recording indicator integrated into HUD with an animated pulsing red dot (Reanimated
withRepeat/withTiming, opacity 1 -> 0.3 at 600ms). - One-tap finish: the green Finish Exercise button stops any active recording and finishes atomically. A small "Stop camera" pill covers the rare film-one-set case.
- Unmounting mid-recording (✕ exit, back gesture) stops the camera so the take still resolves and queues for upload.
- Uses the same HUD variants as non-recording execution.
ExerciseInfoSheetrendered inline.- Haptics on finish/stop actions.
- Keeps device awake with
useKeepAwake().
Post-Exercise Feedback (PatientFeedbackModal.tsx)¶
Rendered inline (not RN Modal) as the FEEDBACK mode — one screen, one button:
- Sets completed:
SetButtoncircles pre-selected to the planned count, so a patient who did everything changes nothing. Hidden on circuit final visits (feedbackOnly). - Pain + exertion: shared
PillRatingRow(0–10, measured layout — a single row when eleven 44pt targets fit, otherwise a staggered two-row grid; small pills get hitSlop up to 44pt). Pre-filled from the mid-exercise quick rating when used. - Note: collapsed behind an "Add a note (optional)" row; expands to a keyboard-aware input (
KeyboardAwareScrollView+KeyboardToolbarwith a Done action). - Single
PrimaryButton: Next Exercise (or Finish Workout on the last step). Tapping it records everything and immediately enters the next exercise. A submit lock prevents a rapid double-press from advancing two steps. - A Back link (no-recording path only) returns to the exercise.
Completion (WorkoutCompletionScreen.tsx)¶
- Root is a
Viewholding aScrollView(paddingBottom: Math.max(insets.bottom, 24)) and theSessionCelebrationoverlay on top of it. - Animated title scale-in (Reanimated
withTiming, scale 0.96 -> 1 in 200 ms). - Celebration: when the summary is reached by finishing the last exercise (
useWorkoutFlowexposesjustFinished), the overlay plays the confetti burst andhapticCelebrate()as the screen appears, rather than when the save answers. A failed save still shows the warning under it, and a retry does not celebrate again. A resume that lands on the summary (progress pointer already past the end) shows it quietly. The quick-finish success view and the logbook's finish sheet mount the same overlay, with the same popping check (CelebrationCheck), once their save succeeds; the logbook sheet then leaves for the dashboard on the overlay'sonDone, which always comes (a fallback fires 0.5 s after the burst's planned end). - Stats card with two columns separated by a vertical divider:
- Total exercises (large green number)
- Total completed sets (large green number)
- Upload status section appears when there are queued uploads (spinner color: green
#10B981). - Per-upload status list with retry for failed uploads (
nestedScrollEnabledinside ScrollView). - Completion is automatic: reaching this screen triggers
persistWorkoutCompletion(drain pending feedback,POST /workouts/:id/complete, 409-tolerant, deduped by a completion ref). The Done button only navigates — or retries if the save failed, with an inline warning state. - Done is disabled while uploads are active.
- Keeps device awake while on this screen.
Exercise List Modal (ExercisesListModal.tsx)¶
- Inline absolute overlay (not RN
Modal), rendered within workout screen withzIndex: 200, elevation: 200. - Conditional render (
nullwhen!visible) instead ofvisibleprop on Modal. - Darkened backdrop
Pressablehandles close on tap. - Shows all exercises with current/completed visual states; the sheet applies its own bottom inset.
- The list is available in
EXERCISE_ACTIVEandRECORD_SETUP(regular mode only). - Jumping lands on the target exercise's entry screen and persists the moved pointer (enforced in
useWorkoutFlow.navigateToExercise). - On Android,
BackHandlerlistener closes the overlay on hardware back press.
Doctor Prescription Display (DoctorPrescriptionDisplay.tsx)¶
- Neutral card with green-tinted border (
rgba(16,185,129,0.15)). - Header with
medical-outlineIonicons icon. - Metrics row: horizontal row of white
metricCardcomponents (label + badge,borderRadius: 12). - Doctor notes section with
chatbubble-ellipses-outlineicon, separated by a thin divider. - Supports compact mode with optional
showInfoIconandonPressprops for use in execution HUDs.
Circuit Mode & Step-Based Sequencing¶
Workout progression is driven by a step sequence built from the exercise list and the selected playback mode.
Sequencing (workoutSequence.ts)¶
WorkoutPlaybackMode:"REGULAR"|"CIRCUIT".isCircuitEligible(exercises): Returnstruewhen any exercise block contains 2+ exercises.buildSteps(exercises, mode): Returns an ordered array ofWorkoutStepobjects.
Each WorkoutStep carries:
| Field | Description |
|---|---|
exerciseId |
The workout exercise record ID |
block |
Exercise block (WARM_UP, MAIN_SESSION, etc.) |
roundNumber |
1-indexed round within the block |
totalRoundsInBlock |
Total rounds for the block (= max planned sets among block exercises) |
visitNumberForExercise |
1-indexed visit to this specific exercise |
totalVisitsForExercise |
Total visits (= planned sets for this exercise) |
isFinalVisitForExercise |
Whether this is the last visit (triggers feedback) |
Regular mode: One step per exercise (all sets handled in-exercise). Each step has isFinalVisitForExercise: true.
Circuit mode: Exercises are grouped by block, then interleaved round-by-round. For a block with exercises A (3 sets), B (2 sets), C (3 sets), the step order is: A1 B1 C1 A2 B2 C2 A3 C3. Exercises with fewer planned sets drop out of later rounds.
Step-based useWorkoutFlow¶
useWorkoutFlowtracks acurrentStepIndexinto the steps array (replacing the old exercise-index approach).currentExerciseandcurrentExerciseIndexare derived from the current step.- Intermediate visits (circuit mode,
isFinalVisitForExercise === false):finishExercise()records one completed set, persists the moved pointer, and auto-advances without a feedback screen — one tap per set. - Final visits (
isFinalVisitForExercise === true):finishExercise()switches to theFEEDBACKmode. - Exercise list navigation is disabled in circuit mode (order is locked by the sequencing algorithm).
- The resume envelope stores the step index plus a
currentExerciseInWorkoutIdchecksum andversion: 1; hydration validates all three (and clamps) before restoring, falling back to a fresh start when the therapist edited the plan.
Web implementation¶
The web workout page (OktaPT-FE/pages/patient/workout/new-layout/[id].tsx) shares the same workoutSequence.ts utility and an equivalent useWorkoutFlow hook. The WorkoutInitialSetupModal shows Regular/Circuit toggle buttons (pill-shaped) when the workout is circuit-eligible.
Data + API Behavior¶
useWorkoutData.ts¶
- Fetches workout by ID (
GET /workouts/:id), including any savedprogressStatefor resume. - Effect-scoped cancellation: a stale response can't update state or fire an alert after the screen is gone.
- No start-time patch — the first progress write sets
timeWorkoutStartserver-side.
useWorkoutFlow.ts + workoutProgressService.ts¶
- Manages the step index, completed sets, feedback, mode transitions, entry resolution, and resume hydration.
- Per-boundary persistence: every exercise boundary sends
PATCH /v2/workouts/:id/progress(resume envelope + the just-finished exercise's actuals, duration, and feedback) throughworkoutProgressService— a fire-and-forget queue with offline retry, transient-error retention, in-flight eviction protection, and a recovery list for feedback the server rejected. - Completion: entering the summary screen flushes the queue, drains any undelivered feedback via the batch endpoint, then
POST /workouts/:id/complete(409-tolerant — a race with quick-finish counts as done). lib/httpClient.tsapplies a 15s request timeout and treats timeouts as retryable network errors; empty success bodies parse asnullinstead of throwing.
videoUploadService.ts¶
- In-memory upload queue with up to 3 concurrent uploads.
- Steps:
- Request upload URL (
/workout-recording/upload-url) - Read local file blob
- PUT upload with XHR progress updates
- Exposes subscribe/retry/clear helpers used by
useVideoUploadProgress.
Shared Styles¶
Shared workout styles live in Okta-Mobile/components/workout/styles.ts and export workoutStyles. These cover container, loading, setup, execution, completion, modal, exercise list, upload section, and progress bar base styles. Screen-specific styles are defined locally via StyleSheet.create at the bottom of each screen file.
Active File Map¶
Okta-Mobile/app/(patient)/(workout)/[id].tsxOkta-Mobile/app/(patient)/(workout)/_layout.tsxOkta-Mobile/hooks/useWorkoutData.tsOkta-Mobile/hooks/useWorkoutFlow.tsOkta-Mobile/lib/workoutSequence.tsOkta-Mobile/hooks/useVideoUploadProgress.tsOkta-Mobile/services/videoUploadService.tsOkta-Mobile/services/workoutProgressService.tsOkta-Mobile/components/workout/styles.tsOkta-Mobile/components/workout/InitialWorkoutSetupScreen.tsxOkta-Mobile/components/workout/WorkoutSetupScreen.tsxOkta-Mobile/components/workout/ExerciseActiveScreen.tsxOkta-Mobile/components/workout/QuickRatingSheet.tsxOkta-Mobile/components/workout/VideoExerciseExecutionScreen.tsxOkta-Mobile/components/workout/PatientFeedbackModal.tsxOkta-Mobile/components/workout/WorkoutCompletionScreen.tsxOkta-Mobile/components/workout/WorkoutProgressBar.tsxOkta-Mobile/components/workout/ExerciseInfoSheet.tsxOkta-Mobile/components/workout/DoctorPrescriptionDisplay.tsxOkta-Mobile/components/workout/WorkoutVideoPlayer.tsxOkta-Mobile/components/workout/ExercisesListModal.tsxOkta-Mobile/components/workout/ExerciseDetailSheet.tsxOkta-Mobile/components/ui/PrimaryButton.tsxOkta-Mobile/components/ui/PillRatingRow.tsxOkta-Mobile/app/(patient)/(workout)/quick-finish/[id].tsxOkta-Mobile/types/workout.ts