feat: enhance selfie verification process with auto-start feature and improved error handling
This commit is contained in:
parent
5b2806f0bb
commit
19687320ee
|
@ -639,6 +639,7 @@ class FaceLivenessController extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Verify that the captured image contains a face
|
||||
Future<bool> _verifyFaceInImage(XFile image) async {
|
||||
try {
|
||||
|
|
|
@ -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,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<void> _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<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 = '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 .',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<CapturedSelfieView>
|
|||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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();
|
||||
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<CapturedSelfieView>
|
|||
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;
|
||||
// Set the liveness flag first
|
||||
widget.selfieController!.isLivenessCheckPassed.value = true;
|
||||
|
||||
if (mounted && !isDisposed) {
|
||||
Get.back(result: widget.controller.capturedImage);
|
||||
}
|
||||
// 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<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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue