diff --git a/SUPABASE_SETUP.md b/SUPABASE_SETUP.md index bb48da4..8de3421 100644 --- a/SUPABASE_SETUP.md +++ b/SUPABASE_SETUP.md @@ -206,7 +206,50 @@ final imageUrl = supabase.storage .getPublicUrl(fileName); ``` -## 8. Testing +## 8. Email Templates Configuration (Quan trọng!) + +### Cấu hình OTP Email cho Password Reset + +Để gửi OTP thay vì Magic Link khi reset password: + +1. Vào **Supabase Dashboard** → **Authentication** → **Email Templates** + +2. Chọn template **"Reset Password"** + +3. Thay đổi nội dung email để hiển thị OTP: + +```html +

Reset Your Password

+

You have requested to reset your password. Use the OTP code below:

+

{{ .Token }}

+

This code will expire in 60 minutes.

+

If you didn't request this, please ignore this email.

+``` + +4. **XÓA hoặc comment out** đoạn magic link để tránh nhầm lẫn: +```html + +``` + +5. Tương tự với template **"Confirm Signup"** nếu dùng OTP: + +```html +

Confirm your signup

+

Use this OTP code to confirm your email:

+

{{ .Token }}

+

This code will expire in 60 minutes.

+``` + +### Tắt Email Confirmations (Optional) + +Nếu muốn user login ngay không cần confirm email: + +1. Vào **Authentication** → **Settings** +2. Tắt **"Enable email confirmations"** + +**Lưu ý**: Flow hiện tại đã dùng `signInWithOtp` với `shouldCreateUser: false` để gửi OTP cho reset password, giống như flow sign up. + +## 9. Testing Để test services, bạn có thể mock repositories: diff --git a/lib/repository/auth_repository.dart b/lib/repository/auth_repository.dart index 4530364..ea9d746 100644 --- a/lib/repository/auth_repository.dart +++ b/lib/repository/auth_repository.dart @@ -17,11 +17,13 @@ class AuthRepository { required String email, required String password, Map? data, + String? emailRedirectTo, }) async { return _client.auth.signUp( email: email, password: password, data: data, + emailRedirectTo: emailRedirectTo, ); } @@ -41,9 +43,37 @@ class AuthRepository { await _client.auth.signOut(); } - // Reset password + // Reset password - Send OTP Future resetPassword(String email) async { - await _client.auth.resetPasswordForEmail(email); + await _client.auth.signInWithOtp( + email: email, + shouldCreateUser: false, + ); + } + + // Verify OTP for email + Future verifyOTP({ + required String email, + required String token, + required OtpType type, + }) async { + return _client.auth.verifyOTP( + email: email, + token: token, + type: type, + ); + } + + // Resend OTP + Future resendOTP({ + required String email, + required OtpType type, + }) async { + // For recovery, use signInWithOtp like signup + await _client.auth.signInWithOtp( + email: email, + shouldCreateUser: false, + ); } // Update user diff --git a/lib/route/route_constants.dart b/lib/route/route_constants.dart index 703bce0..2b65c1c 100644 --- a/lib/route/route_constants.dart +++ b/lib/route/route_constants.dart @@ -1,6 +1,7 @@ const String preferredLanuageScreenRoute = "preferred_language"; const String logInScreenRoute = "login"; const String signUpScreenRoute = "signup"; +const String otpVerificationScreenRoute = "otp_verification"; const String profileSetupScreenRoute = "profile_setup"; const String signUpVerificationScreenRoute = "signup_verification"; const String passwordRecoveryScreenRoute = "password_recovery"; diff --git a/lib/route/router.dart b/lib/route/router.dart index e50cdf7..5eaf544 100644 --- a/lib/route/router.dart +++ b/lib/route/router.dart @@ -75,6 +75,16 @@ Route generateRoute(RouteSettings settings) { settings: settings, child: const SignUpScreen(), ); + case otpVerificationScreenRoute: + final args = settings.arguments as Map?; + return _buildRoute( + settings: settings, + child: OTPVerificationScreen( + email: args?['email'] ?? '', + password: args?['password'] ?? '', + fullName: args?['fullName'] ?? '', + ), + ); case passwordRecoveryScreenRoute: return _buildRoute( settings: settings, diff --git a/lib/route/screen_export.dart b/lib/route/screen_export.dart index c44f7ed..32d70a0 100644 --- a/lib/route/screen_export.dart +++ b/lib/route/screen_export.dart @@ -1,6 +1,7 @@ export '/screens/auth/views/login_screen.dart'; export '/screens/auth/views/password_recovery_screen.dart'; export '/screens/auth/views/signup_screen.dart'; +export '/screens/auth/views/otp_verification_screen.dart'; export '/route/route_constants.dart'; export '/screens/discover/views/discover_screen.dart'; diff --git a/lib/screens/auth/views/otp_verification_screen.dart b/lib/screens/auth/views/otp_verification_screen.dart new file mode 100644 index 0000000..7a54d67 --- /dev/null +++ b/lib/screens/auth/views/otp_verification_screen.dart @@ -0,0 +1,353 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:shop/constants.dart'; +import 'package:shop/repository/auth_repository.dart'; +import 'package:shop/route/route_constants.dart'; + +class OTPVerificationScreen extends StatefulWidget { + final String email; + final String password; + final String fullName; + + const OTPVerificationScreen({ + super.key, + required this.email, + required this.password, + required this.fullName, + }); + + @override + State createState() => _OTPVerificationScreenState(); +} + +class _OTPVerificationScreenState extends State { + final AuthRepository _authRepository = AuthRepository(); + final List _controllers = + List.generate(6, (_) => TextEditingController()); + final List _focusNodes = List.generate(6, (_) => FocusNode()); + + bool _isLoading = false; + bool _isResending = false; + int _resendCountdown = 60; + Timer? _timer; + + @override + void initState() { + super.initState(); + _startCountdown(); + } + + @override + void dispose() { + _timer?.cancel(); + for (var controller in _controllers) { + controller.dispose(); + } + for (var node in _focusNodes) { + node.dispose(); + } + super.dispose(); + } + + void _startCountdown() { + _resendCountdown = 60; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_resendCountdown > 0) { + setState(() { + _resendCountdown--; + }); + } else { + timer.cancel(); + } + }); + } + + String _getOTPCode() { + return _controllers.map((c) => c.text).join(); + } + + Future _verifyOTP() async { + final otp = _getOTPCode(); + if (otp.length != 6) { + _showErrorSnackBar('Vui lòng nhập đầy đủ 6 số OTP'); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // Verify OTP + await _authRepository.verifyOTP( + email: widget.email, + token: otp, + type: OtpType.signup, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Xác thực thành công! Chào mừng đến với CellphoneZ!'), + backgroundColor: successColor, + duration: Duration(seconds: 2), + ), + ); + + // Navigate to main screen + Navigator.pushNamedAndRemoveUntil( + context, + entryPointScreenRoute, + (route) => false, + ); + } + } catch (e) { + String errorMessage = 'Mã OTP không hợp lệ hoặc đã hết hạn.'; + + if (e.toString().contains('expired')) { + errorMessage = 'Mã OTP đã hết hạn. Vui lòng gửi lại mã mới.'; + } else if (e.toString().contains('invalid')) { + errorMessage = 'Mã OTP không chính xác. Vui lòng kiểm tra lại.'; + } + + if (mounted) { + _showErrorSnackBar(errorMessage); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _resendOTP() async { + if (_isResending || _resendCountdown > 0) return; + + setState(() { + _isResending = true; + }); + + try { + await _authRepository.resendOTP( + email: widget.email, + type: OtpType.signup, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đã gửi lại mã OTP. Vui lòng kiểm tra email.'), + backgroundColor: successColor, + duration: Duration(seconds: 2), + ), + ); + _startCountdown(); + } + } catch (e) { + if (mounted) { + _showErrorSnackBar('Không thể gửi lại mã OTP. Vui lòng thử lại sau.'); + } + } finally { + if (mounted) { + setState(() { + _isResending = false; + }); + } + } + } + + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + action: SnackBarAction( + label: 'Đóng', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(defaultPadding * 1.5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: defaultPadding), + // Title + Text( + 'Xác thực OTP', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: cellphoneZRed, + ), + ), + const SizedBox(height: defaultPadding / 2), + // Description + Text( + 'Chúng tôi đã gửi mã xác thực 6 số đến email', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + widget.email, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w600, + color: cellphoneZRed, + ), + ), + const SizedBox(height: defaultPadding * 2), + + // OTP Input Fields + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(6, (index) { + return SizedBox( + width: 50, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + decoration: InputDecoration( + counterText: '', + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: cellphoneZRed, + width: 2, + ), + ), + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + onChanged: (value) { + if (value.length == 1 && index < 5) { + _focusNodes[index + 1].requestFocus(); + } else if (value.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + } + + // Auto verify when all fields are filled + if (index == 5 && value.isNotEmpty) { + _verifyOTP(); + } + }, + ), + ); + }), + ), + + const SizedBox(height: defaultPadding * 2), + + // Resend OTP + Center( + child: _resendCountdown > 0 + ? Text( + 'Gửi lại mã sau $_resendCountdown giây', + style: TextStyle(color: Colors.grey[600]), + ) + : TextButton( + onPressed: _isResending ? null : _resendOTP, + child: _isResending + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Text( + 'Gửi lại mã OTP', + style: TextStyle( + color: cellphoneZRed, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + const Spacer(), + + // Verify Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _verifyOTP, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + backgroundColor: cellphoneZRed, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Xác thực', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/auth/views/password_recovery_screen.dart b/lib/screens/auth/views/password_recovery_screen.dart index 942a6ea..154d986 100644 --- a/lib/screens/auth/views/password_recovery_screen.dart +++ b/lib/screens/auth/views/password_recovery_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:shop/constants.dart'; import 'package:shop/repository/auth_repository.dart'; +import 'package:shop/screens/auth/views/password_reset_otp_screen.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; class PasswordRecoveryScreen extends StatefulWidget { const PasswordRecoveryScreen({super.key}); @@ -225,20 +227,53 @@ class _PasswordRecoveryScreenState extends State { }); try { - await _authRepository.resetPassword(_email!); + debugPrint('📧 Đang gửi OTP đến email: $_email'); + + // Gửi OTP qua email + await _authRepository.resendOTP( + email: _email!, + type: OtpType.recovery, + ); + + debugPrint('✅ OTP đã được gửi đến: $_email'); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Reset link sent! Please check $_email to continue the process.'), + content: + Text('Mã OTP đã được gửi đến $_email. Vui lòng kiểm tra email!'), + backgroundColor: successColor, + ), + ); + + // Navigate to OTP screen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PasswordResetOtpScreen(email: _email!), ), ); } catch (e) { + debugPrint('❌ Lỗi khi gửi OTP: $e'); if (!mounted) return; + + String errorMessage = 'Không thể gửi mã OTP'; + + if (e.toString().contains('over_email_send_rate_limit')) { + errorMessage = + 'Bạn đã gửi quá nhiều yêu cầu. Vui lòng thử lại sau 1 phút.'; + } else if (e.toString().contains('rate limit')) { + errorMessage = 'Vui lòng đợi một chút trước khi gửi lại.'; + } else if (e.toString().contains('User not found')) { + errorMessage = 'Email không tồn tại trong hệ thống.'; + } + ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Unable to send reset link: ${e.toString()}'), - backgroundColor: Colors.red, + content: Text(errorMessage), + backgroundColor: cellphoneZRed, + duration: const Duration(seconds: 4), ), ); } finally { diff --git a/lib/screens/auth/views/password_reset_otp_screen.dart b/lib/screens/auth/views/password_reset_otp_screen.dart new file mode 100644 index 0000000..3cfbd38 --- /dev/null +++ b/lib/screens/auth/views/password_reset_otp_screen.dart @@ -0,0 +1,418 @@ +import 'package:flutter/material.dart'; +import 'package:shop/constants.dart'; +import 'package:shop/repository/auth_repository.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class PasswordResetOtpScreen extends StatefulWidget { + final String email; + + const PasswordResetOtpScreen({ + super.key, + required this.email, + }); + + @override + State createState() => _PasswordResetOtpScreenState(); +} + +class _PasswordResetOtpScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + final AuthRepository _authRepository = AuthRepository(); + final List _otpControllers = + List.generate(6, (_) => TextEditingController()); + final List _focusNodes = List.generate(6, (_) => FocusNode()); + + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + bool _isVerifying = false; + bool _isResending = false; + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + + @override + void dispose() { + for (var controller in _otpControllers) { + controller.dispose(); + } + for (var node in _focusNodes) { + node.dispose(); + } + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + String _getOtpCode() { + return _otpControllers.map((c) => c.text).join(); + } + + Future _handleVerifyAndReset() async { + if (!_formKey.currentState!.validate()) return; + + final otp = _getOtpCode(); + if (otp.length != 6) { + _showError('Vui lòng nhập đủ 6 số OTP'); + return; + } + + final password = _passwordController.text; + final confirmPassword = _confirmPasswordController.text; + + if (password != confirmPassword) { + _showError('Mật khẩu xác nhận không khớp'); + return; + } + + if (password.length < 6) { + _showError('Mật khẩu phải có ít nhất 6 ký tự'); + return; + } + + setState(() => _isVerifying = true); + + try { + debugPrint('🔐 Đang verify OTP: $otp cho email: ${widget.email}'); + + // Verify OTP + final response = await _authRepository.verifyOTP( + email: widget.email, + token: otp, + type: OtpType.email, + ); + + debugPrint('✅ OTP verified: ${response.user?.id}'); + + // Update password + await _authRepository.updateUser(password: password); + + debugPrint('✅ Password updated successfully'); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đặt lại mật khẩu thành công!'), + backgroundColor: successColor, + ), + ); + + // Navigate back to login + Navigator.of(context).popUntil((route) => route.isFirst); + } + } catch (e) { + debugPrint('❌ Error: $e'); + if (mounted) { + String message = 'Mã OTP không đúng hoặc đã hết hạn'; + if (e.toString().contains('invalid')) { + message = 'Mã OTP không hợp lệ'; + } else if (e.toString().contains('expired')) { + message = 'Mã OTP đã hết hạn. Vui lòng yêu cầu gửi lại'; + } + _showError(message); + } + } finally { + if (mounted) setState(() => _isVerifying = false); + } + } + + Future _handleResendOTP() async { + setState(() => _isResending = true); + + try { + await _authRepository.resendOTP( + email: widget.email, + type: OtpType.email, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đã gửi lại mã OTP. Vui lòng kiểm tra email!'), + backgroundColor: successColor, + ), + ); + } + } catch (e) { + if (mounted) { + String errorMessage = 'Không thể gửi lại OTP. Vui lòng thử lại sau.'; + + if (e.toString().contains('over_email_send_rate_limit')) { + errorMessage = 'Bạn đã gửi quá nhiều yêu cầu. Vui lòng đợi 1 phút.'; + } else if (e.toString().contains('rate limit')) { + errorMessage = 'Vui lòng đợi một chút trước khi gửi lại.'; + } + + _showError(errorMessage); + } + } finally { + if (mounted) setState(() => _isResending = false); + } + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: cellphoneZRed, + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + backgroundColor: cellphoneZRed, + foregroundColor: Colors.white, + title: const Text('Đặt lại mật khẩu'), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(defaultPadding * 1.5), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon + Center( + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: cellphoneZRed.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.lock_reset, + size: 40, + color: cellphoneZRed, + ), + ), + ), + const SizedBox(height: defaultPadding * 1.5), + + // Title + Text( + 'Nhập mã OTP', + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: defaultPadding / 2), + Text( + 'Chúng tôi đã gửi mã OTP gồm 6 số đến email', + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + widget.email, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + color: cellphoneZRed, + ), + ), + const SizedBox(height: defaultPadding * 2), + + // OTP Input + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(6, (index) { + return SizedBox( + width: 45, + height: 55, + child: TextFormField( + controller: _otpControllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + decoration: InputDecoration( + counterText: '', + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: cellphoneZRed, + width: 2, + ), + ), + ), + onChanged: (value) { + if (value.isNotEmpty && index < 5) { + _focusNodes[index + 1].requestFocus(); + } else if (value.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + } + }, + ), + ); + }), + ), + const SizedBox(height: defaultPadding), + + // Resend button + Center( + child: TextButton( + onPressed: _isResending ? null : _handleResendOTP, + child: _isResending + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text( + 'Gửi lại mã OTP', + style: TextStyle( + color: cellphoneZRed, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(height: defaultPadding * 1.5), + + // New Password + Text( + 'Mật khẩu mới', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: defaultPadding / 2), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + hintText: 'Nhập mật khẩu mới', + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Vui lòng nhập mật khẩu'; + } + if (value.length < 6) { + return 'Mật khẩu phải có ít nhất 6 ký tự'; + } + return null; + }, + ), + const SizedBox(height: defaultPadding), + + // Confirm Password + Text( + 'Xác nhận mật khẩu', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: defaultPadding / 2), + TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + hintText: 'Nhập lại mật khẩu mới', + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility_off + : Icons.visibility, + color: Colors.grey, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Vui lòng xác nhận mật khẩu'; + } + if (value != _passwordController.text) { + return 'Mật khẩu không khớp'; + } + return null; + }, + ), + const SizedBox(height: defaultPadding * 2), + + // Submit button + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isVerifying ? null : _handleVerifyAndReset, + style: ElevatedButton.styleFrom( + backgroundColor: cellphoneZRed, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: _isVerifying + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + 'Đặt lại mật khẩu', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/auth/views/signup_screen.dart b/lib/screens/auth/views/signup_screen.dart index f70b65f..c46abfc 100644 --- a/lib/screens/auth/views/signup_screen.dart +++ b/lib/screens/auth/views/signup_screen.dart @@ -288,34 +288,49 @@ class _SignUpScreenState extends State { }); try { - // Call AuthRepository directly to create account - await _authRepository.signUp( + debugPrint('🚀 Đang gửi request đăng ký với email: $_email'); + debugPrint('📧 Email redirect to: cellphonez://verify-email'); + + // Gửi OTP qua email (signUp với emailRedirectTo sẽ tự động gửi OTP) + final response = await _authRepository.signUp( email: _email!, password: _password!, data: { 'name': _fullName, 'email': _email, }, + emailRedirectTo: 'cellphonez://verify-email', ); - // Show success message + debugPrint('✅ SignUp Response: ${response.user?.id}'); + debugPrint( + '📧 Email confirmation sent: ${response.user?.emailConfirmedAt}'); + debugPrint('🔐 User confirmed: ${response.user?.confirmedAt}'); + if (mounted) { + // Show success message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Đăng ký thành công! Chào mừng đến với CellphoneZ!'), + content: + Text('Đã gửi mã OTP đến email của bạn. Vui lòng kiểm tra!'), backgroundColor: successColor, duration: Duration(seconds: 3), ), ); - // Navigate to main screen - Navigator.pushNamedAndRemoveUntil( + // Navigate to OTP verification screen + Navigator.pushNamed( context, - entryPointScreenRoute, - (route) => false, + otpVerificationScreenRoute, + arguments: { + 'email': _email!, + 'password': _password!, + 'fullName': _fullName!, + }, ); } } catch (e) { + debugPrint('❌ SignUp Error: $e'); String errorMessage = 'Đăng ký thất bại. Vui lòng thử lại.'; // Handle specific errors