Process data from flows in React Native SDK
When a user types into an input field, answers a quiz, or flips a toggle in a flow, the SDK passes the value to your app through its analytics callback.
Apps most often use that data to:
- Register users on their own backend: Take the email address and the name a user entered in your onboarding flow, and create their account when the flow closes.
- Save answers and preferences: Track what the user picked so your app can act on it later — for example, write it to their Adapty profile as custom attributes.
- Customize future flows: Save quiz answers as custom attributes, then target a later placement so each segment gets a different flow, or a different paywall inside it.
- Feed third-party analytics platforms: Forward answers to Amplitude, Mixpanel, or whichever product analytics you run.
Inputs and selectable groups report their values automatically. To tell inputs apart in your code, give each input a meaningful Element ID and each selectable group a Group ID in the builder.
Before you start
You need:
- Adapty SDK v4 or later: The flow callbacks don’t exist in earlier versions.
- A flow built in the Flow & Paywall Builder: Only flows report input values through this callback.
- A recently published flow version: A flow reports input values only if you published it after this feature became available. If nothing reaches your app, publish a new version of the flow and try again.
Receive input values
Input values reach the same handler as every other analytics event from a flow, under the event name flow_user_input. Register onAnalytics alongside your other flow event handlers:
const unsubscribe = view.setEventHandlers({
onAnalytics(name, params) {
handleFlowInput(name, params);
return false; // keep the flow open
},
});
The onAnalytics callback delivers all analytics events from a flow, including screen views.
- The
nameparameter contains the event name. To filter for user input events, comparenametoflow_user_input. - The
element_typeparameter names the element category. - The value of the input is stored in different parameters depending on the element type:
- Text fields, pickers, and toggles store the user’s input in
value - Selectable groups report the active options in
item_idsanditem_titles
- Text fields, pickers, and toggles store the user’s input in
function handleFlowInput(name, params) {
if (name !== 'flow_user_input') return;
// The screen the input sits on. Pair it with element_id to tell apart
// two fields that share an Element ID on different screens.
const screenId = params.instanceId;
switch (params.element_type) {
case 'text_input':
case 'email_input':
case 'number_input':
case 'phone_input': {
const text = params.value;
break;
}
case 'date_picker':
case 'time_picker':
case 'date_time_picker': {
// Unix time in milliseconds.
const date = new Date(params.value);
break;
}
case 'single_choice': {
const optionId = params.item_ids[0];
break;
}
case 'multi_choice': {
const optionIds = params.item_ids;
break;
}
case 'toggle': {
const isOn = params.value;
break;
}
}
}
To confirm the callback fires, interact with the input in a test build of your own app. If your callback handler doesn’t receive an event, check the prerequisites. Make sure that the flow was published after this feature became available.
When your app receives the input
The default input or selection value never reaches your app through this callback. If a user accepts the option marked Set as default and moves on, no event fires. Don’t read the missing event as “no answer” — the user simply left the default value in place.
The following elements trigger this event:
- Text, email, number, and phone fields
- Date, time, and date-time pickers
- Single-choice and multi-choice selectable groups, and toggles
The following don’t:
- Password fields, product selections, and tab switches
- Inputs inside a Header element, which is shared across screens
- Selectable groups with duplicate or missing option Element IDs, or with a Group ID reused on another screen. Such a group sends nothing at all, rather than part of the answer.
The event is triggered when:
- A field loses focus. A cleared field reports an empty string; a field the user never edited reports nothing. A placeholder is not a value. If the user returns to the field, edits it, and leaves it again, a second event follows.
- The user closes a picker after picking a new value. Closing it unchanged reports nothing.
- The user taps an option or a toggle. A multi-choice event lists every selected option, so deselecting the last one sends two empty arrays.
The event is not triggered when:
- The user types. There is no keystroke stream, only the value the field holds when focus leaves it.
- The user submits or closes the flow. A value still being edited at that moment can be lost; the delivery limitations section covers how to design the last screen around this.
- A value is set without user interaction. An option marked Set as default is pre-selected when the screen opens, and a Set Variable action can select an option or fill an input from another interaction. Neither sends an event; a pre-filled input is reported only once the user edits it.
What you receive
The callback delivers two events. Filter by name to only display flow_user_input events. The response JSON payload looks like this:
{
"name": "flow_user_input",
"instanceId": "scr_registration",
"isBackendEvent": false,
"isCustomerEvent": true,
"element_id": "email",
"element_type": "email_input",
"value": "jane@example.com"
}
| Parameter | Description |
|---|---|
name | flow_user_input for input events, flow_screen_showed for screen views. |
instanceId | The ID of the input’s screen. Element IDs are unique within a screen, not across the flow. If your flow has inputs on more than one screen, pair instanceId with element_id when filtering events. |
element_id | The Element ID of the input, or the Group ID of the selectable group. |
element_type | The type of element that sent the event. It determines which of the parameters below carry the input value. |
value | Text fields, pickers, and toggles only. The input value: a string for text fields, an integer for pickers, a boolean for toggles. |
item_ids | Single-choice and multi-choice selectable groups only. The Element IDs of the selected options, in the order the options appear in the builder. One entry for a single-choice group; any number for a multi-choice group. |
item_titles | Single-choice and multi-choice selectable groups only. The titles of the options listed in item_ids, in the same order. Never empty: an option without a title reports its ID instead. |
isCustomerEvent | Utility flag, always true for this event. It marks events the SDK delivers to your callback. Useful if one handler forwards every flow event to your analytics and you gate on the flag rather than on name. |
isBackendEvent | Utility flag, always false for this event. It marks events Adapty also records for its own analytics. false confirms that what users enter reaches your app and nowhere else — Adapty doesn’t receive or store it. |
What each element reports:
| In the builder | element_type | Parameter that stores value | What it holds |
|---|---|---|---|
| Text, Number, Phone number input | text_input, number_input, phone_input | value | The raw string the user typed. Numbers arrive as strings, not as numeric types. |
| E-mail input | email_input | value | The raw string the user typed, even if it failed the builder’s format validation. Validate it on your side before you use it. |
| Password input | none | none | Sends no event. |
| Date input | date_picker | value | Unix time in milliseconds, as an integer, at local midnight of the selected date. |
| Time input | time_picker | value | Unix time in milliseconds, as an integer, rounded down to the minute. |
| Date & Time input | date_picker and time_picker | value | Two elements, a date picker and a time picker. Each sends its own event. |
| Input switched to Date & Time in the Type dropdown | date_time_picker | value | Unix time in milliseconds, as an integer, rounded down to the minute. |
| Single choice group | single_choice | item_ids, item_titles | Two arrays. item_ids: an array with the selected option’s Element ID. item_titles: an array with that option’s title. |
| Multi-choice group | multi_choice | item_ids, item_titles | Two arrays. item_ids: the Element IDs of every selected option, in the order the options appear in the builder. item_titles: their titles, in the same order. Both arrays are empty when nothing is selected. |
| Toggle group | toggle | value | A boolean. |
To branch on an answer, compare item_ids, not item_titles. A title is derived: the option’s Element Title if you set one, otherwise its text in your default locale, otherwise its Element ID. A user who read the flow in another language saw different text.
Event examples
These examples show the properties available on each event, with illustrative values in comments.
Text, email, number, and phone input (Click to expand)
onAnalytics(name, params) {
name; // 'flow_user_input'
params.name; // 'flow_user_input'
params.instanceId; // 'scr_J260KU5q'
params.isCustomerEvent; // true
params.isBackendEvent; // false
params.element_id; // 'email'
params.element_type; // 'email_input'
params.value; // 'jane@example.com' (string)
} Date, time, and date-time pickers (Click to expand)
onAnalytics(name, params) {
params.element_id; // 'birthday'
params.element_type; // 'date_picker'
params.value; // 645408000000 (Unix milliseconds — 1990-06-15, local midnight)
} Single choice (Click to expand)
onAnalytics(name, params) {
params.element_id; // 'experience'
params.element_type; // 'single_choice'
params.item_ids; // ['pro']
params.item_titles; // ['I train professionally']
} Multi choice (Click to expand)
onAnalytics(name, params) {
params.element_id; // 'interests'
params.element_type; // 'multi_choice'
params.item_ids; // ['sports', 'music']
params.item_titles; // ['Sports', 'Music']
} Toggle (Click to expand)
onAnalytics(name, params) {
params.element_id; // 'reminders'
params.element_type; // 'toggle'
params.value; // true (boolean)
} Delivery and limitations
Flows send raw values — email addresses, phone numbers, and whatever else a user types. Treat everything the analytics callback delivers as personal data, and don’t write it anywhere you wouldn’t write a user’s email address.
- Last value wins: You get one event per field, carrying the value the user settled on rather than a keystroke-by-keystroke stream. If they edit a field, only the last version reaches you.
- No submit guarantee: Values reach you as users move through the flow, and a user can leave at any point. Wait for the flow to close before treating a set of answers as complete.
- Best-effort delivery: If a user closes the flow or minimizes the app while a field is still focused or a picker is still open, that value can be lost.
To make the last field’s value reliable, end the flow with a screen that has no inputs and no pickers, and close the flow from an explicit user action rather than automatically when that screen appears. Moving to the final screen takes focus off the previous field, which is what sends its value.
Store each input value as your handler receives it, and send the complete set when the flow closes. To catch that moment, register an onDisappeared handler alongside onAnalytics. It runs when the flow view is dismissed, whether the user finished the flow or closed it partway.
Use cases
Register users on your backend
Collect values as they arrive and send them once the flow closes, so that one request carries a complete set of answers.
The flow disappears whether a user finished it or abandoned it partway. Guard on the fields you need before you call your backend.
The flow can’t display errors from your backend. The handler’s return value only closes the flow view, and the SDK has no method that sends data into a running flow. If registration fails, for example because the email is already in use, show the error in your own UI after the flow closes.
const flowAnswers = {};
const unsubscribe = view.setEventHandlers({
onAnalytics(name, params) {
if (name === 'flow_user_input' && typeof params.value === 'string') {
flowAnswers[params.element_id] = params.value;
}
return false;
},
onDisappeared() {
if (flowAnswers.email) {
// Send flowAnswers to your backend here to create the account.
}
return false;
},
});
Enrich user profiles with data
To link what a user entered to their profile and avoid asking for the same details twice, update the user profile as the values come in.
For example, if your flow has a text input with the Element ID name and an email input with the Element ID email:
function handleFlowInput(name, params) {
if (name !== 'flow_user_input') return;
if (typeof params.value !== 'string') return;
const profileParams = {};
switch (params.element_id) {
case 'name':
profileParams.firstName = params.value;
break;
case 'email':
profileParams.email = params.value;
break;
default:
return;
}
adapty.updateProfile(profileParams).catch(error => {
// handle the error
});
}
Customize flows shown later
Quiz answers can also decide what a user sees at a later placement — a different flow, or a different paywall inside it.
For example, ask users about their experience with sport in your onboarding flow, then show each group its own flow with different products and copy.
- Add a quiz to your flow. Give the selectable group the Group ID
experience, and each option a meaningful Element ID. - Handle the answers and set custom attributes for the user.
function handleFlowInput(name, params) {
if (name !== 'flow_user_input') return;
if (params.element_id !== 'experience') return;
adapty
.updateProfile({
// Set the custom attribute 'experience' to the option the user selected
// (beginner, amateur, or pro).
codableCustomAttributes: { experience: params.item_ids[0] },
})
.catch(error => {
// handle the error
});
}
- Create a segment for each custom attribute value.
- Create a placement and add an audience for each segment.
- Display the flow for that placement in your app.