Files
Kifi/kifi-app/lib/features/auth/presentation/reset_password_screen.dart

258 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../providers/auth_provider.dart';
import 'auth_screen.dart';
class ResetPasswordScreen extends ConsumerStatefulWidget {
final String email;
const ResetPasswordScreen({super.key, required this.email});
@override
ConsumerState<ResetPasswordScreen> createState() => _ResetPasswordScreenState();
}
class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
final otpController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirm = true;
String? _errorMessage;
@override
void dispose() {
otpController.dispose();
newPasswordController.dispose();
confirmPasswordController.dispose();
super.dispose();
}
Future<void> _resetPassword() async {
final otp = otpController.text.trim();
final newPassword = newPasswordController.text;
final confirmPassword = confirmPasswordController.text;
if (otp.isEmpty || otp.length != 6) {
setState(() => _errorMessage = 'Please enter a valid 6-digit OTP');
return;
}
if (newPassword.isEmpty || newPassword.length < 6) {
setState(() => _errorMessage = 'Password must be at least 6 characters');
return;
}
if (newPassword != confirmPassword) {
setState(() => _errorMessage = 'Passwords do not match');
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
final success = await ref.read(authControllerProvider.notifier).resetPassword(widget.email, otp, newPassword);
if (mounted) {
setState(() => _isLoading = false);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password reset successfully! Please log in.'),
backgroundColor: Color(0xFF6C63FF),
),
);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const AuthScreen()),
(route) => false,
);
} else {
setState(() => _errorMessage = 'Invalid OTP or reset failed. Please try again.');
}
}
}
InputDecoration _buildInputDecoration(String hint, {IconData? prefixIcon, Widget? suffixIcon}) {
return InputDecoration(
hintText: hint,
prefixIcon: prefixIcon != null ? Icon(prefixIcon, color: Colors.grey.shade500) : null,
suffixIcon: suffixIcon,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
leading: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(LucideIcons.chevronLeft, size: 28),
color: Colors.black87,
),
),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(child: Icon(LucideIcons.shieldCheck, size: 40, color: Theme.of(context).primaryColor)),
),
const SizedBox(height: 32),
Text(
'Reset Password',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'We\'ve sent a 6-digit code to\n${widget.email}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
),
const SizedBox(height: 40),
// OTP Field
TextField(
controller: otpController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
maxLength: 6,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 24, letterSpacing: 16),
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: '000000',
hintStyle: TextStyle(color: Colors.grey.shade300, letterSpacing: 16),
counterText: '',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)),
),
),
const SizedBox(height: 24),
// New Password
TextField(
controller: newPasswordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: _buildInputDecoration(
'New Password',
prefixIcon: LucideIcons.lock,
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
),
const SizedBox(height: 16),
// Confirm Password
TextField(
controller: confirmPasswordController,
obscureText: _obscureConfirm,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _resetPassword(),
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: _buildInputDecoration(
'Confirm Password',
prefixIcon: LucideIcons.lock,
suffixIcon: IconButton(
icon: Icon(_obscureConfirm ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _errorMessage != null
? Padding(
padding: const EdgeInsets.only(top: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 14, fontWeight: FontWeight.w500))),
],
),
),
)
: const SizedBox.shrink(),
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _resetPassword,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 2,
),
child: _isLoading
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('Reset Password', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
),
),
],
),
),
),
),
),
),
);
}
}