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. With a single launch flag (isSubscribed: false), the AI Trainer runs a complete free tier for you:
1. Full onboarding. The trainer interviews the user: goals, body parts, injuries, schedule, equipment. Every answer streams to your app as events, so you learn who this user is even if they never pay.
2. A real camera fitness assessment. Up to two guided exercises (a 30-second squat test and a 30-second push-up test) with live motion tracking: rep counting, form scoring, and a fitness level verdict. This is the "wow" moment. The user has now used the product, not read about it.
3. A personalized plan preview. The trainer shows the exact structure of the first workout it would build (phases, minutes, "every exercise set to your level") next to a Create my workout button, and a locked chat input that cycles through prompts a subscriber could ask ("Adjust the workout structure", "Analyze my last workout", "Make it easier on my knees").
4. The conversion moment. Tapping Create my workout or the locked input fires one event, open_subscription_flow. You overlay your paywall. On purchase, the parked workout generates immediately: no relaunch, no second tap.
Free users who have invested five minutes of answers, performed a tracked assessment, and seen their own plan one tap away convert at a far higher rate than users hitting a paywall cold. And because KinesteX never shows a paywall and never processes payments, your app owns the entire purchase flow and 100% of the pricing.
The funnel at a glance:
1Onboarding wizard (free) → trainer_profile_onboarding per step
2 │
3 ▼
4Camera fitness assessment (free) → trainer_assessment_completed
5 │ real motion tracking: reps, form score, fitness level
6 ▼
7Readiness check-in (free)
8 │
9 ▼
10Workout structure preview + locked chat ← the free tier's edge
11 │ user taps "Create my workout" or the locked input
12 ▼
13open_subscription_flow → your paywall, overlaid on top
14 │ you post { "subscription_result": "purchased" }
15 ▼
16Parked workout generates immediately, chat unlocksRequirements: 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).
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. A user who reports knee problems gets a chair squat variant; pressing-chain injuries skip the push-up test. You never have to manage which exercises run. If no test is safe to run, the assessment step is withheld and the user self-reports instead.
- 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 what the chat does before you ask them to pay for it.
The journey, screen by screen
What the free user actually sees, in order:
1. Assessment personalization. After onboarding, the trainer adapts the camera test to the user's profile. The interlude visualizes their own answers (duration, frequency, goal, equipment, focus areas) orbiting while it prepares.
2. The tracked assessment. The user performs the tests in front of the camera and lands on the statistics screen: workout score, accuracy score, activity level, and a per-exercise breakdown with reps counted and calories. This is real KinesteX motion tracking, free.
3. Result confirmation in chat. Back in the chat, the result becomes an editable card: reps, form percentage, and the computed level (Beginner / Intermediate / Advanced), with Retest one tap away.
4. The preview and the paywall trigger. The trainer lays out the first workout's structure: phases with per-phase minutes, built from the user's goal and equipment, with the note "Every exercise set to your level." Below it: 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 on the preview (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, so a purchase from here also produces the workout immediately.
Treat the event as an instruction, not a status: present your subscription flow now, overlaid on top of the trainer view. Do not dismiss the trainer; the parked generation is waiting underneath. The source value is also a strong analytics signal: users arriving via chat_input were sold by the chat itself, which tells you the locked-input preview is doing its job.
Both values arrive through the same handler you already have; route them to the same paywall.
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 rest of 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, the assessment stays reachable, and the user can tap Create my workout again later.
The message is idempotent: the first result wins and repeats are ignored, so a double-fired callback does no harm.
Platforms without runtime messaging (React Native, Flutter, React TS): remount the trainer with isSubscribed: true after a purchase. The confirmed onboarding profile is already persisted, so the user lands back in the chat and taps Create my workout once; only the parked one-tap resume is lost. Standalone / link integrations with no host app can pass subscriptionReturnUrl at launch instead: the trainer redirects the browser there rather than posting the event, and your page relaunches KinesteX with the same userId and isSubscribed: 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. See Assessment & profile sync for the full payloads.
That data powers your win-back campaigns: "You measured 21 squats last week. Your Beginner-to-Intermediate plan is one tap away." And because you pass the profile back on the next launch (pre-filling), a returning free user resumes exactly where they left off instead of starting over.
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.