onStepChange and onOnboardEnd to send events to your own tools (MMP, ads SDK, product analytics).
What you can track from Flowboard callbacks
| Callback | When it fires | Data you receive |
|---|---|---|
onStepChange(stepId, pageStep, formData) | Every time the user lands on a step | stepId (screen id), pageStep (0-based step index), formData (current snapshot of answers) |
onOnboardEnd(formData) | When the flow finishes | formData (final collected answers) |
formData keys match your Flowboard input id values.
Example:
{
"email": "ada@example.com",
"plan": "pro",
"accept_terms": true
}
Keep input
id values stable in Flowboard. They are your analytics data contract.Simple custom tracking examples
Use this pattern when you only need to forward events to your own tracking layer.import { Flowboard } from 'flowboard-react';
export async function launchTrackedFlow() {
await Flowboard.launchOnboarding({
onOnboardEnd: (formData) => {
console.log('Flow finished:', formData);
myTracking.track('flow_completed', {
plan: typeof formData.plan === 'string' ? formData.plan : undefined,
acceptedTerms: Boolean(formData.accept_terms),
});
},
onStepChange: (stepId, pageStep, formData) => {
console.log('User moved to step:', stepId, pageStep, formData);
myTracking.track('flow_step_viewed', {
stepId,
pageStep,
plan: typeof formData.plan === 'string' ? formData.plan : undefined,
});
},
});
}
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flowboard_flutter/flowboard_flutter.dart';
Future<void> launchTrackedFlow(BuildContext context) async {
await Flowboard.launchOnboarding(
context,
onOnboardEnd: (formData) {
debugPrint('Flow finished: $formData');
myTracking.track('flow_completed', {
'plan': formData['plan'],
'acceptedTerms': formData['accept_terms'] == true,
});
},
onStepChange: (stepId, pageStep, formData) {
debugPrint('User moved to step: $stepId $pageStep $formData');
myTracking.track('flow_step_viewed', {
'stepId': stepId,
'pageStep': pageStep,
'plan': formData['plan'],
});
},
);
}
import FlowboardSwiftCore
import FlowboardSwiftUIKit
import UIKit
func launchTrackedFlow(from host: UIViewController) {
Task {
do {
try await Flowboard.launchOnboarding(
from: host,
options: .init(
onOnboardEnd: { formData in
print("Flow finished:", formData)
myTracking.track(
"flow_completed",
properties: [
"plan": formData.string("plan") ?? "",
"acceptedTerms": formData.bool("accept_terms") ?? false
]
)
},
onStepChange: { stepId, pageStep, formData in
print("User moved to step:", stepId ?? "", pageStep, formData)
myTracking.track(
"flow_step_viewed",
properties: [
"stepId": stepId ?? "",
"pageStep": pageStep,
"plan": formData.string("plan") ?? ""
]
)
}
)
)
} catch {
print("Failed to launch Flowboard:", error)
}
}
}
Concrete example: Amplitude + Adjust
The example below sends:- Product analytics events to Amplitude
- Attribution / marketing events to Adjust
For Expo, use a development build or prebuild setup for native SDKs such as Adjust.
React Native / Expo (development build)
npm install @amplitude/analytics-react-native react-native-adjust
import { Flowboard } from 'flowboard-react';
import { init as amplitudeInit, track as amplitudeTrack } from '@amplitude/analytics-react-native';
import { Adjust, AdjustConfig, AdjustEnvironment, AdjustEvent } from 'react-native-adjust';
const ADJUST_STEP_TOKEN = 'abc123';
const ADJUST_COMPLETE_TOKEN = 'xyz789';
export function initTracking() {
amplitudeInit('AMPLITUDE_API_KEY');
const config = new AdjustConfig('ADJUST_APP_TOKEN', AdjustEnvironment.Production);
Adjust.create(config);
}
function trackAdjustStep(stepId: string, pageStep: number) {
const event = new AdjustEvent(ADJUST_STEP_TOKEN);
event.addCallbackParameter('step_id', stepId);
event.addCallbackParameter('page_step', String(pageStep));
Adjust.trackEvent(event);
}
function trackAdjustComplete(plan: string | undefined) {
const event = new AdjustEvent(ADJUST_COMPLETE_TOKEN);
if (plan) event.addCallbackParameter('plan', plan);
Adjust.trackEvent(event);
}
export async function launchTrackedFlow() {
await Flowboard.launchOnboarding({
onStepChange: (stepId, pageStep, formData) => {
const plan = typeof formData.plan === 'string' ? formData.plan : undefined;
amplitudeTrack('flow_step_viewed', {
step_id: stepId,
page_step: pageStep,
plan,
});
if (typeof stepId === 'string' && stepId.length > 0) {
trackAdjustStep(stepId, pageStep);
}
},
onOnboardEnd: (formData) => {
const plan = typeof formData.plan === 'string' ? formData.plan : undefined;
amplitudeTrack('flow_completed', {
plan,
accepted_terms: formData.accept_terms === true,
});
trackAdjustComplete(plan);
},
});
}
Flutter
flutter pub add amplitude_flutter adjust_sdk
import 'package:amplitude_flutter/amplitude.dart';
import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_config.dart';
import 'package:adjust_sdk/adjust_event.dart';
import 'package:flutter/material.dart';
import 'package:flowboard_flutter/flowboard_flutter.dart';
const String adjustStepToken = 'abc123';
const String adjustCompleteToken = 'xyz789';
final Amplitude amplitude = Amplitude.getInstance();
void initTracking() {
amplitude.init('AMPLITUDE_API_KEY');
final config = AdjustConfig('ADJUST_APP_TOKEN', AdjustEnvironment.production);
Adjust.initSdk(config);
}
void trackAdjustStep(String stepId, int pageStep) {
final event = AdjustEvent(adjustStepToken);
event.addCallbackParameter('step_id', stepId);
event.addCallbackParameter('page_step', pageStep.toString());
Adjust.trackEvent(event);
}
void trackAdjustComplete(String? plan) {
final event = AdjustEvent(adjustCompleteToken);
if (plan != null && plan.isNotEmpty) {
event.addCallbackParameter('plan', plan);
}
Adjust.trackEvent(event);
}
Future<void> launchTrackedFlow(BuildContext context) async {
await Flowboard.launchOnboarding(
context,
onStepChange: (stepId, pageStep, formData) {
final plan = formData['plan'] as String?;
amplitude.logEvent('flow_step_viewed', eventProperties: {
'step_id': stepId,
'page_step': pageStep,
'plan': plan,
});
if (stepId.isNotEmpty) {
trackAdjustStep(stepId, pageStep);
}
},
onOnboardEnd: (formData) {
final plan = formData['plan'] as String?;
amplitude.logEvent('flow_completed', eventProperties: {
'plan': plan,
'accepted_terms': formData['accept_terms'] == true,
});
trackAdjustComplete(plan);
},
);
}
import FlowboardSwiftCore
import FlowboardSwiftUIKit
import UIKit
let adjustStepToken = "abc123"
let adjustCompleteToken = "xyz789"
func trackAmplitude(_ event: String, properties: [String: Any]) {
amplitude.track(event, properties: properties)
}
func trackAdjust(token: String, callbackParameters: [String: String]) {
adjust.trackEvent(token: token, callbackParameters: callbackParameters)
}
func launchTrackedFlow(from host: UIViewController) {
Task {
do {
try await Flowboard.launchOnboarding(
from: host,
options: .init(
onStepChange: { stepId, pageStep, formData in
let plan = formData.string("plan")
trackAmplitude(
"flow_step_viewed",
properties: [
"step_id": stepId ?? "",
"page_step": pageStep,
"plan": plan ?? ""
]
)
if let stepId, !stepId.isEmpty {
trackAdjust(
token: adjustStepToken,
callbackParameters: [
"step_id": stepId,
"page_step": String(pageStep)
]
)
}
},
onOnboardEnd: { formData in
let plan = formData.string("plan")
trackAmplitude(
"flow_completed",
properties: [
"plan": plan ?? "",
"accepted_terms": formData.bool("accept_terms") ?? false
]
)
var params: [String: String] = [:]
if let plan, !plan.isEmpty {
params["plan"] = plan
}
trackAdjust(token: adjustCompleteToken, callbackParameters: params)
}
)
)
} catch {
print("Failed to launch Flowboard:", error)
}
}
}
Tracking recommendations
- Avoid sending raw PII to ad attribution tools unless your policy allows it.
- Keep event names stable (
flow_step_viewed,flow_completed). - Forward only the fields your downstream tools actually use.
- Version your event schema if you change
formDatakeys.