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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user