feat(01-08): enhance password reset page with comprehensive error handling

- Enhanced error mapping with specific user-friendly messages
- Added accessibility announcements for success and error states
- Improved success message with detailed step-by-step instructions
- Added resend email functionality with cooldown timer preparation
- Enhanced error disposal when user starts typing
- Added email validation for empty inputs
- Improved PasswordResetForm widget with onEmailChanged callback
- Added spam folder guidance in success instructions
- Enhanced error recovery instructions for different scenarios
This commit is contained in:
Dani B
2026-01-28 12:33:58 -05:00
parent ef471f28b0
commit e69cb9b87e
2 changed files with 97 additions and 10 deletions

View File

@@ -20,6 +20,8 @@ class _ResetPasswordPageState extends ConsumerState<ResetPasswordPage> {
String? _errorMessage; String? _errorMessage;
String? _successMessage; String? _successMessage;
bool _emailSent = false; bool _emailSent = false;
bool _canResendAfter = false;
DateTime? _lastAttemptTime;
@override @override
void dispose() { void dispose() {
@@ -27,9 +29,26 @@ class _ResetPasswordPageState extends ConsumerState<ResetPasswordPage> {
super.dispose(); super.dispose();
} }
/// Handles email input changes and clears errors
void _handleEmailChanged(String value) {
setState(() {
_errorMessage = null;
_successMessage = null;
});
}
Future<void> _handlePasswordReset(String email) async { Future<void> _handlePasswordReset(String email) async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
// Check for empty email
final trimmedEmail = email.trim();
if (trimmedEmail.isEmpty) {
setState(() {
_errorMessage = 'Please enter your email address';
});
return;
}
setState(() { setState(() {
_isLoading = true; _isLoading = true;
_errorMessage = null; _errorMessage = null;
@@ -38,7 +57,7 @@ class _ResetPasswordPageState extends ConsumerState<ResetPasswordPage> {
try { try {
final authProvider = ref.read(authProvider.notifier); final authProvider = ref.read(authProvider.notifier);
await authProvider.resetPassword(email.trim()); await authProvider.resetPassword(trimmedEmail);
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -46,31 +65,51 @@ class _ResetPasswordPageState extends ConsumerState<ResetPasswordPage> {
_emailSent = true; _emailSent = true;
_successMessage = 'Password reset email sent! Check your inbox for further instructions.'; _successMessage = 'Password reset email sent! Check your inbox for further instructions.';
}); });
// Announce success for accessibility
_announceMessageForAccessibility(_successMessage!);
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
_errorMessage = _mapErrorToMessage(e); _errorMessage = _mapErrorToUserFriendlyMessage(e);
}); });
// Announce error for accessibility
_announceErrorForAccessibility(_errorMessage!);
} }
} }
} }
String _mapErrorToMessage(Object error) { String _mapErrorToUserFriendlyMessage(Object error) {
if (error is UserNotFoundException) { if (error is UserNotFoundException) {
return 'No account found with this email address.'; return 'No account found with this email address. Please check the email or sign up for a new account.';
} else if (error is TooManyRequestsException) { } else if (error is TooManyRequestsException) {
return 'Too many reset attempts. Please try again later.'; return 'Too many reset attempts. Please wait 15 minutes before trying again, or contact support.';
} else if (error is NetworkException) { } else if (error is NetworkException) {
return 'Network error. Please check your connection and try again.'; return 'Network connection failed. Please check your internet connection and try again.';
} else if (error is AuthDisabledException) {
return 'Password reset is currently disabled. Please try again later or contact support.';
} else if (error is InvalidTokenException) {
return 'Reset request failed. Please start the password reset process again.';
} else if (error is AuthException) { } else if (error is AuthException) {
return error.message; return error.message;
} else { } else {
return 'An unexpected error occurred. Please try again.'; return 'An unexpected error occurred. Please try again or contact support if the problem persists.';
} }
} }
/// Announces success message for screen readers
void _announceMessageForAccessibility(String message) {
SemanticsService.announce(message, TextDirection.ltr);
}
/// Announces error message for screen readers
void _announceErrorForAccessibility(String errorMessage) {
SemanticsService.announce(errorMessage, TextDirection.ltr);
}
void _navigateToLogin() { void _navigateToLogin() {
Navigator.of(context).pushReplacementNamed('/login'); Navigator.of(context).pushReplacementNamed('/login');
} }
@@ -108,6 +147,7 @@ class _ResetPasswordPageState extends ConsumerState<ResetPasswordPage> {
emailController: _emailController, emailController: _emailController,
isLoading: _isLoading, isLoading: _isLoading,
errorMessage: _errorMessage, errorMessage: _errorMessage,
onEmailChanged: _handleEmailChanged,
onSubmit: _handlePasswordReset, onSubmit: _handlePasswordReset,
), ),
] else ...[ ] else ...[
@@ -223,19 +263,60 @@ class _ResetPasswordPageState extends ConsumerState<ResetPasswordPage> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'1. Open your email inbox\n' '1. Open your email inbox\n'
'2. Look for the password reset email\n' '2. Look for password reset email (check spam folder)\n'
'3. Click the reset link in the email\n' '3. Click reset link in the email\n'
'4. Create a new password', '4. Create a new password\n'
'5. Return to sign in with your new password',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
const SizedBox(height: 8),
Text(
'Didn\'t receive the email? Check your spam folder or try again in a few minutes.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
),
),
], ],
), ),
), ),
const SizedBox(height: 16),
_buildResendSection(),
], ],
), ),
); );
} }
Widget _buildResendSection() {
return Column(
children: [
Text(
"Didn't receive the email?",
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 8),
TextButton(
onPressed: _canResendAfter ? _handleResendEmail : null,
style: TextButton.styleFrom(
foregroundColor: _canResendAfter
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.5),
),
child: Text(
_canResendAfter ? 'Resend Email' : 'Resend Email (available soon)',
),
),
],
);
}
void _handleResendEmail() {
if (_canResendAfter && _emailController.text.isNotEmpty) {
_handlePasswordReset(_emailController.text);
}
}
Widget _buildBackToLoginButton() { Widget _buildBackToLoginButton() {
return OutlinedButton( return OutlinedButton(
onPressed: _navigateToLogin, onPressed: _navigateToLogin,

View File

@@ -9,6 +9,7 @@ class PasswordResetForm extends StatefulWidget {
final bool isLoading; final bool isLoading;
final String? errorMessage; final String? errorMessage;
final ValueChanged<String>? onSubmit; final ValueChanged<String>? onSubmit;
final ValueChanged<String>? onEmailChanged;
final String emailLabel; final String emailLabel;
final String submitButtonText; final String submitButtonText;
final bool autofocusEmail; final bool autofocusEmail;
@@ -22,6 +23,7 @@ class PasswordResetForm extends StatefulWidget {
this.isLoading = false, this.isLoading = false,
this.errorMessage, this.errorMessage,
this.onSubmit, this.onSubmit,
this.onEmailChanged,
this.emailLabel = 'Email', this.emailLabel = 'Email',
this.submitButtonText = 'Send Reset Email', this.submitButtonText = 'Send Reset Email',
this.autofocusEmail = true, this.autofocusEmail = true,
@@ -150,6 +152,10 @@ class _PasswordResetFormState extends State<PasswordResetForm> {
_emailError = null; _emailError = null;
}); });
} }
// Call external callback if provided
if (widget.onEmailChanged != null) {
widget.onEmailChanged!(value);
}
}, },
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')), // Prevent spaces FilteringTextInputFormatter.deny(RegExp(r'\s')), // Prevent spaces