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:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
@ -15,7 +16,7 @@ class AzureOCRService {
|
||||||
final String ocrApiPath = Endpoints.ocrApiPath;
|
final String ocrApiPath = Endpoints.ocrApiPath;
|
||||||
final String faceApiPath = Endpoints.faceApiPath;
|
final String faceApiPath = Endpoints.faceApiPath;
|
||||||
final String faceVerifyPath = Endpoints.faceVerifyPath;
|
final String faceVerifyPath = Endpoints.faceVerifyPath;
|
||||||
|
|
||||||
bool isValidKtp = false;
|
bool isValidKtp = false;
|
||||||
bool isValidKta = false;
|
bool isValidKta = false;
|
||||||
|
|
||||||
|
@ -834,6 +835,9 @@ class AzureOCRService {
|
||||||
// Extract officer name
|
// Extract officer name
|
||||||
_extractNameFromKta(extractedInfo, allLines, fullText);
|
_extractNameFromKta(extractedInfo, allLines, fullText);
|
||||||
|
|
||||||
|
// Extract Position (jabatan)
|
||||||
|
_extractPositionFromKta(extractedInfo, allLines, fullText);
|
||||||
|
|
||||||
// Extract rank (pangkat)
|
// Extract rank (pangkat)
|
||||||
_extractRankFromKta(extractedInfo, allLines, fullText);
|
_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
|
// Extract rank from KTA
|
||||||
void _extractRankFromKta(
|
void _extractRankFromKta(
|
||||||
Map<String, String> extractedInfo,
|
Map<String, String> extractedInfo,
|
||||||
|
@ -1263,6 +1389,8 @@ class AzureOCRService {
|
||||||
|
|
||||||
// Helper to create KtaModel from extracted info
|
// Helper to create KtaModel from extracted info
|
||||||
KtaModel createKtaModel(Map<String, String> extractedInfo) {
|
KtaModel createKtaModel(Map<String, String> extractedInfo) {
|
||||||
|
print('Creating KTA model from extracted info: $extractedInfo');
|
||||||
|
|
||||||
return KtaModel(
|
return KtaModel(
|
||||||
name: extractedInfo['nama'] ?? '',
|
name: extractedInfo['nama'] ?? '',
|
||||||
nrp: extractedInfo['nrp'] ?? '',
|
nrp: extractedInfo['nrp'] ?? '',
|
||||||
|
@ -1271,6 +1399,8 @@ class AzureOCRService {
|
||||||
extractedInfo['issue_date'] ?? extractedInfo['tanggal_terbit'] ?? '',
|
extractedInfo['issue_date'] ?? extractedInfo['tanggal_terbit'] ?? '',
|
||||||
cardNumber:
|
cardNumber:
|
||||||
extractedInfo['card_number'] ?? extractedInfo['nomor_kartu'] ?? '',
|
extractedInfo['card_number'] ?? extractedInfo['nomor_kartu'] ?? '',
|
||||||
|
rank: extractedInfo['pangkat'] ?? '',
|
||||||
|
position: extractedInfo['jabatan'] ?? '',
|
||||||
extraData: {
|
extraData: {
|
||||||
'pangkat': extractedInfo['pangkat'] ?? '',
|
'pangkat': extractedInfo['pangkat'] ?? '',
|
||||||
'tanggal_lahir': extractedInfo['tanggal_lahir'] ?? '',
|
'tanggal_lahir': extractedInfo['tanggal_lahir'] ?? '',
|
||||||
|
|
|
@ -14,6 +14,12 @@ class KtaModel extends Equatable {
|
||||||
/// Issue date of the ID card in format dd-mm-yyyy
|
/// Issue date of the ID card in format dd-mm-yyyy
|
||||||
final String issueDate;
|
final String issueDate;
|
||||||
|
|
||||||
|
/// Officer rank (e.g., IPDA, IPTU, AKP, etc.)
|
||||||
|
final String? rank;
|
||||||
|
|
||||||
|
/// Officer position/role
|
||||||
|
final String? position;
|
||||||
|
|
||||||
/// Card unique identification number
|
/// Card unique identification number
|
||||||
final String cardNumber;
|
final String cardNumber;
|
||||||
|
|
||||||
|
@ -29,6 +35,8 @@ class KtaModel extends Equatable {
|
||||||
required this.policeUnit,
|
required this.policeUnit,
|
||||||
required this.issueDate,
|
required this.issueDate,
|
||||||
required this.cardNumber,
|
required this.cardNumber,
|
||||||
|
this.rank = '',
|
||||||
|
this.position = '',
|
||||||
this.photoUrl,
|
this.photoUrl,
|
||||||
this.extraData,
|
this.extraData,
|
||||||
});
|
});
|
||||||
|
@ -41,6 +49,8 @@ class KtaModel extends Equatable {
|
||||||
policeUnit: json['police_unit'] ?? '',
|
policeUnit: json['police_unit'] ?? '',
|
||||||
issueDate: json['issue_date'] ?? '',
|
issueDate: json['issue_date'] ?? '',
|
||||||
cardNumber: json['card_number'] ?? '',
|
cardNumber: json['card_number'] ?? '',
|
||||||
|
rank: json['rank'] ?? '',
|
||||||
|
position: json['position'] ?? '',
|
||||||
photoUrl: json['photo_url'],
|
photoUrl: json['photo_url'],
|
||||||
extraData: json['extra_data'] as Map<String, dynamic>?,
|
extraData: json['extra_data'] as Map<String, dynamic>?,
|
||||||
);
|
);
|
||||||
|
@ -54,6 +64,8 @@ class KtaModel extends Equatable {
|
||||||
'police_unit': policeUnit,
|
'police_unit': policeUnit,
|
||||||
'issue_date': issueDate,
|
'issue_date': issueDate,
|
||||||
'card_number': cardNumber,
|
'card_number': cardNumber,
|
||||||
|
'rank': rank,
|
||||||
|
'position': position,
|
||||||
'photo_url': photoUrl,
|
'photo_url': photoUrl,
|
||||||
'extra_data': extraData,
|
'extra_data': extraData,
|
||||||
};
|
};
|
||||||
|
@ -67,6 +79,8 @@ class KtaModel extends Equatable {
|
||||||
policeUnit: '',
|
policeUnit: '',
|
||||||
issueDate: '',
|
issueDate: '',
|
||||||
cardNumber: '',
|
cardNumber: '',
|
||||||
|
rank: '',
|
||||||
|
position: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +106,8 @@ class KtaModel extends Equatable {
|
||||||
String? policeUnit,
|
String? policeUnit,
|
||||||
String? issueDate,
|
String? issueDate,
|
||||||
String? cardNumber,
|
String? cardNumber,
|
||||||
|
String? rank,
|
||||||
|
String? position,
|
||||||
String? photoUrl,
|
String? photoUrl,
|
||||||
Map<String, dynamic>? extraData,
|
Map<String, dynamic>? extraData,
|
||||||
}) {
|
}) {
|
||||||
|
@ -101,6 +117,8 @@ class KtaModel extends Equatable {
|
||||||
policeUnit: policeUnit ?? this.policeUnit,
|
policeUnit: policeUnit ?? this.policeUnit,
|
||||||
issueDate: issueDate ?? this.issueDate,
|
issueDate: issueDate ?? this.issueDate,
|
||||||
cardNumber: cardNumber ?? this.cardNumber,
|
cardNumber: cardNumber ?? this.cardNumber,
|
||||||
|
rank: rank ?? this.rank,
|
||||||
|
position: position ?? this.position,
|
||||||
photoUrl: photoUrl ?? this.photoUrl,
|
photoUrl: photoUrl ?? this.photoUrl,
|
||||||
extraData: extraData ?? this.extraData,
|
extraData: extraData ?? this.extraData,
|
||||||
);
|
);
|
||||||
|
@ -135,12 +153,14 @@ class KtaModel extends Equatable {
|
||||||
policeUnit,
|
policeUnit,
|
||||||
issueDate,
|
issueDate,
|
||||||
cardNumber,
|
cardNumber,
|
||||||
|
rank,
|
||||||
|
position,
|
||||||
photoUrl,
|
photoUrl,
|
||||||
extraData,
|
extraData,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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,
|
permanent: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isOfficer) {
|
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
||||||
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
||||||
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _assignControllerReferences(bool isOfficer) {
|
void _assignControllerReferences(bool isOfficer) {
|
||||||
|
@ -931,7 +929,6 @@ class FormRegistrationController extends GetxController {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Update user metadata with common profile info
|
// Update user metadata with common profile info
|
||||||
final metadata =
|
final metadata =
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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:image_picker/image_picker.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/cores/services/edge_function_service.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/data/models/face_model.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';
|
||||||
|
@ -20,7 +21,15 @@ class FacialVerificationService {
|
||||||
final EdgeFunctionService _edgeFunctionService = EdgeFunctionService.instance;
|
final EdgeFunctionService _edgeFunctionService = EdgeFunctionService.instance;
|
||||||
|
|
||||||
// Flag for skipping verification in development mode
|
// 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
|
/// Get all faces in an image using edge function
|
||||||
Future<List<FaceModel>> detectFaces(XFile image) async {
|
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
|
// Helper methods for development mode
|
||||||
FaceModel _createDummyFaceModel(String imagePath, {bool isLive = false}) {
|
FaceModel _createDummyFaceModel(String imagePath, {bool isLive = false}) {
|
||||||
return FaceModel(
|
return FaceModel(
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import 'dart:developer' as dev;
|
import 'dart:developer' as dev;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.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/cores/services/edge_function_service.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/face_model.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/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/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';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
|
|
||||||
class SelfieVerificationController extends GetxController {
|
class SelfieVerificationController extends GetxController {
|
||||||
|
@ -30,7 +35,7 @@ class SelfieVerificationController extends GetxController {
|
||||||
// Liveness detection states
|
// Liveness detection states
|
||||||
final isPerformingLivenessCheck = false.obs;
|
final isPerformingLivenessCheck = false.obs;
|
||||||
final isLivenessCheckPassed = false.obs;
|
final isLivenessCheckPassed = false.obs;
|
||||||
|
|
||||||
// New flag for auto starting verification
|
// New flag for auto starting verification
|
||||||
bool autoStartVerification = false;
|
bool autoStartVerification = false;
|
||||||
|
|
||||||
|
@ -41,6 +46,10 @@ class SelfieVerificationController extends GetxController {
|
||||||
final Rx<FaceComparisonResult?> faceComparisonResult =
|
final Rx<FaceComparisonResult?> faceComparisonResult =
|
||||||
Rx<FaceComparisonResult?>(null);
|
Rx<FaceComparisonResult?>(null);
|
||||||
|
|
||||||
|
// Development mode options
|
||||||
|
final RxBool bypassLivenessCheck = RxBool(false);
|
||||||
|
final RxBool autoVerifyForDev = RxBool(false);
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
SelfieVerificationController({this.idCardController});
|
SelfieVerificationController({this.idCardController});
|
||||||
|
|
||||||
|
@ -48,6 +57,38 @@ class SelfieVerificationController extends GetxController {
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.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
|
// Don't access other controllers during initialization
|
||||||
// Defer any cross-controller communication until after initialization
|
// Defer any cross-controller communication until after initialization
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
|
@ -122,12 +163,11 @@ class SelfieVerificationController extends GetxController {
|
||||||
preventDuplicates: false, // Allow navigation to same route if needed
|
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
|
// and automatic verification should be triggered by the listener in onInit
|
||||||
|
|
||||||
// Reset liveness check flag since we're back
|
// Reset liveness check flag since we're back
|
||||||
isPerformingLivenessCheck.value = false;
|
isPerformingLivenessCheck.value = 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;
|
||||||
|
@ -158,7 +198,7 @@ class SelfieVerificationController extends GetxController {
|
||||||
|
|
||||||
isVerifyingFace.value = true;
|
isVerifyingFace.value = true;
|
||||||
selfieError.value = '';
|
selfieError.value = '';
|
||||||
|
|
||||||
dev.log(
|
dev.log(
|
||||||
'Processing captured image for verification: $imagePath',
|
'Processing captured image for verification: $imagePath',
|
||||||
name: 'SELFIE_VERIFICATION',
|
name: 'SELFIE_VERIFICATION',
|
||||||
|
@ -338,4 +378,199 @@ class SelfieVerificationController extends GetxController {
|
||||||
autoStartVerification = false; // Reset auto verification flag
|
autoStartVerification = false; // Reset auto verification flag
|
||||||
selfieError.value = '';
|
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;
|
mainController.formKey = formKey;
|
||||||
|
|
||||||
final isOfficer = mainController.selectedRole.value?.isOfficer ?? false;
|
final isOfficer = mainController.selectedRole.value?.isOfficer ?? false;
|
||||||
|
|
||||||
// Ensure controller's isOfficer flag matches mainController's selected role
|
// Ensure controller's isOfficer flag matches mainController's selected role
|
||||||
if (controller.isOfficer != isOfficer) {
|
if (controller.isOfficer != isOfficer) {
|
||||||
controller.updateIsOfficerFlag(isOfficer);
|
controller.updateIsOfficerFlag(isOfficer);
|
||||||
|
@ -34,7 +34,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
'Updated controller isOfficer flag to match selected role: $isOfficer',
|
'Updated controller isOfficer flag to match selected role: $isOfficer',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String idCardType = isOfficer ? 'KTA' : 'KTP';
|
final String idCardType = isOfficer ? 'KTA' : 'KTP';
|
||||||
|
|
||||||
Logger().i(
|
Logger().i(
|
||||||
|
@ -287,22 +287,13 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
if (model.policeUnit.isNotEmpty)
|
if (model.policeUnit.isNotEmpty)
|
||||||
_buildInfoRow('Unit', model.policeUnit, context),
|
_buildInfoRow('Unit', model.policeUnit, context),
|
||||||
|
|
||||||
// Get extra data
|
if ((model.position ?? '').isNotEmpty)
|
||||||
if (model.extraData != null) ...[
|
_buildInfoRow('Position', model.position ?? '', context),
|
||||||
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.issueDate.isNotEmpty)
|
if (model.issueDate.isNotEmpty)
|
||||||
_buildInfoRow('Issue Date', model.issueDate, context),
|
_buildInfoRow('Issue Date', model.issueDate, context),
|
||||||
if (model.cardNumber.isNotEmpty)
|
if (model.cardNumber.isNotEmpty)
|
||||||
_buildInfoRow('Card Number', model.cardNumber, context),
|
_buildInfoRow('CNumber', model.cardNumber, context),
|
||||||
|
|
||||||
if (!isValid) _buildDataWarning(),
|
if (!isValid) _buildDataWarning(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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/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/facial_verification_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';
|
||||||
|
@ -32,6 +33,18 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
final mainController = Get.find<FormRegistrationController>();
|
final mainController = Get.find<FormRegistrationController>();
|
||||||
final facialVerificationService = FacialVerificationService.instance;
|
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;
|
mainController.formKey = formKey;
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
|
@ -60,6 +73,7 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
if (!service.skipFaceVerification) return const SizedBox.shrink();
|
if (!service.skipFaceVerification) return const SizedBox.shrink();
|
||||||
|
|
||||||
BuildContext context = Get.context!;
|
BuildContext context = Get.context!;
|
||||||
|
final controller = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems),
|
margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems),
|
||||||
|
@ -69,16 +83,112 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
|
||||||
border: Border.all(color: Colors.amber),
|
border: Border.all(color: Colors.amber),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.code, color: Colors.amber, size: TSizes.iconSm),
|
Row(
|
||||||
const SizedBox(width: TSizes.xs),
|
children: [
|
||||||
Expanded(
|
const Icon(Icons.code, color: Colors.amber, size: TSizes.iconSm),
|
||||||
child: Text(
|
const SizedBox(width: TSizes.xs),
|
||||||
'Development mode: Face verification is skipped',
|
Expanded(
|
||||||
style: Theme.of(
|
child: Text(
|
||||||
context,
|
'Development Mode',
|
||||||
).textTheme.labelSmall?.copyWith(color: Colors.amber),
|
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,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
final isDark = THelperFunctions.isDarkMode(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(
|
return Column(
|
||||||
children: [
|
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(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 200,
|
height: 200,
|
||||||
|
@ -396,25 +554,95 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
// Don't show the normal button if auto-verify is enabled
|
||||||
// Clear any previous images or states before starting new verification
|
if (!(facialVerificationService.skipFaceVerification &&
|
||||||
controller.clearSelfieImage();
|
controller.autoVerifyForDev.value))
|
||||||
controller.resetVerificationState();
|
ElevatedButton.icon(
|
||||||
// Start liveness detection process
|
onPressed: () {
|
||||||
controller.performLivenessDetection();
|
// Clear any previous images or states before starting new verification
|
||||||
},
|
controller.clearSelfieImage();
|
||||||
icon: const Icon(Icons.security),
|
controller.resetVerificationState();
|
||||||
label: const Text('Start Face Verification'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
// Use bypass if enabled
|
||||||
backgroundColor: TColors.primary,
|
if (facialVerificationService.skipFaceVerification &&
|
||||||
foregroundColor: Colors.white,
|
controller.bypassLivenessCheck.value) {
|
||||||
minimumSize: const Size(double.infinity, 45),
|
controller.bypassLivenessCheckWithRandomImage();
|
||||||
shape: RoundedRectangleBorder(
|
} else {
|
||||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
// 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