feat: enhance selfie verification process with auto-start feature and improved error handling

This commit is contained in:
vergiLgood1 2025-05-25 12:09:29 +07:00
parent 5b2806f0bb
commit 19687320ee
5 changed files with 238 additions and 58 deletions

View File

@ -639,6 +639,7 @@ class FaceLivenessController extends GetxController {
} }
} }
// Verify that the captured image contains a face // Verify that the captured image contains a face
Future<bool> _verifyFaceInImage(XFile image) async { Future<bool> _verifyFaceInImage(XFile image) async {
try { try {

View File

@ -1,4 +1,5 @@
import 'dart:developer' as dev; import 'dart:developer' as dev;
import 'dart:io';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -30,6 +31,9 @@ class SelfieVerificationController extends GetxController {
final isPerformingLivenessCheck = false.obs; final isPerformingLivenessCheck = false.obs;
final isLivenessCheckPassed = false.obs; final isLivenessCheckPassed = false.obs;
// New flag for auto starting verification
bool autoStartVerification = false;
// Face comparison results // Face comparison results
final isComparingWithIDCard = false.obs; final isComparingWithIDCard = false.obs;
final isMatchWithIDCard = false.obs; final isMatchWithIDCard = false.obs;
@ -52,20 +56,54 @@ class SelfieVerificationController extends GetxController {
} }
void _initializeAfterDependencies() { void _initializeAfterDependencies() {
// Listen for changes to selfieImage // Listen for changes to selfieImage with improved null safety
ever(selfieImage, (XFile? image) { ever(selfieImage, (XFile? image) {
if (image != null) { dev.log(
// When a selfie is set (after liveness check), 'Selfie image changed: ${image?.path}',
// automatically verify it against ID card name: 'SELFIE_VERIFICATION',
_processCapturedLivenessImage(); );
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 { void performLivenessDetection() async {
try { try {
isPerformingLivenessCheck.value = true; isPerformingLivenessCheck.value = true;
autoStartVerification = true; // Set flag for auto verification
// Check if FaceLivenessController is already registered // Check if FaceLivenessController is already registered
final bool hasExistingController = final bool hasExistingController =
@ -79,38 +117,86 @@ class SelfieVerificationController extends GetxController {
} }
// Register a new controller (will be done automatically by the widget) // 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) { if (result is XFile) {
// Liveness check passed and returned an image // Liveness check passed and returned an image
dev.log(
'Received result from liveness detection, setting selfie image',
name: 'SELFIE_VERIFICATION',
);
selfieImage.value = result; selfieImage.value = result;
isLivenessCheckPassed.value = true; isLivenessCheckPassed.value = true;
isPerformingLivenessCheck.value = false;
// The _processCapturedLivenessImage will be called automatically // The _processCapturedLivenessImage will be called automatically
// due to the ever() listener we set up in onInit // 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 { } else {
// User cancelled or something went wrong // User cancelled or something went wrong
dev.log(
'No result from liveness detection, reset states',
name: 'SELFIE_VERIFICATION',
);
isPerformingLivenessCheck.value = false; isPerformingLivenessCheck.value = false;
isLivenessCheckPassed.value = false; isLivenessCheckPassed.value = false;
autoStartVerification = false;
} }
} catch (e) { } catch (e) {
dev.log('Error in liveness detection: $e', name: 'SELFIE_VERIFICATION'); dev.log('Error in liveness detection: $e', name: 'SELFIE_VERIFICATION');
isPerformingLivenessCheck.value = false; isPerformingLivenessCheck.value = false;
isLivenessCheckPassed.value = false; isLivenessCheckPassed.value = false;
selfieError.value = 'Liveness check failed: $e'; 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<void> _processCapturedLivenessImage() async { Future<void> _processCapturedLivenessImage() async {
if (selfieImage.value == null) return;
try { try {
if (selfieImage.value == null) {
dev.log('No selfie image to process', name: 'SELFIE_VERIFICATION');
selfieError.value = 'Missing selfie image. Please try again.';
return;
}
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;
}
isVerifyingFace.value = true; isVerifyingFace.value = true;
selfieError.value = ''; selfieError.value = '';
dev.log(
'Processing captured image for verification: $imagePath',
name: 'SELFIE_VERIFICATION',
);
// Now verify that the selfie contains a valid face // Now verify that the selfie contains a valid face
final faces = await _edgeFunctionService.detectFaces(selfieImage.value!); // Use a try-catch specifically for the face detection call
try {
final faces = await _edgeFunctionService.detectFaces(
selfieImage.value!,
);
if (faces.isEmpty) { if (faces.isEmpty) {
selfieError.value = 'No face detected in your selfie'; selfieError.value = 'No face detected in your selfie';
@ -125,9 +211,15 @@ class SelfieVerificationController extends GetxController {
// Now compare with ID card if available // Now compare with ID card if available
await _compareWithIdCard(); 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) { } catch (e) {
dev.log('Error processing selfie: $e', name: 'SELFIE_VERIFICATION'); dev.log('Error processing selfie: $e', name: 'SELFIE_VERIFICATION');
selfieError.value = 'Error verifying face: $e'; selfieError.value = 'Error processing selfie: $e';
isVerifyingFace.value = false; isVerifyingFace.value = false;
isSelfieValid.value = false; isSelfieValid.value = false;
} finally { } finally {
@ -135,25 +227,42 @@ class SelfieVerificationController extends GetxController {
} }
} }
// Compare selfie with ID card // Compare selfie with ID card with better null handling
Future<void> _compareWithIdCard() async { Future<void> _compareWithIdCard() async {
if (selfieImage.value == null) { if (selfieImage.value == null) {
dev.log( dev.log(
'No selfie image available for comparison', 'No selfie image available for comparison',
name: 'SELFIE_VERIFICATION', name: 'SELFIE_VERIFICATION',
); );
selfieError.value = 'Missing selfie image for ID comparison';
return; return;
} }
XFile idCardImage; // Find controller if it exists, with better error handling
// find controller if it exists
if (idCardController == null) { if (idCardController == null) {
// If we don't have an ID card controller, log and return try {
Get.put<IdCardVerificationController>; // Try to get the controller if it's registered
if (Get.isRegistered<IdCardVerificationController>()) {
idCardController = Get.find<IdCardVerificationController>();
} 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) { if (idCardController!.idCardImage.value == null) {
dev.log( dev.log(
'No ID card image available for comparison', 'No ID card image available for comparison',
@ -163,7 +272,26 @@ class SelfieVerificationController extends GetxController {
return; 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 { try {
isComparingWithIDCard.value = true; isComparingWithIDCard.value = true;
@ -234,6 +362,7 @@ class SelfieVerificationController extends GetxController {
isLivenessCheckPassed.value = false; isLivenessCheckPassed.value = false;
isPerformingLivenessCheck.value = false; isPerformingLivenessCheck.value = false;
hasConfirmedSelfie.value = false; hasConfirmedSelfie.value = false;
autoStartVerification = false; // Reset auto verification flag
selfieError.value = ''; selfieError.value = '';
} }
} }

View File

@ -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/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/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/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/instruction_banner.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/verification_progress_widget.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'; import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';

View File

@ -315,6 +315,23 @@ class SelfieVerificationStep extends StatelessWidget {
SelfieVerificationController controller, SelfieVerificationController controller,
BuildContext context, 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) { switch (status) {
case VerificationStatus.initial: case VerificationStatus.initial:
return _buildInitialState(controller, context); return _buildInitialState(controller, context);

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/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/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
@ -613,7 +614,7 @@ class _CapturedSelfieViewState extends State<CapturedSelfieView>
color: TColors.primary, color: TColors.primary,
), ),
const SizedBox(width: TSizes.sm), const SizedBox(width: TSizes.sm),
const Text( Text(
'Try Again', 'Try Again',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
@ -631,14 +632,19 @@ class _CapturedSelfieViewState extends State<CapturedSelfieView>
); );
} }
// Update continue button handler to use ValueNotifier // Fix the continue button handler to navigate directly to the selfie verification step
Future<void> _handleContinueButton() async { Future<void> _handleContinueButton() async {
if (!mounted || isDisposed) return; if (!mounted || isDisposed) return;
widget.controller.disposeCamera(); 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; isComparingWithID.value = true;
try {
if (widget.selfieController != null) { if (widget.selfieController != null) {
errorMessage.value = null; errorMessage.value = null;
@ -647,26 +653,54 @@ class _CapturedSelfieViewState extends State<CapturedSelfieView>
name: 'LIVENESS_DEBUG', name: 'LIVENESS_DEBUG',
); );
if (widget.controller.capturedImage != null) { // 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');
// Create a new XFile instance from the path
final xFile = XFile(imagePath);
// 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( dev.log(
'Setting captured image on SelfieVerificationController', 'Successfully set image in selfie controller',
name: 'LIVENESS_DEBUG', name: 'LIVENESS_DEBUG',
); );
// Use Future.delayed to ensure UI updates before navigation // 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)); await Future.delayed(Duration(milliseconds: 100));
widget.selfieController!.selfieImage.value = // Navigate back to selfie verification step by removing liveness detection from stack
widget.controller.capturedImage;
if (mounted && !isDisposed) { if (mounted && !isDisposed) {
Get.back(result: widget.controller.capturedImage); // 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 { } else {
await Future.delayed(Duration(milliseconds: 100)); widget.controller.disposeCamera();
if (mounted && !isDisposed) { if (mounted && !isDisposed) {
Get.back(result: widget.controller.capturedImage); Get.back(
closeOverlays: true,
canPop: true,
result: widget.controller.capturedImage,
);
} }
} }
} catch (e) { } catch (e) {
@ -678,7 +712,7 @@ class _CapturedSelfieViewState extends State<CapturedSelfieView>
if (mounted && !isDisposed) { if (mounted && !isDisposed) {
isComparingWithID.value = false; isComparingWithID.value = false;
errorMessage.value = errorMessage.value =
'Failed to process the captured image. Please try again.'; 'Failed to process the captured image: $e';
} }
} }
} }