From 19687320ee4f7502b674afc0b35a7b836aec6f01 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Sun, 25 May 2025 12:09:29 +0700 Subject: [PATCH] feat: enhance selfie verification process with auto-start feature and improved error handling --- .../face_liveness_detection_controller.dart | 1 + .../selfie_verification_controller.dart | 197 +++++++++++++++--- .../liveness_detection_screen.dart | 3 +- .../selfie_verification_step.dart | 17 ++ .../widgets/captured_selfie_view.dart | 78 +++++-- 5 files changed, 238 insertions(+), 58 deletions(-) diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart index 7f9f3b4..57e6588 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart @@ -639,6 +639,7 @@ class FaceLivenessController extends GetxController { } } + // Verify that the captured image contains a face Future _verifyFaceInImage(XFile image) async { try { diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart index e58c373..60c1e2f 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart @@ -1,4 +1,5 @@ import 'dart:developer' as dev; +import 'dart:io'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; @@ -29,6 +30,9 @@ class SelfieVerificationController extends GetxController { // Liveness detection states final isPerformingLivenessCheck = false.obs; final isLivenessCheckPassed = false.obs; + + // New flag for auto starting verification + bool autoStartVerification = false; // Face comparison results final isComparingWithIDCard = false.obs; @@ -52,20 +56,54 @@ class SelfieVerificationController extends GetxController { } void _initializeAfterDependencies() { - // Listen for changes to selfieImage + // Listen for changes to selfieImage with improved null safety ever(selfieImage, (XFile? image) { - if (image != null) { - // When a selfie is set (after liveness check), - // automatically verify it against ID card - _processCapturedLivenessImage(); + dev.log( + 'Selfie image changed: ${image?.path}', + name: 'SELFIE_VERIFICATION', + ); + + if (image != null && autoStartVerification) { + // When returning from liveness check with a selfie, + // immediately start verification with a delay for UI to update + autoStartVerification = false; // Reset flag + + Future.delayed(Duration(milliseconds: 500), () { + startFaceVerification(); + }); } }); } - // Method to perform liveness detection + // Add public method to start verification process with better error handling + void startFaceVerification() { + if (selfieImage.value == null) { + dev.log( + 'Cannot start verification - no selfie image!', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = 'No selfie image available for verification'; + return; + } + + if (isVerifyingFace.value || isComparingWithIDCard.value) { + dev.log('Verification already in progress', name: 'SELFIE_VERIFICATION'); + return; + } + + dev.log( + 'Starting face verification with image: ${selfieImage.value!.path}', + name: 'SELFIE_VERIFICATION', + ); + + _processCapturedLivenessImage(); + } + + // Method to perform liveness detection with improved navigation void performLivenessDetection() async { try { isPerformingLivenessCheck.value = true; + autoStartVerification = true; // Set flag for auto verification // Check if FaceLivenessController is already registered final bool hasExistingController = @@ -79,55 +117,109 @@ class SelfieVerificationController extends GetxController { } // Register a new controller (will be done automatically by the widget) - final result = await Get.toNamed(AppRoutes.livenessDetection); + final result = await Get.toNamed( + AppRoutes.livenessDetection, + preventDuplicates: false, // Allow navigation to same route if needed + ); + // Handle the result based on what was returned if (result is XFile) { // Liveness check passed and returned an image + dev.log( + 'Received result from liveness detection, setting selfie image', + name: 'SELFIE_VERIFICATION', + ); + selfieImage.value = result; isLivenessCheckPassed.value = true; + isPerformingLivenessCheck.value = false; // The _processCapturedLivenessImage will be called automatically // due to the ever() listener we set up in onInit + } else if (result == 'navigation_handled') { + // Special case: navigation was handled by the captured selfie view + // The user was already navigated back to this screen + dev.log( + 'Navigation handled by captured selfie view', + name: 'SELFIE_VERIFICATION', + ); + isPerformingLivenessCheck.value = false; + // Don't reset other states since they should be set by the captured view } else { // User cancelled or something went wrong + dev.log( + 'No result from liveness detection, reset states', + name: 'SELFIE_VERIFICATION', + ); isPerformingLivenessCheck.value = false; isLivenessCheckPassed.value = false; + autoStartVerification = 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'; + autoStartVerification = false; // Reset flag on error } } - // Process the captured image after liveness check + // Process the captured image after liveness check with better error handling 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; + if (selfieImage.value == null) { + dev.log('No selfie image to process', name: 'SELFIE_VERIFICATION'); + selfieError.value = 'Missing selfie image. Please try again.'; return; } - // Face detected successfully - isSelfieValid.value = true; - isVerifyingFace.value = false; + final imagePath = selfieImage.value!.path; + if (!File(imagePath).existsSync()) { + dev.log( + 'Selfie image file does not exist: $imagePath', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = 'Selfie image file not found. Please try again.'; + return; + } - // Now compare with ID card if available - await _compareWithIdCard(); + isVerifyingFace.value = true; + selfieError.value = ''; + + dev.log( + 'Processing captured image for verification: $imagePath', + name: 'SELFIE_VERIFICATION', + ); + + // Now verify that the selfie contains a valid face + // Use a try-catch specifically for the face detection call + try { + 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('Face detection API error: $e', name: 'SELFIE_VERIFICATION'); + selfieError.value = 'Error detecting face: $e'; + isVerifyingFace.value = false; + isSelfieValid.value = false; + } } catch (e) { dev.log('Error processing selfie: $e', name: 'SELFIE_VERIFICATION'); - selfieError.value = 'Error verifying face: $e'; + selfieError.value = 'Error processing selfie: $e'; isVerifyingFace.value = false; isSelfieValid.value = false; } finally { @@ -135,25 +227,42 @@ class SelfieVerificationController extends GetxController { } } - // Compare selfie with ID card + // Compare selfie with ID card with better null handling Future _compareWithIdCard() async { if (selfieImage.value == null) { dev.log( 'No selfie image available for comparison', name: 'SELFIE_VERIFICATION', ); + selfieError.value = 'Missing selfie image for ID comparison'; return; } - XFile idCardImage; - - // find controller if it exists - + // Find controller if it exists, with better error handling if (idCardController == null) { - // If we don't have an ID card controller, log and return - Get.put; + try { + // Try to get the controller if it's registered + if (Get.isRegistered()) { + idCardController = Get.find(); + } else { + dev.log( + 'ID card controller not found, cannot compare faces', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = 'ID card data not available for comparison'; + return; + } + } catch (e) { + dev.log( + 'Error getting ID card controller: $e', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = 'ID card data not available for comparison'; + return; + } } + // Check if ID card image exists if (idCardController!.idCardImage.value == null) { dev.log( 'No ID card image available for comparison', @@ -163,7 +272,26 @@ class SelfieVerificationController extends GetxController { return; } - idCardImage = idCardController!.idCardImage.value!; + XFile idCardImage = idCardController!.idCardImage.value!; + + // Verify both image files exist + if (!File(idCardImage.path).existsSync()) { + dev.log( + 'ID card image file does not exist: ${idCardImage.path}', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = 'ID card image file not found'; + return; + } + + if (!File(selfieImage.value!.path).existsSync()) { + dev.log( + 'Selfie image file does not exist: ${selfieImage.value!.path}', + name: 'SELFIE_VERIFICATION', + ); + selfieError.value = 'Selfie image file not found'; + return; + } try { isComparingWithIDCard.value = true; @@ -234,6 +362,7 @@ class SelfieVerificationController extends GetxController { isLivenessCheckPassed.value = false; isPerformingLivenessCheck.value = false; hasConfirmedSelfie.value = false; + autoStartVerification = false; // Reset auto verification flag selfieError.value = ''; } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/liveness_detection_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/liveness_detection_screen.dart index 12e6e3d..0e05868 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/liveness_detection_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/liveness_detection_screen.dart @@ -8,7 +8,6 @@ import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-ve import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/countdown_overlay_widget.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/debug_panel.dart'; -import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/error_state_widget.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/instruction_banner.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/verification_progress_widget.dart'; import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart'; @@ -71,7 +70,7 @@ class LivenessDetectionPage extends StatelessWidget { return StateScreen( icon: Icons.camera_alt_outlined, title: 'Camera Error', - subtitle: 'Unable to access camera. Please try again later.', + subtitle: 'Unable to access camera. Please try again later .', ); } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart index a738dc3..520d611 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart @@ -315,6 +315,23 @@ class SelfieVerificationStep extends StatelessWidget { SelfieVerificationController controller, BuildContext context, ) { + // Debug current state + + // Check if we need to auto-start verification (when returning from liveness detection) + if (controller.selfieImage.value != null && + controller.isLivenessCheckPassed.value && + controller.autoStartVerification) { + // Log detection attempt + + // Start verification after the UI is built + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!controller.isVerifyingFace.value && + !controller.isComparingWithIDCard.value) { + controller.startFaceVerification(); + } + }); + } + switch (status) { case VerificationStatus.initial: return _buildInitialState(controller, context); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart index 24a265a..5bc5fb4 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/utils/constants/colors.dart'; @@ -467,7 +468,7 @@ class _CapturedSelfieViewState extends State ); }, ); - }), + }), ], ), ); @@ -613,7 +614,7 @@ class _CapturedSelfieViewState extends State color: TColors.primary, ), const SizedBox(width: TSizes.sm), - const Text( + Text( 'Try Again', style: TextStyle( fontSize: 16, @@ -631,14 +632,19 @@ class _CapturedSelfieViewState extends State ); } - // Update continue button handler to use ValueNotifier + // Fix the continue button handler to navigate directly to the selfie verification step Future _handleContinueButton() async { if (!mounted || isDisposed) return; - widget.controller.disposeCamera(); - isComparingWithID.value = true; - try { + if (widget.controller.capturedImage == null) { + dev.log('Error: No captured image available', name: 'LIVENESS_DEBUG'); + errorMessage.value = 'No image was captured. Please try again.'; + return; + } + + isComparingWithID.value = true; + if (widget.selfieController != null) { errorMessage.value = null; @@ -647,26 +653,54 @@ class _CapturedSelfieViewState extends State name: 'LIVENESS_DEBUG', ); - if (widget.controller.capturedImage != null) { - dev.log( - 'Setting captured image on SelfieVerificationController', - name: 'LIVENESS_DEBUG', - ); + // Create a copy of the captured image path to ensure it remains valid + final imagePath = widget.controller.capturedImage!.path; + dev.log('Captured image path: $imagePath', name: 'LIVENESS_DEBUG'); - // Use Future.delayed to ensure UI updates before navigation - await Future.delayed(Duration(milliseconds: 100)); + // Create a new XFile instance from the path + final xFile = XFile(imagePath); - widget.selfieController!.selfieImage.value = - widget.controller.capturedImage; - - if (mounted && !isDisposed) { - Get.back(result: widget.controller.capturedImage); - } + // Set the liveness flag first + widget.selfieController!.isLivenessCheckPassed.value = true; + + // Set flag to automatically start verification upon returning + widget.selfieController!.autoStartVerification = true; + + // Set the captured image in the selfie controller - do this last + widget.selfieController!.selfieImage.value = xFile; + + dev.log( + 'Successfully set image in selfie controller', + name: 'LIVENESS_DEBUG', + ); + + // Now dispose the camera after we've handled the image + widget.controller.disposeCamera(); + + // Update the controller to ensure changes propagate + widget.selfieController!.update(); + + // Wait a moment to ensure the image is properly set + await Future.delayed(Duration(milliseconds: 100)); + + // Navigate back to selfie verification step by removing liveness detection from stack + if (mounted && !isDisposed) { + // Use Get.offAll to clear the entire navigation stack and go to signup page + // The signup page will show the selfie verification step since we've set the image + Get.offAllNamed('/signup'); // Replace with your actual signup route + + // Alternative: if you know the exact route name for selfie verification step + // Get.offNamed('/signup/selfie-verification'); } } else { - await Future.delayed(Duration(milliseconds: 100)); + widget.controller.disposeCamera(); + if (mounted && !isDisposed) { - Get.back(result: widget.controller.capturedImage); + Get.back( + closeOverlays: true, + canPop: true, + result: widget.controller.capturedImage, + ); } } } catch (e) { @@ -678,7 +712,7 @@ class _CapturedSelfieViewState extends State if (mounted && !isDisposed) { isComparingWithID.value = false; errorMessage.value = - 'Failed to process the captured image. Please try again.'; + 'Failed to process the captured image: $e'; } } }