Welcome to the REL-ID Additional Device Activation codelab! This tutorial builds upon the foundational MFA implementation to add sophisticated device onboarding capabilities using REL-ID Verify's push notification system.
In this codelab, you'll enhance your existing MFA application with:
By completing this codelab, you'll master:
addNewDeviceOptions
events and device activation flowsBefore starting this codelab, ensure you have:
The code to get started is stored in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-react-native.git
Navigate to the relid-MFA-additional-device-activation
folder in the repository you cloned earlier
This codelab extends your MFA application with three core device activation components:
addNewDeviceOptions
event processing and navigation coordinationBefore implementing device activation screens, let's understand the key SDK events and APIs that power the additional device activation workflow.
The device activation process follows this event-driven pattern:
User Completes MFA on Primary Device → SDK Detects New Device On Secondary Device → addNewDeviceOptions Event → VerifyAuthScreen →
Push Notifications Sent → User Approves the Notification On Primary Device → Continue MFA Flow -> Device Activated
Add these TypeScript definitions to understand device activation data structures:
// src/uniken/types/rdnaEvents.ts (device activation additions)
/**
* Device activation options data structure
* Triggered when SDK detects unregistered device during authentication
*/
export interface RDNAAddNewDeviceOptionsData {
userID: string;
newDeviceOptions: string[];
challengeInfo: RDNAChallengeInfo[];
}
/**
* RDNA Notification Body
* Localized content for notification
*/
export interface RDNANotificationBody {
lng: string;
subject: string;
message: string;
label: Record<string, string>;
}
/**
* RDNA Notification Action
* Available actions for notification
*/
export interface RDNANotificationAction {
label: string;
action: string;
authlevel: string;
}
/**
* RDNA Notification Item
* Individual notification structure from API response
*/
export interface RDNANotificationItem {
notification_uuid: string;
create_ts: string;
expiry_timestamp: string;
create_ts_epoch: number;
expiry_timestamp_epoch: number;
body: RDNANotificationBody[];
actions: RDNANotificationAction[];
action_performed: string;
ds_required: boolean;
}
/**
* RDNA Notification Response Data
* Response structure for notifications API
*/
export interface RDNANotificationResponseData {
notifications: RDNANotificationItem[];
start: string;
count: string;
total: string;
}
/**
* RDNA Get Notifications Data
* Unified notification response structure for onGetNotifications event
*/
export interface RDNAGetNotificationsData {
errCode?: number;
error?: RDNAError;
eMethId?: number;
userID?: string;
challengeMode?: number;
authenticationType?: number;
challengeResponse?: RDNAChallengeResponse;
pArgs?: {
service_details: any;
response: {
ResponseData: RDNANotificationResponseData;
ResponseDataLen: number;
StatusMsg: string;
StatusCode: number;
CredOpMode: number;
};
pxyDetails: {
isStarted: number;
isLocalhostOnly: number;
isAutoStarted: number;
isPrivacyEnabled: number;
portType: number;
port: number;
};
};
}
/**
* RDNA Update Notification Response Data
* Response data structure for notification update
*/
export interface RDNAUpdateNotificationResponseData {
status_code: number;
message: string;
notification_uuid: string;
is_ds_verified: boolean;
}
/**
* RDNA Update Notification Data
* Complete response structure for onUpdateNotification event
*/
export interface RDNAUpdateNotificationData {
errCode: number;
error: RDNAError;
eMethId: number;
pArgs: {
service_details: any;
response: {
ResponseData: RDNAUpdateNotificationResponseData;
ResponseDataLen: number;
StatusMsg: string;
StatusCode: number;
CredOpMode: number;
};
pxyDetails: {
isStarted: number;
isLocalhostOnly: number;
isAutoStarted: number;
isPrivacyEnabled: number;
portType: number;
port: number;
};
};
}
Define callback types for device activation events:
// Callback type definitions for device activation events
export type RDNAAddNewDeviceOptionsCallback = (data: RDNAAddNewDeviceOptionsData) => void;
export type RDNAGetNotificationsCallback = (data: RDNAGetNotificationsData) => void;
export type RDNAUpdateNotificationCallback = (data: RDNAUpdateNotificationData) => void;
The addNewDeviceOptions
event is the cornerstone of device activation:
REL-ID Verify enables secure device-to-device approval:
Enhance your existing RdnaService
with device activation APIs. These methods handle REL-ID Verify workflows and notification management.
Extend your RdnaService
class with these device activation methods:
// src/uniken/services/rdnaService.ts (device activation additions)
/**
* Performs REL-ID Verify authentication for device activation
* Sends push notifications to registered devices for approval
* @param verifyAuthStatus User's decision (true = proceed with verification, false = cancel)
* @returns Promise<RDNASyncResponse>
*/
async performVerifyAuth(verifyAuthStatus: boolean): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Performing verify auth with status:', verifyAuthStatus);
RdnaClient.performVerifyAuth(verifyAuthStatus, response => {
console.log('RdnaService - PerformVerifyAuth sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - PerformVerifyAuth sync response success, waiting for async events');
resolve(result);
} else {
console.error('RdnaService - PerformVerifyAuth sync response error:', result);
reject(result);
}
});
});
}
/**
* Initiates fallback device activation flow
* Alternative method when REL-ID Verify is not available/accessible
* @returns Promise<RDNASyncResponse>
*/
async fallbackNewDeviceActivationFlow(): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Starting fallback new device activation flow');
RdnaClient.fallbackNewDeviceActivationFlow(response => {
console.log('RdnaService - FallbackNewDeviceActivationFlow sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - FallbackNewDeviceActivationFlow sync response success, alternative activation started');
resolve(result);
} else {
console.error('RdnaService - FallbackNewDeviceActivationFlow sync response error:', result);
reject(result);
}
});
});
}
/**
* Retrieves server notifications for the current user
* Loads all pending notifications with actions
* @param recordCount Number of records to fetch (0 = all active notifications)
* @param startIndex Index to begin fetching from (must be >= 1)
* @param startDate Start date filter (optional)
* @param endDate End date filter (optional)
* @returns Promise<RDNASyncResponse>
*/
async getNotifications(recordCount: number = 0, startIndex: number = 1, startDate: string = '', endDate: string = ''): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Fetching notifications with recordCount:', recordCount, 'startIndex:', startIndex);
RdnaClient.getNotifications(
recordCount, // recordCount
'', // enterpriseID (optional)
startIndex, // startIndex
startDate, // startDate (optional)
endDate, // endDate (optional)
response => { // syncCallback
console.log('RdnaService - GetNotifications sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - GetNotifications sync response success, waiting for onGetNotifications event');
resolve(result);
} else {
console.error('RdnaService - GetNotifications sync response error:', result);
reject(result);
}
}
);
});
}
/**
* Updates a notification with user action
* Processes user decision on notification actions
* @param notificationId Notification identifier (UUID)
* @param response Action response value selected by user
* @returns Promise<RDNASyncResponse>
*/
async updateNotification(notificationId: string, response: string): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Updating notification:', notificationId, 'with response:', response);
RdnaClient.updateNotification(
notificationId, // notificationId
response, // response
result => { // syncCallback
console.log('RdnaService - UpdateNotification sync callback received');
const syncResponse: RDNASyncResponse = result;
if (syncResponse.error && syncResponse.error.longErrorCode === 0) {
console.log('RdnaService - UpdateNotification sync response success, waiting for onUpdateNotification event');
resolve(syncResponse);
} else {
console.error('RdnaService - UpdateNotification sync response error:', syncResponse);
reject(syncResponse);
}
}
);
});
}
autoActivate
(boolean) - automatically start verificationAll device activation APIs follow the established REL-ID SDK pattern:
longErrorCode === 0
means API call succeededEnhance your existing event manager to handle device activation events. Add support for addNewDeviceOptions
, notification retrieval, and notification updates.
Extend your RdnaEventManager
class with device activation event handling:
// src/uniken/services/rdnaEventManager.ts (device activation additions)
class RdnaEventManager {
// Add device activation event handlers
private addNewDeviceOptionsHandler?: RDNAAddNewDeviceOptionsCallback;
private getNotificationsHandler?: RDNAGetNotificationsCallback;
private updateNotificationHandler?: RDNAUpdateNotificationCallback;
private registerEventListeners() {
// ... existing MFA and MTD listeners ...
// Register device activation event listeners
this.listeners.push(
this.rdnaEmitter.addListener('addNewDeviceOptions', this.onAddNewDeviceOptions.bind(this)),
this.rdnaEmitter.addListener('getNotifications', this.onGetNotifications.bind(this)),
this.rdnaEmitter.addListener('updateNotification', this.onUpdateNotification.bind(this))
);
}
}
Add these event handler methods to your event manager:
/**
* Handles device activation options event
* Triggered when SDK detects unregistered device during authentication
*/
private onAddNewDeviceOptions(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Add new device options event received");
try {
const addNewDeviceOptionsData: RDNAAddNewDeviceOptionsData = JSON.parse(response.response);
console.log("RdnaEventManager - UserID:", addNewDeviceOptionsData.userID);
console.log("RdnaEventManager - Available options:", addNewDeviceOptionsData.newDeviceOptions.length);
console.log("RdnaEventManager - Challenge info count:", addNewDeviceOptionsData.challengeInfo.length);
// Log each activation option for debugging
addNewDeviceOptionsData.newDeviceOptions.forEach((option, index) => {
console.log(`RdnaEventManager - Option ${index + 1}:`, option);
});
if (this.addNewDeviceOptionsHandler) {
this.addNewDeviceOptionsHandler(addNewDeviceOptionsData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse add new device options:", error);
}
}
/**
* Handles get notifications response
* Triggered after getNotifications API call completes
*/
private onGetNotifications(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Get notifications event received");
try {
const getNotificationsData: RDNAGetNotificationsData = JSON.parse(response.response);
console.log("RdnaEventManager - Get notifications data:", {
errCode: getNotificationsData.errCode,
userID: getNotificationsData.userID,
notificationCount: getNotificationsData.pArgs?.response?.ResponseData?.notifications?.length || 0
});
if (this.getNotificationsHandler) {
this.getNotificationsHandler(getNotificationsData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse get notifications:", error);
}
}
/**
* Handles update notification response
* Triggered after updateNotification API call completes
*/
private onUpdateNotification(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Update notification event received");
try {
const updateNotificationData: RDNAUpdateNotificationData = JSON.parse(response.response);
console.log("RdnaEventManager - Update notification data:", {
errCode: updateNotificationData.errCode,
statusCode: updateNotificationData.pArgs?.response?.StatusCode,
statusMsg: updateNotificationData.pArgs?.response?.StatusMsg
});
if (this.updateNotificationHandler) {
this.updateNotificationHandler(updateNotificationData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse update notification:", error);
}
}
Add public methods for setting device activation event handlers:
// Public setter methods for device activation event handlers
public setAddNewDeviceOptionsHandler(callback?: RDNAAddNewDeviceOptionsCallback): void {
this.addNewDeviceOptionsHandler = callback;
}
public setGetNotificationsHandler(callback?: RDNAGetNotificationsCallback): void {
this.getNotificationsHandler = callback;
}
public setUpdateNotificationHandler(callback?: RDNAUpdateNotificationCallback): void {
this.updateNotificationHandler = callback;
}
// Enhanced cleanup method to clear device activation handlers
public clearDeviceActivationHandlers(): void {
this.addNewDeviceOptionsHandler = undefined;
this.getNotificationsHandler = undefined;
this.updateNotificationHandler = undefined;
}
// Enhanced cleanup method to clear all handlers
public cleanup(): void {
// Clear existing MFA handlers
this.clearActivationHandlers();
// Clear device activation handlers
this.clearDeviceActivationHandlers();
// Clear existing MTD handlers
this.clearMTDHandlers();
// Remove all event listeners
this.listeners.forEach(listener => listener.remove());
this.listeners = [];
}
getNotifications()
API callupdateNotification()
API callThe device activation events integrate with existing event management:
// Example of comprehensive event setup in SDKEventProvider
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// Existing MFA event handlers
eventManager.setGetUserHandler(handleGetUser);
eventManager.setGetPasswordHandler(handleGetPassword);
// ... other MFA handlers ...
// Device activation event handlers
eventManager.setAddNewDeviceOptionsHandler(handleAddNewDeviceOptions);
eventManager.setGetNotificationsHandler(handleGetNotifications);
eventManager.setUpdateNotificationHandler(handleUpdateNotification);
// Cleanup on unmount
return () => {
eventManager.cleanup();
};
}, []);
Create the VerifyAuthScreen that handles REL-ID Verify device activation with automatic push notification processing and fallback options.
// src/tutorial/screens/mfa/VerifyAuthScreen.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
StatusBar,
ScrollView,
SafeAreaView,
} from 'react-native';
import { useRoute } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import type { RDNAAddNewDeviceOptionsData, RDNASyncResponse } from '../../../uniken/types/rdnaEvents';
import { RDNASyncUtils } from '../../../uniken/types/rdnaEvents';
import rdnaService from '../../../uniken/services/rdnaService';
import { CloseButton, Button, StatusBanner } from '../components';
import type { RootStackParamList } from '../../navigation/AppNavigator';
type VerifyAuthScreenRouteProp = RouteProp<RootStackParamList, 'VerifyAuthScreen'>;
/**
* Verify Auth Screen Component
*/
const VerifyAuthScreen: React.FC = () => {
const route = useRoute<VerifyAuthScreenRouteProp>();
const {
eventData,
title = 'Additional Device Activation',
subtitle = 'Activate this device for secure access',
responseData,
} = route.params;
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [activationData, setActivationData] = useState<{
userID: string;
options: string[];
} | null>(null);
/**
* Handle close button - direct resetAuthState call
*/
const handleClose = async () => {
try {
console.log('VerifyAuthScreen - Calling resetAuthState');
await rdnaService.resetAuthState();
console.log('VerifyAuthScreen - ResetAuthState successful');
} catch (error) {
console.error('VerifyAuthScreen - ResetAuthState error:', error);
}
};
/**
* Process activation data
*/
const processActivationData = (data: RDNAAddNewDeviceOptionsData) => {
return {
userID: data.userID,
options: data.newDeviceOptions,
};
};
/**
* Handle response data from route params
*/
useEffect(() => {
if (responseData) {
console.log('VerifyAuthScreen - Processing response data from route params:', responseData);
try {
// Process activation data
const processed = processActivationData(responseData);
setActivationData(processed);
console.log('VerifyAuthScreen - Processed activation data:', {
userID: processed.userID,
options: processed.options,
});
// Automatically call performVerifyAuth(true) when data is processed
handleVerifyAuth(true);
} catch (error) {
console.error('VerifyAuthScreen - Failed to process activation data:', error);
setError('Failed to process activation data');
}
}
}, [responseData]);
/**
* Handle REL-ID Verify authentication
*/
const handleVerifyAuth = async (proceed: boolean) => {
if (isProcessing) return;
setIsProcessing(true);
setError('');
try {
console.log('VerifyAuthScreen - Performing verify auth:', proceed);
const syncResponse: RDNASyncResponse = await rdnaService.performVerifyAuth(proceed);
console.log('VerifyAuthScreen - PerformVerifyAuth sync response successful, waiting for async events');
console.log('VerifyAuthScreen - Sync response received:', {
longErrorCode: syncResponse.error?.longErrorCode,
shortErrorCode: syncResponse.error?.shortErrorCode,
errorString: syncResponse.error?.errorString
});
if (proceed) {
// Log success message for approval
console.log('VerifyAuthScreen - REL-ID Verify notification has been sent to registered devices');
}
} catch (error) {
// This catch block handles sync response errors (rejected promises)
console.error('VerifyAuthScreen - PerformVerifyAuth sync error:', error);
// Cast the error back to RDNASyncResponse as per other screens pattern
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsProcessing(false);
}
};
/**
* Handle fallback new device activation flow
*/
const handleFallbackFlow = async () => {
if (isProcessing) return;
setIsProcessing(true);
setError('');
try {
console.log('VerifyAuthScreen - Initiating fallback new device activation flow');
const syncResponse: RDNASyncResponse = await rdnaService.fallbackNewDeviceActivationFlow();
console.log('VerifyAuthScreen - FallbackNewDeviceActivationFlow sync response successful, waiting for async events');
console.log('VerifyAuthScreen - Sync response received:', {
longErrorCode: syncResponse.error?.longErrorCode,
shortErrorCode: syncResponse.error?.shortErrorCode,
errorString: syncResponse.error?.errorString
});
// Log success message for fallback initiation
console.log('VerifyAuthScreen - Alternative device activation process has been initiated');
} catch (error) {
// This catch block handles sync response errors (rejected promises)
console.error('VerifyAuthScreen - FallbackNewDeviceActivationFlow sync error:', error);
// Cast the error back to RDNASyncResponse as per other screens pattern
const result: RDNASyncResponse = error as RDNASyncResponse;
const errorMessage = RDNASyncUtils.getErrorMessage(result);
setError(errorMessage);
} finally {
setIsProcessing(false);
}
};
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
<ScrollView style={styles.container}>
{/* Close Button */}
<CloseButton
onPress={handleClose}
disabled={isProcessing}
/>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{/* Error Display */}
{error && (
<StatusBanner
type="error"
message={error}
/>
)}
{/* Processing Status */}
{isProcessing && (
<View style={styles.processingContainer}>
<StatusBanner
type="info"
message="Processing device activation..."
/>
</View>
)}
{/* Activation Information */}
{activationData && (
<>
{/* Processing Message */}
<View style={styles.messageContainer}>
<Text style={styles.messageTitle}>REL-ID Verify Authentication</Text>
<Text style={styles.messageText}>
REL-ID Verify notification has been sent to your registered devices. Please approve it to activate this device.
</Text>
</View>
{/* Fallback Option */}
<View style={styles.fallbackContainer}>
<Text style={styles.fallbackTitle}>Device Not Handy?</Text>
<Text style={styles.fallbackDescription}>
If you don't have access to your registered devices, you can use an alternative activation method.
</Text>
<Button
title="Activate using fallback method"
onPress={handleFallbackFlow}
loading={isProcessing}
variant="outline"
style={styles.fallbackButton}
/>
</View>
</>
)}
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
container: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
paddingTop: 80, // Add space for close button
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 30,
},
processingContainer: {
marginBottom: 20,
},
messageContainer: {
backgroundColor: '#e3f2fd',
borderRadius: 12,
padding: 20,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: '#2196f3',
},
messageTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1976d2',
marginBottom: 8,
},
messageText: {
fontSize: 16,
color: '#1565c0',
lineHeight: 24,
},
fallbackContainer: {
backgroundColor: '#f5f5f5',
borderRadius: 12,
padding: 20,
marginBottom: 20,
borderWidth: 1,
borderColor: '#e0e0e0',
},
fallbackTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 8,
textAlign: 'center',
},
fallbackDescription: {
fontSize: 14,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 16,
lineHeight: 20,
},
fallbackButton: {
alignSelf: 'center',
paddingHorizontal: 24,
},
});
export default VerifyAuthScreen;
performVerifyAuth(true)
when screen loadsThe following image showcases screen from the sample application:
Create the GetNotificationsScreen that automatically loads server notifications and provides interactive action modals for user responses.
// src/tutorial/screens/notification/GetNotificationsScreen.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
StatusBar,
SafeAreaView,
TouchableOpacity,
ScrollView,
FlatList,
Alert,
ActivityIndicator,
Modal,
} from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import rdnaService from '../../../uniken/services/rdnaService';
import type {
RDNAGetNotificationsData,
RDNANotificationItem,
RDNANotificationAction,
RDNAUpdateNotificationData,
} from '../../../uniken/types/rdnaEvents';
/**
* Route Parameters for Get Notifications Screen
*/
interface GetNotificationsScreenParams {
userID: string;
sessionID: string;
sessionType: number;
jwtToken: string;
loginTime?: string;
userRole?: string;
currentWorkFlow?: string;
}
type GetNotificationsScreenRouteProp = RouteProp<
{ GetNotifications: GetNotificationsScreenParams },
'GetNotifications'
>;
interface Props {
route: GetNotificationsScreenRouteProp;
navigation: GetNotificationsScreenNavigationProp;
}
const GetNotificationsScreen: React.FC<Props> = ({ route, navigation }) => {
// State management
const [notifications, setNotifications] = useState<RDNANotificationItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [selectedNotification, setSelectedNotification] = useState<RDNANotificationItem | null>(null);
const [selectedAction, setSelectedAction] = useState<string>('');
const [isProcessingAction, setIsProcessingAction] = useState<boolean>(false);
const [showActionModal, setShowActionModal] = useState<boolean>(false);
// Extract user parameters
const userParams = route.params;
const userID = userParams?.userID || 'Unknown User';
/**
* Set up notification event handlers
*/
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// Set up notification event handlers
eventManager.setGetNotificationsHandler(handleGetNotificationsResponse);
eventManager.setUpdateNotificationHandler(handleUpdateNotificationResponse);
// Auto-load notifications when screen loads
loadNotifications();
// Cleanup handlers on unmount
return () => {
eventManager.setGetNotificationsHandler(undefined);
eventManager.setUpdateNotificationHandler(undefined);
};
}, []);
/**
* Handle notifications received from onGetNotifications event
*/
const handleNotificationsReceived = (data: RDNAGetNotificationsData) => {
console.log('GetNotificationsScreen - Received notifications event');
// Check if this is the standard response format (has pArgs)
if (data.pArgs) {
const notificationList = data.pArgs.response.ResponseData.notifications;
console.log('GetNotificationsScreen - Received notifications:', notificationList.length);
setNotifications(notificationList);
} else if (data.userID) {
// This is the authentication context format - no notifications in this format
console.log('GetNotificationsScreen - Authentication context format, userID:', data.userID);
setNotifications([]);
} else {
// Unknown format or error
console.log('GetNotificationsScreen - Unknown response format');
setNotifications([]);
}
setIsLoading(false);
};
/**
* Handle update notification response from onUpdateNotification event
*/
const handleUpdateNotificationReceived = (data: RDNAUpdateNotificationData) => {
console.log('GetNotificationsScreen - Received update notification event');
setActionLoading(false);
// Check for errors first
if (data.error.longErrorCode !== 0) {
const errorMessage = data.error.errorString || 'Failed to update notification';
console.error('GetNotificationsScreen - Update notification error:', data.error);
console.error('GetNotificationsScreen - Update notification statusCode:', data.pArgs?.response.StatusCode);
Alert.alert(
'Update Failed',
errorMessage,
[{ text: 'OK' }]
);
return;
}
// Check response status
const responseData = data.pArgs?.response;
if (responseData?.StatusCode === 100) {
const notificationUuid = responseData.ResponseData.notification_uuid;
const message = responseData.StatusMsg;
console.log('GetNotificationsScreen - Update notification success:', message);
setShowActionModal(false);
loadNotifications();
} else {
const statusMessage = responseData?.StatusMsg || 'Unknown error occurred';
console.error('GetNotificationsScreen - Update notification status error:', statusMessage);
Alert.alert(
'Update Failed',
statusMessage,
[
{
text: 'OK',
onPress: () => {
setShowActionModal(false);
// Refresh notifications to get updated status
loadNotifications();
}
}
]
);
}
};
/**
* Load notifications from server
*/
const loadNotifications = async () => {
try {
setError('');
console.log('GetNotificationsScreen - Loading notifications for user:', userID);
await rdnaService.getNotifications();
console.log('GetNotificationsScreen - GetNotifications API called, waiting for response');
} catch (error: any) {
console.error('GetNotificationsScreen - Error loading notifications:', error);
setIsLoading(false);
setIsRefreshing(false);
setError(error?.error?.errorString || 'Failed to load notifications. Please try again.');
}
};
/**
* Handle pull-to-refresh
*/
const handleRefresh = () => {
setIsRefreshing(true);
loadNotifications();
};
/**
* Open action modal for notification
*/
const openActionModal = (notification: RDNANotificationItem) => {
if (!notification.actions || notification.actions.length === 0) {
Alert.alert('No Actions', 'This notification has no available actions.');
return;
}
if (notification.status === 'PROCESSED') {
Alert.alert('Already Processed', 'This notification has already been processed.');
return;
}
setSelectedNotification(notification);
setSelectedAction('');
setShowActionModal(true);
};
/**
* Process selected notification action
*/
const processNotificationAction = async () => {
if (!selectedNotification || !selectedAction) {
Alert.alert('Error', 'Please select an action to proceed.');
return;
}
const action = selectedNotification.actions?.find(a => a.actionId === selectedAction);
if (!action) {
Alert.alert('Error', 'Invalid action selected.');
return;
}
// Show confirmation if required
if (action.requiresConfirmation) {
Alert.alert(
'Confirm Action',
`Are you sure you want to ${action.actionName.toLowerCase()}?`,
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Confirm', onPress: executeNotificationAction }
]
);
} else {
executeNotificationAction();
}
};
/**
* Execute the notification action
*/
const executeNotificationAction = async () => {
if (!selectedNotification || !selectedAction) return;
setIsProcessingAction(true);
try {
console.log('GetNotificationsScreen - Processing notification action:', {
notificationId: selectedNotification.notificationId,
actionId: selectedAction
});
await rdnaService.updateNotification(selectedNotification.notification_uuid, selectedAction);
console.log('GetNotificationsScreen - UpdateNotification API called, waiting for response');
} catch (error: any) {
console.error('GetNotificationsScreen - Error processing action:', error);
setIsProcessingAction(false);
Alert.alert(
'Error',
error?.error?.errorString || 'Failed to process notification action. Please try again.'
);
}
};
/**
* Get status color for notification
*/
const getStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return '#FF9500';
case 'PROCESSED': return '#34C759';
case 'EXPIRED': return '#FF3B30';
default: return '#8E8E93';
}
};
/**
* Get priority color for notification
*/
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'HIGH': return '#FF3B30';
case 'MEDIUM': return '#FF9500';
case 'LOW': return '#34C759';
default: return '#8E8E93';
}
};
/**
* Format timestamp for display
*/
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
/**
* Render notification item
*/
const renderNotificationItem = ({ item }: { item: RDNANotificationItem }) => {
// Get the primary body content (usually first language entry)
const primaryBody = item.body[0] || {};
const { subject = 'No Subject', message = 'No Message' } = primaryBody;
return (
<TouchableOpacity
style={styles.notificationItem}
onPress={() => handleNotificationSelect(item)}
>
<View style={styles.notificationHeader}>
<Text style={styles.notificationTitle}>{subject}</Text>
<Text style={styles.notificationTime}>
{new Date(item.create_ts.replace('UTC', 'Z')).toLocaleString()}
</Text>
</View>
<Text style={styles.notificationMessage} numberOfLines={3}>
{message}
</Text>
<View style={styles.notificationFooter}>
<Text style={styles.notificationCategory}>
{item.actions.length} action{item.actions.length !== 1 ? 's' : ''} available
</Text>
<Text style={styles.notificationType}>
{item.action_performed || 'Pending'}
</Text>
</View>
{item.expiry_timestamp && (
<Text style={styles.notificationExpiry}>
Expires: {new Date(item.expiry_timestamp.replace('UTC', 'Z')).toLocaleString()}
</Text>
)}
</TouchableOpacity>
);
};
/**
* Render action modal
*/
const renderActionModal = () => (
<Modal
visible={showActionModal}
transparent={true}
animationType="slide"
onRequestClose={() => !isProcessingAction && setShowActionModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Notification Actions</Text>
{!isProcessingAction && (
<TouchableOpacity
onPress={() => setShowActionModal(false)}
style={styles.closeButton}
>
<Text style={styles.closeButtonText}>×</Text>
</TouchableOpacity>
)}
</View>
{selectedNotification && (
<View style={styles.modalBody}>
<Text style={styles.modalNotificationTitle}>
{selectedNotification.title}
</Text>
<Text style={styles.modalNotificationMessage}>
{selectedNotification.message}
</Text>
<Text style={styles.actionsLabel}>Select an action:</Text>
{selectedNotification.actions?.map((action) => (
<TouchableOpacity
key={action.actionId}
style={[
styles.actionOption,
selectedAction === action.actionId && styles.selectedActionOption
]}
onPress={() => setSelectedAction(action.actionId)}
disabled={isProcessingAction}
>
<View style={styles.radioButton}>
{selectedAction === action.actionId && <View style={styles.radioButtonSelected} />}
</View>
<View style={styles.actionContent}>
<Text style={styles.actionName}>{action.actionName}</Text>
<Text style={styles.actionType}>{action.actionType}</Text>
</View>
</TouchableOpacity>
))}
<View style={styles.modalActions}>
<Button
title={isProcessingAction ? "Processing..." : "Submit Action"}
onPress={processNotificationAction}
variant="primary"
disabled={!selectedAction || isProcessingAction}
/>
{!isProcessingAction && (
<Button
title="Cancel"
onPress={() => setShowActionModal(false)}
variant="outline"
style={styles.cancelButton}
/>
)}
</View>
</View>
)}
</View>
</View>
</Modal>
);
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Notifications</Text>
<Text style={styles.subtitle}>
Manage your REL-ID notifications
</Text>
<Text style={styles.userInfo}>User: {userID}</Text>
</View>
<View style={styles.content}>
{error && (
<StatusBanner type="error" message={error} />
)}
{isLoading && !isRefreshing ? (
<View style={styles.loadingContainer}>
<StatusBanner
type="processing"
message="Loading notifications..."
/>
</View>
) : (
<FlatList
data={notifications}
keyExtractor={(item) => item.notificationId}
renderItem={renderNotificationItem}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor="#007AFF"
/>
}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyTitle}>No Notifications</Text>
<Text style={styles.emptyMessage}>
You don't have any notifications at the moment.
</Text>
<Button
title="Refresh"
onPress={loadNotifications}
variant="outline"
style={styles.refreshButton}
/>
</View>
}
showsVerticalScrollIndicator={false}
/>
)}
</View>
{renderActionModal()}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
backgroundColor: '#fff',
paddingHorizontal: 20,
paddingTop: 60,
paddingBottom: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666',
marginBottom: 8,
},
userInfo: {
fontSize: 14,
color: '#007AFF',
fontWeight: '500',
},
content: {
flex: 1,
padding: 20,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
},
notificationItem: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
notificationHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
notificationTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
flex: 1,
marginRight: 12,
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
statusText: {
fontSize: 12,
fontWeight: '500',
},
notificationMessage: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 12,
},
notificationFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
metadataContainer: {
flexDirection: 'row',
alignItems: 'center',
},
categoryText: {
fontSize: 12,
color: '#8E8E93',
marginRight: 8,
},
priorityIndicator: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
},
priorityText: {
fontSize: 10,
color: '#fff',
fontWeight: '600',
},
timestampText: {
fontSize: 12,
color: '#8E8E93',
},
actionsPreview: {
borderTopWidth: 1,
borderTopColor: '#f0f0f0',
paddingTop: 8,
},
actionsText: {
fontSize: 12,
color: '#007AFF',
fontWeight: '500',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 60,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
emptyMessage: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 20,
},
refreshButton: {
marginTop: 10,
},
// Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContent: {
backgroundColor: '#fff',
borderRadius: 12,
width: '100%',
maxHeight: '80%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
color: '#333',
},
closeButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 20,
color: '#666',
fontWeight: 'bold',
},
modalBody: {
padding: 20,
},
modalNotificationTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
modalNotificationMessage: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 20,
},
actionsLabel: {
fontSize: 14,
fontWeight: '600',
color: '#333',
marginBottom: 12,
},
actionOption: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e0e0e0',
marginBottom: 8,
},
selectedActionOption: {
borderColor: '#007AFF',
backgroundColor: '#f0f8ff',
},
radioButton: {
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 2,
borderColor: '#ccc',
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
radioButtonSelected: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: '#007AFF',
},
actionContent: {
flex: 1,
},
actionName: {
fontSize: 14,
fontWeight: '500',
color: '#333',
},
actionType: {
fontSize: 12,
color: '#666',
marginTop: 2,
},
modalActions: {
marginTop: 20,
},
cancelButton: {
marginTop: 10,
},
});
export default GetNotificationsScreen;
getNotifications()
when screen loadsgetNotifications
and updateNotification
eventsThe following images showcase screens from the sample application:
|
|
|
Extend your existing SDKEventProvider to handle device activation events and coordinate navigation for the additional device activation workflow.
Enhance your SDKEventProvider with device activation event handling:
// src/uniken/providers/SDKEventProvider.tsx (device activation additions)
import type {
// ... existing imports ...
RDNAAddNewDeviceOptionsData,
RDNAGetNotificationsData,
RDNAUpdateNotificationData
} from '../types/rdnaEvents';
export const SDKEventProvider: React.FC<SDKEventProviderProps> = ({ children }) => {
const [currentScreen, setCurrentScreen] = useState<string | null>(null);
// ... existing MFA event handlers ...
/**
* Event handler for device activation options
* Triggered when SDK detects unregistered device during authentication
*/
const handleAddNewDeviceOptions = useCallback((data: RDNAAddNewDeviceOptionsData) => {
console.log('SDKEventProvider - Add new device options event received for user:', data.userID);
console.log('SDKEventProvider - Available options:', data.newDeviceOptions);
console.log('SDKEventProvider - Challenge info count:', data.challengeInfo.length);
// Use navigateOrUpdate to prevent duplicate screens and update existing screen with new event data
NavigationService.navigateOrUpdate('VerifyAuthScreen', {
eventName: 'addNewDeviceOptions',
eventData: data,
title: 'Additional Device Activation',
subtitle: `Activate this device for user: ${data.userID}`,
// Pass response data directly
responseData: data,
});
}, []);
/**
* Event handler for get notifications response
* Triggered after getNotifications API call completes
*/
const handleGetNotifications = useCallback((data: RDNAGetNotificationsData) => {
console.log('SDKEventProvider - Get notifications event received');
console.log('SDKEventProvider - Total notifications:', data.totalCount);
console.log('SDKEventProvider - Notifications received:', data.notifications.length);
// The GetNotificationsScreen handles this event directly through its own event subscription
// No navigation needed here - this is handled by the screen itself
console.log('SDKEventProvider - Get notifications event handled by GetNotificationsScreen');
}, []);
/**
* Event handler for update notification response
* Triggered after updateNotification API call completes
*/
const handleUpdateNotification = useCallback((data: RDNAUpdateNotificationData) => {
console.log('SDKEventProvider - Update notification event received');
console.log('SDKEventProvider - Notification updated:', {
notificationId: data.notificationId,
actionId: data.actionId,
success: data.success
});
// The GetNotificationsScreen handles this event directly through its own event subscription
// No navigation needed here - this is handled by the screen itself
console.log('SDKEventProvider - Update notification event handled by GetNotificationsScreen');
}, []);
/**
* Enhanced handleUserLoggedIn for device activation support
* Updated to handle drawer navigation with GetNotifications
*/
const handleUserLoggedIn = useCallback((data: RDNAUserLoggedInData) => {
console.log('SDKEventProvider - User logged in event received for user:', data.userID);
console.log('SDKEventProvider - Session ID:', data.challengeResponse.session.sessionID);
console.log('SDKEventProvider - Current workflow:', data.challengeResponse.additionalInfo.currentWorkFlow);
// Extract session and JWT information
const sessionID = data.challengeResponse.session.sessionID;
const sessionType = data.challengeResponse.session.sessionType;
const additionalInfo = data.challengeResponse.additionalInfo;
const jwtToken = additionalInfo.jwtJsonTokenInfo;
const userRole = additionalInfo.idvUserRole;
const currentWorkFlow = additionalInfo.currentWorkFlow;
// Navigate to DrawerNavigator with all session data
// This now includes access to GetNotifications screen
NavigationService.navigate('DrawerNavigator', {
screen: 'Dashboard',
params: {
userID: data.userID,
sessionID,
sessionType,
jwtToken,
loginTime: new Date().toLocaleString(),
userRole,
currentWorkFlow,
}
});
}, []);
/**
* Set up SDK Event Subscriptions on mount
* Enhanced with device activation event handlers
*/
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// Existing MFA event handlers
eventManager.setInitializedHandler(handleInitialized);
eventManager.setGetUserHandler(handleGetUser);
eventManager.setGetActivationCodeHandler(handleGetActivationCode);
eventManager.setGetUserConsentForLDAHandler(handleGetUserConsentForLDA);
eventManager.setGetPasswordHandler(handleGetPassword);
eventManager.setOnUserLoggedInHandler(handleUserLoggedIn);
eventManager.setCredentialsAvailableForUpdateHandler(handleCredentialsAvailableForUpdate);
eventManager.setOnUserLoggedOffHandler(handleUserLoggedOff);
// Device activation event handlers
eventManager.setAddNewDeviceOptionsHandler(handleAddNewDeviceOptions);
eventManager.setGetNotificationsHandler(handleGetNotifications);
eventManager.setUpdateNotificationHandler(handleUpdateNotification);
// Only cleanup on component unmount
return () => {
console.log('SDKEventProvider - Component unmounting, cleaning up event handlers');
eventManager.cleanup();
};
}, []); // Empty dependency array - setup once on mount
// ... rest of component remains the same ...
};
Update your navigation types to support the new device activation screens:
// src/tutorial/navigation/AppNavigator.tsx (type additions)
export type RootStackParamList = {
// ... existing screens ...
// Device Activation Screens
VerifyAuthScreen: {
eventData?: RDNAAddNewDeviceOptionsData;
responseData?: RDNAAddNewDeviceOptionsData;
userID: string;
options: RDNADeviceActivationOption[];
};
// Drawer Navigator (enhanced with GetNotifications)
DrawerNavigator: {
screen: keyof DrawerParamList;
params?: any;
};
};
Update your DrawerNavigator to include the GetNotifications screen:
// src/tutorial/navigation/DrawerNavigator.tsx (already implemented in your project)
import GetNotificationsScreen from '../screens/notification/GetNotificationsScreen';
// DrawerParamList already includes GetNotifications
export type DrawerParamList = {
Dashboard: {
userID: string;
sessionID: string;
sessionType: number;
jwtToken: string;
loginTime?: string;
userRole?: string;
currentWorkFlow?: string;
};
GetNotifications: {
userID: string;
sessionID: string;
sessionType: number;
jwtToken: string;
loginTime?: string;
userRole?: string;
currentWorkFlow?: string;
};
};
// Drawer.Screen for GetNotifications already configured
<Drawer.Screen
name="GetNotifications"
component={GetNotificationsScreen}
initialParams={userParams}
options={{
drawerLabel: 'Get Notifications',
}}
/>
navigateOrUpdate
to prevent duplicate screens during repeated eventsThe enhanced SDKEventProvider coordinates these device activation flows:
addNewDeviceOptions
The provider uses a layered event handling approach:
addNewDeviceOptions
getNotifications
Test your device activation implementation to ensure REL-ID Verify workflows, fallback methods, and notification management work correctly across different scenarios.
Test the complete automatic device activation flow:
# Ensure you have multiple physical devices
# Device A: Already registered with REL-ID
# Device B: New device for activation testing
# Build and deploy to both devices
npx react-native run-ios --device "Device-A-Name"
npx react-native run-ios --device "Device-B-Name"
addNewDeviceOptions
eventperformVerifyAuth(true)
called automaticallySDKEventProvider - Add new device options event received for user: testuser@example.com
SDKEventProvider - Available options: 2
SDKEventProvider - Option 1: {optionId: "verify-auth", optionName: "REL-ID Verify", isDefault: true}
VerifyAuthScreen - Auto-starting REL-ID Verify for user: testuser@example.com
VerifyAuthScreen - PerformVerifyAuth sync response successful
Test the fallback activation when REL-ID Verify is not accessible:
fallbackNewDeviceActivationFlow()
calledVerifyAuthScreen - Starting fallback activation for user: testuser@example.com
VerifyAuthScreen - FallbackNewDeviceActivationFlow sync response successful
Test the GetNotificationsScreen functionality:
getNotifications()
API called again// Check if device is already registered
// Verify MFA flow completion before device detection
// Ensure proper connection profile configuration
// Check GetNotificationsScreen event handler setup
eventManager.setGetNotificationsHandler(handleGetNotificationsResponse);
// Verify API call execution
await rdnaService.getNotifications();
addNewDeviceOptions
event triggers during MFAperformVerifyAuth(true)
executes automaticallyCongratulations! You've successfully implemented a comprehensive Additional Device Activation system with REL-ID Verify push notifications, fallback methods, and notification management.
✅ REL-ID Verify Integration: Automatic push notification-based device activation
✅ VerifyAuthScreen Implementation: Auto-starting activation with real-time status updates
✅ Fallback Activation Methods: Alternative activation when registered devices aren't accessible
✅ GetNotificationsScreen: Server notification management with interactive action processing
✅ Enhanced Drawer Navigation: Seamless access to notifications via enhanced navigation
Your implementation now handles these production scenarios:
You've mastered Advanced Device Activation with REL-ID Verify and built a production-ready system that provides:
Your application now provides enterprise-grade device activation capabilities that enhance security while maintaining user convenience. You're ready to deploy this solution in production environments and scale to support thousands of users across multiple devices.
🚀 You're now equipped to build sophisticated device activation workflows that combine security, usability, and reliability!