Skip to content

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 saved progressState). The route id param 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 entryResolved so 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:
  • InitialWorkoutSetupScreen in WORKOUT_SETUP
  • ExerciseActiveScreen in EXERCISE_ACTIVE (merged setup + execution; the exercise is already running)
  • WorkoutSetupScreen in RECORD_SETUP (recording pre-screen, single "Start Recording" button)
  • VideoExerciseExecutionScreen in RECORDING
  • PatientFeedbackModal in FEEDBACK (a first-class mode, single screen)
  • WorkoutCompletionScreen in COMPLETION
  • ExercisesListModal as 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 applies insets.bottom itself — a bottom-padded container here would double the inset and float the buttons above the home indicator.

Header + Progress

  • Header is configured with Stack.Screen in [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_ACTIVE and RECORD_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.
  • WorkoutProgressBar is shown in all modes except WORKOUT_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 via lib/workoutSetupPrefs.ts, then calls onEnableCamera(mode) when the camera switch is on, otherwise onStartWithoutRecording(mode).
  • Record with camera switch row (off by default): small card with videocam-outline icon, label, and caption. No permission logic in the component — enableCameraForWorkout requests 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 | Circuit control with the selected mode's one-line description beneath. Haptic feedback on change.
  • Remembered preferences (lib/workoutSetupPrefs.ts, AsyncStorage keys workout-setup-camera / workout-setup-mode, device-level): the last-used camera and mode choices pre-fill the screen on the next session. A remembered CIRCUIT mode 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 DoctorPrescriptionDisplay when 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 WebView inside a wrapper View, with the WebView using absoluteFillObject (fixes Android layout issues with flex:1 on WebViews).
  • YouTube: Rendered inside a measured container (onLayout). Player height is computed as Math.min(maxHeight, (width * 9) / 16) for true 16:9 aspect ratio fit. Player only renders when width > 0 to 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 ExerciseInfoSheet if 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.
  • ExerciseInfoSheet rendered 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", fixed facing="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.
  • ExerciseInfoSheet rendered 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: SetButton circles 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 + KeyboardToolbar with 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 View holding a ScrollView (paddingBottom: Math.max(insets.bottom, 24)) and the SessionCelebration overlay 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 (useWorkoutFlow exposes justFinished), the overlay plays the confetti burst and hapticCelebrate() 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's onDone, 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 (nestedScrollEnabled inside 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 with zIndex: 200, elevation: 200.
  • Conditional render (null when !visible) instead of visible prop on Modal.
  • Darkened backdrop Pressable handles close on tap.
  • Shows all exercises with current/completed visual states; the sheet applies its own bottom inset.
  • The list is available in EXERCISE_ACTIVE and RECORD_SETUP (regular mode only).
  • Jumping lands on the target exercise's entry screen and persists the moved pointer (enforced in useWorkoutFlow.navigateToExercise).
  • On Android, BackHandler listener 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-outline Ionicons icon.
  • Metrics row: horizontal row of white metricCard components (label + badge, borderRadius: 12).
  • Doctor notes section with chatbubble-ellipses-outline icon, separated by a thin divider.
  • Supports compact mode with optional showInfoIcon and onPress props 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): Returns true when any exercise block contains 2+ exercises.
  • buildSteps(exercises, mode): Returns an ordered array of WorkoutStep objects.

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

  • useWorkoutFlow tracks a currentStepIndex into the steps array (replacing the old exercise-index approach).
  • currentExercise and currentExerciseIndex are 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 the FEEDBACK mode.
  • Exercise list navigation is disabled in circuit mode (order is locked by the sequencing algorithm).
  • The resume envelope stores the step index plus a currentExerciseInWorkoutId checksum and version: 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 saved progressState for 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 timeWorkoutStart server-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) through workoutProgressService — 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.ts applies a 15s request timeout and treats timeouts as retryable network errors; empty success bodies parse as null instead 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].tsx
  • Okta-Mobile/app/(patient)/(workout)/_layout.tsx
  • Okta-Mobile/hooks/useWorkoutData.ts
  • Okta-Mobile/hooks/useWorkoutFlow.ts
  • Okta-Mobile/lib/workoutSequence.ts
  • Okta-Mobile/hooks/useVideoUploadProgress.ts
  • Okta-Mobile/services/videoUploadService.ts
  • Okta-Mobile/services/workoutProgressService.ts
  • Okta-Mobile/components/workout/styles.ts
  • Okta-Mobile/components/workout/InitialWorkoutSetupScreen.tsx
  • Okta-Mobile/components/workout/WorkoutSetupScreen.tsx
  • Okta-Mobile/components/workout/ExerciseActiveScreen.tsx
  • Okta-Mobile/components/workout/QuickRatingSheet.tsx
  • Okta-Mobile/components/workout/VideoExerciseExecutionScreen.tsx
  • Okta-Mobile/components/workout/PatientFeedbackModal.tsx
  • Okta-Mobile/components/workout/WorkoutCompletionScreen.tsx
  • Okta-Mobile/components/workout/WorkoutProgressBar.tsx
  • Okta-Mobile/components/workout/ExerciseInfoSheet.tsx
  • Okta-Mobile/components/workout/DoctorPrescriptionDisplay.tsx
  • Okta-Mobile/components/workout/WorkoutVideoPlayer.tsx
  • Okta-Mobile/components/workout/ExercisesListModal.tsx
  • Okta-Mobile/components/workout/ExerciseDetailSheet.tsx
  • Okta-Mobile/components/ui/PrimaryButton.tsx
  • Okta-Mobile/components/ui/PillRatingRow.tsx
  • Okta-Mobile/app/(patient)/(workout)/quick-finish/[id].tsx
  • Okta-Mobile/types/workout.ts