Use this file to discover all available pages before exploring further.
Use a custom screen when one step of your flow should be rendered by your app instead of by the standard Flowboard renderer.When Flowboard reaches a screen with type: "custom", the React Native and Flutter SDKs call your customScreenBuilder.On SDKs that expose customScreenBuilder, the builder receives a context object with:
ctx.formData: the data already collected in the flow
ctx.screenData: the JSON for the current custom screen
ctx.onNext(): move to the next screen
ctx.onPrevious(): move to the previous screen
ctx.onFinish(): end the flow
ctx.onJumpTo(screenId): jump to another screen in the same flow
The native iOS example above is a workaround. It launches your own UIKit surface from a custom action, but it does not replace a Flowboard type: "custom" screen inside the flow because that API is not yet exposed in the Swift package.
Use the simple inline approach first when you are prototyping. Move to a router only when you have more than one custom screen or your screen logic starts growing.
For production, keep your customScreenBuilder small and route by screen ID in one place.This gives you a single entry point, keeps launch code clean, and makes each custom screen easier to test.Recommended structure:
Put the router in one dedicated file
Use a switch on screenId
Load each custom screen from its own file
Keep a fallback screen for unknown IDs
// src/flowboard/customScreens.tsximport type { FlowboardContext } from 'flowboard-react';import { FlowboardPaywallScreen } from './screens/FlowboardPaywallScreen';import { FlowboardProfileSummaryScreen } from './screens/FlowboardProfileSummaryScreen';import { UnknownFlowboardScreen } from './screens/UnknownFlowboardScreen';export function buildFlowboardCustomScreen(ctx: FlowboardContext) { const screenId = String(ctx.screenData.id ?? ''); switch (screenId) { case 'paywall': return <FlowboardPaywallScreen ctx={ctx} />; case 'profile_summary': return <FlowboardProfileSummaryScreen ctx={ctx} />; default: return <UnknownFlowboardScreen ctx={ctx} />; }}// src/flowboard/screens/FlowboardPaywallScreen.tsximport { Button, Text, View } from 'react-native';import type { FlowboardContext } from 'flowboard-react';export function FlowboardPaywallScreen({ ctx,}: { ctx: FlowboardContext;}) { const properties = (ctx.screenData.properties as Record<string, unknown> | undefined) ?? {}; const title = typeof properties.title === 'string' ? properties.title : 'Upgrade to Pro'; const ctaLabel = typeof properties.ctaLabel === 'string' ? properties.ctaLabel : 'Continue'; const plan = typeof ctx.formData.plan === 'string' ? ctx.formData.plan : 'free'; return ( <View style={{ flex: 1, justifyContent: 'center', padding: 24 }}> <Text style={{ fontSize: 28, fontWeight: '700' }}>{title}</Text> <Text style={{ marginTop: 12 }}>Current plan: {plan}</Text> <Button title={ctaLabel} onPress={ctx.onNext} /> </View> );}// src/features/onboarding/launchOnboarding.tsimport { Flowboard } from 'flowboard-react';import { buildFlowboardCustomScreen } from '../../flowboard/customScreens';export async function launchOnboarding() { await Flowboard.launchOnboarding({ customScreenBuilder: buildFlowboardCustomScreen, });}
Adjust the example file paths to match your app structure. The important part is the separation between the router and the screen components. On native iOS, keep the same router idea, but route to your own presented UIKit or SwiftUI surfaces from onCustomAction(...).