> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appdna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Onboarding to Premium

> Wire up the core subscription app flow: onboarding, data capture, paywall, and premium unlock

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

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    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
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    import ai.appdna.sdk.AppDNA
    import ai.appdna.sdk.AppDNAOptions
    import ai.appdna.sdk.Environment

    // Application.onCreate()
    // Android always uses Google Play Billing — no billingProvider option needed.
    AppDNA.configure(
        context = this,
        apiKey = "adn_live_xxx",
        environment = Environment.PRODUCTION,
        options = AppDNAOptions()
    )
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    import 'package:appdna_sdk/appdna_sdk.dart';

    await AppDNA.configure(
      apiKey: "adn_live_xxx",
      env: AppDNAEnvironment.production,
      options: AppDNAOptions(billingProvider: AppDNABillingProvider.storeKit2),
    );
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    import { AppDNA } from "@appdna-ai/react-native-sdk";

    await AppDNA.configure("adn_live_xxx", "production", {
      billingProvider: "storeKit2",
    });
    ```
  </Tab>
</Tabs>

***

## Step 2: Present Onboarding

The onboarding flow is designed in the Console -- the SDK renders it natively. Present it on first launch:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    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()
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    val presented = AppDNA.presentOnboarding(
        activity = this,
        flowId = null,
        listener = this
    )

    if (!presented) {
        navigateToHome()
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    // Flutter's present takes a required flowId and returns void.
    await AppDNA.onboarding.present("welcome");
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    const presented = await AppDNA.onboarding.present("welcome");

    if (!presented) {
      navigateToHome();
    }
    ```
  </Tab>
</Tabs>

<Info>
  On iOS and Android, `flowId` is optional — pass `null` to present whatever flow is currently active in the Console, letting you swap onboarding flows remotely without an app update. On Flutter and React Native, `present` takes a specific flow ID.
</Info>

***

## Step 3: Capture Quiz Answers via Callbacks

The onboarding delegate gives you every answer the user provides. Use these for personalization and segmentation.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    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()
        }
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    override fun onOnboardingCompleted(flowId: String, responses: Map<String, Any>) {
        // responses contains all quiz answers
        handleOnboardingComplete(responses)
    }

    override fun onOnboardingDismissed(flowId: String, atStep: Int) {
        navigateToPaywall()
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    @override
    void onOnboardingCompleted(String flowId, Map<String, dynamic> responses) {
      handleOnboardingComplete(responses);
    }

    @override
    void onOnboardingDismissed(String flowId, int atStep) {
      navigateToPaywall();
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    // Register the onboarding delegate once, at app startup.
    AppDNA.onboarding.setDelegate({
      onOnboardingCompleted: (flowId, responses) => {
        handleOnboardingComplete(responses);
      },
      onOnboardingDismissed: (flowId, atStep) => {
        navigateToPaywall();
      },
      // ...implement the other AppDNAOnboardingDelegate methods
    });
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    func handleOnboardingComplete(responses: [String: Any]) {
        // Pass onboarding answers as user traits
        AppDNA.identify(
            userId: authManager.currentUserId,
            traits: responses
        )

        navigateToPaywall()
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    fun handleOnboardingComplete(responses: Map<String, Any>) {
        AppDNA.identify(
            userId = authManager.currentUserId,
            traits = responses
        )
        navigateToPaywall()
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    void handleOnboardingComplete(Map<String, dynamic> responses) {
      AppDNA.identify(authManager.currentUserId, traits: responses);
      navigateToPaywall();
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    function handleOnboardingComplete(responses: Record<string, any>) {
      AppDNA.identify(authManager.currentUserId, responses);
      navigateToPaywall();
    }
    ```
  </Tab>
</Tabs>

### 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:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    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"])
        }
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    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"))
        }
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    @override
    void onOnboardingStepChanged(String flowId, String stepId, int stepIndex, int totalSteps) {
      if (stepId == "login_step") {
        AppDNA.identify(authManager.currentUserId, traits: {"signup_method": "email"});
      }
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    AppDNA.onboarding.setDelegate({
      onOnboardingStepChanged: (flowId, stepId, stepIndex, totalSteps) => {
        if (stepId === "login_step") {
          AppDNA.identify(authManager.currentUserId, { signup_method: "email" });
        }
      },
      // ...implement the other AppDNAOnboardingDelegate methods
    });
    ```
  </Tab>
</Tabs>

**Login after onboarding** -- call `identify()` in your normal auth callback:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    // 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
            ]
        )
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    fun onAuthComplete(user: User) {
        AppDNA.identify(
            userId = user.id,
            traits = mapOf("provider" to user.authProvider, "name" to user.displayName)
        )
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    void onAuthComplete(User user) {
      AppDNA.identify(user.id, traits: {
        "provider": user.authProvider,
        "name": user.displayName,
      });
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    function onAuthComplete(user: User) {
      AppDNA.identify(user.id, {
        provider: user.authProvider,
        name: user.displayName,
      });
    }
    ```
  </Tab>
</Tabs>

<Note>
  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.
</Note>

***

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

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    func navigateToPaywall() {
        AppDNA.presentPaywall(
            id: "post_onboarding",
            from: viewController,
            context: PaywallContext(placement: "onboarding_complete"),
            delegate: self
        )
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    fun navigateToPaywall() {
        AppDNA.presentPaywall(
            activity = this,
            id = "post_onboarding",
            context = PaywallContext(placement = "onboarding_complete"),
            listener = this
        )
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    void navigateToPaywall() {
      AppDNA.paywall.present(
        "post_onboarding",
        context: PaywallContext(placement: "onboarding_complete"),
      );
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    function navigateToPaywall() {
      AppDNA.paywall.present("post_onboarding", {
        placement: "onboarding_complete",
      });
    }
    ```
  </Tab>
</Tabs>

***

## Step 6: Handle Purchase Callbacks

React to purchase results via the paywall delegate:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    // 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()
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    override fun onPaywallPurchaseCompleted(paywallId: String, productId: String, transaction: TransactionInfo) {
        unlockPremium()
        navigateToHome()
    }

    override fun onPaywallPurchaseFailed(paywallId: String, error: Throwable) {
        // Error shown in paywall UI
    }

    override fun onPaywallDismissed(paywallId: String) {
        navigateToHome()
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    @override
    void onPaywallPurchaseCompleted(String paywallId, String productId, Map<String, dynamic> transaction) {
      unlockPremium();
      navigateToHome();
    }

    @override
    void onPaywallDismissed(String paywallId) {
      navigateToHome();
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    // Register the paywall delegate once, at app startup.
    AppDNA.paywall.setDelegate({
      onPaywallPurchaseCompleted: (paywallId, productId, transaction) => {
        unlockPremium();
        navigateToHome();
      },
      onPaywallDismissed: (paywallId) => {
        navigateToHome();
      },
      // ...implement the other AppDNAPaywallDelegate methods
    });
    ```
  </Tab>
</Tabs>

***

## Full Wired Example

Putting it all together in a single coordinator class:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    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
        }
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    import ai.appdna.sdk.*

    class AppCoordinator(
        private val activity: Activity
    ) : AppDNAOnboardingDelegate, AppDNAPaywallDelegate {

        fun start() {
            val presented = AppDNA.presentOnboarding(
                activity = activity, flowId = null, listener = 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(
                activity = activity, id = "post_onboarding",
                context = PaywallContext(placement = "onboarding_complete"),
                listener = 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: Throwable) { }

        override fun onPaywallDismissed(paywallId: String) {
            navigateToHome()
        }

        private fun navigateToHome() { }
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    import 'package:appdna_sdk/appdna_sdk.dart';

    // Dart abstract classes can't be mixed in, so use one standalone handler
    // per delegate and register each with the matching module.
    class AppCoordinator {
      void start() async {
        AppDNA.onboarding.setDelegate(OnboardingHandler(this));
        AppDNA.paywall.setDelegate(PaywallHandler(this));

        // present() takes a required flowId and returns void.
        await AppDNA.onboarding.present("welcome");
      }

      void navigateToPaywall() {
        AppDNA.paywall.present(
          "post_onboarding",
          context: PaywallContext(placement: "onboarding_complete"),
        );
      }

      void navigateToHome() { }
    }

    class OnboardingHandler extends AppDNAOnboardingDelegate {
      OnboardingHandler(this.coordinator);
      final AppCoordinator coordinator;

      @override
      void onOnboardingCompleted(String flowId, Map<String, dynamic> responses) {
        final userId = AuthManager.instance.currentUserId;
        if (userId != null) {
          AppDNA.identify(userId, traits: responses);
        }
        coordinator.navigateToPaywall();
      }

      @override
      void onOnboardingDismissed(String flowId, int atStep) {
        coordinator.navigateToPaywall();
      }
    }

    class PaywallHandler extends AppDNAPaywallDelegate {
      PaywallHandler(this.coordinator);
      final AppCoordinator coordinator;

      @override
      void onPaywallPurchaseCompleted(String paywallId, String productId, Map<String, dynamic> transaction) {
        PremiumManager.instance.unlock();
        coordinator.navigateToHome();
      }

      @override
      void onPaywallDismissed(String paywallId) {
        coordinator.navigateToHome();
      }
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    import { AppDNA } from "@appdna-ai/react-native-sdk";

    function useAppCoordinator() {
      useEffect(() => {
        // Register delegates once. Registering again replaces the previous one;
        // pass null to clear.
        AppDNA.onboarding.setDelegate({
          onOnboardingCompleted: (flowId, responses) => {
            const userId = authManager.currentUserId;
            if (userId) AppDNA.identify(userId, responses);
            navigateToPaywall();
          },
          onOnboardingDismissed: () => {
            navigateToPaywall();
          },
          // ...implement the other AppDNAOnboardingDelegate methods
        });

        AppDNA.paywall.setDelegate({
          onPaywallPurchaseCompleted: (paywallId, productId, transaction) => {
            unlockPremium();
            navigateToHome();
          },
          onPaywallDismissed: () => {
            navigateToHome();
          },
          // ...implement the other AppDNAPaywallDelegate methods
        });

        return () => {
          AppDNA.onboarding.setDelegate(null);
          AppDNA.paywall.setDelegate(null);
        };
      }, []);

      async function start() {
        const presented = await AppDNA.onboarding.present("welcome");
        if (!presented) navigateToPaywall();
      }

      function navigateToPaywall() {
        AppDNA.paywall.present("post_onboarding", {
          placement: "onboarding_complete",
        });
      }

      return { start };
    }
    ```
  </Tab>
</Tabs>

***

## 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](/api-reference/auto-tracked-events) for the full list of events tracked during this flow.
