Orchestration SDKs

Navigating an authentication journey in React Native

PingOne Advanced Identity Cloud PingAM React Native


Each step in an authentication journey, including the first, returns a JourneyNode object representing where you are in the journey.

The node has a type property that indicates the current state:

The node types returned during a journey
Node type Description

ContinueNode

Indicates a step in the middle of the authentication journey that requires input.

Call actions.next(input) to submit the collected payload and advance to the next step.

SuccessNode

Indicates successful authentication.

Access the user object by calling user().

FailureNode

Represents an unexpected failure, such as a network error, a parsing issue, or an internal SDK error.

Access the failure details via node.cause.

ErrorNode

Indicates a server-side validation or request error, such as invalid credentials or exceeding the maximum number of login attempts.

Access the error message via node.message.

To complete an authentication journey you must handle each node type in your client application.

Handling different node types in a journey
import { useJourney } from '@ping-identity/rn-journey';

function LoginScreen() {
  const [node, actions] = useJourney();

  useEffect(() => {
    actions.start('sdkUsernamePasswordJourney');
  }, []);

  if (actions.loading) {
    // Show a loading indicator while the native module is processing
  }

  if (node?.type === 'ContinueNode') {
    // Render input fields for node.callbacks
  }

  if (node?.type === 'SuccessNode') {
    // Authentication succeeded — navigate to the home screen
  }

  if (node?.type === 'FailureNode') {
    // Display node.cause to the user or a generic error message
  }

  if (node?.type === 'ErrorNode') {
    // Display node.message to the user
  }
}

Handling ContinueNode

For ContinueNode steps, the server sends a list of callbacks that require user input.

There are two ways of handling the gathering of the required input:

Normalising callbacks into form fields

The useJourneyForm hook maps all callbacks to a flat list of typed field.kind values, such as text, password, boolean, choice, and so on, so your UI is driven by what kind of input is needed, not which specific callback the server sent.

If the server journey is reconfigured to swap one callback type for another that maps to the same kind, the UI keeps working 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, so you don’t have to write your own required-field checks.

  • Default values are pre-seeded. The hook hydrates form.values from field.defaultValue automatically, so pre-populated server fields just appear.

  • form.meta flags handle any mixed steps. When a step contains a mix of manual fields, FIDO callbacks, and output-only labels, the hasManual, hasIntegrationRequired, and other flags let you conditionally render the right UI sections without examining each callback yourself.

  • Submit payload is built for you. form.input is the ready-to-send payload, so you never assemble the callback array manually.

  • Less boilerplate. The code is shorter and doesn’t grow linearly as the number of callback types increases.

Handling callbacks explicitly by type

You work directly with callback objects using the same callback.type names, such as NameCallback,PasswordCallback.

If you have worked with the native Orchestration SDK for Android or iOS previously you will find this pattern immediately recognisable.

Other benefits:

  • No abstraction to learn. There are no normalised kind values or executionMode concepts to understand — the code says exactly what it does.

  • Full control over the payload. You set callback.value directly on the callback object and pass { callbacks } to actions.next(). What you send is exactly what you set, with no intermediary building the payload.

  • Easier to handle unusual or custom callbacks. If a journey uses a non-standard callback that useJourneyForm does not support, explicit handling is the only way forward.

Normalising callbacks into form fields

To help you handle the required input you can use the useJourneyForm hook.

This hook normalises the callbacks into a flat list of typed fields, and builds the next() input payload for you.

Collecting user input with useJourneyForm
const [node, actions] = useJourney();
const form = useJourneyForm(node);

if (actions.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.id}
                value={Boolean(form.values[field.id] ?? false)}
                onValueChange={val ⇒ form.setValue(field.id, val)}
              />
            );
          }

          if (field.kind === 'choice' && field.options) {
            return (
              <View key={field.id}>
                {field.options.map(option => (
                  <Button
                    key={option.index}
                    title={option.label}
                    onPress={() => form.setValue(field.id, option.index)}
                  />
                ))}
              </View>
            );
          }

          return (
            <TextInput
              key={field.id}
              placeholder={field.prompt}
              secureTextEntry={field.kind === 'password'}
              value={String(form.values[field.id] ?? '')}
              onChangeText={text => form.setValue(field.id, text)}
            />
          );
        })}

      <Button
        title="Continue"
        disabled={!form.canSubmit}
        onPress={() => actions.next(form.input)}
      />
    </View>
  );
}

if (node?.type === 'SuccessNode') {
  return (
    <View>
      <Text>Authentication succeeded.</Text>
      <Button title="Sign out" onPress={() => actions.logoutUser()} />
    </View>
  );
}

if (node?.type === 'ErrorNode') {
  return (
    <View>
      <Text>{node.message}</Text>
      <Button title="Try again" onPress={() => actions.start('sdkUsernamePasswordJourney')} />
    </View>
  );
}

if (node?.type === 'FailureNode') {
  return (
    <View>
      <Text>Something went wrong. Please try again.</Text>
      <Button title="Retry" onPress={() => actions.start('sdkUsernamePasswordJourney')} />
    </View>
  );
}

return null;

The useJourneyForm hook returns form.fields, which is a list of JourneyNormalizedField types, each having the following properties:

JourneyNormalizedField properties
Property Type Description

id

string

A stable identifier for the field in the format CallbackType:index.

For example, NameCallback:0.

prompt

string

The label to display to the user.

kind

string

The input kind. One of:

  • text

  • password

  • number

  • boolean

  • choice

  • kba

  • output

  • unknown

executionMode

string

How the field should be handled. One of:

manual

A field to show to the user to obtain input.

auto_capable

A field that can be handled automatically without input from the user.

integration_required

A field that requires a separate integration, such as FIDO or social sign-on.

output_only

A read-only field such as a label or other text. No user input is required.

unsupported

A field that the Orchestration SDK for React Native does not recognize or support.

requiresUserInput

boolean

Is true if the field needs the user to provide a value before the form can be submitted.

required

boolean

Is true if the field must have a non-empty value.

defaultValue

varies

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

options

JourneyFieldOption[]

The available options to show the user when the kind property is choice or kba.

Each option has an index, label, and value property.

Checking the composition of the form

The useJourneyForm 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 = useJourneyForm(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 such as FIDO is needed
if (form.meta.hasIntegrationRequired) {
  // Show an informational message
}
All form.meta flags
Flag Description

hasManual

true if at least one field requires direct user input.

hasOutputOnly

true if at least one field is display-only (no input required).

hasAutoCapable

true if at least one field can be handled automatically by the client.

hasIntegrationRequired

true if at least one field requires an external integration (for example, FIDO or a social IdP).

hasUnsupported

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

hasRequiredConsentMissing

true if a consent callback is present and the user has not yet accepted.

Checking if the required data is complete

Use the form.canSubmit boolean to check that each callback has been handled appropriately. If the boolean is false, you can check form.issues to list the reasons.

Possible reasons that you could not submit the form include the following:

Reasons preventing form submission
Issue code When it’s raised

NO_ACTIVE_CONTINUE_NODE

Either node is null or not the ContinueNode type.

INTEGRATION_REQUIRED

A callback has an executionMode value of auto_capable or integration_required that needs to be completed.

Callbacks that require integration include those used for FIDO, social login, reCAPTCHA, and PingOne Protect.

UNSUPPORTED_CALLBACK

A callback type the Orchestration SDK doesn’t recognise.

These callbacks have an executionMode value of unsupported.

REQUIRED_CONSENT_MISSING

A callback marked as required is not marked as true.

For example, a TermsAndConditions, ConsentMapping, or BooleanAttributeInput callbacks marked as true: required.

INVALID_VALUE

Field validation has failed.

For example, a required field is empty, a number field has a non-finite value, a choice field has an out-of-range index, or a KBA field is missing either the question or answer.

The useJourneyForm hook is headless. It manages normalized fields and submit planning, but does not render UI by itself, and it does not auto-run callbacks.

Handling callbacks explicitly by type

If you prefer you can handle each callback type explicitly and work directly with node.callbacks.

You’ll need to build up the callback response yourself, for example setting the value the user enters into a form field, and return the complete data to the server by using actions.next({ callbacks });

Handling callbacks explicitly
const [node, actions] = useJourney();

if (actions.loading) return <ActivityIndicator />;

if (node?.type === 'ContinueNode') {
  const callbacks = node.callbacks ?? [];

  return (
    <View>
      {callbacks.map((callback, index) => {

        if (callback.type === 'NameCallback') {
          return (
            <TextInput
              key={index}
              placeholder={callback.prompt}
              onChangeText={text => {
                callback.value = text;
              }}
            />
          );
        }

        if (callback.type === 'PasswordCallback') {
          return (
            <TextInput
              key={index}
              placeholder={callback.prompt}
              secureTextEntry
              onChangeText={text => {
                callback.value = text;
              }}
            />
          );
        }

        return null;
      })}

      <Button
        title="Continue"
        onPress={() => actions.next({ callbacks })}
      />
    </View>
  );
}

if (node?.type === 'SuccessNode') {
  return (
    <View>
      <Text>Authentication succeeded.</Text>
      <Button title="Sign out" onPress={() => actions.logoutUser()} />
    </View>
  );
}

if (node?.type === 'ErrorNode') {
  return (
    <View>
      <Text>{node.message}</Text>
      <Button title="Try again" onPress={() => actions.start('sdkUsernamePasswordJourney')} />
    </View>
  );
}

if (node?.type === 'FailureNode') {
  return (
    <View>
      <Text>Something went wrong. Please try again.</Text>
      <Button title="Retry" onPress={() => actions.start('sdkUsernamePasswordJourney')} />
    </View>
  );
}

return null;

Handling FailureNode and ErrorNode

The Journey module distinguishes between the FailureNode and ErrorNode types for different categories of errors encountered during the authentication flow.

FailureNode

Indicates an unexpected issue that prevents the journey from continuing. This could stem from network problems or data parsing errors.

You can access the underlying error using node.cause 1.

You should display a user-friendly generic error message and log the details for support investigation.

ErrorNode

Signifies an error response from the authentication server, typically an HTTP 4xx or 5xx status code.

These errors often relate to invalid user input or issues server-side.

You can retrieve the specific error message provided by the server using node.message 2, and access the raw JSON response by using node.input 3.

Handling error and failure node types in a journey
import { useJourney } from '@ping-identity/rn-journey';

function LoginScreen() {
  const [node, actions] = useJourney();

  useEffect(() => {
    actions.start('sdkUsernamePasswordJourney');
  }, []);

  if (actions.loading) {
    // Show a loading indicator while the native module is processing
  }

  if (node?.type === 'ContinueNode') {
    // Render input fields for node.callbacks
  }

  if (node?.type === 'SuccessNode') {
    // Authentication succeeded — navigate to the home screen
  }

  if (node?.type === 'FailureNode') {
    // Display a user-friendly message to the user
    const cause = node.cause; (1)
    console.error('Authentication failed:', cause);
  }

  if (node?.type === 'ErrorNode') {
    // Display a user-friendly message to the user
    const message = node.message; (2)
    const rawInput = node.input; (3)
    console.error('Journey error:', message);
    console.error('Server response:', JSON.stringify(rawInput));
  }
}

Handling SuccessNode

When the client reaches the SuccessNode type it securely stores the session token. However, the SuccessNode does not carry token data inline.

Call actions.user() or actions.ssoToken() explicitly to retrieve session information. This keeps sensitive token data out of SDK-managed state until your app requests it.

import { useJourney } from '@ping-identity/rn-journey';

function LoginScreen() {
  const [node, actions] = useJourney();

  useEffect(() => {
    actions.start('sdkUsernamePasswordJourney');
  }, []);

  useEffect(() => {
    if (node?.type !== 'SuccessNode') return;

    async function fetchSession() {
      const user = await actions.user();
      const session = await actions.ssoToken();
    }

    void fetchSession();
  }, [node]);

  if (actions.loading) {
    // Show a loading indicator while the native module is processing
  }

  if (node?.type === 'ContinueNode') {
    // Render input fields for node.callbacks
  }

  if (node?.type === 'FailureNode') {
    // Display node.cause to the user or a generic error message
  }

  if (node?.type === 'ErrorNode') {
    // Display node.message to the user
  }
}

When the journey completes successfully, see Managing sessions and tokens in React Native for how to retrieve the user object and manage tokens.