# Subscription Gating (AI Trainer)

Gate AI Trainer **workout generation** behind your app's subscription while everything else (onboarding, the free fitness assessment, Q&A) stays available. KinesteX never shows a paywall and never processes payments — **your app owns the subscription screen and the purchase flow**. The trainer only enforces the gate and tells you when the user hits it.

**The flow at a glance:**

1. Mount the trainer with `isSubscribed` in `customParams` (from your live entitlements state). Missing/`true` = subscribed; only an explicit `false` gates generation.
2. When a non-subscriber taps **Generate workout**, generation is parked and the SDK emits `open_subscription_flow`. Present your subscription flow **on top of** the trainer view — don't dismiss it; the pending generation is waiting underneath.
3. Every time your flow closes, post exactly **one** `subscription_result` back into the view — `"purchased"` (the parked workout generates immediately, no second tap) or `"dismissed"` (the request is dropped; the trainer resumes and the free assessment stays reachable). Send it from **every** exit path: purchase success, close button, swipe-down, back gesture, purchase failure.

**The event:**

```json
{ "type": "open_subscription_flow", "source": "generate_workout", "date": "06 07 2026 14:52:10" }
```

This event is an **instruction, not a status** — present your subscription flow now. It fires for new users (after onboarding) and returning users (after the readiness check-in) alike, and can also fire when the KinesteX backend rejects a generation mid-session (see backend-managed subscriptions below).

**Edge cases:**

| Scenario | What to do |
|----------|------------|
| App killed during the purchase flow | Pass the fresh `isSubscribed: true` in `customParams` at next mount. The pending generation is not persisted across launches — the user taps Generate again. |
| Purchase fails or is refunded mid-flow | Send `"dismissed"` — the user can retry from the same Generate button. |
| Duplicate messages | Idempotent — the first result wins; repeats are ignored. |
| Status unknown at launch | Omit `isSubscribed` (treated as subscribed). If your entitlements check later resolves to active, post `{ "subscription_result": "purchased" }` — a pending generation resumes, otherwise future generations are simply unlocked. |

**No host app listening?** Standalone/link integrations can pass `subscriptionReturnUrl` in the launch config instead — the trainer redirects the browser there instead of posting `open_subscription_flow`. Your subscription page then relaunches KinesteX with the same `userId` and `isSubscribed: true`. Accepted values: `https://` URLs or app deep links (e.g. `clientapp://subscribe`); script-executing schemes are rejected.

**Backend-managed subscriptions:** for server-side enforcement your backend can store the user's status with KinesteX and it becomes the source of truth, overriding the launch flag — see [Session Auth & Managed Subscriptions](/docs/trainer-api/trainer-api-subscriptions).

**Launch checklist:**
- `isSubscribed` passed in `customParams` on every mount, from your live entitlements state
- `open_subscription_flow` → your subscription screen, overlaid on top of the trainer view
- Every exit path of your flow sends `subscription_result` = `"purchased"` or `"dismissed"`
- Verified end-to-end: blocked generate → overlay → sandbox purchase + `"purchased"` → workout generates without relaunch; closed overlay + `"dismissed"` → trainer resumes

Minimum SDK versions: Swift `KinesteXAIKit` ≥ 1.1.4 (adds the `workoutAction` binding), Kotlin `KinesteX-SDK-Kotlin` ≥ 2.0.6.

**Mount the trainer with subscription status**

_Swift (iOS)_
```swift
// Set this binding to post real-time actions into the trainer.
@State private var trainerAction: [String: Any]? = nil

kit.createTrainerChatView(
    user: UserDetails(age: 32, height: 178, weight: 76, gender: .Male, lifestyle: .Active), // or nil
    style: IStyle(style: "dark"),
    isLoading: $isLoading,
    customParams: [
        "isSubscribed": SubscriptionManager.shared.isActive
    ],
    workoutAction: $trainerAction,     // KinesteXAIKit ≥ 1.1.4
    onMessageReceived: handleMessage
)
```

_Kotlin (Android)_
```kotlin
val webView = KinesteXSDK.createTrainerChatView(
    context = this,
    user = UserDetails(age = 32, height = 178, weight = 76, gender = Gender.MALE, lifestyle = Lifestyle.ACTIVE), // or null
    style = IStyle(style = "dark"),
    customParams = mapOf(
        "isSubscribed" to subscriptionManager.isActive
    ),
    isLoading = isLoading,
    onMessageReceived = ::handleWebViewMessage,
    permissionHandler = this
)
```

_HTML / JavaScript_
```html
// Include isSubscribed in the postData you send after kinestex_loaded.
const postData = {
  userId: "user-123",
  company: "YOUR_COMPANY",
  key: "YOUR_API_KEY",          // or session: "<session_id>" (recommended)
  integration: "AI_TRAINER_CHAT",
  isSubscribed: subscriptionIsActive,   // only an explicit false gates generation
};
iframe.contentWindow.postMessage(JSON.stringify(postData), "https://ai.kinestex.com");
```

**Handle the event and report the outcome**

The trainer waits under your overlay until a subscription_result arrives — send it from every exit path of your flow.

_Swift (iOS)_
```swift
private func handleMessage(_ message: KinestexMessage) {
    switch message {
    case .exit_kinestex:
        showKinesteX = false
    case .custom_type(let data):
        guard let type = data["type"] as? String else { return }
        if type == "open_subscription_flow" {
            presentSubscriptionFlow()
        }
    default:
        break
    }
}

func presentSubscriptionFlow() {
    let paywall = PaywallViewController()
    paywall.onPurchaseCompleted = { [weak self] in
        self?.trainerAction = ["subscription_result": "purchased"] // generation resumes
        paywall.dismiss(animated: true)
    }
    paywall.onDismissed = { [weak self] in
        self?.trainerAction = ["subscription_result": "dismissed"] // trainer resumes
    }
    present(paywall, animated: true)
}
```

_Kotlin (Android)_
```kotlin
private fun handleWebViewMessage(message: WebViewMessage) {
    when (message) {
        is WebViewMessage.ExitKinestex -> closeTrainer()
        is WebViewMessage.CustomType -> when (message.data["type"] as? String) {
            "open_subscription_flow" -> presentSubscriptionFlow()
            else -> Unit
        }
        else -> Unit
    }
}

private fun presentSubscriptionFlow() {
    PaywallSheet(
        onPurchaseCompleted = {
            KinesteXWebViewController.getInstance()
                .sendAction("subscription_result", "purchased") // generation resumes
        },
        onDismissed = {
            KinesteXWebViewController.getInstance()
                .sendAction("subscription_result", "dismissed") // trainer resumes
        }
    ).show(supportFragmentManager, "paywall")
}
```

_HTML / JavaScript_
```html
window.addEventListener("message", (event) => {
  if (event.origin !== "https://ai.kinestex.com") return;
  const message = JSON.parse(event.data);
  if (message.type === "open_subscription_flow") {
    openPaywall({
      onPurchased: () =>
        iframe.contentWindow.postMessage(
          JSON.stringify({ subscription_result: "purchased" }),
          "https://ai.kinestex.com"
        ),
      onDismissed: () =>
        iframe.contentWindow.postMessage(
          JSON.stringify({ subscription_result: "dismissed" }),
          "https://ai.kinestex.com"
        ),
    });
  }
});
```

---
Source: https://www.kinestex.com/docs/guides/guide-subscription-gating · Index: https://www.kinestex.com/llms.txt
