Free Trial & Paid Conversion (AI Trainer)
Turn non-subscribed users into paying customers by letting them experience the product before they ever see your paywall. One launch flag (isSubscribed: false) runs the entire free tier for you:
1. Full onboarding. The trainer interviews the user; every answer streams to your app as events.
2. A real camera fitness assessment. Up to two guided 30-second tests with live rep counting, form scoring, and a fitness level verdict. The user has now used the product, not read about it.
3. A personalized plan preview. The exact structure of their first workout, one tap away, next to a Create my workout button and a locked chat input cycling through prompts a subscriber could ask.
4. The conversion moment. Either tap fires one event, open_subscription_flow; you overlay your paywall. On purchase, the parked workout generates immediately.
A user who has invested five minutes of answers, performed a tracked assessment, and can see their own plan one tap away converts far better than one hitting a paywall cold.
Requirements: the standard AI Trainer Chat integration. For the full flow on native, use Swift KinesteXAIKit ≥ 1.1.4 (adds the workoutAction binding for posting the purchase result) and Kotlin KinesteX-SDK-Kotlin ≥ 2.0.6 (sendAction). Platforms without runtime messaging can remount with isSubscribed: true instead (see Unlock after purchase).
The funnel at a glance
Onboarding → camera assessment → readiness check-in → plan preview with locked chat (all free) → open_subscription_flow → your paywall on top → you post subscription_result → on "purchased" the parked workout generates and the chat unlocks; on "dismissed" the preview stays and the user can try again.
Onboarding interview
FREEGoals, injuries, schedule, equipment. Every answer streams to your app.
trainer_profile_onboardingper step, to your appCamera fitness assessment
FREEReal motion tracking: reps counted, form scored, fitness level measured.
trainer_assessment_completedto your appReadiness check-in
FREESoreness and energy today, so the plan matches the moment.
Plan preview + locked chat
FREEThe first workout's structure, one tap away, next to Create my workout and the locked input cycling through subscriber prompts.
The conversion moment
CONVERSIONEither tap fires one event. Overlay your paywall on top of the trainer.
open_subscription_flowsource: "generate_workout"open_subscription_flowsource: "chat_input"subscription_result: "purchased"The parked workout generates immediately. No relaunch, no second tap.
subscription_result: "dismissed"The preview stays on screen. The user can tap Create my workout again later.
Workout generates, chat unlocks
SUBSCRIBERThe user trains on the plan they already saw, and the full chat opens up.
subscription_result back after your paywall closes.What free users get (and what stays gated)
You control exactly one switch: isSubscribed. Everything else is automatic. A missing flag or true means fully subscribed; only an explicit false enables the free tier.
| Experience | Free user (isSubscribed: false) | Subscriber |
| Onboarding interview (goals, injuries, schedule) | ✅ Full | ✅ Full |
| Camera fitness assessment with motion tracking | ✅ Full, including retests | ✅ Full |
| Assessment results, fitness level, form score | ✅ Full | ✅ Full |
| Readiness check-in | ✅ Full | ✅ Full |
| Personalized workout structure preview | ✅ Shown with Create my workout | Skipped (generates right away) |
| Chat input | 🔒 Locked, shows rotating example prompts, tap = conversion event | ✅ Unlocked |
| Workout generation and training | 🔒 Gated behind open_subscription_flow | ✅ Full |
Two details worth knowing:
- The assessment adapts to injuries on its own. Knee problems swap in a chair squat; pressing-chain injuries skip the push-up test; if no test is safe, the step is withheld and the user self-reports. You never manage which exercises run.
- The locked input is a feature, not a wall. It cycles through real prompts a subscriber could ask, so the free user sees the breadth of the chat before you ask them to pay for it.
The journey, screen by screen
What the free user actually sees, in order:
1. Assessment personalization. The trainer adapts the camera test to the user's profile, visualizing their own answers while it prepares.
2. The tracked assessment. Camera tests end on a statistics screen: workout score, accuracy score, and a per-exercise breakdown with reps and calories.
3. Result confirmation in chat. The result becomes an editable card (reps, form percentage, computed level) with Retest one tap away.
4. The preview and the paywall trigger. The first workout's phase-by-phase structure, then the Create my workout button and the locked chat input. Both routes lead to your subscription flow.
Launch the trainer for free users
Mount the trainer exactly as in the standard integration, passing your live entitlement state as isSubscribed. Pass it on every mount so a purchase made elsewhere (web checkout, promo code, family plan) is reflected on the next launch.
Optional but recommended: brand the trainer as your own coach with aiTrainerName and aiTrainerColor (see Branding & appearance), so the free preview sells your product, not a third-party SDK.
1// Binding used later to post the purchase result back into the trainer.
2@State private var trainerAction: [String: Any]? = nil
3
4kit.createTrainerChatView(
5 user: nil, // or your UserDetails
6 style: IStyle(style: "dark"),
7 isLoading: $isLoading,
8 customParams: [
9 "isSubscribed": SubscriptionManager.shared.isActive, // false = free tier
10 // Optional branding, so the preview sells YOUR coach:
11 "aiTrainerName": "ARIA+",
12 "aiTrainerColor": "#7C4DFF",
13 ],
14 workoutAction: $trainerAction, // KinesteXAIKit >= 1.1.4
15 onMessageReceived: handleMessage
16)The conversion moments and the event you receive
A free user hits the gate from two places, and the source field tells you which:
1{ "type": "open_subscription_flow", "source": "generate_workout", "date": "01 09 2026 14:52:10" }
2{ "type": "open_subscription_flow", "source": "chat_input", "date": "01 09 2026 14:52:10" }"generate_workout": the user tapped Create my workout (or asked a returning-user flow to rebuild their plan). Their readiness answers are parked so the workout can generate the instant they purchase."chat_input": the user tapped the locked chat input. The pending generation is parked the same way.
Both values arrive through the same handler and lead to the same paywall. source is also a strong analytics signal: chat_input conversions were sold by the chat itself.
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 let source = data["source"] as? String // "generate_workout" | "chat_input"
9 analytics.track("paywall_shown", ["source": source ?? "unknown"])
10 presentSubscriptionFlow()
11 }
12 default:
13 break
14 }
15}Unlock after purchase
When your subscription flow closes, tell the trainer what happened by posting exactly one subscription_result, from every exit path (purchase success, close button, swipe-down, back gesture, purchase failure):
"purchased": the user is unlocked for the session, the parked workout generates immediately, and the chat input opens up. No relaunch, no second tap."dismissed": the pending request is dropped. The preview stays on screen and the user can tap Create my workout again later.
No runtime channel on your platform? Two alternatives:
- React Native / Flutter / React TS: remount the trainer with
isSubscribed: trueafter a purchase. The confirmed profile is already persisted; the user lands back in the chat and taps Create my workout once. - Standalone / link integrations with no host app: pass
subscriptionReturnUrlat launch. The trainer redirects the browser there instead of posting the event, and your page relaunches KinesteX with the sameuserIdandisSubscribed: true.
To enforce the status server-side so clients cannot bypass the gate, register the subscription with the Trainer API; the backend then becomes the source of truth and overrides the launch flag.
1func presentSubscriptionFlow() {
2 let paywall = PaywallViewController()
3 paywall.onPurchaseCompleted = { [weak self] in
4 self?.trainerAction = ["subscription_result": "purchased"] // parked workout generates
5 paywall.dismiss(animated: true)
6 }
7 paywall.onDismissed = { [weak self] in
8 self?.trainerAction = ["subscription_result": "dismissed"] // trainer resumes, preview stays
9 }
10 present(paywall, animated: true)
11}What you learn about users who don't convert
The free tier is also a data funnel. Even a user who dismisses the paywall has handed you a complete fitness profile and a measured assessment, all delivered as events while they onboarded:
trainer_profile_onboardingfires at every applied wizard step with that stage's answer (goals, injuries, schedule, equipment), using the same field names as the profile object. Mirror them into your CRM as they arrive.trainer_profile_updatedfires once the questionnaire is confirmed, with the full profile.trainer_assessment_completeddelivers the measured result:
1{
2 "type": "trainer_assessment_completed",
3 "date": "01 09 2026 14:52:10",
4 "results": {
5 "squats": { "reps": 21, "level": "intermediate" },
6 "pushups": { "reps": null, "level": null }
7 },
8 "fitnessLevel": "intermediate"
9}A slot the injury-adapted plan didn't measure keeps its object shape with null fields, so results.squats.reps never throws. Full payloads: Assessment & profile sync.
Launch checklist
isSubscribedpassed on every mount from your live entitlement state (missing ortruemeans subscribed; only an explicitfalseenables the free tier)open_subscription_flowhandled for bothsourcevalues (generate_workoutandchat_input) and routed to your paywall, overlaid on top of the trainer view- Every exit path of your paywall posts
subscription_result="purchased"or"dismissed"(Swift/Kotlin/HTML), or remounts withisSubscribed: true(React Native / Flutter / React TS) - Verified end to end: free onboarding → camera assessment runs and reports results → preview + locked input shown → tap fires the event → sandbox purchase. On Swift / Kotlin / HTML,
"purchased"makes the parked workout generate without a relaunch; on React Native / Flutter / React TS, the remount comes up unlocked and one tap of Create my workout generates - Verified the dismissal path: closed paywall +
"dismissed"(or a remount still onisSubscribed: false) → trainer resumes with the preview still on screen - Onboarding and assessment events (
trainer_profile_onboarding,trainer_profile_updated,trainer_assessment_completed) forwarded to your backend for analytics and win-back campaigns - Purchases made outside the app (web checkout, promo codes) reflected via
isSubscribed: trueon the next mount, or enforced server-side with managed subscriptions
For the mechanics of the gate itself (edge cases, duplicate messages, app killed mid-purchase), see the Subscription gating guide.