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:

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:
  • InitialWorkoutSetupScreen in WORKOUT_SETUP
  • WorkoutSetupScreen in SETUP
  • WorkoutExecutionScreen in EXERCISE_EXECUTION
  • VideoExerciseExecutionScreen in VIDEO_EXERCISE_EXECUTION
  • PatientFeedbackModal inline when showSetCompletion === true
  • WorkoutCompletionScreen in COMPLETION
  • ExercisesListModal as inline overlay

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 closes workout (router.replace("/(tabs)/dashboard")).
  • Right action (exercise list pill) is shown only in SETUP mode.
  • 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)

  • Two OptionCard components with accent bars:
  • Start without recording (green accent, videocam-off-outline icon)
  • Enable camera (gray accent, videocam-outline icon)
  • Each card has a left accent bar, icon container, title + subtitle, and right chevron.
  • Card press animation uses RN Animated.spring scale (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:
  • cloudflareStreamId
  • kinescopeVideoId
  • videoUrl (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 DoctorPrescriptionDisplay when 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 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.

Execution Without Recording (WorkoutExecutionScreen.tsx)

  • Full-screen exercise video or fallback info block.
  • Timer is inline (useState + setInterval), no separate WorkoutTimer component.
  • 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).
  • 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.
  • ExerciseInfoSheet rendered 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):

  1. Sets completed (step 0)
  2. SetButton sub-components: 56x56 green circles (#10B981 when selected)
  3. Reanimated scale pop animation on selection
  4. Auto-advance after selection (~400ms timeout, cleaned up on unmount)
  5. Cancel action closes feedback

  6. Pain + exertion (step 1)

  7. PillRowSelector with PillButton sub-components (11 circular pills, dynamically sized)
  8. Selected state: full color; unselected: 19% opacity tint
  9. Reanimated scale bounce on selection (withSequence(withSpring(1.25), withSpring(1)))
  10. AnimatedGreenButton for Continue: press-in scale to 0.96, spring back, haptic + bounce to 1.08

  11. Notes (step 2)

  12. Multiline notes input
  13. 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 ScrollView with paddingBottom: 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 (nestedScrollEnabled inside 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).
  • isMountedRef guard 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 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 list is opened only in SETUP mode.
  • Allows jumping to another exercise while in SETUP (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.

Data + API Behavior

useWorkoutData.ts

  • Fetches workout by ID (GET /workouts/:id).
  • If timeWorkoutStart is 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.ok and 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].tsx
  • Okta-Mobile/app/(patient)/(workout)/_layout.tsx
  • Okta-Mobile/hooks/useWorkoutData.ts
  • Okta-Mobile/hooks/useWorkoutFlow.ts
  • Okta-Mobile/hooks/useVideoUploadProgress.ts
  • Okta-Mobile/services/videoUploadService.ts
  • Okta-Mobile/components/workout/styles.ts
  • Okta-Mobile/components/workout/InitialWorkoutSetupScreen.tsx
  • Okta-Mobile/components/workout/WorkoutSetupScreen.tsx
  • Okta-Mobile/components/workout/WorkoutExecutionScreen.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/types/workout.ts