Skip to main content

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.
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
}

Step 2: Present Onboarding

The onboarding flow is designed in the Console — the SDK renders it natively. Present it on first launch:
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()
}
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.
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()
    }
}

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:
  1. Open your flow in the Console (Onboarding > Flows)
  2. On a question step, click Add branching rule
  3. 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.
func handleOnboardingComplete(responses: [String: Any]) {
    // Pass onboarding answers as user traits
    AppDNA.identify(
        userId: authManager.currentUserId,
        traits: 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:
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"])
    }
}
Login after onboarding — call identify() in your normal auth callback:
// 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
        ]
    )
}
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.
func navigateToPaywall() {
    AppDNA.presentPaywall(
        id: "post_onboarding",
        from: viewController,
        context: PaywallContext(placement: "onboarding_complete"),
        delegate: self
    )
}

Step 6: Handle Purchase Callbacks

React to purchase results via the paywall delegate:
// 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()
}

Full Wired Example

Putting it all together in a single coordinator class:
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
    }
}

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.