---
title: User profile self-service in React Native apps
description: Learn how to add user profile self-service to your React Native apps, allowing users to manage their own profile information.
component: orchsdks
page_id: orchsdks:journey:use-cases/user-self-service/react-native-user-self-service
canonical_url: https://developer.pingidentity.com/orchsdks/journey/use-cases/user-self-service/react-native-user-self-service.html
llms_txt: https://developer.pingidentity.com/orchsdks/llms.txt
docs_for_agents: https://developer.pingidentity.com/build-with-ai/docs-for-agents.md
revdate: Fri, 05 Jun 2026 16:09:20 +0100
keywords: ["PingOne Advanced Identity Cloud", "PingAM", "Journeys", "React Native", "User Profile", "Self-Service", "Profile Management", "SDK"]
section_ids:
  step_1_installing_modules: Step 1. Installing modules
  step_2_retrieving_user_session_state: Step 2. Retrieving user session state
  journeyusersession_properties: JourneyUserSession properties
  refreshing_the_session_on_screen_focus: Refreshing the session on screen focus
  step_3_retrieving_userinfo: Step 3. Retrieving userinfo
  step_4_sso_token_management: Step 4. SSO token management
  step_5_starting_additional_journeys: Step 5. Starting additional journeys
  step_6_handling_attribute_callbacks: Step 6. Handling attribute callbacks
  common_callback_types: Common callback types
  reading_field_values_and_metadata: Reading field values and metadata
  rendering_attribute_fields: Rendering attribute fields
  submitting_profile_changes: Submitting profile changes
  kba_fields: KBA fields
  complete_profile_management_example: Complete profile management example
---

# User profile self-service in React Native apps

[icon: circle-check, set=far]PingOne Advanced Identity Cloud [icon: circle-check, set=far]PingAM [icon: react, set=fab]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

```shell
yarn add @ping-identity/rn-journey
yarn add @ping-identity/rn-oidc
```

Installing modules with npm

```shell
npm install @ping-identity/rn-journey
npm install @ping-identity/rn-oidc
```

After installation, import the hooks you need:

Importing journey and OIDC hooks

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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>
  );
}
```
