From c253c144810c116ec4eab8c90399b9d05e97aa80 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 10 Jun 2025 12:14:05 +0530 Subject: [PATCH] feat: Refactor OTP authentication to use email instead of phone number --- lib/controller/auth/otp_controller.dart | 99 ++++++++++--------- lib/helpers/services/auth_service.dart | 120 +++++++++++++++++++----- lib/view/auth/otp_login_form.dart | 120 ++++++++++++------------ 3 files changed, 208 insertions(+), 131 deletions(-) diff --git a/lib/controller/auth/otp_controller.dart b/lib/controller/auth/otp_controller.dart index 764c4d9..61c2901 100644 --- a/lib/controller/auth/otp_controller.dart +++ b/lib/controller/auth/otp_controller.dart @@ -2,23 +2,21 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; - +import 'package:marco/helpers/services/auth_service.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; class OTPController extends GetxController { final formKey = GlobalKey(); - // Observables - final RxString phoneNumber = ''.obs; + final RxString email = ''.obs; final RxBool isOTPSent = false.obs; - final RxBool isSending = false.obs; final RxBool isResending = false.obs; - final RxInt timer = 0.obs; Timer? _countdownTimer; - // Text controllers and focus nodes - final TextEditingController phoneController = TextEditingController(); - final List otpControllers = List.generate(4, (_) => TextEditingController()); + final TextEditingController emailController = TextEditingController(); + final List otpControllers = + List.generate(4, (_) => TextEditingController()); final List focusNodes = List.generate(4, (_) => FocusNode()); @override @@ -30,7 +28,7 @@ class OTPController extends GetxController { @override void onClose() { _countdownTimer?.cancel(); - phoneController.dispose(); + emailController.dispose(); for (final controller in otpControllers) { controller.dispose(); } @@ -40,19 +38,28 @@ class OTPController extends GetxController { super.onClose(); } - static Future _sendOTP(String phone) async { - await Future.delayed(const Duration(seconds: 2)); - debugPrint('Sending OTP to $phone'); - return true; + Future _sendOTP(String email) async { + final result = await AuthService.generateOtp(email); + if (result == null) { + debugPrint('OTP sent to $email'); + return true; + } else { + showAppSnackbar( + title: "Error", + message: result['error'] ?? "Failed to send OTP", + type: SnackbarType.error, + ); + return false; + } } Future sendOTP() async { - final phone = phoneController.text.trim(); + final userEmail = emailController.text.trim(); - if (!_validatePhone(phone)) { + if (!_validateEmail(userEmail)) { showAppSnackbar( title: "Error", - message: "Please enter a valid 10-digit mobile number", + message: "Please enter a valid email address", type: SnackbarType.error, ); return; @@ -61,18 +68,12 @@ class OTPController extends GetxController { if (isSending.value) return; isSending.value = true; - final success = await _sendOTP(phone); + final success = await _sendOTP(userEmail); if (success) { - phoneNumber.value = phone; + email.value = userEmail; isOTPSent.value = true; _startTimer(); _clearOTPFields(); - } else { - showAppSnackbar( - title: "Error", - message: "Failed to send OTP. Please try again.", - type: SnackbarType.error, - ); } isSending.value = false; @@ -84,15 +85,9 @@ class OTPController extends GetxController { _clearOTPFields(); - final success = await _sendOTP(phoneNumber.value); + final success = await _sendOTP(email.value); if (success) { _startTimer(); - } else { - showAppSnackbar( - title: "Error", - message: "Failed to resend OTP. Please try again.", - type: SnackbarType.error, - ); } isResending.value = false; @@ -112,19 +107,35 @@ class OTPController extends GetxController { } } - void verifyOTP() { + Future verifyOTP() async { final enteredOTP = otpControllers.map((c) => c.text).join(); - if (enteredOTP.length != otpControllers.length || enteredOTP.contains('')) { + + final result = await AuthService.verifyOtp( + email: email.value, + otp: enteredOTP, + ); + + if (result == null) { + showAppSnackbar( + title: "Success", + message: "OTP verified successfully", + type: SnackbarType.success, + ); + final bool isMpinEnabled = LocalStorage.getIsMpin(); + print('MPIN Enabled? $isMpinEnabled'); + + if (isMpinEnabled) { + Get.toNamed('/auth/mpin-auth'); + } else { + Get.offAllNamed('/home'); + } + } else { showAppSnackbar( title: "Error", - message: "Please enter the complete ${otpControllers.length}-digit OTP", + message: result['error'] ?? "Failed to verify OTP", type: SnackbarType.error, ); - return; } - - // TODO: Add your OTP verification logic - debugPrint('Verifying OTP: $enteredOTP'); } void _clearOTPFields() { @@ -146,10 +157,10 @@ class OTPController extends GetxController { }); } - void resetForChangeNumber() { + void resetForChangeEmail() { isOTPSent.value = false; - phoneNumber.value = ''; - phoneController.clear(); + email.value = ''; + emailController.clear(); _clearOTPFields(); timer.value = 0; @@ -161,8 +172,8 @@ class OTPController extends GetxController { } } - bool _validatePhone(String phone) { - final regex = RegExp(r'^\d{10}$'); - return regex.hasMatch(phone); + bool _validateEmail(String email) { + final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,}$'); + return regex.hasMatch(email); } } diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index 1366e33..3fe7033 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -25,31 +25,7 @@ class AuthService { final responseData = jsonDecode(response.body); if (response.statusCode == 200 && responseData['data'] != null) { - final jwtToken = responseData['data']['token']; - final refreshToken = responseData['data']['refreshToken']; - final mpinToken = responseData['data']['mpinToken']; - - // Log the tokens using the logger - logger.i("JWT Token: $jwtToken"); - if (refreshToken != null) logger.i("Refresh Token: $refreshToken"); - if (mpinToken != null) logger.i("MPIN Token: $mpinToken"); - - await LocalStorage.setJwtToken(jwtToken); - await LocalStorage.setLoggedInUser(true); - - if (refreshToken != null) { - await LocalStorage.setRefreshToken(refreshToken); - } - if (mpinToken != null && mpinToken.isNotEmpty) { - await LocalStorage.setMpinToken(mpinToken); - await LocalStorage.setIsMpin(true); - } else { - await LocalStorage.setIsMpin(false); - await LocalStorage.removeMpinToken(); - } - - Get.put(PermissionController()); - isLoggedIn = true; + await _handleLoginSuccess(responseData['data']); return null; } else if (response.statusCode == 401) { return {"password": "Invalid email or password"}; @@ -300,4 +276,98 @@ class AuthService { return {"error": "Network error. Please check your connection."}; } } + + // Generate OTP API + static Future?> generateOtp(String email) async { + final requestBody = {"email": email}; + + logger.i("Sending generate OTP request: $requestBody"); + + try { + final response = await http.post( + Uri.parse("$_baseUrl/auth/send-otp"), + headers: _headers, + body: jsonEncode(requestBody), + ); + + logger.i( + "Generate OTP API response (${response.statusCode}): ${response.body}"); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200 && responseData['success'] == true) { + logger.i("OTP generated successfully."); + return null; + } else { + return {"error": responseData['message'] ?? "Failed to generate OTP."}; + } + } catch (e) { + logger.e("Exception during generate OTP: $e"); + return {"error": "Network error. Please check your connection."}; + } + } + +// Verify OTP API + static Future?> verifyOtp({ + required String email, + required String otp, + }) async { + final requestBody = { + "email": email, + "otp": otp, + }; + + logger.i("Sending verify OTP request: $requestBody"); + + try { + final response = await http.post( + Uri.parse("$_baseUrl/auth/login-otp"), + headers: _headers, + body: jsonEncode(requestBody), + ); + + logger.i( + "Verify OTP API response (${response.statusCode}): ${response.body}"); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200 && responseData['data'] != null) { + await _handleLoginSuccess(responseData['data']); + logger.i("OTP verified and login state initialized successfully."); + return null; + } else { + return {"error": responseData['message'] ?? "Failed to verify OTP."}; + } + } catch (e) { + logger.e("Exception during verify OTP: $e"); + return {"error": "Network error. Please check your connection."}; + } + } + + static Future _handleLoginSuccess(Map data) async { + final jwtToken = data['token']; + final refreshToken = data['refreshToken']; + final mpinToken = data['mpinToken']; + + logger.i("JWT Token: $jwtToken"); + if (refreshToken != null) logger.i("Refresh Token: $refreshToken"); + if (mpinToken != null) logger.i("MPIN Token: $mpinToken"); + + await LocalStorage.setJwtToken(jwtToken); + await LocalStorage.setLoggedInUser(true); + + if (refreshToken != null) { + await LocalStorage.setRefreshToken(refreshToken); + } + if (mpinToken != null && mpinToken.isNotEmpty) { + await LocalStorage.setMpinToken(mpinToken); + await LocalStorage.setIsMpin(true); + } else { + await LocalStorage.setIsMpin(false); + await LocalStorage.removeMpinToken(); + } + + Get.put(PermissionController()); + isLoggedIn = true; + } } diff --git a/lib/view/auth/otp_login_form.dart b/lib/view/auth/otp_login_form.dart index 596b6f3..1276e80 100644 --- a/lib/view/auth/otp_login_form.dart +++ b/lib/view/auth/otp_login_form.dart @@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/controller/auth/otp_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; class OTPLoginScreen extends StatefulWidget { const OTPLoginScreen({super.key}); @@ -29,42 +30,43 @@ class _OTPLoginScreenState extends State with UIMixin { Widget build(BuildContext context) { final theme = Theme.of(context); - return Obx(() { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium('OTP Verification', fontWeight: 600), - MySpacing.height(24), - if (!controller.isOTPSent.value) ...[ - _buildPhoneInput(), - MySpacing.height(24), - _buildSendOTPButton(theme), - ] else ...[ - _buildOTPSentInfo(), - MySpacing.height(12), - _buildOTPForm(theme), - MySpacing.height(12), - _buildResendOTPSection(theme), - MySpacing.height(12), - _buildVerifyOTPButton(theme), - ], - ], - ), - ); - }); + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('OTP Verification', fontWeight: 600), + MySpacing.height(24), + Obx(() => !controller.isOTPSent.value + ? Column( + children: [ + _buildEmailInput(), + MySpacing.height(24), + _buildSendOTPButton(theme), + ], + ) + : Column( + children: [ + _buildOTPSentInfo(), + MySpacing.height(12), + _buildOTPForm(theme), + MySpacing.height(12), + _buildResendOTPSection(theme), + MySpacing.height(12), + _buildVerifyOTPButton(theme), + ], + )), + ], + ), + ); } - Widget _buildPhoneInput() { + Widget _buildEmailInput() { return TextFormField( - controller: controller.phoneController, - keyboardType: TextInputType.phone, - maxLength: 10, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: controller.emailController, + keyboardType: TextInputType.emailAddress, decoration: InputDecoration( - labelText: 'Enter Mobile Number', + labelText: 'Enter Email Address', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - counterText: '', ), onFieldSubmitted: (_) => controller.sendOTP(), ); @@ -107,39 +109,23 @@ class _OTPLoginScreenState extends State with UIMixin { ), child: Row( children: [ - Icon(Icons.sms_outlined, color: Colors.redAccent, size: 24), + Icon(Icons.email_outlined, color: Colors.redAccent, size: 24), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.bodySmall( - 'OTP sent to', - fontWeight: 500, - ), - MyText.bodySmall( - '+91-${controller.phoneNumber.value}', - fontWeight: 700, - color: Colors.black, - ), + MyText.bodySmall('OTP sent to', fontWeight: 500), + MyText.bodySmall(controller.email.value, + fontWeight: 700, color: Colors.black), ], ), ), TextButton.icon( - onPressed: controller.resetForChangeNumber, + onPressed: controller.resetForChangeEmail, icon: Icon(Icons.edit, size: 16, color: Colors.redAccent), - label: MyText.bodySmall( - 'Change', - fontWeight: 600, - color: Colors.redAccent, - ), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: Colors.redAccent, - ), + label: MyText.bodySmall('Change', + fontWeight: 600, color: Colors.redAccent), ), ], ), @@ -170,6 +156,10 @@ class _OTPLoginScreenState extends State with UIMixin { keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) => controller.onOTPChanged(value, index), + validator: (value) { + if (value == null || value.isEmpty) return ''; + return null; + }, decoration: InputDecoration( counterText: '', filled: true, @@ -204,13 +194,9 @@ class _OTPLoginScreenState extends State with UIMixin { ? const SizedBox( width: 16, height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : MyText.bodySmall( - 'Resend OTP', - fontWeight: 600, - color: Colors.blueAccent, - ), + child: CircularProgressIndicator(strokeWidth: 2)) + : MyText.bodySmall('Resend OTP', + fontWeight: 600, color: Colors.blueAccent), ), ); } @@ -220,7 +206,17 @@ class _OTPLoginScreenState extends State with UIMixin { return Align( alignment: Alignment.center, child: MyButton.rounded( - onPressed: controller.verifyOTP, + onPressed: () { + if (_otpFormKey.currentState!.validate()) { + controller.verifyOTP(); + } else { + showAppSnackbar( + title: "Error", + message: "Please enter all 4 digits of the OTP", + type: SnackbarType.error, + ); + } + }, elevation: 2, padding: MySpacing.xy(24, 16), borderRadiusAll: 10,