Feature_MPIN_OTP #46

Merged
vaibhav.surve merged 8 commits from Feature_MPIN_OTP into main 2025-06-11 09:38:35 +00:00
3 changed files with 208 additions and 131 deletions
Showing only changes of commit c253c14481 - Show all commits

View File

@ -2,23 +2,21 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_snackbar.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 { class OTPController extends GetxController {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
// Observables final RxString email = ''.obs;
final RxString phoneNumber = ''.obs;
final RxBool isOTPSent = false.obs; final RxBool isOTPSent = false.obs;
final RxBool isSending = false.obs; final RxBool isSending = false.obs;
final RxBool isResending = false.obs; final RxBool isResending = false.obs;
final RxInt timer = 0.obs; final RxInt timer = 0.obs;
Timer? _countdownTimer; Timer? _countdownTimer;
// Text controllers and focus nodes final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController(); final List<TextEditingController> otpControllers =
final List<TextEditingController> otpControllers = List.generate(4, (_) => TextEditingController()); List.generate(4, (_) => TextEditingController());
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode()); final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
@override @override
@ -30,7 +28,7 @@ class OTPController extends GetxController {
@override @override
void onClose() { void onClose() {
_countdownTimer?.cancel(); _countdownTimer?.cancel();
phoneController.dispose(); emailController.dispose();
for (final controller in otpControllers) { for (final controller in otpControllers) {
controller.dispose(); controller.dispose();
} }
@ -40,19 +38,28 @@ class OTPController extends GetxController {
super.onClose(); super.onClose();
} }
static Future<bool> _sendOTP(String phone) async { Future<bool> _sendOTP(String email) async {
await Future.delayed(const Duration(seconds: 2)); final result = await AuthService.generateOtp(email);
debugPrint('Sending OTP to $phone'); if (result == null) {
return true; 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 { Future<void> sendOTP() async {
final phone = phoneController.text.trim(); final userEmail = emailController.text.trim();
if (!_validatePhone(phone)) { if (!_validateEmail(userEmail)) {
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Please enter a valid 10-digit mobile number", message: "Please enter a valid email address",
type: SnackbarType.error, type: SnackbarType.error,
); );
return; return;
@ -61,18 +68,12 @@ class OTPController extends GetxController {
if (isSending.value) return; if (isSending.value) return;
isSending.value = true; isSending.value = true;
final success = await _sendOTP(phone); final success = await _sendOTP(userEmail);
if (success) { if (success) {
phoneNumber.value = phone; email.value = userEmail;
isOTPSent.value = true; isOTPSent.value = true;
_startTimer(); _startTimer();
_clearOTPFields(); _clearOTPFields();
} else {
showAppSnackbar(
title: "Error",
message: "Failed to send OTP. Please try again.",
type: SnackbarType.error,
);
} }
isSending.value = false; isSending.value = false;
@ -84,15 +85,9 @@ class OTPController extends GetxController {
_clearOTPFields(); _clearOTPFields();
final success = await _sendOTP(phoneNumber.value); final success = await _sendOTP(email.value);
if (success) { if (success) {
_startTimer(); _startTimer();
} else {
showAppSnackbar(
title: "Error",
message: "Failed to resend OTP. Please try again.",
type: SnackbarType.error,
);
} }
isResending.value = false; 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(); 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( showAppSnackbar(
title: "Error", title: "Error",
message: "Please enter the complete ${otpControllers.length}-digit OTP", message: result['error'] ?? "Failed to verify OTP",
type: SnackbarType.error, type: SnackbarType.error,
); );
return;
} }
// TODO: Add your OTP verification logic
debugPrint('Verifying OTP: $enteredOTP');
} }
void _clearOTPFields() { void _clearOTPFields() {
@ -146,10 +157,10 @@ class OTPController extends GetxController {
}); });
} }
void resetForChangeNumber() { void resetForChangeEmail() {
isOTPSent.value = false; isOTPSent.value = false;
phoneNumber.value = ''; email.value = '';
phoneController.clear(); emailController.clear();
_clearOTPFields(); _clearOTPFields();
timer.value = 0; timer.value = 0;
@ -161,8 +172,8 @@ class OTPController extends GetxController {
} }
} }
bool _validatePhone(String phone) { bool _validateEmail(String email) {
final regex = RegExp(r'^\d{10}$'); final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,}$');
return regex.hasMatch(phone); return regex.hasMatch(email);
} }
} }

View File

@ -25,31 +25,7 @@ class AuthService {
final responseData = jsonDecode(response.body); final responseData = jsonDecode(response.body);
if (response.statusCode == 200 && responseData['data'] != null) { if (response.statusCode == 200 && responseData['data'] != null) {
final jwtToken = responseData['data']['token']; await _handleLoginSuccess(responseData['data']);
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;
return null; return null;
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
return {"password": "Invalid email or password"}; return {"password": "Invalid email or password"};
@ -300,4 +276,98 @@ class AuthService {
return {"error": "Network error. Please check your connection."}; 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;
}
} }

View File

@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/controller/auth/otp_controller.dart'; import 'package:marco/controller/auth/otp_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class OTPLoginScreen extends StatefulWidget { class OTPLoginScreen extends StatefulWidget {
const OTPLoginScreen({super.key}); const OTPLoginScreen({super.key});
@ -29,42 +30,43 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Obx(() { return SingleChildScrollView(
return SingleChildScrollView( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ MyText.bodyMedium('OTP Verification', fontWeight: 600),
MyText.bodyMedium('OTP Verification', fontWeight: 600), MySpacing.height(24),
MySpacing.height(24), Obx(() => !controller.isOTPSent.value
if (!controller.isOTPSent.value) ...[ ? Column(
_buildPhoneInput(), children: [
MySpacing.height(24), _buildEmailInput(),
_buildSendOTPButton(theme), MySpacing.height(24),
] else ...[ _buildSendOTPButton(theme),
_buildOTPSentInfo(), ],
MySpacing.height(12), )
_buildOTPForm(theme), : Column(
MySpacing.height(12), children: [
_buildResendOTPSection(theme), _buildOTPSentInfo(),
MySpacing.height(12), MySpacing.height(12),
_buildVerifyOTPButton(theme), _buildOTPForm(theme),
], MySpacing.height(12),
], _buildResendOTPSection(theme),
), MySpacing.height(12),
); _buildVerifyOTPButton(theme),
}); ],
)),
],
),
);
} }
Widget _buildPhoneInput() { Widget _buildEmailInput() {
return TextFormField( return TextFormField(
controller: controller.phoneController, controller: controller.emailController,
keyboardType: TextInputType.phone, keyboardType: TextInputType.emailAddress,
maxLength: 10,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Enter Mobile Number', labelText: 'Enter Email Address',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
counterText: '',
), ),
onFieldSubmitted: (_) => controller.sendOTP(), onFieldSubmitted: (_) => controller.sendOTP(),
); );
@ -107,39 +109,23 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
), ),
child: Row( child: Row(
children: [ children: [
Icon(Icons.sms_outlined, color: Colors.redAccent, size: 24), Icon(Icons.email_outlined, color: Colors.redAccent, size: 24),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.bodySmall( MyText.bodySmall('OTP sent to', fontWeight: 500),
'OTP sent to', MyText.bodySmall(controller.email.value,
fontWeight: 500, fontWeight: 700, color: Colors.black),
),
MyText.bodySmall(
'+91-${controller.phoneNumber.value}',
fontWeight: 700,
color: Colors.black,
),
], ],
), ),
), ),
TextButton.icon( TextButton.icon(
onPressed: controller.resetForChangeNumber, onPressed: controller.resetForChangeEmail,
icon: Icon(Icons.edit, size: 16, color: Colors.redAccent), icon: Icon(Icons.edit, size: 16, color: Colors.redAccent),
label: MyText.bodySmall( label: MyText.bodySmall('Change',
'Change', fontWeight: 600, color: Colors.redAccent),
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,
),
), ),
], ],
), ),
@ -170,6 +156,10 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => controller.onOTPChanged(value, index), onChanged: (value) => controller.onOTPChanged(value, index),
validator: (value) {
if (value == null || value.isEmpty) return '';
return null;
},
decoration: InputDecoration( decoration: InputDecoration(
counterText: '', counterText: '',
filled: true, filled: true,
@ -204,13 +194,9 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
? const SizedBox( ? const SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2))
) : MyText.bodySmall('Resend OTP',
: MyText.bodySmall( fontWeight: 600, color: Colors.blueAccent),
'Resend OTP',
fontWeight: 600,
color: Colors.blueAccent,
),
), ),
); );
} }
@ -220,7 +206,17 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
return Align( return Align(
alignment: Alignment.center, alignment: Alignment.center,
child: MyButton.rounded( 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, elevation: 2,
padding: MySpacing.xy(24, 16), padding: MySpacing.xy(24, 16),
borderRadiusAll: 10, borderRadiusAll: 10,