Stepping through DaVinci flows in React Native
PingOne React Native
Each step of a DaVinci flow returns a node object representing the current state of the flow.
The node has a type property that indicates the current state:
| Node type | Description |
|---|---|
|
Indicates a step in the middle of the flow that requires input. The node contains a list of collectors. Call |
|
Indicates successful authentication. See Managing sessions and tokens in React Native for post-authentication operations. |
|
Represents an unexpected failure, such as a network error, timeout, or server error. Restart the flow when the app receives this node type. |
|
Indicates that the server rejected submitted data, such as an invalid value or a password that does not meet the configured requirements. Correct the data and submit the node again. |
To complete a DaVinci flow, handle each node type in your React Native application.
The node value is null until the flow is started, so the example starts the flow when the screen mounts.
The empty catch block is safe because start() records failures in the hook’s error state.
import { useEffect } from 'react';
import { useDaVinci } from '@ping-identity/rn-davinci';
function LoginScreen() {
const { node, next, start, loading, error } = useDaVinci(davinciClient);
useEffect(() => {
start().catch(() => {}); // error surfaced via hook state
}, [start]);
if (loading) {
// Show a loading indicator while the native module is processing
}
if (node?.type === 'ContinueNode') {
// Render input fields for node.collectors
// Call next(input) after collecting values
}
if (node?.type === 'SuccessNode') {
// Authentication succeeded, navigate to the home screen
}
if (node?.type === 'FailureNode') {
// Render a retry button whose onPress handler calls start()
}
if (node?.type === 'ErrorNode') {
// Display node.message or another user-friendly message
}
if (error) {
// Display or log the hook-level error
}
}
Handling ContinueNode
For ContinueNode steps, the server sends a list of collectors that require input or an action from the app.
There are two ways of handling the gathering of the required input:
- Normalizing collectors into form fields
-
The
useDaVinciFormhook maps collectors to a flat list of normalized fields, such as text, password, boolean, single-select, and multi-select fields, so your UI is driven by the kind of input that is needed.If the server flow changes from one collector type to another that maps to the same kind, the UI can continue to work without a code change.
Other benefits:
-
Validation is built in.
form.canSubmitandform.issuestell you whether the step is ready to submit and why not. -
Default values are pre-seeded. The hook hydrates
form.valuesfrom collector defaults automatically. -
form.metaflags describe mixed steps, including manual input, output-only content, integrations, and unsupported collectors. -
The submit payload is built for you.
form.inputis ready to pass tonext(). -
The code is shorter and does not grow linearly as the number of collector types increases.
-
- Handling collectors explicitly by type
-
You work directly with collector objects and their
typevalues, such asTEXT,PASSWORD, andSINGLE_SELECT.This approach gives you direct control over the values sent to the server. Build a
collectorsarray and pass it tonext({ collectors }).Other benefits:
-
There is no normalization layer to learn. The code can use the collector types returned by the flow.
-
You can handle a collector type or integration that your form renderer does not support.
-
You can choose exactly which collector values to submit for the current node.
-
Normalizing collectors into form fields
To help you handle the required input, you can use the useDaVinciForm hook.
This hook normalizes the collectors into a flat list of typed fields and builds the next() input payload for you.
import { useEffect } from 'react';
import { ActivityIndicator, Button, Switch, Text, TextInput, View } from 'react-native';
import { useDaVinci, useDaVinciForm } from '@ping-identity/rn-davinci';
function LoginScreen() {
const { node, next, start, logoutUser, loading } = useDaVinci(davinciClient);
const form = useDaVinciForm(node, { next });
useEffect(() => {
start().catch(() => {}); // error surfaced via hook state
}, [start]);
if (loading) return <ActivityIndicator />;
if (node?.type === 'ContinueNode') {
return (
<View>
{form.fields
.filter(field => field.requiresUserInput)
.map(field => {
if (field.kind === 'boolean') {
return (
<Switch
key={field.key}
value={Boolean(form.values[field.key] ?? false)}
onValueChange={value => form.setValue(field.key, value)}
/>
);
}
if (field.kind === 'singleSelect' && field.options) {
return (
<View key={field.key}>
{field.options.map(option => (
<Button
key={option.value}
title={option.label}
onPress={() => form.setValue(field.key, option.value)}
/>
))}
</View>
);
}
return (
<TextInput
key={field.key}
placeholder={field.label}
secureTextEntry={field.kind === 'password'}
value={String(form.values[field.key] ?? '')}
onChangeText={text => form.setValue(field.key, text)}
/>
);
})}
<Button
title="Continue"
disabled={!form.canSubmit}
onPress={() => next(form.input)}
/>
</View>
);
}
if (node?.type === 'SuccessNode') {
return (
<View>
<Text>Authentication succeeded.</Text>
<Button title="Sign out" onPress={() => logoutUser()} />
</View>
);
}
if (node?.type === 'ErrorNode') {
return (
<View>
<Text>{node.message}</Text>
<Button title="Try again" onPress={() => start()} />
</View>
);
}
if (node?.type === 'FailureNode') {
return (
<View>
<Text>Something went wrong. Please try again.</Text>
<Button title="Retry" onPress={() => start()} />
</View>
);
}
return null;
}
The useDaVinciForm hook returns form.fields, which is a list of normalized collector types, each having the following properties:
| Property | Type | Description |
|---|---|---|
|
|
The collector key used to build the input passed to |
|
|
The collector type returned by the server, such as |
|
|
The high-level input kind. Examples include |
|
|
How the collector should be handled. One of:
|
|
|
Is |
|
varies |
The pre-populated value from the server, if any. |
|
varies |
The available choices for a selection collector, if provided. |
Checking the composition of the form
The useDaVinciForm hook provides the form.meta property. This provides flags that describe the composition of the current step, which you can use to conditionally render UI:
const form = useDaVinciForm(node);
// Only render a submit button when there are manual fields
if (form.meta.hasManual) {
// Show input fields and a submit button
}
// Inform the user an integration is needed
if (form.meta.hasIntegrationRequired) {
// Show an informational message
}
| Flag | Description |
|---|---|
|
|
|
|
|
Always |
|
|
|
|
Checking if the required data is complete
Use the form.canSubmit boolean to check that the required data has been handled appropriately.
If the boolean is false, check form.issues to identify the reasons.
| Issue code | When it is raised |
|---|---|
|
The current |
|
A required manual collector does not have a value. |
|
A collector requires an external integration before submission. |
|
The step contains a collector type that the form helper does not support. |
|
The |
Handling collectors explicitly by type
If you prefer, handle each collector type explicitly and build the input for next() yourself.
Set the values in a collectors array and pass it to next({ collectors }):
import { useEffect, useState } from 'react';
import { ActivityIndicator, Button, Text, TextInput, View } from 'react-native';
import { useDaVinci } from '@ping-identity/rn-davinci';
function LoginScreen() {
const { node, next, start, loading } = useDaVinci(davinciClient);
const [values, setValues] = useState<Record<string, string>>({});
useEffect(() => {
start().catch(() => {}); // error surfaced via hook state
}, [start]);
if (loading) return <ActivityIndicator />;
if (node?.type === 'ContinueNode') {
const collectors = node.collectors ?? [];
return (
<View>
{collectors.map(collector => {
if (collector.type === 'TEXT') {
return (
<TextInput
key={collector.key}
placeholder={collector.label}
onChangeText={value => {
setValues(previous => ({ …previous, [collector.key]: value }));
}}
/>
);
}
if (collector.type === 'PASSWORD') {
return (
<TextInput
key={collector.key}
placeholder={collector.label}
secureTextEntry
onChangeText={value => {
setValues(previous => ({ …previous, [collector.key]: value }));
}}
/>
);
}
return null;
})}
<Button
title="Continue"
onPress={() =>
next({
collectors: collectors.map(collector => ({
key: collector.key,
value: values[collector.key] ?? '',
})),
})
}
/>
</View>
);
}
if (node?.type === 'SuccessNode') {
return <Text>Authentication succeeded.</Text>;
}
if (node?.type === 'ErrorNode') {
return <Text>{node.message}</Text>;
}
if (node?.type === 'FailureNode') {
return <Button title="Retry" onPress={() => start()} />;
}
return null;
}
Do not rely on mutating the returned node or collector objects to update the native DaVinci state.
Use the next() action with a collectors array to submit the values explicitly.
Handling unsupported fields
If the underlying native SDK cannot create a collector from the server response, the bridge exposes it through ContinueNode.unsupportedFields.
if (node?.type === 'ContinueNode' && node.unsupportedFields?.length) {
console.warn('Unsupported fields:', node.unsupportedFields);
}
|
Each unsupported field provides a |
Handling FailureNode and ErrorNode
The DaVinci module distinguishes between FailureNode and ErrorNode for different categories of errors encountered during the flow.
FailureNode-
Indicates an unexpected issue that prevents the flow from continuing. This can stem from network problems, timeouts, or server errors.
You can access the underlying failure details using
node.causewhen it is available.Offer the user a way to restart the flow and log the details for support investigation.
ErrorNode-
Indicates that the server rejected submitted data, such as invalid user input or a password that does not meet the configured requirements.
You can retrieve the specific message using
node.messageand inspect the raw response usingnode.inputwhen it is available.Display the error message and offer the user a way to restart the flow. The hook accepts
next()only for an activeContinueNode.
import { useDaVinci } from '@ping-identity/rn-davinci';
function LoginScreen() {
const { node, start } = useDaVinci(davinciClient);
if (node?.type === 'FailureNode') {
console.error('Flow failed:', node.cause);
return <Button title="Retry" onPress={() => start()} />;
}
if (node?.type === 'ErrorNode') {
console.error('Flow error:', node.message);
console.error('Server response:', JSON.stringify(node.input));
}
}
For errors that occur while submitting collector values or advancing the flow, see Error handling.
Handling SuccessNode
When the flow reaches SuccessNode, authentication has completed successfully.
Use user() to retrieve the active OIDC session through the client or the actions returned by useDaVinci.
import { useEffect } from 'react';
import { useDaVinci } from '@ping-identity/rn-davinci';
function LoginScreen() {
const { node, user } = useDaVinci(davinciClient);
useEffect(() => {
if (node?.type !== 'SuccessNode') return;
async function loadSession() {
const session = await user();
if (session) {
console.log('Access token:', session.accessToken);
console.log('Refresh token:', session.refreshToken);
console.log('Expires in:', session.expiresIn);
}
}
void loadSession();
}, [node, user]);
}
The user() method returns null if no active session is available.
See Managing sessions and tokens in React Native for how to retrieve the user session and manage tokens.
Handling specific collectors
Some collectors need handling beyond ordinary text, password, and selection fields. They map to different execution modes and may display information, trigger a flow branch, or report progress while the user completes an action elsewhere.
Use these pages for the collector-specific behavior:
-
Boolean collectors use manual Boolean input and can enforce a required confirmation.
-
Read-only text collectors display flow content without contributing a submission value.
-
Flow collectors trigger an immediate flow action and bypass other form values.
-
Polling collectors report asynchronous progress and terminal statuses.
-
QR code collectors display image data for an out-of-band step without submitting the image.