Orchestration SDKs

User profile self-service in React Native apps

PingOne Advanced Identity Cloud PingAM React Native

User profile self-service covers two related use cases: reading and displaying the authenticated user’s session data and attributes, and running additional authentication journeys (such as profile update or password change) on behalf of an already-authenticated user. The React Native SDK handles both through the @ping-identity/rn-journey and @ping-identity/rn-oidc packages.

Step 1. Installing modules

To install the module into your React Native project, use yarn or npm as follows:

  • yarn

  • npm

Installing modules with yarn
yarn add @ping-identity/rn-journey
yarn add @ping-identity/rn-oidc
Installing modules with npm
npm install @ping-identity/rn-journey
npm install @ping-identity/rn-oidc

After installation, import the hooks you need:

Importing journey and OIDC hooks
import { useJourney, JourneyProvider } from '@ping-identity/rn-journey';
import { useOidc, OidcProvider } from '@ping-identity/rn-oidc';

Step 2. Retrieving user session state

After a user authenticates through a Journey, the journey client retains the active session. Call actions.user() to retrieve the current JourneyUserSession object, which contains the session accessToken, refreshToken, expiresIn and related metadata. Call actions.ssoToken() to return the SSO token object, which contains the SSO token value, successUrl, and realm:

Retrieving session and SSO token state
import { useJourney } from '@ping-identity/rn-journey';

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

  useEffect(() => {
    async function loadSession() {
      const [session, ssoToken] = await Promise.all([
        actions.user(),
        actions.ssoToken(),
      ]);

      if (session) {
        console.log('Access token:', session.accessToken);
        console.log('Refresh token:', session.refreshToken);
        console.log('Expires in:', session.expiresIn);
      } else {
        console.log('No OIDC session found.');
      }

      if (ssoToken) {
        console.log('SSO token value:', ssoToken.value);
        console.log('Realm:', ssoToken.realm);
      } else {
        console.log('No SSO session found.');
      }
    }
    void loadSession();
  }, [actions]);
}

actions.user() returns null when no authenticated session is available. Always check for null before accessing session properties. actions.ssoToken() executes successfully without OIDC.

JourneyUserSession properties

Property Type Description

accessToken

string

OAuth 2.0 access token issued by the server.

refreshToken

string | undefined

OAuth 2.0 refresh token, when issued.

expiresIn

number | undefined

Token lifetime in seconds from issuance.

userInfo

object | undefined

User profile claims from OIDC userinfo, when resolved.

Refreshing the session on screen focus

User profile screens should refresh session state each time the screen comes into focus to ensure the displayed data is current. Use useFocusEffect from React Navigation:

Refreshing session state on screen focus
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';
import { useJourney } from '@ping-identity/rn-journey';

function UserProfileScreen() {
  const [, actions] = useJourney();
  const [session, setSession] = useState(null);

  useFocusEffect(
    useCallback(() => {
      void actions.user().then(setSession).catch(console.error);
    }, [actions]),
  );
}

Step 3. Retrieving userinfo

Call actions.userinfo() to fetch the OpenID Connect userinfo endpoint and return the user’s profile attributes. The result is a plain object whose keys depend on the scopes granted during authentication:

Fetching userinfo via useJourney
const [, actions] = useJourney();

const userInfo = await actions.userinfo();
// { sub: 'user123', given_name: 'Jane', family_name: 'Doe', email: 'jane@example.com', ... }

For OIDC sessions managed through @ping-identity/rn-oidc, use oidcActions.userinfo() instead:

Fetching userinfo via useOidc
const [, oidcActions] = useOidc();

const userInfo = await oidcActions.userinfo();

The oidcActions.userinfo() function accepts a cache boolean parameter. The default, false, initiates a fresh network request rather than serving cached values:

Requesting fresh or cached userinfo
// Default: fresh network request
const userInfo = await oidcActions.userinfo();

// Serve from cache when available
const cachedUserInfo = await oidcActions.userinfo(true);

Step 4. SSO token management

Call actions.ssoToken() to retrieve the current AM SSO token. This is useful when constructing requests to protected AM APIs (for example, the device client self-service APIs):

Retrieving the current SSO token
const ssoToken = await actions.ssoToken();
if (ssoToken) {
  console.log('SSO token value:', ssoToken.value);
  console.log('Realm:', ssoToken.realm);
}

Step 5. Starting additional journeys

You can run additional authentication journeys on behalf of an already-authenticated user. The SDK automatically attaches the existing SSO session to outgoing calls, so the journey server receives the user’s context without requiring re-authentication.

Common additional journeys include:

  • Profile attribute updates (for example, sdkProfileManagement)

  • Password change

  • KBA setup

  • Terms and conditions acceptance

  • MFA registration

Start an additional journey by calling actions.start() with the journey name. Pass forceAuth: true to ensure the journey runs even if a valid session already exists, and noSession: true to prevent the journey from issuing a replacement session token:

Starting an additional journey for an authenticated user
import { useJourney } from '@ping-identity/rn-journey';

const [node, actions] = useJourney(journeyClient);

// Start the profile management journey
await actions.start('sdkProfileManagement', {
  forceAuth: true,
  noSession: true,
});
Option Type Description

forceAuth

boolean

true to force traversal of an authentication journey, even if the user already has a valid session. Default: false.

noSession

boolean

true to prevent the journey from issuing a new session token upon successful completion. Use this for profile-management journeys to avoid replacing the user’s existing login session. Default: false.

Always set noSession: true for profile management journeys. Without it, the server replaces the user’s current session token with a new one from the profile journey, resetting all time-to-live values and potentially invalidating existing tokens in other parts of your app.

Step 6. Handling attribute callbacks

Profile management journeys typically return attribute collection callbacks. The useJourneyForm hook normalizes these callbacks into typed fields that your UI can render.

Common callback types

Callback type Field ref.type Description

StringAttributeInputCallback

StringAttributeInputCallback

Collects a string attribute such as a city name, telephone number, or email address.

NumberAttributeInputCallback

NumberAttributeInputCallback

Collects a numeric attribute such as age or an account number.

BooleanAttributeInputCallback

BooleanAttributeInputCallback

Collects a boolean attribute, typically presented as a toggle or checkbox.

ChoiceCallback

ChoiceCallback

Provides a list of choices and collects the user’s selection by index.

ConfirmationCallback

ConfirmationCallback

Presents a message with confirmation options (for example, OK / Cancel).

KbaCreateCallback

KbaCreateCallback

Collects a knowledge-based authentication question and answer pair.

TermsAndConditionsCallback

TermsAndConditionsCallback

Displays the current terms and conditions and collects the user’s agreement.

Reading field values and metadata

All attribute callbacks have executionMode: 'manual'. The prompt property provides a user-facing label; the defaultValue property contains the current attribute value (pre-populated from the user’s profile):

Reading field values and metadata from attribute callbacks
import { useJourneyForm } from '@ping-identity/rn-journey';

const form = useJourneyForm(node);

for (const field of form.fields) {
  console.log('Field ID:', field.id);
  console.log('Callback type:', field.ref.type);
  console.log('Prompt:', field.prompt);         // e.g. "First Name"
  console.log('Current value:', field.defaultValue);
  console.log('Required:', field.required);
  console.log('Kind:', field.kind);             // 'text', 'number', 'boolean', 'choice', 'kba'
}

Rendering attribute fields

Use field.ref.type to determine the appropriate UI control for each field:

Rendering attribute fields by callback type
import React from 'react';
import { Switch, Text, TextInput, View } from 'react-native';
import { useJourneyForm } from '@ping-identity/rn-journey';

function ProfileForm({ node, onSubmit }) {
  const form = useJourneyForm(node);

  return (
    <View>
      {form.fields.map((field) => {
        // Skip non-manual fields
        if (field.executionMode !== 'manual' || !field.requiresUserInput) {
          return null;
        }

        const value = form.values[field.id] ?? field.defaultValue;

        switch (field.ref.type) {
          case 'StringAttributeInputCallback':
            return (
              <View key={field.id}>
                <Text>{field.prompt}</Text>
                <TextInput
                  value={String(value ?? '')}
                  onChangeText={(text) => form.setValue(field.id, text)}
                  placeholder={field.prompt}
                />
              </View>
            );

          case 'NumberAttributeInputCallback':
            return (
              <View key={field.id}>
                <Text>{field.prompt}</Text>
                <TextInput
                  keyboardType="numeric"
                  value={value != null ? String(value) : ''}
                  onChangeText={(text) =>
                    form.setValue(field.id, text === '' ? '' : Number(text))
                  }
                />
              </View>
            );

          case 'BooleanAttributeInputCallback':
            return (
              <View key={field.id}>
                <Text>{field.prompt}</Text>
                <Switch
                  value={Boolean(value)}
                  onValueChange={(v) => form.setValue(field.id, v)}
                />
              </View>
            );

          case 'ChoiceCallback':
            return (
              <View key={field.id}>
                <Text>{field.prompt}</Text>
                {field.options?.map((option) => (
                  <Text
                    key={option.index}
                    onPress={() => form.setValue(field.id, option.index)}
                    style={value === option.index ? { fontWeight: 'bold' } : {}}
                  >
                    {option.label}
                  </Text>
                ))}
              </View>
            );

          case 'TermsAndConditionsCallback':
            return (
              <View key={field.id}>
                <Text>{field.message ?? field.prompt}</Text>
                <Switch
                  value={Boolean(value)}
                  onValueChange={(v) => form.setValue(field.id, v)}
                />
              </View>
            );

          default:
            return null;
        }
      })}

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

Submitting profile changes

After the user updates the fields, call actions.next() with form.input to submit the changes to the server:

Submitting profile changes to the server
const [node, actions] = useJourney(journeyClient);
const form = useJourneyForm(node);

async function handleSave() {
  if (!form.canSubmit) return;
  await actions.next(form.input);
}

KBA fields

KbaCreateCallback fields have field.kind === 'kba'. Their value is an object with three properties:

Setting a KBA field value
form.setValue(field.id, {
  selectedQuestion: 'What was the name of your first pet?',
  selectedAnswer: 'Fluffy',
  allowUserDefinedQuestions: false,
});

Pre-defined questions are available in field.options:

Reading pre-defined KBA questions from field options
const kbaField = form.getFieldByType('KbaCreateCallback');
const questions = kbaField?.options ?? [];
// [{ index: 0, value: 'What was...', label: 'What was...' }, ...]

Complete profile management example

The following component runs a profile management journey after initial authentication, handles attribute callbacks, and submits the changes:

Complete profile management screen component
import React, { useEffect } from 'react';
import {
  ActivityIndicator,
  Button,
  ScrollView,
  Switch,
  Text,
  TextInput,
  View,
} from 'react-native';
import { useJourney, useJourneyForm } from '@ping-identity/rn-journey';

function ProfileManagementScreen({ journeyClient }) {
  const [node, actions] = useJourney(journeyClient);
  const form = useJourneyForm(node);

  useEffect(() => {
    void actions.start('sdkProfileManagement', {
      forceAuth: true,
      noSession: true,
    });
  }, []);

      return () => {
      void actions.dispose();
    };
  }, []);

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

  if (node?.type === 'SuccessNode') {
    return <Text>Profile updated successfully.</Text>;
  }

  if (node?.type === 'FailureNode') {
    return <Text>{node.message ?? 'Profile update failed.'}</Text>;
  }

  if (!node) {
    return <Text>Starting profile journey...</Text>;
  }

  return (
    <ScrollView>
      {form.fields.map((field) => {
        if (field.executionMode !== 'manual' || !field.requiresUserInput) {
          return null;
        }
        const value = form.values[field.id] ?? field.defaultValue;

        if (field.kind === 'text') {
          return (
            <View key={field.id}>
              <Text>{field.prompt}</Text>
              <TextInput
                value={String(value ?? '')}
                onChangeText={(text) => form.setValue(field.id, text)}
              />
            </View>
          );
        }

        if (field.kind === 'number') {
          return (
            <View key={field.id}>
              <Text>{field.prompt}</Text>
              <TextInput
                keyboardType="numeric"
                value={value != null ? String(value) : ''}
                onChangeText={(text) =>
                  form.setValue(field.id, text === '' ? '' : Number(text))
                }
              />
            </View>
          );
        }

        if (field.kind === 'boolean') {
          return (
            <View key={field.id}>
              <Text>{field.prompt}</Text>
              <Switch
                value={Boolean(value)}
                onValueChange={(v) => form.setValue(field.id, v)}
              />
            </View>
          );
        }

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

        return null;
      })}

      <Button
        title="Save Profile"
        disabled={!form.canSubmit || actions.loading}
        onPress={() => actions.next(form.input)}
      />

      {actions.error ? (
        <Text style={{ color: 'red' }}>{actions.error.message}</Text>
      ) : null}
    </ScrollView>
  );
}