Supported on: iOS SDK
1.0.61+ · Android SDK 1.0.33+ · Flutter SDK 1.0.3+Present an Onboarding Flow
Present a specific onboarding flow by ID. The method resolves to abool — check it:
Before Flutter SDK 1.0.20 this returned
Future<void>, so an unknown flow ID completed exactly
like a successful presentation and the only way to find out was to read the device log. If you are
on an older version, upgrade — the call itself is unchanged and source-compatible.The Flutter 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 and to pass an OnboardingContext:
Module Methods
The static
AppDNA.presentOnboarding(flowId) is a convenience that calls AppDNA.onboarding.present(flowId) with no context.
OnboardingContext
Pass additional context when presenting a flow:The
experimentOverrides parameter is intended for development and QA only. In production, experiment variants are assigned automatically by the AppDNA backend.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 Flutter paywall docs for the full delegate surface.
AppDNAOnboardingDelegate
Implement theAppDNAOnboardingDelegate abstract class 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 Flutter.
Selection Modes
Question steps support two selection modes:Apple permission strings
A permission step requests a real OS permission. On iOS, add the Info.plist key for a permission type only when your flow authors a permission step of that type (inios/Runner/Info.plist). If the key is missing, the step skips the request safely and records the permission as unavailable.
- Every iOS build needs
NSLocationWhenInUseUsageDescription, whether or not your flow has a location step: the SDK links CoreLocation and Google Maps. - From Flutter 1.0.20 (iOS SDK 1.0.82) the SDK no longer references the Contacts, EventKit, App Tracking Transparency or Photos-library APIs unless your flow uses those permission steps, so you no longer need their strings just because the SDK is linked. If App Store Connect still asks for a key, add it and tell us. On Flutter 1.0.19 and earlier the strings are still required.
- The image-picker block uses the system photo picker, which needs no photo-library permission.
- Android: the SDK’s own manifest merges
INTERNET,ACCESS_NETWORK_STATE,WAKE_LOCK,POST_NOTIFICATIONSandVIBRATEinto yours; its library dependencies (Firebase, Google Play Billing, Google Maps) may merge their own.
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 Dart 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.
Map block: route line and camera
With Interactive (pan & zoom) on and the Google provider set in Settings → Apps → Maps, the map is drawn natively (see Maps). A route’s line comes from, in order: a polyline yourmapRoutes supplies; the Route from a variable, when it resolves; the authored Fixed route line; else straight segments between the stops. The polyline must use Google’s encoding, precision 5 — precision-6 polylines (polyline6, e.g. Mapbox / OSRM) are not supported: read as precision 5 their coordinates come out ten times too large, so they are usually rejected as out of range (the stops are used instead), but a route whose every point lies within 9° of the equator and 18° of the prime meridian decodes in range and draws a wrong line. Always encode at precision 5.
With Auto-fit to the route and stops on (the default), the camera frames the whole route line plus the stops, with a fixed padding, and re-frames when a host mapRoutes reply arrives after the first paint; your own pan and zoom are otherwise never overridden. A single point is shown at zoom 15. With Auto-fit off, the camera centres on the authored map centre at the authored zoom. On iOS, routes that cross the antimeridian (±180° longitude) are not framed correctly: the fit does not wrap around it, so such a route is framed across the long way round the globe. Android frames with the Google Maps SDK’s bounds builder.
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. A sign-in action advances only on a reply with a recognised type: {}, null, or an unknown type blocks it with “Sign-in isn’t available right now. Please try again later.” (both platforms).
How long the SDK waits for a sign-in answer. For every action in the table below the plugin’s bridge waits at least 120 seconds for your onBeforeStepAdvance reply — long enough for OS sign-in sheets, account pickers, 2FA and a backend round trip — or longer if you set a larger AppDNAOptions.vetoTimeout. Every other hook is bounded by vetoTimeout (default 5 s), except that a refresh gets 8 s (a larger vetoTimeout does not extend it) and onBeforeStepRender is cut off at 3 s by the SDK. If no answer arrives in time (or your handler throws), an auth action does not advance: it blocks with “Sign-in isn’t available right now. Please try again later.” and a late reply is dropped. So:
- Make the sign-in handler idempotent. If sign-in completed after the deadline, the user’s retry should find the session already established and answer
{'type': 'proceed'}at once. - Answer promptly when the user cancels the OS sheet — return
{'type': 'stay'}(or ablockwith a message). The 120 s is a ceiling, not a wait.
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?and fail explicitly ifnullrather 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
StepAdvanceResult Dart class). Recognized shapes:
Existing
next, skip, link and permission button actions are unchanged. Host apps that already implement AppDNAOnboardingDelegate keep working without changes — the default onBeforeStepAdvance returns {}, which advances every step except a sign-in action: a sign-in action (including social_login) is blocked until your handler answers it with an explicit type. Host apps with no Dart delegate get the “Sign-in isn’t available right now. Please try again later.” block when an auth action fires; for those flows, either implement the delegate or keep using next as the button action.Custom View Registration
custom_view blocks are rendered by a host-native view registered on each platform, not by a Dart widget — the Flutter plugin is a thin wrapper and does not host a Flutter surface inside the native onboarding step. There is no AppDNA.registerCustomView(...) in the Dart API.
Register the view on the native side and reference its identifier from the custom_view block in the Console:
- iOS — register a SwiftUI/UIKit view factory with the native AppDNA SDK in your
AppDelegate. - Android — register a native view factory with the AppDNA SDK in your
Application.
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.
Flag CTAs
A button can record the user’s choice without leaving the flow. Set its action to Flag & continue in the Console and give it a flag key (and optionally a value, defaulttrue). Tapping it advances the step exactly like a normal Continue button — same
required-field validation, same next-step rules — and writes one key.
Use it when a step offers something your app must act on later: “add a private workshop”,
“start the trial”, “talk to a human”. Routing to that destination mid-onboarding abandons a
flow the user is halfway through; the flag lets you route once, after onboarding finishes.
The flag arrives in two places. It is in the step’s own answers, alongside every other value
that step collected, and it is also collected flat under flags so you don’t have to walk
every step to find it:
The value is always a string —
"true" when no value was authored — so it behaves
identically across iOS, Android, Flutter and React Native.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 (the iOS Location content block uses Apple MapKit search instead), and displays a dropdown of suggestions. The user selects a result and the SDK stores the complete structured data. If the user types without selecting a suggestion, the step response keeps the typed String, on both platforms and both surfaces (form-step field or content block).
Location Data Structure
A selection contains the fields below.AppDNA.deepLinks.getLocationData(fieldId) returns them as a LocationData whose fields, except formattedAddress, are nullable: after typing without selecting it returns only formattedAddress and rawQuery (the typed text) with null coordinates and null city / state / country, and with no (or an empty) answer it returns null. A selection fills the fields that have a value, the same way on both platforms and both surfaces (form-step field or content block; the iOS content block has no stateCode). See LocationData.
Accessing Location Data
Location data is returned as a nested map 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 supports two async hooks that let you intercept step transitions for server-side validation, dynamic content loading, or custom routing logic.onBeforeStepAdvance
Called before the SDK advances to the next step. Return aFuture<Map<String, dynamic>> describing what happens next (there is no StepAdvanceResult Dart class):
Advance result map
Use stay for “I handled it; keep the user here”
Return a stay map 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 aFuture<Map<String, dynamic>?> config-override map to dynamically modify the step’s content, or null for no override (there is no StepConfigOverride Dart class):
fieldDefaults, title, subtitle, ctaText, fieldOptions, dataContext (see Host-supplied data), mapRoutes. The SDK merges these into the step config before rendering.
fieldOptions is { blockId: [option, …] } — supply a Select’s options from your own backend, with your own auth and cache. A block you do not name keeps its authored options.
The SDK waits up to 3 seconds for this callback and then renders without the override, falling back to cached and then authored options. Do your own network work with your own timeout — a reply that arrives after the user has left the step is discarded rather than applied to a screen they are no longer looking at.
Both hooks are
async — the SDK shows a loading indicator while waiting for your response. If either hook throws an error or times out, the SDK proceeds normally — except for a sign-in action in onBeforeStepAdvance, which blocks with “Sign-in isn’t available right now. Please try again later.” (see Authentication entry). The bridge waits up to AppDNAOptions.vetoTimeout (default 5 s) for each hook; onBeforeStepRender is still cut off at 3 s by the SDK, a refresh gets 8 s (a larger vetoTimeout does not extend it), and sign-in actions wait at least 120 s.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 these 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.
Image Tile Layouts
Withdisplay_style: "image_tiles", field_config.tile_image_layout controls where the label sits
relative to the photo. Text over a photograph is only readable when the photo cooperates, so the
other two layouts give the text a surface of its own.
An option’s
image_overlay_color tints the photo only — in image_strip and contained it
stops at the text surface, so a dark scrim for legibility does not also mute the band color.
Dynamic option lists (Option Sets)
A Select can take its options from an Option Set — a named list managed outside the flow — instead of a list authored inline. Editing the set reaches devices without republishing the flows that use it, which is the point: a customer who adds an item should not need anyone to touch a flow.
The list is never fetched in front of the user. The SDK resolves what to show synchronously,
in this order:
- a cached copy of the set
- the embedded page — the first N items, published inside the flow config
- the options authored on the block
- nothing, and the step is skipped
field_options is still used when a set is bound — it is the embedded page. An app built before
this feature renders those items rather than an empty list, so a set is safe to introduce without
waiting for everyone to update.The selected item
After a choice,{{selected.<field_id>.…}} resolves the option rather than just its value, so
a later screen can say “You chose Sunrise Estate — Lakeside” without the author repeating the list:
The value your integrations already receive is unchanged. The option payload is deliberately
kept out of
responses, so webhook bodies are byte-identical for anyone not using this.
Template Variables
Any text an author writes can reference an answer the user has already given:
Variables resolve in every text field of every onboarding block — text, labels, placeholders,
an option’s label and subtitle, image and media URLs, list items and the text inside a block’s
settings. (An option’s stored value can come from data only on a Select whose options follow a host
list — see Host-supplied data; a fixed option’s value stays fixed.) A reference that cannot resolve and has no fallback renders as empty: an option whose
label cannot resolve is hidden, and a summary stat is hidden, because a raw token in a large numeral is
more jarring than an absent card. Publishing warns about references nothing supplies.
SDKs before iOS
1.0.82 / Android 1.0.54 resolve variables only in a block’s text, label,
placeholder, badge and toggle label, an option’s label, subtitle and leading text, and a summary
stat’s value and label — and show an unresolved reference there as the literal text the author typed.
Host-supplied data
Some screens show values only your backend knows — a recommendation, a live price, a booking slot. Your app supplies them by returning adataContext from onBeforeStepRender, and whatever is in
that dictionary becomes addressable in the Console as {{hook_data.…}}.
{{hook_data.booking.unitPrice}} in any slot that resolves variables, or
binds one of a block’s properties to a path in the block’s Dynamic Data section (stored as the
block’s bindings, for example {"image_url": "hook_data.recommendations.0.imageUrl"}) to drive a
value that is not a text slot — an image or video URL, a colour, a number. Paths walk dictionaries and
index lists, so hook_data.recommendations.0.imageUrl resolves against a list.
Two things worth knowing:
- The SDK makes no network call for this. It hands you the step and takes back a dictionary; you fetch with your own client, your own auth and your own caching.
- Both sides have to line up. A value you supply is only read if an author has referenced that path in the Console, and a path an author references renders empty if nothing supplies it. The Implementation Manual we generate for a flow lists both halves.
Images and other settings from your data
Text fields take a{{hook_data.…}} token directly. An image field — an Image block, a Select
option’s image, a Map block’s photo or marker — has a From host data mode in the Console instead:
the author picks a path and keeps the uploaded image as the fallback. The Console stores that as a
data_templates entry beside the static value:
https URL and the fallback
otherwise; an app on an older SDK never reads data_templates and shows the fallback. That is what
makes a flow safe to publish while installed apps are still on older versions.
Lists that follow your data
A Select can build its options from a list you return. In the Console, turn on Options from host data on the Select, choose the list path and fill in the template option — its label, subtitle, image and stored value each come from the list item ({{item.name}}, {{item.imageUrl}},
{{item.id}}), and its static text stays as the fallback older SDKs show.
- One option is generated per list item, up to the limit the author sets (20 by default, at most 50). Fixed options the author added are listed after the generated ones.
- The stored answer is the item’s value (
{{item.id}}above), soresponsesrecords the venue the user picked — not a slot name. Two items with the same value show once. - An empty or missing list shows the author’s empty-state text, or hides the Select if they chose that. A required Select with nothing to pick never blocks the user.
- Navigation rules cannot compare a generated answer to a fixed value — the values are whatever your app sends. Branch on “answered”, or on a fixed option.
While your data loads
The step appears immediately — nothing waits to paint. When you have set a delegate and the step referenceshook_data, a Select whose options depend on your data shows no generated options until
your reply arrives, for at most 3 seconds, and cannot be submitted meanwhile (pressing Continue
says “Loading…”); fixed options and everything else render normally. If your reply is later than
3 seconds, the Select settles without it (its empty state, if the author set one) and the late
reply is ignored.
”Show more”: refresh a list in place
iOS SDK
1.0.82+ · Android SDK 1.0.54+ · Flutter and React Native: the release whose appdna_feature_parity is 1.0.82. Read Older app builds before re-authoring a live button.- In the Console, add a Button and set its action to Refresh this step. Optionally fill
Value sent to your app (e.g.
recs_more): it reaches your handler asvalue, exactly as typed. Set the Select’s Max items to the longest list you will ever return (up to 50). - In your app, handle
onElementInteractionwhereaction == "refresh", and return the list indataContext, under the same key the Select repeats over, withadvance: false:
- The tapped button shows a spinner (its label hidden, its size and colours kept) until your reply
lands. While any interaction on the step is in flight, the step’s other Refresh this step buttons
cannot be tapped; everything else stays usable — the user can keep picking, searching, or tap Next.
While the step’s first
onBeforeStepRenderreply is still loading, Refresh this step buttons cannot be tapped either: there is no first page yet to show more of. dataContextmerges into the step’shook_dataper top-level key: a key you return replaces that key’s value whole, and keys you do not return keep their value. To remove a key, set it to Dartnull— e.g.{'banner': null}. Keys are names, not paths —"a.b"is a key calleda.b; to change a nested value, return its whole top-level value.- Everything that reads the key re-resolves: the Select’s generated options,
{{hook_data.…}}text, images.dataContextis honoured on the reply to every interaction (OTP, calendar, press-and-hold…), not onlyrefresh. If the same reply also carriesfieldOptionsfor the Select, those win over the list. nullchanges nothing and the button works again; no toast is shown — show your own UI if a fetch fails. A delegate that does not handlerefreshanswersnullafter one bridge round trip — the spinner shows for that moment and then ends.- The SDK waits at most 8 seconds for a
refreshreply. After that the spinner ends, nothing changes, and a later reply is ignored. The plugin’s bridge normally waits at mostAppDNAOptions.vetoTimeout(default 5 seconds; honoured on both iOS and Android from Flutter 1.0.20) for any delegate callback — but arefreshreply gets 8 seconds: the bridge waits that long even whenvetoTimeoutis shorter, and the SDK drops a reply after 8 s even whenvetoTimeoutis larger. Every other interaction keeps thevetoTimeoutlimit. - A reply that arrives after the user left the step (Next or Back) is dropped whole,
advanceincluded. - With
advance: truethe reply is applied first, then the step advances through the required-field check. If the user’s pick vanished from the list, the step stays, showing the new list and the validation message. - The data lives in memory for this flow presentation only: it is never persisted, sent to our backend, written to the responses or tracked. Keep one reply under ~256 KB.
- A Refresh this step button inside an option’s sheet, or on a Sound button (whose tap plays its audio), never fires. Publishing warns about both.
max must cover the longest list. Items past the Select’s Max items (default 20, at most 50)
are silently not rendered; a debug log names the block, the list’s length and max.
On a revisit, return the list the user last saw. When the user goes Back and returns,
onBeforeStepRender runs again, and its reply wins over earlier Show more pages for the keys it sets.
Keep your paging state per step and answer with the accumulated list — otherwise the list collapses to
page 1 and a pick from a later page is cleared. (On iOS the same re-run also happens when a paywall
shown from the flow is dismissed.)
Older app builds
A newly published flow can reach an app build with an older SDK. What a Refresh this step button does there:
Ship the app build with this SDK and a
refresh handler first; re-author the button after.
Behaviour changes in this release
These apply to every flow, including flows that never use Show more:- An interaction reply’s
inputValuePatchesmerge key by key onto the current answers. Before, the whole snapshot from the moment of the tap was written back, which reverted anything the user changed while the call was in flight (OTP, calendar, press-and-hold…). - A late interaction reply is dropped once the user has left the step or started a new visit to it.
Before, iOS always applied it — and
advance: truecould complete a step the user had already left — and Android applied it during the ≤ 250 ms exit animation. A slow reply no longer blocks interactions on the next step. - Android catches an exception thrown by
onElementInteraction, logs it and treats it asnull, instead of crashing. - On Android, Refresh this step buttons inside a Row, a Stack, a Section background or a Carousel page now fire; before, they silently did nothing. On iOS, Carousel pages change the same way.
- Flows already using Refresh this step (since iOS 1.0.79 / Android 1.0.51) now get the 8-second
deadline, the two tap locks above, the spinner and — on Flutter and React Native — the 8-second
minimum bridge wait (before, the bridge’s 5-second timeout: fixed on Flutter, the
vetoTimeoutdefault on React Native). - A tap on the outgoing step while it slides away is refused. Before, it reached your app carrying the incoming step’s id.
fieldConfigPatches/fieldOptionsapplied by any interaction are dropped when the same step view is reused for a new visit inside the exit animation (a very quick Back-and-forward). A normal Back then forward already dropped them.
Host data contract
Paste a sample of yourdataContext into Flow settings › Host data contract in the Console. It
fills the variable picker with your real paths, drives the editor preview (a repeat Select shows one
option per sample item) and lets publishing warn about paths your app does not send. It is sample
data for the Console only — it is never sent to devices; do not paste real personal data.
Supported on: {{hook_data.…}} in the original text slots — iOS SDK 1.0.76+ · Android SDK
1.0.48+ · Flutter SDK 1.0.14+ · React Native SDK 1.0.13+. Every other field, image data
templates and lists — iOS SDK 1.0.82+ · Android SDK 1.0.54+ · Flutter and React Native: the
release whose appdna_feature_parity is 1.0.82.
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 Dart 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 Flutter 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
Flutter has no host-callable paywall dismiss: the SDK owns the paywall’s lifecycle. A successful restore that finds products auto-dismisses it. To keep the paywall up instead, callAppDNA.skipNextAutoDismissOnRestore(true) before the user taps Restore — for example when you present the paywall. The paywall then stays on screen until the user closes it. Do not plan a Dart overlay (such as a “Restored — tap continue” message) for that moment: Flutter widgets render under the native paywall, so the user would not see it. onPaywallRestoreCompleted reaches Dart asynchronously, after the native SDK has already decided whether to dismiss, so setting the flag inside that callback is too late — and the flag would then carry over to the next restore. onPaywallDismissed fires exactly once.
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.
