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:
WORKOUT_SETUP -> SETUP -> EXERCISE_EXECUTION or VIDEO_EXERCISE_EXECUTION -> inline feedback -> SETUP -> ... -> COMPLETION
Main router file: Okta-Mobile/app/(patient)/(workout)/[id].tsx
Screen Orchestration ([id].tsx)¶
- Uses
useWorkoutData(id)to fetch workout and start time. - Uses
useWorkoutFlow(workout)for mode/state transitions. - Shows a loading spinner (
ActivityIndicator+ "Loading workout...") while data is fetching. - Keeps local UI state:
showSetCompletion(shows inline post-exercise feedback)showExercisesList(opens exercise list modal)- Renders:
InitialWorkoutSetupScreeninWORKOUT_SETUPWorkoutSetupScreeninSETUPWorkoutExecutionScreeninEXERCISE_EXECUTIONVideoExerciseExecutionScreeninVIDEO_EXERCISE_EXECUTIONPatientFeedbackModalinline whenshowSetCompletion === trueWorkoutCompletionScreeninCOMPLETIONExercisesListModalas inline overlay
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 closes workout (
router.replace("/(tabs)/dashboard")). - Right action (exercise list pill) is shown only in
SETUPmode. 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)¶
- Two
OptionCardcomponents with accent bars: - Start without recording (green accent,
videocam-off-outlineicon) - Enable camera (gray accent,
videocam-outlineicon) - Each card has a left accent bar, icon container, title + subtitle, and right chevron.
- Card press animation uses RN
Animated.springscale (0.97 on press, 1 on release). - Haptics on selection.
Permission behavior¶
Permission logic is centralized in useWorkoutFlow.enableCameraForWorkout():
- Requests both camera and microphone permissions.
- Enables recording mode only if both are granted.
- Moves to SETUP only when permissions are granted.
On load, useWorkoutFlow checks permission status with getCameraPermissionsAsync/getMicrophonePermissionsAsync (no permission prompt on mount).
Exercise Setup (WorkoutSetupScreen.tsx)¶
- Shows exercise title and a green sets/reps pill badge.
- Shows video player (with loading skeleton) if any of these exist:
cloudflareStreamIdkinescopeVideoIdvideoUrl(YouTube URL)- Layout order: title -> sets/reps pill -> video -> description -> doctor prescription.
- YouTube playback is container-aware and centered (16:9 frame fit inside container).
- Shows exercise description when present.
- Shows full doctor guideline card via
DoctorPrescriptionDisplaywhen prescription data exists. - Floating bottom action buttons with drop shadows and frosted glass borders:
- Optional red button: start with video recording (only when camera preference enabled and permissions granted)
- Green button: start exercise (or start without video)
- Haptics on both start buttons (
ImpactFeedbackStyle.Medium). - Content padding adjusts based on whether the video button is shown (140px vs 80px floating bar height).
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.
Execution Without Recording (WorkoutExecutionScreen.tsx)¶
- Full-screen exercise video or fallback info block.
- Timer is inline (
useState+setInterval), no separateWorkoutTimercomponent. - 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). - Floating bottom button with two styles:
- While recording: lavender/purple (
rgba(201,130,224,0.75)) "Finish Recording" - After recording: green (
rgba(16,185,129,0.75)) "Finish Exercise" - 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) to avoid Android WebView/Camera layering issues.
Implemented as 3 steps (integer-indexed: 0, 1, 2):
- Sets completed (step 0)
SetButtonsub-components: 56x56 green circles (#10B981when selected)- Reanimated scale pop animation on selection
- Auto-advance after selection (~400ms timeout, cleaned up on unmount)
-
Cancel action closes feedback
-
Pain + exertion (step 1)
PillRowSelectorwithPillButtonsub-components (11 circular pills, dynamically sized)- Selected state: full color; unselected: 19% opacity tint
- Reanimated scale bounce on selection (
withSequence(withSpring(1.25), withSpring(1))) -
AnimatedGreenButtonfor Continue: press-in scale to 0.96, spring back, haptic + bounce to 1.08 -
Notes (step 2)
- Multiline notes input
- Back + Complete bottom nav
Animation + interaction details:
- transitionDirection state (1 or -1) drives directional step animations.
- Step enter/exit uses Reanimated FadeInRight/FadeInLeft/FadeOutLeft/FadeOutRight (springified, damping: 18, stiffness: 220).
- StepProgressDots sub-component: completed dots pop (spring 1 -> 1.4 -> 1), current dot pulses opacity infinitely.
- Notes step uses Reanimated useAnimatedKeyboard() directly for keyboard-aware bottom offset (replaces KeyboardAwareScrollView/KeyboardToolbar). Bottom padding = bottomInset + Math.max(0, kbHeight - insets.bottom).
- No translated horizontal container is used (prevents Android hitbox mismatch issues).
Completion (WorkoutCompletionScreen.tsx)¶
- Root is a
ScrollViewwithpaddingBottom: Math.max(insets.bottom, 24). - Animated title scale-in (Reanimated
withSpring, scale 0.5 -> 1,damping: 12, stiffness: 200). - 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). - Done button disabled while uploads are active or while completion API is in flight.
- After pressing Done, button enters loading state (spinner + loading text).
isMountedRefguard prevents state updates after unmount.- 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 list is opened only in
SETUPmode. - Allows jumping to another exercise while in
SETUP(enforced inuseWorkoutFlow.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.
Data + API Behavior¶
useWorkoutData.ts¶
- Fetches workout by ID (
GET /workouts/:id). - If
timeWorkoutStartis empty, patches start time (PATCH /workouts/:id).
useWorkoutFlow.ts¶
- Manages index, completed sets, feedback, and mode transitions.
- Completion submission:
- Optional feedback patch:
PATCH /v2/exercises/workouts/:id/exercises/patient-feedback - Workout completion:
POST /workouts/:id/complete - Both completion requests validate
response.okand throw on failure.
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/hooks/useVideoUploadProgress.tsOkta-Mobile/services/videoUploadService.tsOkta-Mobile/components/workout/styles.tsOkta-Mobile/components/workout/InitialWorkoutSetupScreen.tsxOkta-Mobile/components/workout/WorkoutSetupScreen.tsxOkta-Mobile/components/workout/WorkoutExecutionScreen.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/types/workout.ts