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 callbacks 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-ios.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 callback processing and navigation coordinationBefore implementing device activation screens, let's understand the key SDK callbacks 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 Callback → VerifyAuthViewController →
Push Notifications Sent → User Approves the Notification On Primary Device → Continue MFA Flow -> Device Activated
Understanding the SDK callback signatures is essential for device activation:
// RDNACallbacks Protocol - Device Activation Methods
/**
* Device activation options callback
* Triggered when SDK detects unregistered device during authentication
*/
func getAddNewDeviceOptions(
_ userID: String,
newDeviceOptions: [String],
challengeInfo: [RDNAChallengeInfo]
)
/**
* Get notifications response callback
* Triggered after getNotifications API call completes
*/
func onGetNotifications(_ status: RDNAStatusGetNotifications)
/**
* Update notification response callback
* Triggered after updateNotification API call completes
*/
func onUpdateNotification(_ status: RDNAStatusUpdateNotification)
The addNewDeviceOptions callback is the cornerstone of device activation:
userID: String // User identifier
newDeviceOptions: [String] // Available activation methods
challengeInfo: [RDNAChallengeInfo] // Challenge metadata
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:
// RDNAService.swift (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 RDNAError
*/
func performVerifyAuth(_ verifyAuthStatus: Bool) -> RDNAError {
print("RDNAService - Performing verify auth with status: \(verifyAuthStatus)")
let error = rdna.performVerifyAuth(verifyAuthStatus)
if error.longErrorCode == 0 {
print("RDNAService - PerformVerifyAuth successful")
} else {
print("RDNAService - PerformVerifyAuth failed: \(error.errorString)")
}
return error
}
/**
* Initiates fallback device activation flow
* Alternative method when REL-ID Verify is not available/accessible
* @returns RDNAError
*/
func fallbackNewDeviceActivationFlow() -> RDNAError {
print("RDNAService - Starting fallback new device activation flow")
let error = rdna.fallbackNewDeviceActivationFlow()
if error.longErrorCode == 0 {
print("RDNAService - FallbackNewDeviceActivationFlow successful, alternative activation started")
} else {
print("RDNAService - FallbackNewDeviceActivationFlow failed: \(error.errorString)")
}
return error
}
/**
* 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 RDNAError
*/
func getNotifications(
recordCount: Int = 0,
startIndex: Int = 1,
startDate: String = "",
endDate: String = ""
) -> RDNAError {
print("RDNAService - Fetching notifications with recordCount: \(recordCount), startIndex: \(startIndex)")
let error = rdna.getNotifications(
Int32(recordCount),
withEnterpriseID: "",
withStart: Int32(startIndex),
withStartDate: startDate,
withEndDate: endDate
)
if error.longErrorCode == 0 {
print("RDNAService - GetNotifications request successful, waiting for onGetNotifications callback")
} else {
print("RDNAService - GetNotifications failed: \(error.errorString)")
}
return error
}
/**
* 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 RDNAError
*/
func updateNotification(_ notificationID: String, response: String) -> RDNAError {
print("RDNAService - Updating notification: \(notificationID) with response: \(response)")
let error = rdna.updateNotification(notificationID, withResponse: response)
if error.longErrorCode == 0 {
print("RDNAService - UpdateNotification request successful, waiting for onUpdateNotification callback")
} else {
print("RDNAService - UpdateNotification failed: \(error.errorString)")
}
return error
}
verifyAuthStatus (Bool) - automatically start verificationAll device activation APIs follow the established REL-ID SDK pattern:
longErrorCode == 0 means API call succeededEnhance your existing delegate manager to handle device activation callbacks. Add support for addNewDeviceOptions, notification retrieval, and notification updates.
Extend your RDNADelegateManager class with device activation callback handling:
// RDNADelegateManager.swift (device activation additions)
class RDNADelegateManager: NSObject, RDNACallbacks {
static let shared = RDNADelegateManager()
// MARK: - Device Activation Closures
var onAddNewDeviceOptions: ((_ userID: String, _ options: [String],
_ info: [RDNAChallengeInfo]) -> Void)?
var onGetNotifications: ((RDNAStatusGetNotifications) -> Void)?
var onUpdateNotification: ((RDNAStatusUpdateNotification) -> Void)?
// MARK: - Device Activation Callback Implementations
/**
* Handles device activation options callback
* Triggered when SDK detects unregistered device during authentication
*/
func getAddNewDeviceOptions(
_ userID: String,
newDeviceOptions: [String],
challengeInfo: [RDNAChallengeInfo]
) {
print("RDNADelegateManager - Add new device options callback received")
print("RDNADelegateManager - UserID: \(userID)")
print("RDNADelegateManager - Available options: \(newDeviceOptions.count)")
print("RDNADelegateManager - Challenge info count: \(challengeInfo.count)")
// Log each activation option for debugging
newDeviceOptions.enumerated().forEach { index, option in
print("RDNADelegateManager - Option \(index + 1): \(option)")
}
DispatchQueue.main.async { [weak self] in
self?.onAddNewDeviceOptions?(userID, newDeviceOptions, challengeInfo)
}
}
/**
* Handles get notifications response
* Triggered after getNotifications API call completes
*/
func onGetNotifications(_ status: RDNAStatusGetNotifications) {
print("RDNADelegateManager - Get notifications callback received")
// Parse notification data
let notificationCount = status.notificationResponse?.notifications?.count ?? 0
print("RDNADelegateManager - Notification count: \(notificationCount)")
DispatchQueue.main.async { [weak self] in
self?.onGetNotifications?(status)
}
}
/**
* Handles update notification response
* Triggered after updateNotification API call completes
*/
func onUpdateNotification(_ status: RDNAStatusUpdateNotification) {
print("RDNADelegateManager - Update notification callback received")
print("RDNADelegateManager - Status code: \(status.statusCode)")
print("RDNADelegateManager - Status message: \(status.statusMessage ?? "")")
DispatchQueue.main.async { [weak self] in
self?.onUpdateNotification?(status)
}
}
}
Add cleanup methods to properly manage device activation callbacks:
// Enhanced cleanup method to clear device activation handlers
func clearDeviceActivationHandlers() {
onAddNewDeviceOptions = nil
onGetNotifications = nil
onUpdateNotification = nil
}
// Enhanced cleanup method to clear all handlers
func cleanup() {
// Clear existing MFA handlers
clearActivationHandlers()
// Clear device activation handlers
clearDeviceActivationHandlers()
// Clear existing MTD handlers
clearMTDHandlers()
}
getNotifications() API callupdateNotification() API callThe device activation callbacks integrate with existing event management:
// Example of comprehensive callback setup in AppCoordinator
func setupGlobalCallbackNavigation() {
let delegateManager = RDNADelegateManager.shared
// Existing MFA callback handlers
delegateManager.onGetUser = { [weak self] userNames, recentUser, response, error in
self?.showCheckUser(userNames: userNames, recentlyLoggedInUser: recentUser,
response: response, error: error)
}
delegateManager.onGetPassword = { [weak self] userID, challengeMode, attemptsLeft, response, error in
self?.showPasswordScreen(userID: userID, challengeMode: challengeMode,
attemptsLeft: attemptsLeft, response: response, error: error)
}
// Device activation callback handlers
delegateManager.onAddNewDeviceOptions = { [weak self] userID, options, info in
self?.showVerifyAuth(userID: userID, options: options, info: info)
}
delegateManager.onGetNotifications = { [weak self] status in
// GetNotificationsViewController handles this directly
print("AppCoordinator - Get notifications callback handled by screen")
}
delegateManager.onUpdateNotification = { [weak self] status in
// GetNotificationsViewController handles this directly
print("AppCoordinator - Update notification callback handled by screen")
}
}
Create the VerifyAuthViewController that handles REL-ID Verify device activation with automatic push notification processing and fallback options.
// VerifyAuthViewController.swift
import UIKit
class VerifyAuthViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subtitleLabel: UILabel!
@IBOutlet weak var messageContainer: UIView!
@IBOutlet weak var messageTitle: UILabel!
@IBOutlet weak var messageText: UILabel!
@IBOutlet weak var fallbackContainer: UIView!
@IBOutlet weak var fallbackTitle: UILabel!
@IBOutlet weak var fallbackDescription: UILabel!
@IBOutlet weak var fallbackButton: UIButton!
@IBOutlet weak var processingContainer: UIView!
@IBOutlet weak var processingLabel: UILabel!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var resultContainer: UIView!
@IBOutlet weak var resultLabel: UILabel!
@IBOutlet weak var closeButton: UIButton!
// MARK: - Properties
var responseData: (userID: String, options: [String], info: [RDNAChallengeInfo])?
private var isProcessing: Bool = false {
didSet {
updateProcessingState()
}
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
processActivationData()
}
// MARK: - Setup
private func setupUI() {
view.backgroundColor = UIColor(hex: "#f8f9fa")
// Title
titleLabel.text = "Additional Device Activation"
titleLabel.font = .boldSystemFont(ofSize: 28)
titleLabel.textColor = UIColor(hex: "#2c3e50")
subtitleLabel.text = "Activate this device for secure access"
subtitleLabel.font = .systemFont(ofSize: 16)
subtitleLabel.textColor = UIColor(hex: "#7f8c8d")
// Close button
closeButton.setTitle("✕", for: .normal)
closeButton.titleLabel?.font = .systemFont(ofSize: 28)
closeButton.setTitleColor(UIColor(hex: "#95a5a6"), for: .normal)
closeButton.layer.cornerRadius = 20
closeButton.backgroundColor = .clear
// Message container
messageContainer.backgroundColor = UIColor(hex: "#e3f2fd")
messageContainer.layer.cornerRadius = 12
messageContainer.layer.borderWidth = 4
messageContainer.layer.borderColor = UIColor(hex: "#2196f3")?.cgColor
messageTitle.text = "REL-ID Verify Authentication"
messageTitle.font = .boldSystemFont(ofSize: 18)
messageTitle.textColor = UIColor(hex: "#1976d2")
messageText.text = "REL-ID Verify notification has been sent to your registered devices. Please approve it to activate this device."
messageText.font = .systemFont(ofSize: 16)
messageText.textColor = UIColor(hex: "#1565c0")
messageText.numberOfLines = 0
// Fallback container
fallbackContainer.backgroundColor = UIColor(hex: "#f5f5f5")
fallbackContainer.layer.cornerRadius = 12
fallbackContainer.layer.borderWidth = 1
fallbackContainer.layer.borderColor = UIColor(hex: "#e0e0e0")?.cgColor
fallbackTitle.text = "Device Not Handy?"
fallbackTitle.font = .boldSystemFont(ofSize: 18)
fallbackTitle.textColor = UIColor(hex: "#2c3e50")
fallbackDescription.text = "If you don't have access to your registered devices, you can use an alternative activation method."
fallbackDescription.font = .systemFont(ofSize: 14)
fallbackDescription.textColor = UIColor(hex: "#7f8c8d")
fallbackDescription.numberOfLines = 0
fallbackButton.setTitle("Activate using fallback method", for: .normal)
fallbackButton.titleLabel?.font = .boldSystemFont(ofSize: 16)
fallbackButton.setTitleColor(UIColor(hex: "#3498db"), for: .normal)
fallbackButton.layer.cornerRadius = 8
fallbackButton.layer.borderWidth = 2
fallbackButton.layer.borderColor = UIColor(hex: "#3498db")?.cgColor
fallbackButton.backgroundColor = .white
// Processing container
processingContainer.backgroundColor = UIColor(hex: "#e3f2fd")
processingContainer.layer.cornerRadius = 12
processingContainer.isHidden = true
processingLabel.text = "Processing device activation..."
processingLabel.font = .systemFont(ofSize: 16)
processingLabel.textColor = UIColor(hex: "#1976d2")
// Result container (initially hidden)
resultContainer.isHidden = true
resultContainer.layer.cornerRadius = 12
}
private func processActivationData() {
guard let data = responseData else {
showResult("No activation data available", isSuccess: false)
return
}
print("VerifyAuthViewController - Processing activation data")
print("VerifyAuthViewController - UserID: \(data.userID)")
print("VerifyAuthViewController - Options: \(data.options)")
// Automatically call performVerifyAuth(true) when data is processed
handleVerifyAuth(true)
}
// MARK: - Actions
@IBAction func closeButtonTapped(_ sender: UIButton) {
handleClose()
}
@IBAction func fallbackButtonTapped(_ sender: UIButton) {
handleFallbackFlow()
}
private func handleClose() {
print("VerifyAuthViewController - Calling resetAuthState")
let error = RDNAService.shared.resetAuthState()
if error.longErrorCode == 0 {
print("VerifyAuthViewController - ResetAuthState successful")
} else {
print("VerifyAuthViewController - ResetAuthState error: \(error.errorString)")
}
}
// MARK: - Device Activation Methods
private func handleVerifyAuth(_ proceed: Bool) {
guard !isProcessing else { return }
isProcessing = true
hideResult()
print("VerifyAuthViewController - Performing verify auth: \(proceed)")
let error = RDNAService.shared.performVerifyAuth(proceed)
if error.longErrorCode != 0 {
// Sync error
print("VerifyAuthViewController - PerformVerifyAuth sync error: \(error.errorString)")
showResult(error.errorString ?? "Failed to start verification", isSuccess: false)
isProcessing = false
} else {
// Success - waiting for async events
print("VerifyAuthViewController - PerformVerifyAuth successful, waiting for async events")
if proceed {
print("VerifyAuthViewController - REL-ID Verify notification has been sent to registered devices")
}
}
}
private func handleFallbackFlow() {
guard !isProcessing else { return }
isProcessing = true
hideResult()
print("VerifyAuthViewController - Initiating fallback new device activation flow")
let error = RDNAService.shared.fallbackNewDeviceActivationFlow()
if error.longErrorCode != 0 {
// Sync error
print("VerifyAuthViewController - FallbackNewDeviceActivationFlow sync error: \(error.errorString)")
showResult(error.errorString ?? "Failed to start fallback activation", isSuccess: false)
isProcessing = false
} else {
// Success - waiting for async events
print("VerifyAuthViewController - FallbackNewDeviceActivationFlow successful, waiting for async events")
print("VerifyAuthViewController - Alternative device activation process has been initiated")
}
}
// MARK: - UI Updates
private func updateProcessingState() {
processingContainer.isHidden = !isProcessing
fallbackButton.isEnabled = !isProcessing
fallbackButton.alpha = isProcessing ? 0.5 : 1.0
closeButton.isEnabled = !isProcessing
if isProcessing {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
private func showResult(_ message: String, isSuccess: Bool) {
resultLabel.text = message
if isSuccess {
resultContainer.backgroundColor = UIColor(hex: "#f0f8f0")
resultLabel.textColor = UIColor(hex: "#27ae60")
} else {
resultContainer.backgroundColor = UIColor(hex: "#fff0f0")
resultLabel.textColor = UIColor(hex: "#e74c3c")
}
resultContainer.isHidden = false
}
private func hideResult() {
resultContainer.isHidden = true
}
}
performVerifyAuth(true) when screen loadsresetAuthState()The following image showcases screen from the sample application:

Create the GetNotificationsViewController that automatically loads server notifications and provides interactive action modals for user responses.
// GetNotificationsViewController.swift
import UIKit
struct NotificationItem {
let notificationUUID: String
let subject: String
let message: String
let createTimestamp: String
let expiryTimestamp: String?
let actionPerformed: String?
let actions: [NotificationAction]
let createEpoch: TimeInterval
struct NotificationAction {
let action: String
let label: String
}
}
class GetNotificationsViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subtitleLabel: UILabel!
@IBOutlet weak var userInfoLabel: UILabel!
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var emptyStateView: UIView!
@IBOutlet weak var emptyTitleLabel: UILabel!
@IBOutlet weak var emptyMessageLabel: UILabel!
@IBOutlet weak var refreshButton: UIButton!
@IBOutlet weak var loadingContainer: UIView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var backButton: UIButton!
// MARK: - Properties
var userID: String = ""
var sessionID: String = ""
var sessionType: Int = 0
var jwtToken: String = ""
private var notifications: [NotificationItem] = []
private var isLoading: Bool = false {
didSet {
updateLoadingState()
}
}
private var selectedNotification: NotificationItem?
private var selectedAction: String = ""
private var isProcessingAction: Bool = false
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupTableView()
setupCallbacks()
loadNotifications()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
clearCallbacks()
}
// MARK: - Setup
private func setupUI() {
view.backgroundColor = UIColor(hex: "#f5f5f5")
// Title
titleLabel.text = "Notifications"
titleLabel.font = .boldSystemFont(ofSize: 24)
titleLabel.textColor = UIColor(hex: "#333333")
subtitleLabel.text = "Manage your REL-ID notifications"
subtitleLabel.font = .systemFont(ofSize: 16)
subtitleLabel.textColor = UIColor(hex: "#666666")
userInfoLabel.text = "User: \(userID)"
userInfoLabel.font = .systemFont(ofSize: 14, weight: .medium)
userInfoLabel.textColor = UIColor(hex: "#007AFF")
// Back button
backButton.setTitle("←", for: .normal)
backButton.titleLabel?.font = .systemFont(ofSize: 32)
backButton.setTitleColor(UIColor(hex: "#007AFF"), for: .normal)
// Empty state
emptyTitleLabel.text = "No Notifications"
emptyTitleLabel.font = .boldSystemFont(ofSize: 18)
emptyTitleLabel.textColor = UIColor(hex: "#333333")
emptyMessageLabel.text = "You don't have any notifications at the moment."
emptyMessageLabel.font = .systemFont(ofSize: 14)
emptyMessageLabel.textColor = UIColor(hex: "#666666")
emptyMessageLabel.numberOfLines = 0
refreshButton.setTitle("Refresh", for: .normal)
refreshButton.titleLabel?.font = .boldSystemFont(ofSize: 16)
refreshButton.setTitleColor(UIColor(hex: "#007AFF"), for: .normal)
refreshButton.layer.cornerRadius = 8
refreshButton.layer.borderWidth = 2
refreshButton.layer.borderColor = UIColor(hex: "#007AFF")?.cgColor
refreshButton.backgroundColor = .white
emptyStateView.isHidden = true
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
tableView.register(NotificationCell.self, forCellReuseIdentifier: "NotificationCell")
}
private func setupCallbacks() {
let delegateManager = RDNADelegateManager.shared
// Set up notification callback handlers
delegateManager.onGetNotifications = { [weak self] status in
self?.handleGetNotificationsResponse(status)
}
delegateManager.onUpdateNotification = { [weak self] status in
self?.handleUpdateNotificationResponse(status)
}
}
private func clearCallbacks() {
let delegateManager = RDNADelegateManager.shared
delegateManager.onGetNotifications = nil
delegateManager.onUpdateNotification = nil
}
// MARK: - Actions
@IBAction func backButtonTapped(_ sender: UIButton) {
navigationController?.popViewController(animated: true)
}
@IBAction func refreshButtonTapped(_ sender: UIButton) {
loadNotifications()
}
// MARK: - Notifications Loading
private func loadNotifications() {
guard !isLoading else { return }
isLoading = true
emptyStateView.isHidden = true
print("GetNotificationsViewController - Loading notifications for user: \(userID)")
let error = RDNAService.shared.getNotifications()
if error.longErrorCode != 0 {
print("GetNotificationsViewController - Error loading notifications: \(error.errorString)")
isLoading = false
showAlert(title: "Error", message: error.errorString ?? "Failed to load notifications")
} else {
print("GetNotificationsViewController - GetNotifications API called, waiting for response")
}
}
private func handleGetNotificationsResponse(_ status: RDNAStatusGetNotifications) {
print("GetNotificationsViewController - Received notifications callback")
isLoading = false
// Parse notification response
guard let notificationResponse = status.notificationResponse,
let notificationsList = notificationResponse.notifications else {
print("GetNotificationsViewController - No notifications available")
notifications = []
updateEmptyState()
return
}
print("GetNotificationsViewController - Received \(notificationsList.count) notifications")
// Convert to NotificationItem objects
notifications = notificationsList.compactMap { notification in
guard let bodies = notification.body, let firstBody = bodies.first,
let subject = firstBody.subject,
let message = firstBody.message else {
return nil
}
let actions = (notification.actions ?? []).compactMap { action -> NotificationItem.NotificationAction? in
guard let actionLabel = action.label,
let actionValue = action.action else {
return nil
}
return NotificationItem.NotificationAction(action: actionValue, label: actionLabel)
}
return NotificationItem(
notificationUUID: notification.notification_uuid ?? "",
subject: subject,
message: message,
createTimestamp: notification.create_ts ?? "",
expiryTimestamp: notification.expiry_timestamp,
actionPerformed: notification.action_performed,
actions: actions,
createEpoch: TimeInterval(notification.create_ts_epoch)
)
}
// Sort by create epoch (most recent first)
notifications.sort { $0.createEpoch > $1.createEpoch }
updateEmptyState()
tableView.reloadData()
}
// MARK: - Notification Actions
private func openActionModal(for notification: NotificationItem) {
guard !notification.actions.isEmpty else {
showAlert(title: "No Actions", message: "This notification has no available actions.")
return
}
guard notification.actionPerformed == nil || notification.actionPerformed?.isEmpty == true else {
showAlert(title: "Already Processed", message: "This notification has already been processed.")
return
}
selectedNotification = notification
selectedAction = ""
// Show action selection modal
let alertController = UIAlertController(
title: "Notification Actions",
message: notification.subject,
preferredStyle: .actionSheet
)
for action in notification.actions {
let alertAction = UIAlertAction(title: action.label, style: .default) { [weak self] _ in
self?.selectedAction = action.action
self?.executeNotificationAction()
}
alertController.addAction(alertAction)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alertController.addAction(cancelAction)
present(alertController, animated: true)
}
private func executeNotificationAction() {
guard let notification = selectedNotification,
!selectedAction.isEmpty else {
return
}
isProcessingAction = true
print("GetNotificationsViewController - Processing notification action:")
print(" notificationID: \(notification.notificationUUID)")
print(" action: \(selectedAction)")
let error = RDNAService.shared.updateNotification(notification.notificationUUID, response: selectedAction)
if error.longErrorCode != 0 {
print("GetNotificationsViewController - Error processing action: \(error.errorString)")
isProcessingAction = false
showAlert(title: "Error", message: error.errorString ?? "Failed to process notification action")
} else {
print("GetNotificationsViewController - UpdateNotification API called, waiting for response")
}
}
private func handleUpdateNotificationResponse(_ status: RDNAStatusUpdateNotification) {
print("GetNotificationsViewController - Received update notification callback")
isProcessingAction = false
let isSuccess = status.statusCode == 100
if isSuccess {
print("GetNotificationsViewController - Update notification success")
showAlert(title: "Success", message: status.statusMessage ?? "Notification updated successfully") {
[weak self] in
self?.loadNotifications()
}
} else {
print("GetNotificationsViewController - Update notification failed: \(status.statusMessage ?? "")")
showAlert(title: "Update Failed", message: status.statusMessage ?? "Failed to update notification")
}
}
// MARK: - UI Updates
private func updateLoadingState() {
if isLoading {
loadingContainer.isHidden = false
activityIndicator.startAnimating()
} else {
loadingContainer.isHidden = true
activityIndicator.stopAnimating()
}
}
private func updateEmptyState() {
let isEmpty = notifications.isEmpty
emptyStateView.isHidden = !isEmpty
tableView.isHidden = isEmpty
}
private func showAlert(title: String, message: String, completion: (() -> Void)? = nil) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?()
})
present(alert, animated: true)
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension GetNotificationsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return notifications.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationCell", for: indexPath) as! NotificationCell
let notification = notifications[indexPath.row]
cell.configure(with: notification)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let notification = notifications[indexPath.row]
openActionModal(for: notification)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
// MARK: - NotificationCell
class NotificationCell: UITableViewCell {
private let containerView = UIView()
private let subjectLabel = UILabel()
private let messageLabel = UILabel()
private let timestampLabel = UILabel()
private let actionsLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
backgroundColor = .clear
selectionStyle = .none
// Container view
containerView.backgroundColor = .white
containerView.layer.cornerRadius = 12
containerView.layer.shadowColor = UIColor.black.cgColor
containerView.layer.shadowOffset = CGSize(width: 0, height: 1)
containerView.layer.shadowOpacity = 0.05
containerView.layer.shadowRadius = 2
contentView.addSubview(containerView)
// Subject label
subjectLabel.font = .boldSystemFont(ofSize: 16)
subjectLabel.textColor = UIColor(hex: "#333333")
subjectLabel.numberOfLines = 1
containerView.addSubview(subjectLabel)
// Message label
messageLabel.font = .systemFont(ofSize: 14)
messageLabel.textColor = UIColor(hex: "#666666")
messageLabel.numberOfLines = 3
containerView.addSubview(messageLabel)
// Timestamp label
timestampLabel.font = .systemFont(ofSize: 12)
timestampLabel.textColor = UIColor(hex: "#8E8E93")
containerView.addSubview(timestampLabel)
// Actions label
actionsLabel.font = .systemFont(ofSize: 12, weight: .medium)
actionsLabel.textColor = UIColor(hex: "#007AFF")
containerView.addSubview(actionsLabel)
setupConstraints()
}
private func setupConstraints() {
containerView.translatesAutoresizingMaskIntoConstraints = false
subjectLabel.translatesAutoresizingMaskIntoConstraints = false
messageLabel.translatesAutoresizingMaskIntoConstraints = false
timestampLabel.translatesAutoresizingMaskIntoConstraints = false
actionsLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// Container view
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6),
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6),
// Subject label
subjectLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16),
subjectLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
subjectLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
// Timestamp label
timestampLabel.topAnchor.constraint(equalTo: subjectLabel.bottomAnchor, constant: 4),
timestampLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
timestampLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
// Message label
messageLabel.topAnchor.constraint(equalTo: timestampLabel.bottomAnchor, constant: 8),
messageLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
messageLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
// Actions label
actionsLabel.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 12),
actionsLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
actionsLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
actionsLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16)
])
}
func configure(with notification: NotificationItem) {
subjectLabel.text = notification.subject
messageLabel.text = notification.message
// Format timestamp
if let createDate = ISO8601DateFormatter().date(from: notification.createTimestamp.replacingOccurrences(of: "UTC", with: "Z")) {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
timestampLabel.text = formatter.string(from: createDate)
} else {
timestampLabel.text = notification.createTimestamp
}
// Actions count
let actionCount = notification.actions.count
actionsLabel.text = "\(actionCount) action\(actionCount != 1 ? "s" : "") available"
// Gray out if already processed
if let actionPerformed = notification.actionPerformed, !actionPerformed.isEmpty {
containerView.alpha = 0.6
actionsLabel.text = "Processed: \(actionPerformed)"
} else {
containerView.alpha = 1.0
}
}
}
getNotifications() when screen loadsonGetNotifications and onUpdateNotification callbacksThe following images showcase screens from the sample application:
|
|
|
Extend your existing AppCoordinator to handle device activation callbacks and coordinate navigation for the additional device activation workflow.
Enhance your AppCoordinator with device activation navigation coordination:
// AppCoordinator.swift (device activation additions)
class AppCoordinator {
static let shared = AppCoordinator()
private weak var navigationController: UINavigationController?
private let storyboard = UIStoryboard(name: "Main", bundle: nil)
// MARK: - Device Activation Navigation Methods
func showVerifyAuth(userID: String, options: [String], info: [RDNAChallengeInfo]) {
print("AppCoordinator - Showing VerifyAuthViewController for user: \(userID)")
print("AppCoordinator - Available options: \(options.count)")
guard let vc = storyboard.instantiateViewController(
withIdentifier: "VerifyAuthViewController"
) as? VerifyAuthViewController else {
print("AppCoordinator - Failed to instantiate VerifyAuthViewController")
return
}
vc.responseData = (userID: userID, options: options, info: info)
navigationController?.pushViewController(vc, animated: true)
}
func showGetNotifications(userID: String, sessionID: String, sessionType: Int, jwtToken: String) {
print("AppCoordinator - Showing GetNotificationsViewController")
guard let vc = storyboard.instantiateViewController(
withIdentifier: "GetNotificationsViewController"
) as? GetNotificationsViewController else {
print("AppCoordinator - Failed to instantiate GetNotificationsViewController")
return
}
vc.userID = userID
vc.sessionID = sessionID
vc.sessionType = sessionType
vc.jwtToken = jwtToken
navigationController?.pushViewController(vc, animated: true)
}
// MARK: - Enhanced Global Callback Navigation Setup
private func setupGlobalCallbackNavigation() {
let delegateManager = RDNADelegateManager.shared
// Existing MFA callback handlers
delegateManager.onInitialized = { [weak self] response in
print("AppCoordinator - onInitialized called")
// Wait for getUser callback
}
delegateManager.onGetUser = { [weak self] userNames, recentlyLoggedInUser, response, error in
print("AppCoordinator - onGetUser called")
self?.showCheckUser(
userNames: userNames,
recentlyLoggedInUser: recentlyLoggedInUser,
response: response,
error: error
)
}
delegateManager.onGetActivationCode = { [weak self] userID, verificationKey, attemptsLeft, response, error in
print("AppCoordinator - onGetActivationCode called")
self?.showActivationCode(
userID: userID,
verificationKey: verificationKey,
attemptsLeft: attemptsLeft,
response: response,
error: error
)
}
delegateManager.onGetPassword = { [weak self] userID, challengeMode, attemptsLeft, response, error in
print("AppCoordinator - onGetPassword called (mode: \(challengeMode.rawValue))")
self?.showPasswordScreen(
userID: userID,
challengeMode: challengeMode,
attemptsLeft: attemptsLeft,
response: response,
error: error
)
}
delegateManager.onUserLoggedIn = { [weak self] userID, response, error in
print("AppCoordinator - onUserLoggedIn called")
self?.showDashboard(userID: userID, response: response, error: error)
}
// Device activation callback handlers
delegateManager.onAddNewDeviceOptions = { [weak self] userID, options, info in
print("AppCoordinator - onAddNewDeviceOptions called")
print("AppCoordinator - UserID: \(userID)")
print("AppCoordinator - Options: \(options)")
self?.showVerifyAuth(userID: userID, options: options, info: info)
}
delegateManager.onGetNotifications = { [weak self] status in
print("AppCoordinator - onGetNotifications called")
// GetNotificationsViewController handles this directly
print("AppCoordinator - Get notifications callback handled by screen")
}
delegateManager.onUpdateNotification = { [weak self] status in
print("AppCoordinator - onUpdateNotification called")
// GetNotificationsViewController handles this directly
print("AppCoordinator - Update notification callback handled by screen")
}
// MTD threats
delegateManager.onTerminateWithThreats = { [weak self] threats in
print("AppCoordinator - onTerminateWithThreats called")
self?.showSecurityExit()
}
}
}
Update your DashboardViewController to include navigation to GetNotifications:
// DashboardViewController.swift (enhanced with GetNotifications)
class DashboardViewController: UIViewController {
@IBOutlet weak var menuButton: UIButton!
@IBOutlet weak var userInfoValue: UILabel!
@IBOutlet weak var sessionIDValue: UILabel!
@IBOutlet weak var sessionTypeValue: UILabel!
// MARK: - Properties
private var userID: String = ""
private var response: RDNAChallengeResponse?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
MenuViewController.setupSideMenu(for: self)
updateSessionInfo()
}
// MARK: - Navigation to GetNotifications
func navigateToNotifications() {
print("DashboardViewController - Navigating to GetNotifications")
guard let response = response else {
print("DashboardViewController - No response data available")
return
}
let sessionInfo = response.sessionInfo
AppCoordinator.shared.showGetNotifications(
userID: userID,
sessionID: sessionInfo.sessionID,
sessionType: Int(sessionInfo.sessionType.rawValue),
jwtToken: ""
)
}
@IBAction func menuTapped(_ sender: UIButton) {
MenuViewController.presentMenu(from: self)
}
}
The enhanced AppCoordinator coordinates these device activation flows:
addNewDeviceOptionsTest 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
# For Device A:
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphoneos -destination 'platform=iOS,name=Device-A-Name'
# For Device B:
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphoneos -destination 'platform=iOS,name=Device-B-Name'
addNewDeviceOptions callbackperformVerifyAuth(true) called automaticallyAppCoordinator - onAddNewDeviceOptions called
AppCoordinator - UserID: testuser@example.com
AppCoordinator - Available options: 2
VerifyAuthViewController - Auto-starting REL-ID Verify for user: testuser@example.com
RDNAService - PerformVerifyAuth successful
Test the fallback activation when REL-ID Verify is not accessible:
fallbackNewDeviceActivationFlow() calledVerifyAuthViewController - Starting fallback activation for user: testuser@example.com
RDNAService - FallbackNewDeviceActivationFlow successful
Test the GetNotificationsViewController functionality:
getNotifications() API called again// Check if device is already registered
// Verify MFA flow completion before device detection
// Ensure proper connection profile configuration
// Check GetNotificationsViewController callback handler setup
delegateManager.onGetNotifications = { [weak self] status in
self?.handleGetNotificationsResponse(status)
}
// Verify API call execution
let error = RDNAService.shared.getNotifications()
addNewDeviceOptions callback 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
✅ VerifyAuthViewController Implementation: Auto-starting activation with real-time status updates
✅ Fallback Activation Methods: Alternative activation when registered devices aren't accessible
✅ GetNotificationsViewController: Server notification management with interactive action processing
✅ Enhanced Navigation: Seamless access to notifications via enhanced menu 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!