@ping-identity/rn-journey
v1.1.0
Published
Ping Identity Journey for React Native
Downloads
3,202
Readme
Ping Identity React Native Journey
This module exposes native-backed Journey clients for Android and iOS.
Integrating the SDK into your project
Note: This module requires that the
@ping-identity/rn-coremodule is already set up and installed.
# Install & setup the core module
yarn add @ping-identity/rn-core
# Install the rn-journey module
yarn add @ping-identity/rn-journey
# If you are developing your app using iOS, run this command
cd ios && pod installOptional integration packages:
yarn add @ping-identity/rn-storage
yarn add @ping-identity/rn-loggerHow to Use the SDK
Step 1: Create a minimal client
Use this baseline configuration first.
import { createJourneyClient } from '@ping-identity/rn-journey';
const client = createJourneyClient({
serverUrl: 'https://example.com/am',
realm: 'alpha',
cookie: 'iPlanetDirectoryPro',
timeout: 30000,
});Step 2: Add OIDC module (with optional storage)
Add modules.oidc when your Journey flow needs OIDC token/session operations (for example
user(), refresh(), userinfo(), and revoke()).
Storage inside modules.oidc.storage and modules.session.storage is optional. Configure it only
if you need native-backed persistence; otherwise omit storage values.
Set modules.oidc.par to true to enable the Pushed Authorization Request flow. The native
SDK reads the PAR endpoint from the provider's OIDC discovery document. To override it, set
modules.oidc.openId.pushedAuthorizationRequestEndpoint.
Warning: If the provider's discovery document does not advertise a PAR endpoint and no override is set, the native SDK sends the authorization request to an empty URL instead of failing fast. Confirm PAR support in your provider's discovery document, or set
pushedAuthorizationRequestEndpointexplicitly, before enablingpar: true.
import { createJourneyClient } from '@ping-identity/rn-journey';
import {
CacheStrategy,
configureOidcStorage,
configureSessionStorage,
} from '@ping-identity/rn-storage';
const sessionStorage = configureSessionStorage({
android: {
fileName: 'journey-session',
keyAlias: 'journey-session',
strongBoxPreferred: true,
cacheStrategy: 'cache_on_failure',
},
ios: {
account: 'com.example.app.session',
encryptor: true,
cacheable: false,
},
});
const oidcStorage = configureOidcStorage({
android: {
fileName: 'journey-oidc',
keyAlias: 'journey-oidc',
strongBoxPreferred: true,
cacheStrategy: CacheStrategy.CACHE_ON_FAILURE,
},
ios: {
account: 'com.example.app.oidc',
encryptor: true,
cacheable: false,
},
});
const client = createJourneyClient({
serverUrl: 'https://example.com/am',
realm: 'alpha',
cookie: 'iPlanetDirectoryPro',
timeout: 30000,
modules: {
oidc: {
clientId: 'rn-client',
discoveryEndpoint:
'https://example.com/am/oauth2/alpha/.well-known/openid-configuration',
redirectUri: 'com.example.app://callback',
scopes: ['openid', 'profile', 'email'],
par: true,
storage: oidcStorage,
},
session: {
storage: sessionStorage,
},
},
});Pass optional integrations through config.modules. The JS API is createJourneyClient(config).
When provided, storage handles in modules.session.storage and modules.oidc.storage must come
from configureSessionStorage(...) / configureOidcStorage(...).
Step 3: Add logging integration (optional)
If you install the logger package, pass a JS logger instance created via
@ping-identity/rn-logger.
If the logger package is not installed/configured, do not pass logger values in Journey config.
import { createJourneyClient } from '@ping-identity/rn-journey';
import { logger } from '@ping-identity/rn-logger';
const jsLogger = logger({ level: 'debug' });
const client = createJourneyClient({
serverUrl: 'https://example.com/am',
logger: jsLogger,
modules: {
oidc: {
clientId: 'rn-client',
discoveryEndpoint:
'https://example.com/am/oauth2/alpha/.well-known/openid-configuration',
redirectUri: 'com.example.app://callback',
scopes: ['openid'],
},
},
});Drive the Journey imperatively
const firstNode = await client.start('Login');
const nextNode = await client.next({
callbacks: [
{ type: 'NameCallback', value: 'demo-user' },
{ type: 'PasswordCallback', value: 'demo-password' },
],
});
const resumedNode = await client.resume('com.example.app://callback?code=123');
const session = await client.user();
const refreshedSession = await client.refresh();
const userInfo = await client.userinfo();
const ssoToken = await client.ssoToken();
await client.revoke();
await client.logoutUser();
await client.dispose();useJourney does not auto-advance nodes. Progression policy is app-controlled via explicit next(...) calls.
Start options are supported when initiating a journey:
const node = await client.start('Login', {
forceAuth: true,
noSession: true,
});Handle node states explicitly in your UI flow:
const node = await client.start('Login');
switch (node.type) {
case 'ContinueNode':
await client.next({
callbacks: [{ type: 'NameCallback', value: 'demo-user' }],
});
break;
case 'ErrorNode':
console.log(node.message);
break;
case 'FailureNode':
console.log(node.cause ?? node.message);
break;
case 'SuccessNode':
console.log('Authenticated');
break;
}Approving a device authorization grant
When this device is acting as the approving device in an RFC 8628 device
authorization grant, pass the verification_uri_complete URL from the device
authorization response as the verificationUri start option of the
useJourney hook. After the Journey flow authenticates
the user, the native SDK extracts the user_code from that URL and approves
the requesting device.
Post Authentication Operations
After a Journey login succeeds, use the following operations to inspect and manage the active user session:
const userSession = await client.user();
const ssoToken = await client.ssoToken();
if (userSession) {
const refreshedSession = await client.refresh();
const userInfo = await client.userinfo();
await client.revoke();
}
await client.logoutUser();user()returns token/session payload (accessToken, optionalrefreshToken,expiresIn, optionaluserInfo).ssoToken()returns Journey SSO session payload (value,successUrl,realm) when available.refresh()refreshes token payload for the active user.userinfo()fetches user claims for the active user.revoke()revokes access/refresh tokens for the active user.logoutUser()signs out and clears the active Journey user session.
Use the React hook
import { useJourney } from '@ping-identity/rn-journey';
const [node, actions] = useJourney(client);
await actions.start('Login');
if (node?.type === 'ContinueNode') {
await actions.next({
callbacks: [{ type: 'NameCallback', value: 'demo-user' }],
});
}Start options are supported when initiating a journey:
const node = await actions.start('Login', {
forceAuth: true,
noSession: true,
});To approve an RFC 8628 device authorization grant, pass the verificationUri
start option (see Approving a device authorization grant):
await actions.start('Login', {
verificationUri: 'https://example.com/device?user_code=WDJB-MJHT',
});After the Journey flow authenticates the user, the native SDK extracts the
user_code from that URL and approves the requesting device automatically;
no extra submit step is required in the app.
Share Journey state across multiple screens (optional)
import { JourneyProvider, useJourney } from '@ping-identity/rn-journey';
function App(): React.ReactElement {
return (
<JourneyProvider client={client}>
<AuthNavigator />
</JourneyProvider>
);
}
function LoginScreen(): React.ReactElement {
const [node, actions] = useJourney();
return <></>;
}Render callbacks with useJourneyForm
import { callbackType } from '@ping-identity/rn-types';
import { useJourney, useJourneyForm } from '@ping-identity/rn-journey';
const [node, actions] = useJourney(client);
const form = useJourneyForm(node);
const usernameField = form.getFieldByType(callbackType.NameCallback);
const passwordField = form.getFieldByType(callbackType.PasswordCallback);
form.setValueByType(callbackType.NameCallback, 'demo-user');
form.setValueByType(callbackType.PasswordCallback, 'demo-password');
if (form.canSubmit) {
await actions.next(form.input);
}useJourneyForm is headless. It manages normalized fields and submit planning, but does not render UI and does not auto-run callbacks.
Each normalized field includes executionMode and requiresUserInput.
| executionMode | Meaning | requiresUserInput default |
| ---------------------- | ----------------------------------------------------------------- | --------------------------- |
| manual | Callback value is submitted from form/planned input. | true |
| auto_capable | Callback can be executed by app auto-progress policy. | false |
| integration_required | Callback family needs extra native/app integration before submit. | false |
| output_only | Display/system callback, no input value expected. | false |
| unsupported | Callback is not currently handled by helper submit logic. | false |
requiresUserInput exceptions:
| Callback Type | executionMode | requiresUserInput | Reason |
| --------------------- | --------------- | ------------------- | ---------------------------------------------------------------------------------------- |
| HiddenValueCallback | manual | false | Hidden payload should pass through submit planning without forcing a visible input step. |
TODO(test-runner app)(TODO-SEPARATE-TICKET): add Journey integration and E2E tests (including
SuspendedTextOutputCallbackdeep link/email resume flow) once the test-runner app is set up.
Core callback support
The following AM core callbacks are supported on Android and iOS:
| Callback Type | Description | Input Handling |
| --------------------------------- | ------------------------------------------------------------ | -------------- |
| BooleanAttributeInputCallback | Collects true or false input. | Manual input |
| ChoiceCallback | Collects a single selection from available choices. | Manual input |
| ConfirmationCallback | Collects one selected option from a list. | Manual input |
| ConsentMappingCallback | Prompts the user to consent to sharing profile data. | Manual input |
| HiddenValueCallback | Carries non-visual form values. | Manual input |
| KbaCreateCallback | Collects knowledge-based question and answer values. | Manual input |
| MetadataCallback | Injects key-value metadata into the flow. | Output-only |
| NameCallback | Collects a username. | Manual input |
| NumberAttributeInputCallback | Collects a numeric value. | Manual input |
| PasswordCallback | Collects a password or OTP value. | Manual input |
| PollingWaitCallback | Instructs the client to wait and resubmit later. | Output-only |
| StringAttributeInputCallback | Collects string attribute values. | Manual input |
| SuspendedTextOutputCallback | Pauses flow and resumes through external callback/deep link. | Output-only |
| TermsAndConditionsCallback | Collects acceptance of configured terms and conditions. | Manual input |
| TextInputCallback | Collects arbitrary text input. | Manual input |
| TextOutputCallback | Provides display-only message content. | Output-only |
| ValidatedCreatePasswordCallback | Collects password input with policy validation. | Manual input |
| ValidatedCreateUsernameCallback | Collects username input with policy validation. | Manual input |
Integration-dependent families (for example, device profile, FIDO, PingOne Protect, redirect/IdP, and ReCaptcha callbacks) are surfaced in node payloads and require client-side integration before submission (executionMode: 'integration_required').
For FIDO callbacks, run explicit Journey-scoped APIs from @ping-identity/rn-fido (registerForJourney(...) / authenticateForJourney(...)) and then call next(...).
Native parity contract
Journey FIDO callback handling is standardized across Android and iOS with the following native contract:
| Area | Standardized behavior |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Callback type resolution | Runtime native callback class is authoritative; raw callback JSON type is fallback only. |
| Callback execution ownership | FIDO callback execution is owned by @ping-identity/rn-fido Journey-scoped APIs, not next(...) callback mutations. |
| Hidden callback ownership | Journey/native callback execution remains the source of truth for callback state; helper layer does not force custom override values. |
Error handling
All promise rejections throw a JourneyError instance, which extends PingError extends Error.
Use instanceof to narrow the error type:
import { JourneyError } from '@ping-identity/rn-journey';
try {
await client.start('Login');
} catch (err) {
if (err instanceof JourneyError) {
console.log(err.code, err.type, err.message);
}
}Stable Journey error codes:
JOURNEY_CONFIG_ERRORJOURNEY_INIT_ERRORJOURNEY_START_ERRORJOURNEY_NEXT_ERRORJOURNEY_RESUME_ERRORJOURNEY_USER_ERRORJOURNEY_LOGOUT_ERRORJOURNEY_DISPOSE_ERRORJOURNEY_STATE_ERRORJOURNEY_CALLBACK_APPLY_ERRORJOURNEY_UNSUPPORTED_CALLBACK_ERRORJOURNEY_MISSING_INTEGRATION_ERROR
License
This project is licensed under the MIT License - see the LICENSE file for details
