This codelab demonstrates how to implement Session Management flow using the RELID iOS SDK. 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.

What You'll Learn

Get the Code from GitHub

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-ios.git

Navigate to the relid-MFA-session-management folder in the repository you cloned earlier

What You'll Need

The sample app provides a complete session management implementation. Let's examine the key components:

Component

Purpose

Sample App Reference

SessionManager

Global session state management

Sources/Uniken/Session/SessionManager.swift

SessionAlertViewController

UI with countdown and extension

Sources/Uniken/Session/SessionAlertViewController.swift

RDNADelegateManager

SDK callback handler

Sources/Uniken/Services/RDNADelegateManager.swift

RDNAService

SDK API wrapper

Sources/Uniken/Services/RDNAService.swift

Session Management Event Types

The RELID SDK triggers three main session management events:

Event Type

Description

User Action Required

onSessionTimeout

Hard session timeout - session already expired

User must acknowledge and app navigates to home

onSessionTimeOutNotification

Idle session warning - session will expire soon

User can extend session or let it expire

onSessionExtensionResponse

Response from session extension API call

Handle success/failure of extension attempt

Session Management Flow Architecture

The session management flow follows this pattern:

  1. SDK monitors session activity based on gateway configuration
  2. Idle Warning: onSessionTimeOutNotification triggers with countdown and extension option
  3. Extension Request: User can call extendSessionIdleTimeout() API
  4. Extension Response: onSessionExtensionResponse provides success/failure result
  5. Hard Timeout: onSessionTimeout forces app navigation when session expires

Extend your existing RDNADelegateManager to handle session management callbacks. The delegate manager conforms to the SDK's RDNACallbacks protocol and dispatches events via closure properties:

// Sources/Uniken/Services/RDNADelegateManager.swift
class RDNADelegateManager: NSObject, RDNACallbacks {
    static let shared = RDNADelegateManager()

    // MARK: - Session Management Callback Closures

    /// Callback for hard session timeout (session already expired)
    var onSessionTimeout: ((_ message: String) -> Void)?

    /// Callback for idle session timeout notification (session expiring soon)
    var onSessionTimeOutNotification: ((_ userID: String,
                                       _ message: String,
                                       _ timeLeftInSeconds: Int,
                                       _ sessionCanBeExtended: Int,
                                       _ info: RDNAGeneralInfo) -> Void)?

    /// Callback for session extension response
    var onSessionExtensionResponse: ((_ userID: String,
                                     _ info: RDNAGeneralInfo,
                                     _ status: RDNARequestStatus,
                                     _ error: RDNAError) -> Void)?

    // MARK: - RDNACallbacks Protocol Implementation

    func onSessionTimeout(_ message: String) {
        print("RDNADelegateManager - Hard session timeout: \(message)")

        // Dispatch to main thread for UI updates
        DispatchQueue.main.async { [weak self] in
            self?.onSessionTimeout?(message)
        }
    }

    func onSessionTimeOutNotification(_ userID: String,
                                     message: String,
                                     timeLeftInSecond: Int32,
                                     sessionCanBeExtended: Int32,
                                     info: RDNAGeneralInfo) {
        print("RDNADelegateManager - Session timeout notification: \(timeLeftInSecond)s remaining")

        // Convert Int32 to Int for Swift convenience
        DispatchQueue.main.async { [weak self] in
            self?.onSessionTimeOutNotification?(
                userID,
                message,
                Int(timeLeftInSecond),
                Int(sessionCanBeExtended),
                info
            )
        }
    }

    func onSessionExtensionResponse(_ userID: String,
                                   info: RDNAGeneralInfo,
                                   status: RDNARequestStatus,
                                   error: RDNAError) {
        let isSuccess = error.longErrorCode == 0 && status.statusCode == 100
        print("RDNADelegateManager - Session extension response: \(isSuccess ? "success" : "failure")")

        DispatchQueue.main.async { [weak self] in
            self?.onSessionExtensionResponse?(userID, info, status, error)
        }
    }

    private override init() {
        super.init()
    }
}

Key iOS Patterns:

Add session extension capability to your RDNAService wrapper:

// Sources/Uniken/Services/RDNAService.swift
class RDNAService {
    static let shared = RDNAService()

    private let rdna = RDNA.sharedInstance()

    private init() {}

    /**
     * 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 callback will be triggered with detailed response
     * 3. The extension success/failure will be determined by the async callback response
     *
     * @returns RDNAError with synchronous validation result
     */
    func extendSessionIdleTimeout() -> RDNAError {
        print("RDNAService - Extending session idle timeout")

        let error = rdna.extendSessionIdleTimeout()

        if error.longErrorCode == 0 {
            print("RDNAService - Extension API call successful, waiting for onSessionExtensionResponse")
        } else {
            print("RDNAService - Extension API call failed: \(error.errorString ?? "")")
        }

        return error
    }
}

Important Session Extension Logic

When handling session extension, two response layers must be considered:

Response Layer

Purpose

Success Criteria

Failure Handling

Sync Response

API call validation

error.longErrorCode == 0

Immediate error alert

Async Callback

Extension result

status.statusCode == 100 & error.longErrorCode == 0

Display error message

Note: The synchronous response only indicates the API call was accepted. The actual extension success/failure is communicated through the onSessionExtensionResponse callback.

Create a singleton SessionManager to handle all session-related state and UI:

// Sources/Uniken/Session/SessionManager.swift
class SessionManager {
    static let shared = SessionManager()

    // MARK: - State Properties
    private weak var presentedAlert: SessionAlertViewController?
    private var isProcessingExtension = false

    // Operation tracking to prevent duplicate requests
    private enum Operation {
        case none
        case extending
    }
    private var currentOperation: Operation = .none

    private init() {}

    // MARK: - Setup

    func setupSessionHandlers() {
        let delegateManager = RDNADelegateManager.shared

        // Register hard timeout handler
        delegateManager.onSessionTimeout = { [weak self] message in
            self?.handleHardTimeout(message: message)
        }

        // Register idle timeout notification handler
        delegateManager.onSessionTimeOutNotification = { [weak self] userID, message, timeLeft, canExtend, info in
            self?.handleIdleTimeoutNotification(
                userID: userID,
                message: message,
                timeLeftInSeconds: timeLeft,
                sessionCanBeExtended: canExtend,
                info: info
            )
        }

        // Register session extension response handler
        delegateManager.onSessionExtensionResponse = { [weak self] userID, info, status, error in
            self?.handleSessionExtensionResponse(
                userID: userID,
                info: info,
                status: status,
                error: error
            )
        }
    }

    // MARK: - Event Handlers

    private func handleHardTimeout(message: String) {
        print("SessionManager - Hard session timeout")

        showTimeoutAlert(
            type: .hardTimeout,
            message: message,
            timeLeftInSeconds: 0,
            canExtend: false
        )
    }

    private func handleIdleTimeoutNotification(userID: String,
                                              message: String,
                                              timeLeftInSeconds: Int,
                                              sessionCanBeExtended: Int,
                                              info: RDNAGeneralInfo) {
        print("SessionManager - Idle session warning: \(timeLeftInSeconds)s remaining, canExtend: \(sessionCanBeExtended == 1)")

        showTimeoutAlert(
            type: .idleTimeout,
            message: message,
            timeLeftInSeconds: timeLeftInSeconds,
            canExtend: sessionCanBeExtended == 1
        )
    }

    private func handleSessionExtensionResponse(userID: String,
                                               info: RDNAGeneralInfo,
                                               status: RDNARequestStatus,
                                               error: RDNAError) {
        // Only process if we're currently extending
        guard currentOperation == .extending else {
            print("SessionManager - Extension response received but no extend operation in progress, ignoring")
            return
        }

        let isSuccess = error.longErrorCode == 0 && status.statusCode == 100

        if isSuccess {
            print("SessionManager - Session extension successful")
            dismissAlert()
        } else {
            print("SessionManager - Session extension failed: \(error.errorString ?? "")")

            let errorMessage = error.longErrorCode != 0
                ? error.errorString ?? "Unknown error"
                : status.statusMessage

            showErrorAlert(title: "Extension Failed", message: errorMessage)

            presentedAlert?.setProcessing(false)
            resetState()
        }
    }

    // MARK: - Extension Logic

    private func handleExtendSession() {
        print("SessionManager - User chose to extend session")

        // Prevent duplicate requests
        guard currentOperation == .none else {
            print("SessionManager - Operation already in progress, ignoring extend request")
            return
        }

        isProcessingExtension = true
        currentOperation = .extending
        presentedAlert?.setProcessing(true)

        // Call SDK extension API
        let error = RDNAService.shared.extendSessionIdleTimeout()

        if error.longErrorCode != 0 {
            // Immediate sync error
            showErrorAlert(
                title: "Extension Failed",
                message: error.errorString ?? "Unknown error"
            )
            presentedAlert?.setProcessing(false)
            resetState()
        }
        // Success case: wait for async onSessionExtensionResponse callback
    }

    private func handleDismissAlert(isHardTimeout: Bool) {
        print("SessionManager - User dismissed alert")

        dismissAlert()

        if isHardTimeout {
            // Navigate to home screen
            AppCoordinator.shared.showTutorialHome()
        }
    }

    // MARK: - UI Presentation

    private func showTimeoutAlert(type: SessionAlertViewController.AlertType,
                                 message: String,
                                 timeLeftInSeconds: Int,
                                 canExtend: Bool) {
        // Ensure we're on main thread
        guard Thread.isMainThread else {
            DispatchQueue.main.async {
                self.showTimeoutAlert(
                    type: type,
                    message: message,
                    timeLeftInSeconds: timeLeftInSeconds,
                    canExtend: canExtend
                )
            }
            return
        }

        // Dismiss existing alert if any
        dismissAlert()

        // Instantiate from storyboard
        guard let alert = UIStoryboard(name: "SessionAlert", bundle: nil)
            .instantiateViewController(withIdentifier: "SessionAlertViewController")
            as? SessionAlertViewController else {
            print("SessionManager - Failed to instantiate SessionAlertViewController")
            return
        }

        // Configure alert
        alert.configure(
            type: type,
            message: message,
            timeLeftInSeconds: timeLeftInSeconds,
            canExtend: canExtend,
            onExtend: { [weak self] in
                self?.handleExtendSession()
            },
            onDismiss: { [weak self] in
                self?.handleDismissAlert(isHardTimeout: type == .hardTimeout)
            }
        )

        // Present modally
        if let topViewController = getTopViewController() {
            alert.modalPresentationStyle = .overFullScreen
            alert.modalTransitionStyle = .crossDissolve
            topViewController.present(alert, animated: true)
            presentedAlert = alert
        }
    }

    private func dismissAlert() {
        presentedAlert?.dismiss(animated: true) { [weak self] in
            self?.presentedAlert = nil
            self?.resetState()
        }
    }

    private func resetState() {
        isProcessingExtension = false
        currentOperation = .none
    }

    private func showErrorAlert(title: String, message: String) {
        guard let topVC = getTopViewController() else { return }

        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        topVC.present(alert, animated: true)
    }

    private func getTopViewController() -> UIViewController? {
        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let window = windowScene.windows.first(where: { $0.isKeyWindow }),
              let rootVC = window.rootViewController else {
            return nil
        }

        var topVC = rootVC
        while let presented = topVC.presentedViewController {
            topVC = presented
        }
        return topVC
    }
}

Key Features:

Create the modal UI for displaying session information and handling user interactions:

// Sources/Uniken/Session/SessionAlertViewController.swift
import UIKit

class SessionAlertViewController: UIViewController {

    // MARK: - IBOutlets
    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var headerView: UIView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var countdownContainerView: UIView!
    @IBOutlet weak var countdownLabel: UILabel!
    @IBOutlet weak var extendButton: UIButton!
    @IBOutlet weak var dismissButton: UIButton!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    // MARK: - Alert Types
    enum AlertType {
        case hardTimeout    // Red, session expired - mandatory
        case idleTimeout    // Orange, session expiring - optional extend
    }

    // MARK: - Properties
    private var alertType: AlertType = .idleTimeout
    private var message: String = ""
    private var timeLeftInSeconds: Int = 0
    private var canExtend: Bool = false
    private var onExtend: (() -> Void)?
    private var onDismiss: (() -> Void)?

    // Countdown management
    private var countdown: Int = 0
    private var countdownTimer: Timer?
    private var backgroundTime: Date?

    // MARK: - Configuration

    func configure(type: AlertType,
                   message: String,
                   timeLeftInSeconds: Int,
                   canExtend: Bool,
                   onExtend: @escaping () -> Void,
                   onDismiss: @escaping () -> Void) {
        self.alertType = type
        self.message = message
        self.timeLeftInSeconds = timeLeftInSeconds
        self.canExtend = canExtend
        self.onExtend = onExtend
        self.onDismiss = onDismiss

        self.countdown = timeLeftInSeconds

        // Update UI if view is loaded
        loadViewIfNeeded()
        updateUIForAlertType()
        startCountdown()
    }

    func setProcessing(_ processing: Bool) {
        extendButton.isEnabled = !processing
        dismissButton.isEnabled = !processing

        if processing {
            extendButton.setTitle("Extending...", for: .normal)
            activityIndicator.startAnimating()
        } else {
            extendButton.setTitle("Extend Session", for: .normal)
            activityIndicator.stopAnimating()
        }
    }

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        updateUIForAlertType()
        setupBackgroundHandling()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        startCountdown()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        stopCountdown()
    }

    deinit {
        stopCountdown()
        NotificationCenter.default.removeObserver(self)
    }

    // MARK: - UI Setup

    private func setupUI() {
        // Container styling
        containerView.layer.cornerRadius = 16
        containerView.layer.shadowColor = UIColor.black.cgColor
        containerView.layer.shadowOffset = CGSize(width: 0, height: 4)
        containerView.layer.shadowOpacity = 0.3
        containerView.layer.shadowRadius = 8

        // Header styling
        headerView.layer.cornerRadius = 16
        headerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

        // Button styling
        extendButton.layer.cornerRadius = 8
        dismissButton.layer.cornerRadius = 8
        dismissButton.layer.borderWidth = 1
        dismissButton.layer.borderColor = UIColor.systemGray3.cgColor

        // Countdown container
        countdownContainerView.layer.cornerRadius = 8
        countdownContainerView.backgroundColor = UIColor.systemGray6
    }

    private func updateUIForAlertType() {
        switch alertType {
        case .hardTimeout:
            // Red theme for hard timeout
            headerView.backgroundColor = UIColor(red: 0.9, green: 0.2, blue: 0.2, alpha: 1.0)
            titleLabel.text = "🔐 Session Expired"
            subtitleLabel.text = "Your session has expired. You will be redirected to the home screen."
            countdownContainerView.isHidden = true
            extendButton.isHidden = true
            dismissButton.setTitle("Close", for: .normal)

        case .idleTimeout:
            // Orange theme for idle timeout
            headerView.backgroundColor = UIColor(red: 1.0, green: 0.6, blue: 0.0, alpha: 1.0)
            titleLabel.text = "⚠️ Session Timeout Warning"

            if canExtend {
                subtitleLabel.text = "Your session will expire soon. You can extend it or let it timeout."
                extendButton.isHidden = false
            } else {
                subtitleLabel.text = "Your session will expire soon."
                extendButton.isHidden = true
            }

            countdownContainerView.isHidden = false
            dismissButton.setTitle("Close", for: .normal)
        }

        messageLabel.text = message
        updateCountdownDisplay()
    }

    // MARK: - Countdown Management

    private func startCountdown() {
        stopCountdown()

        guard countdown > 0 else { return }

        countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            guard let self = self else { return }

            if self.countdown > 0 {
                self.countdown -= 1
                self.updateCountdownDisplay()
            } else {
                self.stopCountdown()
            }
        }
    }

    private func stopCountdown() {
        countdownTimer?.invalidate()
        countdownTimer = nil
    }

    private func updateCountdownDisplay() {
        let minutes = countdown / 60
        let seconds = countdown % 60
        countdownLabel.text = String(format: "%d:%02d", minutes, seconds)
    }

    // MARK: - Background/Foreground Handling

    private func setupBackgroundHandling() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidEnterBackground),
            name: UIApplication.didEnterBackgroundNotification,
            object: nil
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillEnterForeground),
            name: UIApplication.willEnterForegroundNotification,
            object: nil
        )
    }

    @objc private func appDidEnterBackground() {
        // Record the time when app goes to background
        backgroundTime = Date()
        print("SessionAlertViewController - App going to background, recording time")
    }

    @objc private func appWillEnterForeground() {
        // Calculate elapsed time and adjust countdown
        guard let backgroundTime = backgroundTime else { return }

        let elapsedSeconds = Int(Date().timeIntervalSince(backgroundTime))
        print("SessionAlertViewController - App returning to foreground, elapsed: \(elapsedSeconds)s")

        // Update countdown based on actual elapsed time
        countdown = max(0, countdown - elapsedSeconds)
        print("SessionAlertViewController - Countdown updated to: \(countdown)s")

        updateCountdownDisplay()
        self.backgroundTime = nil
    }

    // MARK: - Actions

    @IBAction func extendButtonTapped(_ sender: UIButton) {
        print("SessionAlertViewController - Extend button tapped")
        onExtend?()
    }

    @IBAction func dismissButtonTapped(_ sender: UIButton) {
        print("SessionAlertViewController - Dismiss button tapped")
        onDismiss?()
    }
}

Key iOS Patterns:

The following images showcase screens from the sample application:

Session Extend Screen

Session Timeout Screen

Initialize the session management system in your SceneDelegate:

// SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene,
              willConnectTo session: UISceneSession,
              options connectionOptions: UIScene.ConnectionOptions) {

        guard let windowScene = scene as? UIWindowScene else { return }

        // Create window and navigation controller
        let window = UIWindow(windowScene: windowScene)
        let navigationController = UINavigationController()
        window.rootViewController = navigationController
        self.window = window
        window.makeKeyAndVisible()

        // Configure app coordinator with navigation
        AppCoordinator.shared.configure(with: navigationController)

        // Setup session management handlers (SESSION-SPECIFIC)
        SessionManager.shared.setupSessionHandlers()

        // Start app flow
        AppCoordinator.shared.start()
    }
}

Integration with App Coordinator

The AppCoordinator handles navigation based on SDK callbacks:

// Sources/Tutorial/Navigation/AppCoordinator.swift
class AppCoordinator {
    static let shared = AppCoordinator()

    private weak var navigationController: UINavigationController?

    func configure(with navigationController: UINavigationController) {
        self.navigationController = navigationController
        setupGlobalCallbackNavigation()
    }

    private func setupGlobalCallbackNavigation() {
        let delegateManager = RDNADelegateManager.shared

        // MFA flow callbacks
        delegateManager.onGetUser = { [weak self] userNames, recentUser, response, error in
            self?.showCheckUser(userNames: userNames,
                               recentlyLoggedInUser: recentUser,
                               response: response,
                               error: error)
        }

        delegateManager.onUserLoggedIn = { [weak self] userID, response, error in
            self?.showDashboard(userID: userID, response: response, error: error)
        }
    }

    func showTutorialHome() {
        guard let vc = storyboard.instantiateViewController(
            withIdentifier: "TutorialHomeViewController"
        ) as? TutorialHomeViewController else { return }

        navigationController?.setViewControllers([vc], animated: true)
    }
}

Integration Benefits

This architecture provides several advantages:

Test Scenarios

  1. Hard Session Timeout: Test mandatory session expiration
  2. Idle Session Timeout with Extension: Test warning with successful extension
  3. Extension Failure: Test extension API failure handling
  4. Background/Foreground Timer: Test countdown accuracy across app states
  5. Multiple Session Events: Test handling of rapid session events

Common Session Management Test Scenarios

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

Testing Background/Foreground Accuracy

Critical test for production reliability:

// Test background/foreground timer accuracy
func testBackgroundTimer() {
    // 1. Trigger idle session timeout notification
    // 2. Note the countdown time (e.g., 60 seconds)
    // 3. Background the app for 30 seconds (press Home button)
    // 4. Foreground the app (tap app icon)
    // 5. Verify countdown shows ~30 seconds remaining

    print("Testing background timer accuracy")
    print("Initial countdown: \(initialCountdown)")
    print("Expected remaining after 30s background: \(initialCountdown - 30)")
}

Debugging Session Callbacks

Use these debugging techniques to verify session functionality:

// Verify callback registration in SessionManager.setupSessionHandlers()
print("Session callbacks registered:")
print("- Hard timeout handler: \(RDNADelegateManager.shared.onSessionTimeout != nil)")
print("- Idle notification handler: \(RDNADelegateManager.shared.onSessionTimeOutNotification != nil)")
print("- Extension response handler: \(RDNADelegateManager.shared.onSessionExtensionResponse != nil)")

// Log session event data
print("Session timeout notification received:")
print("- User ID: \(userID)")
print("- Time left: \(timeLeftInSeconds)s")
print("- Can extend: \(sessionCanBeExtended == 1)")
print("- Message: \(message)")

Using Xcode Debugger

Build and Run:

# Command line build
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphonesimulator

# Or press Cmd+R in Xcode

Debugging Tools:

Modal Not Appearing

Cause: Session callbacks not properly registered Solution: Verify SessionManager.shared.setupSessionHandlers() is called in SceneDelegate

// In SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
          options connectionOptions: UIScene.ConnectionOptions) {
    // ... window setup ...

    SessionManager.shared.setupSessionHandlers()  // CRITICAL
}

Cause: Closures not set on RDNADelegateManager Solution: Check that closure properties are assigned in setupSessionHandlers()

Timer Accuracy Issues

Cause: Countdown doesn't account for background time Solution: Implement NotificationCenter observers for app lifecycle

private func setupBackgroundHandling() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(appDidEnterBackground),
        name: UIApplication.didEnterBackgroundNotification,
        object: nil
    )

    NotificationCenter.default.addObserver(
        self,
        selector: #selector(appWillEnterForeground),
        name: UIApplication.willEnterForegroundNotification,
        object: nil
    )
}

@objc private func appDidEnterBackground() {
    backgroundTime = Date()
}

@objc private func appWillEnterForeground() {
    guard let backgroundTime = backgroundTime else { return }
    let elapsedSeconds = Int(Date().timeIntervalSince(backgroundTime))
    countdown = max(0, countdown - elapsedSeconds)
    updateCountdownDisplay()
    self.backgroundTime = nil
}

Extension API Failures

Cause: Calling extension API when sessionCanBeExtended is false Solution: Check extension eligibility before API call

private func handleExtendSession() {
    guard canExtend else {
        showErrorAlert(title: "Extension Not Available",
                      message: "This session cannot be extended.")
        return
    }

    let error = RDNAService.shared.extendSessionIdleTimeout()
    // ... handle error
}

Cause: Multiple concurrent extension requests Solution: Use operation tracking to prevent duplicates

private enum Operation {
    case none
    case extending
}

private var currentOperation: Operation = .none

private func handleExtendSession() {
    guard currentOperation == .none else {
        print("Extension already in progress")
        return
    }

    currentOperation = .extending
    // ... perform extension
}

Modal Dismissal Issues

Cause: Weak reference to alert view controller becomes nil Solution: Store strong reference during presentation, clear after dismissal

private weak var presentedAlert: SessionAlertViewController?  // Weak to avoid cycles

private func showTimeoutAlert(...) {
    // ... configure alert ...
    topViewController.present(alert, animated: true)
    presentedAlert = alert  // Store weak reference
}

private func dismissAlert() {
    presentedAlert?.dismiss(animated: true) { [weak self] in
        self?.presentedAlert = nil  // Clear reference
        self?.resetState()
    }
}

Memory Leaks

Cause: Retain cycles from closures Solution: Always use [weak self] in closure captures

// CORRECT
delegateManager.onSessionTimeout = { [weak self] message in
    self?.handleHardTimeout(message: message)
}

// WRONG - creates retain cycle
delegateManager.onSessionTimeout = { message in
    self.handleHardTimeout(message: message)
}

Best Practice: Test session management behavior on both physical devices and simulators with different timeout scenarios. Use Instruments' Leaks tool to verify no memory leaks.

Important Security Guidelines

  1. Never bypass session timeouts - Always respect hard timeout requirements
  2. Limit extension attempts - Implement reasonable limits on extension requests
  3. Log session events securely - Track session management for security analysis using os_log
  4. Keep SDK updated - Regular updates include latest session security features
  5. Test thoroughly - Verify session behavior across different usage patterns

Session Extension Guidelines

Extension Scenario

Recommended Action

Implementation

Frequent Extensions

Set reasonable limits

Track extension count per session in SessionManager

Critical Operations

Allow extensions during important tasks

Context-aware extension logic based on current screen

Inactive Sessions

Enforce timeouts

Don't extend completely idle sessions

Memory and Performance

deinit {
    // CRITICAL: Clean up observers
    NotificationCenter.default.removeObserver(self)

    // CRITICAL: Invalidate timers
    stopCountdown()

    // CRITICAL: Clear closures
    onExtend = nil
    onDismiss = nil

    print("SessionAlertViewController deallocated")
}

Session Data Protection

import os.log

private let logger = Logger(subsystem: "com.yourapp.session", category: "SessionManager")

func handleSessionEvent() {
    // Use appropriate log levels
    logger.info("Session event received")  // Info only
    logger.debug("Session details: \(privateData, privacy: .private)")  // Redacted in release
}

Congratulations! You've successfully learned how to implement comprehensive session management functionality with:

Next Steps