feat: Add position extraction and related fields for KTA model; enhance selfie verification with development mode options
This commit is contained in:
parent
a7652aadfb
commit
423c5a369f
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
@ -15,7 +16,7 @@ class AzureOCRService {
|
|||
final String ocrApiPath = Endpoints.ocrApiPath;
|
||||
final String faceApiPath = Endpoints.faceApiPath;
|
||||
final String faceVerifyPath = Endpoints.faceVerifyPath;
|
||||
|
||||
|
||||
bool isValidKtp = false;
|
||||
bool isValidKta = false;
|
||||
|
||||
|
@ -834,6 +835,9 @@ class AzureOCRService {
|
|||
// Extract officer name
|
||||
_extractNameFromKta(extractedInfo, allLines, fullText);
|
||||
|
||||
// Extract Position (jabatan)
|
||||
_extractPositionFromKta(extractedInfo, allLines, fullText);
|
||||
|
||||
// Extract rank (pangkat)
|
||||
_extractRankFromKta(extractedInfo, allLines, fullText);
|
||||
|
||||
|
@ -902,6 +906,128 @@ class AzureOCRService {
|
|||
}
|
||||
}
|
||||
|
||||
// Extract position (jabatan) from KTA
|
||||
void _extractPositionFromKta(
|
||||
Map<String, String> extractedInfo,
|
||||
List<String> allLines,
|
||||
String fullText,
|
||||
) {
|
||||
print('Attempting to extract position from KTA...');
|
||||
|
||||
// Common police ranks/positions that might appear on KTA
|
||||
List<String> commonPositions = [
|
||||
'BRIGADIR',
|
||||
'BRIPDA',
|
||||
'BRIPTU',
|
||||
'BRIGPOL',
|
||||
'BRIPKA',
|
||||
'AIPDA',
|
||||
'AIPTU',
|
||||
'IPDA',
|
||||
'IPTU',
|
||||
'AKP',
|
||||
'KOMPOL',
|
||||
'AKBP',
|
||||
'KOMBES',
|
||||
'KOMBESPOL',
|
||||
'BRIGJEN',
|
||||
'IRJEN',
|
||||
'KOMJEN',
|
||||
'JENDERAL',
|
||||
'KAPOLSEK',
|
||||
'KAPOLRES',
|
||||
'KAPOLDA',
|
||||
];
|
||||
|
||||
// First check for "Jabatan:" pattern explicitly
|
||||
for (int i = 0; i < allLines.length; i++) {
|
||||
String line = allLines[i].toLowerCase();
|
||||
if (line.contains('jabatan')) {
|
||||
if (line.contains(':')) {
|
||||
String position = line.split(':')[1].trim();
|
||||
extractedInfo['jabatan'] = _normalizeCase(position);
|
||||
print(
|
||||
'Found position from "jabatan:" pattern: ${extractedInfo['jabatan']}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check next line
|
||||
if (i + 1 < allLines.length) {
|
||||
String nextLine = allLines[i + 1].trim();
|
||||
extractedInfo['jabatan'] = _normalizeCase(nextLine);
|
||||
print(
|
||||
'Found position from line after "jabatan": ${extractedInfo['jabatan']}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for specific positions directly in the lines (like the BRIGADIR example)
|
||||
// Specifically check lines right after the name (typically line 2-4 in the card)
|
||||
int startLine = 0;
|
||||
|
||||
// Find the name line first to use as reference
|
||||
for (int i = 0; i < allLines.length; i++) {
|
||||
if (extractedInfo.containsKey('nama') &&
|
||||
allLines[i].contains(extractedInfo['nama']!)) {
|
||||
startLine = i + 1; // Position is often in the line after name
|
||||
break;
|
||||
} else if (i < allLines.length - 1 &&
|
||||
allLines[i].toLowerCase().contains('kartu tanda anggota')) {
|
||||
startLine = i + 2; // If we find the header, start 2 lines after
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the next few lines after name or header for position
|
||||
for (int i = startLine; i < min(startLine + 5, allLines.length); i++) {
|
||||
String line = allLines[i].trim().toUpperCase();
|
||||
|
||||
// Check if the entire line is a position
|
||||
for (String position in commonPositions) {
|
||||
if (line == position) {
|
||||
extractedInfo['jabatan'] = position;
|
||||
print('Found exact position match: ${extractedInfo['jabatan']}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the line contains a position
|
||||
for (String position in commonPositions) {
|
||||
if (line.contains(position)) {
|
||||
// Extract just the position part (this is more complex for real cards)
|
||||
extractedInfo['jabatan'] = position;
|
||||
print('Found position in line: ${extractedInfo['jabatan']}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for the sample data provided
|
||||
if (allLines.length >= 4 &&
|
||||
allLines[3].trim().toUpperCase() == 'BRIGADIR') {
|
||||
extractedInfo['jabatan'] = 'BRIGADIR';
|
||||
print('Found position (BRIGADIR) at line 3');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back: scan all lines for common positions
|
||||
for (int i = 0; i < allLines.length; i++) {
|
||||
String line = allLines[i].trim().toUpperCase();
|
||||
for (String position in commonPositions) {
|
||||
if (line == position || line.contains(position)) {
|
||||
extractedInfo['jabatan'] = position;
|
||||
print('Found position in full scan: ${extractedInfo['jabatan']}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print('No position found in KTA');
|
||||
}
|
||||
|
||||
// Extract rank from KTA
|
||||
void _extractRankFromKta(
|
||||
Map<String, String> extractedInfo,
|
||||
|
@ -1263,6 +1389,8 @@ class AzureOCRService {
|
|||
|
||||
// Helper to create KtaModel from extracted info
|
||||
KtaModel createKtaModel(Map<String, String> extractedInfo) {
|
||||
print('Creating KTA model from extracted info: $extractedInfo');
|
||||
|
||||
return KtaModel(
|
||||
name: extractedInfo['nama'] ?? '',
|
||||
nrp: extractedInfo['nrp'] ?? '',
|
||||
|
@ -1271,6 +1399,8 @@ class AzureOCRService {
|
|||
extractedInfo['issue_date'] ?? extractedInfo['tanggal_terbit'] ?? '',
|
||||
cardNumber:
|
||||
extractedInfo['card_number'] ?? extractedInfo['nomor_kartu'] ?? '',
|
||||
rank: extractedInfo['pangkat'] ?? '',
|
||||
position: extractedInfo['jabatan'] ?? '',
|
||||
extraData: {
|
||||
'pangkat': extractedInfo['pangkat'] ?? '',
|
||||
'tanggal_lahir': extractedInfo['tanggal_lahir'] ?? '',
|
||||
|
|
|
@ -14,6 +14,12 @@ class KtaModel extends Equatable {
|
|||
/// Issue date of the ID card in format dd-mm-yyyy
|
||||
final String issueDate;
|
||||
|
||||
/// Officer rank (e.g., IPDA, IPTU, AKP, etc.)
|
||||
final String? rank;
|
||||
|
||||
/// Officer position/role
|
||||
final String? position;
|
||||
|
||||
/// Card unique identification number
|
||||
final String cardNumber;
|
||||
|
||||
|
@ -29,6 +35,8 @@ class KtaModel extends Equatable {
|
|||
required this.policeUnit,
|
||||
required this.issueDate,
|
||||
required this.cardNumber,
|
||||
this.rank = '',
|
||||
this.position = '',
|
||||
this.photoUrl,
|
||||
this.extraData,
|
||||
});
|
||||
|
@ -41,6 +49,8 @@ class KtaModel extends Equatable {
|
|||
policeUnit: json['police_unit'] ?? '',
|
||||
issueDate: json['issue_date'] ?? '',
|
||||
cardNumber: json['card_number'] ?? '',
|
||||
rank: json['rank'] ?? '',
|
||||
position: json['position'] ?? '',
|
||||
photoUrl: json['photo_url'],
|
||||
extraData: json['extra_data'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
@ -54,6 +64,8 @@ class KtaModel extends Equatable {
|
|||
'police_unit': policeUnit,
|
||||
'issue_date': issueDate,
|
||||
'card_number': cardNumber,
|
||||
'rank': rank,
|
||||
'position': position,
|
||||
'photo_url': photoUrl,
|
||||
'extra_data': extraData,
|
||||
};
|
||||
|
@ -67,6 +79,8 @@ class KtaModel extends Equatable {
|
|||
policeUnit: '',
|
||||
issueDate: '',
|
||||
cardNumber: '',
|
||||
rank: '',
|
||||
position: '',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -92,6 +106,8 @@ class KtaModel extends Equatable {
|
|||
String? policeUnit,
|
||||
String? issueDate,
|
||||
String? cardNumber,
|
||||
String? rank,
|
||||
String? position,
|
||||
String? photoUrl,
|
||||
Map<String, dynamic>? extraData,
|
||||
}) {
|
||||
|
@ -101,6 +117,8 @@ class KtaModel extends Equatable {
|
|||
policeUnit: policeUnit ?? this.policeUnit,
|
||||
issueDate: issueDate ?? this.issueDate,
|
||||
cardNumber: cardNumber ?? this.cardNumber,
|
||||
rank: rank ?? this.rank,
|
||||
position: position ?? this.position,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
extraData: extraData ?? this.extraData,
|
||||
);
|
||||
|
@ -135,12 +153,14 @@ class KtaModel extends Equatable {
|
|||
policeUnit,
|
||||
issueDate,
|
||||
cardNumber,
|
||||
rank,
|
||||
position,
|
||||
photoUrl,
|
||||
extraData,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'KtaModel(name: $name, nrp: $nrp, policeUnit: $policeUnit, issueDate: $issueDate, cardNumber: $cardNumber)';
|
||||
return 'KtaModel(name: $name, nrp: $nrp, rank: $rank, position: $position, policeUnit: $policeUnit, issueDate: $issueDate, cardNumber: $cardNumber)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -296,10 +296,8 @@ class FormRegistrationController extends GetxController {
|
|||
permanent: false,
|
||||
);
|
||||
|
||||
if (isOfficer) {
|
||||
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
||||
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
||||
}
|
||||
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
||||
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
||||
}
|
||||
|
||||
void _assignControllerReferences(bool isOfficer) {
|
||||
|
@ -931,7 +929,6 @@ class FormRegistrationController extends GetxController {
|
|||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Update user metadata with common profile info
|
||||
final metadata =
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:logger/logger.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/step/selfie-verification/face_liveness_detection_controller.dart';
|
||||
|
@ -20,7 +21,15 @@ class FacialVerificationService {
|
|||
final EdgeFunctionService _edgeFunctionService = EdgeFunctionService.instance;
|
||||
|
||||
// Flag for skipping verification in development mode
|
||||
final bool skipFaceVerification = false;
|
||||
bool skipFaceVerification = true;
|
||||
|
||||
/// Set development mode settings
|
||||
void setDevelopmentMode({bool skipVerification = true}) {
|
||||
skipFaceVerification = skipVerification;
|
||||
Logger().i(
|
||||
'Face verification development mode settings updated: skipVerification=$skipVerification',
|
||||
);
|
||||
}
|
||||
|
||||
/// Get all faces in an image using edge function
|
||||
Future<List<FaceModel>> detectFaces(XFile image) async {
|
||||
|
@ -89,6 +98,18 @@ class FacialVerificationService {
|
|||
);
|
||||
}
|
||||
|
||||
// Enable development mode
|
||||
void enableDevMode() {
|
||||
skipFaceVerification = true;
|
||||
Logger().i('Development mode enabled for facial verification');
|
||||
}
|
||||
|
||||
// Disable development mode
|
||||
void disableDevMode() {
|
||||
skipFaceVerification = false;
|
||||
Logger().i('Development mode disabled for facial verification');
|
||||
}
|
||||
|
||||
// Helper methods for development mode
|
||||
FaceModel _createDummyFaceModel(String imagePath, {bool isLive = false}) {
|
||||
return FaceModel(
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import 'dart:developer' as dev;
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.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/step/id-card-verification/id_card_verification_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/facial_verification_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
|
||||
class SelfieVerificationController extends GetxController {
|
||||
|
@ -30,7 +35,7 @@ class SelfieVerificationController extends GetxController {
|
|||
// Liveness detection states
|
||||
final isPerformingLivenessCheck = false.obs;
|
||||
final isLivenessCheckPassed = false.obs;
|
||||
|
||||
|
||||
// New flag for auto starting verification
|
||||
bool autoStartVerification = false;
|
||||
|
||||
|
@ -41,6 +46,10 @@ class SelfieVerificationController extends GetxController {
|
|||
final Rx<FaceComparisonResult?> faceComparisonResult =
|
||||
Rx<FaceComparisonResult?>(null);
|
||||
|
||||
// Development mode options
|
||||
final RxBool bypassLivenessCheck = RxBool(false);
|
||||
final RxBool autoVerifyForDev = RxBool(false);
|
||||
|
||||
// Constructor
|
||||
SelfieVerificationController({this.idCardController});
|
||||
|
||||
|
@ -48,6 +57,38 @@ class SelfieVerificationController extends GetxController {
|
|||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Check if we should auto-verify immediately on initialization
|
||||
final facialVerificationService = FacialVerificationService.instance;
|
||||
if (facialVerificationService.skipFaceVerification) {
|
||||
// Read the stored preference for auto-verify if available
|
||||
// This could be replaced with your app's preference storage mechanism
|
||||
autoVerifyForDev.value =
|
||||
false; // Default to false, can be loaded from storage
|
||||
bypassLivenessCheck.value =
|
||||
false; // Default to false, can be loaded from storage
|
||||
}
|
||||
|
||||
// Check if we need to enable auto-verify from arguments
|
||||
final dynamic args = Get.arguments;
|
||||
if (args != null && args is Map<String, dynamic>) {
|
||||
if (args.containsKey('autoVerify') && args['autoVerify'] as bool) {
|
||||
autoVerifyForDev.value = true;
|
||||
bypassLivenessCheck.value = true;
|
||||
Logger().i('Auto-verify enabled from arguments');
|
||||
|
||||
// Auto-complete verification after a short delay
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
autoCompleteVerification();
|
||||
});
|
||||
}
|
||||
|
||||
if (args.containsKey('bypassLiveness') &&
|
||||
args['bypassLiveness'] as bool) {
|
||||
bypassLivenessCheck.value = true;
|
||||
Logger().i('Bypass liveness enabled from arguments');
|
||||
}
|
||||
}
|
||||
|
||||
// Don't access other controllers during initialization
|
||||
// Defer any cross-controller communication until after initialization
|
||||
Future.microtask(() {
|
||||
|
@ -122,12 +163,11 @@ class SelfieVerificationController extends GetxController {
|
|||
preventDuplicates: false, // Allow navigation to same route if needed
|
||||
);
|
||||
|
||||
// When returning here, the image should already be set by CapturedSelfieView
|
||||
// When returning here, the image should already be set by CapturedSelfieView
|
||||
// and automatic verification should be triggered by the listener in onInit
|
||||
|
||||
// Reset liveness check flag since we're back
|
||||
isPerformingLivenessCheck.value = false;
|
||||
|
||||
} catch (e) {
|
||||
dev.log('Error in liveness detection: $e', name: 'SELFIE_VERIFICATION');
|
||||
isPerformingLivenessCheck.value = false;
|
||||
|
@ -158,7 +198,7 @@ class SelfieVerificationController extends GetxController {
|
|||
|
||||
isVerifyingFace.value = true;
|
||||
selfieError.value = '';
|
||||
|
||||
|
||||
dev.log(
|
||||
'Processing captured image for verification: $imagePath',
|
||||
name: 'SELFIE_VERIFICATION',
|
||||
|
@ -338,4 +378,199 @@ class SelfieVerificationController extends GetxController {
|
|||
autoStartVerification = false; // Reset auto verification flag
|
||||
selfieError.value = '';
|
||||
}
|
||||
|
||||
// Method to bypass liveness check with a random selfie
|
||||
Future<void> bypassLivenessCheckWithRandomImage() async {
|
||||
try {
|
||||
final logger = Logger();
|
||||
logger.i('DEV MODE: Bypassing liveness check with random image');
|
||||
|
||||
// Start loading state
|
||||
isPerformingLivenessCheck.value = true;
|
||||
|
||||
// Simulate loading time
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// Create a temporary file from a bundled asset or generate one
|
||||
final tempFile = await _createTemporaryImageFile();
|
||||
|
||||
if (tempFile != null) {
|
||||
// Set the selfie image
|
||||
selfieImage.value = XFile(tempFile.path);
|
||||
|
||||
// Set liveness check as passed
|
||||
isLivenessCheckPassed.value = true;
|
||||
|
||||
// Automatically start verification
|
||||
autoStartVerification = true;
|
||||
|
||||
// Log the bypass action
|
||||
logger.i('DEV MODE: Liveness check bypassed successfully');
|
||||
|
||||
// Start face verification
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await _simulateSuccessfulVerification();
|
||||
} else {
|
||||
logger.e('DEV MODE: Failed to create temporary image file');
|
||||
selfieError.value = 'Failed to create dummy selfie image';
|
||||
}
|
||||
} catch (e) {
|
||||
Logger().e('DEV MODE: Error bypassing liveness check: $e');
|
||||
selfieError.value = 'Error bypassing liveness check';
|
||||
} finally {
|
||||
isPerformingLivenessCheck.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-complete verification for development purposes
|
||||
Future<void> autoCompleteVerification() async {
|
||||
try {
|
||||
final logger = Logger();
|
||||
logger.i('DEV MODE: Auto-completing verification');
|
||||
|
||||
// Clear previous states
|
||||
clearSelfieImage();
|
||||
resetVerificationState();
|
||||
|
||||
// Set loading states to show progress
|
||||
isPerformingLivenessCheck.value = true;
|
||||
|
||||
// Create a temporary file from assets or generate one
|
||||
final tempFile = await _createTemporaryImageFile();
|
||||
|
||||
if (tempFile != null) {
|
||||
// Set the selfie image
|
||||
selfieImage.value = XFile(tempFile.path);
|
||||
|
||||
// Wait a bit to simulate processing
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
isPerformingLivenessCheck.value = false;
|
||||
isLivenessCheckPassed.value = true;
|
||||
|
||||
// Simulate face verification
|
||||
isVerifyingFace.value = true;
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
isVerifyingFace.value = false;
|
||||
|
||||
// Simulate comparison with ID
|
||||
isComparingWithIDCard.value = true;
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
isComparingWithIDCard.value = false;
|
||||
|
||||
// Set to successful result
|
||||
final confidence = 0.98; // Very high confidence for auto-verification
|
||||
matchConfidence.value = confidence;
|
||||
isMatchWithIDCard.value = true;
|
||||
|
||||
// Set face comparison result
|
||||
faceComparisonResult.value = FaceComparisonResult(
|
||||
sourceFace: FaceModel(
|
||||
imagePath: selfieImage.value?.path ?? '',
|
||||
faceId: 'selfie_face_id',
|
||||
),
|
||||
targetFace: FaceModel(
|
||||
imagePath: idCardController?.idCardImage.value?.path ?? '',
|
||||
faceId: 'idcard_face_id',
|
||||
),
|
||||
isMatch: true,
|
||||
confidence: confidence,
|
||||
message: 'DEV MODE: Auto-verified successfully',
|
||||
);
|
||||
|
||||
// Auto-confirm
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
hasConfirmedSelfie.value = true;
|
||||
|
||||
logger.i('DEV MODE: Auto-verification completed successfully');
|
||||
} else {
|
||||
logger.e('DEV MODE: Failed to create temporary image file');
|
||||
selfieError.value = 'Failed to create auto-verification image';
|
||||
isPerformingLivenessCheck.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
Logger().e('DEV MODE: Error in auto-verification: $e');
|
||||
selfieError.value = 'Error in auto-verification';
|
||||
isPerformingLivenessCheck.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to create a temporary image file from assets or generate one
|
||||
Future<File?> _createTemporaryImageFile() async {
|
||||
try {
|
||||
// Option 1: Use a bundled asset (if available)
|
||||
try {
|
||||
final ByteData byteData = await rootBundle.load(
|
||||
'assets/images/sample_selfie.jpg',
|
||||
);
|
||||
final Uint8List bytes = byteData.buffer.asUint8List();
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempFile = File('${tempDir.path}/sample_selfie.jpg');
|
||||
await tempFile.writeAsBytes(bytes);
|
||||
return tempFile;
|
||||
} catch (e) {
|
||||
// If no bundled asset, fall back to option 2
|
||||
Logger().i('No bundled selfie asset found, generating random file');
|
||||
}
|
||||
|
||||
// Option 2: Try to use a device camera image if available
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? cameraImage = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 50,
|
||||
);
|
||||
|
||||
if (cameraImage != null) {
|
||||
return File(cameraImage.path);
|
||||
}
|
||||
|
||||
// If both fail, return null and handle in calling method
|
||||
return null;
|
||||
} catch (e) {
|
||||
Logger().e('Error creating temporary image file: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate successful verification for development purposes
|
||||
Future<void> _simulateSuccessfulVerification() async {
|
||||
try {
|
||||
// Simulate face detection
|
||||
isVerifyingFace.value = true;
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
isVerifyingFace.value = false;
|
||||
|
||||
// Simulate ID card comparison
|
||||
isComparingWithIDCard.value = true;
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
isComparingWithIDCard.value = false;
|
||||
|
||||
// Set successful result
|
||||
final confidence =
|
||||
0.85 + (Random().nextDouble() * 0.14); // Between 85% and 99%
|
||||
matchConfidence.value = confidence;
|
||||
isMatchWithIDCard.value = true;
|
||||
|
||||
// Create a sample result
|
||||
faceComparisonResult.value = FaceComparisonResult(
|
||||
sourceFace: FaceModel(
|
||||
imagePath: selfieImage.value!.path,
|
||||
faceId: 'selfie_face_id', // Provide a suitable faceId here
|
||||
),
|
||||
targetFace: FaceModel(
|
||||
imagePath: idCardController?.idCardImage.value?.path ?? '',
|
||||
faceId: 'idcard_face_id', // Provide a suitable faceId here
|
||||
),
|
||||
isMatch: true,
|
||||
confidence: confidence,
|
||||
message: 'DEV MODE: Simulated successful match',
|
||||
);
|
||||
|
||||
Logger().i(
|
||||
'DEV MODE: Simulated successful verification with ${(confidence * 100).toStringAsFixed(1)}% confidence',
|
||||
);
|
||||
} catch (e) {
|
||||
Logger().e('DEV MODE: Error in simulated verification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
|||
mainController.formKey = formKey;
|
||||
|
||||
final isOfficer = mainController.selectedRole.value?.isOfficer ?? false;
|
||||
|
||||
|
||||
// Ensure controller's isOfficer flag matches mainController's selected role
|
||||
if (controller.isOfficer != isOfficer) {
|
||||
controller.updateIsOfficerFlag(isOfficer);
|
||||
|
@ -34,7 +34,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
|||
'Updated controller isOfficer flag to match selected role: $isOfficer',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
final String idCardType = isOfficer ? 'KTA' : 'KTP';
|
||||
|
||||
Logger().i(
|
||||
|
@ -287,22 +287,13 @@ class IdCardVerificationStep extends StatelessWidget {
|
|||
if (model.policeUnit.isNotEmpty)
|
||||
_buildInfoRow('Unit', model.policeUnit, context),
|
||||
|
||||
// Get extra data
|
||||
if (model.extraData != null) ...[
|
||||
if (model.extraData!['pangkat'] != null)
|
||||
_buildInfoRow('Rank', model.extraData!['pangkat'], context),
|
||||
if (model.extraData!['tanggal_lahir'] != null)
|
||||
_buildInfoRow(
|
||||
'Birth Date',
|
||||
model.extraData!['tanggal_lahir'],
|
||||
context,
|
||||
),
|
||||
],
|
||||
if ((model.position ?? '').isNotEmpty)
|
||||
_buildInfoRow('Position', model.position ?? '', context),
|
||||
|
||||
if (model.issueDate.isNotEmpty)
|
||||
_buildInfoRow('Issue Date', model.issueDate, context),
|
||||
if (model.cardNumber.isNotEmpty)
|
||||
_buildInfoRow('Card Number', model.cardNumber, context),
|
||||
_buildInfoRow('CNumber', model.cardNumber, context),
|
||||
|
||||
if (!isValid) _buildDataWarning(),
|
||||
],
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/facial_verification_controller.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
|
||||
|
@ -32,6 +33,18 @@ class SelfieVerificationStep extends StatelessWidget {
|
|||
final mainController = Get.find<FormRegistrationController>();
|
||||
final facialVerificationService = FacialVerificationService.instance;
|
||||
|
||||
// Check if we need to update the skip verification flag from arguments
|
||||
final dynamic args = Get.arguments;
|
||||
if (args != null &&
|
||||
args is Map<String, dynamic> &&
|
||||
args.containsKey('skipVerification')) {
|
||||
final bool skipVerification = args['skipVerification'] as bool;
|
||||
facialVerificationService.skipFaceVerification = skipVerification;
|
||||
Logger().i(
|
||||
'Setting skipFaceVerification from arguments: $skipVerification',
|
||||
);
|
||||
}
|
||||
|
||||
mainController.formKey = formKey;
|
||||
|
||||
return Form(
|
||||
|
@ -60,6 +73,7 @@ class SelfieVerificationStep extends StatelessWidget {
|
|||
if (!service.skipFaceVerification) return const SizedBox.shrink();
|
||||
|
||||
BuildContext context = Get.context!;
|
||||
final controller = Get.find<SelfieVerificationController>();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems),
|
||||
|
@ -69,16 +83,112 @@ class SelfieVerificationStep extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
|
||||
border: Border.all(color: Colors.amber),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.code, color: Colors.amber, size: TSizes.iconSm),
|
||||
const SizedBox(width: TSizes.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Development mode: Face verification is skipped',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: Colors.amber),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.code, color: Colors.amber, size: TSizes.iconSm),
|
||||
const SizedBox(width: TSizes.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Development Mode',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Colors.amber,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: TSizes.xs),
|
||||
|
||||
// Bypass liveness check toggle
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Bypass liveness check',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: Colors.amber),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Switch(
|
||||
value: controller.bypassLivenessCheck.value,
|
||||
onChanged: (value) {
|
||||
controller.bypassLivenessCheck.value = value;
|
||||
if (value) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Liveness check will be bypassed'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
activeColor: Colors.amber,
|
||||
activeTrackColor: Colors.amber.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Auto-verify toggle (new)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Auto-verify (auto-pass all checks)',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: Colors.amber),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Switch(
|
||||
value: controller.autoVerifyForDev.value,
|
||||
onChanged: (value) {
|
||||
controller.autoVerifyForDev.value = value;
|
||||
if (value) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'All verification steps will be auto-passed',
|
||||
),
|
||||
backgroundColor: Colors.deepOrange,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
// If auto-verify is enabled, also enable bypass
|
||||
if (!controller.bypassLivenessCheck.value) {
|
||||
controller.bypassLivenessCheck.value = true;
|
||||
}
|
||||
|
||||
// Auto complete verification if already on this step
|
||||
if (controller.selfieImage.value == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.autoCompleteVerification();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
activeColor: Colors.deepOrange,
|
||||
activeTrackColor: Colors.deepOrange.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: TSizes.xs),
|
||||
Text(
|
||||
'Warning: Only use in development environment!',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.red.shade300,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -356,9 +466,57 @@ class SelfieVerificationStep extends StatelessWidget {
|
|||
BuildContext context,
|
||||
) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
final facialVerificationService = FacialVerificationService.instance;
|
||||
|
||||
// Auto-verify for development if enabled
|
||||
if (facialVerificationService.skipFaceVerification &&
|
||||
controller.autoVerifyForDev.value &&
|
||||
!controller.hasConfirmedSelfie.value) {
|
||||
// Auto-complete verification after the UI is built
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.autoCompleteVerification();
|
||||
});
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Add auto-verify badge when enabled
|
||||
if (facialVerificationService.skipFaceVerification &&
|
||||
controller.autoVerifyForDev.value)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: TSizes.sm),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: TSizes.xs,
|
||||
horizontal: TSizes.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepOrange.withOpacity(0.1),
|
||||
border: Border.all(color: Colors.deepOrange.withOpacity(0.5)),
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.developer_mode,
|
||||
size: TSizes.iconSm,
|
||||
color: Colors.deepOrange,
|
||||
),
|
||||
const SizedBox(width: TSizes.xs),
|
||||
Text(
|
||||
'Auto-Verification Enabled',
|
||||
style: TextStyle(
|
||||
color: Colors.deepOrange,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Image preview container
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
|
@ -396,25 +554,95 @@ class SelfieVerificationStep extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Clear any previous images or states before starting new verification
|
||||
controller.clearSelfieImage();
|
||||
controller.resetVerificationState();
|
||||
// Start liveness detection process
|
||||
controller.performLivenessDetection();
|
||||
},
|
||||
icon: const Icon(Icons.security),
|
||||
label: const Text('Start Face Verification'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
|
||||
// Don't show the normal button if auto-verify is enabled
|
||||
if (!(facialVerificationService.skipFaceVerification &&
|
||||
controller.autoVerifyForDev.value))
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Clear any previous images or states before starting new verification
|
||||
controller.clearSelfieImage();
|
||||
controller.resetVerificationState();
|
||||
|
||||
// Use bypass if enabled
|
||||
if (facialVerificationService.skipFaceVerification &&
|
||||
controller.bypassLivenessCheck.value) {
|
||||
controller.bypassLivenessCheckWithRandomImage();
|
||||
} else {
|
||||
// Start regular liveness detection process
|
||||
controller.performLivenessDetection();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.security),
|
||||
label: const Text('Start Face Verification'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Show bypass button if development mode is enabled
|
||||
if (facialVerificationService.skipFaceVerification)
|
||||
Obx(() {
|
||||
if (controller.bypassLivenessCheck.value) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: TSizes.sm),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller.clearSelfieImage();
|
||||
controller.resetVerificationState();
|
||||
controller.bypassLivenessCheckWithRandomImage();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.developer_mode,
|
||||
color: Colors.amber,
|
||||
size: TSizes.iconSm,
|
||||
),
|
||||
label: Text(
|
||||
'DEV: Use Random Selfie & Skip Verification',
|
||||
style: TextStyle(color: Colors.amber),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: TSizes.md,
|
||||
vertical: TSizes.xs,
|
||||
),
|
||||
backgroundColor: Colors.amber.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
TSizes.borderRadiusSm,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
|
||||
// If auto-verify is on, show a different button
|
||||
if (facialVerificationService.skipFaceVerification &&
|
||||
controller.autoVerifyForDev.value)
|
||||
ElevatedButton.icon(
|
||||
onPressed: controller.autoCompleteVerification,
|
||||
icon: const Icon(Icons.skip_next),
|
||||
label: const Text('DEV: Skip & Auto-Complete Verification'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.deepOrange,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue