This codelab demonstrates how to implement Session Management flow using the react-native-rdna-client npm plugin. Session management provides critical security features including automatic session timeout handling, idle session warnings with extension capabilities, and seamless session lifecycle management to prevent unexpected user logouts.
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-session-management
folder in the repository you cloned earlier
react-native-rdna-client
plugin installed and configuredThe sample app provides a complete session management implementation. Let's examine the key components:
Component | Purpose | Sample App Reference |
Session Context | Global session state management |
|
Session Modal | UI with countdown and extension |
|
Event Handling | Extended event manager |
|
Session Types | TypeScript interfaces |
|
The RELID SDK triggers three main session management events:
Event Type | Description | User Action Required |
Hard session timeout - session already expired | User must acknowledge and app navigates to home | |
Idle session warning - session will expire soon | User can extend session or let it expire | |
Response from session extension API call | Handle success/failure of extension attempt |
The session management flow follows this pattern:
onSessionTimeOutNotification
triggers with countdown and extension optionextendSessionIdleTimeout()
APIonSessionExtensionResponse
provides success/failure resultonSessionTimeout
forces app navigation when session expiresDefine TypeScript interfaces for comprehensive session timeout handling:
// src/uniken/types/rdnaEvents.ts (additions)
/**
* RDNA Session Timeout Data
* Event triggered when session times out (hard timeout)
*/
export interface RDNASessionTimeoutData {
message: string;
}
/**
* RDNA Session Timeout Notification Data
* Event triggered before session timeout with extension option
*/
export interface RDNASessionTimeoutNotificationData {
userID: string;
message: string;
timeLeftInSeconds: number;
sessionCanBeExtended: number; // 0 = cannot extend, 1 = can extend
info: {
sessionType: number;
currentWorkFlow: string;
};
}
/**
* RDNA Session Extension Response Data
* Response received after attempting to extend session timeout
*/
export interface RDNASessionExtensionResponseData {
status: RDNAStatus;
error: RDNAError;
}
// Session Management Callback Types
export type RDNASessionTimeoutCallback = (data: RDNASessionTimeoutData) => void;
export type RDNASessionTimeoutNotificationCallback = (data: RDNASessionTimeoutNotificationData) => void;
export type RDNASessionExtensionResponseCallback = (data: RDNASessionExtensionResponseData) => void;
Session management handles two distinct scenarios:
Session Type | Trigger | User Options | Implementation |
Hard Timeout | Session already expired | Close button only | Navigate to home screen |
Idle Warning | Session expiring soon | Extend or Close | API call or natural expiry |
Extend your existing event manager to handle session management events:
// src/uniken/services/rdnaEventManager.ts (additions)
class RdnaEventManager {
// Add session management callback properties
private sessionTimeoutHandler?: RDNASessionTimeoutCallback;
private sessionTimeoutNotificationHandler?: RDNASessionTimeoutNotificationCallback;
private sessionExtensionResponseHandler?: RDNASessionExtensionResponseCallback;
private registerEventListeners() {
// ... existing listeners ...
// Add session management event listeners
this.listeners.push(
this.rdnaEmitter.addListener('onSessionTimeout', this.onSessionTimeout.bind(this)),
this.rdnaEmitter.addListener('onSessionTimeOutNotification', this.onSessionTimeOutNotification.bind(this)),
this.rdnaEmitter.addListener('onSessionExtensionResponse', this.onSessionExtensionResponse.bind(this))
);
}
/**
* Handles session timeout events for mandatory sessions
*/
private onSessionTimeout(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Session timeout event received");
try {
let sessionTimeoutData: RDNASessionTimeoutData;
if (typeof response.response === 'string') {
// Treat the string as a plain message
sessionTimeoutData = {
message: response.response
};
} else {
// If it's already an object, use it directly
sessionTimeoutData = response.response as RDNASessionTimeoutData;
}
console.log("RdnaEventManager - Session timeout message:", sessionTimeoutData.message);
if (this.sessionTimeoutHandler) {
this.sessionTimeoutHandler(sessionTimeoutData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to handle session timeout:", error);
}
}
/**
* Handles session timeout notification events for idle sessions
*/
private onSessionTimeOutNotification(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Session timeout notification event received");
try {
const sessionNotificationData: RDNASessionTimeoutNotificationData = JSON.parse(response.response);
console.log("RdnaEventManager - Session timeout notification:", {
userID: sessionNotificationData.userID,
timeLeft: sessionNotificationData.timeLeftInSeconds,
canExtend: sessionNotificationData.sessionCanBeExtended === 1
});
if (this.sessionTimeoutNotificationHandler) {
this.sessionTimeoutNotificationHandler(sessionNotificationData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse session timeout notification:", error);
}
}
/**
* Handles session extension response events
*/
private onSessionExtensionResponse(response: RDNAJsonResponse) {
console.log("RdnaEventManager - Session extension response event received");
try {
const sessionExtensionData: RDNASessionExtensionResponseData = JSON.parse(response.response);
console.log("RdnaEventManager - Session extension response:", {
statusCode: sessionExtensionData.status.statusCode,
statusMessage: sessionExtensionData.status.statusMessage,
errorCode: sessionExtensionData.error.longErrorCode
});
if (this.sessionExtensionResponseHandler) {
this.sessionExtensionResponseHandler(sessionExtensionData);
}
} catch (error) {
console.error("RdnaEventManager - Failed to parse session extension response:", error);
}
}
// Handler setter methods
public setSessionTimeoutHandler(callback?: RDNASessionTimeoutCallback): void {
this.sessionTimeoutHandler = callback;
}
public setSessionTimeoutNotificationHandler(callback?: RDNASessionTimeoutNotificationCallback): void {
this.sessionTimeoutNotificationHandler = callback;
}
public setSessionExtensionResponseHandler(callback?: RDNASessionExtensionResponseCallback): void {
this.sessionExtensionResponseHandler = callback;
}
}
Key features of session event handling:
Add session extension capability to your RELID service:
// src/uniken/services/rdnaService.ts (addition)
export class RdnaService {
/**
* Extends the idle session timeout
*
* This method extends the current idle session timeout when the session is eligible for extension.
* Should be called in response to onSessionTimeOutNotification events when sessionCanBeExtended = 1.
* After calling this method, the SDK will trigger an onSessionExtensionResponse event with the result.
*
* @see https://developer.uniken.com/docs/extend-session-timeout
*
* Response Validation Logic:
* 1. Check error.longErrorCode: 0 = success, > 0 = error
* 2. An onSessionExtensionResponse event will be triggered with detailed response
* 3. The extension success/failure will be determined by the async event response
*
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async extendSessionIdleTimeout(): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
console.log('RdnaService - Extending session idle timeout');
RdnaClient.extendSessionIdleTimeout(response => {
console.log('RdnaService - ExtendSessionIdleTimeout sync callback received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - ExtendSessionIdleTimeout sync response success, waiting for onSessionExtensionResponse event');
resolve(result);
} else {
console.error('RdnaService - ExtendSessionIdleTimeout sync response error:', result);
reject(result);
}
});
});
}
}
When handling session extension, two response layers must be considered:
Response Layer | Purpose | Success Criteria | Failure Handling |
Sync Response | API call validation |
| Immediate rejection |
Async Event | Extension result |
| Display error message |
Note: The sync response only indicates the API call was accepted. The actual extension success/failure is communicated through the onSessionExtensionResponse
event.
Create a React Context to manage session state across your application:
// src/uniken/SessionContext/SessionContext.tsx
interface SessionState {
// Modal visibility and data
isSessionModalVisible: boolean;
sessionTimeoutData?: RDNASessionTimeoutData;
sessionTimeoutNotificationData?: RDNASessionTimeoutNotificationData;
isProcessing: boolean;
// Session management methods
showSessionTimeout: (data: RDNASessionTimeoutData) => void;
showSessionTimeoutNotification: (data: RDNASessionTimeoutNotificationData) => void;
hideSessionModal: () => void;
handleExtendSession: () => void;
handleDismiss: () => void;
}
export const SessionProvider: React.FC<SessionProviderProps> = ({ children }) => {
const [isSessionModalVisible, setIsSessionModalVisible] = useState(false);
const [sessionTimeoutData, setSessionTimeoutData] = useState<RDNASessionTimeoutData | undefined>();
const [sessionTimeoutNotificationData, setSessionTimeoutNotificationData] = useState<RDNASessionTimeoutNotificationData | undefined>();
const [isProcessing, setIsProcessing] = useState(false);
// Ref to track current session operation to avoid conflicts
const currentOperationRef = useRef<'none' | 'extend'>('none');
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// Set up session event handlers
eventManager.setSessionTimeoutHandler((data: RDNASessionTimeoutData) => {
showSessionTimeout(data);
});
eventManager.setSessionTimeoutNotificationHandler((data: RDNASessionTimeoutNotificationData) => {
showSessionTimeoutNotification(data);
});
eventManager.setSessionExtensionResponseHandler((data: RDNASessionExtensionResponseData) => {
handleSessionExtensionResponse(data);
});
return () => {
// Cleanup handlers
eventManager.setSessionTimeoutHandler(undefined);
eventManager.setSessionTimeoutNotificationHandler(undefined);
eventManager.setSessionExtensionResponseHandler(undefined);
};
}, []);
};
The context handles session extension with comprehensive state management:
const handleExtendSession = async () => {
console.log('SessionProvider - User chose to extend session');
if (currentOperationRef.current !== 'none') {
console.log('SessionProvider - Operation already in progress, ignoring extend request');
return;
}
setIsProcessing(true);
currentOperationRef.current = 'extend';
try {
// Call extend session API
await rdnaService.extendSessionIdleTimeout();
console.log('SessionProvider - Session extension API called successfully');
// Note: We don't hide the modal immediately as we're waiting for onSessionExtensionResponse
// The response handler will determine success/failure and take appropriate action
} catch (error) {
console.error('SessionProvider - Session extension failed:', error);
setIsProcessing(false);
currentOperationRef.current = 'none';
const result: RDNASyncResponse = error as RDNASyncResponse;
Alert.alert(
'Extension Failed',
`Failed to extend session:\n${result.error.errorString}\n\nError Code: ${result.error.longErrorCode}`,
[{ text: 'OK', style: 'default' }]
);
}
};
const handleSessionExtensionResponse = (data: RDNASessionExtensionResponseData) => {
console.log('SessionProvider - Processing session extension response');
// Only process if we're currently extending
if (currentOperationRef.current !== 'extend') {
console.log('SessionProvider - Extension response received but no extend operation in progress, ignoring');
return;
}
const isSuccess = data.error.longErrorCode === 0 && data.status.statusCode === 100;
if (isSuccess) {
console.log('SessionProvider - Session extension successful');
hideSessionModal();
} else {
console.log('SessionProvider - Session extension failed');
setIsProcessing(false);
currentOperationRef.current = 'none';
const errorMessage = data.error.longErrorCode !== 0
? data.error.errorString
: data.status.statusMessage;
Alert.alert(
'Extension Failed',
`Failed to extend session:\n${errorMessage}`,
[{ text: 'OK', style: 'default' }]
);
}
};
Key features of the session context:
Create a modal component to display session information and handle user interactions:
// src/uniken/components/modals/SessionModal.tsx
interface SessionModalProps {
visible: boolean;
sessionTimeoutData?: RDNASessionTimeoutData;
sessionTimeoutNotificationData?: RDNASessionTimeoutNotificationData;
isProcessing?: boolean;
onExtendSession?: () => void;
onDismiss?: () => void;
}
const SessionModal: React.FC<SessionModalProps> = ({
visible,
sessionTimeoutData,
sessionTimeoutNotificationData,
isProcessing = false,
onExtendSession,
onDismiss,
}) => {
const [countdown, setCountdown] = useState<number>(0);
const backgroundTimeRef = useRef<number | null>(null);
// Determine session type
const isMandatoryTimeout = !!sessionTimeoutData;
const isIdleTimeout = !!sessionTimeoutNotificationData;
const canExtendSession = sessionTimeoutNotificationData?.sessionCanBeExtended === 1;
// Initialize countdown from notification data
useEffect(() => {
if (sessionTimeoutNotificationData?.timeLeftInSeconds) {
const timeLeft = sessionTimeoutNotificationData.timeLeftInSeconds;
setCountdown(timeLeft);
}
}, [sessionTimeoutNotificationData]);
// Countdown timer effect
useEffect(() => {
if (countdown > 0 && (isIdleTimeout || isMandatoryTimeout) && visible) {
const timer = setTimeout(() => {
setCountdown(prev => prev - 1);
}, 1000);
return () => clearTimeout(timer);
}
}, [countdown, isIdleTimeout, isMandatoryTimeout, visible]);
};
Critical feature for accurate countdown when app goes to background:
// Handle app state changes for accurate countdown when app goes to background/foreground
useEffect(() => {
const handleAppStateChange = (nextAppState: string) => {
if (visible && (isIdleTimeout || isMandatoryTimeout)) {
if (nextAppState === 'background' || nextAppState === 'inactive') {
// App going to background - record the time
backgroundTimeRef.current = Date.now();
console.log('SessionModal - App going to background, recording time');
} else if (nextAppState === 'active' && backgroundTimeRef.current) {
// App returning to foreground - calculate elapsed time
const elapsedSeconds = Math.floor((Date.now() - backgroundTimeRef.current) / 1000);
console.log(`SessionModal - App returning to foreground, elapsed: ${elapsedSeconds}s`);
// Update countdown based on actual elapsed time
setCountdown(prevCountdown => {
const newCountdown = Math.max(0, prevCountdown - elapsedSeconds);
console.log(`SessionModal - Countdown updated: ${prevCountdown} -> ${newCountdown}`);
return newCountdown;
});
backgroundTimeRef.current = null;
}
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription?.remove();
};
}, [visible, isIdleTimeout, isMandatoryTimeout]);
Prevent accidental modal dismissal on Android:
// Disable back button when modal is visible
useEffect(() => {
const handleBackPress = () => {
if (visible) {
return true; // Prevent default back action
}
return false;
};
const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => backHandler.remove();
}, [visible]);
return (
<Modal visible={visible} transparent={true} animationType="fade">
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
{/* Header with session type indicator */}
<View style={[styles.modalHeader, { backgroundColor: config.headerColor }]}>
<Text style={styles.modalTitle}>
{isMandatoryTimeout ? '🔐 Session Expired' : '⚠️ Session Timeout Warning'}
</Text>
<Text style={styles.modalSubtitle}>
{isMandatoryTimeout
? 'Your session has expired. You will be redirected to the home screen.'
: canExtendSession
? 'Your session will expire soon. You can extend it or let it timeout.'
: 'Your session will expire soon.'
}
</Text>
</View>
{/* Content with countdown for idle timeout */}
<View style={styles.contentContainer}>
<Text style={styles.sessionMessage}>
{getDisplayMessage()}
</Text>
{/* Countdown display for idle timeout */}
{isIdleTimeout && countdown > 0 && (
<View style={styles.countdownContainer}>
<Text style={styles.countdownLabel}>Time Remaining:</Text>
<Text style={styles.countdownText}>
{Math.floor(countdown / 60)}:{(countdown % 60).toString().padStart(2, '0')}
</Text>
</View>
)}
</View>
{/* Action Buttons */}
<View style={styles.buttonContainer}>
{/* Hard timeout - only Close option */}
{isMandatoryTimeout && (
<TouchableOpacity style={styles.secondaryButton} onPress={onDismiss}>
<Text style={styles.secondaryButtonText}>Close</Text>
</TouchableOpacity>
)}
{/* Idle timeout - extend or dismiss options */}
{isIdleTimeout && (
<>
{canExtendSession && onExtendSession && (
<TouchableOpacity
style={[styles.primaryButton, isProcessing && styles.buttonDisabled]}
onPress={onExtendSession}
disabled={isProcessing}
>
{isProcessing ? (
<View style={styles.buttonLoadingContent}>
<ActivityIndicator size="small" color="#ffffff" />
<Text style={styles.primaryButtonText}>Extending...</Text>
</View>
) : (
<Text style={styles.primaryButtonText}>Extend Session</Text>
)}
</TouchableOpacity>
)}
<TouchableOpacity
style={[styles.secondaryButton, isProcessing && styles.buttonDisabled]}
onPress={onDismiss}
disabled={isProcessing}
>
<Text style={styles.secondaryButtonText}>Close</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
</View>
</Modal>
);
Key features of the session modal:
The following image showcase screens from the sample application:
|
|
Wrap your application with the session context provider:
// App.tsx
import SessionProvider from './src/uniken/SessionContext';
import { NavigationContainer } from '@react-navigation/native';
import { navigationRef } from './src/tutorial/navigation/NavigationService';
const AppContent = () => {
const {
isSessionModalVisible,
sessionTimeoutData,
sessionTimeoutNotificationData,
isProcessing,
handleExtendSession,
handleDismiss,
} = useSession();
return (
<>
<NavigationContainer ref={navigationRef}>
{/* Your existing navigation */}
</NavigationContainer>
{/* Global Session Management Modal */}
<SessionModal
visible={isSessionModalVisible}
sessionTimeoutData={sessionTimeoutData}
sessionTimeoutNotificationData={sessionTimeoutNotificationData}
isProcessing={isProcessing}
onExtendSession={handleExtendSession}
onDismiss={handleDismiss}
/>
</>
);
};
function App() {
return (
<SessionProvider>
<AppContent />
</SessionProvider>
);
}
The context provider approach offers several advantages:
Session Type | Test Case | Expected Behavior | Validation Points |
Hard Timeout | Session expires | Modal with Close button only | Navigate to home screen |
Idle Warning | Session expiring soon | Modal with countdown and Extend button | Extension API call works |
Extension Success | Extend session API succeeds | Modal dismisses, session continues | No navigation occurs |
Extension Failure | Extend session API fails | Error alert, modal remains | User can retry or close |
Background Timer | App goes to background during countdown | Timer accurately reflects elapsed time | Countdown resumes correctly |
Critical test for production reliability:
// Test background/foreground timer accuracy
const testBackgroundTimer = async () => {
// 1. Trigger idle session timeout notification
// 2. Note the countdown time (e.g., 60 seconds)
// 3. Background the app for 30 seconds
// 4. Foreground the app
// 5. Verify countdown shows ~30 seconds remaining
console.log('Testing background timer accuracy');
console.log('Initial countdown:', initialCountdown);
console.log('Time spent in background:', backgroundTime);
console.log('Expected remaining time:', initialCountdown - backgroundTime);
console.log('Actual remaining time:', currentCountdown);
};
Use these debugging techniques to verify session functionality:
// Verify callback registration
console.log('Session callbacks:', {
timeout: !!eventManager.sessionTimeoutHandler,
notification: !!eventManager.sessionTimeoutNotificationHandler,
extension: !!eventManager.sessionExtensionResponseHandler
});
// Log session event data
console.log('Session timeout notification:', {
userID: data.userID,
timeLeft: data.timeLeftInSeconds,
canExtend: data.sessionCanBeExtended === 1,
message: data.message
});
Cause: Session callbacks not properly registered Solution: Verify SessionProvider
wraps your app and callbacks are set
Cause: Event listeners not attached Solution: Check that rdnaEmitter.addListener
calls are successful
Cause: Countdown doesn't account for background time Solution: Implement AppState change handling
// Correct background/foreground handling
useEffect(() => {
const handleAppStateChange = (nextAppState: string) => {
if (nextAppState === 'background') {
backgroundTimeRef.current = Date.now();
} else if (nextAppState === 'active' && backgroundTimeRef.current) {
const elapsedSeconds = Math.floor((Date.now() - backgroundTimeRef.current) / 1000);
setCountdown(prev => Math.max(0, prev - elapsedSeconds));
backgroundTimeRef.current = null;
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription?.remove();
}, []);
Cause: Calling extension API when sessionCanBeExtended
is false Solution: Check extension eligibility before API call
const handleExtendSession = async () => {
if (sessionTimeoutNotificationData?.sessionCanBeExtended !== 1) {
Alert.alert('Extension Not Available', 'This session cannot be extended.');
return;
}
// Proceed with extension API call
await rdnaService.extendSessionIdleTimeout();
};
Cause: Multiple concurrent extension requests Solution: Use operation tracking to prevent duplicates
const currentOperationRef = useRef<'none' | 'extend'>('none');
const handleExtendSession = async () => {
if (currentOperationRef.current !== 'none') {
console.log('Extension already in progress');
return;
}
currentOperationRef.current = 'extend';
// ... perform extension
};
Cause: Back button dismissing modal on Android Solution: Implement BackHandler prevention
useEffect(() => {
const handleBackPress = () => {
if (visible) {
return true; // Prevent default back action
}
return false;
};
const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => backHandler.remove();
}, [visible]);
Best Practice: Test session management behavior on both iOS and Android devices with different timeout scenarios.
Extension Scenario | Recommended Action | Implementation |
Frequent Extensions | Set reasonable limits | Track extension count per session |
Critical Operations | Allow extensions during important tasks | Context-aware extension logic |
Inactive Sessions | Enforce timeouts | Don't extend completely idle sessions |
// Proper cleanup in SessionProvider
useEffect(() => {
const eventManager = rdnaService.getEventManager();
// Set handlers
eventManager.setSessionTimeoutHandler(handleSessionTimeout);
// Cleanup function
return () => {
eventManager.setSessionTimeoutHandler(undefined);
eventManager.setSessionTimeoutNotificationHandler(undefined);
eventManager.setSessionExtensionResponseHandler(undefined);
};
}, []);
Congratulations! You've successfully learned how to implement comprehensive session management functionality with:
Your session management implementation now provides: