KinesteX

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
1{ "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:


ScenarioWhat to do
App killed during the purchase flowPass 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-flowSend "dismissed" — the user can retry from the same Generate button.
Duplicate messagesIdempotent — the first result wins; repeats are ignored.
Status unknown at launchOmit 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.


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 example:
1// Set this binding to post real-time actions into the trainer.
2@State private var trainerAction: [String: Any]? = nil
3
4kit.createTrainerChatView(
5    user: UserDetails(age: 32, height: 178, weight: 76, gender: .Male, lifestyle: .Active), // or nil
6    style: IStyle(style: "dark"),
7    isLoading: $isLoading,
8    customParams: [
9        "isSubscribed": SubscriptionManager.shared.isActive
10    ],
11    workoutAction: $trainerAction,     // KinesteXAIKit ≥ 1.1.4
12    onMessageReceived: handleMessage
13)
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 example:
1private func handleMessage(_ message: KinestexMessage) {
2    switch message {
3    case .exit_kinestex:
4        showKinesteX = false
5    case .custom_type(let data):
6        guard let type = data["type"] as? String else { return }
7        if type == "open_subscription_flow" {
8            presentSubscriptionFlow()
9        }
10    default:
11        break
12    }
13}
14
15func presentSubscriptionFlow() {
16    let paywall = PaywallViewController()
17    paywall.onPurchaseCompleted = { [weak self] in
18        self?.trainerAction = ["subscription_result": "purchased"] // generation resumes
19        paywall.dismiss(animated: true)
20    }
21    paywall.onDismissed = { [weak self] in
22        self?.trainerAction = ["subscription_result": "dismissed"] // trainer resumes
23    }
24    present(paywall, animated: true)
25}