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