Step 5. Implementing the Push client for React Native
PingOne Advanced Identity Cloud PingAM React Native
This page guides you through implementing the Push client in your React Native application to support Push-based Multi-Factor Authentication (MFA).
It covers dependency setup, Push client initialization, credential management, handling different push notification types, and custom storage options.
Adding core dependencies
To add the Push module to your React Native project:
-
npm
-
yarn
npm install @ping-identity/rn-push @ping-identity/rn-core
yarn add @ping-identity/rn-push @ping-identity/rn-core
The Push module also requires the React Native Firebase packages, which handle FCM token delivery on Android and provide the permission-request API on iOS:
-
npm
-
yarn
npm install @react-native-firebase/app @react-native-firebase/messaging
yarn add @react-native-firebase/app @react-native-firebase/messaging
Because these packages include native iOS code, run pod install in the ios directory after installing them:
cd ios && pod install
Initializing the Push client
The Push module provides two patterns for initializing and managing the Push client: a React context provider for component-tree access, and a standalone factory function for imperative use.
Using the PushProvider and usePush hook
Wrapping your application with PushProvider makes the Push client available throughout your component tree by using the usePush hook. This is the recommended approach for most applications.
The optional config prop accepts the same options as createPushClient(). See Configuration options for the full list.
PushProviderimport { PushProvider } from '@ping-identity/rn-push';
function App() {
return (
<PushProvider config={{ timeoutMs: 20000, notificationCleanupConfig: { cleanupMode: 'HYBRID' } }}>
<YourNavigator />
</PushProvider>
);
}
import { usePush } from '@ping-identity/rn-push';
function PushScreen() {
const [data, { loading, error, refresh }] = usePush();
if (loading) return <ActivityIndicator />;
if (error) return <Text>{error.message}</Text>;
if (!data) return null;
const { client, credentials, deviceToken, pendingNotifications } = data;
// Render your push MFA UI here:
// - 'client' to approve/deny notifications
// - 'credentials' to list registered devices
// - 'pendingNotifications' to show outstanding requests
}
Using the factory function
For cases where you need direct control over the Push client lifecycle, create a client with createPushClient().
When you’re done with the client, call close() to clean up any temporary files and free memory.
import { createPushClient } from '@ping-identity/rn-push';
const pushClient = await createPushClient({
enableCredentialCache: false,
timeoutMs: 15000,
});
try {
// ... use pushClient
} finally {
// Close the client when done
await pushClient.close();
}
Configuration options
The properties you can use to customize the Push client configuration are as follows:
- enableCredentialCache
-
Whether to enable in-memory caching of credentials.
By default, this is set to
falsefor security reasons, as an attacker could potentially access cached credentials from memory dumps. - timeoutMs
-
The timeout for network operations, in milliseconds.
The default value is
15000ms (15 seconds). - storage
-
The storage configuration to use for Push credentials and notifications.
If not provided, the default platform storage is used (SQLite on Android, Keychain on iOS).
Learn more in Customizing Journey module storage in React Native.
- notificationCleanupConfig
-
Optional configuration for the automatic cleanup of push notifications.
Learn more in Cleaning up stored notifications.
- logger
-
The logger instance used for logging messages.
Defaults to the global logger instance.
Learn more in Configuring logging in React Native.
- ios
-
iOS-specific configuration options.
If not provided, default values are used for all iOS-specific settings.
Currently accepts
encryptionEnabled(boolean, defaults totrue).
Registering for push notifications and handling device tokens
Before the Push module can receive push notifications, your app must request permission from the operating system and forward device tokens to the Push client.
Requesting permission and obtaining the device token on Android
On Android, Firebase Cloud Messaging delivers a device token automatically after the app connects to Firebase. Use @react-native-firebase/messaging to obtain and forward the token:
import messaging from '@react-native-firebase/messaging';
// Request notification permission (Android 13+)
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
// Get the current FCM token and register it with the Push client
const token = await messaging().getToken();
await pushClient.setDeviceToken(token);
// Listen for token refresh events and update the Push client
const unsubscribe = messaging().onTokenRefresh(async (newToken) => {
await pushClient.setDeviceToken(newToken);
});
}
Requesting permission and obtaining the device token on iOS
On iOS, the APNs device token is registered automatically by the native SDK through the AppDelegate. Your app must request notification permission before APNs will deliver any tokens or notifications, but the token itself is forwarded to the Push client by the native layer, so no manual setDeviceToken() call is needed.
import messaging from '@react-native-firebase/messaging';
// Request notification permission (required before APNs will deliver notifications)
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (!enabled) {
// User denied permission; push notifications will not be delivered
showPermissionDeniedMessage();
}
The Push client’s onTokenRegistered subscription fires when the native layer has forwarded the APNs token, giving you a reliable signal that the client is ready to receive push notifications.
Subscribing to token registration events
The Push module also emits an event when the native layer has registered a device token.
Subscribe to this event to keep the Push client in sync:
useEffect(() => {
const unsubscribe = pushClient.onTokenRegistered((token) => {
// Token has been registered or refreshed
console.log('Token registered:', token);
});
return unsubscribe;
}, [pushClient]);
Updating device tokens
Under certain circumstances, the client operating system issues a new device token that your app needs to use for receiving push notifications.
What can cause the device token for push messages to change?
The device token used to receive push messages can change due to a number of circumstances:
- Uninstalling and reinstalling the client app
-
If the user uninstalls and then reinstalls the app, the OS regenerates the device token.
This is one of the most common reasons for a token change.
- Clearing app data
-
Clearing the application’s data by using the device settings causes the OS to issue a new device token upon next launch of the app.
- Revoking and regranting Push permission
-
The OS might issue a new device token if a user revokes and then re-enables push notifications.
- Push services expiring or invalidating tokens
-
The push services themselves, such as the Apple Push Notification service (APNs) or Google’s Firebase Cloud Messaging (FCM) service might invalidate device tokens for various reasons.
The OS issues a new device token upon next launch of the app if the push service invalidates the existing tokens.
- Updating the operating system
-
Occasionally, OS updates, especially major versions, might result in the push notification service issuing a new token.
Updating the OS can also clear app data, which would also mean the app requires a new device token on next launch.
- Updating or migrating apps
-
If you change the package or bundle IDs of your client app that uses push notification, or alter the signing keys, the OS might invalidate existing device tokens and issue a new one.
Similarly, if the user restores the app from backup, or migrates the app to a different device, the OS might issue a new device token, even if restoring the app to the same physical device.
The Push module provides methods for updating the device token associated with accounts it has registered to receive Push notifications. These methods also contact the server that registered the device to update its copy of the device token.
Failing to update the device token on both the client and the server will prevent push messages from arriving, which will cause authentication to fail.
|
Updating existing accounts with a new device token is only supported by the following server:
|
// Update the token globally across all registered credentials
await pushClient.setDeviceToken(newToken);
// Update the token for a specific credential only
await pushClient.setDeviceToken(newToken, credentialId);
Getting the current device token
Use the getDeviceToken() method to retrieve the device token currently stored by the Push client:
const token = await pushClient.getDeviceToken();
if (token) {
console.log('Current device token:', token);
} else {
// No token registered yet
}
Refreshing the device token on Android
On Android, you can use refreshToken() to explicitly fetch the current FCM registration token and register it with the Push client.
This is useful when automatic token delivery has not yet completed, for example when the app starts before the Firebase connection is established.
// Android only; on iOS this is a no-op (APNs token is delivered through AppDelegate)
const token = await pushClient.refreshToken();
if (token) {
console.log('Token refreshed:', token);
}
Registering a device using an authentication journey
Before a user can receive push notifications, their device must be registered with the server. Registration is typically done as part of an enrollment journey that contains the Push Registration node.
When the server reaches the Push Registration node, it returns a PollingWaitCallback. This callback carries the pushauth:// URI that the Push module needs to create the device credentials. The journey then pauses while the registration completes.
Your application needs to:
-
Extract the URI from the
PollingWaitCallbackmessage field. -
Call
addCredentialFromUri()with that URI to register the device. -
Pass the callbacks back unchanged to advance the journey past the registration step.
import { useJourney } from '@ping-identity/rn-journey';
import { usePush, PushError } from '@ping-identity/rn-push';
function PushEnrollmentScreen() {
const [node, actions] = useJourney();
const [pushData] = usePush();
useEffect(() => {
actions.start('MFAwithPush');
}, []);
useEffect(() => {
async function handlePushRegistrationNode() {
if (node?.type !== 'ContinueNode') return;
if (!pushData?.client) return;
const pollingCallback = node.callbacks?.find(
cb => cb.type === 'PollingWaitCallback',
);
if (!pollingCallback) return;
// The Push Registration node embeds the pushauth:// URI in the message field
const uri = pollingCallback.message;
if (!uri?.startsWith('pushauth://')) return;
try {
await pushData.client.addCredentialFromUri(uri);
// Device registered; advance the journey with an empty payload
await actions.next({ callbacks: node.callbacks ?? [] });
} catch (error) {
if (error instanceof PushError) {
if (error.code === 'duplicate_credential') {
// Device is already registered; still advance the journey
await actions.next({ callbacks: node.callbacks ?? [] });
} else {
showError(`Registration failed: ${error.message}`);
}
}
}
}
void handlePushRegistrationNode();
}, [node, pushData?.client]);
if (actions.loading) return <ActivityIndicator />;
if (node?.type === 'SuccessNode') {
return <Text>Device registered successfully.</Text>;
}
if (node?.type === 'ErrorNode') {
return <Text>{node.message}</Text>;
}
if (node?.type === 'FailureNode') {
return <Text>Something went wrong. Please try again.</Text>;
}
return null;
}
Managing Push credentials
The Push module stores a credential for each registered device and account pair. You can retrieve, update, or remove these credentials as needed.
For example, your app could display registered devices on an account settings screen, let users rename a credential, or allow them to remove a device they no longer use.
Getting Push credentials
You can get a list of all registered Push credentials or get an individual credential by passing its ID as a parameter.
-
All Push credentials
-
Specific Push credential
const credentials = await pushClient.getCredentials();
if (credentials.length === 0) {
showMessage('No credentials found');
} else {
credentials.forEach(credential => {
const displayName = credential.displayAccountName ?? credential.accountName;
const displayOrg = credential.displayIssuer ?? credential.issuer;
const logoUrl = credential.imageURL;
const bgColor = credential.backgroundColor;
const locked = credential.isLocked;
// Render each credential as a list item, e.g.:
// "Babs Jensen / Example.com" with the org logo and a locked badge if locked
renderCredentialRow({ displayName, displayOrg, logoUrl, bgColor, locked });
});
}
const credential = await pushClient.getCredential(credentialId);
if (credential !== null) {
const displayName = credential.displayAccountName ?? credential.accountName;
const displayOrg = credential.displayIssuer ?? credential.issuer;
const registeredAt = new Date(credential.createdAt).toLocaleDateString();
showCredentialDetail({ displayName, displayOrg, registeredAt });
} else {
showMessage('Credential not found');
}
Updating Push credentials
You can update the display properties of a stored credential with new values by using the saveCredential() method. Pass the updated credential object into the method as a parameter:
const updated = {
...credential,
displayAccountName: 'Babs Jensen',
displayIssuer: 'Example.com Checking Account',
};
await pushClient.saveCredential(updated);
Deleting Push credentials
Use the deleteCredential() method to remove individual credentials from the client device. Pass the credential ID into the method as a parameter:
const deleted = await pushClient.deleteCredential(credentialId);
if (deleted) {
showMessage('Credential removed');
} else {
showMessage('Credential not found');
}
Responding to push notifications
When your app receives a push notification from Firebase (Android) or APNs (iOS), you need to forward the message payload to the Push module for processing.
Processing incoming messages
import messaging from '@react-native-firebase/messaging';
// Handle background messages (Android and iOS)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
const notification = await pushClient.processNotification(remoteMessage.data);
if (notification) {
// Notification stored; user will respond when they open the app
}
});
// Handle foreground messages
useEffect(() => {
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
const notification = await pushClient.processNotification(remoteMessage.data);
if (notification) {
const message = notification.messageText ?? 'Authentication request received';
switch (notification.pushType) {
case 'default':
showDefaultNotification({ id: notification.id, message });
break;
case 'challenge':
showChallengeNotification({ id: notification.id, message });
break;
case 'biometric':
showBiometricPrompt({ id: notification.id, message });
break;
}
}
});
return unsubscribe;
}, [pushClient]);
You can also subscribe to the Push module’s notification event to react to notifications processed by other parts of your app:
useEffect(() => {
const unsubscribe = pushClient.onNotification((notification) => {
// Notification has been processed and stored
if (notification) showNotificationUI(notification);
});
return unsubscribe;
}, [pushClient]);
If your app receives push payloads as raw strings or JWTs rather than as dictionary objects, use processNotificationFromMessage() instead:
const notification = await pushClient.processNotificationFromMessage(rawMessage);
if (notification) {
// Handle the notification as usual
}
The Push module supports three push notification types depending on your use case and provides methods for handling each one.
Responding to tap to accept notifications
The Tap to accept notification type displays an Accept and a Reject button for the user to choose how to proceed.
This is the default notification type.
Call the approveNotification() method to approve the push notification, or denyNotification() to reject it:
// To approve
const approved = await pushClient.approveNotification(notificationId);
if (approved) {
showMessage('Authentication approved');
}
// To deny
const denied = await pushClient.denyNotification(notificationId);
if (denied) {
showMessage('Authentication denied');
}
Responding to display challenge code notifications
The Display Number Challenge notification type displays a number that the user must match on their device, either by entering the value or selecting it from multiple options.
Use the getNumbersChallenge() utility to parse the challenge values from the notification, then use approveChallengeNotification() to return the user’s selection to the server:
import { getNumbersChallenge } from '@ping-identity/rn-push';
// Parse the challenge numbers from the notification
const options = getNumbersChallenge(notification); // e.g., [12, 34, 56]
// Display options to the user and capture their selection
// ...
// Submit the selected number as a string
const challengeResponse = String(userSelectedOption); // e.g., "34"
const approved = await pushClient.approveChallengeNotification(notificationId, challengeResponse);
if (approved) {
showMessage('Authentication approved');
}
Responding to biometrics to accept notifications
The Use Biometrics to Accept notification type initially displays the same Accept and a Reject buttons for the user to choose how to proceed.
If the user selects to accept the notification, the client device presents its biometric options for the user to authenticate with.
Use the approveBiometricNotification() method to respond to the notification after the user successfully authenticates:
// After the user authenticates with biometrics
const authMethod = 'fingerprint'; // or 'face', 'iris', etc.
const approved = await pushClient.approveBiometricNotification(notificationId, authMethod);
if (approved) {
showMessage('Authentication approved');
}
Managing stored notifications
The Push module stores all notifications, including any that are still pending a response.
Getting pending notifications
You can get a list of pending notifications that have not yet been approved or rejected by using the getPendingNotifications() method:
const notifications = await pushClient.getPendingNotifications();
if (notifications.length > 0) {
notifications.forEach(notification => {
const message = notification.messageText ?? 'Authentication request received';
const type = notification.pushType; // 'default' | 'challenge' | 'biometric'
const receivedAt = new Date(notification.createdAt).toLocaleTimeString();
renderPendingRow({ id: notification.id, message, type, receivedAt });
});
} else {
showEmptyState();
}
You can use the getNotification() method to get an individual notification by passing its ID as a parameter:
const notification = await pushClient.getNotification(notificationId);
if (notification !== null) {
const message = notification.messageText ?? 'Authentication request received';
const type = notification.pushType;
const receivedAt = new Date(notification.createdAt).toLocaleTimeString();
showNotificationDetails({ message, type, receivedAt });
} else {
showMessage('Notification not found');
}
Getting all stored notifications
Use the getAllNotifications() method to get every notification stored on the device, including those that have already been responded to:
const notifications = await pushClient.getAllNotifications();
notifications.forEach(notification => {
const message = notification.messageText ?? 'Authentication request';
const type = notification.pushType;
const receivedAt = new Date(notification.createdAt).toLocaleDateString();
const respondedAt = notification.respondedAt
? new Date(notification.respondedAt).toLocaleDateString()
: null;
const status = notification.pending ? 'Pending' : (notification.approved ? 'Approved' : 'Denied');
renderHistoryRow({ message, type, receivedAt, respondedAt, status });
});
Cleaning up stored notifications
The Push module provides automatic cleanup of push notifications.
This prevents your app from accumulating too many push notification records, which improves performance and reduces storage usage.
You can customize notification cleanup by providing a notificationCleanupConfig when initializing the Push client:
const pushClient = await createPushClient({
notificationCleanupConfig: {
// Choose a cleanup mode: 'NONE', 'COUNT_BASED', 'AGE_BASED', or 'HYBRID'
cleanupMode: 'HYBRID',
// Maximum notifications to keep when using COUNT_BASED or HYBRID mode
maxStoredNotifications: 50,
// Maximum age in days when using AGE_BASED or HYBRID mode
maxNotificationAgeDays: 14,
},
});
The properties you can provide are as follows:
- cleanupMode
-
The strategy the module uses to clean up notifications.
Choose from one of the following:
'NONE'-
The module does not perform automatic cleanup of notifications.
'COUNT_BASED'-
The module keeps a maximum number of notifications and deletes the oldest first when the limit is exceeded.
This is the default.
'AGE_BASED'-
The module deletes notifications that are older than the specified number of days.
'HYBRID'-
The module applies both count and age limits to the stored notifications.
- maxStoredNotifications
-
The maximum number of notifications to keep, if using the
'COUNT_BASED'mode.If the number is exceeded, the module deletes the oldest notifications first to reach the threshold.
The default is
100. - maxNotificationAgeDays
-
The maximum number of days to keep a notification before removing it.
Defaults to
30.
You can also trigger notification cleanup on-demand by using the cleanupNotifications() method:
// Clean up notifications for all credentials
const removedCount = await pushClient.cleanupNotifications();
console.log(`Removed ${removedCount} old notifications`);
// Clean up notifications for a specific credential
const removedForCredential = await pushClient.cleanupNotifications(credentialId);
console.log(`Removed ${removedForCredential} old notifications for credential`);
Authenticating with push notifications in a journey
When the server needs to verify a user’s identity using push MFA, it sends a push notification to the user’s registered device and pauses the authentication journey at the Push Wait node.
The flow works as follows:
-
The server receives a sign-in request and starts the authentication journey.
-
The Push Sender node sends a push notification to the user’s device.
-
The journey pauses at the Push Wait node, which returns a
ConfirmationCallbackand aHiddenValueCallbackto your client. -
The user receives and responds to the push notification on their device.
-
The Push module sends the user’s decision directly to the server.
-
Your journey client polls the journey by calling
actions.next()until the server resolves the wait and returns the next node.
While the journey is paused at the Push Wait node, your application receives two callbacks from the server:
ConfirmationCallback-
Represents the decision the user is expected to make. The push notification itself collects the user’s decision, so no in-app interaction is needed for this callback.
HiddenValueCallback-
Carries internal state used by the server. Your client passes this back unchanged when advancing the journey.
Your application should poll the journey at a regular interval, calling actions.next() with the unchanged callbacks until the server advances the journey.
import { useJourney } from '@ping-identity/rn-journey';
import { useRef } from 'react';
function PushAuthScreen() {
const [node, actions] = useJourney();
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
actions.start('MFAwithPush');
}, []);
useEffect(() => {
if (node?.type !== 'ContinueNode') {
// Clear any active polling when the journey advances
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
return;
}
const confirmationCallback = node.callbacks?.find(
cb => cb.type === 'ConfirmationCallback',
);
const hiddenValueCallback = node.callbacks?.find(
cb => cb.type === 'HiddenValueCallback',
);
// Poll the Push Wait node until the server resolves the notification
if (confirmationCallback && hiddenValueCallback) {
const pollIntervalMs = 4000;
pollingRef.current = setInterval(async () => {
try {
// Pass the callbacks back unchanged; the Push module has already
// communicated the user’s decision to the server directly
await actions.next({ callbacks: node.callbacks ?? [] });
} catch {
// Journey may have already advanced; stop polling
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
}
}, pollIntervalMs);
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
};
}
}, [node]);
if (actions.loading) return <ActivityIndicator />;
if (node?.type === 'ContinueNode') {
const hasPushWait =
node.callbacks?.some(cb => cb.type === 'ConfirmationCallback') &&
node.callbacks?.some(cb => cb.type === 'HiddenValueCallback');
if (hasPushWait) {
return (
<View>
<Text>Check your device for a push notification.</Text>
<ActivityIndicator />
</View>
);
}
}
if (node?.type === 'SuccessNode') {
return <Text>Authentication successful.</Text>;
}
if (node?.type === 'ErrorNode') {
return <Text>{node.message}</Text>;
}
if (node?.type === 'FailureNode') {
return <Text>Something went wrong. Please try again.</Text>;
}
return null;
}
|
The user’s approval or denial of the notification is sent directly to the server by the Push module when the user responds to the notification. Your journey client does not need to set any value on the callbacks. It only needs to keep polling until the server resolves the Push Wait node. |
Handling errors
The Push module throws PushError instances with a code property that identifies the specific failure:
import { PushError } from '@ping-identity/rn-push';
try {
await pushClient.addCredentialFromUri(uri);
} catch (error) {
if (error instanceof PushError) {
switch (error.code) {
case 'invalid_uri':
showError('The registration URI is invalid');
break;
case 'duplicate_credential':
showError('This device is already registered');
break;
case 'network_failure':
showError('Check your network connection and try again');
break;
case 'credential_locked':
showError('This credential is locked by policy');
break;
default:
showError(`Push error: ${error.message}`);
}
}
}
The full set of error codes is as follows:
credential_locked-
The credential is locked by a policy.
credential_not_found-
No credential exists with the specified ID.
device_token_not_set-
No device token has been registered with the push service.
duplicate_credential-
A credential for this account is already registered.
initialization_failed-
The native layer failed to initialize.
invalid_parameter_value-
A parameter value is outside the expected range or format.
invalid_platform-
The platform string in the payload is unrecognized.
invalid_push_type-
The push type string in the payload is unrecognized.
invalid_uri-
The provided
pushauth://URI is malformed. message_parsing_failed-
The incoming push payload could not be parsed.
missing_required_parameter-
A required parameter was not provided.
network_failure-
A network request failed.
no_handler_for_platform-
No push handler is registered for the payload’s platform.
not_initialized-
The Push client has not been initialized.
notification_not_found-
No notification exists with the specified ID.
policy_violation-
A push action was blocked by a server policy.
registration_failed-
The server rejected the device registration.
storage_failure-
A credential or notification storage operation failed.