Orchestration SDKs

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:

The node types returned during a DaVinci flow
Node type Description

ContinueNode

Indicates a step in the middle of the flow that requires input.

The node contains a list of collectors. Call next(input) to submit the collected values and advance to the next step.

SuccessNode

Indicates successful authentication.

See Managing sessions and tokens in React Native for post-authentication operations.

FailureNode

Represents an unexpected failure, such as a network error, timeout, or server error.

Restart the flow when the app receives this node type.

ErrorNode

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.

Handling different node types in a DaVinci flow
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 useDaVinciForm hook 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.canSubmit and form.issues tell you whether the step is ready to submit and why not.

  • Default values are pre-seeded. The hook hydrates form.values from collector defaults automatically.

  • form.meta flags describe mixed steps, including manual input, output-only content, integrations, and unsupported collectors.

  • The submit payload is built for you. form.input is ready to pass to next().

  • 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 type values, such as TEXT, PASSWORD, and SINGLE_SELECT.

This approach gives you direct control over the values sent to the server. Build a collectors array and pass it to next({ 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.

Collecting user input with useDaVinciForm
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:

DaVinci normalized collector properties
Property Type Description

key

string

The collector key used to build the input passed to next().

type

string

The collector type returned by the server, such as TEXT or PASSWORD.

kind

string

The high-level input kind. Examples include text, password, boolean, singleSelect, multiSelect, phone, device, output, polling, qrCode, flow, and integration.

executionMode

string

How the collector should be handled. One of:

manual

The collector requires a value from the app before the flow can continue.

immediate

The collector advances the flow immediately, without waiting for other form fields. This includes SUBMIT_BUTTON, ACTION, FLOW_BUTTON, and FLOW_LINK collectors.

output_only

The collector displays information but does not require a value from the user. This includes LABEL, READ_ONLY_TEXT, POLLING, and QR_CODE collectors.

integration_required

The collector requires a separate integration before the flow can continue. Integration packages register the collector types they own when your app initializes them, such as the external IdP and PingOne Protect packages.

unsupported

The collector type is not handled by the React Native bridge and no initialized integration package has registered it.

requiresUserInput

boolean

Is true when the collector needs direct user input.

defaultValue

varies

The pre-populated value from the server, if any.

options

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:

Using form metadata
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
}
All form.meta flags
Flag Description

hasManual

true if at least one collector requires direct user input.

hasOutputOnly

true if at least one collector is display-only.

hasAutoCapable

Always false for the current React Native DaVinci form helper.

hasIntegrationRequired

true if at least one collector requires an external integration.

hasUnsupported

true if the step contains a collector type the SDK does not support.

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.

Possible reasons preventing form submission
Issue code When it is raised

NO_ACTIVE_CONTINUE_NODE

The current node is not an active ContinueNode.

REQUIRED_VALUE_MISSING

A required manual collector does not have a value.

INTEGRATION_REQUIRED

A collector requires an external integration before submission.

UNSUPPORTED_COLLECTOR

The step contains a collector type that the form helper does not support.

The useDaVinciForm hook is headless. It manages normalized collectors and submit planning, but does not render UI by itself and does not automatically advance ordinary collectors.

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 }):

Handling collectors explicitly
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.

Checking for unsupported fields
if (node?.type === 'ContinueNode' && node.unsupportedFields?.length) {
  console.warn('Unsupported fields:', node.unsupportedFields);
}

Each unsupported field provides a key and type, allowing your UI to show a placeholder or prevent submission.

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.cause when 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.message and inspect the raw response using node.input when it is available.

Display the error message and offer the user a way to restart the flow. The hook accepts next() only for an active ContinueNode.

Handling error and failure node types in a DaVinci flow
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.

Handle a successful DaVinci flow
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:

Disposing an incomplete flow

If a user leaves a screen while a flow is in progress, call dispose() to release the native DaVinci instance:

Disposing an incomplete flow
await davinciClient.dispose();

Calling dispose() releases the in-flight flow. It does not sign the user out or revoke tokens.