From 16a27f1cc84e4008bac82dfd7dd7ced65d2f32ae Mon Sep 17 00:00:00 2001 From: Dani B Date: Wed, 28 Jan 2026 11:50:03 -0500 Subject: [PATCH] feat(01-05): create password reset request page - Complete reset password page with email input and validation - Shows success message after email sent - Displays helpful instructions and next steps - Handles loading and error states properly - Integrates with AuthProvider for password reset - Includes accessibility features and responsive design - Provides 'Back to Login' navigation option - Clear error mapping for different failure scenarios --- .../pages/reset_password_page.dart | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 lib/features/authentication/presentation/pages/reset_password_page.dart diff --git a/lib/features/authentication/presentation/pages/reset_password_page.dart b/lib/features/authentication/presentation/pages/reset_password_page.dart new file mode 100644 index 0000000..4b312a1 --- /dev/null +++ b/lib/features/authentication/presentation/pages/reset_password_page.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import '../../widgets/password_reset_form.dart'; +import '../../../../core/errors/auth_exceptions.dart'; +import '../providers/auth_provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Password reset request page where users can initiate password recovery +class ResetPasswordPage extends ConsumerStatefulWidget { + const ResetPasswordPage({super.key}); + + @override + ConsumerState createState() => _ResetPasswordPageState(); +} + +class _ResetPasswordPageState extends ConsumerState { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + + bool _isLoading = false; + String? _errorMessage; + String? _successMessage; + bool _emailSent = false; + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + Future _handlePasswordReset(String email) async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + _errorMessage = null; + _successMessage = null; + }); + + try { + final authProvider = ref.read(authProvider.notifier); + await authProvider.resetPassword(email.trim()); + + if (mounted) { + setState(() { + _isLoading = false; + _emailSent = true; + _successMessage = 'Password reset email sent! Check your inbox for further instructions.'; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _errorMessage = _mapErrorToMessage(e); + }); + } + } + } + + String _mapErrorToMessage(Object error) { + if (error is UserNotFoundException) { + return 'No account found with this email address.'; + } else if (error is TooManyRequestsException) { + return 'Too many reset attempts. Please try again later.'; + } else if (error is NetworkException) { + return 'Network error. Please check your connection and try again.'; + } else if (error is AuthException) { + return error.message; + } else { + return 'An unexpected error occurred. Please try again.'; + } + } + + void _navigateToLogin() { + Navigator.of(context).pushReplacementNamed('/login'); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Scaffold( + appBar: AppBar( + title: const Text('Reset Password'), + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: _navigateToLogin, + tooltip: 'Back to Login', + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 32), + _buildHeader(), + const SizedBox(height: 32), + if (!_emailSent) ...[ + _buildInstructions(), + const SizedBox(height: 32), + PasswordResetForm( + formKey: _formKey, + emailController: _emailController, + isLoading: _isLoading, + errorMessage: _errorMessage, + onSubmit: _handlePasswordReset, + ), + ] else ...[ + _buildSuccessMessage(), + const SizedBox(height: 32), + _buildBackToLoginButton(), + ], + ], + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Column( + children: [ + Icon( + Icons.lock_reset_outlined, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + 'Forgot Password?', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + Widget _buildInstructions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Enter your email address below and we\'ll send you a link to reset your password.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSuccessMessage() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Icon( + Icons.mark_email_read_outlined, + size: 48, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const SizedBox(height: 16), + Text( + 'Check Your Email', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 8), + Text( + _successMessage ?? 'We\'ve sent password reset instructions to your email address.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Next Steps:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '1. Open your email inbox\n' + '2. Look for the password reset email\n' + '3. Click the reset link in the email\n' + '4. Create a new password', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBackToLoginButton() { + return OutlinedButton( + onPressed: _navigateToLogin, + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Back to Login'), + ); + } +} \ No newline at end of file