🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Additional Device Activation Flow Codelab
  3. You are here → Forgot Password Flow Implementation

Welcome to the REL-ID Forgot Password codelab! This tutorial builds upon your existing MFA implementation to add secure password recovery capabilities using REL-ID SDK's verification challenge.

What You'll Build

In this codelab, you'll enhance your existing MFA application with:

What You'll Learn

By completing this codelab, you'll master:

  1. Forgot Password API Integration: Implementing forgotPassword() API with proper sync response handling
  2. Conditional UI Logic: Display forgot password based on challengeMode and ENABLE_FORGOT_PASSWORD configuration
  3. Verification Challenge Handling: Managing getActivationCode callbacks for OTP/email verification
  4. Dynamic Post-Verification Flow: Navigate through LDA consent or direct password reset paths
  5. Complete Event Chain Management: Orchestrate forgot password → verification → reset → login sequences
  6. Production Security Patterns: Implement secure password recovery with comprehensive error handling

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

The code to get started can be found 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-forgot-password folder in the repository you cloned earlier

Codelab Architecture Overview

This codelab extends your MFA application with three core forgot password components:

  1. Enhanced VerifyPasswordViewController: Conditional forgot password button with proper challenge mode detection
  2. Forgot Password API Integration: Service layer implementation following established SDK patterns
  3. Event Chain Management: Complete forgot password event sequence handling with navigation coordination

Before implementing forgot password functionality, let's understand the key SDK events and APIs that power the password recovery workflow.

Forgot Password Event Flow

The password recovery process follows this event-driven pattern:

VerifyPasswordViewController (challengeMode=VERIFY and ENABLE_FORGOT_PASSWORD=true) → forgotPassword() API → getActivationCode Callback →
User Enters OTP → setActivationCode() API → getUserConsentForLDA/getPassword Callback →
Password Reset Complete → onUserLoggedIn Callback → Dashboard

Core Forgot Password Event Types

The REL-ID SDK triggers these main events during forgot password flow:

Event Type

Description

User Action Required

getActivationCode

Verification challenge triggered after forgotPassword()

User enters OTP/verification code

getUserConsentForLDA

LDA setup required after verification (Path A)

User approves biometric authentication setup

getPassword

Direct password reset required (Path B)

User creates new password with policy validation

onUserLoggedIn

Automatic login after successful password reset

System navigates to dashboard automatically

Conditional Display Logic

Forgot password functionality requires specific conditions:

// Forgot password display conditions
challengeMode == .CHALLENGE_OP_VERIFY AND ENABLE_FORGOT_PASSWORD == "true"

Condition

Description

Display Forgot Password

challengeMode = .CHALLENGE_OP_VERIFY

Manual password entry mode

✅ Required condition

challengeMode = .CHALLENGE_OP_SET

Password creation mode

❌ Not applicable

ENABLE_FORGOT_PASSWORD = "true"

Server feature enabled

✅ Required configuration

ENABLE_FORGOT_PASSWORD = "false"

Server feature disabled

❌ Hide forgot password button

Forgot Password API Pattern

Add these Swift definitions to understand the forgot password API structure:

// Sources/Uniken/Services/RDNAService.swift (forgot password addition)

/**
 * Initiates forgot password flow for password reset
 * @param userId User ID for the forgot password flow
 * @returns RDNAError with longErrorCode 0 for success, > 0 for error
 */
func forgotPassword(_ userId: String) -> RDNAError {
    let error = rdna.forgotPassword(userId)
    return error
}

Let's implement the forgot password API in your service layer following established REL-ID SDK patterns.

Enhance RDNAService.swift with Forgot Password

Add the forgot password method to your existing service implementation:

// Sources/Uniken/Services/RDNAService.swift (addition to existing class)

/**
 * Initiates forgot password flow for password reset
 *
 * This method initiates the forgot password flow when challengeMode == CHALLENGE_OP_VERIFY
 * and ENABLE_FORGOT_PASSWORD is true. It triggers a verification challenge followed by
 * password reset process. Can only be used on an active device and requires user verification.
 *
 * @see https://developer.uniken.com/docs/forgot-password
 *
 * Workflow:
 * 1. User initiates forgot password
 * 2. SDK triggers verification challenge (e.g., activation code, email OTP)
 * 3. User completes challenge
 * 4. SDK validates challenge
 * 5. User sets new password
 * 6. SDK logs user in automatically
 *
 * Response Validation Logic:
 * 1. Check error.longErrorCode: 0 = success, > 0 = error
 * 2. Success typically starts verification challenge flow
 * 3. Error Code 170 = Feature not supported
 * 4. Async events will be handled by RDNADelegateManager callbacks
 *
 * @param userId User ID for the forgot password flow
 * @returns RDNAError with longErrorCode indicating success/failure
 */
func forgotPassword(_ userId: String) -> RDNAError {
    print("RDNAService - Initiating forgot password flow for userId: \(userId)")

    let error = rdna.forgotPassword(userId)

    if error.longErrorCode == 0 {
        print("RDNAService - ForgotPassword sync response success, starting verification challenge")
    } else {
        print("RDNAService - ForgotPassword sync response error: \(error.errorString ?? "Unknown error")")
    }

    return error
}

Service Pattern Consistency

Notice how this implementation follows the exact pattern established by other service methods:

Pattern Element

Implementation Detail

Synchronous Call

Direct SDK method call with immediate RDNAError response

Error Checking

Validates longErrorCode == 0 for success

Logging Strategy

Comprehensive console logging for debugging

Error Handling

Returns RDNAError for caller to handle

Now let's enhance your VerifyPasswordViewController to display forgot password functionality conditionally based on challenge mode and server configuration.

Add Conditional Display Logic

Implement the logic to determine when forgot password should be available:

// Sources/Tutorial/Screens/MFA/VerifyPasswordViewController.swift (additions)

/**
 * Check if forgot password is enabled from challenge info
 * According to documentation: Show "Forgot Password" only when:
 * - challengeMode is CHALLENGE_OP_VERIFY (manual password entry)
 * - ENABLE_FORGOT_PASSWORD is true
 */
private func isForgotPasswordEnabled() -> Bool {
    // Must be VERIFY mode (challengeMode = 0)
    if challengeMode != .CHALLENGE_OP_VERIFY {
        return false
    }

    // Check for ENABLE_FORGOT_PASSWORD in response.info (if available)
    if let response = response,
       let challengeInfo = response.info.first(where: { $0.infoKey == "ENABLE_FORGOT_PASSWORD" }) {
        return challengeInfo.infoMessage.lowercased() == "true"
    }

    // Default to true for challengeMode VERIFY if configuration is not available
    // This maintains backward compatibility
    // This can be handled on the app side as well if not configured on the server
    return true
}

Add State Management for Forgot Password

Enhance your view controller's state management to handle forgot password loading:

// Sources/Tutorial/Screens/MFA/VerifyPasswordViewController.swift (property additions)

private var isSubmitting = false
private var isForgotPasswordLoading = false

/**
 * Check if any operation is in progress
 */
private func isLoading() -> Bool {
    return isSubmitting || isForgotPasswordLoading
}

Implement Forgot Password Handler

Add the forgot password handling logic with proper error management:

// Sources/Tutorial/Screens/MFA/VerifyPasswordViewController.swift (handler implementation)

/**
 * Handle forgot password flow
 */
@IBAction func forgotPasswordTapped(_ sender: UIButton) {
    print("VerifyPasswordViewController - Forgot Password button tapped")

    if isForgotPasswordLoading || isSubmitting { return }

    isForgotPasswordLoading = true
    hideResult()

    print("VerifyPasswordViewController - Initiating forgot password flow for userID: \(userID)")

    // Call SDK forgotPassword method
    let error = RDNAService.shared.forgotPassword(userID)

    print("VerifyPasswordViewController - Sync response received: longErrorCode=\(error.longErrorCode), errorString=\(error.errorString ?? "nil")")

    if error.longErrorCode != 0 {
        // This handles sync response errors
        if error.errorCode == .ERR_FEATURE_OR_OPERATION_NOT_SUPPORTED {
            showResult("Forgot Password feature is not enabled on the server", isSuccess: false)
        } else {
            showResult(error.errorString ?? "Failed to initiate forgot password", isSuccess: false)
        }
        isForgotPasswordLoading = false
        return
    }

    // Success case - SDK will trigger getActivationCode callback
    showResult("Password reset initiated. Please check for verification code.", isSuccess: true)
    isForgotPasswordLoading = false

    // No navigation needed here - RDNADelegateManager handles the callback
    // and AppCoordinator will navigate to ActivationCodeViewController
}

Add Conditional UI Setup

Implement the forgot password button visibility with proper loading states:

// Sources/Tutorial/Screens/MFA/VerifyPasswordViewController.swift (UI setup)

private func setupUI() {
    // ... existing UI setup code ...

    // Configure forgot password button visibility
    updateForgotPasswordButtonVisibility()
}

private func updateForgotPasswordButtonVisibility() {
    forgotPasswordButton.isHidden = !isForgotPasswordEnabled()
}

private func updateLoadingState() {
    let loading = isLoading()

    passwordTextField.isEnabled = !loading
    submitButton.isEnabled = !loading && !passwordTextField.text!.isEmpty
    forgotPasswordButton.isEnabled = !loading

    if isForgotPasswordLoading {
        forgotPasswordButton.setTitle("Initiating password reset...", for: .normal)
        // You can add an activity indicator here if desired
    } else {
        forgotPasswordButton.setTitle("Forgot Password?", for: .normal)
    }
}

The forgot password flow involves a sequence of events that your delegate manager must handle properly. Let's ensure your event handling supports the complete flow.

Callback Chain Overview

After calling forgotPassword(), the SDK triggers this event sequence:

// Complete forgot password event flow
forgotPassword() → getActivationCode → getUserConsentForLDA/getPassword → onUserLoggedIn

Verify RDNADelegateManager Configuration

Ensure your RDNADelegateManager.swift has proper handlers for all forgot password events:

// Sources/Uniken/Services/RDNADelegateManager.swift (verify these handlers exist)

/**
 * Handle activation code request (triggered after forgotPassword)
 */
func getActivationCode(_ userID: String, verificationKey: String, attemptsLeft: Int32,
                      response: RDNAChallengeResponse, error: RDNAError) {
    DispatchQueue.main.async { [weak self] in
        print("DelegateManager - getActivationCode triggered for forgot password flow")
        self?.onGetActivationCode?(
            userID,
            verificationKey,
            Int(attemptsLeft),
            response,
            error
        )
    }
}

/**
 * Handle LDA consent request (one possible path after verification)
 */
func getUserConsentForLDA(_ userID: String, attemptsLeft: Int32,
                         response: RDNAChallengeResponse, error: RDNAError) {
    DispatchQueue.main.async { [weak self] in
        print("DelegateManager - getUserConsentForLDA triggered in forgot password flow")
        self?.onGetUserConsentForLDA?(
            userID,
            Int(attemptsLeft),
            response,
            error
        )
    }
}

/**
 * Handle password reset request (alternative path after verification)
 */
func getPassword(_ userID: String, challenge mode: RDNAChallengeOpMode,
                attemptsLeft: Int32, response: RDNAChallengeResponse, error: RDNAError) {
    DispatchQueue.main.async { [weak self] in
        print("DelegateManager - getPassword triggered in forgot password flow (mode: \(mode.rawValue))")
        self?.onGetPassword?(
            userID,
            mode,
            Int(attemptsLeft),
            response,
            error
        )
    }
}

/**
 * Handle successful login (final step of forgot password flow)
 */
func userLoggedIn(_ userID: String, response: RDNAChallengeResponse, error: RDNAError) {
    DispatchQueue.main.async { [weak self] in
        print("DelegateManager - userLoggedIn triggered - forgot password flow complete")
        self?.onUserLoggedIn?(
            userID,
            response,
            error
        )
    }
}

Verify AppCoordinator Navigation

Ensure your AppCoordinator.swift has proper navigation setup for forgot password events:

// Sources/Tutorial/Navigation/AppCoordinator.swift (navigation setup)

private func setupGlobalCallbackNavigation() {
    // ... existing callback setup ...

    RDNADelegateManager.shared.onGetActivationCode = { [weak self] userID, verificationKey, attemptsLeft, response, error in
        print("AppCoordinator - onGetActivationCode called (forgot password verification)")
        self?.showActivationCode(
            userID: userID,
            verificationKey: verificationKey,
            attemptsLeft: attemptsLeft,
            response: response,
            error: error
        )
    }

    RDNADelegateManager.shared.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
        )
    }

    RDNADelegateManager.shared.onUserLoggedIn = { [weak self] userID, response, error in
        print("AppCoordinator - onUserLoggedIn called - navigation to dashboard")
        self?.showDashboard(userID: userID, response: response, error: error)
    }
}

Your AppCoordinator already handles forgot password navigation through the existing MFA callbacks. Let's verify the configuration supports the flow properly.

Verify Navigation Configuration

Your AppCoordinator should already have these navigation methods from the MFA codelab:

// Sources/Tutorial/Navigation/AppCoordinator.swift (verify these exist)

func showActivationCode(userID: String, verificationKey: String, attemptsLeft: Int,
                       response: RDNAChallengeResponse, error: RDNAError) {
    // Check if already on top (avoid duplicate navigation)
    if let existingVC = navigationController?.topViewController as? ActivationCodeViewController {
        existingVC.reconfigure(
            userID: userID,
            verificationKey: verificationKey,
            attemptsLeft: attemptsLeft,
            response: response,
            error: error
        )
        return
    }

    guard let vc = storyboard.instantiateViewController(
        withIdentifier: "ActivationCodeViewController"
    ) as? ActivationCodeViewController else {
        print("Failed to instantiate ActivationCodeViewController")
        return
    }

    vc.configure(
        userID: userID,
        verificationKey: verificationKey,
        attemptsLeft: attemptsLeft,
        response: response,
        error: error
    )
    navigationController?.pushViewController(vc, animated: true)
}

func showPasswordScreen(userID: String, challengeMode: RDNAChallengeOpMode,
                       attemptsLeft: Int, response: RDNAChallengeResponse, error: RDNAError) {
    // Check for existing view controller to avoid duplicates
    if let existingVC = navigationController?.topViewController as? VerifyPasswordViewController {
        existingVC.reconfigure(
            userID: userID,
            challengeMode: challengeMode,
            attemptsLeft: attemptsLeft,
            response: response,
            error: error
        )
        return
    }

    if let existingVC = navigationController?.topViewController as? SetPasswordViewController {
        existingVC.reconfigure(
            userID: userID,
            challengeMode: challengeMode,
            attemptsLeft: attemptsLeft,
            response: response,
            error: error
        )
        return
    }

    let storyboardID = (challengeMode == .CHALLENGE_OP_VERIFY)
        ? "VerifyPasswordViewController"
        : "SetPasswordViewController"

    guard let vc = storyboard.instantiateViewController(
        withIdentifier: storyboardID
    ) as? UIViewController else {
        print("Failed to instantiate password view controller")
        return
    }

    if let verifyVC = vc as? VerifyPasswordViewController {
        verifyVC.configure(
            userID: userID,
            challengeMode: challengeMode,
            attemptsLeft: attemptsLeft,
            response: response,
            error: error
        )
    } else if let setVC = vc as? SetPasswordViewController {
        setVC.configure(
            userID: userID,
            challengeMode: challengeMode,
            attemptsLeft: attemptsLeft,
            response: response,
            error: error
        )
    }

    navigationController?.pushViewController(vc, animated: true)
}

func showDashboard(userID: String, response: RDNAChallengeResponse, error: RDNAError) {
    guard let vc = storyboard.instantiateViewController(
        withIdentifier: "DashboardViewController"
    ) as? DashboardViewController else {
        print("Failed to instantiate DashboardViewController")
        return
    }

    vc.configure(userID: userID, response: response, error: error)
    navigationController?.setViewControllers([vc], animated: true)
}

Navigation Flow Patterns

Key patterns used in forgot password navigation:

Pattern

Purpose

Check existing top controller

Prevent duplicate navigation when SDK retries

reconfigure() method

Update existing screen instead of pushing new one

configure() method

Set up new screen before pushing

setViewControllers for dashboard

Clear navigation stack on successful login

Let's test your forgot password implementation with comprehensive scenarios to ensure proper functionality.

Test Scenario 1: Standard Forgot Password Flow

Setup Requirements:

Test Steps:

  1. Launch app and navigate to VerifyPasswordViewController
  2. Verify "Forgot Password?" button is visible
  3. Tap forgot password button
  4. Verify loading state displays: "Initiating password reset..."
  5. Complete OTP verification when ActivationCodeViewController appears
  6. Follow either LDA consent or password reset path
  7. Confirm automatic login to dashboard

Expected Results:

Test Scenario 2: Forgot Password Disabled

Setup Requirements:

Test Steps:

  1. Navigate to VerifyPasswordViewController
  2. Verify forgot password button is NOT visible
  3. Confirm only standard password verification is available

Expected Results:

Test Scenario 3: Wrong Challenge Mode

Setup Requirements:

Test Steps:

  1. Navigate to VerifyPasswordViewController with challengeMode = .CHALLENGE_OP_SET
  2. Verify forgot password button is NOT visible
  3. Confirm only password creation flow is available

Expected Results:

Prepare your forgot password implementation for production deployment with these essential considerations.

Security Validation Checklist

User Experience Optimization

Memory Management

Here's your complete reference implementation combining all the patterns and best practices covered in this codelab.

Enhanced VerifyPasswordViewController with Forgot Password

// Sources/Tutorial/Screens/MFA/VerifyPasswordViewController.swift (complete implementation)

import UIKit

class VerifyPasswordViewController: UIViewController {

    // MARK: - IBOutlets
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    @IBOutlet weak var userIDLabel: UILabel!
    @IBOutlet weak var attemptsLabel: UILabel!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var submitButton: UIButton!
    @IBOutlet weak var forgotPasswordButton: UIButton!
    @IBOutlet weak var resultBanner: UIView!
    @IBOutlet weak var resultLabel: UILabel!

    // MARK: - Properties
    private var userID: String = ""
    private var challengeMode: RDNAChallengeOpMode = .CHALLENGE_OP_VERIFY
    private var attemptsLeft: Int = 0
    private var response: RDNAChallengeResponse?
    private var error: RDNAError?

    private var isSubmitting = false
    private var isForgotPasswordLoading = false

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupTextField()
        updateInfoLabels()
        checkAndDisplayErrors()
    }

    // MARK: - Configuration Methods

    func configure(userID: String, challengeMode: RDNAChallengeOpMode, attemptsLeft: Int,
                  response: RDNAChallengeResponse, error: RDNAError) {
        self.userID = userID
        self.challengeMode = challengeMode
        self.attemptsLeft = attemptsLeft
        self.response = response
        self.error = error

        // Update UI if view is already loaded
        if isViewLoaded {
            updateInfoLabels()
            checkAndDisplayErrors()
            updateForgotPasswordButtonVisibility()
        }
    }

    func reconfigure(userID: String, challengeMode: RDNAChallengeOpMode, attemptsLeft: Int,
                    response: RDNAChallengeResponse, error: RDNAError) {
        // Same as configure but called for SDK retries
        self.userID = userID
        self.challengeMode = challengeMode
        self.attemptsLeft = attemptsLeft
        self.response = response
        self.error = error

        updateInfoLabels()
        checkAndDisplayErrors()
        updateForgotPasswordButtonVisibility()
        passwordTextField.text = ""
    }

    // MARK: - Setup Methods

    private func setupUI() {
        titleLabel.text = "Verify Password"
        subtitleLabel.text = "Enter your password to continue"

        submitButton.layer.cornerRadius = 8
        forgotPasswordButton.layer.cornerRadius = 8

        resultBanner.layer.cornerRadius = 8
        resultBanner.isHidden = true

        updateForgotPasswordButtonVisibility()
    }

    private func setupTextField() {
        passwordTextField.delegate = self
        passwordTextField.isSecureTextEntry = true
        passwordTextField.placeholder = "Enter your password"
        passwordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
    }

    private func updateInfoLabels() {
        userIDLabel.text = "User: \(userID)"

        if attemptsLeft > 0 {
            attemptsLabel.text = "\(attemptsLeft) attempt\(attemptsLeft > 1 ? "s" : "") remaining"
            attemptsLabel.textColor = attemptsLeft <= 2 ? .systemRed : .systemOrange
            attemptsLabel.isHidden = false
        } else {
            attemptsLabel.isHidden = true
        }
    }

    private func checkAndDisplayErrors() {
        guard let error = error else { return }

        if error.longErrorCode != 0 {
            showResult(error.errorString ?? "An error occurred", isSuccess: false)
        }
    }

    // MARK: - Forgot Password Logic

    /**
     * Check if forgot password is enabled from challenge info
     */
    private func isForgotPasswordEnabled() -> Bool {
        // Must be VERIFY mode
        if challengeMode != .CHALLENGE_OP_VERIFY { return false }

        // Check for ENABLE_FORGOT_PASSWORD in response.info
        if let response = response,
           let challengeInfo = response.info.first(where: { $0.infoKey == "ENABLE_FORGOT_PASSWORD" }) {
            return challengeInfo.infoMessage.lowercased() == "true"
        }

        // Default to true for VERIFY mode if configuration is not available
        return true
    }

    private func updateForgotPasswordButtonVisibility() {
        forgotPasswordButton.isHidden = !isForgotPasswordEnabled()
    }

    private func isLoading() -> Bool {
        return isSubmitting || isForgotPasswordLoading
    }

    private func updateLoadingState() {
        let loading = isLoading()

        passwordTextField.isEnabled = !loading
        submitButton.isEnabled = !loading && !passwordTextField.text!.isEmpty
        forgotPasswordButton.isEnabled = !loading

        if isForgotPasswordLoading {
            forgotPasswordButton.setTitle("Initiating password reset...", for: .normal)
        } else {
            forgotPasswordButton.setTitle("Forgot Password?", for: .normal)
        }
    }

    // MARK: - Actions

    @IBAction func submitTapped(_ sender: UIButton) {
        guard let password = passwordTextField.text, !password.trimmingCharacters(in: .whitespaces).isEmpty else {
            showResult("Please enter your password", isSuccess: false)
            return
        }

        if isSubmitting { return }

        isSubmitting = true
        hideResult()
        updateLoadingState()

        print("VerifyPasswordViewController - Submitting password")

        // Call SDK setPassword method
        let error = RDNAService.shared.setPassword(password, challengeMode: challengeMode)

        if error.longErrorCode != 0 {
            showResult(error.errorString ?? "Password verification failed", isSuccess: false)
            passwordTextField.text = ""
        } else {
            showResult("Password verified successfully", isSuccess: true)
        }

        isSubmitting = false
        updateLoadingState()
    }

    @IBAction func forgotPasswordTapped(_ sender: UIButton) {
        print("VerifyPasswordViewController - Forgot Password button tapped")

        if isForgotPasswordLoading || isSubmitting { return }

        isForgotPasswordLoading = true
        hideResult()
        updateLoadingState()

        print("VerifyPasswordViewController - Initiating forgot password flow for userID: \(userID)")

        // Call SDK forgotPassword method
        let error = RDNAService.shared.forgotPassword(userID)

        print("VerifyPasswordViewController - Sync response received: longErrorCode=\(error.longErrorCode), errorString=\(error.errorString ?? "nil")")

        if error.longErrorCode != 0 {
            if error.errorCode == .ERR_FEATURE_OR_OPERATION_NOT_SUPPORTED {
                showResult("Forgot Password feature is not enabled on the server", isSuccess: false)
            } else {
                showResult(error.errorString ?? "Failed to initiate forgot password", isSuccess: false)
            }
            isForgotPasswordLoading = false
            updateLoadingState()
            return
        }

        // Success case - SDK will trigger getActivationCode callback
        showResult("Password reset initiated. Please check for verification code.", isSuccess: true)
        isForgotPasswordLoading = false
        updateLoadingState()

        // No navigation needed here - RDNADelegateManager handles callback
    }

    @IBAction func closeTapped(_ sender: UIButton) {
        // Reset auth state and navigate back
        let error = RDNAService.shared.resetAuthState()
        if error.longErrorCode == 0 {
            AppCoordinator.shared.showTutorialHome()
        }
    }

    // MARK: - Helper Methods

    private func showResult(_ message: String, isSuccess: Bool) {
        resultLabel.text = message
        resultBanner.backgroundColor = isSuccess ? UIColor.systemGreen.withAlphaComponent(0.2) : UIColor.systemRed.withAlphaComponent(0.2)
        resultLabel.textColor = isSuccess ? .systemGreen : .systemRed
        resultBanner.isHidden = false

        if !isSuccess {
            view.shake()
        }
    }

    private func hideResult() {
        resultBanner.isHidden = true
    }

    @objc private func textFieldDidChange() {
        updateLoadingState()
    }
}

// MARK: - UITextFieldDelegate

extension VerifyPasswordViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        submitTapped(submitButton)
        return true
    }
}

The following image showcases the screen from the sample application:

Forgot Password Screen

Congratulations! You've successfully implemented secure forgot password functionality with the REL-ID SDK.

🚀 What You've Accomplished

Conditional Forgot Password UI - Smart display logic based on challenge mode and server configuration ✅ Secure API Integration - Proper forgotPassword() implementation with error handling ✅ Event Chain Management - Complete flow from verification to password reset to login ✅ Production-Ready Code - Comprehensive error handling, loading states, and security practices ✅ User Experience Excellence - Clear feedback, intuitive flow, and accessibility support

📚 Additional Resources

🔐 You've mastered secure password recovery with REL-ID SDK!

Your implementation provides users with a seamless, secure way to recover their accounts while maintaining the highest security standards. Use this foundation to build robust authentication experiences that users can trust.