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

354 lines
14 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,
),
),
),
),
],
),
),
),
),
),
),
);
}
}