Supported on: iOS SDK
1.0.70+ · Android SDK 1.0.42+ · React Native SDK 1.0.7+ (New Architecture only)Present an Onboarding Flow
Present a specific onboarding flow by ID. The method returns aPromise<boolean> that resolves true once the native flow surface is shown, or false when nothing could present it. It never rejects:
false as a no-op and a debug-level log entry is emitted.
The React Native API takes the flow ID as a positional argument. To present the active flow configured in remote config from the AppDNA Console, the canonical pattern is to pick an explicit flow ID at the call site — the Console controls which flow is active by toggling published flows.
Module Access
Access the onboarding module directly to use additional methods likesetDelegate:
Module Methods
The static
AppDNA.presentOnboarding(flowId) is a convenience that calls AppDNA.onboarding.present(flowId).
present() takes a flow ID and nothing else. To personalize a flow, set user traits
(AppDNA.identify(userId, traits)) or session data (AppDNA.session.set(...)) before presenting —
the flow’s audience rules and templated copy read from those. Experiment variants are assigned by the
AppDNA backend; there is no client-side override.Paywalls inside onboarding flows
Steps withoutcome: present_paywall open a paywall and route the user based on the result (purchase / fail / dismiss → next step). The host’s AppDNAPaywallDelegate (registered via AppDNA.paywall.setDelegate(...)) fires for these embedded paywalls identically to standalone presentations — purchase, restore, action, and post-purchase callbacks all reach the host. See the React Native paywall docs for the full delegate surface.
AppDNAOnboardingDelegate
Implement theAppDNAOnboardingDelegate interface to receive onboarding flow lifecycle events. The delegate fires for every step transition and at flow completion / dismissal. For server-driven async hooks that can block or redirect step advancement, see Async Step Hooks below.
Set the Delegate
Example Implementation
Step Types
Onboarding flows support the following step types, configured in the AppDNA Console:Step types and their content are defined entirely in the Console. The SDK renders them natively — you do not need to build UI for individual step types from React Native.
Selection Modes
Question steps support two selection modes:Content Blocks
Each onboarding step is composed of content blocks that control the visual layout. Blocks are configured in the Console and rendered natively on each platform. The block string the Console emits is the same string the native SDK switches on. The following block types are available:Layout primitives
Interactive blocks
Form input blocks
Form inputs are individual blocks (not a singleform block). Each input type emits its current value through the step’s inputValues map and is included in the step responses on completion.
Blocks are configured entirely in the Console. The SDK renders them automatically — no TypeScript code is needed unless you want to register a host-provided custom view (see Custom View Registration below).
Advanced blocks
Available on iOS SDK
1.0.67+ / Android SDK 1.0.39+.
These wrap the native renderers — see the iOS and Android onboarding docs for each block’s
field_config fields and for the onElementInteraction hook that powers the interactive ones.
Button Actions
button and social_login blocks emit a structured payload when tapped. The action value on the button (configured in the Console) determines what shape the payload takes and lets you route to the right backend call (sign in, register, request OTP, etc.).
If the user has not completed required form fields on the step, the native SDK shows an inline validation toast and does not emit the action — same gate as the standard next button.
Flow control
Authentication entry
When the user taps a button with one of these actions, the SDK calls youronBeforeStepAdvance handler with the payload below. Show a “Signing in…” spinner by returning { type: 'block', message: '...' } while async auth runs, then return { type: 'proceed' } / { type: 'proceedWithData', data: {...} } when the backend responds.
Account lifecycle
request_otp channel resolution
request_otp and verify_otp include a channel field so your handler knows which delivery method to call back on. The SDK resolves it in this order:
- Explicit — set the button’s
action_valueto"sms","email","whatsapp", or"voice"in the Console (case-insensitive). - Auto-detect — if the step has exactly one phone-typed input (
input_phone) →channel: "sms"; exactly one email-typed input (input_email) →channel: "email". - Omitted — if the step has both an email AND a phone input (or neither), the
channelkey is absent from the payload. Check withstepData?.channel as string | undefinedand fail explicitly ifnull/undefinedrather than guessing. Setaction_valueon the button to disambiguate.
recipient so you don’t have to look it up from the step responses.
Handling auth actions
Existing
next, skip, link, permission, and social_login button actions are unchanged. Host apps that already implement AppDNAOnboardingDelegate keep working without changes — the default onBeforeStepAdvance returns { type: 'proceed' }, so flows that don’t switch on stepData?.action advance normally. Host apps that have NO delegate set will see the no-delegate warning above when an auth action fires; for those flows, either implement the delegate or keep using next as the button action.Custom Views
Acustom_view block renders a view you supply. On React Native it is registered from your
native code — Swift or Kotlin — not from JavaScript:
custom_view block.
Block Styling
Every content block supports ablock_style design token that controls appearance properties such as padding, margin, background color, corner radius, border, shadow, and opacity. Block styles are configured in the Console and applied automatically by the native renderers.
Visibility Conditions
Blocks can be shown or hidden based on user responses, bindings, or device attributes. Visibility conditions are configured per-block in the Console using rules likeanswer_equals, binding_not_empty, platform_is, and locale_matches. The native SDK evaluates conditions client-side before rendering each block.
Entrance Animations
Each block supports an entrance animation that plays when the block first appears. Animations are configured per-block in the Console. Supported animation types includefade, slide_up, slide_down, slide_left, slide_right, scale, flip, and bounce. You can configure duration, delay, and easing curve.
Form Steps
Theform step type provides native input fields for collecting structured user data. Each form can contain multiple fields with validation, conditional visibility, and custom configuration. Fields are rendered using native iOS controls (UIKit / SwiftUI) and native Android controls (Jetpack Compose).
Supported Input Types
Field Validation
Form fields support built-in and custom validation:- Required fields — marked in the Console, the SDK prevents advancing until filled.
- Regex patterns — custom validation (e.g.,
^[A-Z]{2}\d{4}$for a code format). - Min/max values — for
number,slider, andstepperfields. - Max length — for
textandtextareafields.
Conditional Fields
Fields can depend on other fields usingdepends_on rules. For example, a “Company name” field can appear only when the user selects “Employed” in a previous field. Supported operators: equals, not_equals, contains, not_empty, empty, gt, lt, is_set.
Form Responses
Form field values are included in theresponses map passed to onOnboardingCompleted, keyed by step ID. Each step’s value is a map of field ID to field value.
Location Fields
Thelocation field type provides an autocomplete search input that returns structured location data including city, state, country, coordinates, and timezone.
When the user starts typing (e.g., “New York”), the native SDK debounces the input (300ms), calls the AppDNA geocoding proxy, and displays a dropdown of suggestions. The user selects a result and the SDK stores the complete structured data.
Location Data Structure
Each location selection contains:Accessing Location Data
Location data is returned as a nested object in theresponses payload to onOnboardingCompleted:
Template Engine
Location data is accessible in dynamic content templates rendered by the native SDK:Configuration in Console
In the onboarding flow editor, add alocation field to a form step and configure:
Location autocomplete uses a server-side proxy — no third-party SDK is added to your app binary. The geocoding provider (Mapbox by default) can be configured in Settings > Geocoding.
Interactive Chat Steps
Theinteractive_chat step type renders a conversational UI that forwards each user message to your webhook and renders the reply. This is how you integrate your own LLM, agent, or rule-based backend into an onboarding flow.
How it works
- User types a message in the chat step.
- The native SDK
POSTs the conversation payload to thewebhook_urlconfigured in the Console (with any custom headers you set). - Your server responds with JSON in the schema below.
- The native SDK renders the AI reply, quick-reply buttons, media, etc.
Request payload (SDK → your webhook)
Bearer YOUR_TOKEN (the SDK does not add the Bearer prefix automatically).
The context object is opaque session state you control — whatever you returned in data on a previous turn gets echoed back here on every subsequent call. This is how you integrate threaded AI backends (OpenAI Assistants, hosted LLMs with threadId, etc.) without replaying the full history on every request. context is absent on turn 1 (your server hasn’t written anything yet) and accumulates across turns; later values overwrite earlier ones per-key (last-write-wins). See Threaded backends below for the full pattern.
Response schema (your webhook → SDK)
messages[].content is required — every other field is optional. The SDK reads messages[] to render chat bubbles, so if you omit it or return a different shape, the reply won’t render.
Full response schema
Unknown fields in your response are ignored, so you can also include your own server-side state at the top level (it just won’t round-trip — use
data for anything you want back).
Minimal working example
Node.js (Express):Threaded backends: round-tripping session state
If your AI service uses a thread/session handle (OpenAI Assistants API, a hosted LLM with conversation memory, or any third-party AI proxy that mints a session id), do not replay the full conversation history on every turn — latency and cost grow linearly with conversation length. Instead, return your session handle indata once; the SDK accumulates it and echoes it back in context on every subsequent turn.
The wire contract in plain English:
- You write to
contextby returning fields underdatain any response. - You read from
contextoffreq.body.contexton every request after the first. - Keys are merged across turns; later values overwrite earlier ones per-key.
- The SDK never inspects or mutates
context— it’s opaque to your server. You choose the keys and the shape.
threadId. If your backend rotates the thread (expiry, failure, migration), just return the new id in data — the SDK overwrites the cached value and uses it on the next turn automatically.
context round-trip IS your session storage.
The same pattern works for anything opaque you need per-conversation: API rate-limit bucket IDs, A/B cohort labels, tool-call state, reasoning scratchpads — whatever your AI backend needs to resume, stash it in
data and read it from context on the next turn.Console configuration
In the onboarding flow editor, add a step of type Interactive Chat, then set:Auto-tracked events
The standard onboarding step events (
onboarding_step_viewed, onboarding_step_completed) also fire for chat steps.
Async Step Hooks
The onboarding delegate carries four async veto hooks. They are not observations: native pauses the flow and waits for your answer before it proceeds. Each is optional; if you omit one — or take longer thanvetoTimeout (5 seconds by default) — native applies the hook’s own default and moves on.
Their TypeScript return type is
Promise<Record<string, unknown>> (or | null) — the objects below
are what native decodes out of it.
onBeforeStepAdvance
Called before the SDK advances to the next step. Return aStepAdvanceResult to control what happens
next:
StepAdvanceResult
Use stay for “I handled it; keep the user here”
Use { type: 'stay', ... } when your hook handled the user’s action — for example, sent a
password-reset email — and you want the user to remain on the same step. This is distinct from block
(error styling) and from proceed (advances).
onBeforeStepRender
Called before a step is rendered. Return aStepConfigOverride to dynamically modify the step’s
content, or null to leave it as authored:
StepConfigOverride: fieldDefaults, title, subtitle, ctaText,
layoutOverrides. The SDK merges these into the step config before rendering.
onElementInteraction
Called when the user interacts with a block inside a step — a button, a chip, a toggle. Return a live-state result to update the step in place, ornull for no change.
onPermissionRequest
Called before the SDK shows an OS permission prompt from apermission step. Return
{ handledByHost: true, granted } to short-circuit it (you asked for the permission yourself, or you
know the answer), or null to let the native flow run.
Every hook is
async — the SDK shows a loading indicator while waiting. If a hook throws or times
out, the SDK applies the default in the table above and continues; it never hangs the flow.Row Direction and Distribution
Therow content block supports configurable direction and distribution, set in the Console:
Button Gradients
Buttons support gradient backgrounds configured in the Console:
Gradients override the solid
background_color when set.
Select Display Styles
input_select form-input blocks (and select form fields) support three display styles:
The display style is configured per select field in the Console under
field_config.display_style. Question-step option lists always render in a fixed grid layout; the styles above apply specifically to input_select / select.
Progress Bar Custom Colors
Theprogress_bar content block supports custom color configuration:
Colors are configured per block in the Console. When not set, the SDK uses the app’s primary theme color.
Per-Step Progress Visibility
The progress indicator can be hidden on specific steps while still counting them in the total progress. This is useful for splash screens, permission prompts, or transition steps where the progress bar would be distracting. Configurehide_progress per step in the Console under Step Design > Logic. When enabled, the progress bar is hidden on that step but the step still contributes to the overall progress calculation (e.g., step 3 of 5 still advances progress to 60%).
This is a per-step override of the flow-level
show_progress setting. If show_progress is false at the flow level, the progress bar is hidden on all steps regardless of hide_progress.Conditional Branching
Onboarding flows support conditional branching — the next step can change based on the user’s answer. Branching is configured entirely in the Console. Example:- In the Console, open your onboarding flow (Onboarding > Flows).
- On a question step, click Add branching rule.
- Map each answer option to a target step.
- The SDK handles routing automatically — no TypeScript code needed.
responses map in onOnboardingCompleted.
Auto-Tracked Events
The native SDK automatically tracks the following onboarding-related events:
Each event includes the
flowId, stepId, and stepIndex where applicable.
Configuration in Console
Onboarding flows are managed in the AppDNA Console:- Navigate to Onboarding > Flows.
- Create a new flow or edit an existing one.
- Add steps (welcome, question, value_prop, form, interactive_chat, custom) and configure their content.
- Set targeting rules to control which users see the flow.
- Publish the flow to make it available to the SDK via the config bundle.
Full Example
Paywall trigger nodes — entitlement-aware skip + restore behavior
Onboarding flows can includepaywall_trigger graph nodes that present a paywall in the middle of a flow and route the user via per-outcome targets (on_success_target, on_fail_target, on_dismiss_target). Triggers are configured in the AppDNA Console and the SDK presents them automatically when the flow reaches the node.
Entitlement-aware skip (default ON)
Each paywall_trigger node carries askip_if_subscribed flag (default true). When the native SDK reaches the trigger, it checks the user’s entitlement state first:
- User has an active subscription → SDK skips presentation, emits an
onboarding_paywall_skipevent withreason=user_already_subscribed, and routes the flow via the configured skip target (see below). - User has no active subscription → SDK presents the paywall as before; outcome routing is unchanged.
true at read time.
From React Native you can check the same entitlement state used by the gate via:
Subscribed-user skip routing — picking the target
The console editor exposes a dedicated dropdown directly under the Skip if user already has an active subscription toggle: When skipped (user is already subscribed), go to. Four options:
The native SDKs resolve the target with this chain at runtime:
on_subscribed_skip_target → falls back to on_success_target → falls back to “follow downstream edge”. The fallback means flows authored without an explicit skip target that worked around this by setting on_success_target = complete_flow continue to behave identically.
Warning callout in the editor
If you open a paywall_trigger that has Skip if user already has an active subscription on but the resolved skip target is “Follow downstream edge” and the next node in the graph is another paywall_trigger, the editor displays an inline warning:Heads up: this paywall is set to skip for subscribed users, but the next step is another paywall trigger — subscribed users will still see it. Set “When skipped” to “Complete flow” to bypass the rest of the paywall chain.One click on Complete flow in the dropdown clears the warning and fixes the chain. This is the most common authoring trap with multi-paywall flows (main paywall → winback paywall → end); the warning surfaces it before publish.
Restore now auto-dismisses + routes to success
Tapping Restore Subscription on a paywall now:- Fires
onPaywallRestoreCompleted(paywallId, restoredProductIds)on yourAppDNAPaywallDelegate. - Auto-dismisses the native paywall surface when
restoredProductIdsis non-empty (i.e., the restore actually found entitlements). - Routes the onboarding flow via the trigger’s
on_success_target— same path as a real purchase.
restoredProductIds: []) the paywall stays up so the user can attempt a fresh purchase or close manually.
Hosts that handle their own dismiss
If youronPaywallRestoreCompleted handler already dismisses the paywall itself (e.g., to show a “Restored — tap continue when ready” overlay), the SDK’s internal dispatchedDismiss guard ensures the auto-dismiss never fires twice — the first caller wins. onPaywallDismissed fires exactly once regardless of who triggered the dismissal.
AppDNA.identify(userId) refreshes the entitlement cache
When you call AppDNA.identify(...) after the user signs in, the native SDK silently refreshes its entitlement cache in the background. The next paywall_trigger gate then reflects the identified user’s StoreKit / Play Billing subscriptions instead of the prior anonymous user’s empty entitlement state.
AppDNA.identify(...) again with the same user ID — the cache refresh is idempotent.
Migration risk callout
Measurement mode (wheel_picker)
wheel_picker supports an optional measurement mode for capturing a weight, height, temperature, or any custom quantity with a unit toggle. Enable it in the console by setting a measurement type (Weight, Height, Temperature, or Custom) on the wheel picker. When set, the picker renders with:
- a unit toggle (e.g. kg ⇄ lbs, cm ⇄ in, °C ⇄ °F) that converts the value live, and
- a measurement visual style —
ruler(tick tape),gauge(radial dial),dial(rotary drum), orwheel(classic drum).
Reported value
The SDK stores the value in the base unit (the first unit you define) so it stays stable when the user flips units. Alongside it, the SDK reports the chosen unit and the displayed value:
The element-interaction callback receives
{ value, display_value, unit }, where value is the base-unit value.
When no measurement type is set, wheel_picker behaves exactly as before.
