🎯 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 events 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-flutter.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 VerifyPasswordScreen: Conditional forgot password link 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:

VerifyPasswordScreen (challengeMode=0 and ENABLE_FORGOT_PASSWORD=true) → forgotPassword() API → getActivationCode Event →
User Enters OTP → setActivationCode() API → getUserConsentForLDA/getPassword Event →
Password Reset Complete → onUserLoggedIn Event → 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 == 0 AND ENABLE_FORGOT_PASSWORD == "true"

Condition

Description

Display Forgot Password

challengeMode = 0

Manual password entry mode

✅ Required condition

challengeMode = 1

Password creation mode

❌ Not applicable

ENABLE_FORGOT_PASSWORD = "true"

Server feature enabled

✅ Required configuration

ENABLE_FORGOT_PASSWORD = "false"

Server feature disabled

❌ Hide forgot password link

Forgot Password API Pattern

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

// lib/uniken/services/rdna_service.dart (forgot password addition)

/// Initiates forgot password flow for password reset
///
/// ## Parameters
/// - [userId]: Optional user ID for the forgot password flow
///
/// ## Returns
/// Future<RDNASyncResponse> that resolves with sync response structure
Future<RDNASyncResponse> forgotPassword([String? userId]) async {
  print('RdnaService - Initiating forgot password flow for userId: ${userId ?? "current user"}');

  final response = await _rdnaClient.forgotPassword(userId);

  if (response.error?.longErrorCode == 0) {
    print('RdnaService - ForgotPassword sync success, starting verification challenge');
    return response;
  } else {
    print('RdnaService - ForgotPassword sync error: ${response.error?.errorString}');
    throw response;
  }
}

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

Enhance rdnaService.dart with Forgot Password

Add the forgot password method to your existing service implementation:

// lib/uniken/services/rdna_service.dart (addition to existing class)

/// Initiates forgot password flow for password reset
///
/// This method initiates the forgot password flow when challengeMode == 0 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 Also
/// 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 event listeners
///
/// ## Parameters
/// - [userId]: Optional user ID for the forgot password flow
///
/// ## Returns
/// Future<RDNASyncResponse> that resolves with sync response structure
Future<RDNASyncResponse> forgotPassword([String? userId]) async {
  print('RdnaService - Initiating forgot password flow for userId: ${userId ?? "current user"}');

  final response = await _rdnaClient.forgotPassword(userId);

  print('RdnaService - ForgotPassword sync response received');
  print('RdnaService - Sync response:');
  print('  Long Error Code: ${response.error?.longErrorCode}');
  print('  Short Error Code: ${response.error?.shortErrorCode}');

  return response;
}

Service Pattern Consistency

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

Pattern Element

Implementation Detail

Async/Await

Uses Flutter's async/await for asynchronous operations

Error Checking

Validates longErrorCode == 0 for success

Logging Strategy

Comprehensive print statements for debugging

Direct Return

Returns response directly - caller handles success/error

Now let's enhance your VerifyPasswordScreen 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:

// lib/tutorial/screens/mfa/verify_password_screen.dart (additions)

/// Check if forgot password is enabled from challenge info
/// Show "Forgot Password" only when:
/// - challengeMode is 0 (manual password entry)
/// - ENABLE_FORGOT_PASSWORD is true
bool _isForgotPasswordEnabled() {
  if (widget.eventData?.challengeMode != 0) return false;

  // Check for ENABLE_FORGOT_PASSWORD in challenge info
  final challengeInfo = widget.eventData?.challengeResponse?.challengeInfo;
  if (challengeInfo != null) {
    final enableForgotPassword = challengeInfo.firstWhere(
      (info) => info.key == 'ENABLE_FORGOT_PASSWORD',
      orElse: () => RDNAChallengeInfo(key: '', value: 'false'),
    ).value;
    return enableForgotPassword?.toLowerCase() == 'true';
  }

  // Default to true for challengeMode 0 if configuration is not available
  // This maintains backward compatibility
  return true;
}

Add State Management for Forgot Password

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

// lib/tutorial/screens/mfa/verify_password_screen.dart (state additions)

class _VerifyPasswordScreenState extends ConsumerState<VerifyPasswordScreen> {
  final _passwordController = TextEditingController();
  String? _error;
  bool _isSubmitting = false;
  bool _isForgotPasswordLoading = false;
  String? _userId;
  int? _attemptsLeft;

  /// Check if any operation is in progress
  bool _isAnyOperationInProgress() {
    return _isSubmitting || _isForgotPasswordLoading;
  }
}

Implement Forgot Password Handler

Add the forgot password handling logic with proper error management:

// lib/tutorial/screens/mfa/verify_password_screen.dart (handler implementation)

/// Handle forgot password flow
Future<void> _handleForgotPassword() async {
  if (_isForgotPasswordLoading || _isSubmitting) return;

  setState(() {
    _isForgotPasswordLoading = true;
    _error = null;
    _validationMessage = null;
  });

  print('VerifyPasswordScreen - Initiating forgot password flow for userID: $_userId');

  final rdnaService = RdnaService.getInstance();
  final response = await rdnaService.forgotPassword(_userId);

  print('VerifyPasswordScreen - ForgotPassword sync response received');
  print('  Long Error Code: ${response.error?.longErrorCode}');
  print('  Short Error Code: ${response.error?.shortErrorCode}');

  if (response.error?.longErrorCode == 0) {
    // Success - SDK will handle async events (getActivationCode, getPassword, etc.)
    print('VerifyPasswordScreen - Forgot password initiated, waiting for async events');
    setState(() {
      _validationSuccess = true;
      _validationMessage = 'Password reset initiated successfully!';
      _isForgotPasswordLoading = false;
    });
  } else {
    setState(() {
      _error = response.error?.errorString ?? 'Failed to initiate forgot password';
      _isForgotPasswordLoading = false;
    });
  }
}

Add Conditional UI Rendering

Implement the forgot password link with proper loading states:

// Forgot Password Link - Only show when challengeMode == 0 and ENABLE_FORGOT_PASSWORD is true
if (_isForgotPasswordEnabled()) ...[
  const SizedBox(height: 16),
  if (_isForgotPasswordLoading)
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: const [
        SizedBox(
          width: 16,
          height: 16,
          child: CircularProgressIndicator(
            strokeWidth: 2,
            valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF3498db)),
          ),
        ),
        SizedBox(width: 12),
        Text(
          'Initiating password reset...',
          style: TextStyle(
            fontSize: 16,
            color: Color(0xFF3498db),
            fontWeight: FontWeight.w500,
          ),
        ),
      ],
    )
  else
    TextButton(
      onPressed: _isAnyOperationInProgress() ? null : _handleForgotPassword,
      child: Text(
        'Forgot Password?',
        style: TextStyle(
          fontSize: 16,
          color: _isAnyOperationInProgress()
              ? const Color(0xFFbdc3c7)
              : const Color(0xFF3498db),
          decoration: TextDecoration.underline,
          fontWeight: FontWeight.w500,
        ),
      ),
    ),
],

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

Event Chain Overview

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

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

Verify Event Manager Configuration

Ensure your rdnaEventManager.dart has proper handlers for all forgot password events:

// lib/uniken/services/rdna_event_manager.dart (verify these handlers exist)

/// Handle activation code request (triggered after forgotPassword)
void setGetActivationCodeHandler(RDNAGetActivationCodeCallback? callback) {
  _getActivationCodeHandler = callback;
}

void _onGetActivationCode(dynamic activationCodeData) {
  final data = activationCodeData as RDNAGetActivationCode;
  print('EventManager - onGetActivationCode triggered for forgot password flow');

  if (_getActivationCodeHandler != null) {
    _getActivationCodeHandler!(data);
  }
}

/// Handle LDA consent request (one possible path after verification)
void setGetUserConsentForLDAHandler(RDNAGetUserConsentForLDACallback? callback) {
  _getUserConsentForLDAHandler = callback;
}

void _onGetUserConsentForLDA(dynamic ldaConsentData) {
  final data = ldaConsentData as RDNAGetUserConsentForLDA;
  print('EventManager - onGetUserConsentForLDA triggered in forgot password flow');

  if (_getUserConsentForLDAHandler != null) {
    _getUserConsentForLDAHandler!(data);
  }
}

/// Handle password reset request (alternative path after verification)
void setGetPasswordHandler(RDNAGetPasswordCallback? callback) {
  _getPasswordHandler = callback;
}

void _onGetPassword(dynamic passwordData) {
  final data = passwordData as RDNAGetPassword;
  print('EventManager - onGetPassword triggered in forgot password flow');
  print('  Challenge Mode: ${data.challengeMode}');

  if (_getPasswordHandler != null) {
    _getPasswordHandler!(data);
  }
}

/// Handle successful login (final step of forgot password flow)
void setUserLoggedInHandler(RDNAUserLoggedInCallback? callback) {
  _userLoggedInHandler = callback;
}

void _onUserLoggedIn(dynamic loggedInData) {
  final data = loggedInData as RDNAUserLoggedIn;
  print('EventManager - onUserLoggedIn triggered - forgot password flow complete');

  if (_userLoggedInHandler != null) {
    _userLoggedInHandler!(data);
  }
}

Ensure your navigation system properly handles the forgot password event chain with type-safe route management.

Enhance Navigation Routes

Update your navigation configuration to support forgot password parameters:

// lib/tutorial/navigation/app_router.dart (route enhancements)

final appRouter = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/verify-password',
      name: 'verifyPasswordScreen',
      builder: (context, state) {
        final data = state.extra as RDNAGetPassword?;
        return VerifyPasswordScreen(eventData: data);
      },
    ),
    GoRoute(
      path: '/activation-code',
      name: 'activationCodeScreen',
      builder: (context, state) {
        final data = state.extra as RDNAGetActivationCode?;
        return ActivationCodeScreen(eventData: data);
      },
    ),
    GoRoute(
      path: '/set-password',
      name: 'setPasswordScreen',
      builder: (context, state) {
        final data = state.extra as RDNAGetPassword?;
        return SetPasswordScreen(eventData: data);
      },
    ),
    GoRoute(
      path: '/lda-consent',
      name: 'ldaConsentScreen',
      builder: (context, state) {
        final data = state.extra as RDNAGetUserConsentForLDA?;
        return UserLDAConsentScreen(eventData: data);
      },
    ),
    GoRoute(
      path: '/dashboard',
      name: 'dashboardScreen',
      builder: (context, state) {
        final data = state.extra as RDNAUserLoggedIn?;
        return DashboardScreen(eventData: data);
      },
    ),
  ],
);

SDK Event Provider Integration

Ensure proper event routing in your global SDK event provider:

// lib/uniken/providers/sdk_event_provider.dart

class _SDKEventProviderWidgetState extends ConsumerState<SDKEventProviderWidget> {
  @override
  void initState() {
    super.initState();
    _setupSDKEventHandlers();
  }

  void _setupSDKEventHandlers() {
    final rdnaService = RdnaService.getInstance();
    final eventManager = rdnaService.getEventManager();

    // Navigation handlers for forgot password event chain
    eventManager.setGetActivationCodeHandler(_handleGetActivationCode);
    eventManager.setGetPasswordHandler(_handleGetPassword);
    eventManager.setGetUserConsentForLDAHandler(_handleGetUserConsentForLDA);
    eventManager.setUserLoggedInHandler(_handleUserLoggedIn);
  }

  void _handleGetActivationCode(RDNAGetActivationCode data) {
    appRouter.goNamed('activationCodeScreen', extra: data);
  }

  void _handleGetPassword(RDNAGetPassword data) {
    // Route based on challengeMode
    if (data.challengeMode == 0) {
      appRouter.goNamed('verifyPasswordScreen', extra: data);
    } else {
      appRouter.goNamed('setPasswordScreen', extra: data);
    }
  }

  void _handleGetUserConsentForLDA(RDNAGetUserConsentForLDA data) {
    appRouter.goNamed('ldaConsentScreen', extra: data);
  }

  void _handleUserLoggedIn(RDNAUserLoggedIn data) {
    appRouter.goNamed('dashboardScreen', extra: data);
  }
}

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 VerifyPasswordScreen
  2. Verify "Forgot Password?" link is visible
  3. Tap forgot password link
  4. Verify loading state displays: "Initiating password reset..."
  5. Complete OTP verification when ActivationCodeScreen 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 VerifyPasswordScreen
  2. Verify forgot password link 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 VerifyPasswordScreen with challengeMode = 1
  2. Verify forgot password link is NOT visible
  3. Confirm only password creation flow is available

Expected Results:

Run Tests with Flutter

# Run on connected device
flutter run

# Run with verbose logging
flutter run -v

# Run on specific device
flutter run -d <device-id>

# Hot reload during testing (press 'r' in terminal)
# Hot restart during testing (press 'R' in terminal)

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

Security Validation Checklist

User Experience Optimization

Flutter-Specific Best Practices

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

Enhanced VerifyPasswordScreen with Forgot Password

// lib/tutorial/screens/mfa/verify_password_screen.dart (complete implementation)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:rdna_client/rdna_struct.dart';
import '../../../uniken/services/rdna_service.dart';
import '../../../uniken/utils/rdna_event_utils.dart';
import '../components/custom_button.dart';
import '../components/custom_input.dart';
import '../components/status_banner.dart';
import '../components/close_button.dart';

class VerifyPasswordScreen extends ConsumerStatefulWidget {
  final RDNAGetPassword? eventData;

  const VerifyPasswordScreen({
    super.key,
    this.eventData,
  });

  @override
  ConsumerState<VerifyPasswordScreen> createState() =>
      _VerifyPasswordScreenState();
}

class _VerifyPasswordScreenState extends ConsumerState<VerifyPasswordScreen> {
  final _passwordController = TextEditingController();
  String? _error;
  bool _isSubmitting = false;
  bool _isForgotPasswordLoading = false;
  String? _validationMessage;
  bool _validationSuccess = false;
  bool _obscurePassword = true;
  String? _userId;
  int? _attemptsLeft;

  @override
  void initState() {
    super.initState();
    _processEventData();
  }

  @override
  void didUpdateWidget(VerifyPasswordScreen oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Re-process when eventData changes (SDK re-triggers getPassword with error)
    if (widget.eventData != oldWidget.eventData) {
      print('VerifyPasswordScreen - Event data updated, re-processing');
      _processEventData();
    }
  }

  void _processEventData() {
    if (widget.eventData == null) return;

    final data = widget.eventData!;

    setState(() {
      _userId = data.userId;
      _attemptsLeft = data.attemptsLeft;
    });

    // Check for API errors first
    if (RDNAEventUtils.hasApiError(data.error)) {
      final errorMessage = RDNAEventUtils.getErrorMessage(data.error, null);
      setState(() {
        _error = errorMessage;
        _validationSuccess = false;
      });
      return;
    }

    // Check for status errors
    if (RDNAEventUtils.hasStatusError(data.challengeResponse?.status)) {
      final errorMessage = RDNAEventUtils.getErrorMessage(
          null, data.challengeResponse?.status);
      setState(() {
        _error = errorMessage;
        _validationSuccess = false;
      });
      return;
    }

    // Success - ready for password input
    setState(() {
      _validationSuccess = true;
      _validationMessage = 'Ready to verify password';
      _error = null;
    });
  }

  /// Check if forgot password is enabled from challenge info
  bool _isForgotPasswordEnabled() {
    if (widget.eventData?.challengeMode != 0) return false;

    final challengeInfo = widget.eventData?.challengeResponse?.challengeInfo;
    if (challengeInfo != null) {
      final enableForgotPassword = challengeInfo.firstWhere(
        (info) => info.key == 'ENABLE_FORGOT_PASSWORD',
        orElse: () => RDNAChallengeInfo(key: '', value: 'false'),
      ).value;
      return enableForgotPassword?.toLowerCase() == 'true';
    }

    return true;
  }

  /// Handle forgot password flow
  Future<void> _handleForgotPassword() async {
    if (_isForgotPasswordLoading || _isSubmitting) return;

    setState(() {
      _isForgotPasswordLoading = true;
      _error = null;
      _validationMessage = null;
    });

    final rdnaService = RdnaService.getInstance();
    final response = await rdnaService.forgotPassword(_userId);

    if (response.error?.longErrorCode == 0) {
      setState(() {
        _validationSuccess = true;
        _validationMessage = 'Password reset initiated successfully!';
        _isForgotPasswordLoading = false;
      });
    } else {
      setState(() {
        _error = response.error?.errorString ?? 'Failed to initiate forgot password';
        _isForgotPasswordLoading = false;
      });
    }
  }

  Future<void> _handleVerifyPassword() async {
    final password = _passwordController.text;

    setState(() {
      _isSubmitting = true;
      _error = null;
    });

    final rdnaService = RdnaService.getInstance();
    final response = await rdnaService.setPassword(
      password,
      RDNAChallengeOpMode.RDNA_CHALLENGE_OP_VERIFY,
    );

    if (response.error?.longErrorCode == 0) {
      setState(() {
        _validationSuccess = true;
        _isSubmitting = false;
      });
    } else {
      setState(() {
        _error = response.error?.errorString ?? 'Unknown error';
        _isSubmitting = false;
      });
    }
  }

  Future<void> _handleClose() async {
    final rdnaService = RdnaService.getInstance();
    await rdnaService.resetAuthState();
  }

  bool _isFormValid() {
    return _passwordController.text.isNotEmpty && _error == null;
  }

  bool _isAnyOperationInProgress() {
    return _isSubmitting || _isForgotPasswordLoading;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusScope.of(context).unfocus(),
      child: Scaffold(
        backgroundColor: const Color(0xFFF8F9FA),
        body: SafeArea(
          child: Stack(
            children: [
              Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const Text(
                      'Verify Password',
                      style: TextStyle(
                        fontSize: 28,
                        fontWeight: FontWeight.bold,
                        color: Color(0xFF2c3e50),
                      ),
                      textAlign: TextAlign.center,
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'Enter your password to continue',
                      style: TextStyle(
                        fontSize: 16,
                        color: Color(0xFF7f8c8d),
                      ),
                      textAlign: TextAlign.center,
                    ),
                    const SizedBox(height: 30),

                    if (_userId != null) ...[
                      const Text(
                        'Welcome back',
                        style: TextStyle(fontSize: 18, color: Color(0xFF2c3e50)),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        _userId!,
                        style: const TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: Color(0xFF3498db),
                        ),
                      ),
                      const SizedBox(height: 20),
                    ],

                    if (_attemptsLeft != null)
                      StatusBanner(
                        type: StatusBannerType.warning,
                        message: '$_attemptsLeft attempt${_attemptsLeft != 1 ? 's' : ''} remaining',
                      ),

                    if (_error != null)
                      StatusBanner(
                        type: StatusBannerType.error,
                        message: _error!,
                      ),

                    CustomInput(
                      label: 'Password',
                      value: _passwordController.text,
                      onChanged: (value) {
                        setState(() {
                          _passwordController.text = value;
                          if (_error != null) _error = null;
                        });
                      },
                      placeholder: 'Enter your password',
                      obscureText: _obscurePassword,
                      enabled: !_isAnyOperationInProgress(),
                      suffixIcon: IconButton(
                        icon: Icon(
                          _obscurePassword ? Icons.visibility : Icons.visibility_off,
                          color: const Color(0xFF7f8c8d),
                        ),
                        onPressed: () {
                          setState(() {
                            _obscurePassword = !_obscurePassword;
                          });
                        },
                      ),
                    ),

                    CustomButton(
                      title: _isSubmitting ? 'Verifying...' : 'Verify Password',
                      onPress: _handleVerifyPassword,
                      loading: _isSubmitting,
                      disabled: !_isFormValid() || _isAnyOperationInProgress(),
                    ),

                    // Forgot Password Link
                    if (_isForgotPasswordEnabled()) ...[
                      const SizedBox(height: 16),
                      if (_isForgotPasswordLoading)
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: const [
                            SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(
                                strokeWidth: 2,
                                valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF3498db)),
                              ),
                            ),
                            SizedBox(width: 12),
                            Text(
                              'Initiating password reset...',
                              style: TextStyle(
                                fontSize: 16,
                                color: Color(0xFF3498db),
                                fontWeight: FontWeight.w500,
                              ),
                            ),
                          ],
                        )
                      else
                        TextButton(
                          onPressed: _isAnyOperationInProgress() ? null : _handleForgotPassword,
                          child: Text(
                            'Forgot Password?',
                            style: TextStyle(
                              fontSize: 16,
                              color: _isAnyOperationInProgress()
                                  ? const Color(0xFFbdc3c7)
                                  : const Color(0xFF3498db),
                              decoration: TextDecoration.underline,
                              fontWeight: FontWeight.w500,
                            ),
                          ),
                        ),
                    ],

                    const SizedBox(height: 20),
                    Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: const Color(0xFFe8f4f8),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Text(
                        'Enter your password to verify your identity and continue.',
                        style: TextStyle(
                          fontSize: 14,
                          color: Color(0xFF2c3e50),
                        ),
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ],
                ),
              ),
              CustomCloseButton(
                onPressed: _handleClose,
                disabled: _isAnyOperationInProgress(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _passwordController.dispose();
    super.dispose();
  }
}

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.