feat(01-06): create password update page with validation
- Implemented UpdatePasswordPage with password strength indicator - Added password validation utility with comprehensive checks - Enhanced AuthProvider with updatePasswordFromReset method - Extended AuthRepository interface and implementation for password reset - Added InvalidTokenException to auth exception hierarchy - Includes accessibility features and error handling - Password strength indicator with real-time feedback Files: - lib/features/authentication/presentation/pages/update_password_page.dart - lib/core/utils/password_validator.dart - lib/providers/auth_provider.dart - lib/features/authentication/domain/repositories/auth_repository.dart - lib/features/authentication/data/repositories/auth_repository_impl.dart - lib/core/errors/auth_exceptions.dart
This commit is contained in:
@@ -120,6 +120,19 @@ class EmailNotVerifiedException extends AuthException {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exception thrown when a reset token is invalid or expired
|
||||||
|
class InvalidTokenException extends AuthException {
|
||||||
|
const InvalidTokenException({
|
||||||
|
String message = 'Reset token is invalid or has expired',
|
||||||
|
String? code,
|
||||||
|
dynamic originalError,
|
||||||
|
}) : super(
|
||||||
|
message: message,
|
||||||
|
code: code ?? 'INVALID_TOKEN',
|
||||||
|
originalError: originalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Exception thrown when too many login attempts are made
|
/// Exception thrown when too many login attempts are made
|
||||||
class TooManyRequestsException extends AuthException {
|
class TooManyRequestsException extends AuthException {
|
||||||
const TooManyRequestsException({
|
const TooManyRequestsException({
|
||||||
@@ -198,6 +211,17 @@ class AuthExceptionFactory {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lowerMessage.contains('invalid token') ||
|
||||||
|
lowerMessage.contains('token expired') ||
|
||||||
|
lowerMessage.contains('reset token') ||
|
||||||
|
lowerMessage.contains('session has expired')) {
|
||||||
|
return InvalidTokenException(
|
||||||
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Reset token is invalid or has expired',
|
||||||
|
code: errorCode,
|
||||||
|
originalError: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (lowerMessage.contains('user not found') ||
|
if (lowerMessage.contains('user not found') ||
|
||||||
lowerMessage.contains('no user found') ||
|
lowerMessage.contains('no user found') ||
|
||||||
lowerMessage.contains('user does not exist')) {
|
lowerMessage.contains('user does not exist')) {
|
||||||
|
|||||||
92
lib/core/utils/password_validator.dart
Normal file
92
lib/core/utils/password_validator.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/// Utility class for password validation and strength checking
|
||||||
|
class PasswordValidator {
|
||||||
|
/// Minimum password length
|
||||||
|
static const int minLength = 8;
|
||||||
|
|
||||||
|
/// Validates a password against security requirements
|
||||||
|
///
|
||||||
|
/// Returns [PasswordValidationResult] with detailed validation information
|
||||||
|
static PasswordValidationResult validate(String password) {
|
||||||
|
final errors = <String>[];
|
||||||
|
double strength = 0.0;
|
||||||
|
|
||||||
|
// Check length
|
||||||
|
if (password.length < minLength) {
|
||||||
|
errors.add('Password must be at least $minLength characters');
|
||||||
|
} else {
|
||||||
|
strength += 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for uppercase letter
|
||||||
|
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
errors.add('Password must contain at least one uppercase letter');
|
||||||
|
} else {
|
||||||
|
strength += 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for lowercase letter
|
||||||
|
if (!password.contains(RegExp(r'[a-z]'))) {
|
||||||
|
errors.add('Password must contain at least one lowercase letter');
|
||||||
|
} else {
|
||||||
|
strength += 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for number
|
||||||
|
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||||
|
errors.add('Password must contain at least one number');
|
||||||
|
} else {
|
||||||
|
strength += 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus points for special characters
|
||||||
|
if (password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
|
||||||
|
strength = (strength + 0.1).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus points for longer passwords
|
||||||
|
if (password.length >= 12) {
|
||||||
|
strength = (strength + 0.1).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PasswordValidationResult(
|
||||||
|
isValid: errors.isEmpty,
|
||||||
|
errors: errors,
|
||||||
|
strength: strength,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets password strength text based on strength value
|
||||||
|
static String getStrengthText(double strength) {
|
||||||
|
if (strength <= 0.25) return 'Weak';
|
||||||
|
if (strength <= 0.5) return 'Fair';
|
||||||
|
if (strength <= 0.75) return 'Good';
|
||||||
|
return 'Strong';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets password strength color based on strength value
|
||||||
|
static String getStrengthColor(double strength) {
|
||||||
|
if (strength <= 0.25) return 'red';
|
||||||
|
if (strength <= 0.5) return 'orange';
|
||||||
|
if (strength <= 0.75) return 'yellow';
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of password validation
|
||||||
|
class PasswordValidationResult {
|
||||||
|
final bool isValid;
|
||||||
|
final List<String> errors;
|
||||||
|
final double strength;
|
||||||
|
|
||||||
|
const PasswordValidationResult({
|
||||||
|
required this.isValid,
|
||||||
|
required this.errors,
|
||||||
|
required this.strength,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Gets the strength text for this result
|
||||||
|
String get strengthText => PasswordValidator.getStrengthText(strength);
|
||||||
|
|
||||||
|
/// Gets the strength color name for this result
|
||||||
|
String get strengthColor => PasswordValidator.getStrengthColor(strength);
|
||||||
|
}
|
||||||
@@ -302,6 +302,33 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updatePasswordFromReset(String newPassword) async {
|
||||||
|
try {
|
||||||
|
// Extract the current session to verify we have a valid reset token
|
||||||
|
final session = _supabase.auth.currentSession;
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
throw const InvalidTokenException(
|
||||||
|
message: 'No valid password reset session found. Please request a new reset link.',
|
||||||
|
code: 'NO_RESET_SESSION',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password using Supabase updateUser
|
||||||
|
await _supabase.auth.updateUser(
|
||||||
|
UserAttributes(
|
||||||
|
password: newPassword,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// After successful password update, sign out to force re-authentication
|
||||||
|
await _supabase.auth.signOut();
|
||||||
|
} catch (e) {
|
||||||
|
throw AuthExceptionFactory.fromSupabaseError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispose of any active subscriptions
|
/// Dispose of any active subscriptions
|
||||||
///
|
///
|
||||||
/// Call this when the repository is no longer needed to prevent memory leaks
|
/// Call this when the repository is no longer needed to prevent memory leaks
|
||||||
|
|||||||
@@ -175,11 +175,26 @@ abstract class AuthRepository {
|
|||||||
///
|
///
|
||||||
/// Creates an anonymous user session that can be upgraded to a full account later
|
/// Creates an anonymous user session that can be upgraded to a full account later
|
||||||
///
|
///
|
||||||
/// Returns the anonymous [AuthUser] on successful sign in
|
/// Returns anonymous [AuthUser] on successful sign in
|
||||||
///
|
///
|
||||||
/// Throws:
|
/// Throws:
|
||||||
/// - [NetworkException] if network connection fails
|
/// - [NetworkException] if network connection fails
|
||||||
/// - [AuthDisabledException] if anonymous sign in is disabled
|
/// - [AuthDisabledException] if anonymous sign in is disabled
|
||||||
/// - [AuthException] for other authentication errors
|
/// - [AuthException] for other authentication errors
|
||||||
Future<AuthUser> signInAnonymously();
|
Future<AuthUser> signInAnonymously();
|
||||||
|
|
||||||
|
/// Updates password from password reset token
|
||||||
|
///
|
||||||
|
/// This method is used when users click on password reset links
|
||||||
|
/// from their email. The reset token is typically extracted from
|
||||||
|
/// the deep link URL by the authentication system.
|
||||||
|
///
|
||||||
|
/// [newPassword] - The new password to set for the user
|
||||||
|
///
|
||||||
|
/// Throws:
|
||||||
|
/// - [InvalidTokenException] if reset token is expired or invalid
|
||||||
|
/// - [WeakPasswordException] if new password doesn't meet requirements
|
||||||
|
/// - [NetworkException] if network connection fails
|
||||||
|
/// - [AuthException] for other authentication errors
|
||||||
|
Future<void> updatePasswordFromReset(String newPassword);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../widgets/auth_button.dart';
|
||||||
|
import '../providers/auth_provider.dart';
|
||||||
|
import '../../../../core/errors/auth_exceptions.dart';
|
||||||
|
import '../../../../core/utils/password_validator.dart';
|
||||||
|
|
||||||
|
/// Password update page for handling password reset from email links
|
||||||
|
///
|
||||||
|
/// This page handles the password reset flow when users click on reset links
|
||||||
|
/// from their email. It validates the reset token and allows users to set
|
||||||
|
/// a new password.
|
||||||
|
class UpdatePasswordPage extends ConsumerStatefulWidget {
|
||||||
|
const UpdatePasswordPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<UpdatePasswordPage> createState() => _UpdatePasswordPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UpdatePasswordPageState extends ConsumerState<UpdatePasswordPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _newPasswordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _showPassword = false;
|
||||||
|
bool _showConfirmPassword = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
bool _passwordUpdated = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_newPasswordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePasswordUpdate() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final authProvider = ref.read(authProvider.notifier);
|
||||||
|
await authProvider.updatePasswordFromReset(
|
||||||
|
_newPasswordController.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_passwordUpdated = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = _mapErrorToMessage(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _mapErrorToMessage(Object error) {
|
||||||
|
if (error is InvalidTokenException) {
|
||||||
|
return 'This password reset link has expired or is invalid. Please request a new one.';
|
||||||
|
} else if (error is WeakPasswordException) {
|
||||||
|
return 'Password is too weak. Please choose a stronger password.';
|
||||||
|
} else if (error is SessionExpiredException) {
|
||||||
|
return 'Password reset session expired. Please request a new reset link.';
|
||||||
|
} 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPasswordValid() {
|
||||||
|
final password = _newPasswordController.text;
|
||||||
|
return password.length >= 8 &&
|
||||||
|
password.contains(RegExp(r'[A-Z]')) &&
|
||||||
|
password.contains(RegExp(r'[a-z]')) &&
|
||||||
|
password.contains(RegExp(r'[0-9]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a new password';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 8) {
|
||||||
|
return 'Password must be at least 8 characters';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
return 'Password must contain at least one uppercase letter';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[a-z]'))) {
|
||||||
|
return 'Password must contain at least one lowercase letter';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[0-9]'))) {
|
||||||
|
return 'Password must contain at least one number';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateConfirmPassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please confirm your new password';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != _newPasswordController.text) {
|
||||||
|
return 'Passwords do not match';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Update Password'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: _navigateToLogin,
|
||||||
|
tooltip: 'Cancel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
if (!_passwordUpdated) ...[
|
||||||
|
_buildHeader(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildInstructions(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildPasswordForm(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSubmitButton(),
|
||||||
|
if (_errorMessage != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildErrorMessage(),
|
||||||
|
],
|
||||||
|
] else ...[
|
||||||
|
_buildSuccessMessage(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildLoginButton(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.lock_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Set New 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(
|
||||||
|
'Create a strong password for your account security.',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordForm() {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _newPasswordController,
|
||||||
|
obscureText: !_showPassword,
|
||||||
|
enabled: !_isLoading,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'New Password',
|
||||||
|
hintText: 'Enter your new password',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_showPassword ? Icons.visibility : Icons.visibility_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_showPassword = !_showPassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: _validatePassword,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
obscureText: !_showConfirmPassword,
|
||||||
|
enabled: !_isLoading,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Confirm Password',
|
||||||
|
hintText: 'Confirm your new password',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_showConfirmPassword ? Icons.visibility : Icons.visibility_off,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_showConfirmPassword = !_showConfirmPassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: _validateConfirmPassword,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildPasswordStrengthIndicator(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordStrengthIndicator() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Password Strength:',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_getPasswordStrengthText(),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: _getPasswordStrengthColor(),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: _getPasswordStrength(),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(_getPasswordStrengthColor()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Password must contain:\n• At least 8 characters\n• Uppercase and lowercase letters\n• At least one number',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getPasswordStrength() {
|
||||||
|
final password = _newPasswordController.text;
|
||||||
|
double strength = 0.0;
|
||||||
|
|
||||||
|
if (password.length >= 8) strength += 0.25;
|
||||||
|
if (password.contains(RegExp(r'[A-Z]'))) strength += 0.25;
|
||||||
|
if (password.contains(RegExp(r'[a-z]'))) strength += 0.25;
|
||||||
|
if (password.contains(RegExp(r'[0-9]'))) strength += 0.25;
|
||||||
|
|
||||||
|
return strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getPasswordStrengthText() {
|
||||||
|
final strength = _getPasswordStrength();
|
||||||
|
if (strength <= 0.25) return 'Weak';
|
||||||
|
if (strength <= 0.5) return 'Fair';
|
||||||
|
if (strength <= 0.75) return 'Good';
|
||||||
|
return 'Strong';
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getPasswordStrengthColor() {
|
||||||
|
final strength = _getPasswordStrength();
|
||||||
|
if (strength <= 0.25) return Colors.red;
|
||||||
|
if (strength <= 0.5) return Colors.orange;
|
||||||
|
if (strength <= 0.75) return Colors.yellow.shade700;
|
||||||
|
return Colors.green;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubmitButton() {
|
||||||
|
return AuthButton(
|
||||||
|
text: 'Update Password',
|
||||||
|
onPressed: _isPasswordValid() ? _handlePasswordUpdate : null,
|
||||||
|
isLoading: _isLoading,
|
||||||
|
icon: const Icon(Icons.lock_reset),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorMessage() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.check_circle_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Password Updated!',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Your password has been successfully updated. You can now sign in with your new password.',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoginButton() {
|
||||||
|
return AuthButton(
|
||||||
|
text: 'Sign In',
|
||||||
|
onPressed: _navigateToLogin,
|
||||||
|
variant: AuthButtonVariant.primary,
|
||||||
|
icon: const Icon(Icons.login),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -393,6 +393,28 @@ class AuthProvider extends StateNotifier<AuthState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates password from reset token (password reset flow)
|
||||||
|
///
|
||||||
|
/// [newPassword] - The new password to set
|
||||||
|
///
|
||||||
|
/// Throws AuthException if update fails
|
||||||
|
Future<void> updatePasswordFromReset(String newPassword) async {
|
||||||
|
if (state.isLoading) return;
|
||||||
|
|
||||||
|
state = state.copyWith(isLoading: true, clearError: true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _authRepository.updatePasswordFromReset(newPassword);
|
||||||
|
state = state.copyWith(isLoading: false);
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
error: e.toString(),
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Clears any authentication error
|
/// Clears any authentication error
|
||||||
void clearError() {
|
void clearError() {
|
||||||
state = state.copyWith(clearError: true);
|
state = state.copyWith(clearError: true);
|
||||||
|
|||||||
Reference in New Issue
Block a user