Push MFA for React Native — enrollment, credential management, notification processing, and approve/deny/challenge/biometric responses.
Note: This module requires that the
@ping-identity/rn-coreand@ping-identity/rn-storagemodules are already set up and installed.
# Install & setup the core module
yarn add @ping-identity/rn-core
# Install the rn-storage module
yarn add @ping-identity/rn-storage
# Install the rn-push module
yarn add @ping-identity/rn-push
# If you are developing your app using iOS, run this command
cd ios && pod install
Optional integration packages:
yarn add @ping-identity/rn-logger
FCM peer dependency (Android) — add to your app's build.gradle:
dependencies {
implementation platform('com.google.firebase:firebase-bom:33.0.0')
implementation 'com.google.firebase:firebase-messaging'
}
APNs setup (iOS) — enable Push Notifications capability in Xcode and add the APNs key in your Apple Developer portal.
The SDK needs to receive the platform push token and incoming messages from your existing push infrastructure. Choose the scenario that matches your app.
No native code changes needed. Wire up token and message delivery from your JS push library to the SDK. The examples below use @react-native-firebase/messaging — adapt to your library as needed.
Ping sends data-only messages (no notification field), which means background handlers fire correctly even when the app is killed.
index.js — background/quit handler, registered at the top level before AppRegistry:
import { createPushClient } from '@ping-identity/rn-push';
yourPushLibrary.setBackgroundMessageHandler(async (message) => {
const pushClient = await createPushClient();
const notification = await pushClient.processNotification(message.data);
if (notification) {
// Post a tray notification using your library's display API
}
});
App setup — token wiring, foreground handler, and cold-start tap:
import { PushProvider, usePush } from '@ping-identity/rn-push';
// Seed the token on startup and keep it fresh
const token = await yourPushLibrary.getToken();
await pushClient.setDeviceToken(token);
yourPushLibrary.onTokenRefresh((token) => {
void pushClient.setDeviceToken(token);
});
// Foreground messages
yourPushLibrary.onMessage(async (message) => {
// Use processNotification() if your library gives you a key-value data map.
// Use processNotificationFromMessage() if it gives you a raw string payload.
const notification = await pushClient.processNotification(message.data);
if (notification) {
/* show approve/deny UI */
}
});
// Cold-start tap: app was killed and user tapped the tray notification.
yourPushLibrary.getInitialNotification().then(async (message) => {
if (message) await pushClient.processNotification(message.data);
});
Then use PushProvider and usePush as normal — see How to use the SDK.
Android — add two lines to your existing FirebaseMessagingService:
override fun onNewToken(token: String) {
existingSdk.updateToken(token)
RNPingPushBridge.forwardToken(token) // ← add
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
existingSdk.handleMessage(remoteMessage)
RNPingPushBridge.forwardNotification(remoteMessage.data) // ← add
}
iOS — add to your existing AppDelegate. Set UNUserNotificationCenter.current().delegate = self in didFinishLaunchingWithOptions if not already set, then add:
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
existingSdk.register(deviceToken)
RNPingPushBridge.forwardToken(deviceToken) // ← add
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
existingSdk.handleNotification(userInfo)
RNPingPushBridge.forwardNotification(userInfo) // ← add
completionHandler(.newData)
}
// Show banner when foregrounded
func userNotificationCenter(_ center: UNUserNotificationCenter, // ← add if not present
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound, .badge])
}
// Forward banner taps to the Ping Push SDK
func userNotificationCenter(_ center: UNUserNotificationCenter, // ← add if not present
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
RNPingPushBridge.forwardNotification(userInfo) // ← add
completionHandler()
}
Android — tray notification text
If you want to post a tray notification when the app is backgrounded, use RNPingPushBridge.extractNotificationText to decode the message body from the JWT without reimplementing the logic yourself:
override fun onMessageReceived(remoteMessage: RemoteMessage) {
existingSdk.handleMessage(remoteMessage)
RNPingPushBridge.forwardNotification(remoteMessage.data)
if (!isAppInForeground()) {
val (title, body) = RNPingPushBridge.extractNotificationText(
remoteMessage.data,
getString(R.string.my_notification_title),
getString(R.string.my_notification_body),
)
// post your tray notification using title and body
}
}
usePush and PushProvider handle the full client lifecycle — creation, data fetching, token subscription, and cleanup.
Wrap your navigator once:
import { PushProvider } from '@ping-identity/rn-push';
export default function App() {
return (
<PushProvider config={{ timeoutMs: 20000 }}>
<NavigationContainer>{/* screens */}</NavigationContainer>
</PushProvider>
);
}
Use in any descendant screen:
import { usePush } from '@ping-identity/rn-push';
export default 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, pendingNotifications, allNotifications } = data;
// Enroll
await client.addCredentialFromUri(uri);
// Approve / deny
await client.approveNotification(notification.id);
await client.denyNotification(notification.id);
await client.approveChallengeNotification(notification.id, selectedNumber);
await refresh(); // re-fetch after any mutation
}
Listen for incoming notifications:
useEffect(() => {
if (!data) return;
return data.client.onNotification((notification) => {
if (notification) {
/* show approval UI */
}
});
}, [data]);
All methods return a Promise and reject with PushError on failure.
Credentials
| Method | Description |
|---|---|
addCredentialFromUri(uri) |
Enroll from a pushauth:// URI. Returns the new PushCredential. |
getCredentials() |
Return all enrolled PushCredential[]. |
getCredential(id) |
Return a single PushCredential by id. |
deleteCredential(id) |
Remove an enrolled account. |
Notifications
| Method | Description |
|---|---|
getPendingNotifications() |
Return PushNotification[] awaiting a response. |
getAllNotifications() |
Return the full notification history. |
getNotification(id) |
Return a single notification by id. |
approveNotification(id) |
Approve a default-type notification. |
denyNotification(id) |
Deny any notification. |
approveChallengeNotification(id, answer) |
Approve a challenge-type notification with the selected number. Use getNumbersChallenge(notification) to parse the options. |
approveBiometricNotification(id, method) |
Approve a biometric-type notification after the biometric prompt. |
cleanupNotifications(credentialId?) |
Run the configured cleanup strategy; optionally scoped to one credential. Returns the count removed. |
Token and lifecycle
| Method | Description |
|---|---|
getDeviceToken() |
Return the current FCM/APNs token, or null if not yet registered. |
refreshToken() |
(Android only) Request a new FCM token from the OS. |
onNotification(callback) |
Subscribe to incoming push messages. Returns an unsubscribe function. |
onTokenRegistered(callback) |
Subscribe to device token updates. Returns an unsubscribe function. |
close() |
Release native resources and remove all subscriptions. |
Helpers
| Function | Description |
|---|---|
getNumbersChallenge(notification) |
Parse the comma-separated numbersChallenge field into number[]. |
import { createPushClient } from '@ping-identity/rn-push';
import { logger } from '@ping-identity/rn-logger';
const client = await createPushClient({
logger: logger({ level: 'debug' }),
});
By default, push credentials and notifications are stored in a platform-default location. Pass a custom storage handle (created by configurePushStorage from @ping-identity/rn-storage) to isolate storage per app or configuration.
import { createPushClient } from '@ping-identity/rn-push';
import { configurePushStorage } from '@ping-identity/rn-storage';
const client = await createPushClient({
storage: configurePushStorage({
android: {
keyAlias: 'push_key',
fileName: 'push_db',
strongBoxPreferred: true,
},
ios: {
account: 'com.example.push',
encryptor: true,
},
}),
});
Tray notification text, channel name, colour, and icon are defined in your app's own resource
files — the SDK does not ship default strings or colors for these. See PushMessagingService in
the sample app for a working example.
All methods reject with a PushError instance, which extends PingError extends Error.
Use instanceof to narrow the type:
import { PushError } from '@ping-identity/rn-push';
try {
await client.addCredentialFromUri(uri);
} catch (err) {
if (err instanceof PushError) {
console.log(err.code, err.type, err.message);
}
}
| Code | Meaning |
|---|---|
'invalid_uri' |
Not a valid pushauth:// URI. |
'duplicate_credential' |
A credential with the same identifier already exists. |
'registration_failed' |
Device registration with the push service failed. |
'notification_not_found' |
Notification does not exist or has expired. |
'device_token_not_set' |
No device token registered. |
'network_failure' |
Network error communicating with the push service. |
'storage_failure' |
Credential or notification storage operation failed. |
'not_initialized' |
Client was not initialized before calling a method. |
See PushErrorCode in the package types for the full list.
MIT