feat: Refactor OTP authentication to use email instead of phone number
This commit is contained in:
parent
25dfcf3e08
commit
c253c14481
@ -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<FormState>();
|
||||
|
||||
// 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<TextEditingController> otpControllers = List.generate(4, (_) => TextEditingController());
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final List<TextEditingController> otpControllers =
|
||||
List.generate(4, (_) => TextEditingController());
|
||||
final List<FocusNode> 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<bool> _sendOTP(String phone) async {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
debugPrint('Sending OTP to $phone');
|
||||
return true;
|
||||
Future<bool> _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<void> 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<void> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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<Map<String, String>?> 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<Map<String, String>?> 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<void> _handleLoginSuccess(Map<String, dynamic> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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<OTPLoginScreen> 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<OTPLoginScreen> 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<OTPLoginScreen> 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<OTPLoginScreen> 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<OTPLoginScreen> 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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user