Orchestration SDKs

Resuming journeys using magic links in React Native

PingOne Advanced Identity Cloud PingAM React Native

You can configure your React Native app to capture the URI of the magic link the user visits. The key part of this deep-link URI is the suspendedId query parameter.

You can then use the Journey client’s resume() method, rather than start(), to continue the journey. You must pass in the URI that contains the suspendedId parameter.

Installing the SDK packages

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

  • yarn

  • npm

yarn add @ping-identity/rn-journey
npm install @ping-identity/rn-journey

Because this package includes native iOS code, run pod install in the ios directory after installing it:

cd ios && pod install

Configuring a custom URI scheme for deep linking

Magic links encode the suspendedId as a query parameter in the URI that opens your app.

You must register a custom URI scheme in the Android and iOS apps so that the operating system routes the link to your application.

Registering a URI scheme on Android

Add an <intent-filter> to the activity that handles deep links (typically MainActivity) in android/app/src/main/AndroidManifest.xml:

Registering a deep-link intent-filter in AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"
    ...>

    <!-- Standard launch intent-filter -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- Deep link intent-filter for magic links -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="myapp"
            android:host="example.com" />
    </intent-filter>

</activity>

Replace myapp and example.com with the scheme and host that match the Magic Link Custom URL configured on your Email Suspend Node.

Using android:launchMode="singleTask" ensures that clicking a magic link while the app is already running brings the existing instance to the foreground and delivers the URI through onNewIntent rather than creating a duplicate activity.

Registering a URI scheme on iOS

  1. In Xcode, open your project’s Info tab and add a new URL type under URL Types.

    • Identifier: Your bundle ID, for example com.example.myapp.

    • URL Schemes: The scheme portion of your deep link, for example myapp.

    Alternatively, add the entry directly to ios/<YourApp>/Info.plist:

    Registering a custom URI scheme in Info.plist
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>myapp</string>
            </array>
            <key>CFBundleURLName</key>
            <string>com.example.myapp</string>
        </dict>
    </array>
  2. Forward the URL to React Native’s Linking module in ios/<YourApp>/AppDelegate.swift:

Forwarding deep-link URLs to React Native in AppDelegate.swift
import React

// Inside AppDelegate class
func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
    return RCTLinkingManager.application(app, open: url, options: options)
}

// Handle universal links and cold-start URLs
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    return RCTLinkingManager.application(
        application,
        continue: userActivity,
        restorationHandler: restorationHandler
    )
}

Creating the Journey client

Creating the Journey client
import { createJourneyClient } from '@ping-identity/rn-journey';

const journeyClient = createJourneyClient({
  serverUrl: 'https://openam-forgerock-sdks.forgeblocks.com/am',
  realm: 'alpha',
  cookie: 'ch15fefc5407912',
  timeout: 30000,
});

The client instance is typically created once, at module scope or inside an initialization hook. The same client instance can handle both start() and resume() calls.

Detecting the suspended journey

When the journey reaches an Email Suspend node, the SDK receives a ContinueNode containing a SuspendedTextOutputCallback.

Use this to show a "check your email" message and wait for the magic link to be tapped:

Detecting a suspended journey node
import type { JourneyNode } from '@ping-identity/rn-journey';

function handleNode(node: JourneyNode) {
  if (node.type === 'ContinueNode') {
    const suspended = node.callbacks?.find(
      (cb) => cb.type === 'SuspendedTextOutputCallback',
    );
    if (suspended) {
      // Journey is paused — show "check your email" UI
      showMessage(suspended.message);
      return;
    }
    // Otherwise render the callback inputs as normal
  }
}

To handle the incoming URI and resume the journey, use React Native’s Linking API with actions.resume() from the useJourney hook.

The hook handles node state, loading, and error internally:

Handling a deep-link URL and resuming the journey
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
import { useJourney } from '@ping-identity/rn-journey';

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

  useEffect(() => {
    // App launched cold from the link — the URL won’t fire an event, so fetch it directly
    Linking.getInitialURL().then((url) => { if (url) actions.resume(url); });
    // App already running — the OS fires this event when the link is tapped
    const sub = Linking.addEventListener('url', ({ url }) => actions.resume(url));
    return () => sub.remove();
  }, []);

  // Render based on node ...
}

The resume() call accepts the complete URI string, including the suspendedId query parameter, and returns the next journey node, in the same manner as start() and next().

Handling errors

If resume() fails, for example, because the suspendedId has already been used or has expired, then useJourney sets actions.error to a JourneyError instance.

Read actions.error to surface the problem to the user:

Handling a resume error in the UI
export function LoginScreen() {
  const [node, actions] = useJourney(journeyClient);

  if (actions.error) {
    return (
      <View>
        <Text>{actions.error.message}</Text>
        <Button title="Start over" onPress={() => actions.start('Login')} />
      </View>
    );
  }

  // ...
}

A suspendedId is single-use. Once the journey is resumed successfully, the identifier is invalidated.

If the user visits the same link multiple times, actions.error is set with a JOURNEY_RESUME_ERROR code.