322 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:marco/helpers/services/auth_service.dart';
 | |
| import 'package:marco/helpers/services/storage/local_storage.dart';
 | |
| import 'package:marco/helpers/widgets/my_form_validator.dart';
 | |
| import 'package:marco/helpers/widgets/my_snackbar.dart';
 | |
| import 'package:marco/helpers/services/app_logger.dart';
 | |
| import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
 | |
| import 'package:marco/controller/permission_controller.dart';
 | |
| import 'package:marco/controller/project_controller.dart';
 | |
| 
 | |
| class MPINController extends GetxController {
 | |
|   final MyFormValidator basicValidator = MyFormValidator();
 | |
|   final isNewUser = false.obs;
 | |
|   final isChangeMpin = false.obs;
 | |
|   final RxBool isLoading = false.obs;
 | |
|   final formKey = GlobalKey<FormState>();
 | |
| 
 | |
|   // Updated to 4-digit MPIN
 | |
|   final digitControllers = List.generate(4, (_) => TextEditingController());
 | |
|   final focusNodes = List.generate(4, (_) => FocusNode());
 | |
| 
 | |
|   final retypeControllers = List.generate(4, (_) => TextEditingController());
 | |
|   final retypeFocusNodes = List.generate(4, (_) => FocusNode());
 | |
| 
 | |
|   final RxInt failedAttempts = 0.obs;
 | |
| 
 | |
|   @override
 | |
|   void onInit() {
 | |
|     super.onInit();
 | |
|     final bool hasMpin = LocalStorage.getIsMpin();
 | |
|     isNewUser.value = !hasMpin;
 | |
|     logSafe("onInit called. isNewUser: ${isNewUser.value}");
 | |
|   }
 | |
| 
 | |
|   /// Enable Change MPIN mode
 | |
|   void setChangeMpinMode() {
 | |
|     isChangeMpin.value = true;
 | |
|     isNewUser.value = false;
 | |
|     clearFields();
 | |
|     clearRetypeFields();
 | |
|     logSafe("setChangeMpinMode activated");
 | |
|   }
 | |
| 
 | |
|   /// Handle digit entry and focus movement
 | |
|   void onDigitChanged(String value, int index, {bool isRetype = false}) {
 | |
|     logSafe(
 | |
|         "onDigitChanged -> index: $index, value: $value, isRetype: $isRetype");
 | |
|     final nodes = isRetype ? retypeFocusNodes : focusNodes;
 | |
|     if (value.isNotEmpty && index < 3) {
 | |
|       nodes[index + 1].requestFocus();
 | |
|     } else if (value.isEmpty && index > 0) {
 | |
|       nodes[index - 1].requestFocus();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Submit MPIN for verification or generation
 | |
|   Future<void> onSubmitMPIN() async {
 | |
|     logSafe("onSubmitMPIN triggered");
 | |
| 
 | |
|     if (!formKey.currentState!.validate()) {
 | |
|       logSafe("Form validation failed", level: LogLevel.warning);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     final enteredMPIN = digitControllers.map((c) => c.text).join();
 | |
|     logSafe("Entered MPIN: $enteredMPIN");
 | |
| 
 | |
|     if (enteredMPIN.length < 4) {
 | |
|       _showError("Please enter all 4 digits.");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (isNewUser.value || isChangeMpin.value) {
 | |
|       final retypeMPIN = retypeControllers.map((c) => c.text).join();
 | |
|       logSafe("Retyped MPIN: $retypeMPIN");
 | |
| 
 | |
|       if (retypeMPIN.length < 4) {
 | |
|         _showError("Please enter all 4 digits in Retype MPIN.");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (enteredMPIN != retypeMPIN) {
 | |
|         _showError("MPIN and Retype MPIN do not match.");
 | |
|         clearFields();
 | |
|         clearRetypeFields();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       final bool success = await generateMPIN(mpin: enteredMPIN);
 | |
| 
 | |
|       if (success) {
 | |
|         logSafe("MPIN generation/change successful.");
 | |
|         showAppSnackbar(
 | |
|           title: "Success",
 | |
|           message: isChangeMpin.value
 | |
|               ? "MPIN changed successfully."
 | |
|               : "MPIN generated successfully. Please login again.",
 | |
|           type: SnackbarType.success,
 | |
|         );
 | |
|         await LocalStorage.logout();
 | |
|       } else {
 | |
|         logSafe("MPIN generation/change failed.", level: LogLevel.warning);
 | |
|         clearFields();
 | |
|         clearRetypeFields();
 | |
|       }
 | |
|     } else {
 | |
|       logSafe("Existing user. Proceeding to verify MPIN.");
 | |
|       await verifyMPIN();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Forgot MPIN
 | |
|   Future<void> onForgotMPIN() async {
 | |
|     logSafe("onForgotMPIN called");
 | |
|     isNewUser.value = true;
 | |
|     isChangeMpin.value = false;
 | |
|     clearFields();
 | |
|     clearRetypeFields();
 | |
|   }
 | |
| 
 | |
|   /// Switch to login/enter MPIN screen
 | |
|   void switchToEnterMPIN() {
 | |
|     logSafe("switchToEnterMPIN called");
 | |
|     isNewUser.value = false;
 | |
|     isChangeMpin.value = false;
 | |
|     clearFields();
 | |
|     clearRetypeFields();
 | |
|   }
 | |
| 
 | |
|   /// Show error snackbar
 | |
|   void _showError(String message) {
 | |
|     logSafe("ERROR: $message", level: LogLevel.error);
 | |
|     showAppSnackbar(
 | |
|       title: "Error",
 | |
|       message: message,
 | |
|       type: SnackbarType.error,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /// Navigate to dashboard
 | |
|   /// Navigate to tenant selection after MPIN verification
 | |
|   void _navigateToTenantSelection({String? message}) {
 | |
|     if (message != null) {
 | |
|       logSafe("Navigating to Tenant Selection with message: $message");
 | |
|       showAppSnackbar(
 | |
|         title: "Success",
 | |
|         message: message,
 | |
|         type: SnackbarType.success,
 | |
|       );
 | |
|     }
 | |
|      Get.offAllNamed('/select-tenant');
 | |
|   }
 | |
| 
 | |
|   /// Clear the primary MPIN fields
 | |
|   void clearFields() {
 | |
|     logSafe("clearFields called");
 | |
|     for (final c in digitControllers) {
 | |
|       c.clear();
 | |
|     }
 | |
|     focusNodes.first.requestFocus();
 | |
|   }
 | |
| 
 | |
|   /// Clear the retype MPIN fields
 | |
|   void clearRetypeFields() {
 | |
|     logSafe("clearRetypeFields called");
 | |
|     for (final c in retypeControllers) {
 | |
|       c.clear();
 | |
|     }
 | |
|     retypeFocusNodes.first.requestFocus();
 | |
|   }
 | |
| 
 | |
|   /// Cleanup
 | |
|   @override
 | |
|   void onClose() {
 | |
|     logSafe("onClose called");
 | |
|     for (final controller in digitControllers) {
 | |
|       controller.dispose();
 | |
|     }
 | |
|     for (final node in focusNodes) {
 | |
|       node.dispose();
 | |
|     }
 | |
|     for (final controller in retypeControllers) {
 | |
|       controller.dispose();
 | |
|     }
 | |
|     for (final node in retypeFocusNodes) {
 | |
|       node.dispose();
 | |
|     }
 | |
|     super.onClose();
 | |
|   }
 | |
| 
 | |
|   /// Generate MPIN for new user/change MPIN
 | |
|   Future<bool> generateMPIN({required String mpin}) async {
 | |
|     try {
 | |
|       isLoading.value = true;
 | |
|       logSafe("generateMPIN started");
 | |
| 
 | |
|       final employeeInfo = LocalStorage.getEmployeeInfo();
 | |
|       final String? employeeId = employeeInfo?.id;
 | |
| 
 | |
|       if (employeeId == null || employeeId.isEmpty) {
 | |
|         isLoading.value = false;
 | |
|         _showError("Missing employee ID.");
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       logSafe("Calling AuthService.generateMpin for employeeId: $employeeId");
 | |
| 
 | |
|       final response = await AuthService.generateMpin(
 | |
|         employeeId: employeeId,
 | |
|         mpin: mpin,
 | |
|       );
 | |
| 
 | |
|       isLoading.value = false;
 | |
| 
 | |
|       if (response == null) {
 | |
|         return true;
 | |
|       } else {
 | |
|         logSafe("MPIN generation returned error: $response",
 | |
|             level: LogLevel.warning);
 | |
|         showAppSnackbar(
 | |
|           title: "MPIN Operation Failed",
 | |
|           message: "Please check your inputs.",
 | |
|           type: SnackbarType.error,
 | |
|         );
 | |
|         basicValidator.addErrors(response);
 | |
|         basicValidator.validateForm();
 | |
|         basicValidator.clearErrors();
 | |
|         return false;
 | |
|       }
 | |
|     } catch (e) {
 | |
|       isLoading.value = false;
 | |
|       logSafe("Exception in generateMPIN", level: LogLevel.error, error: e);
 | |
|       _showError("Failed to process MPIN.");
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Verify MPIN for existing user
 | |
|   Future<void> verifyMPIN() async {
 | |
|     logSafe("verifyMPIN triggered");
 | |
| 
 | |
|     final enteredMPIN = digitControllers.map((c) => c.text).join();
 | |
|     if (enteredMPIN.length < 4) {
 | |
|       _showError("Please enter all 4 digits.");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     final mpinToken = await LocalStorage.getMpinToken();
 | |
|     if (mpinToken == null || mpinToken.isEmpty) {
 | |
|       _showError("Missing MPIN token. Please log in again.");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       isLoading.value = true;
 | |
| 
 | |
|       final fcmToken = await FirebaseNotificationService().getFcmToken();
 | |
| 
 | |
|       final response = await AuthService.verifyMpin(
 | |
|         mpin: enteredMPIN,
 | |
|         mpinToken: mpinToken,
 | |
|         fcmToken: fcmToken ?? '',
 | |
|       );
 | |
| 
 | |
|       isLoading.value = false;
 | |
| 
 | |
|       if (response == null) {
 | |
|         logSafe("MPIN verified successfully");
 | |
|         await LocalStorage.setBool('mpin_verified', true);
 | |
| 
 | |
|         // 🔹 Ensure controllers are injected and loaded
 | |
|         final token = await LocalStorage.getJwtToken();
 | |
|         if (token != null && token.isNotEmpty) {
 | |
|           if (!Get.isRegistered<PermissionController>()) {
 | |
|             Get.put(PermissionController());
 | |
|             await Get.find<PermissionController>().loadData(token);
 | |
|           }
 | |
|           if (!Get.isRegistered<ProjectController>()) {
 | |
|             Get.put(ProjectController(), permanent: true);
 | |
|             await Get.find<ProjectController>().fetchProjects();
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         showAppSnackbar(
 | |
|           title: "Success",
 | |
|           message: "MPIN Verified Successfully",
 | |
|           type: SnackbarType.success,
 | |
|         );
 | |
|         _navigateToTenantSelection();
 | |
|       } else {
 | |
|         final errorMessage = response["error"] ?? "Invalid MPIN";
 | |
|         logSafe("MPIN verification failed: $errorMessage",
 | |
|             level: LogLevel.warning);
 | |
|         showAppSnackbar(
 | |
|           title: "Error",
 | |
|           message: errorMessage,
 | |
|           type: SnackbarType.error,
 | |
|         );
 | |
|         clearFields();
 | |
|         onInvalidMPIN();
 | |
|       }
 | |
|     } catch (e) {
 | |
|       isLoading.value = false;
 | |
|       logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
 | |
|       _showError("Something went wrong. Please try again.");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Increment failed attempts and warn
 | |
|   void onInvalidMPIN() {
 | |
|     failedAttempts.value++;
 | |
|     if (failedAttempts.value >= 3) {
 | |
|       showAppSnackbar(
 | |
|         title: "Error",
 | |
|         message: "Too many failed attempts. Consider logging in again.",
 | |
|         type: SnackbarType.error,
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| }
 |