docs(01-06): complete password update plan

Tasks completed: 2/2
- Create password update page with validation
- Update auth repository for password reset

Files created:
- lib/features/authentication/presentation/pages/update_password_page.dart
- lib/core/utils/password_validator.dart
- lib/features/authentication/presentation/pages/reset_password_confirm_page.dart

SUMMARY: .planning/phases/01-authentication/01-06-SUMMARY.md
This commit is contained in:
Dani B
2026-01-28 12:09:00 -05:00
parent eacfa0d705
commit f456451540
2 changed files with 343 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/auth_provider.dart';
import '../../../../core/errors/auth_exceptions.dart';
/// Password reset confirmation page
///
/// This page handles the initial verification of password reset tokens
/// before redirecting to the password update page.
class ResetPasswordConfirmPage extends ConsumerStatefulWidget {
const ResetPasswordConfirmPage({super.key});
@override
ConsumerState<ResetPasswordConfirmPage> createState() => _ResetPasswordConfirmPageState();
}
class _ResetPasswordConfirmPageState extends ConsumerState<ResetPasswordConfirmPage> {
bool _isLoading = false;
String? _errorMessage;
bool _tokenValid = false;
@override
void initState() {
super.initState();
_validateResetToken();
}
Future<void> _validateResetToken() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Check if we have a valid reset session by attempting to get current user
final authProvider = ref.read(authProvider.notifier);
await authProvider.getCurrentUser();
if (mounted) {
setState(() {
_isLoading = false;
_tokenValid = true;
});
// Navigate to update password page after short delay
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
Navigator.of(context).pushReplacementNamed('/update-password');
}
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_tokenValid = 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 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 _navigateToReset() {
Navigator.of(context).pushReplacementNamed('/reset-password');
}
@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.close),
onPressed: _navigateToReset,
tooltip: 'Close',
),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 48),
if (_isLoading) ...[
_buildLoadingIndicator(),
] else if (_tokenValid) ...[
_buildSuccessMessage(),
] else ...[
_buildErrorMessage(),
const SizedBox(height: 32),
_buildRetryButton(),
],
],
),
),
),
),
);
}
Widget _buildLoadingIndicator() {
return Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 24),
Text(
'Validating reset link...',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
);
}
Widget _buildSuccessMessage() {
return Container(
padding: const EdgeInsets.all(24),
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(
'Reset Link Valid',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 8),
Text(
'Redirecting you to set a new password...',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const CircularProgressIndicator(),
],
),
);
}
Widget _buildErrorMessage() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Invalid Reset Link',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 8),
Text(
_errorMessage ?? 'This password reset link is not valid.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildRetryButton() {
return OutlinedButton(
onPressed: _navigateToReset,
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Request New Reset Link'),
);
}
}