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
Future<bool> _verifyFaceInImage(XFile image) async {
try {

View File

@ -1,4 +1,5 @@
import 'dart:developer' as dev;
import 'dart:io';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
@ -30,6 +31,9 @@ class SelfieVerificationController extends GetxController {
final isPerformingLivenessCheck = false.obs;
final isLivenessCheckPassed = false.obs;
// New flag for auto starting verification
bool autoStartVerification = false;
// Face comparison results
final isComparingWithIDCard = false.obs;
final isMatchWithIDCard = 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,38 +117,86 @@ 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<void> _processCapturedLivenessImage() async {
if (selfieImage.value == null) return;
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;
selfieError.value = '';
dev.log(
'Processing captured image for verification: $imagePath',
name: 'SELFIE_VERIFICATION',
);
// 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) {
selfieError.value = 'No face detected in your selfie';
@ -125,9 +211,15 @@ class SelfieVerificationController extends GetxController {
// 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<void> _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<IdCardVerificationController>;
try {
// 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) {
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 = '';
}
}

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/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 .',
);
}

View File

@ -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);

View File

@ -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';
@ -613,7 +614,7 @@ class _CapturedSelfieViewState extends State<CapturedSelfieView>
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<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 {
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;
try {
if (widget.selfieController != null) {
errorMessage.value = null;
@ -647,26 +653,54 @@ class _CapturedSelfieViewState extends State<CapturedSelfieView>
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(
'Setting captured image on SelfieVerificationController',
'Successfully set image in selfie controller',
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));
widget.selfieController!.selfieImage.value =
widget.controller.capturedImage;
// Navigate back to selfie verification step by removing liveness detection from stack
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 {
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<CapturedSelfieView>
if (mounted && !isDisposed) {
isComparingWithID.value = false;
errorMessage.value =
'Failed to process the captured image. Please try again.';
'Failed to process the captured image: $e';
}
}
}