Use Case: Onboarding to Premium
The most common integration pattern for subscription apps. This guide walks through the full flow: app launch, onboarding with data capture, user identification, paywall presentation, purchase handling, and premium unlock.
What You’ll Build
App launch → Initialize SDK → Present onboarding → Capture quiz answers →
Identify user with traits → Present paywall → Handle purchase → Unlock premium
Step 1: Initialize the SDK
Configure AppDNA as early as possible in your app’s lifecycle.
iOS
Android
Flutter
React Native
import AppDNASDK
// AppDelegate.swift
func application(_ app: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppDNA.configure(
apiKey: "adn_live_xxx",
environment: .production,
options: AppDNAOptions(billingProvider: .storeKit2)
)
return true
}
import ai.appdna.sdk.AppDNA
import ai.appdna.sdk.AppDNAOptions
import ai.appdna.sdk.BillingProvider
// Application.onCreate()
AppDNA.configure(
context = this,
apiKey = "adn_live_xxx",
environment = Environment.PRODUCTION,
options = AppDNAOptions(billingProvider = BillingProvider.PLAY_BILLING)
)
import 'package:appdna_sdk/appdna_sdk.dart';
await AppDNA.configure(
"adn_live_xxx",
environment: AppDNAEnvironment.production,
options: AppDNAOptions(billingProvider: BillingProvider.storeKit2),
);
import { AppDNA } from "@appdna/react-native-sdk";
await AppDNA.configure("adn_live_xxx", "production", {
billingProvider: "auto",
});
Step 2: Present Onboarding
The onboarding flow is designed in the Console — the SDK renders it natively. Present it on first launch:
iOS
Android
Flutter
React Native
let presented = AppDNA.presentOnboarding(
flowId: nil, // Uses the active flow from Console
from: viewController,
delegate: self
)
if !presented {
// No active flow or config not loaded -- skip to home
navigateToHome()
}
val presented = AppDNA.presentOnboarding(
flowId = null,
activity = this,
delegate = this
)
if (!presented) {
navigateToHome()
}
final presented = await AppDNA.onboarding.present(
flowId: null,
);
if (!presented) {
navigateToHome();
}
const presented = await AppDNA.onboarding.present(null);
if (!presented) {
navigateToHome();
}
Passing null for flowId presents whatever flow is currently active in the Console. This lets you swap onboarding flows remotely without an app update.
Step 3: Capture Quiz Answers via Callbacks
The onboarding delegate gives you every answer the user provides. Use these for personalization and segmentation.
iOS
Android
Flutter
React Native
class AppCoordinator: AppDNAOnboardingDelegate {
func onOnboardingStepChanged(flowId: String, stepId: String, stepIndex: Int, totalSteps: Int) {
// Update progress UI if you have one
}
func onOnboardingCompleted(flowId: String, responses: [String: Any]) {
// responses contains all quiz answers:
// ["fitness_goal": "lose_weight", "experience_level": "beginner", "age_group": "25-34"]
handleOnboardingComplete(responses: responses)
}
func onOnboardingDismissed(flowId: String, atStep: Int) {
// User skipped onboarding -- still proceed
navigateToPaywall()
}
}
override fun onOnboardingCompleted(flowId: String, responses: Map<String, Any>) {
// responses contains all quiz answers
handleOnboardingComplete(responses)
}
override fun onOnboardingDismissed(flowId: String, atStep: Int) {
navigateToPaywall()
}
@override
void onOnboardingCompleted(String flowId, Map<String, dynamic> responses) {
handleOnboardingComplete(responses);
}
@override
void onOnboardingDismissed(String flowId, int atStep) {
navigateToPaywall();
}
const unsubComplete = AppDNA.onboarding.onCompleted(
(flowId: string, responses: Record<string, any>) => {
handleOnboardingComplete(responses);
}
);
const unsubDismissed = AppDNA.onboarding.onDismissed(
(flowId: string, atStep: number) => {
navigateToPaywall();
}
);
Conditional Branching
Onboarding flows support conditional branching — the next step changes based on the user’s answer. This is configured entirely in the Console:
Step 1: "What's your fitness goal?"
→ "Lose weight" → Step 2a: Weight loss program details
→ "Build muscle" → Step 2b: Muscle building program details
→ "Stay active" → Step 2c: General wellness details
To configure branching:
- Open your flow in the Console (Onboarding > Flows)
- On a question step, click Add branching rule
- Map each answer option to a target step
The SDK handles the routing automatically — no code needed.
Step 4: Identify the User with Traits
After onboarding (or after login), link the user’s identity and pass the quiz answers as traits. These traits power experiment targeting, push segmentation, and analytics.
iOS
Android
Flutter
React Native
func handleOnboardingComplete(responses: [String: Any]) {
// Pass onboarding answers as user traits
AppDNA.identify(
userId: authManager.currentUserId,
traits: responses
)
navigateToPaywall()
}
fun handleOnboardingComplete(responses: Map<String, Any>) {
AppDNA.identify(
userId = authManager.currentUserId,
traits = responses
)
navigateToPaywall()
}
void handleOnboardingComplete(Map<String, dynamic> responses) {
AppDNA.identify(authManager.currentUserId, traits: responses);
navigateToPaywall();
}
function handleOnboardingComplete(responses: Record<string, any>) {
AppDNA.identify(authManager.currentUserId, responses);
navigateToPaywall();
}
Handling Login
Depending on your app’s flow, user login can happen at different points:
Login during onboarding — if your onboarding includes a signup/login step, call identify() in the step callback:
iOS
Android
Flutter
React Native
func onOnboardingStepChanged(flowId: String, stepId: String, stepIndex: Int, totalSteps: Int) {
if stepId == "login_step" {
// User completed login/signup step
// Your auth flow runs here, then:
AppDNA.identify(userId: authManager.currentUserId, traits: ["signup_method": "email"])
}
}
override fun onOnboardingStepChanged(flowId: String, stepId: String, stepIndex: Int, totalSteps: Int) {
if (stepId == "login_step") {
AppDNA.identify(userId = authManager.currentUserId, traits = mapOf("signup_method" to "email"))
}
}
@override
void onOnboardingStepChanged(String flowId, String stepId, int stepIndex, int totalSteps) {
if (stepId == "login_step") {
AppDNA.identify(authManager.currentUserId, traits: {"signup_method": "email"});
}
}
AppDNA.onboarding.onStepChanged((flowId, stepId, stepIndex, totalSteps) => {
if (stepId === "login_step") {
AppDNA.identify(authManager.currentUserId, { signup_method: "email" });
}
});
Login after onboarding — call identify() in your normal auth callback:
iOS
Android
Flutter
React Native
// After OAuth or email/password login completes
func authDidComplete(user: User) {
AppDNA.identify(
userId: user.id,
traits: [
"provider": user.authProvider, // "google", "apple", "email"
"name": user.displayName
]
)
}
fun onAuthComplete(user: User) {
AppDNA.identify(
userId = user.id,
traits = mapOf("provider" to user.authProvider, "name" to user.displayName)
)
}
void onAuthComplete(User user) {
AppDNA.identify(user.id, traits: {
"provider": user.authProvider,
"name": user.displayName,
});
}
function onAuthComplete(user: User) {
AppDNA.identify(user.id, {
provider: user.authProvider,
name: user.displayName,
});
}
Before identify() is called, the SDK tracks events against an anonymous ID. Once you call identify(), all past anonymous events are automatically linked to the user.
Step 5: Present the Paywall
Show the paywall immediately after onboarding. The paywall is designed in the Console — the SDK renders it with built-in purchase handling.
iOS
Android
Flutter
React Native
func navigateToPaywall() {
AppDNA.presentPaywall(
id: "post_onboarding",
from: viewController,
context: PaywallContext(placement: "onboarding_complete"),
delegate: self
)
}
fun navigateToPaywall() {
AppDNA.presentPaywall(
id = "post_onboarding",
activity = this,
context = PaywallContext(placement = "onboarding_complete"),
delegate = this
)
}
void navigateToPaywall() {
AppDNA.paywall.present(
"post_onboarding",
context: PaywallContext(placement: "onboarding_complete"),
);
}
function navigateToPaywall() {
AppDNA.paywall.present("post_onboarding", {
placement: "onboarding_complete",
});
}
Step 6: Handle Purchase Callbacks
React to purchase results via the paywall delegate:
iOS
Android
Flutter
React Native
// Purchase succeeded -- unlock premium
func onPaywallPurchaseCompleted(paywallId: String, productId: String, transaction: TransactionInfo) {
unlockPremium()
navigateToHome()
}
// Purchase failed -- paywall stays visible for retry
func onPaywallPurchaseFailed(paywallId: String, error: Error) {
// Error is shown in the paywall UI automatically
}
// User dismissed without purchasing -- freemium path
func onPaywallDismissed(paywallId: String) {
navigateToHome()
}
override fun onPaywallPurchaseCompleted(paywallId: String, productId: String, transaction: TransactionInfo) {
unlockPremium()
navigateToHome()
}
override fun onPaywallPurchaseFailed(paywallId: String, error: Exception) {
// Error shown in paywall UI
}
override fun onPaywallDismissed(paywallId: String) {
navigateToHome()
}
@override
void onPaywallPurchaseCompleted(String paywallId, String productId, TransactionInfo transaction) {
unlockPremium();
navigateToHome();
}
@override
void onPaywallDismissed(String paywallId) {
navigateToHome();
}
AppDNA.paywall.onPurchaseCompleted((paywallId, productId, transaction) => {
unlockPremium();
navigateToHome();
});
AppDNA.paywall.onDismissed((paywallId) => {
navigateToHome();
});
Full Wired Example
Putting it all together in a single coordinator class:
iOS
Android
Flutter
React Native
import AppDNASDK
class AppCoordinator: AppDNAOnboardingDelegate, AppDNAPaywallDelegate {
private let viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
// MARK: - Entry Point
func start() {
let presented = AppDNA.presentOnboarding(
flowId: nil,
from: viewController,
delegate: self
)
if !presented { navigateToPaywall() }
}
// MARK: - Onboarding
func onOnboardingStarted(flowId: String) { }
func onOnboardingStepChanged(flowId: String, stepId: String, stepIndex: Int, totalSteps: Int) { }
func onOnboardingCompleted(flowId: String, responses: [String: Any]) {
// Pass quiz answers as traits for segmentation
if let userId = AuthManager.shared.currentUserId {
AppDNA.identify(userId: userId, traits: responses)
}
navigateToPaywall()
}
func onOnboardingDismissed(flowId: String, atStep: Int) {
navigateToPaywall()
}
// MARK: - Paywall
private func navigateToPaywall() {
AppDNA.presentPaywall(
id: "post_onboarding",
from: viewController,
context: PaywallContext(placement: "onboarding_complete"),
delegate: self
)
}
func onPaywallPresented(paywallId: String) { }
func onPaywallAction(paywallId: String, action: PaywallAction) { }
func onPaywallPurchaseStarted(paywallId: String, productId: String) { }
func onPaywallPurchaseCompleted(paywallId: String, productId: String, transaction: TransactionInfo) {
PremiumManager.shared.unlock()
navigateToHome()
}
func onPaywallPurchaseFailed(paywallId: String, error: Error) { }
func onPaywallDismissed(paywallId: String) {
navigateToHome() // Freemium path
}
private func navigateToHome() {
// Navigate to main app screen
}
}
import ai.appdna.sdk.*
class AppCoordinator(
private val activity: Activity
) : AppDNAOnboardingDelegate, AppDNAPaywallDelegate {
fun start() {
val presented = AppDNA.presentOnboarding(
flowId = null, activity = activity, delegate = this
)
if (!presented) navigateToPaywall()
}
// Onboarding
override fun onOnboardingStarted(flowId: String) { }
override fun onOnboardingStepChanged(flowId: String, stepId: String, stepIndex: Int, totalSteps: Int) { }
override fun onOnboardingCompleted(flowId: String, responses: Map<String, Any>) {
AuthManager.currentUserId?.let { userId ->
AppDNA.identify(userId = userId, traits = responses)
}
navigateToPaywall()
}
override fun onOnboardingDismissed(flowId: String, atStep: Int) {
navigateToPaywall()
}
// Paywall
private fun navigateToPaywall() {
AppDNA.presentPaywall(
id = "post_onboarding", activity = activity,
context = PaywallContext(placement = "onboarding_complete"),
delegate = this
)
}
override fun onPaywallPresented(paywallId: String) { }
override fun onPaywallAction(paywallId: String, action: PaywallAction) { }
override fun onPaywallPurchaseStarted(paywallId: String, productId: String) { }
override fun onPaywallPurchaseCompleted(paywallId: String, productId: String, transaction: TransactionInfo) {
PremiumManager.unlock()
navigateToHome()
}
override fun onPaywallPurchaseFailed(paywallId: String, error: Exception) { }
override fun onPaywallDismissed(paywallId: String) {
navigateToHome()
}
private fun navigateToHome() { }
}
import 'package:appdna_sdk/appdna_sdk.dart';
class AppCoordinator extends AppDNAOnboardingDelegate with AppDNAPaywallDelegate {
void start() async {
final presented = await AppDNA.onboarding.present(flowId: null);
if (!presented) navigateToPaywall();
}
// Onboarding
@override
void onOnboardingCompleted(String flowId, Map<String, dynamic> responses) {
final userId = AuthManager.instance.currentUserId;
if (userId != null) {
AppDNA.identify(userId, traits: responses);
}
navigateToPaywall();
}
@override
void onOnboardingDismissed(String flowId, int atStep) {
navigateToPaywall();
}
// Paywall
void navigateToPaywall() {
AppDNA.paywall.present(
"post_onboarding",
context: PaywallContext(placement: "onboarding_complete"),
);
}
@override
void onPaywallPurchaseCompleted(String paywallId, String productId, TransactionInfo transaction) {
PremiumManager.instance.unlock();
navigateToHome();
}
@override
void onPaywallDismissed(String paywallId) {
navigateToHome();
}
void navigateToHome() { }
}
import { AppDNA } from "@appdna/react-native-sdk";
function useAppCoordinator() {
useEffect(() => {
const unsubOnboarding = AppDNA.onboarding.onCompleted(
(flowId, responses) => {
const userId = authManager.currentUserId;
if (userId) AppDNA.identify(userId, responses);
navigateToPaywall();
}
);
const unsubDismissed = AppDNA.onboarding.onDismissed(() => {
navigateToPaywall();
});
const unsubPurchase = AppDNA.paywall.onPurchaseCompleted(
(paywallId, productId, transaction) => {
unlockPremium();
navigateToHome();
}
);
const unsubPaywallDismiss = AppDNA.paywall.onDismissed(() => {
navigateToHome();
});
return () => {
unsubOnboarding();
unsubDismissed();
unsubPurchase();
unsubPaywallDismiss();
};
}, []);
async function start() {
const presented = await AppDNA.onboarding.present(null);
if (!presented) navigateToPaywall();
}
function navigateToPaywall() {
AppDNA.paywall.present("post_onboarding", {
placement: "onboarding_complete",
});
}
return { start };
}
What Happens Behind the Scenes
You don’t need to manage any of this — the SDK handles it automatically:
- Onboarding config is fetched from the Console and cached locally (works offline)
- Every step event is tracked automatically (
onboarding_flow_started, step_viewed, step_completed, etc.)
- User traits sync to AppDNA for segmentation and experiment targeting
- Paywall config and product prices are resolved from cache
- Purchases are verified server-side automatically
- All events are batched and sent reliably (persisted to disk, retried on failure)
See Auto-Tracked Events for the full list of events tracked during this flow.