From 0ee2d7213438eead830d79176a810cbf2065441c Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Sat, 24 May 2025 21:51:30 +0700 Subject: [PATCH] feat: Implement facial verification service and selfie verification controller - Added FacialVerificationService for handling face detection and comparison. - Created SelfieVerificationController to manage selfie capture and validation against ID card. - Integrated liveness detection functionality within the selfie verification process. - Developed SignupWithRoleController for user registration with role selection. - Introduced PersonalInfoController for managing user personal information during signup. - Added CountdownOverlayWidget for displaying countdown during liveness check. - Enhanced error handling and validation across controllers. --- .../docs/face_detection_troubleshooting.mdx | 180 +++++ .../presentasion/bindings/auth_bindings.dart | 10 +- .../identity_verification_controller.dart | 714 ------------------ .../email_verification_controller.dart | 0 .../forgot_password_controller.dart | 0 .../location_selection_controller.dart | 0 .../selfie_verification_controller.dart | 379 ---------- .../{basic => signin}/signin_controller.dart | 0 .../id_card_verification_controller.dart | 0 .../image_verification_controller.dart | 0 .../identity_verification_controller.dart | 516 +++++++++++++ .../officer_info_controller.dart | 0 .../unit_info_controller.dart | 0 .../registration_form_controller.dart | 16 +- .../face_liveness_detection_controller.dart | 127 +++- .../facial_verification_controller.dart | 2 +- .../selfie_verification_controller.dart | 257 +++++++ .../signup_with_role_controller.dart | 0 .../personal_info_controller.dart | 0 .../email_verification_screen.dart | 2 +- .../forgot-password/forgot_password.dart | 2 +- .../basic/id_card_verification_step.dart | 3 +- .../basic/identity_verification_step.dart | 204 ++--- .../basic/liveness_detection_screen.dart | 112 ++- .../basic/personal_info_step.dart | 3 +- .../basic/selfie_verification_step.dart | 8 +- .../basic/widgets/camera_preview_widget.dart | 264 ++++++- .../basic/widgets/captured_selfie_view.dart | 360 ++++++--- .../widgets/countdown_overlay_widget.dart | 79 ++ .../basic/widgets/debug_panel.dart | 4 +- .../basic/widgets/error_state_widget.dart | 2 +- .../basic/widgets/instruction_banner.dart | 167 ++-- .../widgets/verification_progress_widget.dart | 184 ++--- .../officer/officer_info_step.dart | 2 +- .../officer/unit_info_step.dart | 4 +- .../widgets/city_selection.dart | 2 +- .../face_verification_section.dart | 4 +- .../identity_verification/id_info_form.dart | 2 +- .../place_of_birth_field.dart | 2 +- .../verification_action_button.dart | 2 +- .../verification_status_message.dart | 2 +- .../pages/signin/signin_screen.dart | 2 +- .../pages/signup/signup_with_role_screen.dart | 2 +- .../src/utils/debug/liveness_debug_utils.dart | 6 +- 44 files changed, 1997 insertions(+), 1628 deletions(-) create mode 100644 sigap-mobile/docs/face_detection_troubleshooting.mdx delete mode 100644 sigap-mobile/lib/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{basic => others}/email_verification_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{basic => others}/forgot_password_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{basic => others}/location_selection_controller.dart (100%) delete mode 100644 sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{basic => signin}/signin_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/id-card-verification/id_card_verification_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/id-card-verification/image_verification_controller.dart (100%) create mode 100644 sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/officer-information/officer_info_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/officer-information/unit_info_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{basic => signup}/registration_form_controller.dart (98%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/selfie-verification/face_liveness_detection_controller.dart (88%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/selfie-verification/facial_verification_controller.dart (98%) create mode 100644 sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{basic => signup}/signup_with_role_controller.dart (100%) rename sigap-mobile/lib/src/features/auth/presentasion/controllers/{ => signup}/viewer-information/personal_info_controller.dart (100%) create mode 100644 sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/countdown_overlay_widget.dart diff --git a/sigap-mobile/docs/face_detection_troubleshooting.mdx b/sigap-mobile/docs/face_detection_troubleshooting.mdx new file mode 100644 index 0000000..986a72a --- /dev/null +++ b/sigap-mobile/docs/face_detection_troubleshooting.mdx @@ -0,0 +1,180 @@ +# Face Detection Troubleshooting Guide + +## Common Detection Issue + +If you're experiencing issues with face detection failing or consistently returning zero faces (`Detected 0 faces`), this guide will help you understand and resolve the problem. + +## Understanding the Problem + +The ML Kit face detection can fail for several reasons, but one of the most common is related to image orientation and processing. This is especially problematic on iOS devices where camera orientation metadata isn't correctly passed to the ML Kit detector. + +### Common Error Logs + +``` +[LIVENESS_CONTROLLER] Detected 0 faces +[LIVENESS_CONTROLLER] Detected 0 faces +[LIVENESS_CONTROLLER] Detected 0 faces +``` + +When you see these logs repeatedly, it means that the face detector is unable to identify any faces in the camera frames, even when a face is clearly visible to the user. + +## Root Causes + +1. **Image Orientation Issues**: On iOS especially, the orientation metadata in images captured from the camera may not be correctly interpreted by ML Kit. + +2. **Format Compatibility**: Different platforms use different image formats (YUV420, NV21, BGRA8888), and improper handling can lead to detection failures. + +3. **Image Processing**: Camera frames need proper conversion before they can be processed by ML Kit. + +4. **Resolution Issues**: Low-resolution images can make face detection difficult. + +## Solution Implementation + +Our solution implements several fixes based on common patterns found on GitHub and in the ML Kit community: + +### 1. Platform-Specific Image Format Handling + +```dart +cameraController = CameraController( + frontCamera, + ResolutionPreset.high, // Higher resolution + enableAudio: false, + imageFormatGroup: Platform.isIOS ? ImageFormatGroup.bgra8888 : ImageFormatGroup.yuv420, +); +``` + +We use different image format groups for iOS and Android to ensure compatibility. + +### 2. Proper Image Rotation + +For real-time detection, we carefully calculate the correct rotation: + +```dart +if (Platform.isIOS) { + // iOS-specific rotation handling + rotation = InputImageRotationValue.fromRawValue(sensorOrientation); +} else if (Platform.isAndroid) { + // Android-specific rotation calculation + var rotationCompensation = orientations[cameraController!.value.deviceOrientation]; + if (rotationCompensation == null) return null; + + if (camera.lensDirection == CameraLensDirection.front) { + rotationCompensation = (sensorOrientation + rotationCompensation) % 360; + } else { + rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; + } + + rotation = InputImageRotationValue.fromRawValue(rotationCompensation); +} +``` + +### 3. Platform-Specific Image Processing + +For Android, we handle YUV420 format which requires proper handling of multiple image planes: + +```dart +if (Platform.isAndroid) { + // Android requires proper YUV420 handling with all planes + final plane1 = image.planes[0]; + final plane2 = image.planes[1]; + final plane3 = image.planes[2]; + + return InputImage.fromBytes( + bytes: Uint8List.fromList([...plane1.bytes, ...plane2.bytes, ...plane3.bytes]), + metadata: InputImageMetadata( + size: Size(image.width.toDouble(), image.height.toDouble()), + rotation: rotation, + format: format, + bytesPerRow: plane1.bytesPerRow, + ), + ); +} +``` + +### 4. Fix for Captured Images on iOS + +When capturing a still image (as opposed to real-time stream processing), we apply a special fix for iOS to ensure proper orientation: + +```dart +Future _processAndFixImageOrientation(XFile originalImage) async { + if (Platform.isIOS) { + try { + // Get temp directory + final directory = await getApplicationDocumentsDirectory(); + final path = directory.path; + final filename = 'processed_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final outputPath = '$path/$filename'; + + // Read and decode + final imageBytes = await originalImage.readAsBytes(); + final originalDecodedImage = imglib.decodeImage(imageBytes); + + // Fix orientation + final orientedImage = imglib.bakeOrientation(originalDecodedImage); + + // Save + final processedImageFile = File(outputPath); + await processedImageFile.writeAsBytes(imglib.encodeJpg(orientedImage)); + + return XFile(processedImageFile.path); + } catch (e) { + return originalImage; // Fall back if processing fails + } + } + + // No fix needed for Android usually + return originalImage; +} +``` + +### 5. Image Verification + +We added a verification step that checks if a captured image actually contains a face: + +```dart +Future _verifyFaceInImage(XFile image) async { + try { + final inputImage = InputImage.fromFilePath(image.path); + final faces = await faceDetector.processImage(inputImage); + return faces.isNotEmpty; + } catch (e) { + return false; + } +} +``` + +## How These Fixes Work + +1. **For iOS**: We explicitly handle orientation using the `bakeOrientation` method from the `image` package, which ensures that the image pixels are correctly rotated in memory rather than relying on metadata. + +2. **For Android**: We handle all three YUV planes and ensure the correct rotation is calculated based on device orientation and camera direction. + +3. **For Both Platforms**: We use higher resolution settings and proper image format groups. + +## Additional Dependencies Required + +To implement these fixes, make sure to add these dependencies to your `pubspec.yaml`: + +```yaml +dependencies: + image: ^4.0.17 # For orientation baking + path_provider: ^2.0.15 # For accessing temporary directories +``` + +## Common Pitfalls + +1. **Missing Planes**: Always check that image planes are available before processing. +2. **Rotation Calculation**: Be careful with rotation calculation, as different platforms and camera directions require different formulas. +3. **Memory Usage**: Higher resolution images improve detection but consume more memory. Monitor performance. +4. **Permissions**: Ensure camera permissions are properly granted. + +## References + +- [Google ML Kit Documentation](https://developers.google.com/ml-kit) +- [Flutter Camera Plugin](https://pub.dev/packages/camera) +- [Image Package Documentation](https://pub.dev/packages/image) +- [Face Detection GitHub Issue - Flutter](https://github.com/flutter/flutter/issues/79116) + +## Conclusion + +Face detection issues can often be fixed by properly handling image orientation and format. The implementation details vary by platform, so it's important to have platform-specific handling. By applying these fixes, you should see a significant improvement in face detection reliability. diff --git a/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart b/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart index ab05c2a..dae8a7c 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/bindings/auth_bindings.dart @@ -1,10 +1,10 @@ import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/email_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/forgot_password_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/signin_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/others/email_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/others/forgot_password_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/other/registration_form_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/signup_with_role_controller.dart'; class AuthControllerBindings extends Bindings { @override diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart deleted file mode 100644 index 7a8daba..0000000 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart +++ /dev/null @@ -1,714 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sigap/src/cores/services/azure_ocr_service.dart'; -import 'package:sigap/src/features/auth/data/models/face_model.dart'; -import 'package:sigap/src/features/auth/data/models/kta_model.dart'; -import 'package:sigap/src/features/auth/data/models/ktp_model.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart'; - -class IdentityVerificationController extends GetxController { - // Singleton instance - static IdentityVerificationController get instance => Get.find(); - - // Dependencies - final bool isOfficer; - final AzureOCRService _ocrService = AzureOCRService(); - final FacialVerificationService _faceService = - FacialVerificationService.instance; - - // Local storage keys - static const String _kOcrResultsKey = 'ocr_results'; - static const String _kOcrModelKey = 'ocr_model'; - static const String _kIdCardTypeKey = 'id_card_type'; - - // Controllers for form fields - final TextEditingController nikController = TextEditingController(); - final TextEditingController fullNameController = TextEditingController(); - final TextEditingController placeOfBirthController = TextEditingController(); - final TextEditingController birthDateController = TextEditingController(); - final TextEditingController addressController = TextEditingController(); - - // Form validation errors - final RxString nikError = RxString(''); - final RxString fullNameError = RxString(''); - final RxString placeOfBirthError = RxString(''); - final RxString birthDateError = RxString(''); - final RxString genderError = RxString(''); - final RxString addressError = RxString(''); - - // ID verification states - final RxBool isVerifying = RxBool(false); - final RxBool isVerified = RxBool(false); - final RxString verificationMessage = RxString(''); - - // Face verification states - final RxBool isVerifyingFace = RxBool(false); - final RxBool isFaceVerified = RxBool(false); - final RxString faceVerificationMessage = RxString(''); - final Rx faceComparisonResult = - Rx(null); - - // Gender selection dropdown - final Rx selectedGender = Rx('Male'); - - // Form validation state - final RxBool isFormValid = RxBool(true); - - // Flag to prevent infinite loop - bool _isApplyingData = false; - - // UI control states - final RxBool isNikReadOnly = RxBool(false); - final RxBool isPreFilledNik = false.obs; - - // Storage for extracted data - final RxMap ocrData = RxMap({}); - final String? extractedIdCardNumber; - final String? extractedName; - - // Data saving states - final RxBool isSavingData = RxBool(false); - final RxBool isDataSaved = RxBool(false); - final RxString dataSaveMessage = RxString(''); - - // Summary data for review page - final RxMap summaryData = RxMap({}); - - // Verification status of different sections - final RxBool isBasicInfoVerified = RxBool(false); - final RxBool isIdCardVerified = RxBool(false); - final RxBool isSelfieVerified = RxBool(false); - final RxBool isContactInfoVerified = RxBool(false); - final RxBool isLoadingSummary = RxBool(false); - - IdentityVerificationController({ - this.extractedIdCardNumber = '', - this.extractedName = '', - required this.isOfficer, - }); - - @override - void onInit() { - super.onInit(); - // Set default gender value - selectedGender.value = selectedGender.value ?? 'Male'; - - // Load data in sequence - _initializeData(); - } - - // Initialize all data in sequence - Future _initializeData() async { - try { - // First load OCR data - await loadOcrDataFromLocalStorage(); - - // Then load data from previous steps for summary - await loadAllStepsData(); - } catch (e) { - print('Error initializing data: $e'); - } - } - - // Load data from all previous steps - Future loadAllStepsData() async { - try { - isLoadingSummary.value = true; - - // Get references to all controllers - final registrationController = Get.find(); - final idCardController = Get.find(); - final selfieController = Get.find(); - - // Load data from each controller - _loadBasicInfoData(registrationController); - _loadIdCardData(idCardController); - _loadSelfieData(selfieController); - - // Pre-fill form with extracted data - _prefillFormWithExtractedData(idCardController); - } catch (e) { - print('Error loading steps data: $e'); - } finally { - isLoadingSummary.value = false; - } - } - - // Load basic information - void _loadBasicInfoData(FormRegistrationController controller) { - try { - // Add basic info to summary - summaryData['email'] = SignupWithRoleController().emailController.text; - summaryData['phone'] = PersonalInfoController.instance.phoneController.text; - summaryData['role'] = controller.selectedRole.value?.name ?? 'Unknown'; - - isBasicInfoVerified.value = true; - } catch (e) { - print('Error loading basic info: $e'); - isBasicInfoVerified.value = false; - } - } - - // Load ID card data - void _loadIdCardData(IdCardVerificationController controller) { - try { - // Add ID card info to summary - summaryData['idCardType'] = isOfficer ? 'KTA' : 'KTP'; - summaryData['idCardValid'] = controller.isIdCardValid.value; - summaryData['idCardConfirmed'] = controller.hasConfirmedIdCard.value; - summaryData['extractedInfo'] = controller.extractedInfo; - - // Add model data - if (isOfficer) { - summaryData['ktaModel'] = controller.ktaModel.value?.toJson(); - } else { - summaryData['ktpModel'] = controller.ktpModel.value?.toJson(); - } - - isIdCardVerified.value = - controller.isIdCardValid.value && controller.hasConfirmedIdCard.value; - } catch (e) { - print('Error loading ID card data: $e'); - isIdCardVerified.value = false; - } - } - - // Load selfie data - void _loadSelfieData(SelfieVerificationController controller) { - try { - // Add selfie verification info to summary - summaryData['selfieValid'] = controller.isSelfieValid.value; - summaryData['selfieConfirmed'] = controller.hasConfirmedSelfie.value; - summaryData['livenessCheckPassed'] = - controller.isLivenessCheckPassed.value; - summaryData['faceMatchResult'] = controller.isMatchWithIDCard.value; - summaryData['faceMatchConfidence'] = controller.matchConfidence.value; - - isSelfieVerified.value = - controller.isSelfieValid.value && controller.hasConfirmedSelfie.value; - } catch (e) { - print('Error loading selfie data: $e'); - isSelfieVerified.value = false; - } - } - - // Pre-fill form with extracted data - void _prefillFormWithExtractedData(IdCardVerificationController controller) { - try { - if (!isOfficer && controller.ktpModel.value != null) { - // Extract KTP data - final ktp = controller.ktpModel.value!; - - if (ktp.nik.isNotEmpty) { - nikController.text = ktp.nik; - summaryData['nik'] = ktp.nik; - } - - if (ktp.name.isNotEmpty) { - fullNameController.text = ktp.name; - summaryData['fullName'] = ktp.name; - } - - if (ktp.birthPlace.isNotEmpty) { - placeOfBirthController.text = ktp.birthPlace; - summaryData['placeOfBirth'] = ktp.birthPlace; - } - - if (ktp.birthDate.isNotEmpty) { - birthDateController.text = ktp.birthDate; - summaryData['birthDate'] = ktp.birthDate; - } - - if (ktp.gender.isNotEmpty) { - // Convert gender to the format expected by the dropdown - String gender = ktp.gender.toLowerCase(); - if (gender.contains('laki') || gender == 'male') { - selectedGender.value = 'Male'; - } else if (gender.contains('perempuan') || gender == 'female') { - selectedGender.value = 'Female'; - } - summaryData['gender'] = selectedGender.value; - } - - if (ktp.address.isNotEmpty) { - addressController.text = ktp.address; - summaryData['address'] = ktp.address; - } - - // Make NIK field read-only since it's extracted from KTP - isNikReadOnly.value = true; - } else if (isOfficer && controller.ktaModel.value != null) { - // Extract KTA data - final kta = controller.ktaModel.value!; - - if (kta.name.isNotEmpty) { - fullNameController.text = kta.name; - summaryData['fullName'] = kta.name; - } - - // Extract birth date from extra data if available - if (kta.extraData != null && - kta.extraData!.containsKey('tanggal_lahir') && - kta.extraData!['tanggal_lahir'] != null) { - birthDateController.text = kta.extraData!['tanggal_lahir']; - summaryData['birthDate'] = kta.extraData!['tanggal_lahir']; - } - } - } catch (e) { - print('Error prefilling form with extracted data: $e'); - } - } - - // Load OCR data from local storage - Future loadOcrDataFromLocalStorage() async { - try { - final prefs = await SharedPreferences.getInstance(); - - final String? storedIdCardType = prefs.getString(_kIdCardTypeKey); - print( - 'Stored ID card type: $storedIdCardType, Current isOfficer: $isOfficer', - ); - - if (storedIdCardType == null || - (isOfficer && storedIdCardType != 'KTA') || - (!isOfficer && storedIdCardType != 'KTP')) { - print('No matching ID card data found in storage or type mismatch'); - return; - } - - final String? jsonData = prefs.getString(_kOcrResultsKey); - if (jsonData != null) { - print('Found OCR data in storage: ${jsonData.length} chars'); - final Map results = jsonDecode(jsonData); - ocrData.assignAll(results); - print('OCR data loaded: ${results.length} items'); - - final String? modelJson = prefs.getString(_kOcrModelKey); - if (modelJson != null) { - try { - if (isOfficer) { - final ktaModel = KtaModel.fromJson(jsonDecode(modelJson)); - print('KTA model loaded successfully'); - applyKtaDataToForm(ktaModel); - } else { - final ktpModel = KtpModel.fromJson(jsonDecode(modelJson)); - print('KTP model loaded successfully - NIK: ${ktpModel.nik}'); - applyKtpDataToForm(ktpModel); - } - isNikReadOnly.value = true; - } catch (e) { - print('Error parsing model JSON: $e'); - } - } - } else { - print('No OCR data found in storage'); - } - } catch (e) { - print('Error loading OCR data from local storage: $e'); - } finally { - if (ocrData.isEmpty) { - print('Falling back to FormRegistrationController data'); - _safeApplyIdCardData(); - } - } - } - - // Apply KTP data to form - void applyKtpDataToForm(KtpModel ktpModel) { - if (ktpModel.nik.isNotEmpty) nikController.text = ktpModel.nik; - if (ktpModel.name.isNotEmpty) fullNameController.text = ktpModel.name; - if (ktpModel.birthPlace.isNotEmpty) - placeOfBirthController.text = ktpModel.birthPlace; - if (ktpModel.birthDate.isNotEmpty) - birthDateController.text = ktpModel.birthDate; - - if (ktpModel.gender.isNotEmpty) { - String gender = ktpModel.gender.toLowerCase(); - if (gender.contains('laki') || gender == 'male') { - selectedGender.value = 'Male'; - } else if (gender.contains('perempuan') || gender == 'female') { - selectedGender.value = 'Female'; - } - } - - if (ktpModel.address.isNotEmpty) addressController.text = ktpModel.address; - - // Mark as verified - isVerified.value = true; - verificationMessage.value = 'KTP information loaded successfully'; - } - - // Apply KTA data to form - void applyKtaDataToForm(KtaModel ktaModel) { - if (ktaModel.name.isNotEmpty) fullNameController.text = ktaModel.name; - - if (ktaModel.extraData != null && - ktaModel.extraData!.containsKey('tanggal_lahir') && - ktaModel.extraData!['tanggal_lahir'] != null) { - birthDateController.text = ktaModel.extraData!['tanggal_lahir']; - } - - isVerified.value = true; - verificationMessage.value = 'KTA information loaded successfully'; - } - - // Safe method to apply ID card data without risk of stack overflow - void _safeApplyIdCardData() { - if (_isApplyingData) return; - - try { - _isApplyingData = true; - - if (!Get.isRegistered()) return; - - final formController = Get.find(); - if (formController.idCardData.value == null) return; - - final idCardData = formController.idCardData.value; - if (idCardData != null) { - if (!isOfficer && idCardData is KtpModel) { - applyKtpDataToForm(idCardData); - isNikReadOnly.value = true; - } else if (isOfficer && idCardData is KtaModel) { - applyKtaDataToForm(idCardData); - } - } - } catch (e) { - print('Error applying ID card data: $e'); - } finally { - _isApplyingData = false; - } - } - - // Validate form inputs - bool validate(GlobalKey? formKey) { - isFormValid.value = true; - clearErrors(); - - // Validate required fields based on officer status - if (!isOfficer) { - // KTP validation - if (nikController.text.isEmpty) { - nikError.value = 'NIK is required'; - isFormValid.value = false; - } else if (nikController.text.length != 16) { - nikError.value = 'NIK must be 16 digits'; - isFormValid.value = false; - } - - if (fullNameController.text.isEmpty) { - fullNameError.value = 'Full name is required'; - isFormValid.value = false; - } - - if (placeOfBirthController.text.isEmpty) { - placeOfBirthError.value = 'Place of birth is required'; - isFormValid.value = false; - } - } else { - // KTA validation - if (fullNameController.text.isEmpty) { - fullNameError.value = 'Full name is required'; - isFormValid.value = false; - } - } - - // Common validations - if (birthDateController.text.isEmpty) { - birthDateError.value = 'Birth date is required'; - isFormValid.value = false; - } - - if (selectedGender.value == null) { - genderError.value = 'Gender is required'; - isFormValid.value = false; - } - - // Verify previous steps completion - bool allPreviousStepsCompleted = - isBasicInfoVerified.value && - isIdCardVerified.value && - isSelfieVerified.value; - - if (!allPreviousStepsCompleted) { - isFormValid.value = false; - verificationMessage.value = - 'Please complete all previous steps before submitting'; - } - - // Update summary data - _updateSummaryWithFormData(); - - return isFormValid.value; - } - - // Update summary with current form data - void _updateSummaryWithFormData() { - summaryData['nik'] = nikController.text; - summaryData['fullName'] = fullNameController.text; - summaryData['placeOfBirth'] = placeOfBirthController.text; - summaryData['birthDate'] = birthDateController.text; - summaryData['gender'] = selectedGender.value; - summaryData['address'] = addressController.text; - } - - // Verify ID card with OCR data - void verifyIdCardWithOCR() { - try { - isVerifying.value = true; - - final formController = Get.find(); - final idCardData = formController.idCardData.value; - - if (idCardData != null) { - if (!isOfficer && idCardData is KtpModel) { - bool nikMatches = nikController.text == idCardData.nik; - bool nameMatches = _compareNames( - fullNameController.text, - idCardData.name, - ); - - if (nikMatches && nameMatches) { - isVerified.value = true; - verificationMessage.value = - 'KTP information verified successfully!'; - } else { - isVerified.value = false; - verificationMessage.value = - 'Information doesn\'t match with KTP. Please check and try again.'; - } - } else if (isOfficer && idCardData is KtaModel) { - bool nameMatches = _compareNames( - fullNameController.text, - idCardData.name, - ); - - if (nameMatches) { - isVerified.value = true; - verificationMessage.value = - 'KTA information verified successfully!'; - } else { - isVerified.value = false; - verificationMessage.value = - 'Information doesn\'t match with KTA. Please check and try again.'; - } - } - } else { - isVerified.value = false; - verificationMessage.value = - 'No ID card data available from previous step.'; - } - } catch (e) { - isVerified.value = false; - verificationMessage.value = 'Error during verification: ${e.toString()}'; - print('Error in ID card verification: $e'); - } finally { - isVerifying.value = false; - } - } - - // Compare names accounting for formatting differences - bool _compareNames(String name1, String name2) { - String normalizedName1 = name1.toLowerCase().trim().replaceAll( - RegExp(r'\s+'), - ' ', - ); - String normalizedName2 = name2.toLowerCase().trim().replaceAll( - RegExp(r'\s+'), - ' ', - ); - - if (normalizedName1 == normalizedName2) return true; - - if (normalizedName1.contains(normalizedName2) || - normalizedName2.contains(normalizedName1)) - return true; - - var parts1 = normalizedName1.split(' '); - var parts2 = normalizedName2.split(' '); - - int matches = 0; - for (var part1 in parts1) { - for (var part2 in parts2) { - if (part1.length > 2 && - part2.length > 2 && - (part1.contains(part2) || part2.contains(part1))) { - matches++; - break; - } - } - } - - return matches >= (parts1.length / 2).floor(); - } - - // Verify face match using FacialVerificationService - void verifyFaceMatch() { - if (_faceService.skipFaceVerification) { - // Development mode - use dummy data - isFaceVerified.value = true; - faceVerificationMessage.value = - 'Face verification skipped (development mode)'; - - final idCardController = Get.find(); - final selfieController = Get.find(); - - if (idCardController.idCardImage.value != null && - selfieController.selfieImage.value != null) { - faceComparisonResult.value = FaceComparisonResult( - sourceFace: FaceModel( - imagePath: idCardController.idCardImage.value!.path, - faceId: 'dummy-id-card-id', - confidence: 0.95, - boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, - ), - targetFace: FaceModel( - imagePath: selfieController.selfieImage.value!.path, - faceId: 'dummy-selfie-id', - confidence: 0.95, - boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, - ), - isMatch: true, - confidence: 0.92, - message: 'Face verification passed (development mode)', - ); - } - return; - } - - isVerifyingFace.value = true; - - final idCardController = Get.find(); - final selfieController = Get.find(); - - if (idCardController.idCardImage.value == null || - selfieController.selfieImage.value == null) { - isFaceVerified.value = false; - faceVerificationMessage.value = - 'Both ID card and selfie are required for face verification.'; - isVerifyingFace.value = false; - return; - } - - _faceService - .compareFaces( - idCardController.idCardImage.value!, - selfieController.selfieImage.value!, - ) - .then((result) { - faceComparisonResult.value = result; - isFaceVerified.value = result.isMatch; - faceVerificationMessage.value = result.message; - }) - .catchError((e) { - isFaceVerified.value = false; - faceVerificationMessage.value = 'Error during face verification: $e'; - print('Face verification error: $e'); - }) - .whenComplete(() { - isVerifyingFace.value = false; - }); - } - - // Clear all validation errors - void clearErrors() { - nikError.value = ''; - fullNameError.value = ''; - placeOfBirthError.value = ''; - birthDateError.value = ''; - genderError.value = ''; - addressError.value = ''; - isFormValid.value = true; - } - - // Prefill form with extracted data - void prefillExtractedData() { - if (extractedIdCardNumber != null && extractedIdCardNumber!.isNotEmpty) { - nikController.text = extractedIdCardNumber!; - } - - if (extractedName != null && extractedName!.isNotEmpty) { - fullNameController.text = extractedName!; - } - - isPreFilledNik.value = true; - } - - // Save registration data - Future saveRegistrationData() async { - try { - isSavingData.value = true; - dataSaveMessage.value = 'Saving your registration data...'; - - // Final validation - if (!validate(null)) { - dataSaveMessage.value = 'Please fix the errors before submitting'; - return false; - } - - // Update summary with final data - _updateSummaryWithFormData(); - - // Format data according to models - Map formattedData = { - // Match format from summaryData to match ProfileModel and OfficerModel - 'nik': nikController.text, - 'fullName': fullNameController.text, - 'placeOfBirth': placeOfBirthController.text, - 'birthDate': birthDateController.text, - 'gender': selectedGender.value, - 'address': addressController.text, - }; - - // Add all other summary data for completeness - summaryData.forEach((key, value) { - if (!formattedData.containsKey(key)) { - formattedData[key] = value; - } - }); - - // Use FormRegistrationController for actual submission - final formController = Get.find(); - final result = await formController.saveRegistrationData( - summaryData: formattedData, - ); - - if (result) { - isDataSaved.value = true; - dataSaveMessage.value = 'Registration data saved successfully!'; - } else { - isDataSaved.value = false; - dataSaveMessage.value = - 'Failed to save registration data. Please try again.'; - } - - return result; - } catch (e) { - isDataSaved.value = false; - dataSaveMessage.value = 'Error saving registration data: $e'; - print('Error saving registration data: $e'); - return false; - } finally { - isSavingData.value = false; - } - } - - @override - void onClose() { - // Dispose controllers to prevent memory leaks - nikController.dispose(); - fullNameController.dispose(); - placeOfBirthController.dispose(); - birthDateController.dispose(); - addressController.dispose(); - super.onClose(); - } -} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/email_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/others/email_verification_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/email_verification_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/others/email_verification_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/forgot_password_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/others/forgot_password_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/forgot_password_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/others/forgot_password_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/location_selection_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/others/location_selection_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/location_selection_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/others/location_selection_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart deleted file mode 100644 index be452a7..0000000 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart +++ /dev/null @@ -1,379 +0,0 @@ -import 'dart:io'; - -import 'package:get/get.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:sigap/src/features/auth/data/models/face_model.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart'; -import 'package:sigap/src/utils/constants/app_routes.dart'; -import 'package:sigap/src/utils/helpers/error_handler.dart'; -import 'package:sigap/src/utils/helpers/error_utils.dart'; - -class SelfieVerificationController extends GetxController { - // MARK: - Dependencies - final FacialVerificationService _facialVerificationService = - FacialVerificationService.instance; - late FaceLivenessController _livenessController; - - // MARK: - Constants - final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB - - // Form validation - final RxBool isFormValid = RxBool(true); - final RxString selfieError = RxString(''); - final RxString selfieValidationMessage = RxString(''); - - // Image state - final Rx selfieImage = Rx(null); - final Rx selfieFace = Rx(FaceModel.empty()); - - // Process state flags - final RxBool isVerifyingFace = RxBool(false); - final RxBool isUploadingSelfie = RxBool(false); - final RxBool isPerformingLivenessCheck = RxBool(false); - final RxBool isComparingWithIDCard = RxBool(false); - - // Verification results - final RxBool isSelfieValid = RxBool(false); - final RxBool isLivenessCheckPassed = RxBool(false); - final RxBool hasConfirmedSelfie = RxBool(false); - - // Face comparison results - final Rx faceComparisonResult = - Rx(null); - final RxBool isMatchWithIDCard = RxBool(false); - final RxDouble matchConfidence = RxDouble(0.0); - final RxString selfieImageFaceId = RxString(''); - - @override - void onInit() { - super.onInit(); - _livenessController = Get.put(FaceLivenessController()); - - // Listen to liveness detection completion - ever(_livenessController.status, (LivenessStatus status) { - if (status == LivenessStatus.completed && - _livenessController.capturedImage != null) { - // When liveness check completes successfully, update selfie data - _processCapturedLivenessImage(); - } - }); - } - - // Process the image captured during liveness detection - Future _processCapturedLivenessImage() async { - if (_livenessController.capturedImage == null) return; - - try { - // Update selfie data - selfieImage.value = _livenessController.capturedImage; - - // Generate face model from liveness controller - selfieFace.value = _livenessController.generateFaceModel(); - isLivenessCheckPassed.value = true; - isSelfieValid.value = true; - selfieImageFaceId.value = selfieFace.value.faceId; - selfieValidationMessage.value = - 'Liveness check passed! Your face is verified.'; - - // Now that we have a valid selfie with liveness verification, compare with ID card - await compareWithIDCardPhoto(); - } catch (e) { - _handleError('Failed to process liveness image', e); - } finally { - isPerformingLivenessCheck.value = false; - } - } - - // Process the image captured during liveness detection - public for debugging - Future processCapturedLivenessImage() async { - return _processCapturedLivenessImage(); - } - - // MARK: - Public Methods - - /// Validate the selfie data for form submission - bool validate() { - clearErrors(); - - if (selfieImage.value == null) { - selfieError.value = 'Please take a selfie for verification'; - isFormValid.value = false; - } else if (!isSelfieValid.value) { - selfieError.value = 'Your selfie image is not valid'; - isFormValid.value = false; - } else if (!hasConfirmedSelfie.value) { - selfieError.value = 'Please confirm your selfie image'; - isFormValid.value = false; - } - - return isFormValid.value; - } - - /// Clear all error messages - void clearErrors() { - selfieError.value = ''; - selfieValidationMessage.value = ''; - } - - /// Perform liveness detection - Future performLivenessDetection() async { - try { - _setLoading(isPerformingLivenessCheck: true); - - // Reset any existing selfie data - _resetVerificationData(); - - // Navigate to liveness detection page - final result = await Get.toNamed(AppRoutes.livenessDetection); - - // If user cancelled or closed the screen without completing - if (result == null) { - _setLoading(isPerformingLivenessCheck: false); - } - // Processing will continue when liveness detection is complete, - // handled by _processCapturedLivenessImage() via the status listener - } catch (e) { - _handleError('Failed to start liveness detection', e); - _setLoading(isPerformingLivenessCheck: false); - } - } - - /// Clear Selfie Image and reset all verification data - void clearSelfieImage() { - selfieImage.value = null; - _resetVerificationData(); - } - - /// Confirm the selfie image after validation - void confirmSelfieImage() { - if (isSelfieValid.value && isMatchWithIDCard.value) { - hasConfirmedSelfie.value = true; - } - } - - /// Manual trigger for comparing with ID card - Future verifyFaceMatchWithIDCard() async { - if (selfieImage.value == null) { - selfieError.value = 'Please take a selfie first'; - return; - } - - try { - // Get the ID card controller - final idCardController = Get.find(); - - if (idCardController.idCardImage.value == null) { - selfieValidationMessage.value = - 'ID card image is required for comparison'; - return; - } - - // Compare with ID card photo - await compareWithIDCardPhoto(); - } catch (e) { - selfieValidationMessage.value = 'Face verification failed: $e'; - } - } - - // MARK: - Private Helper Methods - - /// Pick an image from the specified source - Future _pickImage(ImageSource source) async { - final ImagePicker picker = ImagePicker(); - return picker.pickImage( - source: source, - preferredCameraDevice: CameraDevice.front, - imageQuality: 80, - ); - } - - /// Check if a file size is within the allowed limit - Future _isFileSizeValid(XFile file) async { - final fileSize = await File(file.path).length(); - return fileSize <= maxFileSizeBytes; - } - - /// Update face data with new liveness check results - void _updateFaceData(FaceModel face) { - selfieFace.value = face; - isLivenessCheckPassed.value = face.isLive; - selfieImageFaceId.value = face.faceId; - isSelfieValid.value = face.isLive; - selfieValidationMessage.value = face.message; - } - - /// Update comparison results - void _updateComparisonResult(FaceComparisonResult result) { - faceComparisonResult.value = result; - isMatchWithIDCard.value = result.isMatch; - matchConfidence.value = result.confidence; - selfieValidationMessage.value = result.message; - } - - /// Reset all verification-related data - void _resetVerificationData() { - // Clear validation state - selfieError.value = ''; - selfieValidationMessage.value = ''; - isSelfieValid.value = false; - isLivenessCheckPassed.value = false; - hasConfirmedSelfie.value = false; - - // Clear face data - selfieFace.value = FaceModel.empty(); - - // Clear comparison data - faceComparisonResult.value = null; - isMatchWithIDCard.value = false; - matchConfidence.value = 0.0; - selfieImageFaceId.value = ''; - } - - /// Handle errors in a consistent way - void _handleError(String baseMessage, dynamic error) { - print('$baseMessage: $error'); - selfieError.value = ErrorHandler.getUIErrorMessage(error); - isSelfieValid.value = false; - } - - /// Set loading states in a consistent way - void _setLoading({ - bool? isVerifyingFace, - bool? isUploadingSelfie, - bool? isPerformingLivenessCheck, - bool? isComparingWithIDCard, - }) { - if (isVerifyingFace != null) this.isVerifyingFace.value = isVerifyingFace; - if (isUploadingSelfie != null) - this.isUploadingSelfie.value = isUploadingSelfie; - if (isPerformingLivenessCheck != null) - this.isPerformingLivenessCheck.value = isPerformingLivenessCheck; - if (isComparingWithIDCard != null) - this.isComparingWithIDCard.value = isComparingWithIDCard; - } - - /// Compare selfie with ID card photo - Future compareWithIDCardPhoto() async { - final idCardController = Get.find(); - - if (selfieImage.value == null || - idCardController.idCardImage.value == null) { - print('Cannot compare faces: Missing images'); - return; - } - - try { - _setLoading(isComparingWithIDCard: true); - - if (_facialVerificationService.skipFaceVerification) { - await _handleDevelopmentModeComparison(idCardController); - return; - } - - // Pass the existing face models if available to avoid redundant detection - FaceModel? sourceFace = - idCardController.idCardFace.value.hasValidFace - ? idCardController.idCardFace.value - : null; - FaceModel? targetFace = - selfieFace.value.hasValidFace ? selfieFace.value : null; - - // Compare faces using EdgeFunction via FacialVerificationService - final comparisonResult = await _facialVerificationService.compareFaces( - idCardController.idCardImage.value!, - selfieImage.value!, - sourceModel: sourceFace, - targetModel: targetFace, - ); - - _updateComparisonResult(comparisonResult); - } on EdgeFunctionException catch (e) { - // Handle specific errors with user-friendly messages - ErrorHandler.logError('Face comparison', e); - - faceComparisonResult.value = FaceComparisonResult.error( - FaceModel.empty(), - FaceModel.empty(), - e.message, - ); - - isMatchWithIDCard.value = false; - matchConfidence.value = 0.0; - selfieValidationMessage.value = e.message; - } catch (e) { - ErrorHandler.logError('Face comparison', e); - selfieValidationMessage.value = ErrorHandler.getUIErrorMessage(e); - isMatchWithIDCard.value = false; - matchConfidence.value = 0.0; - } finally { - _setLoading(isComparingWithIDCard: false); - } - } - - /// Also clear loading states when closing liveness detection - Future cancelLivenessDetection() async { - _setLoading(isPerformingLivenessCheck: false, isVerifyingFace: false); - selfieValidationMessage.value = 'Liveness check was cancelled'; - } - - // Di SelfieVerificationController - void resetVerificationState() { - isLivenessCheckPassed.value = false; - faceComparisonResult.value = null; - matchConfidence.value = 0.0; - selfieError.value = ''; - hasConfirmedSelfie.value = false; - // Reset other relevant states - } - - /// Handle development mode dummy validation - Future _handleDevelopmentModeValidation() async { - isSelfieValid.value = true; - isLivenessCheckPassed.value = true; - selfieImageFaceId.value = - 'dummy-face-id-${DateTime.now().millisecondsSinceEpoch}'; - selfieValidationMessage.value = - 'Selfie validation successful (development mode)'; - - selfieFace.value = FaceModel( - imagePath: selfieImage.value!.path, - faceId: selfieImageFaceId.value, - confidence: 0.95, - boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, - ).withLiveness( - isLive: true, - confidence: 0.92, - message: 'Liveness check passed (development mode)', - ); - - await compareWithIDCardPhoto(); - } - - /// Handle development mode comparison dummy data - Future _handleDevelopmentModeComparison( - IdCardVerificationController idCardController, - ) async { - final sourceFace = - idCardController.idCardFace.value.hasValidFace - ? idCardController.idCardFace.value - : FaceModel( - imagePath: idCardController.idCardImage.value!.path, - faceId: - 'dummy-id-card-face-${DateTime.now().millisecondsSinceEpoch}', - confidence: 0.95, - boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, - ); - - final comparisonResult = FaceComparisonResult( - sourceFace: sourceFace, - targetFace: selfieFace.value, - isMatch: true, - confidence: 0.91, - message: 'Face matching successful (development mode)', - ); - - _updateComparisonResult(comparisonResult); - } -} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/signin_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin/signin_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/signin_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signin/signin_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/id-card-verification/id_card_verification_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/id-card-verification/id_card_verification_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/id-card-verification/image_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/id-card-verification/image_verification_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/id-card-verification/image_verification_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/id-card-verification/image_verification_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart new file mode 100644 index 0000000..bc8a057 --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart @@ -0,0 +1,516 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/auth/data/models/face_model.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/id-card-verification/id_card_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/facial_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/viewer-information/personal_info_controller.dart'; + +class IdentityVerificationController extends GetxController { + // Singleton instance + static IdentityVerificationController get instance => Get.find(); + + // Directly reference controllers from previous steps + late IdCardVerificationController idCardController; + late SelfieVerificationController selfieController; + late PersonalInfoController personalInfoController; + late FormRegistrationController mainController; + + // Dependencies + final bool isOfficer; + final FacialVerificationService _faceService = + FacialVerificationService.instance; + + // Form controllers + final TextEditingController nikController = TextEditingController(); + final TextEditingController nrpController = TextEditingController(); + final TextEditingController fullNameController = TextEditingController(); + final TextEditingController placeOfBirthController = TextEditingController(); + final TextEditingController birthDateController = TextEditingController(); + final TextEditingController addressController = TextEditingController(); + + // Form validation errors + final RxString nikError = RxString(''); + final RxString nrpError = RxString(''); + final RxString fullNameError = RxString(''); + final RxString placeOfBirthError = RxString(''); + final RxString birthDateError = RxString(''); + final RxString genderError = RxString(''); + final RxString addressError = RxString(''); + + // ID verification states + final RxBool isVerifying = RxBool(false); + final RxBool isVerified = RxBool(false); + final RxString verificationMessage = RxString(''); + + // Face verification states + final RxBool isVerifyingFace = RxBool(false); + final RxBool isFaceVerified = RxBool(false); + final RxString faceVerificationMessage = RxString(''); + final Rx faceComparisonResult = + Rx(null); + + // UI state variables + final Rx selectedGender = Rx('Male'); + final RxBool isNikReadOnly = RxBool(false); + final RxBool isNrpReadOnly = RxBool(false); + final RxBool isFormValid = RxBool(true); + final RxBool isPreFilledNik = false.obs; + + // Data saving states + final RxBool isSavingData = RxBool(false); + final RxBool isDataSaved = RxBool(false); + final RxString dataSaveMessage = RxString(''); + + // Summary data for review page + final RxMap summaryData = RxMap({}); + + // Storage for extracted data + final String? extractedIdCardNumber; + final String? extractedName; + + // Verification status variables (computed from previous steps) + final RxBool isPersonalInfoVerified = RxBool(false); + final RxBool isIdCardVerified = RxBool(false); + final RxBool isSelfieVerified = RxBool(false); + final RxBool isContactInfoVerified = RxBool(false); + final RxBool isLoadingSummary = RxBool(false); + + IdentityVerificationController({ + this.extractedIdCardNumber = '', + this.extractedName = '', + required this.isOfficer, + }); + + @override + void onInit() { + super.onInit(); + + // Get controllers from previous steps + mainController = Get.find(); + personalInfoController = Get.find(); + idCardController = Get.find(); + selfieController = Get.find(); + + // Set default gender value + selectedGender.value = selectedGender.value ?? 'Male'; + + // Initialize form controllers + nikController.text = idCardController.ktpModel.value?.nik ?? ''; + nrpController.text = idCardController.ktaModel.value?.nrp ?? ''; + fullNameController.text = + idCardController.ktpModel.value?.name ?? + idCardController.ktaModel.value?.name ?? + ''; + placeOfBirthController.text = + idCardController.ktpModel.value?.birthPlace ?? ''; + birthDateController.text = idCardController.ktpModel.value?.birthDate ?? ''; + addressController.text = idCardController.ktpModel.value?.address ?? ''; + + isNikReadOnly.value = idCardController.ktpModel.value != null; + isNrpReadOnly.value = idCardController.ktaModel.value != null; + + // Initialize data + _initializeData(); + } + + // Initialize all data + Future _initializeData() async { + try { + // Check verification status from previous steps + _updateVerificationStatus(); + + // Pre-fill form with data from ID card step + _prefillFormFromIdCard(); + + // Build summary data + _buildSummaryData(); + } catch (e) { + print('Error initializing data: $e'); + } + } + + // Update verification status by checking previous steps + void _updateVerificationStatus() { + // Basic info is from the main registration controller + isPersonalInfoVerified.value = personalInfoController.isFormValid.value; + + // ID card verification from id card controller + isIdCardVerified.value = + idCardController.isIdCardValid.value && + idCardController.hasConfirmedIdCard.value; + + // Selfie verification from selfie controller + isSelfieVerified.value = + selfieController.isSelfieValid.value && + selfieController.hasConfirmedSelfie.value; + } + + // Pre-fill form with data from ID card step + void _prefillFormFromIdCard() { + try { + if (!isOfficer && idCardController.ktpModel.value != null) { + // For citizen - use KTP data + final ktp = idCardController.ktpModel.value!; + + // Fill form fields + nikController.text = ktp.nik; + fullNameController.text = ktp.name; + placeOfBirthController.text = ktp.birthPlace; + birthDateController.text = ktp.birthDate; + + // Set gender selection + if (ktp.gender.toLowerCase().contains('laki') || + ktp.gender.toLowerCase() == 'male') { + selectedGender.value = 'Male'; + } else if (ktp.gender.toLowerCase().contains('perempuan') || + ktp.gender.toLowerCase() == 'female') { + selectedGender.value = 'Female'; + } + + // Fill address + addressController.text = ktp.address; + + // Lock NIK field as it's from official ID + isNikReadOnly.value = true; + } else if (isOfficer && idCardController.ktaModel.value != null) { + // For officer - use KTA data + final kta = idCardController.ktaModel.value!; + + // Fill form fields with available KTA data + fullNameController.text = kta.name; + + // KTA often has less data than KTP, check for extra fields + if (kta.extraData != null && + kta.extraData!.containsKey('tanggal_lahir')) { + birthDateController.text = kta.extraData!['tanggal_lahir'] ?? ''; + } + } + } catch (e) { + print('Error pre-filling form: $e'); + } + } + + // Build summary data from all steps + void _buildSummaryData() { + // Clear existing data + summaryData.clear(); + + // Add data from main controller + summaryData['firstName'] = personalInfoController.firstNameController.value; + summaryData['lastName'] = personalInfoController.lastNameController.value; + summaryData['phone'] = personalInfoController.phoneController.value; + summaryData['address'] = personalInfoController.addressController.value; + summaryData['bio'] = personalInfoController.bioController.value; + + // Add data from ID card controller + summaryData['idCardType'] = isOfficer ? 'KTA' : 'KTP'; + summaryData['hasValidIdCard'] = isIdCardVerified.value; + + // Add data from selfie controller + summaryData['hasSelfie'] = isSelfieVerified.value; + summaryData['faceMatchConfidence'] = selfieController.matchConfidence.value; + + // Add current form values + _updateSummaryWithFormData(); + } + + // Validate form inputs + bool validate(GlobalKey? formKey) { + isFormValid.value = true; + clearErrors(); + + // Validate required fields based on officer status + if (!isOfficer) { + // KTP validation + if (nikController.text.isEmpty) { + nikError.value = 'NIK is required'; + isFormValid.value = false; + } else if (nikController.text.length != 16) { + nikError.value = 'NIK must be 16 digits'; + isFormValid.value = false; + } + + if (fullNameController.text.isEmpty) { + fullNameError.value = 'Full name is required'; + isFormValid.value = false; + } + + if (placeOfBirthController.text.isEmpty) { + placeOfBirthError.value = 'Place of birth is required'; + isFormValid.value = false; + } + } else { + // KTA validation + if (fullNameController.text.isEmpty) { + fullNameError.value = 'Full name is required'; + isFormValid.value = false; + } + } + + // Common validations + if (birthDateController.text.isEmpty) { + birthDateError.value = 'Birth date is required'; + isFormValid.value = false; + } + + if (selectedGender.value == null) { + genderError.value = 'Gender is required'; + isFormValid.value = false; + } + + // Verify previous steps completion + bool allPreviousStepsCompleted = + isPersonalInfoVerified.value && + isIdCardVerified.value && + isSelfieVerified.value; + + if (!allPreviousStepsCompleted) { + isFormValid.value = false; + verificationMessage.value = + 'Please complete all previous steps before submitting'; + } + + // Update summary data with latest form values + _updateSummaryWithFormData(); + + return isFormValid.value; + } + + // Update summary with form data + void _updateSummaryWithFormData() { + summaryData['nik'] = nikController.text; + summaryData['fullName'] = fullNameController.text; + summaryData['placeOfBirth'] = placeOfBirthController.text; + summaryData['birthDate'] = birthDateController.text; + summaryData['gender'] = selectedGender.value; + summaryData['address'] = addressController.text; + } + + // Clear all validation errors + void clearErrors() { + nikError.value = ''; + fullNameError.value = ''; + placeOfBirthError.value = ''; + birthDateError.value = ''; + genderError.value = ''; + addressError.value = ''; + isFormValid.value = true; + } + + // Prefill form with extracted data + void prefillExtractedData() { + if (extractedIdCardNumber != null && extractedIdCardNumber!.isNotEmpty) { + nikController.text = extractedIdCardNumber!; + } + + if (extractedName != null && extractedName!.isNotEmpty) { + fullNameController.text = extractedName!; + } + + isPreFilledNik.value = true; + } + + // Verify ID card with OCR data + void verifyIdCardWithOCR() { + try { + isVerifying.value = true; + + if (!isOfficer && idCardController.ktpModel.value != null) { + final ktpModel = idCardController.ktpModel.value!; + + bool nikMatches = nikController.text == ktpModel.nik; + bool nameMatches = _compareNames( + fullNameController.text, + ktpModel.name, + ); + + if (nikMatches && nameMatches) { + isVerified.value = true; + verificationMessage.value = 'KTP information verified successfully!'; + } else { + isVerified.value = false; + verificationMessage.value = + 'Information doesn\'t match with KTP. Please check and try again.'; + } + } else if (isOfficer && idCardController.ktaModel.value != null) { + final ktaModel = idCardController.ktaModel.value!; + + bool nameMatches = _compareNames( + fullNameController.text, + ktaModel.name, + ); + + if (nameMatches) { + isVerified.value = true; + verificationMessage.value = 'KTA information verified successfully!'; + } else { + isVerified.value = false; + verificationMessage.value = + 'Information doesn\'t match with KTA. Please check and try again.'; + } + } else { + isVerified.value = false; + verificationMessage.value = + 'No ID card data available from previous step.'; + } + } catch (e) { + isVerified.value = false; + verificationMessage.value = 'Error during verification: ${e.toString()}'; + print('Error in ID card verification: $e'); + } finally { + isVerifying.value = false; + } + } + + // Compare names accounting for formatting differences + bool _compareNames(String name1, String name2) { + String normalizedName1 = name1.toLowerCase().trim().replaceAll( + RegExp(r'\s+'), + ' ', + ); + String normalizedName2 = name2.toLowerCase().trim().replaceAll( + RegExp(r'\s+'), + ' ', + ); + + if (normalizedName1 == normalizedName2) return true; + + if (normalizedName1.contains(normalizedName2) || + normalizedName2.contains(normalizedName1)) + return true; + + var parts1 = normalizedName1.split(' '); + var parts2 = normalizedName2.split(' '); + + int matches = 0; + for (var part1 in parts1) { + for (var part2 in parts2) { + if (part1.length > 2 && + part2.length > 2 && + (part1.contains(part2) || part2.contains(part1))) { + matches++; + break; + } + } + } + + return matches >= (parts1.length / 2).floor(); + } + + // Verify face match using FacialVerificationService + void verifyFaceMatch() { + if (_faceService.skipFaceVerification) { + // Development mode - use dummy data + isFaceVerified.value = true; + faceVerificationMessage.value = + 'Face verification skipped (development mode)'; + + if (idCardController.idCardImage.value != null && + selfieController.selfieImage.value != null) { + faceComparisonResult.value = FaceComparisonResult( + sourceFace: FaceModel( + imagePath: idCardController.idCardImage.value!.path, + faceId: 'dummy-id-card-id', + confidence: 0.95, + boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, + ), + targetFace: FaceModel( + imagePath: selfieController.selfieImage.value!.path, + faceId: 'dummy-selfie-id', + confidence: 0.95, + boundingBox: {'x': 0.1, 'y': 0.1, 'width': 0.8, 'height': 0.8}, + ), + isMatch: true, + confidence: 0.92, + similarity: 92.0, + similarityThreshold: 70.0, + message: 'Face verification passed (development mode)', + ); + } + return; + } + + isVerifyingFace.value = true; + + if (idCardController.idCardImage.value == null || + selfieController.selfieImage.value == null) { + isFaceVerified.value = false; + faceVerificationMessage.value = + 'Both ID card and selfie are required for face verification.'; + isVerifyingFace.value = false; + return; + } + + _faceService + .compareFaces( + idCardController.idCardImage.value!, + selfieController.selfieImage.value!, + ) + .then((result) { + faceComparisonResult.value = result; + isFaceVerified.value = result.isMatch; + faceVerificationMessage.value = result.message; + }) + .catchError((e) { + isFaceVerified.value = false; + faceVerificationMessage.value = 'Error during face verification: $e'; + print('Face verification error: $e'); + }) + .whenComplete(() { + isVerifyingFace.value = false; + }); + } + + // Save registration data + Future saveRegistrationData() async { + try { + isSavingData.value = true; + dataSaveMessage.value = 'Saving your registration data...'; + + // Final validation + if (!validate(null)) { + dataSaveMessage.value = 'Please fix the errors before submitting'; + return false; + } + + // Update summary with final form data + _updateSummaryWithFormData(); + + // Send the data to the main controller for submission + final result = await mainController.saveRegistrationData( + summaryData: summaryData, + ); + + if (result) { + isDataSaved.value = true; + dataSaveMessage.value = 'Registration data saved successfully!'; + } else { + isDataSaved.value = false; + dataSaveMessage.value = + 'Failed to save registration data. Please try again.'; + } + + return result; + } catch (e) { + isDataSaved.value = false; + dataSaveMessage.value = 'Error saving registration data: $e'; + print('Error saving registration data: $e'); + return false; + } finally { + isSavingData.value = false; + } + } + + @override + void onClose() { + // Dispose form controllers + nikController.dispose(); + fullNameController.dispose(); + placeOfBirthController.dispose(); + birthDateController.dispose(); + addressController.dispose(); + super.onClose(); + } +} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/officer-information/officer_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/officer-information/officer_info_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/officer-information/officer_info_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/officer-information/officer_info_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/officer-information/unit_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/officer-information/unit_info_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/officer-information/unit_info_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/officer-information/unit_info_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/registration_form_controller.dart similarity index 98% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/registration_form_controller.dart index 1800c62..a81a158 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/registration_form_controller.dart @@ -4,13 +4,13 @@ import 'package:get_storage/get_storage.dart'; import 'package:logger/logger.dart'; import 'package:sigap/src/cores/services/supabase_service.dart'; import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/officer-information/officer_info_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/officer-information/unit_info_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/officer-information/officer_info_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/officer-information/unit_info_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/id-card-verification/id_card_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/viewer-information/personal_info_controller.dart'; import 'package:sigap/src/features/daily-ops/data/models/index.dart'; import 'package:sigap/src/features/personalization/data/models/index.dart'; import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart'; @@ -445,7 +445,7 @@ class FormRegistrationController extends GetxController { case 1: return idCardVerificationController.validate(); case 2: - return selfieVerificationController.validate(); + return selfieVerificationController.isMatchWithIDCard.value; case 3: return selectedRole.value?.isOfficer == true ? officerInfoController!.validate(formKey) diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart similarity index 88% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart index b445495..b7fe1b4 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart @@ -20,6 +20,7 @@ enum LivenessStatus { checkSmile, checkEyesOpen, readyForPhoto, + countdown, photoTaken, completed, failed, @@ -52,6 +53,10 @@ class FaceLivenessController extends GetxController { final isCaptured = false.obs; final successfulSteps = [].obs; + // Countdown timer state + final countdownSeconds = 5.obs; + Timer? _countdownTimer; + // Image processing XFile? capturedImage; // Removed imageStreamSubscription as startImageStream does not return a StreamSubscription @@ -92,6 +97,7 @@ class FaceLivenessController extends GetxController { @override void onClose() { dev.log('FaceLivenessController closing...', name: 'LIVENESS_CONTROLLER'); + _countdownTimer?.cancel(); _cleanup(); super.onClose(); } @@ -149,7 +155,7 @@ class FaceLivenessController extends GetxController { cameraController = CameraController( frontCamera, ResolutionPreset - .high, // Changed from medium to high for better detection + .low, // Changed from medium to high for better detection enableAudio: false, imageFormatGroup: Platform.isIOS @@ -158,7 +164,7 @@ class FaceLivenessController extends GetxController { ); await cameraController!.initialize(); - + // Set flash off to improve face detection try { await cameraController!.setFlashMode(FlashMode.off); @@ -221,7 +227,7 @@ class FaceLivenessController extends GetxController { // Detect faces final faces = await faceDetector.processImage(inputImage); - + // Log the face detection attempt if (faces.isEmpty) { dev.log( @@ -234,10 +240,9 @@ class FaceLivenessController extends GetxController { name: 'LIVENESS_CONTROLLER', ); } - + // Process face detection results await _processFaceDetection(faces); - } catch (e) { dev.log('Error processing image: $e', name: 'LIVENESS_CONTROLLER'); } finally { @@ -276,7 +281,7 @@ class FaceLivenessController extends GetxController { var rotationCompensation = orientations[cameraController!.value.deviceOrientation]; if (rotationCompensation == null) return null; - + if (camera.lensDirection == CameraLensDirection.front) { rotationCompensation = (sensorOrientation + rotationCompensation) % 360; @@ -284,7 +289,7 @@ class FaceLivenessController extends GetxController { rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; } - + rotation = InputImageRotationValue.fromRawValue(rotationCompensation); dev.log( 'Android camera rotation set to: $rotationCompensation', @@ -406,25 +411,25 @@ class FaceLivenessController extends GetxController { case LivenessStatus.checkLeftRotation: if (isFaceLeft.value) { - _completeCurrentStep('✓ Looked left'); + _completeCurrentStep('Looked left'); } break; case LivenessStatus.checkRightRotation: if (isFaceRight.value) { - _completeCurrentStep('✓ Looked right'); + _completeCurrentStep('Looked right'); } break; case LivenessStatus.checkSmile: if (isSmiled.value) { - _completeCurrentStep('✓ Smiled detected'); + _completeCurrentStep('Smiled detected'); } break; case LivenessStatus.checkEyesOpen: if (isEyeOpen.value) { - _completeCurrentStep('✓ Eyes open confirmed'); + _completeCurrentStep('Eyes open confirmed'); } break; @@ -511,16 +516,42 @@ class FaceLivenessController extends GetxController { status.value = LivenessStatus.readyForPhoto; isFaceReadyForPhoto.value = true; - // Auto-capture after a short delay - Timer(Duration(seconds: 1), () { - if (!isCaptured.value) { + // Start countdown instead of immediately taking picture + _startCountdown(); + } + + // Start countdown timer before taking the photo + void _startCountdown() { + dev.log('Starting countdown before capture', name: 'LIVENESS_CONTROLLER'); + status.value = LivenessStatus.countdown; + countdownSeconds.value = 5; // Reset to 5 seconds + + // Cancel any existing timer + _countdownTimer?.cancel(); + + // Create a periodic timer that fires every second + _countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) { + countdownSeconds.value--; + + // When countdown reaches zero, take the picture + if (countdownSeconds.value <= 0) { + timer.cancel(); captureImage(); } }); } + // Cancel countdown if needed + void cancelCountdown() { + _countdownTimer?.cancel(); + countdownSeconds.value = 5; + status.value = LivenessStatus.readyForPhoto; + } + // Capture image with improved processing Future captureImage() async { + _countdownTimer?.cancel(); // Ensure timer is cancelled + try { if (cameraController == null || !cameraController!.value.isInitialized) { dev.log('Camera not ready for capture', name: 'LIVENESS_CONTROLLER'); @@ -571,7 +602,7 @@ class FaceLivenessController extends GetxController { await Future.delayed(Duration(milliseconds: 500)); continue; } - + break; } catch (e) { retryCount++; @@ -601,7 +632,7 @@ class FaceLivenessController extends GetxController { status.value = LivenessStatus.failed; } } - + // Verify that the captured image contains a face Future _verifyFaceInImage(XFile image) async { try { @@ -703,6 +734,8 @@ class FaceLivenessController extends GetxController { return 'Please smile for the camera'; case LivenessStatus.checkEyesOpen: return 'Keep your eyes wide open'; + case LivenessStatus.countdown: + return 'Get ready! Taking photo in ${countdownSeconds.value}...'; case LivenessStatus.readyForPhoto: return 'Perfect! Hold still for photo capture'; case LivenessStatus.photoTaken: @@ -719,6 +752,7 @@ class FaceLivenessController extends GetxController { // Handle cancellation (called when user goes back) void handleCancellation() { dev.log('Handling cancellation...', name: 'LIVENESS_CONTROLLER'); + _countdownTimer?.cancel(); // Cancel countdown timer _cleanup(); } @@ -747,6 +781,7 @@ class FaceLivenessController extends GetxController { // Cancel timers stepTimer?.cancel(); stabilityTimer?.cancel(); + _countdownTimer?.cancel(); // Restart the process status.value = LivenessStatus.detectingFace; @@ -768,10 +803,10 @@ class FaceLivenessController extends GetxController { // Add all steps as completed successfulSteps.clear(); successfulSteps.addAll([ - '✓ Looked left (debug skip)', - '✓ Looked right (debug skip)', - '✓ Smiled detected (debug skip)', - '✓ Eyes open confirmed (debug skip)', + 'Looked left (debug skip)', + 'Looked right (debug skip)', + 'Smiled detected (debug skip)', + 'Eyes open confirmed (debug skip)', ]); currentStepIndex = verificationSteps.length; @@ -819,10 +854,14 @@ class FaceLivenessController extends GetxController { // Cancel timers stepTimer?.cancel(); stabilityTimer?.cancel(); + _countdownTimer?.cancel(); // Stop image stream with error handling try { - cameraController?.stopImageStream(); + if (cameraController?.value.isInitialized == true && + cameraController?.value.isStreamingImages == true) { + cameraController?.stopImageStream(); + } } catch (e) { dev.log( 'Error stopping image stream during cleanup: $e', @@ -832,11 +871,15 @@ class FaceLivenessController extends GetxController { // Dispose camera with error handling try { - cameraController?.dispose(); + if (cameraController?.value.isInitialized == true) { + cameraController?.dispose(); + } } catch (e) { dev.log('Error disposing camera: $e', name: 'LIVENESS_CONTROLLER'); } + cameraController = null; + // Close ML Kit detectors try { faceDetector.close(); @@ -849,6 +892,46 @@ class FaceLivenessController extends GetxController { } } + // Pause the liveness detection process + void pauseDetection() { + dev.log('Pausing liveness detection process', name: 'LIVENESS_CONTROLLER'); + + _countdownTimer?.cancel(); + stepTimer?.cancel(); + stabilityTimer?.cancel(); + + // Stop image stream + try { + if (cameraController?.value.isInitialized == true && + cameraController?.value.isStreamingImages == true) { + cameraController?.stopImageStream(); + } + } catch (e) { + dev.log('Error stopping image stream: $e', name: 'LIVENESS_CONTROLLER'); + } + + status.value = LivenessStatus.preparing; + } + + // Dispose camera + void disposeCamera() { + dev.log('Disposing camera resources', name: 'LIVENESS_CONTROLLER'); + _cleanup(); + } + + // cancel the liveness detection process + void cancelLivenessDetection() { + dev.log( + 'Cancelling liveness detection process', + name: 'LIVENESS_CONTROLLER', + ); + _cleanup(); + status.value = LivenessStatus.preparing; + isFaceInFrame.value = false; + isCaptured.value = false; + capturedImage = null; + } + // Generate face model FaceModel generateFaceModel() { if (capturedImage == null) { diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/facial_verification_controller.dart similarity index 98% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/facial_verification_controller.dart index 239dee3..e63037e 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/facial_verification_controller.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:sigap/src/cores/services/edge_function_service.dart'; import 'package:sigap/src/features/auth/data/models/face_model.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; /// Service for handling facial verification /// This class serves as a bridge between UI controllers and face detection functionality diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart new file mode 100644 index 0000000..06174a1 --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart @@ -0,0 +1,257 @@ +import 'dart:developer' as dev; + +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:sigap/src/cores/services/edge_function_service.dart'; +import 'package:sigap/src/features/auth/data/models/face_model.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/id-card-verification/id_card_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/utils/constants/app_routes.dart'; + +class SelfieVerificationController extends GetxController { + // Image picker instance + final _imagePicker = ImagePicker(); + + // Edge function service for face comparison + final _edgeFunctionService = EdgeFunctionService.instance; + + // Main controller reference + IdCardVerificationController? idCardController; + + // States + final selfieImage = Rx(null); + final isVerifyingFace = false.obs; + final isSelfieValid = false.obs; + final selfieError = RxString(''); + final isUploadingSelfie = false.obs; + final hasConfirmedSelfie = false.obs; + + // Liveness detection states + final isPerformingLivenessCheck = false.obs; + final isLivenessCheckPassed = false.obs; + + // Face comparison results + final isComparingWithIDCard = false.obs; + final isMatchWithIDCard = false.obs; + final matchConfidence = 0.0.obs; + final Rx faceComparisonResult = + Rx(null); + + // Constructor + SelfieVerificationController({this.idCardController}); + + @override + void onInit() { + super.onInit(); + + // Try to find the ID card verification controller + try { + idCardController = Get.find(); + dev.log( + 'Found IdCardVerificationController', + name: 'SELFIE_VERIFICATION', + ); + } catch (e) { + dev.log( + 'IdCardVerificationController not found, will use idCardController fallback', + name: 'SELFIE_VERIFICATION', + ); + } + + // Listen for changes to selfieImage + ever(selfieImage, (XFile? image) { + if (image != null) { + // When a selfie is set (after liveness check), + // automatically verify it against ID card + _processCapturedLivenessImage(); + } + }); + } + + // Method to perform liveness detection + void performLivenessDetection() async { + try { + isPerformingLivenessCheck.value = true; + + // Check if FaceLivenessController is already registered + final bool hasExistingController = + Get.isRegistered(); + + // Clear existing controller if it exists to ensure fresh state + if (hasExistingController) { + final existingController = Get.find(); + existingController.handleCancellation(); + await Get.delete(); + } + + // Register a new controller (will be done automatically by the widget) + final result = await Get.toNamed(AppRoutes.livenessDetection); + + if (result is XFile) { + // Liveness check passed and returned an image + selfieImage.value = result; + isLivenessCheckPassed.value = true; + + // The _processCapturedLivenessImage will be called automatically + // due to the ever() listener we set up in onInit + } else { + // User cancelled or something went wrong + isPerformingLivenessCheck.value = false; + isLivenessCheckPassed.value = false; + } + } catch (e) { + dev.log('Error in liveness detection: $e', name: 'SELFIE_VERIFICATION'); + isPerformingLivenessCheck.value = false; + isLivenessCheckPassed.value = false; + selfieError.value = 'Liveness check failed: $e'; + } + } + + // Process the captured image after liveness check + Future _processCapturedLivenessImage() async { + if (selfieImage.value == null) return; + + try { + isVerifyingFace.value = true; + selfieError.value = ''; + + // Now verify that the selfie contains a valid face + final faces = await _edgeFunctionService.detectFaces(selfieImage.value!); + + if (faces.isEmpty) { + selfieError.value = 'No face detected in your selfie'; + isVerifyingFace.value = false; + isSelfieValid.value = false; + return; + } + + // Face detected successfully + isSelfieValid.value = true; + isVerifyingFace.value = false; + + // Now compare with ID card if available + await _compareWithIdCard(); + } catch (e) { + dev.log('Error processing selfie: $e', name: 'SELFIE_VERIFICATION'); + selfieError.value = 'Error verifying face: $e'; + isVerifyingFace.value = false; + isSelfieValid.value = false; + } finally { + isPerformingLivenessCheck.value = false; + } + } + + // Compare selfie with ID card + Future _compareWithIdCard() async { + if (selfieImage.value == null) { + dev.log( + 'No selfie image available for comparison', + name: 'SELFIE_VERIFICATION', + ); + return; + } + + // Check for ID card image from either controller + // Check for ID card image from IdCardVerificationController + XFile? idCardImage; + + if (idCardController != null && + idCardController!.idCardImage.value != null) { + idCardImage = idCardController!.idCardImage.value; + dev.log( + 'Using ID card image from IdCardVerificationController', + name: 'SELFIE_VERIFICATION', + ); + } + + if (idCardImage == null) { + dev.log( + 'No ID card image available for comparison', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = + 'Cannot compare with ID card - no ID card image found'; + return; + } + try { + isComparingWithIDCard.value = true; + + dev.log( + 'Starting face comparison between ID card and selfie', + name: 'SELFIE_VERIFICATION', + ); + dev.log('ID card path: ${idCardImage.path}', name: 'SELFIE_VERIFICATION'); + dev.log( + 'Selfie path: ${selfieImage.value!.path}', + name: 'SELFIE_VERIFICATION', + ); + + // Compare faces using edge function + final result = await _edgeFunctionService.compareFaces( + idCardImage, + selfieImage.value!, + similarityThreshold: 70.0, // Use 70% as default threshold + ); + + // Update comparison results + faceComparisonResult.value = result; + isMatchWithIDCard.value = result.isMatch; + matchConfidence.value = result.confidence; + + dev.log( + 'Face comparison complete: Match=${result.isMatch}, ' + 'Confidence=${(result.confidence * 100).toStringAsFixed(1)}%, ' + 'Message=${result.message}', + name: 'SELFIE_VERIFICATION', + ); + + if (!result.isMatch) { + selfieError.value = result.message; + } + } catch (e) { + dev.log('Error comparing faces: $e', name: 'SELFIE_VERIFICATION'); + selfieError.value = 'Error comparing with ID card: $e'; + isMatchWithIDCard.value = false; + matchConfidence.value = 0.0; + } finally { + isComparingWithIDCard.value = false; + } + } + + // Clear selfie image + void clearSelfieImage() { + selfieImage.value = null; + isSelfieValid.value = false; + hasConfirmedSelfie.value = false; + selfieError.value = ''; + } + + // Confirm the selfie + void confirmSelfieImage() { + if (selfieImage.value != null && isSelfieValid.value) { + hasConfirmedSelfie.value = true; + + // If we have a main controller, update its state + if (idCardController != null) { + idCardController!.hasConfirmedIdCard.value = true; + } + } + } + + void clearErrors() { + selfieError.value = ''; + } + + // Reset verification state + void resetVerificationState() { + isVerifyingFace.value = false; + isComparingWithIDCard.value = false; + isMatchWithIDCard.value = false; + matchConfidence.value = 0.0; + faceComparisonResult.value = null; + isLivenessCheckPassed.value = false; + isPerformingLivenessCheck.value = false; + hasConfirmedSelfie.value = false; + selfieError.value = ''; + } +} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/signup_with_role_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/signup_with_role_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/viewer-information/personal_info_controller.dart similarity index 100% rename from sigap-mobile/lib/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart rename to sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/viewer-information/personal_info_controller.dart diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/email-verification/email_verification_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/email-verification/email_verification_screen.dart index d163682..68a5688 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/email-verification/email_verification_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/email-verification/email_verification_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/email_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/others/email_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/otp_input_field.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart index ffb8162..46b67ea 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/forgot_password_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/others/forgot_password_controller.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart'; import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/id_card_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/id_card_verification_step.dart index 0121577..5ca8ada 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/id_card_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/id_card_verification_step.dart @@ -3,7 +3,8 @@ import 'package:get/get.dart'; import 'package:sigap/src/features/auth/data/models/kta_model.dart'; import 'package:sigap/src/features/auth/data/models/ktp_model.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart'; + +import 'package:sigap/src/features/auth/presentasion/controllers/signup/id-card-verification/id_card_verification_controller.dart'; import 'package:sigap/src/shared/widgets/image_upload/image_source_dialog.dart'; import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart'; import 'package:sigap/src/shared/widgets/info/tips_container.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/identity_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/identity_verification_step.dart index 19eb252..ca58022 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/identity_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/identity_verification_step.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:logger/logger.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; + +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart'; -import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/verification_summary.dart'; import 'package:sigap/src/shared/widgets/form/form_section_header.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; @@ -14,10 +16,14 @@ class IdentityVerificationStep extends StatelessWidget { @override Widget build(BuildContext context) { final formKey = GlobalKey(); - final controller = Get.find(); final mainController = Get.find(); + final controller = Get.find(); + + Get.find(); + mainController.formKey = formKey; + // Extract isOfficer to avoid the error when used in Obx final isOfficer = mainController.selectedRole.value?.isOfficer ?? false; return Form( @@ -35,16 +41,8 @@ class IdentityVerificationStep extends StatelessWidget { const SizedBox(height: TSizes.spaceBtwItems), // Verification Progress Card - Obx(() => _buildVerificationProgressCard(controller)), - - const SizedBox(height: TSizes.spaceBtwItems), - - // Registration Summary - Obx( - () => VerificationSummary( - summaryData: controller.summaryData, - isOfficer: isOfficer, - ), + GetBuilder( + builder: (ctrl) => _buildVerificationProgressCard(ctrl), ), const SizedBox(height: TSizes.spaceBtwItems), @@ -77,61 +75,65 @@ class IdentityVerificationStep extends StatelessWidget { const SizedBox(height: TSizes.spaceBtwSections), // Save & Submit Button - Obx( - () => ElevatedButton( - onPressed: - controller.isSavingData.value - ? null - : () => _submitRegistrationData(controller, context), - style: ElevatedButton.styleFrom( - backgroundColor: TColors.primary, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(TSizes.buttonRadius), + GetBuilder( + id: 'saveButton', + builder: + (ctrl) => ElevatedButton( + onPressed: + ctrl.isSavingData.value + ? null + : () => _submitRegistrationData(ctrl, context), + style: ElevatedButton.styleFrom( + backgroundColor: TColors.primary, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(TSizes.buttonRadius), + ), + disabledBackgroundColor: TColors.primary.withOpacity(0.3), + ), + child: + ctrl.isSavingData.value + ? const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + SizedBox(width: TSizes.sm), + Text('Submitting...'), + ], + ) + : const Text('Submit Registration'), ), - disabledBackgroundColor: TColors.primary.withOpacity(0.3), - ), - child: - controller.isSavingData.value - ? const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ), - SizedBox(width: TSizes.sm), - Text('Submitting...'), - ], - ) - : const Text('Submit Registration'), - ), ), // Save Result Message - Obx( - () => - controller.dataSaveMessage.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: TSizes.sm), - child: Text( - controller.dataSaveMessage.value, - style: TextStyle( - color: - controller.isDataSaved.value - ? Colors.green - : TColors.error, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ) - : const SizedBox.shrink(), + GetBuilder( + id: 'saveMessage', + builder: + (ctrl) => + ctrl.dataSaveMessage.value.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(top: TSizes.sm), + child: Text( + ctrl.dataSaveMessage.value, + style: TextStyle( + color: + ctrl.isDataSaved.value + ? Colors.green + : TColors.error, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ) + : const SizedBox.shrink(), ), ], ), @@ -142,8 +144,9 @@ class IdentityVerificationStep extends StatelessWidget { Widget _buildVerificationProgressCard( IdentityVerificationController controller, ) { + // Instead of using Obx, we directly use the current values final bool allVerified = - controller.isBasicInfoVerified.value && + controller.isPersonalInfoVerified.value && controller.isIdCardVerified.value && controller.isSelfieVerified.value; @@ -189,7 +192,7 @@ class IdentityVerificationStep extends StatelessWidget { // Basic Info status _buildVerificationItem( 'Basic Information', - controller.isBasicInfoVerified.value, + controller.isPersonalInfoVerified.value, ), // ID Card status @@ -292,38 +295,45 @@ class IdentityVerificationStep extends StatelessWidget { return; } + Logger().i('Submitting registration data...'); + Logger().i('Nik: ${controller.idCardController.ktpModel.value}'); + Logger().i('Selfie: ${controller.selfieController.selfieImage.value}'); + Logger().i('ID Card: ${controller.idCardController.idCardImage.value}'); + Logger().i( + 'Personal Info: ${controller.personalInfoController.phoneController.value}', + ); // Save registration data - final result = await controller.saveRegistrationData(); + // final result = await controller.saveRegistrationData(); - if (result) { - // Navigate to success page or show success dialog - showDialog( - context: context, - barrierDismissible: false, - builder: - (context) => AlertDialog( - title: Row( - children: [ - Icon(Icons.check_circle, color: Colors.green), - SizedBox(width: TSizes.sm), - Text('Registration Successful'), - ], - ), - content: Text( - 'Your registration has been submitted successfully. You will be notified once your account is verified.', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - // Navigate to login or home page - Get.offAllNamed('/login'); - }, - child: Text('Go to Login'), - ), - ], - ), - ); - } + // if (result) { + // // Navigate to success page or show success dialog + // showDialog( + // context: context, + // barrierDismissible: false, + // builder: + // (context) => AlertDialog( + // title: Row( + // children: [ + // Icon(Icons.check_circle, color: Colors.green), + // SizedBox(width: TSizes.sm), + // Text('Registration Successful'), + // ], + // ), + // content: Text( + // 'Your registration has been submitted successfully. You will be notified once your account is verified.', + // ), + // actions: [ + // TextButton( + // onPressed: () { + // Navigator.of(context).pop(); + // // Navigate to login or home page + // Get.offAllNamed('/login'); + // }, + // child: Text('Go to Login'), + // ), + // ], + // ), + // ); + // } } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/liveness_detection_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/liveness_detection_screen.dart index ea69e9e..44b9a36 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/liveness_detection_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/liveness_detection_screen.dart @@ -2,10 +2,11 @@ import 'dart:developer' as dev; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/basic/widgets/camera_preview_widget.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/basic/widgets/captured_selfie_view.dart'; +import 'package:sigap/src/features/auth/presentasion/pages/registration-form/basic/widgets/countdown_overlay_widget.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/basic/widgets/debug_panel.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/basic/widgets/error_state_widget.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/basic/widgets/instruction_banner.dart'; @@ -73,7 +74,8 @@ class LivenessDetectionPage extends StatelessWidget { name: 'LIVENESS_DEBUG', ); // Handle cleanup - if (selfieController != null) { + if (Get.isRegistered()) { + final controller = Get.find(); dev.log( 'Cancelling liveness detection and resetting loading state', name: 'LIVENESS_DEBUG', @@ -85,36 +87,65 @@ class LivenessDetectionPage extends StatelessWidget { backgroundColor: Colors.white, appBar: _buildAppBar(context, controller, selfieController), body: Obx(() { - dev.log( - 'Rebuilding body: ' - 'Camera state: ${controller.cameraController?.value.isInitialized}, ' - 'Status: ${controller.status.value}, ' - 'Steps: ${controller.successfulSteps.length}', - name: 'LIVENESS_DEBUG', - ); + try { + dev.log( + 'Rebuilding body: ' + 'Camera state: ${controller.cameraController?.value.isInitialized}, ' + 'Status: ${controller.status.value}, ' + 'Steps: ${controller.successfulSteps.length}', + name: 'LIVENESS_DEBUG', + ); - // Show loading indicator while camera initializes - if (controller.cameraController == null) { - dev.log('Camera controller is null', name: 'LIVENESS_DEBUG'); - return ErrorStateWidget(message: 'Camera initialization failed'); - } + // Show loading indicator while camera initializes + if (controller.cameraController == null) { + dev.log('Camera controller is null', name: 'LIVENESS_DEBUG'); + return ErrorStateWidget(message: 'Camera initialization failed'); + } - if (!controller.cameraController!.value.isInitialized) { - dev.log('Camera not initialized yet', name: 'LIVENESS_DEBUG'); - return _buildCameraInitializingState(); - } + if (!controller.cameraController!.value.isInitialized) { + dev.log('Camera not initialized yet', name: 'LIVENESS_DEBUG'); + return _buildCameraInitializingState(); + } - // Show captured image when complete - if (controller.isCaptured.value) { - dev.log('Showing captured view', name: 'LIVENESS_DEBUG'); - return CapturedSelfieView( - controller: controller, - selfieController: selfieController, + // Show captured image when complete + if (controller.isCaptured.value) { + dev.log('Showing captured view', name: 'LIVENESS_DEBUG'); + return CapturedSelfieView( + controller: controller, + selfieController: selfieController, + ); + } + + // Main liveness detection UI with improved layout + return _buildMainDetectionView(context, controller); + } catch (e) { + dev.log( + 'Error in LivenessDetectionPage build: $e', + name: 'LIVENESS_DEBUG', + ); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Colors.red.shade300, + ), + SizedBox(height: 16), + Text( + 'An error occurred with the camera', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 24), + ElevatedButton( + onPressed: () => Get.back(), + child: Text('Go Back'), + ), + ], + ), ); } - - // Main liveness detection UI - return _buildMainDetectionView(context, controller); }), ), ); @@ -184,7 +215,7 @@ class LivenessDetectionPage extends StatelessWidget { ); } - // Main detection view UI + // Main detection view UI with the new layout structure Widget _buildMainDetectionView( BuildContext context, FaceLivenessController controller, @@ -193,25 +224,34 @@ class LivenessDetectionPage extends StatelessWidget { return Stack( children: [ + // Main content area with specified layout structure Column( children: [ - // Instruction banner - InstructionBanner(controller: controller), + // 1. Header with instructions (smaller to give more space to camera) + Container( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: InstructionBanner(controller: controller), + ), - const SizedBox(height: 24), - - // Camera preview with face overlay + // 2. Content area with camera preview (expanded to fill available space) Expanded( + flex: 8, // Give most of the space to camera child: CameraPreviewWidget( controller: controller, screenWidth: screenSize.width, ), ), - // Completed steps progress - VerificationProgressWidget(controller: controller), + // 3. Bottom verification progress list (small fixed height) + Container( + padding: const EdgeInsets.only(bottom: 16), + child: VerificationProgressWidget(controller: controller), + ), ], ), + + // Overlay components + CountdownOverlayWidget(controller: controller), ], ); } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/personal_info_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/personal_info_step.dart index 4c14bb7..df64159 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/personal_info_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/personal_info_step.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart'; + +import 'package:sigap/src/features/auth/presentasion/controllers/signup/viewer-information/personal_info_controller.dart'; import 'package:sigap/src/shared/widgets/form/form_section_header.dart'; import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/selfie_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/selfie_verification_step.dart index 9fb43aa..8972f57 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/selfie_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/selfie_verification_step.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/facial_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart'; import 'package:sigap/src/shared/widgets/info/tips_container.dart'; import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart'; @@ -32,8 +31,7 @@ class SelfieVerificationStep extends StatelessWidget { final controller = Get.find(); final mainController = Get.find(); final facialVerificationService = FacialVerificationService.instance; - final FaceLivenessController faceLivenessController = - Get.find(); + mainController.formKey = formKey; return Form( diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/camera_preview_widget.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/camera_preview_widget.dart index 8f3468f..7cac4c1 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/camera_preview_widget.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/camera_preview_widget.dart @@ -1,5 +1,7 @@ +import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; class CameraPreviewWidget extends StatelessWidget { @@ -14,47 +16,237 @@ class CameraPreviewWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - alignment: Alignment.center, - children: [ - // Camera background - Container( - width: screenWidth * 0.85, - height: screenWidth * 0.85, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(24), - ), - ), + final double screenHeight = MediaQuery.of(context).size.height; + // Calculate available height for camera preview + final double availableHeight = + screenHeight * 0.6; // Use 60% of screen height - // Camera preview - ClipRRect( - borderRadius: BorderRadius.circular(24), - child: SizedBox( - width: screenWidth * 0.85, - height: screenWidth * 0.85, - child: controller.cameraController!.buildPreview(), - ), - ), + // Use the smallest dimension to ensure a square preview + final double previewSize = + availableHeight < screenWidth + ? availableHeight + : screenWidth * 0.92; // Use 92% of screen width if height is large - // Scanning animation - Positioned( - top: 0, - child: Container( - width: screenWidth * 0.65, - height: 2, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.transparent, - TColors.primary.withOpacity(0.8), - Colors.transparent, + return Obx(() { + final bool isInitialized = + controller.cameraController?.value.isInitialized ?? false; + final bool isActive = + true; // Always show camera when controller is initialized + final bool isCountdown = + controller.status.value == LivenessStatus.countdown; + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + alignment: Alignment.center, + children: [ + // Camera frame/background + Container( + width: previewSize, + height: previewSize, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.grey.shade300, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + spreadRadius: 0, + ), ], ), ), - ), + + // Full camera feed + if (isInitialized && isActive) + SizedBox( + width: previewSize, + height: previewSize, + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: SizedBox( + width: previewSize, + height: previewSize, + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: + previewSize / + controller + .cameraController! + .value + .aspectRatio, + height: previewSize, + child: CameraPreview( + controller.cameraController!, + ), + ), + ), + ), + ), + // Overlay for better face visibility + Container( + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.2), + ], + stops: const [0.7, 1.0], + center: Alignment.center, + radius: 0.9, + ), + ), + ), + ], + ), + ), + ) + else + // Show placeholder when camera is not active + Container( + width: previewSize, + height: previewSize, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(24), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.camera_alt_outlined, + size: 48, + color: Colors.grey.shade600, + ), + const SizedBox(height: 16), + Text( + 'Camera is initializing...', + style: TextStyle(color: Colors.grey.shade700), + ), + ], + ), + ), + ), + + // Scanning animation when not in countdown + if (isInitialized && isActive && !isCountdown) + Positioned( + top: previewSize * 0.2, // Position at 20% from the top + child: _buildScanningAnimation(previewSize), + ), + + // Face guide overlay + if (isInitialized && isActive) + Center( + child: Container( + width: + previewSize * + 0.7, // Make face guide 70% of the camera preview + height: previewSize * 0.7, + decoration: BoxDecoration( + border: Border.all( + color: _getFaceGuideColor(), + width: 2.5, + strokeAlign: BorderSide.strokeAlignOutside, + ), + shape: BoxShape.circle, + ), + child: Obx( + () => + controller.isFaceInFrame.value + ? Center() + : Center( + child: Icon( + Icons.face, + color: Colors.white.withOpacity(0.7), + size: 48, + ), + ), + ), + ), + ), + + // Instructions overlay + if (isInitialized && isActive && !isCountdown) + Positioned( + bottom: 20, + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + _getActionText(), + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], ), - ], + ); + }); + } + + // Build scanning animation widget + Widget _buildScanningAnimation(double previewSize) { + return Container( + width: previewSize * 0.8, + height: 3, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + TColors.primary.withOpacity(0.8), + Colors.transparent, + ], + ), + ), ); } + + // Get action text based on current status + String _getActionText() { + switch (controller.status.value) { + case LivenessStatus.detectingFace: + return 'Position your face within the circle'; + case LivenessStatus.checkLeftRotation: + return 'Turn your head to the left'; + case LivenessStatus.checkRightRotation: + return 'Turn your head to the right'; + case LivenessStatus.checkSmile: + return 'Please smile'; + case LivenessStatus.checkEyesOpen: + return 'Keep your eyes open'; + case LivenessStatus.readyForPhoto: + return 'Perfect! Hold still'; + default: + return 'Follow instructions'; + } + } + + // Function to color the face guide based on detection state + Color _getFaceGuideColor() { + if (controller.status.value == LivenessStatus.countdown) { + return Colors.green; // Green during countdown + } else if (controller.isFaceInFrame.value) { + return controller.isFaceReadyForPhoto.value + ? Colors.green + : TColors.primary; + } else { + return Colors.white.withOpacity(0.7); + } + } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/captured_selfie_view.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/captured_selfie_view.dart index 512eaa9..d5ed1f0 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/captured_selfie_view.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/captured_selfie_view.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; -class CapturedSelfieView extends StatelessWidget { +class CapturedSelfieView extends StatefulWidget { final FaceLivenessController controller; final SelfieVerificationController? selfieController; @@ -17,105 +17,195 @@ class CapturedSelfieView extends StatelessWidget { this.selfieController, }); + @override + State createState() => _CapturedSelfieViewState(); +} + +class _CapturedSelfieViewState extends State { + // Add a flag for loading state during edge function comparison + bool isComparingWithID = false; + String? errorMessage; + bool isDisposed = false; + + @override + void initState() { + super.initState(); + // Ensure camera is paused when showing the captured image + widget.controller.pauseDetection(); + } + + @override + void dispose() { + isDisposed = true; + super.dispose(); + } + @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.white, Colors.green.shade50], + return SingleChildScrollView( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.green.shade50], + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Success icon - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.green.shade50, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check_circle_outline, - color: Colors.green.shade600, - size: 48, - ), - ), - - const SizedBox(height: 20), - - Text( - 'Verification Successful!', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.green.shade700, - ), - ), - - const SizedBox(height: 8), - - Text( - 'Your identity has been verified', - style: TextStyle(fontSize: 16, color: Colors.black54), - ), - - const SizedBox(height: 32), - - // Display captured image - if (controller.capturedImage != null) - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(150), - border: Border.all(color: Colors.white, width: 4), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 20, - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(150), - child: Image.file( - File(controller.capturedImage!.path), - width: 200, - height: 200, - fit: BoxFit.cover, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Success icon + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.green.shade50, + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_circle_outline, + color: Colors.green.shade600, + size: 48, ), ), - ), - const SizedBox(height: 32), + const SizedBox(height: 20), - // Completed steps list - _buildCompletedStepsList(), - - const SizedBox(height: 32), - - // Continue button - clear loading state properly - ElevatedButton( - onPressed: () => _handleContinueButton(), - style: ElevatedButton.styleFrom( - backgroundColor: TColors.primary, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 56), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + Text( + 'Verification Successful!', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), ), - elevation: 0, - ), - child: const Text( - 'Continue', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), + + const SizedBox(height: 8), + + Text( + 'Your selfie has been captured successfully', + style: TextStyle(fontSize: 16, color: Colors.black54), + ), + + const SizedBox(height: 32), + + // Display captured image + if (widget.controller.capturedImage != null) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(150), + border: Border.all(color: Colors.white, width: 4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(150), + child: Image.file( + File(widget.controller.capturedImage!.path), + width: 200, + height: 200, + fit: BoxFit.cover, + ), + ), + ), + + const SizedBox(height: 24), + + // Completed steps list + _buildCompletedStepsList(), + + const SizedBox(height: 24), + + // Show error message if there's any + if (errorMessage != null) + Container( + padding: EdgeInsets.all(16), + margin: EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.shade200), + ), + child: Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.red), + SizedBox(width: 12), + Expanded( + child: Text( + errorMessage!, + style: TextStyle(color: Colors.red.shade700), + ), + ), + ], + ), + ), + + // Continue button - clear loading state properly + ElevatedButton( + onPressed: isComparingWithID ? null : _handleContinueButton, + style: ElevatedButton.styleFrom( + backgroundColor: TColors.primary, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: + isComparingWithID + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + SizedBox(width: 12), + Text('Processing...'), + ], + ) + : const Text( + 'Continue', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Try again button when there's an error + if (errorMessage != null) + TextButton( + onPressed: () { + // Reset errors and go back to try again + if (!isDisposed) { + setState(() { + errorMessage = null; + }); + } + widget.controller.disposeCamera(); + Get.back(); + }, + style: TextButton.styleFrom( + foregroundColor: TColors.primary, + ), + child: Text('Try Again'), + ), + ], ), - ], + ), ), ), ); @@ -124,7 +214,7 @@ class CapturedSelfieView extends StatelessWidget { // Build the completed steps list Widget _buildCompletedStepsList() { return Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), @@ -143,22 +233,24 @@ class CapturedSelfieView extends StatelessWidget { children: [ Icon(Icons.verified, color: Colors.green.shade600, size: 20), const SizedBox(width: 8), - Text( - 'All verification steps completed', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Colors.black87, + Flexible( + child: Text( + 'All verification steps completed', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.black87, + ), ), ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 12), - ...controller.successfulSteps.map( + ...widget.controller.successfulSteps.map( (step) => Padding( - padding: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Container( @@ -173,10 +265,15 @@ class CapturedSelfieView extends StatelessWidget { size: 14, ), ), - const SizedBox(width: 12), - Text( - step, - style: const TextStyle(fontSize: 14, color: Colors.black87), + const SizedBox(width: 8), + Flexible( + child: Text( + step, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), ), ], ), @@ -187,32 +284,61 @@ class CapturedSelfieView extends StatelessWidget { ); } - // Handle the continue button - void _handleContinueButton() { + // Handle the continue button with edge function integration + Future _handleContinueButton() async { + // Make sure camera is fully disposed when we leave + widget.controller.disposeCamera(); + + // Avoid setState if widget is disposed + if (!mounted) return; + // Reset loading state in selfie controller before navigating back try { - if (selfieController != null) { + if (widget.selfieController != null) { + // Show loading state + setState(() { + isComparingWithID = true; + errorMessage = null; + }); + dev.log( - 'Found SelfieVerificationController, handling success', + 'Found SelfieVerificationController, setting captured selfie', name: 'LIVENESS_DEBUG', ); - // Connect with SelfieVerificationController - if (controller.capturedImage != null) { + + // Set the captured image + if (widget.controller.capturedImage != null) { dev.log( 'Setting captured image on SelfieVerificationController', name: 'LIVENESS_DEBUG', ); - selfieController?.selfieImage.value = controller.capturedImage; - // selfieController._processCapturedLivenessImage(); + + // First finish the navigation to prevent state updates after dispose + Future.microtask(() { + widget.selfieController!.selfieImage.value = + widget.controller.capturedImage; + Get.back(result: widget.controller.capturedImage); + }); } + } else { + // If no selfie controller, just go back with the result + Future.microtask( + () => Get.back(result: widget.controller.capturedImage), + ); } } catch (e) { dev.log( 'Error connecting with SelfieVerificationController: $e', name: 'LIVENESS_DEBUG', ); - // Continue without selfie controller + + if (mounted) { + setState(() { + isComparingWithID = false; + errorMessage = + 'Failed to process the captured image. Please try again.'; + }); + } } - Get.back(result: controller.capturedImage); } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/countdown_overlay_widget.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/countdown_overlay_widget.dart new file mode 100644 index 0000000..1cecddd --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/countdown_overlay_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/utils/constants/colors.dart'; + +class CountdownOverlayWidget extends StatelessWidget { + final FaceLivenessController controller; + + const CountdownOverlayWidget({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (controller.status.value != LivenessStatus.countdown) { + return SizedBox.shrink(); + } + + final seconds = controller.countdownSeconds.value; + + return Container( + color: Colors.black54, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Countdown circle + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black45, + border: Border.all(color: TColors.primary, width: 4), + ), + child: Center( + child: Text( + '$seconds', + style: TextStyle( + color: Colors.white, + fontSize: 64, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + SizedBox(height: 24), + Text( + 'Hold still', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8), + Text( + 'Keep your face centered in the frame', + style: TextStyle(color: Colors.white70, fontSize: 16), + ), + SizedBox(height: 32), + // Cancel button + TextButton.icon( + onPressed: controller.cancelCountdown, + icon: Icon(Icons.cancel_outlined, color: Colors.white70), + label: Text('Cancel', style: TextStyle(color: Colors.white70)), + style: TextButton.styleFrom( + backgroundColor: Colors.black38, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + ], + ), + ); + }); + } +} diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/debug_panel.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/debug_panel.dart index 4d1e2f6..1f1d1fc 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/debug_panel.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/debug_panel.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; /// Shows the debug panel for liveness detection void showLivenessDebugPanel( diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/error_state_widget.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/error_state_widget.dart index aa5e112..bf527ad 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/error_state_widget.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/error_state_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; class ErrorStateWidget extends StatelessWidget { diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/instruction_banner.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/instruction_banner.dart index fb85f40..29add4b 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/instruction_banner.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/instruction_banner.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; class InstructionBanner extends StatelessWidget { @@ -10,104 +10,83 @@ class InstructionBanner extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - decoration: BoxDecoration( - color: TColors.primary.withOpacity(0.08), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: TColors.primary.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - Icons.face_retouching_natural, - color: TColors.primary, - size: 20, - ), + return Obx(() { + final direction = controller.getCurrentDirection(); + final isCountdown = controller.status.value == LivenessStatus.countdown; + + // Select appropriate icon based on current status + IconData iconData; + Color iconColor; + + switch (controller.status.value) { + case LivenessStatus.detectingFace: + iconData = Icons.face; + iconColor = TColors.primary; + break; + case LivenessStatus.checkLeftRotation: + iconData = Icons.rotate_left; + iconColor = TColors.primary; + break; + case LivenessStatus.checkRightRotation: + iconData = Icons.rotate_right; + iconColor = TColors.primary; + break; + case LivenessStatus.checkSmile: + iconData = Icons.sentiment_satisfied_alt; + iconColor = TColors.primary; + break; + case LivenessStatus.checkEyesOpen: + iconData = Icons.remove_red_eye; + iconColor = TColors.primary; + break; + case LivenessStatus.readyForPhoto: + case LivenessStatus.countdown: + iconData = Icons.check_circle; + iconColor = Colors.green; + break; + case LivenessStatus.failed: + iconData = Icons.error; + iconColor = Colors.red; + break; + default: + iconData = Icons.info; + iconColor = TColors.primary; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: + isCountdown + ? Colors.green.withOpacity(0.1) + : TColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + isCountdown + ? Colors.green.withOpacity(0.3) + : TColors.primary.withOpacity(0.3), + width: 1, ), - const SizedBox(width: 16), - Expanded( - child: Obx( - () => Text( - controller.getCurrentDirection(), + ), + child: Row( + children: [ + Icon(iconData, color: iconColor, size: 24, + ), + SizedBox(width: 12), + Expanded( + child: Text( + direction, style: TextStyle( - fontSize: 15, + fontSize: 16, fontWeight: FontWeight.w500, - color: TColors.primary, - height: 1.4, + color: Colors.black87, ), ), ), - ), - // Status indicator - Obx( - () => Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: _getStatusColor(controller.status.value), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _getStatusText(controller.status.value), - style: TextStyle(color: Colors.white, fontSize: 12), - ), - ), - ), - ], - ), - ); - } - - // Function to get status color - Color _getStatusColor(LivenessStatus status) { - switch (status) { - case LivenessStatus.preparing: - case LivenessStatus.detectingFace: - return Colors.orange; - case LivenessStatus.failed: - return Colors.red; - case LivenessStatus.completed: - case LivenessStatus.photoTaken: - return Colors.green; - default: - return TColors.primary; - } - } - - // Function to get status text - String _getStatusText(LivenessStatus status) { - switch (status) { - case LivenessStatus.preparing: - return 'Preparing'; - case LivenessStatus.detectingFace: - return 'Detecting'; - case LivenessStatus.checkLeftRotation: - return 'Look Left'; - case LivenessStatus.checkRightRotation: - return 'Look Right'; - case LivenessStatus.checkSmile: - return 'Smile'; - case LivenessStatus.checkEyesOpen: - return 'Open Eyes'; - case LivenessStatus.readyForPhoto: - return 'Ready'; - case LivenessStatus.photoTaken: - return 'Processing'; - case LivenessStatus.completed: - return 'Success'; - case LivenessStatus.failed: - return 'Failed'; - default: - return 'Unknown'; - } + ], + ), + ); + }); } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/verification_progress_widget.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/verification_progress_widget.dart index 53131ff..868050a 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/verification_progress_widget.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/basic/widgets/verification_progress_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; class VerificationProgressWidget extends StatelessWidget { @@ -10,106 +10,106 @@ class VerificationProgressWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.all(20), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - spreadRadius: 0, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.verified_outlined, color: TColors.primary, size: 20), - const SizedBox(width: 8), - Text( - 'Verification Progress', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Colors.black87, - ), - ), - ], - ), - const SizedBox(height: 16), + return Obx(() { + final steps = controller.verificationSteps; + final completedSteps = controller.successfulSteps; - // Progress indicator - Obx( - () => LinearProgressIndicator( - value: controller.successfulSteps.length / 4, + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Progress indicator + LinearProgressIndicator( + value: steps.isEmpty ? 0 : completedSteps.length / steps.length, backgroundColor: Colors.grey.shade200, color: TColors.primary, minHeight: 6, borderRadius: BorderRadius.circular(3), ), - ), + const SizedBox(height: 12), - const SizedBox(height: 16), + // Completed steps + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate(steps.length, (index) { + final isCompleted = index < controller.currentStepIndex; + final isInProgress = index == controller.currentStepIndex; - // Steps list - Obx(() { - if (controller.successfulSteps.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text( - 'Follow the instructions to complete verification', - style: TextStyle( - fontSize: 14, - color: Colors.black54, - fontStyle: FontStyle.italic, + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, ), - ), - ); - } - - return Column( - children: - controller.successfulSteps - .map( - (step) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.green.shade50, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check, - color: Colors.green.shade600, - size: 14, - ), - ), - const SizedBox(width: 12), - Text( - step, - style: const TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), - ], - ), + decoration: BoxDecoration( + color: + isCompleted + ? Colors.green.withOpacity(0.1) + : isInProgress + ? TColors.primary.withOpacity(0.1) + : Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + isCompleted + ? Colors.green.withOpacity(0.3) + : isInProgress + ? TColors.primary.withOpacity(0.3) + : Colors.grey.withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isCompleted + ? Icons.check_circle + : isInProgress + ? Icons.timelapse + : Icons.circle_outlined, + size: 16, + color: + isCompleted + ? Colors.green + : isInProgress + ? TColors.primary + : Colors.grey, + ), + const SizedBox(width: 6), + Text( + _getShortStepName(steps[index]), + style: TextStyle( + fontSize: 12, + color: + isCompleted + ? Colors.green + : isInProgress + ? TColors.primary + : Colors.grey, + fontWeight: + isInProgress + ? FontWeight.bold + : FontWeight.normal, ), - ) - .toList(), - ); - }), - ], - ), - ); + ), + ], + ), + ); + }), + ), + ], + ), + ); + }); + } + + // Get shorter step names for chips + String _getShortStepName(String step) { + if (step.contains('left')) return 'Look Left'; + if (step.contains('right')) return 'Look Right'; + if (step.contains('smile')) return 'Smile'; + if (step.contains('eyes')) return 'Eyes Open'; + return step; } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/officer_info_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/officer_info_step.dart index 41257e2..2210ac4 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/officer_info_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/officer_info_step.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/officer-information/officer_info_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/officer-information/officer_info_controller.dart'; import 'package:sigap/src/shared/widgets/form/form_section_header.dart'; import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/unit_info_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/unit_info_step.dart index bdde40b..b633964 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/unit_info_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/officer/unit_info_step.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/officer-information/unit_info_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/officer-information/unit_info_controller.dart'; import 'package:sigap/src/shared/widgets/dropdown/custom_dropdown.dart'; import 'package:sigap/src/shared/widgets/form/form_section_header.dart'; import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; @@ -27,7 +27,7 @@ class UnitInfoStep extends StatelessWidget { title: 'Unit Information', subtitle: 'Please provide your unit details', ), - + const SizedBox(height: TSizes.spaceBtwItems), // Position field diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/city_selection.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/city_selection.dart index 1d83434..f4eed25 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/city_selection.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/city_selection.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/data/models/administrative_division.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/location_selection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/others/location_selection_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/face_verification_section.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/face_verification_section.dart index 3e7e59f..1aabc2e 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/face_verification_section.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/face_verification_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/facial_verification_controller.dart'; import 'package:sigap/src/shared/widgets/form/form_section_header.dart'; import 'package:sigap/src/shared/widgets/form/verification_status.dart'; import 'package:sigap/src/shared/widgets/info/tips_container.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart index 3090c12..487b847 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/place_of_birth_field.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_status_message.dart'; import 'package:sigap/src/shared/widgets/form/date_picker_field.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/place_of_birth_field.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/place_of_birth_field.dart index 5adcdef..3ee9267 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/place_of_birth_field.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/place_of_birth_field.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/data/models/administrative_division.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/city_selection.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_action_button.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_action_button.dart index 539c8c8..7b952c7 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_action_button.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_action_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; import 'package:sigap/src/shared/widgets/buttons/custom_elevated_button.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_status_message.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_status_message.dart index 862e64c..47d2465 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_status_message.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/verification_status_message.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/identity-verification/identity_verification_controller.dart'; import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart index 8a67a18..6a07739 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/signin_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart'; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart index 1e76d6b..b6e8bd7 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/signup_with_role_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/signup_with_role_controller.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart'; diff --git a/sigap-mobile/lib/src/utils/debug/liveness_debug_utils.dart b/sigap-mobile/lib/src/utils/debug/liveness_debug_utils.dart index 0e5ea59..a22222b 100644 --- a/sigap-mobile/lib/src/utils/debug/liveness_debug_utils.dart +++ b/sigap-mobile/lib/src/utils/debug/liveness_debug_utils.dart @@ -2,8 +2,8 @@ import 'dart:developer' as dev; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/face_liveness_detection_controller.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/face_liveness_detection_controller.dart'; +import 'package:sigap/src/features/auth/presentasion/controllers/signup/selfie-verification/selfie_verification_controller.dart'; /// Utility class for debugging the liveness detection and verification process class LivenessDebugUtils { @@ -166,7 +166,7 @@ class LivenessDebugUtils { if (Get.isRegistered()) { final controller = Get.find(); - controller.cancelLivenessDetection(); + // controller.cancelLivenessDetection(); controller.clearSelfieImage(); dev.log('Reset SelfieVerificationController', name: _logName); }