# Custom Workout

Create and execute personalized workout sequences with custom exercises, repetitions, durations, and rest periods. Define your own workout flow with full control over exercise order and timing.

**How it works:**
1. Initialize the SDK with custom workout integration option
2. Pass your custom workout exercises array
3. Wait for `all_resources_loaded` message
4. Send `workout_activity_action: start` to begin

**WorkoutSequenceExercise Parameters:**
- `exerciseId` - Exercise ID from KinesteX API or admin panel
- `reps` - Number of repetitions
- `duration` - Duration in seconds (null = unlimited time for reps)
- `includeRestPeriod` - Include rest period before exercise
- `restDuration` - Rest duration in seconds

**Tip:** To create sets, duplicate the same exercise in the array multiple times.

**Custom Workout Setup**

_Swift (iOS)_
```swift
@State var workoutAction: [String: Any]? = nil

let exercises = [
    WorkoutSequenceExercise(
        exerciseId: "jz73VFlUyZ9nyd64OjRb",
        reps: 15,
        duration: nil,
        includeRestPeriod: true,
        restDuration: 20
    ),
    WorkoutSequenceExercise(
        exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
        reps: 10,
        duration: 30,
        includeRestPeriod: true,
        restDuration: 15
    ),
    WorkoutSequenceExercise(
        exerciseId: "gJGOiZhCvJrhEP7sTy78",
        reps: 20,
        duration: nil,
        includeRestPeriod: false,
        restDuration: 0
    )
]

kinestex.createCustomWorkoutView(
    exercises: exercises,
    user: userDetails,
    style: nil,
    isLoading: $isLoading,
    workoutAction: $workoutAction,
    onMessageReceived: { message in
        if case .custom_type(let value) = message,
           let type = value["type"] as? String,
           type == "all_resources_loaded" {
            // Start workout when resources are ready
            workoutAction = ["workout_activity_action": "start"]
        }
    }
)
```

_Kotlin (Android)_
```kotlin
// Define custom workout exercises
val customExercises = listOf(
    WorkoutSequenceExercise(
        exerciseId = "jz73VFlUyZ9nyd64OjRb",
        reps = 15,
        duration = null,
        includeRestPeriod = true,
        restDuration = 20
    ),
    WorkoutSequenceExercise(
        exerciseId = "ZVMeLsaXQ9Tzr5JYXg29",
        reps = 10,
        duration = 30,
        includeRestPeriod = true,
        restDuration = 15
    )
)

KinesteXSDK.createCustomWorkoutView(
    context = this,
    customWorkouts = customExercises,
    user = userDetails, // optional user details
    isLoading = viewModel.isLoading,
    customParams = mutableMapOf("style" to "dark"),
    onMessageReceived = { message ->
        if (message is WebViewMessage.AllResourcesLoaded) {
            // Start workout when ready
        }
    },
    permissionHandler = this
)
```

_React Native_
```jsx
// Step 1: Define custom workout exercises
const customWorkoutExercises: WorkoutSequenceExercise[] = [
  {
    exerciseId: "jz73VFlUyZ9nyd64OjRb", // exercise id from kinestex api
    reps: 15,                            // number of reps
    duration: null,                      // null = unlimited time for reps
    includeRestPeriod: true,             // include rest before exercise
    restDuration: 20,                    // rest duration in seconds
  },
  {
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  },
  // Duplicate exercise to create a set
  {
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  },
];

// Step 2: Configure postData with customWorkoutExercises
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
  style: {
    style: 'dark',
  },
};

// Step 3: Handle SDK messages
const handleMessage = (type: string, data: any) => {
  switch (type) {
    case 'all_resources_loaded':
      // SDK is ready - show the view and start workout
      setAllResourcesLoaded(true);
      kinestexSDKRef.current?.sendAction("workout_activity_action", "start");
      break;
    case 'workout_exit_request':
      setAllResourcesLoaded(false);
      setShowKinestex(false);
      break;
    case 'exit_kinestex':
      setAllResourcesLoaded(false);
      break;
  }
};

// Step 4: Render with conditional visibility
<View style={[
  styles.sdkContainer,
  showKinestex && !allResourcesLoaded && styles.hiddenSdkContainer,
]}>
  {showKinestex && (
    <KinestexSDK
      ref={kinestexSDKRef}
      data={postData}
      integrationOption={IntegrationOption.CUSTOM_WORKOUT}
      handleMessage={handleMessage}
    />
  )}
</View>
```

_Flutter_
```dart
// Define custom workout exercises
final customWorkoutExercises = [
  WorkoutSequenceExercise(
    exerciseId: "jz73VFlUyZ9nyd64OjRb",
    reps: 15,
    duration: null,
    includeRestPeriod: true,
    restDuration: 20,
  ),
  WorkoutSequenceExercise(
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  ),
];

KinesteXAIFramework.createCustomWorkoutView(
  isShowKinestex: showKinesteX,
  customWorkouts: customWorkoutExercises,
  customParams: {"style": "dark"},
  isLoading: ValueNotifier<bool>(false),
  onMessageReceived: (message) {
    // Custom events arrive as CustomType; the event name is in data['type']
    if (message is CustomType && message.data['type'] == 'all_resources_loaded') {
      // Start workout when ready
    }
  },
)
```

_HTML / JavaScript_
```html
// Define custom workout exercises
const customWorkoutExercises = [
  {
    exerciseId: "jz73VFlUyZ9nyd64OjRb",
    reps: 15,
    duration: null,
    includeRestPeriod: true,
    restDuration: 20,
  },
  {
    exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
    reps: 10,
    duration: 30,
    includeRestPeriod: true,
    restDuration: 15,
  },
];

const config = {
  ...postData,
  customWorkoutExercises: customWorkoutExercises,
};

const srcURL = "https://ai.kinestex.com/custom-workout";
webView.src = srcURL;
webView.onload = () => {
  sendMessage();
};

// Listen for ready signal
window.addEventListener("message", (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'all_resources_loaded') {
    // Start workout
    webView.contentWindow.postMessage(
      { action: "workout_activity_action", value: "start" },
      srcURL
    );
  }
});
```

_React (TypeScript)_
```tsx
import { useRef, useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

// Define custom workout exercises
interface WorkoutSequenceExercise {
  exerciseId: string;
  reps: number | null;
  duration: number | null;
  includeRestPeriod: boolean;
  restDuration: number;
}

const customWorkoutExercises: WorkoutSequenceExercise[] = [
  {
    exerciseId: "jz73VFlUyZ9nyd64OjRb",
    reps: 15,
    duration: null,
    includeRestPeriod: true,
    restDuration: 20,
  },
];

// postData structure with customWorkoutExercises
const postData: IPostData = {
  key: 'YOUR_API_KEY',
  userId: 'YOUR_USER_ID',
  company: 'YOUR_COMPANY_NAME',
  customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
  style: {
    style: 'dark',
  },
};

const ref = useRef<KinesteXSDKCamera>(null);

<KinesteXSDK
  ref={ref}
  data={postData}
  integrationOption={IntegrationOption.CUSTOM_WORKOUT}
  handleMessage={(type, data) => {
    if (type === 'all_resources_loaded') {
      ref.current?.sendAction("workout_activity_action", "start");
    }
  }}
/>
```

## Complete Example

Full implementation with state management, loading indicators, and proper message handling.

**Complete Implementation**

_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit

struct CustomWorkoutView: View {
    @State private var showKinesteX = false
    @State private var isLoading = false
    @State private var allResourcesLoaded = false
    @State var workoutAction: [String: Any]? = nil

    // Initialize KinesteXAIKit with your credentials
    let kinestex = KinesteXAIKit(
        apiKey: "YOUR_API_KEY",
        companyName: "YOUR_COMPANY_NAME",
        userId: "YOUR_USER_ID"
    )

    // Define custom workout exercises
    let exercises = [
        WorkoutSequenceExercise(
            exerciseId: "jz73VFlUyZ9nyd64OjRb",
            reps: 15,
            duration: nil,
            includeRestPeriod: true,
            restDuration: 20
        ),
        WorkoutSequenceExercise(
            exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
            reps: 10,
            duration: 30,
            includeRestPeriod: true,
            restDuration: 15
        ),
        WorkoutSequenceExercise(
            exerciseId: "gJGOiZhCvJrhEP7sTy78",
            reps: 20,
            duration: nil,
            includeRestPeriod: false,
            restDuration: 0
        )
    ]

    var body: some View {
        VStack {
            Text("Custom Workout")
                .font(.title)
                .padding()

            Spacer()

            Button(action: {
                showKinesteX.toggle()
            }) {
                Text("Start Custom Workout")
                    .font(.title3)
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.green.cornerRadius(10))
                    .padding(.horizontal)
            }

            Spacer()
        }
        .fullScreenCover(isPresented: $showKinesteX) {
            kinestex.createCustomWorkoutView(
                exercises: exercises,
                user: nil,
                style: nil,
                isLoading: $isLoading,
                workoutAction: $workoutAction,
                onMessageReceived: { message in
                    switch message {
                    case .custom_type(let value):
                        guard let type = value["type"] as? String else { return }
                        if type == "all_resources_loaded" {
                            allResourcesLoaded = true
                            // Start workout when resources are ready
                            workoutAction = ["workout_activity_action": "start"]
                        }
                    case .exit_kinestex(_):
                        showKinesteX = false
                        allResourcesLoaded = false
                    case .workout_overview(let data):
                        print("Workout overview: \(data)")
                    case .error_occurred(let data):
                        print("Error: \(data)")
                    default:
                        print("Message received: \(message)")
                    }
                }
            )
        }
    }
}

#Preview {
    CustomWorkoutView()
}
```

_React Native_
```jsx
import { StyleSheet, View, Button, Text } from "react-native";
import { useEffect, useRef, useState } from "react";
import { Camera } from "expo-camera";
import { SafeAreaView } from "react-native-safe-area-context";
import KinestexSDK from "kinestex-sdk-react-native";
import {
  KinesteXSDKCamera,
  WorkoutSequenceExercise,
  IPostData,
  IntegrationOption
} from "kinestex-sdk-react-native/src/types";

export default function CustomWorkoutScreen() {
  const kinestexSDKRef = useRef<KinesteXSDKCamera>(null);
  const [permission, setPermission] = useState(false);
  const [allResourcesLoaded, setAllResourcesLoaded] = useState(false);
  const [showKinestex, setShowKinestex] = useState(true);

  // Define workout sequence of exercises
  const customWorkoutExercises: WorkoutSequenceExercise[] = [
    {
      exerciseId: "jz73VFlUyZ9nyd64OjRb",
      reps: 15,
      duration: null, // unlimited time to complete reps
      includeRestPeriod: true,
      restDuration: 20,
    },
    {
      exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
      reps: 10,
      duration: 30,
      includeRestPeriod: true,
      restDuration: 15,
    },
    // Duplicate to create a set
    {
      exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
      reps: 10,
      duration: 30,
      includeRestPeriod: true,
      restDuration: 15,
    },
    {
      exerciseId: "gJGOiZhCvJrhEP7sTy78",
      reps: 20,
      duration: null,
      includeRestPeriod: false,
      restDuration: 0,
    },
  ];

  // Configuration data with customWorkoutExercises
  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    company: 'YOUR_COMPANY_NAME',
    userId: "user-123",
    customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
    style: {
      style: "dark",
    },
  };

  // Request camera permission
  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      if (status === "granted") {
        setPermission(true);
      }
    })();
  }, []);

  // Handle messages from SDK
  const handleMessage = (type: string, data: { [key: string]: any }) => {
    switch (type) {
      case "exit_kinestex":
        console.log("User wishes to exit");
        setAllResourcesLoaded(false);
        break;
      case "workout_exit_request":
        console.log("Workout exit request:", data);
        setAllResourcesLoaded(false);
        setShowKinestex(false);
        break;
      case "all_resources_loaded":
        console.log("All resources loaded");
        setAllResourcesLoaded(true);
        // Start the workout
        kinestexSDKRef.current?.sendAction("workout_activity_action", "start");
        break;
      case "workout_overview":
        console.log("Workout overview:", data);
        break;
      case "error_occurred":
        console.log("Error:", data);
        break;
      default:
        console.log("Message:", type, data);
        break;
    }
  };

  if (!permission) {
    return (
      <View style={styles.launchContainer}>
        <Text style={styles.statusText}>Camera permission required</Text>
      </View>
    );
  }

  return (
    <SafeAreaView style={styles.safeArea}>
      <Text style={styles.statusText}>
        {!showKinestex
          ? "KinesteX is not activated"
          : allResourcesLoaded
          ? "All resources loaded"
          : "KinesteX is loading in background"}
      </Text>
      <View
        style={[
          styles.sdkContainer,
          showKinestex && !allResourcesLoaded && styles.hiddenSdkContainer,
        ]}
      >
        {showKinestex ? (
          <KinestexSDK
            ref={kinestexSDKRef}
            data={postData}
            integrationOption={IntegrationOption.CUSTOM_WORKOUT}
            handleMessage={handleMessage}
          />
        ) : (
          <View style={styles.launchContainer}>
            <Button
              title="Show Kinestex Again"
              onPress={() => setShowKinestex(true)}
            />
          </View>
        )}
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: "black",
  },
  statusText: {
    padding: 8,
    textAlign: "center",
    color: "white",
  },
  launchContainer: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "black",
  },
  sdkContainer: {
    flex: 1,
  },
  hiddenSdkContainer: {
    width: 0,
    height: 0,
    overflow: "hidden",
  },
});
```

_React (TypeScript)_
```tsx
import React, { useRef, useState } from 'react';
import {
  IntegrationOption,
  KinesteXSDK,
  type IPostData,
  type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';

// Define workout sequence exercise type
interface WorkoutSequenceExercise {
  exerciseId: string;
  reps: number | null;
  duration: number | null;
  includeRestPeriod: boolean;
  restDuration: number;
}

const CustomWorkoutScreen: React.FC = () => {
  const ref = useRef<KinesteXSDKCamera>(null);
  const [allResourcesLoaded, setAllResourcesLoaded] = useState(false);
  const [showKinestex, setShowKinestex] = useState(true);

  const customWorkoutExercises: WorkoutSequenceExercise[] = [
    {
      exerciseId: "jz73VFlUyZ9nyd64OjRb",
      reps: 15,
      duration: null,
      includeRestPeriod: true,
      restDuration: 20,
    },
    {
      exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
      reps: 10,
      duration: 30,
      includeRestPeriod: true,
      restDuration: 15,
    },
  ];

  // Include customWorkoutExercises in postData
  const postData: IPostData = {
    key: 'YOUR_API_KEY',
    company: 'YOUR_COMPANY_NAME',
    userId: "user-123",
    customWorkoutExercises: customWorkoutExercises,
    style: { style: "dark" },
  };

  const handleMessage = (type: string, data: Record<string, unknown>) => {
    switch (type) {
      case "all_resources_loaded":
        setAllResourcesLoaded(true);
        ref.current?.sendAction("workout_activity_action", "start");
        break;
      case "workout_exit_request":
      case "exit_kinestex":
        setAllResourcesLoaded(false);
        setShowKinestex(false);
        break;
    }
  };

  return (
    <div style={{ height: '100vh', width: '100vw', backgroundColor: 'black' }}>
      <div style={{
        display: showKinestex && !allResourcesLoaded ? 'none' : 'block',
        height: '100%',
        width: '100%',
      }}>
        {showKinestex ? (
          <KinesteXSDK
            ref={ref}
            data={postData}
            integrationOption={IntegrationOption.CUSTOM_WORKOUT}
            handleMessage={handleMessage}
          />
        ) : (
          <button onClick={() => setShowKinestex(true)}>
            Show Kinestex Again
          </button>
        )}
      </div>
    </div>
  );
};

export default CustomWorkoutScreen;
```

_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await KinesteXAIFramework.initialize(
    apiKey: "your_api_key",
    companyName: "your_company_name",
    userId: "your_user_id",
  );
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void dispose() {
    disposeKinesteXAIFramework();
    super.dispose();
  }

  Future<void> disposeKinesteXAIFramework() async {
    await KinesteXAIFramework.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KinesteX Custom Workout',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // The KinesteX view is ALWAYS mounted. We only toggle visibility.
  final ValueNotifier<bool> showKinesteX = ValueNotifier<bool>(false);

  // Signals when SDK reports all resources loaded.
  final ValueNotifier<bool> allResourcesLoaded = ValueNotifier<bool>(false);

  // Optional: SDK loading notifier.
  final ValueNotifier<bool> sdkLoading = ValueNotifier<bool>(false);

  bool permissionGranted = false;

  late final List<WorkoutSequenceExercise> customWorkoutExercises;

  @override
  void initState() {
    super.initState();
    _checkCameraPermission();

    customWorkoutExercises = const [
      WorkoutSequenceExercise(
        exerciseId: "jz73VFlUyZ9nyd64OjRb",
        reps: 15,
        duration: null,
        includeRestPeriod: true,
        restDuration: 20,
      ),
      WorkoutSequenceExercise(
        exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
        reps: 10,
        duration: 30,
        includeRestPeriod: true,
        restDuration: 15,
      ),
      // Duplicate to create a set
      WorkoutSequenceExercise(
        exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
        reps: 10,
        duration: 30,
        includeRestPeriod: true,
        restDuration: 15,
      ),
      WorkoutSequenceExercise(
        exerciseId: "gJGOiZhCvJrhEP7sTy78",
        reps: 20,
        duration: null,
        includeRestPeriod: false,
        restDuration: 0,
      ),
    ];
  }

  void _checkCameraPermission() async {
    if (await Permission.camera.request() != PermissionStatus.granted) {
      setState(() => permissionGranted = false);
      _showCameraAccessDeniedAlert();
    } else {
      setState(() => permissionGranted = true);
    }
  }

  void _showCameraAccessDeniedAlert() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: const Text("Camera Permission Denied"),
            content: const Text(
              "Camera access is required for this app to function properly.",
            ),
            actions: <Widget>[
              TextButton(
                child: const Text("OK"),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ],
          );
        },
      );
    });
  }

  void handleWebViewMessage(WebViewMessage message) {
    if (message is ExitKinestex) {
      setState(() {
        allResourcesLoaded.value = false;
        showKinesteX.value = false;
      });
      return;
    }

    try {
      final data = message.data;
      final type = data['type'];

      switch (type) {
        case 'all_resources_loaded':
          allResourcesLoaded.value = true;
          showKinesteX.value = true;
          KinesteXAIFramework.sendAction(
            "workout_activity_action",
            "start",
          );
          break;

        case 'workout_exit_request':
          allResourcesLoaded.value = false;
          showKinesteX.value = false;
          break;

        case 'error_occurred':
          final errorMsg = data['message']?.toString() ?? 'Unknown error';
          print('Error from KinesteX SDK: $errorMsg');
          break;

        default:
          break;
      }
    } catch (_) {
      allResourcesLoaded.value = false;
      showKinesteX.value = false;
    }
  }

  Widget createCustomWorkoutView() {
    return Center(
      child: KinesteXAIFramework.createCustomWorkoutView(
        customWorkouts: customWorkoutExercises,
        isShowKinestex: showKinesteX,
        isLoading: sdkLoading,
        onMessageReceived: handleWebViewMessage,
      ),
    );
  }

  Widget buildCornerIndicator() {
    return SafeArea(
      child: Padding(
        padding: const EdgeInsets.only(top: 8, right: 8),
        child: ValueListenableBuilder<bool>(
          valueListenable: allResourcesLoaded,
          builder: (context, loaded, _) {
            return ValueListenableBuilder<bool>(
              valueListenable: showKinesteX,
              builder: (context, visible, __) {
                Color bg = Colors.black.withOpacity(0.75);
                IconData icon = Icons.hourglass_bottom;
                String label = 'KinesteX loading...';

                if (!permissionGranted) {
                  bg = Colors.red.withOpacity(0.85);
                  icon = Icons.videocam_off;
                  label = 'Camera permission required';
                } else if (!loaded) {
                  bg = Colors.orange.withOpacity(0.85);
                  icon = Icons.downloading;
                  label = 'Loading in background';
                } else if (loaded && visible) {
                  bg = Colors.green.withOpacity(0.85);
                  icon = Icons.check_circle;
                  label = 'All resources loaded';
                } else if (loaded && !visible) {
                  bg = Colors.grey.withOpacity(0.85);
                  icon = Icons.visibility_off;
                  label = 'KinesteX hidden';
                }

                return Container(
                  constraints: const BoxConstraints(maxWidth: 260),
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 10,
                  ),
                  decoration: BoxDecoration(
                    color: bg,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Icon(icon, color: Colors.white, size: 18),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          label,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 13,
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                      ),
                      if (!permissionGranted)
                        TextButton(
                          onPressed: _checkCameraPermission,
                          child: const Text(
                            'Grant',
                            style: TextStyle(color: Colors.white),
                          ),
                        )
                      else if (loaded && !visible)
                        TextButton(
                          onPressed: () {
                            showKinesteX.value = true;
                            KinesteXAIFramework.sendAction(
                              "workout_activity_action",
                              "start",
                            );
                          },
                          child: const Text(
                            'Show',
                            style: TextStyle(color: Colors.white),
                          ),
                        )
                      else if (!loaded)
                        const SizedBox(
                          height: 16,
                          width: 16,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.white,
                          ),
                        ),
                    ],
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          ValueListenableBuilder<bool>(
            valueListenable: showKinesteX,
            builder: (context, isVisible, _) {
              return IgnorePointer(
                ignoring: !isVisible,
                child: AnimatedOpacity(
                  duration: const Duration(milliseconds: 250),
                  opacity: isVisible ? 1.0 : 0.0,
                  child: createCustomWorkoutView(),
                ),
              );
            },
          ),
          Positioned(top: 0, right: 0, child: buildCornerIndicator()),
        ],
      ),
    );
  }
}
```

_HTML / JavaScript_
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>KinesteX: Custom Workout</title>
    <style>
      body { margin: 0; font-family: Arial, sans-serif; }
      #statusText { padding: 8px; text-align: center; background: #333; color: white; }
      #launchButton { display: block; margin: 20px auto; padding: 15px 30px; font-size: 16px; cursor: pointer; }
    </style>
  </head>
  <body>
    <div id="statusText">Click button to start custom workout</div>
    <button id="launchButton">Start Custom Workout</button>
    <div id="webViewContainer" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none; background: black;">
      <iframe id="webView" frameborder="0" allow="camera; autoplay; accelerometer; gyroscope; magnetometer" sandbox="allow-same-origin allow-scripts" style="width: 100%; height: 100%"></iframe>
    </div>

    <script>
      // Define custom workout exercises
      const customWorkoutExercises = [
        {
          exerciseId: "jz73VFlUyZ9nyd64OjRb",
          reps: 15,
          duration: null, // unlimited time to complete reps
          includeRestPeriod: true,
          restDuration: 20,
        },
        {
          exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
          reps: 10,
          duration: 30,
          includeRestPeriod: true,
          restDuration: 15,
        },
        {
          exerciseId: "gJGOiZhCvJrhEP7sTy78",
          reps: 20,
          duration: null,
          includeRestPeriod: false,
          restDuration: 0,
        },
      ];

      // Configuration data
      const postData = {
        userId: "YOUR_USER_ID",
        company: "YOUR_COMPANY",
        key: "YOUR_API_KEY",
        style: "dark",
        customWorkoutExercises: customWorkoutExercises,
      };

      // IMPORTANT: This is the URL for the Custom Workout integration
      const srcURL = "https://ai.kinestex.com/custom-workout";
      let allResourcesLoaded = false;

      // Get references to iframe and container at the top
      const webView = document.getElementById("webView");
      const webViewContainer = document.getElementById("webViewContainer");

      // Send configuration data to the iframe via postMessage
      // Includes retry logic for Apple Safari compatibility
      function sendMessage() {
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(postData, srcURL);
        } else {
          setTimeout(() => {
            try {
              webView.contentWindow.postMessage(postData, srcURL);
            } catch {
              webView.contentWindow.postMessage(postData, srcURL);
            }
          }, 100);
        }
      }

      // Send action to the SDK (e.g., start workout)
      const sendAction = (action, value) => {
        const messagePayload = { [action]: String(value) };
        if (webView.contentWindow) {
          webView.contentWindow.postMessage(messagePayload, srcURL);
        }
      };

      document.getElementById("launchButton").addEventListener("click", function () {
        webViewContainer.style.display = "block";
        document.getElementById("statusText").textContent = "Loading custom workout...";
        webView.src = srcURL;
        webView.onload = sendMessage;
      });

      window.addEventListener("message", function (event) {
        if (event.origin !== "https://ai.kinestex.com") return;

        let message;
        try {
          message = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
        } catch {
          return;
        }

        const statusText = document.getElementById("statusText");

        switch (message.type) {
          case "kinestex_launched":
            console.log("KinesteX launched");
            break;
          case "kinestex_loaded":
            console.log("KinesteX loaded, sending post data");
            sendMessage();
            break;
          case "all_resources_loaded":
            allResourcesLoaded = true;
            statusText.textContent = "All resources loaded - starting workout";
            // Start the workout after resources are loaded
            sendAction("workout_activity_action", "start");
            break;
          case "workout_overview":
            console.log("Workout overview:", message);
            break;
          case "workout_exit_request":
          case "exit_kinestex":
            allResourcesLoaded = false;
            webViewContainer.style.display = "none";
            statusText.textContent = "Workout ended. Click button to restart.";
            console.log("User exited the custom workout");
            break;
          case "error_occurred":
            console.error("Error:", message);
            statusText.textContent = "Error occurred: " + (message.message || "Unknown error");
            break;
          default:
            console.log("Message received:", message.type, message);
            break;
        }
      });
    </script>
  </body>
</html>
```

---
Source: https://www.kinestex.com/docs/integration/custom-workout · Index: https://www.kinestex.com/llms.txt
