@trustchex/react-native-sdk 1.362.6 → 1.381.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TrustchexSDK.podspec +3 -3
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +64 -19
- package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +636 -301
- package/ios/Camera/TrustchexCameraView.swift +166 -119
- package/ios/OpenCV/OpenCVHelper.h +0 -7
- package/ios/OpenCV/OpenCVHelper.mm +0 -60
- package/ios/OpenCV/OpenCVModule.h +0 -4
- package/ios/OpenCV/OpenCVModule.mm +440 -358
- package/lib/module/Shared/Components/DebugOverlay.js +541 -0
- package/lib/module/Shared/Components/FaceCamera.js +1 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +44 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +270 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +708 -1593
- package/lib/module/Shared/Components/IdentityDocumentCamera.types.js +3 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +273 -0
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +1 -8
- package/lib/module/Shared/Libs/mrz.utils.js +202 -9
- package/lib/module/Translation/Resources/en.js +0 -4
- package/lib/module/Translation/Resources/tr.js +0 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts +30 -0
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +35 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -56
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +88 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +116 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +93 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +1 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +0 -4
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +0 -4
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Shared/Components/DebugOverlay.tsx +656 -0
- package/src/Shared/Components/FaceCamera.tsx +1 -0
- package/src/Shared/Components/IdentityDocumentCamera.constants.ts +44 -0
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +342 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +1105 -2324
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +136 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +364 -0
- package/src/Shared/Components/QrCodeScannerCamera.tsx +1 -9
- package/src/Shared/Components/TrustchexCamera.tsx +1 -0
- package/src/Shared/Libs/mrz.utils.ts +238 -26
- package/src/Translation/Resources/en.ts +0 -4
- package/src/Translation/Resources/tr.ts +0 -4
- package/src/version.ts +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/* eslint-disable react-native/no-inline-styles */
|
|
4
4
|
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
5
|
-
import { View, StyleSheet, Text as TextView, Platform, StatusBar, Vibration, Linking,
|
|
5
|
+
import { View, StyleSheet, Text as TextView, Platform, StatusBar, Vibration, Linking, ActivityIndicator, PermissionsAndroid, Animated } from 'react-native';
|
|
6
6
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
7
|
import { TrustchexCamera } from "./TrustchexCamera.js";
|
|
8
8
|
import { NativeModules } from 'react-native';
|
|
@@ -17,16 +17,17 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
17
17
|
import { speak, resetLastMessage } from "../Libs/tts.utils.js";
|
|
18
18
|
import AppContext from "../Contexts/AppContext.js";
|
|
19
19
|
import { useTheme } from "../Contexts/ThemeContext.js";
|
|
20
|
+
import DebugOverlay, { TestModePanel } from "./DebugOverlay.js";
|
|
21
|
+
import { getStatusMessage, getFrameToScreenTransform, transformBoundsToScreen, getScanAreaBounds, angleBetweenPoints, detectDocumentType, determineDocumentTypeToSet, areMRZFieldsEqual, hasRequiredMRZFields, validateFacePosition } from "./IdentityDocumentCamera.utils.js";
|
|
22
|
+
import { handlePassportFlow, handleIDFrontFlow, handleIDBackFlow, getNextStepAfterHologram } from "./IdentityDocumentCamera.flows.js";
|
|
23
|
+
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_MRZ_READS, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, PASSPORT_MRZ_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
|
|
24
|
+
|
|
25
|
+
// Re-export types for backward compatibility
|
|
20
26
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
21
27
|
const {
|
|
22
28
|
OpenCVModule
|
|
23
29
|
} = NativeModules;
|
|
24
|
-
const
|
|
25
|
-
const HOLOGRAM_DETECTION_THRESHOLD = 1000; // Lowered for better sensitivity while still filtering noise
|
|
26
|
-
const HOLOGRAM_DETECTION_RETRY_COUNT = 3; // More attempts but faster each (~1s per attempt)
|
|
27
|
-
const SECOND_FACE_DETECTION_RETRY_COUNT = 10;
|
|
28
|
-
const MIN_BRIGHTNESS_THRESHOLD = 45;
|
|
29
|
-
const MAX_CONSECUTIVE_QUALITY_FAILURES = 30;
|
|
30
|
+
const AnimatedText = Animated.createAnimatedComponent(TextView);
|
|
30
31
|
const IdentityDocumentCamera = ({
|
|
31
32
|
onlyMRZScan,
|
|
32
33
|
onIdentityDocumentScanned,
|
|
@@ -47,7 +48,6 @@ const IdentityDocumentCamera = ({
|
|
|
47
48
|
isTorchOnRef.current = val;
|
|
48
49
|
_setIsTorchOn(val);
|
|
49
50
|
}, []);
|
|
50
|
-
const [_exposure, _setExposure] = useState(0);
|
|
51
51
|
const isCameraInitialized = useRef(false);
|
|
52
52
|
const [currentFaceImage, setCurrentFaceImage] = useState(undefined);
|
|
53
53
|
const [_currentHologramMaskImage, setCurrentHologramMaskImage] = useState(undefined);
|
|
@@ -58,7 +58,7 @@ const IdentityDocumentCamera = ({
|
|
|
58
58
|
const [hasGuideShown, setHasGuideShown] = useState(false);
|
|
59
59
|
const [status, setStatus] = useState('SEARCHING');
|
|
60
60
|
const [nextStep, setNextStep] = useState('SCAN_ID_FRONT_OR_PASSPORT');
|
|
61
|
-
const [
|
|
61
|
+
const [_completedStep, setCompletedStep] = useState(null);
|
|
62
62
|
const [detectedDocumentType, setDetectedDocumentType] = useState('UNKNOWN');
|
|
63
63
|
const hologramDetectionCurrentRetryCount = useRef(0);
|
|
64
64
|
const secondaryFaceDetectionCurrentRetryCount = useRef(0);
|
|
@@ -69,12 +69,10 @@ const IdentityDocumentCamera = ({
|
|
|
69
69
|
const lastValidMRZText = useRef(null);
|
|
70
70
|
const lastValidMRZFields = useRef(null);
|
|
71
71
|
const validMRZConsecutiveCount = useRef(0);
|
|
72
|
-
const REQUIRED_CONSISTENT_MRZ_READS = 2; // Require 2 consistent valid reads
|
|
73
72
|
|
|
74
73
|
// Document type stability tracking - require consistent detections from good quality frames
|
|
75
74
|
const lastDetectedDocType = useRef('UNKNOWN');
|
|
76
75
|
const consistentDocTypeCount = useRef(0);
|
|
77
|
-
const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3; // Require 3 consistent detections from quality frames
|
|
78
76
|
|
|
79
77
|
// Frame quality tracking - persist across callbacks
|
|
80
78
|
const lastFrameQuality = useRef({
|
|
@@ -86,70 +84,16 @@ const IdentityDocumentCamera = ({
|
|
|
86
84
|
// Barcode caching - persist detected barcode across frames for reliability
|
|
87
85
|
const cachedBarcode = useRef(null);
|
|
88
86
|
|
|
87
|
+
// Error message flash animation
|
|
88
|
+
const errorFlashAnim = useRef(new Animated.Value(1)).current;
|
|
89
|
+
|
|
89
90
|
// Test mode tracking
|
|
90
91
|
const [testModeData, setTestModeData] = useState(null);
|
|
91
|
-
|
|
92
|
-
// Helper to compare MRZ field values (ignore raw text variations)
|
|
93
|
-
const areMRZFieldsEqual = useCallback((fields1, fields2) => {
|
|
94
|
-
if (!fields1 || !fields2) return false;
|
|
95
|
-
// Compare critical fields that define document identity
|
|
96
|
-
return fields1.documentNumber === fields2.documentNumber && fields1.birthDate === fields2.birthDate && fields1.expirationDate === fields2.expirationDate && fields1.firstName === fields2.firstName && fields1.lastName === fields2.lastName && fields1.issuingState === fields2.issuingState;
|
|
97
|
-
}, []);
|
|
98
|
-
|
|
99
|
-
// Helper functions to reduce duplication
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Check if all required MRZ fields are present
|
|
103
|
-
*/
|
|
104
|
-
const hasRequiredMRZFields = useCallback(fields => !!fields?.firstName && !!fields?.lastName && !!fields?.documentNumber && !!fields?.birthDate, []);
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Log detailed MRZ information for debugging and verification
|
|
108
|
-
*/
|
|
109
|
-
const logMRZDetails = useCallback((stepName, fields, mrzText, consecutiveReads, isDebugMode) => {
|
|
110
|
-
if (isDebugMode) {
|
|
111
|
-
debugLog('IdentityDocumentCamera', `[${stepName}] MRZ validated successfully (consistent reads: ${consecutiveReads})`);
|
|
112
|
-
debugLog('IdentityDocumentCamera', `[${stepName}] Final MRZ Fields:`, {
|
|
113
|
-
documentNumber: fields?.documentNumber,
|
|
114
|
-
name: `${fields?.lastName} ${fields?.firstName}`,
|
|
115
|
-
birthDate: fields?.birthDate,
|
|
116
|
-
expirationDate: fields?.expirationDate,
|
|
117
|
-
nationality: fields?.nationality || fields?.issuingState,
|
|
118
|
-
sex: fields?.sex,
|
|
119
|
-
personalId: fields?.optional1
|
|
120
|
-
});
|
|
121
|
-
if (mrzText) {
|
|
122
|
-
const mrzLines = mrzText.split('\n').map(l => l.replace(/\s/g, '')).filter(l => l.length >= 20 && /^[A-Z0-9<]+$/.test(l));
|
|
123
|
-
debugLog('IdentityDocumentCamera', `[${stepName}] MRZ lines (${mrzLines.length}):`);
|
|
124
|
-
mrzLines.forEach((line, idx) => {
|
|
125
|
-
debugLog('IdentityDocumentCamera', ` ${idx + 1}: ${line}`);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}, []);
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Log MRZ validation failure details for debugging
|
|
133
|
-
*/
|
|
134
|
-
const logMRZValidationFailure = useCallback((stepName, hasRequiredFields, parsedData, retryCount, isDebugMode) => {
|
|
135
|
-
if (isDebugMode) {
|
|
136
|
-
const debugInfo = {
|
|
137
|
-
hasRequiredFields,
|
|
138
|
-
isValid: parsedData?.valid,
|
|
139
|
-
retryCount
|
|
140
|
-
};
|
|
141
|
-
if (parsedData?.valid) {
|
|
142
|
-
debugInfo.consistentReads = validMRZConsecutiveCount.current;
|
|
143
|
-
debugInfo.requiredReads = REQUIRED_CONSISTENT_MRZ_READS;
|
|
144
|
-
debugInfo.fieldsMatch = areMRZFieldsEqual(lastValidMRZFields.current, parsedData?.fields);
|
|
145
|
-
}
|
|
146
|
-
debugLog('IdentityDocumentCamera', `[${stepName}] MRZ detected but validation failed - retrying`, debugInfo);
|
|
147
|
-
}
|
|
148
|
-
}, [areMRZFieldsEqual]);
|
|
149
92
|
const lastHologramCaptureTime = useRef(0);
|
|
150
|
-
const HOLOGRAM_CAPTURE_INTERVAL = 250; // ms between captures - allows flash to fully stabilize for accurate diff
|
|
151
93
|
const hologramFramesWithoutFace = useRef(0); // Track consecutive frames without face during hologram collection
|
|
152
|
-
const
|
|
94
|
+
const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
|
|
95
|
+
const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
|
|
96
|
+
const isStepTransitionInProgress = useRef(false); // Prevent duplicate step transitions from rapid frames
|
|
153
97
|
|
|
154
98
|
const faceDetectionErrorCount = useRef(0);
|
|
155
99
|
const brightnessHistory = useRef([]);
|
|
@@ -189,6 +133,8 @@ const IdentityDocumentCamera = ({
|
|
|
189
133
|
useEffect(() => {
|
|
190
134
|
if (isFocused && hasPermission && hasGuideShown) {
|
|
191
135
|
setIsActive(true);
|
|
136
|
+
isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
|
|
137
|
+
isStepTransitionInProgress.current = false;
|
|
192
138
|
} else {
|
|
193
139
|
setIsActive(false);
|
|
194
140
|
faceImages.current = [];
|
|
@@ -201,11 +147,14 @@ const IdentityDocumentCamera = ({
|
|
|
201
147
|
lastValidMRZText.current = null;
|
|
202
148
|
lastValidMRZFields.current = null;
|
|
203
149
|
validMRZConsecutiveCount.current = 0;
|
|
204
|
-
|
|
205
|
-
lastValidMRZFields.current = null;
|
|
206
|
-
validMRZConsecutiveCount.current = 0;
|
|
207
|
-
cachedBarcode.current = null; // Clear cached barcode on new scan
|
|
150
|
+
cachedBarcode.current = null;
|
|
208
151
|
lastVoiceGuidanceMessage.current = '';
|
|
152
|
+
isCompletionCallbackInvoked.current = false;
|
|
153
|
+
isStepTransitionInProgress.current = false;
|
|
154
|
+
// Clear all captured image states from previous scan
|
|
155
|
+
setCurrentFaceImage(undefined);
|
|
156
|
+
setCurrentHologramImage(undefined);
|
|
157
|
+
setCurrentSecondaryFaceImage(undefined);
|
|
209
158
|
resetLastMessage();
|
|
210
159
|
}
|
|
211
160
|
return () => {
|
|
@@ -215,36 +164,23 @@ const IdentityDocumentCamera = ({
|
|
|
215
164
|
setHologramImageCount(0);
|
|
216
165
|
setLatestHologramFaceImage(undefined);
|
|
217
166
|
lastVoiceGuidanceMessage.current = '';
|
|
167
|
+
isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
|
|
168
|
+
isStepTransitionInProgress.current = false;
|
|
169
|
+
// Clear all captured image states on unmount
|
|
170
|
+
setCurrentFaceImage(undefined);
|
|
171
|
+
setCurrentHologramImage(undefined);
|
|
172
|
+
setCurrentSecondaryFaceImage(undefined);
|
|
218
173
|
resetLastMessage();
|
|
219
174
|
};
|
|
220
175
|
}, [isFocused, hasPermission, hasGuideShown]);
|
|
221
176
|
useEffect(() => {
|
|
222
|
-
if (hasGuideShown)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} else if (status === 'INCORRECT') {
|
|
228
|
-
message = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? t('identityDocumentCamera.wrongSideFront') : nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.wrongSideBack') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.wrongSideFront') : t('identityDocumentCamera.alignPhotoSide');
|
|
229
|
-
} else if (isBrightnessLow) {
|
|
230
|
-
message = t('identityDocumentCamera.lowBrightness');
|
|
231
|
-
} else if (isFrameBlurry) {
|
|
232
|
-
message = t('identityDocumentCamera.avoidBlur');
|
|
233
|
-
} else if (status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0) {
|
|
234
|
-
message = nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.idCardBackDetected') : detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportDetected') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.idCardFrontDetected') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument');
|
|
235
|
-
} else if (elementsOutsideScanArea.length > 0) {
|
|
236
|
-
message = t('identityDocumentCamera.centerDocument');
|
|
237
|
-
} else if ((status === 'SCANNING' || status === 'SEARCHING') && !allElementsDetected) {
|
|
238
|
-
message = nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.alignIDBack') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.alignPassport') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.alignIDFront') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument');
|
|
239
|
-
} else {
|
|
240
|
-
message = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : '';
|
|
241
|
-
}
|
|
242
|
-
if (appContext.currentWorkflowStep?.data?.voiceGuidanceActive && message && message !== lastVoiceGuidanceMessage.current) {
|
|
243
|
-
lastVoiceGuidanceMessage.current = message;
|
|
244
|
-
speak(message, true);
|
|
245
|
-
}
|
|
177
|
+
if (!hasGuideShown || !appContext.currentWorkflowStep?.data?.voiceGuidanceActive) return;
|
|
178
|
+
const message = getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t);
|
|
179
|
+
if (message && message !== lastVoiceGuidanceMessage.current) {
|
|
180
|
+
lastVoiceGuidanceMessage.current = message;
|
|
181
|
+
speak(message, true);
|
|
246
182
|
}
|
|
247
|
-
}, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, isFrameBlurry, nextStep, status,
|
|
183
|
+
}, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, isFrameBlurry, nextStep, status, detectedDocumentType, allElementsDetected, elementsOutsideScanArea, t]);
|
|
248
184
|
useEffect(() => {
|
|
249
185
|
if (status === 'INCORRECT') {
|
|
250
186
|
const timeout = setTimeout(() => {
|
|
@@ -263,6 +199,30 @@ const IdentityDocumentCamera = ({
|
|
|
263
199
|
}
|
|
264
200
|
}, [nextStep]);
|
|
265
201
|
|
|
202
|
+
// Reset completion callback flag when transitioning away from COMPLETED
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (nextStep !== 'COMPLETED') {
|
|
205
|
+
isCompletionCallbackInvoked.current = false;
|
|
206
|
+
}
|
|
207
|
+
isStepTransitionInProgress.current = false;
|
|
208
|
+
}, [nextStep]);
|
|
209
|
+
|
|
210
|
+
// Error flash animation - flash red text when wrong side detected
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (status === 'INCORRECT') {
|
|
213
|
+
errorFlashAnim.setValue(1);
|
|
214
|
+
Animated.loop(Animated.sequence([Animated.timing(errorFlashAnim, {
|
|
215
|
+
toValue: 0.3,
|
|
216
|
+
duration: 300,
|
|
217
|
+
useNativeDriver: false
|
|
218
|
+
}), Animated.timing(errorFlashAnim, {
|
|
219
|
+
toValue: 1,
|
|
220
|
+
duration: 300,
|
|
221
|
+
useNativeDriver: false
|
|
222
|
+
})])).start();
|
|
223
|
+
}
|
|
224
|
+
}, [status, errorFlashAnim]);
|
|
225
|
+
|
|
266
226
|
// Native OpenCV: detect hologram from sequence of face images
|
|
267
227
|
const detectHologramNative = useCallback(async images => {
|
|
268
228
|
try {
|
|
@@ -281,19 +241,21 @@ const IdentityDocumentCamera = ({
|
|
|
281
241
|
return [];
|
|
282
242
|
}, []);
|
|
283
243
|
|
|
284
|
-
// Native OpenCV: compare
|
|
285
|
-
const
|
|
286
|
-
) => {
|
|
244
|
+
// Native OpenCV: compare face visual similarity (device-side validation before backend FaceNet)
|
|
245
|
+
const compareFaceVisualSimilarityNative = async (faceImage1, faceImage2) => {
|
|
287
246
|
try {
|
|
288
|
-
if (!
|
|
289
|
-
return await OpenCVModule.
|
|
247
|
+
if (!faceImage1 || !faceImage2) return null;
|
|
248
|
+
return await OpenCVModule.compareFaceVisualSimilarity(faceImage1, faceImage2);
|
|
290
249
|
} catch (error) {
|
|
291
|
-
|
|
250
|
+
if (isDebugEnabled()) {
|
|
251
|
+
logError('[Face Visual Similarity] Comparison error:', error);
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
292
254
|
}
|
|
293
255
|
};
|
|
294
256
|
|
|
295
257
|
// Native OpenCV: crop face images from full frame
|
|
296
|
-
const getFaceImages = async (facesToDetect, image, width, height) => {
|
|
258
|
+
const getFaceImages = async (facesToDetect, image, width, height, widerRightPadding = false) => {
|
|
297
259
|
if (!facesToDetect.length || !image || width <= 0 || height <= 0) {
|
|
298
260
|
return [];
|
|
299
261
|
}
|
|
@@ -304,13 +266,46 @@ const IdentityDocumentCamera = ({
|
|
|
304
266
|
width: f.bounds.width,
|
|
305
267
|
height: f.bounds.height
|
|
306
268
|
}));
|
|
307
|
-
const croppedFaces = await OpenCVModule.cropFaceImages(image, faceBounds, width, height);
|
|
269
|
+
const croppedFaces = await OpenCVModule.cropFaceImages(image, faceBounds, width, height, widerRightPadding);
|
|
308
270
|
return croppedFaces ?? [];
|
|
309
271
|
} catch (error) {
|
|
310
272
|
logError('[getFaceImages] Native face crop failed:', error);
|
|
311
273
|
return [];
|
|
312
274
|
}
|
|
313
275
|
};
|
|
276
|
+
|
|
277
|
+
// Check if face image has glare (brightness exceeds threshold)
|
|
278
|
+
const checkFaceGlare = async faceImage => {
|
|
279
|
+
try {
|
|
280
|
+
// Check entire face region for glare
|
|
281
|
+
const hasGlare = await OpenCVModule.isRectangularRegionBright(faceImage, 0, 0, 100,
|
|
282
|
+
// Full face width
|
|
283
|
+
100,
|
|
284
|
+
// Full face height
|
|
285
|
+
MAX_BRIGHTNESS_THRESHOLD);
|
|
286
|
+
return hasGlare;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return false; // Assume no glare on error
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Check if face is fully visible (not cropped at edges)
|
|
293
|
+
const isFaceFullyVisible = (face, frameWidth, frameHeight) => {
|
|
294
|
+
const margin = FACE_EDGE_MARGIN_PERCENT;
|
|
295
|
+
const bounds = face.bounds;
|
|
296
|
+
return bounds.x >= frameWidth * margin && bounds.y >= frameHeight * margin && bounds.x + bounds.width <= frameWidth * (1 - margin) && bounds.y + bounds.height <= frameHeight * (1 - margin);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Check if document image has glare
|
|
300
|
+
const checkDocumentGlare = async (image, width, height) => {
|
|
301
|
+
try {
|
|
302
|
+
// Check center 80% region for glare (document area)
|
|
303
|
+
const hasGlare = await OpenCVModule.isRectangularRegionBright(image, Math.round(width * 0.1), Math.round(height * 0.1), Math.round(width * 0.8), Math.round(height * 0.8), MAX_BRIGHTNESS_THRESHOLD);
|
|
304
|
+
return hasGlare;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
314
309
|
const setNextStepAndVibrate = useCallback((nextStepType, fromStep) => {
|
|
315
310
|
if (fromStep) {
|
|
316
311
|
setCompletedStep(fromStep);
|
|
@@ -318,15 +313,20 @@ const IdentityDocumentCamera = ({
|
|
|
318
313
|
|
|
319
314
|
// Turn flash on and reset hologram counters when entering SCAN_HOLOGRAM
|
|
320
315
|
if (nextStepType === 'SCAN_HOLOGRAM' && fromStep !== 'SCAN_HOLOGRAM') {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
316
|
+
const isMidCollection = faceImages.current.length > 0 && faceImages.current.length < HOLOGRAM_IMAGE_COUNT;
|
|
317
|
+
if (!isMidCollection) {
|
|
318
|
+
setIsTorchOn(true);
|
|
319
|
+
hologramDetectionCurrentRetryCount.current = 0;
|
|
320
|
+
secondaryFaceDetectionCurrentRetryCount.current = 0;
|
|
321
|
+
hologramFramesWithoutFace.current = 0;
|
|
322
|
+
faceImages.current = [];
|
|
323
|
+
hologramImageCountRef.current = 0;
|
|
324
|
+
setHologramImageCount(0);
|
|
325
|
+
setLatestHologramFaceImage(undefined);
|
|
326
|
+
// Clear previous hologram state to prevent premature completion
|
|
327
|
+
setCurrentHologramImage(undefined);
|
|
328
|
+
setCurrentHologramMaskImage(undefined);
|
|
329
|
+
}
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// Clean up flash and hologram state when leaving SCAN_HOLOGRAM step
|
|
@@ -336,20 +336,23 @@ const IdentityDocumentCamera = ({
|
|
|
336
336
|
hologramImageCountRef.current = 0;
|
|
337
337
|
setHologramImageCount(0);
|
|
338
338
|
setLatestHologramFaceImage(undefined);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
console.log('[Flash] Turning off flash and clearing hologram images when leaving step');
|
|
345
|
-
}
|
|
339
|
+
isHologramDetectionInProgress.current = false;
|
|
340
|
+
lastFacePosition.current = null;
|
|
341
|
+
cachedBarcode.current = null;
|
|
342
|
+
setDocumentPlaneBounds(null);
|
|
343
|
+
setSecondaryFaceBounds(null);
|
|
346
344
|
}
|
|
347
345
|
setNextStep(nextStepType);
|
|
348
346
|
Vibration.vibrate(100);
|
|
349
347
|
|
|
350
348
|
// Reset MRZ retry counter for each new step so retries start fresh
|
|
351
349
|
mrzDetectionCurrentRetryCount.current = 0;
|
|
352
|
-
|
|
350
|
+
// Only clear MRZ text when entering SCAN_ID_BACK (new MRZ expected).
|
|
351
|
+
// Preserve across SCAN_HOLOGRAM so passport completion has MRZ data.
|
|
352
|
+
if (nextStepType === 'SCAN_ID_BACK') {
|
|
353
|
+
lastValidMRZText.current = null;
|
|
354
|
+
lastValidMRZFields.current = null;
|
|
355
|
+
}
|
|
353
356
|
validMRZConsecutiveCount.current = 0;
|
|
354
357
|
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
355
358
|
|
|
@@ -360,59 +363,90 @@ const IdentityDocumentCamera = ({
|
|
|
360
363
|
}, 1000);
|
|
361
364
|
}
|
|
362
365
|
}, [setIsTorchOn]);
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
console.log('[DocType] faces:', facesParam.length, 'mrzFields:', !!mrzFields, 'mrzText:', !!mrzTextParam, 'textLen:', ocrText?.length, 'hasSignature:', hasSignatureMatch);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ID Back: no face + ID MRZ
|
|
372
|
-
if (facesParam.length === 0 && mrzFields?.documentCode === 'I') {
|
|
373
|
-
return 'ID_BACK';
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Passport: face + passport MRZ
|
|
377
|
-
if (facesParam.length > 0 && mrzFields?.documentCode === 'P') {
|
|
378
|
-
return 'PASSPORT';
|
|
379
|
-
}
|
|
366
|
+
const transitionStepWithCallback = useCallback((nextStepType, fromStep, scannedData) => {
|
|
367
|
+
if (isStepTransitionInProgress.current) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
isStepTransitionInProgress.current = true;
|
|
380
371
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
setIsTorchOn(false);
|
|
387
|
-
}
|
|
372
|
+
// Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
|
|
373
|
+
if (nextStepType !== 'SCAN_HOLOGRAM') {
|
|
374
|
+
setIsTorchOn(false);
|
|
375
|
+
}
|
|
376
|
+
setNextStepAndVibrate(nextStepType, fromStep);
|
|
388
377
|
|
|
389
|
-
|
|
390
|
-
|
|
378
|
+
// Prevent the COMPLETED handler from firing a duplicate callback with
|
|
379
|
+
// potentially wrong detectedDocumentType. This transition already sends
|
|
380
|
+
// the correct scannedData below.
|
|
381
|
+
if (nextStepType === 'COMPLETED') {
|
|
382
|
+
isCompletionCallbackInvoked.current = true;
|
|
383
|
+
}
|
|
391
384
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
385
|
+
// Only notify parent for step completions, not intermediate transitions.
|
|
386
|
+
// The COMPLETED handler constructs final data from accumulated state.
|
|
387
|
+
// For ID cards, front/back data must be sent incrementally since parent stores them separately.
|
|
388
|
+
const isIntermediatePassportStep = scannedData.documentType === 'PASSPORT' && nextStepType !== 'COMPLETED';
|
|
389
|
+
if (!isIntermediatePassportStep) {
|
|
390
|
+
setTimeout(() => {
|
|
391
|
+
onIdentityDocumentScanned(scannedData);
|
|
392
|
+
}, 1000);
|
|
393
|
+
}
|
|
394
|
+
}, [onIdentityDocumentScanned, setNextStepAndVibrate, setIsTorchOn]);
|
|
395
|
+
const handleFaceAndText = useCallback(async (text, faces, frameWidth, frameHeight, barcode, image, elementsOutside, scannedText) => {
|
|
396
|
+
// Classify faces: Primary (>= 5% of frame) vs Secondary (< 5%)
|
|
397
|
+
const primaryFaces = faces.filter(face => face.bounds.width >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT && face.bounds.height >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT);
|
|
398
|
+
const secondaryFaces = faces.filter(face => face.bounds.width < frameWidth * MIN_CARD_FACE_SIZE_PERCENT || face.bounds.height < frameWidth * MIN_CARD_FACE_SIZE_PERCENT);
|
|
399
|
+
|
|
400
|
+
// All faces for processing
|
|
401
|
+
const allDetectedFaces = faces;
|
|
402
|
+
|
|
403
|
+
// Validate primary face meets ICAO standards (face height 70-80% of image, aspect ratio ~1:1.25)
|
|
404
|
+
let primaryFaceICAOCompliant = false;
|
|
405
|
+
if (primaryFaces.length > 0) {
|
|
406
|
+
const face = primaryFaces[0];
|
|
407
|
+
const faceHeightPercent = face.bounds.height / frameHeight * 100;
|
|
408
|
+
const aspectRatio = face.bounds.width / face.bounds.height;
|
|
409
|
+
|
|
410
|
+
// ICAO: face height 70-80% of image, width:height ratio between 0.75 and 0.85
|
|
411
|
+
primaryFaceICAOCompliant = faceHeightPercent >= 70 && faceHeightPercent <= 80 && aspectRatio >= 0.75 && aspectRatio <= 0.85;
|
|
412
|
+
}
|
|
413
|
+
if (isDebugEnabled() && nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && faces.length > 0) {
|
|
414
|
+
debugLog('IdentityDocumentCamera', '[FACE DETECTION] All faces', {
|
|
415
|
+
totalFaces: faces.length,
|
|
416
|
+
primaryFacesCount: primaryFaces.length,
|
|
417
|
+
secondaryFacesCount: secondaryFaces.length,
|
|
418
|
+
frameWidth,
|
|
419
|
+
frameHeight,
|
|
420
|
+
minPrimarySize: frameWidth * MIN_CARD_FACE_SIZE_PERCENT,
|
|
421
|
+
primaryFaceICAOCompliant,
|
|
422
|
+
faceDetails: faces.map((f, i) => ({
|
|
423
|
+
index: i,
|
|
424
|
+
width: Math.round(f.bounds.width),
|
|
425
|
+
height: Math.round(f.bounds.height),
|
|
426
|
+
x: Math.round(f.bounds.x),
|
|
427
|
+
y: Math.round(f.bounds.y),
|
|
428
|
+
widthPercent: (f.bounds.width / frameWidth * 100).toFixed(1) + '%',
|
|
429
|
+
heightPercent: (f.bounds.height / frameHeight * 100).toFixed(1) + '%',
|
|
430
|
+
aspectRatio: (f.bounds.width / f.bounds.height).toFixed(2),
|
|
431
|
+
category: f.bounds.width >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT && f.bounds.height >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT ? 'primary' : 'secondary'
|
|
432
|
+
}))
|
|
433
|
+
});
|
|
434
|
+
}
|
|
413
435
|
|
|
414
|
-
//
|
|
415
|
-
const
|
|
436
|
+
// Get scan area bounds for face filtering
|
|
437
|
+
const {
|
|
438
|
+
isInsideScan
|
|
439
|
+
} = getScanAreaBounds(frameWidth, frameHeight);
|
|
440
|
+
|
|
441
|
+
// Filter to only faces inside scan area (for hologram, exclude passport secondary faces)
|
|
442
|
+
const facesInsideScanArea = primaryFaces.filter(face => isInsideScan(face.bounds.x, face.bounds.y, face.bounds.width, face.bounds.height));
|
|
443
|
+
if (isDebugEnabled() && nextStep === 'SCAN_HOLOGRAM' && primaryFaces.length > facesInsideScanArea.length) {
|
|
444
|
+
debugLog('IdentityDocumentCamera', '[HOLOGRAM] Filtered out faces outside scan area', {
|
|
445
|
+
totalFaces: primaryFaces.length,
|
|
446
|
+
facesInside: facesInsideScanArea.length,
|
|
447
|
+
filtered: primaryFaces.length - facesInsideScanArea.length
|
|
448
|
+
});
|
|
449
|
+
}
|
|
416
450
|
|
|
417
451
|
// Cache barcode when detected, use cached value if current frame has no barcode
|
|
418
452
|
// This handles inconsistent barcode detection across frames
|
|
@@ -432,87 +466,99 @@ const IdentityDocumentCamera = ({
|
|
|
432
466
|
setIsTorchOn(false);
|
|
433
467
|
}
|
|
434
468
|
if (nextStep === 'COMPLETED') {
|
|
469
|
+
// Prevent multiple callback invocations from repeated frames
|
|
470
|
+
if (isCompletionCallbackInvoked.current) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
isCompletionCallbackInvoked.current = true;
|
|
435
474
|
setStatus('SCANNED');
|
|
475
|
+
// Construct scanned data from available state and invoke callback
|
|
476
|
+
// Use MRZ document code as ultimate authority for document type —
|
|
477
|
+
// detectedDocumentType may be wrong if locked from early noisy frames
|
|
478
|
+
const completedDocType = lastValidMRZFields.current?.documentCode === 'P' ? 'PASSPORT' : detectedDocumentType;
|
|
479
|
+
const scannedData = {
|
|
480
|
+
documentType: completedDocType,
|
|
481
|
+
image: image ?? '',
|
|
482
|
+
faceImage: currentFaceImage,
|
|
483
|
+
secondaryFaceImage: currentSecondaryFaceImage,
|
|
484
|
+
hologramImage: currentHologramImage,
|
|
485
|
+
mrzText: lastValidMRZText.current ?? undefined,
|
|
486
|
+
mrzFields: lastValidMRZFields.current ?? undefined,
|
|
487
|
+
barcodeValue: barcodeToUse?.rawValue ?? undefined
|
|
488
|
+
};
|
|
489
|
+
if (isDebugEnabled()) {
|
|
490
|
+
debugLog('IdentityDocumentCamera', '[COMPLETED] Final scanned data', {
|
|
491
|
+
documentType: completedDocType,
|
|
492
|
+
hasFaceImage: !!scannedData.faceImage,
|
|
493
|
+
hasSecondaryFace: !!scannedData.secondaryFaceImage,
|
|
494
|
+
secondaryFaceImageLength: scannedData.secondaryFaceImage?.length || 0,
|
|
495
|
+
currentSecondaryFaceLength: currentSecondaryFaceImage?.length || 0,
|
|
496
|
+
hasHologramImage: !!scannedData.hologramImage,
|
|
497
|
+
hasMRZ: !!scannedData.mrzFields,
|
|
498
|
+
hasBarcode: !!scannedData.barcodeValue
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
setTimeout(() => {
|
|
502
|
+
onIdentityDocumentScanned(scannedData);
|
|
503
|
+
}, 500);
|
|
436
504
|
return;
|
|
437
505
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (nextStep === 'SCAN_ID_BACK' && cardSizedFaces.length > 0) {
|
|
442
|
-
setStatus('INCORRECT');
|
|
506
|
+
|
|
507
|
+
// Skip elementsOutside check during SCAN_HOLOGRAM - allow document tilting for hologram capture
|
|
508
|
+
if (elementsOutside && nextStep !== 'SCAN_HOLOGRAM') {
|
|
443
509
|
return;
|
|
444
510
|
}
|
|
445
511
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
512
|
+
// Parse MRZ early to use for document type detection
|
|
513
|
+
// Use JavaScript MRZ validation with corrections
|
|
514
|
+
// Prefer MRZ-only text if available (from detected MRZ blocks),
|
|
515
|
+
// otherwise fall back to all text (for backward compatibility)
|
|
516
|
+
const textForValidation = scannedText?.mrzOnlyText || text;
|
|
517
|
+
const mrzValidationResult = mrzUtils.validateMRZWithCorrections(textForValidation);
|
|
518
|
+
const parsedMRZData = {
|
|
519
|
+
valid: mrzValidationResult.valid,
|
|
520
|
+
fields: mrzValidationResult.fields || null
|
|
521
|
+
};
|
|
522
|
+
// Extract raw MRZ lines from text if validation succeeded
|
|
523
|
+
const mrzText = parsedMRZData.valid ? mrzUtils.fixMRZ(text) : null;
|
|
524
|
+
|
|
525
|
+
// CRITICAL: Only detect document type during initial scan step
|
|
526
|
+
// For SCAN_HOLOGRAM and beyond, use the locked detectedDocumentType to avoid
|
|
527
|
+
// interruptions when user tilts document (MRZ may not be visible)
|
|
528
|
+
// However, if MRZ code 'P' is detected, always use PASSPORT — the lock may
|
|
529
|
+
// be wrong (ID_FRONT locked before passport MRZ became readable)
|
|
530
|
+
const documentType = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectDocumentType(primaryFaces, text, parsedMRZData?.fields, frameWidth, mrzText) : parsedMRZData?.fields?.documentCode === 'P' ? 'PASSPORT' : detectedDocumentType;
|
|
531
|
+
|
|
532
|
+
// Crop faces once document type is confirmed or we're past the initial step
|
|
533
|
+
const shouldCropFaces = documentType === 'ID_FRONT' || documentType === 'PASSPORT' || nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
534
|
+
const croppedFaces = shouldCropFaces ? await getFaceImages(primaryFaces, image ?? '', frameWidth, frameHeight) : [];
|
|
449
535
|
|
|
450
536
|
// Validate document plane consistency across all captures
|
|
451
537
|
let facePositionValid = true;
|
|
452
|
-
if (
|
|
453
|
-
const currentFaceBounds =
|
|
538
|
+
if (primaryFaces.length > 0 && primaryFaces[0]) {
|
|
539
|
+
const currentFaceBounds = primaryFaces[0].bounds;
|
|
454
540
|
if (lastFacePosition.current) {
|
|
455
|
-
|
|
456
|
-
// Use looser tolerance during hologram step since flash toggling causes position jitter
|
|
457
|
-
const xDiff = Math.abs(currentFaceBounds.x - lastFacePosition.current.x);
|
|
458
|
-
const yDiff = Math.abs(currentFaceBounds.y - lastFacePosition.current.y);
|
|
459
|
-
const widthDiff = Math.abs(currentFaceBounds.width - lastFacePosition.current.width);
|
|
460
|
-
const heightDiff = Math.abs(currentFaceBounds.height - lastFacePosition.current.height);
|
|
461
|
-
const tolerance = nextStep === 'SCAN_HOLOGRAM' ? 0.5 : 0.2;
|
|
462
|
-
const xTolerance = lastFacePosition.current.width * tolerance;
|
|
463
|
-
const yTolerance = lastFacePosition.current.height * tolerance;
|
|
464
|
-
const sizeTolerance = lastFacePosition.current.width * tolerance;
|
|
465
|
-
facePositionValid = xDiff <= xTolerance && yDiff <= yTolerance && widthDiff <= sizeTolerance && heightDiff <= sizeTolerance;
|
|
466
|
-
if (!facePositionValid) {
|
|
467
|
-
if (isDebugEnabled()) {
|
|
468
|
-
console.log(`[DocPlane] Face position changed - document moved (xDiff=${xDiff.toFixed(1)}, yDiff=${yDiff.toFixed(1)}) - rejecting frame`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Update reference position to follow gradual movement (sliding window)
|
|
473
|
-
lastFacePosition.current = {
|
|
474
|
-
x: currentFaceBounds.x,
|
|
475
|
-
y: currentFaceBounds.y,
|
|
476
|
-
width: currentFaceBounds.width,
|
|
477
|
-
height: currentFaceBounds.height
|
|
478
|
-
};
|
|
479
|
-
} else {
|
|
480
|
-
// First capture - store reference position
|
|
481
|
-
lastFacePosition.current = {
|
|
482
|
-
x: currentFaceBounds.x,
|
|
483
|
-
y: currentFaceBounds.y,
|
|
484
|
-
width: currentFaceBounds.width,
|
|
485
|
-
height: currentFaceBounds.height
|
|
486
|
-
};
|
|
487
|
-
console.log('[DocPlane] Stored reference face position for document plane validation');
|
|
541
|
+
facePositionValid = validateFacePosition(currentFaceBounds, lastFacePosition.current, nextStep === 'SCAN_HOLOGRAM');
|
|
488
542
|
}
|
|
489
543
|
|
|
490
|
-
// Update
|
|
491
|
-
|
|
544
|
+
// Update reference position (sliding window)
|
|
545
|
+
lastFacePosition.current = {
|
|
546
|
+
x: currentFaceBounds.x,
|
|
547
|
+
y: currentFaceBounds.y,
|
|
548
|
+
width: currentFaceBounds.width,
|
|
549
|
+
height: currentFaceBounds.height
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Update debug overlay bounds
|
|
492
553
|
if (facePositionValid && frameDimensions) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let scale;
|
|
499
|
-
let offsetX = 0;
|
|
500
|
-
let offsetY = 0;
|
|
501
|
-
if (frameAspect > screenAspect) {
|
|
502
|
-
// Frame is wider - scale by height, crop width
|
|
503
|
-
scale = screen.height / frameDimensions.height;
|
|
504
|
-
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
505
|
-
} else {
|
|
506
|
-
// Frame is taller - scale by width, crop height
|
|
507
|
-
scale = screen.width / frameDimensions.width;
|
|
508
|
-
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
509
|
-
}
|
|
554
|
+
const {
|
|
555
|
+
scale,
|
|
556
|
+
offsetX,
|
|
557
|
+
offsetY
|
|
558
|
+
} = getFrameToScreenTransform(frameDimensions.width, frameDimensions.height);
|
|
510
559
|
const cropPadding = Math.max(currentFaceBounds.width * 0.15, currentFaceBounds.height * 0.15);
|
|
511
560
|
setDocumentPlaneBounds({
|
|
512
|
-
|
|
513
|
-
y: currentFaceBounds.y * scale - offsetY,
|
|
514
|
-
width: currentFaceBounds.width * scale,
|
|
515
|
-
height: currentFaceBounds.height * scale,
|
|
561
|
+
...transformBoundsToScreen(currentFaceBounds, scale, offsetX, offsetY),
|
|
516
562
|
cropPadding: cropPadding * scale
|
|
517
563
|
});
|
|
518
564
|
}
|
|
@@ -521,31 +567,35 @@ const IdentityDocumentCamera = ({
|
|
|
521
567
|
// Capture and persist face only after document type is confirmed
|
|
522
568
|
// This prevents locking a face before we know what document we're scanning
|
|
523
569
|
let faceImageToUse = currentFaceImage;
|
|
524
|
-
if (shouldCropFaces && croppedFaces.length > 0 && croppedFaces[0] && facePositionValid) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
570
|
+
if (shouldCropFaces && croppedFaces.length > 0 && croppedFaces[0] && facePositionValid && !currentFaceImage) {
|
|
571
|
+
// Validate face quality before accepting
|
|
572
|
+
const faceFullyVisible = primaryFaces[0] && isFaceFullyVisible(primaryFaces[0], frameWidth, frameHeight);
|
|
573
|
+
const hasGlare = await checkFaceGlare(croppedFaces[0]);
|
|
574
|
+
if (!faceFullyVisible || hasGlare) {
|
|
575
|
+
// Reject face with glare or partially visible
|
|
529
576
|
if (isDebugEnabled()) {
|
|
530
|
-
|
|
577
|
+
debugLog('IdentityDocumentCamera', '[FACE] Rejected', {
|
|
578
|
+
fullyVisible: faceFullyVisible,
|
|
579
|
+
hasGlare
|
|
580
|
+
});
|
|
531
581
|
}
|
|
582
|
+
// Continue scanning without locking this face
|
|
583
|
+
} else {
|
|
584
|
+
faceImageToUse = croppedFaces[0];
|
|
585
|
+
setCurrentFaceImage(croppedFaces[0]);
|
|
532
586
|
}
|
|
533
587
|
}
|
|
588
|
+
|
|
589
|
+
// Skip OCR text checks during SCAN_HOLOGRAM - flash and tilting make text unreadable
|
|
590
|
+
// but we only need face detection for hologram collection
|
|
534
591
|
if (!text || text.length < 5 || !image) {
|
|
535
|
-
|
|
536
|
-
|
|
592
|
+
if (nextStep !== 'SCAN_HOLOGRAM') {
|
|
593
|
+
setStatus('SEARCHING');
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// During SCAN_HOLOGRAM, allow processing even if text is not readable
|
|
537
597
|
}
|
|
538
598
|
|
|
539
|
-
// Use JavaScript MRZ validation with corrections
|
|
540
|
-
// Prefer MRZ-only text if available (from detected MRZ blocks),
|
|
541
|
-
// otherwise fall back to all text (for backward compatibility)
|
|
542
|
-
const textForValidation = scannedText?.mrzOnlyText || text;
|
|
543
|
-
const mrzValidationResult = mrzUtils.validateMRZWithCorrections(textForValidation);
|
|
544
|
-
const parsedMRZData = {
|
|
545
|
-
valid: mrzValidationResult.valid,
|
|
546
|
-
fields: mrzValidationResult.fields || null
|
|
547
|
-
};
|
|
548
|
-
|
|
549
599
|
// Capture test mode data
|
|
550
600
|
if (testMode && text && text.includes('<')) {
|
|
551
601
|
const mrzOnlyText = scannedText?.mrzOnlyText || text;
|
|
@@ -555,239 +605,124 @@ const IdentityDocumentCamera = ({
|
|
|
555
605
|
});
|
|
556
606
|
}
|
|
557
607
|
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
const mrzLines = text.split('\n').filter(line => line.includes('<') && line.length > 20);
|
|
561
|
-
if (mrzLines.length >= 2) {
|
|
562
|
-
console.log('[MRZ Debug] Raw OCR text lines:', mrzLines.map(l => `"${l}"`));
|
|
563
|
-
console.log('[MRZ Debug] Validation result:', {
|
|
564
|
-
valid: mrzValidationResult.valid,
|
|
565
|
-
format: mrzValidationResult.format,
|
|
566
|
-
documentCode: mrzValidationResult.fields?.documentCode,
|
|
567
|
-
documentNumber: mrzValidationResult.fields?.documentNumber,
|
|
568
|
-
optional1: mrzValidationResult.fields?.optional1,
|
|
569
|
-
error: mrzValidationResult.error
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Extract raw MRZ lines from text if validation succeeded
|
|
575
|
-
const mrzText = parsedMRZData.valid ? mrzUtils.fixMRZ(text) : null;
|
|
576
|
-
|
|
577
|
-
// MRZ stability check - require consistent valid reads to avoid OCR noise
|
|
578
|
-
// Compare parsed field values instead of raw text to handle OCR variations in filler characters
|
|
579
|
-
// Only proceed with MRZ if it's actually valid and has all required fields
|
|
608
|
+
// MRZ stability: require consistent valid reads across frames
|
|
609
|
+
// Skip during SCAN_HOLOGRAM - document type already locked
|
|
580
610
|
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
581
|
-
if (mrzText && parsedMRZData?.valid === true && parsedMRZData?.fields && mrzHasRequiredFields) {
|
|
611
|
+
if (nextStep !== 'SCAN_HOLOGRAM' && mrzText && parsedMRZData?.valid === true && parsedMRZData?.fields && mrzHasRequiredFields) {
|
|
582
612
|
const currentFields = parsedMRZData.fields;
|
|
583
613
|
if (areMRZFieldsEqual(lastValidMRZFields.current, currentFields)) {
|
|
584
|
-
// Same MRZ data detected again - increment counter
|
|
585
614
|
validMRZConsecutiveCount.current++;
|
|
586
615
|
} else {
|
|
587
|
-
// Different MRZ data - reset counter and store new data
|
|
588
|
-
if (isDebugEnabled()) {
|
|
589
|
-
console.log(`[MRZ Stability] New MRZ detected, resetting counter (was ${validMRZConsecutiveCount.current}, doc: ${currentFields.documentNumber?.slice(-4)})`);
|
|
590
|
-
}
|
|
591
616
|
lastValidMRZFields.current = currentFields;
|
|
592
617
|
lastValidMRZText.current = mrzText;
|
|
593
618
|
validMRZConsecutiveCount.current = 1;
|
|
594
619
|
}
|
|
595
|
-
} else {
|
|
596
|
-
// Invalid or no MRZ - don't reset completely, just skip this frame
|
|
597
|
-
// This allows temporary OCR noise without losing progress
|
|
598
620
|
}
|
|
621
|
+
// else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
|
|
599
622
|
|
|
600
623
|
// Check if we have enough consistent valid reads
|
|
601
624
|
const mrzStableAndValid = validMRZConsecutiveCount.current >= REQUIRED_CONSISTENT_MRZ_READS && parsedMRZData?.valid === true && areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
|
|
602
625
|
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
626
|
+
// ============================================================================
|
|
627
|
+
// SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
|
|
628
|
+
// ============================================================================
|
|
606
629
|
if (nextStep === 'SCAN_ID_BACK') {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
console.log(`[ID_BACK Scan] Wrong side detected - faces: ${hasFaces}, signature: ${hasSignature}, passport MRZ: ${hasPassportMRZ}, passport pattern: ${hasPassportMRZPattern}`);
|
|
630
|
+
const handleIdBackStep = async () => {
|
|
631
|
+
// Guard: wrong side detection (front or passport when back is expected)
|
|
632
|
+
const hasFaces = primaryFaces.length > 0;
|
|
633
|
+
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
634
|
+
const hasPassportMRZ = parsedMRZData?.fields?.documentCode === 'P';
|
|
635
|
+
const hasPassportMRZPattern = mrzText && PASSPORT_MRZ_PATTERN.test(mrzText);
|
|
636
|
+
if (hasFaces || hasSignature || hasPassportMRZ || hasPassportMRZPattern) {
|
|
637
|
+
setStatus('INCORRECT');
|
|
638
|
+
return;
|
|
617
639
|
}
|
|
618
|
-
setStatus('INCORRECT');
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
640
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if (isDebugEnabled()) {
|
|
626
|
-
console.log('[ID_BACK Scan] ERROR: Passport detected in ID_BACK step - skipping to COMPLETED');
|
|
627
|
-
}
|
|
628
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
629
|
-
setTimeout(() => {
|
|
630
|
-
onIdentityDocumentScanned({
|
|
641
|
+
// Safety: passport somehow reached ID_BACK step
|
|
642
|
+
if (detectedDocumentType === 'PASSPORT') {
|
|
643
|
+
transitionStepWithCallback('COMPLETED', 'SCAN_ID_BACK', {
|
|
631
644
|
image,
|
|
632
645
|
documentType: 'PASSPORT',
|
|
633
646
|
mrzText: mrzText ?? undefined,
|
|
634
647
|
mrzFields: parsedMRZData?.fields
|
|
635
648
|
});
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
// CRITICAL: Require all document elements to be in frame before accepting
|
|
657
|
-
// For ID_BACK: MRZ + barcode (check directly, not via state to avoid timing issues)
|
|
658
|
-
const hasBarcode = !!barcodeToUse?.rawValue;
|
|
659
|
-
const allRequiredElementsInFrame = hasMRZ && hasBarcode || onlyMRZScan;
|
|
660
|
-
|
|
661
|
-
// Don't block based on bounds - just ensure elements are present
|
|
662
|
-
setElementsOutsideScanArea([]);
|
|
663
|
-
if (!allRequiredElementsInFrame && hasMRZ && mrzAccepted) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
setElementsOutsideScanArea([]);
|
|
652
|
+
const flowResult = handleIDBackFlow(mrzText, parsedMRZData?.fields, parsedMRZData?.valid === true, mrzStableAndValid, hasRequiredMRZFields(parsedMRZData?.fields), barcodeToUse?.rawValue, onlyMRZScan);
|
|
653
|
+
if (!flowResult.shouldProceed) {
|
|
654
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
655
|
+
setStatus(mrzText ? 'SCANNING' : 'SEARCHING');
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Check for glare on ID back before accepting
|
|
660
|
+
const hasGlare = await checkDocumentGlare(image, frameWidth, frameHeight);
|
|
661
|
+
if (hasGlare) {
|
|
662
|
+
if (isDebugEnabled()) {
|
|
663
|
+
debugLog('IdentityDocumentCamera', '[ID_BACK] Rejected - glare detected');
|
|
664
|
+
}
|
|
665
|
+
setStatus('SCANNING');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
664
668
|
if (isDebugEnabled()) {
|
|
665
|
-
|
|
669
|
+
debugLog('IdentityDocumentCamera', '[ID_BACK] MRZ validated', {
|
|
670
|
+
documentNumber: parsedMRZData?.fields?.documentNumber,
|
|
671
|
+
reads: validMRZConsecutiveCount.current
|
|
672
|
+
});
|
|
666
673
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (hasMRZ && mrzAccepted && barcodeAccepted && allRequiredElementsInFrame) {
|
|
671
|
-
logMRZDetails('ID_BACK Scan', parsedMRZData?.fields, mrzText, validMRZConsecutiveCount.current, isDebugEnabled());
|
|
672
|
-
const scannedData = {
|
|
674
|
+
setDetectedDocumentType('ID_BACK');
|
|
675
|
+
setStatus('SCANNED');
|
|
676
|
+
transitionStepWithCallback('COMPLETED', 'SCAN_ID_BACK', {
|
|
673
677
|
image,
|
|
674
678
|
documentType: 'ID_BACK',
|
|
675
679
|
mrzText: mrzText ?? undefined,
|
|
676
680
|
mrzFields: parsedMRZData?.fields,
|
|
677
681
|
barcodeValue: barcodeToUse?.rawValue ?? undefined
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
setIsTorchOn(false);
|
|
682
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
683
|
-
setTimeout(() => {
|
|
684
|
-
onIdentityDocumentScanned(scannedData);
|
|
685
|
-
}, 1000);
|
|
686
|
-
} else {
|
|
687
|
-
if (hasMRZ && !mrzAccepted) {
|
|
688
|
-
logMRZValidationFailure('ID_BACK Scan', hasRequiredFields, parsedMRZData, mrzDetectionCurrentRetryCount.current, isDebugEnabled());
|
|
689
|
-
} else if (hasMRZ && mrzAccepted && !barcodeAccepted) {
|
|
690
|
-
if (isDebugEnabled()) {
|
|
691
|
-
console.log('[ID_BACK Scan] MRZ valid but barcode check failed - retrying', {
|
|
692
|
-
onlyMRZScan,
|
|
693
|
-
hasBarcodeValue: !!barcodeToUse?.rawValue,
|
|
694
|
-
barcodeMatchesMRZ,
|
|
695
|
-
barcodeContainsMRZ,
|
|
696
|
-
mrzOptional1: parsedMRZData?.fields?.optional1,
|
|
697
|
-
barcodeValue: barcodeToUse?.rawValue,
|
|
698
|
-
barcodeValueTrimmed: barcodeToUse?.rawValue?.trim(),
|
|
699
|
-
optional1Trimmed: parsedMRZData?.fields?.optional1?.trim(),
|
|
700
|
-
barcodeSource: barcodeToUse === cachedBarcode.current ? 'cached' : 'current'
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
mrzDetectionCurrentRetryCount.current++;
|
|
705
|
-
setStatus(hasMRZ ? 'SCANNING' : 'SEARCHING');
|
|
706
|
-
}
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
handleIdBackStep();
|
|
707
685
|
return;
|
|
708
686
|
}
|
|
709
|
-
const documentType = detectDocumentType(cardSizedFaces, text, parsedMRZData?.fields, frameWidth, mrzText);
|
|
710
|
-
|
|
711
|
-
// Update detected document type only during initial scan step
|
|
712
|
-
// CRITICAL: Only set document type from non-blurry, stable frames
|
|
713
|
-
// Once set to PASSPORT or definitively identified, preserve it to avoid incorrect flow transitions
|
|
714
|
-
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && detectedDocumentType === 'UNKNOWN') {
|
|
715
|
-
// Determine the document type to set based on current frame analysis
|
|
716
|
-
let docTypeToSet = documentType;
|
|
717
|
-
if (documentType === 'PASSPORT') {
|
|
718
|
-
// Passport detected definitively - candidate for locking in
|
|
719
|
-
docTypeToSet = 'PASSPORT';
|
|
720
|
-
} else if (documentType === 'UNKNOWN' && cardSizedFaces.length > 0 && parsedMRZData?.fields?.documentCode === 'P') {
|
|
721
|
-
// Early passport detection: face detected + passport MRZ code (even if not fully valid yet)
|
|
722
|
-
docTypeToSet = 'PASSPORT';
|
|
723
|
-
} else if (documentType === 'ID_FRONT') {
|
|
724
|
-
// Check if this is actually a passport based on MRZ code
|
|
725
|
-
// Passports can be misdetected as ID_FRONT when signature-like text is visible
|
|
726
|
-
if (parsedMRZData?.fields?.documentCode === 'P') {
|
|
727
|
-
if (isDebugEnabled()) {
|
|
728
|
-
console.log('[DocType] Correcting misdetection: ID_FRONT → PASSPORT (MRZ code P)');
|
|
729
|
-
}
|
|
730
|
-
docTypeToSet = 'PASSPORT';
|
|
731
|
-
} else if (parsedMRZData?.fields?.documentCode === 'I') {
|
|
732
|
-
// MRZ confirms it's an ID card
|
|
733
|
-
docTypeToSet = 'ID_FRONT';
|
|
734
|
-
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
735
|
-
// Passport MRZ pattern visible but not parsed yet - wait for proper classification
|
|
736
|
-
if (isDebugEnabled()) {
|
|
737
|
-
console.log('[DocType] Passport MRZ pattern detected but not parsed - waiting instead of locking as ID_FRONT');
|
|
738
|
-
}
|
|
739
|
-
docTypeToSet = 'UNKNOWN';
|
|
740
|
-
} else {
|
|
741
|
-
// No MRZ code and no passport pattern - safe to classify as ID_FRONT
|
|
742
|
-
// ID cards typically don't have MRZ on front (only on back)
|
|
743
|
-
docTypeToSet = 'ID_FRONT';
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
docTypeToSet = 'UNKNOWN';
|
|
747
|
-
}
|
|
748
687
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
688
|
+
// Turn off torch when ID_FRONT detected during initial scan
|
|
689
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && (documentType === 'ID_FRONT' || documentType === 'PASSPORT') && isTorchOnRef.current) {
|
|
690
|
+
setIsTorchOn(false);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Lock document type from stable quality frames during initial scan.
|
|
694
|
+
// Also allow correcting ID_FRONT → PASSPORT when passport MRZ appears later.
|
|
695
|
+
// Passport MRZ may not be visible in early frames (while signature is),
|
|
696
|
+
// causing premature ID_FRONT lock that must be correctable.
|
|
697
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && (detectedDocumentType === 'UNKNOWN' || detectedDocumentType === 'ID_FRONT')) {
|
|
698
|
+
const docTypeToSet = determineDocumentTypeToSet(documentType, primaryFaces, parsedMRZData?.fields, mrzText);
|
|
699
|
+
|
|
700
|
+
// If already locked as ID_FRONT, only allow correction to PASSPORT
|
|
701
|
+
if (detectedDocumentType === 'ID_FRONT' && docTypeToSet === 'PASSPORT') {
|
|
702
|
+
setDetectedDocumentType('PASSPORT');
|
|
703
|
+
consistentDocTypeCount.current = 0;
|
|
704
|
+
lastDetectedDocType.current = 'PASSPORT';
|
|
705
|
+
} else if (detectedDocumentType === 'UNKNOWN') {
|
|
706
|
+
if (lastFrameQuality.current.hasAcceptableQuality && docTypeToSet !== 'UNKNOWN') {
|
|
707
|
+
if (docTypeToSet === lastDetectedDocType.current) {
|
|
708
|
+
consistentDocTypeCount.current++;
|
|
709
|
+
} else {
|
|
710
|
+
lastDetectedDocType.current = docTypeToSet;
|
|
711
|
+
consistentDocTypeCount.current = 1;
|
|
757
712
|
}
|
|
758
713
|
if (consistentDocTypeCount.current >= REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS) {
|
|
759
|
-
// Stable detection confirmed - lock it in
|
|
760
|
-
if (isDebugEnabled()) {
|
|
761
|
-
console.log(`[DocType] Locked document type: ${docTypeToSet} (stable for ${consistentDocTypeCount.current} quality frames)`);
|
|
762
|
-
}
|
|
763
714
|
setDetectedDocumentType(docTypeToSet);
|
|
764
715
|
}
|
|
765
|
-
} else {
|
|
766
|
-
// Document type changed - reset counter
|
|
767
|
-
if (isDebugEnabled()) {
|
|
768
|
-
console.log(`[DocType Stability] Type changed: ${lastDetectedDocType.current} → ${docTypeToSet}, resetting counter`);
|
|
769
|
-
}
|
|
770
|
-
lastDetectedDocType.current = docTypeToSet;
|
|
771
|
-
consistentDocTypeCount.current = 1;
|
|
772
|
-
}
|
|
773
|
-
} else if (!lastFrameQuality.current.hasAcceptableQuality && docTypeToSet !== 'UNKNOWN') {
|
|
774
|
-
// Poor quality frame - don't use for document type detection
|
|
775
|
-
if (isDebugEnabled()) {
|
|
776
|
-
console.log(`[DocType Stability] Skipping poor quality frame (blurry: ${lastFrameQuality.current.isBlurry}, brightness: ${lastFrameQuality.current.brightness.toFixed(0)})`);
|
|
777
716
|
}
|
|
778
717
|
}
|
|
779
718
|
}
|
|
780
|
-
// Document type is now locked and won't be changed after initial scan
|
|
781
|
-
// Hologram and subsequent steps use the preserved detectedDocumentType state
|
|
782
|
-
|
|
783
719
|
const scannedData = {
|
|
784
720
|
image,
|
|
785
721
|
documentType,
|
|
786
722
|
mrzText: mrzText ?? undefined,
|
|
787
723
|
mrzFields: parsedMRZData?.fields
|
|
788
724
|
};
|
|
789
|
-
|
|
790
|
-
if (isWrongSide) {
|
|
725
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && documentType === 'ID_BACK') {
|
|
791
726
|
setStatus('INCORRECT');
|
|
792
727
|
return;
|
|
793
728
|
}
|
|
@@ -796,172 +731,241 @@ const IdentityDocumentCamera = ({
|
|
|
796
731
|
if (faceImageToUse) {
|
|
797
732
|
scannedData.faceImage = faceImageToUse;
|
|
798
733
|
}
|
|
734
|
+
const continueScanning = (incrementMrzRetry = false) => {
|
|
735
|
+
if (incrementMrzRetry) {
|
|
736
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
737
|
+
}
|
|
738
|
+
setStatus('SCANNING');
|
|
739
|
+
};
|
|
799
740
|
if (!onlyMRZScan) {
|
|
800
|
-
// Hologram detection during SCAN_HOLOGRAM step
|
|
741
|
+
// Hologram detection during SCAN_HOLOGRAM step
|
|
801
742
|
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
// Always crop to the same face region across all hologram frames so
|
|
807
|
-
// OpenCV receives consistently-sized images for comparison.
|
|
808
|
-
// Use current face bounds if available, otherwise fall back to last known position.
|
|
809
|
-
const hologramFaceBounds = cardSizedFaces.length > 0 && cardSizedFaces[0] ? cardSizedFaces[0].bounds : lastFacePosition.current;
|
|
743
|
+
// CRITICAL: Only use faces inside scan area for hologram
|
|
744
|
+
// This prevents passport secondary faces (outside frame or on right side) from being used
|
|
745
|
+
const hologramFaceBounds = facesInsideScanArea.length > 0 && facesInsideScanArea[0] ? facesInsideScanArea[0].bounds : lastFacePosition.current;
|
|
810
746
|
let primaryFaceOnly;
|
|
811
747
|
if (hologramFaceBounds && image) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
748
|
+
// Verify face is fully visible before using for hologram
|
|
749
|
+
const faceFullyVisible = hologramFaceBounds ? hologramFaceBounds.x >= frameWidth * FACE_EDGE_MARGIN_PERCENT && hologramFaceBounds.y >= frameHeight * FACE_EDGE_MARGIN_PERCENT && hologramFaceBounds.x + hologramFaceBounds.width <= frameWidth * (1 - FACE_EDGE_MARGIN_PERCENT) && hologramFaceBounds.y + hologramFaceBounds.height <= frameHeight * (1 - FACE_EDGE_MARGIN_PERCENT) : false;
|
|
750
|
+
if (faceFullyVisible) {
|
|
751
|
+
const hologramCropped = await getFaceImages([{
|
|
752
|
+
bounds: hologramFaceBounds,
|
|
753
|
+
rollAngle: 0,
|
|
754
|
+
yawAngle: 0
|
|
755
|
+
}], image, frameWidth, frameHeight, true // widerRightPadding for hologram detection
|
|
756
|
+
);
|
|
757
|
+
primaryFaceOnly = hologramCropped[0] ?? faceImageToUse;
|
|
758
|
+
} else {
|
|
759
|
+
// Face not fully visible - skip this frame
|
|
760
|
+
if (isDebugEnabled()) {
|
|
761
|
+
debugLog('IdentityDocumentCamera', '[HOLOGRAM] Face not fully visible', {
|
|
762
|
+
faceX: hologramFaceBounds.x,
|
|
763
|
+
faceY: hologramFaceBounds.y,
|
|
764
|
+
faceWidth: hologramFaceBounds.width,
|
|
765
|
+
faceHeight: hologramFaceBounds.height,
|
|
766
|
+
frameWidth,
|
|
767
|
+
frameHeight
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
primaryFaceOnly = undefined;
|
|
771
|
+
}
|
|
818
772
|
} else {
|
|
819
773
|
primaryFaceOnly = faceImageToUse;
|
|
820
774
|
}
|
|
821
775
|
|
|
822
776
|
// Skip face position validation for hologram — flash toggling causes position jitter
|
|
823
777
|
if (primaryFaceOnly) {
|
|
824
|
-
// Reset consecutive no-face counter since we have a face
|
|
825
778
|
hologramFramesWithoutFace.current = 0;
|
|
826
779
|
if (currentHologramImage) {
|
|
827
780
|
scannedData.hologramImage = currentHologramImage;
|
|
828
781
|
} else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
|
|
829
|
-
//
|
|
830
|
-
const
|
|
831
|
-
const timeSinceLastCapture = now - lastHologramCaptureTime.current;
|
|
782
|
+
// Space out captures for better variation
|
|
783
|
+
const timeSinceLastCapture = Date.now() - lastHologramCaptureTime.current;
|
|
832
784
|
if (faceImages.current.length === 0 || timeSinceLastCapture >= HOLOGRAM_CAPTURE_INTERVAL) {
|
|
833
|
-
//
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
hologramImageCountRef.current = faceImages.current.length;
|
|
837
|
-
|
|
838
|
-
// Only update state at first and last frame to minimize re-renders
|
|
839
|
-
if (faceImages.current.length === 1 || faceImages.current.length === HOLOGRAM_IMAGE_COUNT) {
|
|
840
|
-
setHologramImageCount(faceImages.current.length);
|
|
841
|
-
setLatestHologramFaceImage(primaryFaceOnly);
|
|
842
|
-
}
|
|
843
|
-
if (isDebugEnabled()) {
|
|
844
|
-
console.log(`[Hologram] Collected ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT} face images`);
|
|
785
|
+
// Keep torch on during hologram scan for consistent lighting
|
|
786
|
+
if (!isTorchOnRef.current) {
|
|
787
|
+
setIsTorchOn(true);
|
|
845
788
|
}
|
|
846
789
|
|
|
847
|
-
//
|
|
790
|
+
// Check hologram face for glare
|
|
791
|
+
const hasGlare = await checkFaceGlare(primaryFaceOnly);
|
|
792
|
+
if (!hasGlare) {
|
|
793
|
+
faceImages.current.push(primaryFaceOnly);
|
|
794
|
+
lastHologramCaptureTime.current = Date.now();
|
|
795
|
+
hologramImageCountRef.current = faceImages.current.length;
|
|
796
|
+
if (isDebugEnabled()) {
|
|
797
|
+
debugLog('IdentityDocumentCamera', '[HOLOGRAM] Frame captured', {
|
|
798
|
+
frameIndex: faceImages.current.length - 1,
|
|
799
|
+
totalFrames: faceImages.current.length
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
} else if (isDebugEnabled()) {
|
|
803
|
+
debugLog('IdentityDocumentCamera', '[HOLOGRAM] Rejected glare frame', {
|
|
804
|
+
collectedCount: faceImages.current.length
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
if (faceImages.current.length > 0) {
|
|
808
|
+
// Update UI state at first and last frame only
|
|
809
|
+
if (faceImages.current.length === 1 || faceImages.current.length === HOLOGRAM_IMAGE_COUNT) {
|
|
810
|
+
setHologramImageCount(faceImages.current.length);
|
|
811
|
+
setLatestHologramFaceImage(primaryFaceOnly);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
848
814
|
}
|
|
849
815
|
} else if (faceImages.current.length >= HOLOGRAM_IMAGE_COUNT) {
|
|
850
|
-
//
|
|
851
|
-
if (
|
|
852
|
-
|
|
816
|
+
// Guard: skip if already processing or max retries exhausted
|
|
817
|
+
if (isHologramDetectionInProgress.current) return;
|
|
818
|
+
if (hologramDetectionCurrentRetryCount.current >= HOLOGRAM_DETECTION_RETRY_COUNT) {
|
|
819
|
+
faceImages.current = [];
|
|
820
|
+
hologramImageCountRef.current = 0;
|
|
821
|
+
setHologramImageCount(0);
|
|
822
|
+
setLatestHologramFaceImage(undefined);
|
|
823
|
+
return;
|
|
853
824
|
}
|
|
825
|
+
|
|
826
|
+
// Process collected face images for hologram detection
|
|
827
|
+
isHologramDetectionInProgress.current = true;
|
|
854
828
|
try {
|
|
855
829
|
const [hologramMask, hologram] = await detectHologramNative(faceImages.current);
|
|
830
|
+
if (isDebugEnabled()) {
|
|
831
|
+
debugLog('IdentityDocumentCamera', '[Hologram] Native result', {
|
|
832
|
+
hasHologram: !!hologram,
|
|
833
|
+
hasHologramMask: !!hologramMask,
|
|
834
|
+
hologramLength: hologram?.length || 0
|
|
835
|
+
});
|
|
836
|
+
}
|
|
856
837
|
if (hologram) {
|
|
857
838
|
setCurrentHologramMaskImage(hologramMask);
|
|
858
839
|
scannedData.hologramImage = hologram;
|
|
859
840
|
setCurrentHologramImage(hologram);
|
|
860
841
|
if (isDebugEnabled()) {
|
|
861
|
-
|
|
842
|
+
debugLog('IdentityDocumentCamera', '[Hologram] ✓ Saved hologram image');
|
|
862
843
|
}
|
|
863
844
|
} else {
|
|
864
845
|
if (isDebugEnabled()) {
|
|
865
|
-
|
|
846
|
+
debugLog('IdentityDocumentCamera', '[Hologram] ✗ No hologram detected');
|
|
866
847
|
}
|
|
867
848
|
}
|
|
868
849
|
} catch (error) {
|
|
869
850
|
console.error('[Hologram] Processing error:', error);
|
|
870
851
|
} finally {
|
|
871
|
-
// Keep flash on - will turn off when step changes
|
|
872
852
|
faceImages.current = [];
|
|
873
853
|
hologramImageCountRef.current = 0;
|
|
874
854
|
setHologramImageCount(0);
|
|
875
855
|
setLatestHologramFaceImage(undefined);
|
|
876
856
|
hologramDetectionCurrentRetryCount.current++;
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
857
|
+
isHologramDetectionInProgress.current = false;
|
|
858
|
+
// Turn off torch after detection completes
|
|
859
|
+
setIsTorchOn(false);
|
|
880
860
|
}
|
|
881
861
|
}
|
|
882
862
|
} else {
|
|
883
|
-
// No face detected for hologram collection
|
|
884
|
-
// Track consecutive frames without face for safety timeout
|
|
885
863
|
hologramFramesWithoutFace.current++;
|
|
886
|
-
if (isDebugEnabled()) {
|
|
887
|
-
console.log(`[Hologram] No face detected - frame ${hologramFramesWithoutFace.current}/${HOLOGRAM_MAX_FRAMES_WITHOUT_FACE}`);
|
|
888
|
-
}
|
|
889
864
|
}
|
|
890
865
|
} else if (currentHologramImage) {
|
|
891
866
|
scannedData.hologramImage = currentHologramImage;
|
|
892
|
-
} else if (faceImages.current.length > 0) {
|
|
893
|
-
// Safety cleanup: not in hologram step but have images collected
|
|
894
|
-
faceImages.current = [];
|
|
895
|
-
hologramImageCountRef.current = 0;
|
|
896
|
-
setHologramImageCount(0);
|
|
897
|
-
setLatestHologramFaceImage(undefined);
|
|
898
|
-
if (isDebugEnabled()) {
|
|
899
|
-
console.log('[Hologram] Defensive cleanup - cleared images outside hologram step');
|
|
900
|
-
}
|
|
901
867
|
}
|
|
902
868
|
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
869
|
+
// Secondary face capture (continuous during initial scan and hologram detection)
|
|
870
|
+
// MLI (Multi Layer Image) is small secondary face on Turkish ID cards
|
|
871
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' || nextStep === 'SCAN_HOLOGRAM') {
|
|
872
|
+
if (isDebugEnabled() && allDetectedFaces.length > 1) {
|
|
873
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Entry conditions check', {
|
|
874
|
+
hasCurrentSecondary: !!currentSecondaryFaceImage,
|
|
875
|
+
hasPrimaryFace: !!scannedData.faceImage,
|
|
876
|
+
totalFaces: allDetectedFaces.length,
|
|
877
|
+
facePositionValid,
|
|
878
|
+
willAttemptDetection: !currentSecondaryFaceImage && !!scannedData.faceImage && allDetectedFaces.length > 1 && facePositionValid
|
|
879
|
+
});
|
|
880
|
+
}
|
|
908
881
|
if (currentSecondaryFaceImage) {
|
|
909
882
|
scannedData.secondaryFaceImage = currentSecondaryFaceImage;
|
|
910
|
-
} else if (
|
|
911
|
-
//
|
|
912
|
-
const
|
|
913
|
-
)
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
883
|
+
} else if (scannedData.faceImage && allDetectedFaces.length > 1 && facePositionValid) {
|
|
884
|
+
// Detect MLI face (smaller than main face, to the right)
|
|
885
|
+
const primaryFace = primaryFaces[0];
|
|
886
|
+
if (isDebugEnabled() && primaryFace) {
|
|
887
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Starting detection', {
|
|
888
|
+
totalFaces: allDetectedFaces.length,
|
|
889
|
+
primaryFaceSize: `${Math.round(primaryFace.bounds.width)}x${Math.round(primaryFace.bounds.height)}`,
|
|
890
|
+
primaryFacePos: `x:${Math.round(primaryFace.bounds.x)} y:${Math.round(primaryFace.bounds.y)}`,
|
|
891
|
+
otherFaces: allDetectedFaces.filter(f => f !== primaryFace).map(f => ({
|
|
892
|
+
size: `${Math.round(f.bounds.width)}x${Math.round(f.bounds.height)}`,
|
|
893
|
+
pos: `x:${Math.round(f.bounds.x)} y:${Math.round(f.bounds.y)}`,
|
|
894
|
+
isRight: f.bounds.x > primaryFace.bounds.x,
|
|
895
|
+
verticalRange: `${Math.round(primaryFace.bounds.y - primaryFace.bounds.height * 0.3)}-${Math.round(primaryFace.bounds.y + primaryFace.bounds.height * 1.3)}`,
|
|
896
|
+
inVerticalRange: f.bounds.y >= primaryFace.bounds.y - primaryFace.bounds.height * 0.3 && f.bounds.y <= primaryFace.bounds.y + primaryFace.bounds.height * 1.3
|
|
897
|
+
}))
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const potentialMLIFaces = allDetectedFaces.filter(f => f !== primaryFace && f.bounds.x > primaryFace.bounds.x &&
|
|
901
|
+
// MLI is to the right of main face
|
|
902
|
+
f.bounds.y >= primaryFace.bounds.y - primaryFace.bounds.height * 0.3 &&
|
|
903
|
+
// Same vertical level (±30%)
|
|
904
|
+
f.bounds.y <= primaryFace.bounds.y + primaryFace.bounds.height * 1.3);
|
|
905
|
+
if (isDebugEnabled()) {
|
|
906
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Position filter result', {
|
|
907
|
+
potentialMLICount: potentialMLIFaces.length
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (potentialMLIFaces.length > 0 && potentialMLIFaces[0]) {
|
|
911
|
+
const secondaryFace = potentialMLIFaces[0];
|
|
912
|
+
|
|
913
|
+
// Crop MLI face separately
|
|
914
|
+
const mliFaceCropped = await getFaceImages([primaryFace, secondaryFace], image ?? '', frameWidth, frameHeight);
|
|
915
|
+
if (mliFaceCropped.length > 1 && mliFaceCropped[1]) {
|
|
916
|
+
// Visual similarity check with lenient threshold
|
|
917
|
+
const visualResult = await compareFaceVisualSimilarityNative(mliFaceCropped[0], mliFaceCropped[1]);
|
|
918
|
+
const similarityScore = visualResult?.similarity || 0;
|
|
919
|
+
const isLikelySamePerson = similarityScore >= 0.2; // Very lenient: 20%
|
|
920
|
+
|
|
921
|
+
if (isDebugEnabled()) {
|
|
922
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Similarity check', {
|
|
923
|
+
visualSimilarity: similarityScore.toFixed(3),
|
|
924
|
+
isLikelySamePerson,
|
|
925
|
+
threshold: 0.2
|
|
926
|
+
});
|
|
932
927
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
928
|
+
if (isLikelySamePerson) {
|
|
929
|
+
// Skip glare check for MLI - it's a small printed photo with different reflective properties
|
|
930
|
+
// Backend will validate quality
|
|
931
|
+
scannedData.secondaryFaceImage = mliFaceCropped[1];
|
|
932
|
+
setCurrentSecondaryFaceImage(scannedData.secondaryFaceImage);
|
|
933
|
+
if (isDebugEnabled()) {
|
|
934
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Captured successfully', {
|
|
935
|
+
similarity: similarityScore.toFixed(3),
|
|
936
|
+
imageLength: mliFaceCropped[1]?.length || 0,
|
|
937
|
+
imageSet: !!scannedData.secondaryFaceImage,
|
|
938
|
+
stateSet: !!currentSecondaryFaceImage,
|
|
939
|
+
primarySize: `${Math.round(primaryFace.bounds.width)}x${Math.round(primaryFace.bounds.height)}`,
|
|
940
|
+
secondarySize: `${Math.round(secondaryFace.bounds.width)}x${Math.round(secondaryFace.bounds.height)}`
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (frameDimensions) {
|
|
944
|
+
const {
|
|
945
|
+
scale,
|
|
946
|
+
offsetX,
|
|
947
|
+
offsetY
|
|
948
|
+
} = getFrameToScreenTransform(frameDimensions.width, frameDimensions.height);
|
|
949
|
+
const secondaryBounds = secondaryFace.bounds;
|
|
950
|
+
setSecondaryFaceBounds(transformBoundsToScreen(secondaryBounds, scale, offsetX, offsetY));
|
|
951
|
+
}
|
|
952
|
+
} else if (isDebugEnabled()) {
|
|
953
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Rejected - similarity too low', {
|
|
954
|
+
similarity: similarityScore.toFixed(3),
|
|
955
|
+
threshold: 0.2
|
|
945
956
|
});
|
|
946
|
-
} else {
|
|
947
|
-
setSecondaryFaceBounds(null);
|
|
948
957
|
}
|
|
949
958
|
}
|
|
950
|
-
if (isDebugEnabled()) {
|
|
951
|
-
console.log('[SecondaryFace] ✓ Captured and validated as similar to main face (same document plane)');
|
|
952
|
-
}
|
|
953
|
-
} else {
|
|
954
|
-
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
955
|
-
if (isDebugEnabled()) {
|
|
956
|
-
console.log('[SecondaryFace] ✗ Rejected - not similar enough to main face');
|
|
957
|
-
}
|
|
958
959
|
}
|
|
959
960
|
} else {
|
|
960
961
|
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
962
|
+
if (isDebugEnabled() && allDetectedFaces.length > 1) {
|
|
963
|
+
debugLog('IdentityDocumentCamera', '[MLI FACE] Conditions not met', {
|
|
964
|
+
hasPrimaryFace: !!scannedData.faceImage,
|
|
965
|
+
primaryFacesCount: primaryFaces.length,
|
|
966
|
+
allDetectedFaces: allDetectedFaces.length,
|
|
967
|
+
facePositionValid
|
|
968
|
+
});
|
|
965
969
|
}
|
|
966
970
|
}
|
|
967
971
|
} else if (currentSecondaryFaceImage) {
|
|
@@ -970,238 +974,126 @@ const IdentityDocumentCamera = ({
|
|
|
970
974
|
}
|
|
971
975
|
}
|
|
972
976
|
|
|
973
|
-
//
|
|
974
|
-
//
|
|
977
|
+
// ============================================================================
|
|
978
|
+
// SCAN_HOLOGRAM STEP
|
|
979
|
+
// ============================================================================
|
|
975
980
|
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
const hasFaces = cardSizedFaces.length > 0;
|
|
979
|
-
const hasBarcode = !!barcode?.rawValue; // ID back has barcode, front doesn't
|
|
980
|
-
|
|
981
|
-
// For passport: back side has no photo and different text pattern
|
|
982
|
-
// For ID card: back side has no photo, has barcode
|
|
983
|
-
const isWrongSideForHologram = !hasFaces || hasBarcode;
|
|
984
|
-
if (isWrongSideForHologram) {
|
|
985
|
-
if (isDebugEnabled()) {
|
|
986
|
-
console.log(`[SCAN_HOLOGRAM] Wrong side detected - no faces: ${!hasFaces}, has barcode: ${hasBarcode} - expecting front side with photo`);
|
|
987
|
-
}
|
|
981
|
+
// Guard: barcode visible = back side shown
|
|
982
|
+
if (barcode?.rawValue) {
|
|
988
983
|
setStatus('INCORRECT');
|
|
989
984
|
return;
|
|
990
985
|
}
|
|
986
|
+
const isCollecting = faceImages.current.length > 0;
|
|
987
|
+
const maxRetriesReached = hologramDetectionCurrentRetryCount.current >= HOLOGRAM_DETECTION_RETRY_COUNT;
|
|
988
|
+
|
|
989
|
+
// Wait for face if not yet started collecting
|
|
990
|
+
if (!isCollecting && primaryFaces.length === 0) {
|
|
991
|
+
hologramFramesWithoutFace.current++;
|
|
992
|
+
if (hologramFramesWithoutFace.current >= HOLOGRAM_MAX_FRAMES_WITHOUT_FACE) {
|
|
993
|
+
setStatus('INCORRECT');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
continueScanning();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
991
999
|
|
|
992
|
-
//
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
// Don't skip if actively collecting images
|
|
996
|
-
const isActivelyCollecting = faceImages.current.length > 0 && faceImages.current.length < HOLOGRAM_IMAGE_COUNT;
|
|
997
|
-
const hologramConditionMet = !!scannedData.hologramImage || hologramDetectionCurrentRetryCount.current >= HOLOGRAM_DETECTION_RETRY_COUNT && !isActivelyCollecting ||
|
|
998
|
-
// Don't skip if mid-collection
|
|
999
|
-
faceDetectionTimeout && !isActivelyCollecting; // Don't timeout if mid-collection
|
|
1000
|
-
|
|
1001
|
-
// During hologram scan, we ONLY care about hologram collection - no other checks
|
|
1002
|
-
// Secondary face, MRZ, document type checks are all skipped
|
|
1003
|
-
// Document type was already definitively determined in the initial scan phase
|
|
1000
|
+
// Complete when hologram captured OR retries exhausted (not mid-collection)
|
|
1001
|
+
const stepComplete = (!!scannedData.hologramImage || maxRetriesReached && !isCollecting) && !!faceImageToUse; // Require face before completing
|
|
1004
1002
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (faceDetectionTimeout && isDebugEnabled()) {
|
|
1011
|
-
console.log('[SCAN_HOLOGRAM] Face detection timeout - proceeding without hologram');
|
|
1003
|
+
if (stepComplete) {
|
|
1004
|
+
// Ensure preserved MRZ data is included (current frame may not have
|
|
1005
|
+
// readable MRZ due to flash/tilting during hologram capture)
|
|
1006
|
+
if (!scannedData.mrzText && lastValidMRZText.current) {
|
|
1007
|
+
scannedData.mrzText = lastValidMRZText.current;
|
|
1012
1008
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
setIsTorchOn(false);
|
|
1016
|
-
}
|
|
1017
|
-
// Route based on PRESERVED detectedDocumentType state (set during initial scan)
|
|
1018
|
-
// Also check current frame's documentType and MRZ code as fallback
|
|
1019
|
-
// Passport has no back side - go directly to COMPLETED
|
|
1020
|
-
const isPassport = detectedDocumentType === 'PASSPORT' || documentType === 'PASSPORT' || parsedMRZData?.fields?.documentCode === 'P';
|
|
1021
|
-
if (isDebugEnabled()) {
|
|
1022
|
-
console.log('[SCAN_HOLOGRAM] Document type check:', {
|
|
1023
|
-
detectedDocumentType,
|
|
1024
|
-
documentType,
|
|
1025
|
-
mrzCode: parsedMRZData?.fields?.documentCode,
|
|
1026
|
-
isPassport
|
|
1027
|
-
});
|
|
1009
|
+
if (!scannedData.mrzFields && lastValidMRZFields.current) {
|
|
1010
|
+
scannedData.mrzFields = lastValidMRZFields.current;
|
|
1028
1011
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
if (isDebugEnabled()) {
|
|
1036
|
-
console.log('[SCAN_HOLOGRAM] ID card detected - proceeding to back scan');
|
|
1037
|
-
}
|
|
1038
|
-
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
|
|
1039
|
-
}
|
|
1040
|
-
setTimeout(() => {
|
|
1041
|
-
onIdentityDocumentScanned(scannedData);
|
|
1042
|
-
}, 1000);
|
|
1012
|
+
setStatus('SCANNED');
|
|
1013
|
+
|
|
1014
|
+
// Use scannedData.mrzFields which we just ensured has preserved MRZ
|
|
1015
|
+
const mrzDocCode = scannedData.mrzFields?.documentCode;
|
|
1016
|
+
const nextStepAfterHologram = getNextStepAfterHologram(detectedDocumentType, documentType, mrzDocCode);
|
|
1017
|
+
transitionStepWithCallback(nextStepAfterHologram, 'SCAN_HOLOGRAM', scannedData);
|
|
1043
1018
|
return;
|
|
1044
1019
|
}
|
|
1045
|
-
|
|
1046
|
-
// Don't fall through to document type branching
|
|
1047
|
-
setStatus('SCANNING');
|
|
1020
|
+
continueScanning();
|
|
1048
1021
|
return;
|
|
1049
1022
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1023
|
+
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
// INITIAL SCAN STEP - Detect document type and validate
|
|
1026
|
+
// ============================================================================
|
|
1027
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1028
|
+
// Determine which flow handler to use.
|
|
1029
|
+
// Current-frame passport detection always takes precedence over a locked
|
|
1030
|
+
// ID_FRONT — passport MRZ may not appear until later frames.
|
|
1031
|
+
const flowDocumentType = documentType === 'PASSPORT' ? 'PASSPORT' : detectedDocumentType !== 'UNKNOWN' ? detectedDocumentType : documentType;
|
|
1032
|
+
const handlePassportInitialStep = async () => {
|
|
1033
|
+
const flowResult = handlePassportFlow(primaryFaces, mrzText, parsedMRZData?.fields, mrzStableAndValid, onlyMRZScan, hasRequiredMRZFields(parsedMRZData?.fields), !!faceImageToUse);
|
|
1034
|
+
if (!flowResult.shouldProceed) {
|
|
1035
|
+
continueScanning(true);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Check for glare on passport before accepting
|
|
1040
|
+
const hasGlare = await checkDocumentGlare(image, frameWidth, frameHeight);
|
|
1041
|
+
if (hasGlare) {
|
|
1055
1042
|
if (isDebugEnabled()) {
|
|
1056
|
-
|
|
1043
|
+
debugLog('IdentityDocumentCamera', '[PASSPORT] Rejected - glare detected');
|
|
1057
1044
|
}
|
|
1058
|
-
|
|
1045
|
+
continueScanning(false);
|
|
1059
1046
|
return;
|
|
1060
1047
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1048
|
+
setDetectedDocumentType('PASSPORT');
|
|
1049
|
+
setStatus('SCANNED');
|
|
1050
|
+
const nextPassportStep = flowResult.nextAction === 'PROCEED_TO_COMPLETED' ? 'COMPLETED' : 'SCAN_HOLOGRAM';
|
|
1051
|
+
transitionStepWithCallback(nextPassportStep, 'SCAN_ID_FRONT_OR_PASSPORT', scannedData);
|
|
1052
|
+
};
|
|
1053
|
+
const handleIdFrontInitialStep = async () => {
|
|
1054
|
+
const flowResult = handleIDFrontFlow(primaryFaces, text, mrzText, parsedMRZData?.fields, mrzDetectionCurrentRetryCount.current);
|
|
1055
|
+
if (!flowResult.shouldProceed) {
|
|
1056
|
+
if (flowResult.nextAction === 'REJECT_AS_PASSPORT') {
|
|
1057
|
+
setDetectedDocumentType('UNKNOWN');
|
|
1058
|
+
consistentDocTypeCount.current = 0;
|
|
1059
|
+
lastDetectedDocType.current = 'UNKNOWN';
|
|
1060
|
+
}
|
|
1061
|
+
continueScanning(flowResult.nextAction !== 'REJECT_AS_PASSPORT');
|
|
1071
1062
|
return;
|
|
1072
1063
|
}
|
|
1073
1064
|
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
if (
|
|
1077
|
-
if (parsedMRZData.fields.documentCode === 'I') {
|
|
1078
|
-
if (isDebugEnabled()) {
|
|
1079
|
-
console.log('[ID_FRONT Scan] MRZ confirms ID card (code I)');
|
|
1080
|
-
}
|
|
1081
|
-
} else if (parsedMRZData.fields.documentCode === 'P') {
|
|
1082
|
-
if (isDebugEnabled()) {
|
|
1083
|
-
console.log('[ID_FRONT Scan] MRZ shows passport (code P) - rejecting as ID_FRONT');
|
|
1084
|
-
}
|
|
1085
|
-
setStatus('SCANNING');
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
1089
|
-
// No parsed MRZ BUT passport MRZ pattern visible (P<TUR, P<USA, etc.)
|
|
1090
|
-
// This is likely a passport with OCR errors - wait for proper parsing
|
|
1065
|
+
// Check for glare on ID front before accepting
|
|
1066
|
+
const hasGlare = await checkDocumentGlare(image, frameWidth, frameHeight);
|
|
1067
|
+
if (hasGlare) {
|
|
1091
1068
|
if (isDebugEnabled()) {
|
|
1092
|
-
|
|
1069
|
+
debugLog('IdentityDocumentCamera', '[ID_FRONT] Rejected - glare detected');
|
|
1093
1070
|
}
|
|
1094
|
-
|
|
1095
|
-
setStatus('SCANNING');
|
|
1071
|
+
continueScanning(false);
|
|
1096
1072
|
return;
|
|
1097
1073
|
}
|
|
1098
|
-
// No MRZ or no passport pattern - proceed as ID card
|
|
1099
|
-
// ID cards typically don't have MRZ on front side (only on back)
|
|
1100
|
-
|
|
1101
|
-
// CRITICAL: Lock document type state to ID_FRONT before proceeding
|
|
1102
|
-
// This ensures hologram completion knows it's an ID card (needs ID_BACK step)
|
|
1103
1074
|
setDetectedDocumentType('ID_FRONT');
|
|
1104
1075
|
setStatus('SCANNED');
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// At this point detectedDocumentType is definitively set
|
|
1109
|
-
if (detectedDocumentType === 'PASSPORT') {
|
|
1110
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1111
|
-
} else {
|
|
1112
|
-
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1113
|
-
}
|
|
1114
|
-
setTimeout(() => {
|
|
1115
|
-
onIdentityDocumentScanned(scannedData);
|
|
1116
|
-
}, 1000);
|
|
1117
|
-
} else {
|
|
1118
|
-
if (isDebugEnabled()) {
|
|
1119
|
-
console.log('[ID_FRONT Scan] Confirmed as ID card - proceeding to hologram');
|
|
1120
|
-
}
|
|
1121
|
-
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1122
|
-
setTimeout(() => {
|
|
1123
|
-
onIdentityDocumentScanned(scannedData);
|
|
1124
|
-
}, 1000);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
// Note: SCAN_HOLOGRAM completion is now handled in the unified block above
|
|
1128
|
-
} else if (documentType === 'PASSPORT') {
|
|
1129
|
-
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && !scannedData.hologramImage) {
|
|
1130
|
-
if (onlyMRZScan) {
|
|
1131
|
-
const hasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
1132
|
-
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
1133
|
-
if (!!scannedData.mrzText && hasRequiredFields && mrzStableAndValid) {
|
|
1134
|
-
const hasFace = cardSizedFaces.length > 0;
|
|
1135
|
-
const hasMRZ = !!mrzText;
|
|
1136
|
-
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1137
|
-
setElementsOutsideScanArea([]);
|
|
1138
|
-
if (!allRequiredElementsInFrame) {
|
|
1139
|
-
console.log('[Passport Scan] MRZ valid but waiting for all elements in frame (face + MRZ)');
|
|
1140
|
-
setStatus('SCANNING');
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
logMRZDetails('Passport Scan', parsedMRZData?.fields, mrzText, validMRZConsecutiveCount.current, isDebugEnabled());
|
|
1144
|
-
setDetectedDocumentType('PASSPORT');
|
|
1145
|
-
setStatus('SCANNED');
|
|
1146
|
-
setIsTorchOn(false);
|
|
1147
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1148
|
-
setTimeout(() => {
|
|
1149
|
-
onIdentityDocumentScanned(scannedData);
|
|
1150
|
-
}, 1000);
|
|
1151
|
-
return; // CRITICAL: Exit after MRZ-only scan completes to prevent fall-through
|
|
1152
|
-
} else {
|
|
1153
|
-
if (!!scannedData.mrzText && !mrzStableAndValid) {
|
|
1154
|
-
logMRZValidationFailure('Passport Scan', hasRequiredFields, parsedMRZData, mrzDetectionCurrentRetryCount.current, isDebugEnabled());
|
|
1155
|
-
}
|
|
1156
|
-
mrzDetectionCurrentRetryCount.current++;
|
|
1157
|
-
setStatus('SCANNING');
|
|
1158
|
-
return; // Don't fall through to else-if
|
|
1159
|
-
}
|
|
1160
|
-
} else {
|
|
1161
|
-
// Normal passport scan (with hologram) - require MRZ to be detected before proceeding
|
|
1162
|
-
const hasFace = cardSizedFaces.length > 0;
|
|
1163
|
-
const hasMRZ = !!mrzText;
|
|
1164
|
-
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1165
|
-
setElementsOutsideScanArea([]);
|
|
1166
|
-
if (!allRequiredElementsInFrame) {
|
|
1167
|
-
console.log('[Passport Scan] Valid but waiting for all elements in frame (face + MRZ)');
|
|
1168
|
-
setStatus('SCANNING');
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1076
|
+
const nextIdFrontStep = onlyMRZScan ? 'SCAN_ID_BACK' : 'SCAN_HOLOGRAM';
|
|
1077
|
+
transitionStepWithCallback(nextIdFrontStep, 'SCAN_ID_FRONT_OR_PASSPORT', scannedData);
|
|
1078
|
+
};
|
|
1171
1079
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
setStatus('SCANNING');
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
console.log('[Passport Scan] MRZ confirmed passport (code P) - definitively identified, proceeding to hologram');
|
|
1180
|
-
// CRITICAL: Lock document type state to PASSPORT before proceeding to hologram
|
|
1181
|
-
// This ensures hologram completion knows it's a passport (no ID_BACK step)
|
|
1182
|
-
setDetectedDocumentType('PASSPORT');
|
|
1183
|
-
setStatus('SCANNED');
|
|
1184
|
-
setIsTorchOn(false);
|
|
1185
|
-
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1186
|
-
setTimeout(() => {
|
|
1187
|
-
onIdentityDocumentScanned(scannedData);
|
|
1188
|
-
}, 1000);
|
|
1189
|
-
}
|
|
1080
|
+
// PASSPORT FLOW: Face + MRZ with code 'P'
|
|
1081
|
+
if (flowDocumentType === 'PASSPORT') {
|
|
1082
|
+
handlePassportInitialStep();
|
|
1083
|
+
return;
|
|
1190
1084
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
setStatus('SCANNING');
|
|
1197
|
-
} else {
|
|
1198
|
-
// Document type UNKNOWN - continue scanning until we can classify it
|
|
1199
|
-
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1200
|
-
console.log('[Document Scan] Type UNKNOWN - waiting for clearer detection (faces:', cardSizedFaces.length, 'mrzCode:', parsedMRZData?.fields?.documentCode || 'none', 'text length:', text.length, ')');
|
|
1085
|
+
|
|
1086
|
+
// ID CARD FLOW: Face + No passport MRZ pattern
|
|
1087
|
+
if (flowDocumentType === 'ID_FRONT') {
|
|
1088
|
+
handleIdFrontInitialStep();
|
|
1089
|
+
return;
|
|
1201
1090
|
}
|
|
1202
|
-
|
|
1091
|
+
|
|
1092
|
+
// UNKNOWN - Continue scanning
|
|
1093
|
+
continueScanning();
|
|
1094
|
+
return;
|
|
1203
1095
|
}
|
|
1204
|
-
}, [nextStep, frameDimensions, currentHologramImage,
|
|
1096
|
+
}, [nextStep, frameDimensions, currentHologramImage, detectedDocumentType, currentFaceImage, testMode, onlyMRZScan, setIsTorchOn, transitionStepWithCallback, onIdentityDocumentScanned, currentSecondaryFaceImage, detectHologramNative]);
|
|
1205
1097
|
const handleFrame = useCallback(async event => {
|
|
1206
1098
|
if (!isCameraInitialized.current) {
|
|
1207
1099
|
return;
|
|
@@ -1329,47 +1221,18 @@ const IdentityDocumentCamera = ({
|
|
|
1329
1221
|
},
|
|
1330
1222
|
cornerPoints: b.cornerPoints ?? []
|
|
1331
1223
|
}));
|
|
1332
|
-
|
|
1333
|
-
// Log barcode detection for debugging (only when scanning ID back)
|
|
1334
|
-
if (barcodes.length > 0 && nextStep === 'SCAN_ID_BACK' && isDebugEnabled()) {
|
|
1335
|
-
console.log(`[Barcode JS] Detected ${barcodes.length} barcode(s):`);
|
|
1336
|
-
barcodes.forEach((b, idx) => {
|
|
1337
|
-
const formatNames = {
|
|
1338
|
-
5: 'PDF417',
|
|
1339
|
-
64: 'QR_CODE',
|
|
1340
|
-
1: 'CODE_128',
|
|
1341
|
-
2: 'CODE_39',
|
|
1342
|
-
13: 'EAN_13',
|
|
1343
|
-
8: 'EAN_8',
|
|
1344
|
-
4096: 'AZTEC',
|
|
1345
|
-
16: 'DATA_MATRIX'
|
|
1346
|
-
};
|
|
1347
|
-
const formatName = formatNames[b.format] || `UNKNOWN(${b.format})`;
|
|
1348
|
-
console.log(` [${idx + 1}] ${formatName}: ${b.rawValue.substring(0, 50)}`);
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
1224
|
}
|
|
1352
1225
|
|
|
1353
1226
|
// Update all debug overlay bounds continuously when debug mode is enabled
|
|
1354
1227
|
if (isDebugEnabled() && frameDimensions) {
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
1364
|
-
} else {
|
|
1365
|
-
scale = screen.width / frameDimensions.width;
|
|
1366
|
-
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
1367
|
-
}
|
|
1368
|
-
const scanLeft = (screen.width * 0.05 + offsetX) / scale;
|
|
1369
|
-
const scanTop = (screen.height * 0.36 + offsetY) / scale;
|
|
1370
|
-
const scanRight = (screen.width * 0.95 + offsetX) / scale;
|
|
1371
|
-
const scanBottom = (screen.height * 0.64 + offsetY) / scale;
|
|
1372
|
-
const isInsideScan = (x, y, w, h) => x >= scanLeft && y >= scanTop && x + w <= scanRight && y + h <= scanBottom;
|
|
1228
|
+
const {
|
|
1229
|
+
scale,
|
|
1230
|
+
offsetX,
|
|
1231
|
+
offsetY
|
|
1232
|
+
} = getFrameToScreenTransform(frameDimensions.width, frameDimensions.height);
|
|
1233
|
+
const {
|
|
1234
|
+
isInsideScan
|
|
1235
|
+
} = getScanAreaBounds(frameDimensions.width, frameDimensions.height);
|
|
1373
1236
|
|
|
1374
1237
|
// Update barcode bounds
|
|
1375
1238
|
if (barcodes.length > 0 && barcodes[0]) {
|
|
@@ -1383,16 +1246,7 @@ const IdentityDocumentCamera = ({
|
|
|
1383
1246
|
x: c.x * scale - offsetX,
|
|
1384
1247
|
y: c.y * scale - offsetY
|
|
1385
1248
|
}));
|
|
1386
|
-
|
|
1387
|
-
const dx = transformedCorners[1].x - transformedCorners[0].x;
|
|
1388
|
-
const dy = transformedCorners[1].y - transformedCorners[0].y;
|
|
1389
|
-
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1390
|
-
}
|
|
1391
|
-
if (isDebugEnabled()) {
|
|
1392
|
-
console.log('[Debug] Barcode detected:', {
|
|
1393
|
-
bbox,
|
|
1394
|
-
angle
|
|
1395
|
-
});
|
|
1249
|
+
angle = angleBetweenPoints(transformedCorners[0], transformedCorners[1]);
|
|
1396
1250
|
}
|
|
1397
1251
|
setBarcodeBounds({
|
|
1398
1252
|
x: bbox.left * scale - offsetX,
|
|
@@ -1453,51 +1307,20 @@ const IdentityDocumentCamera = ({
|
|
|
1453
1307
|
|
|
1454
1308
|
// Detect MRZ and signature text areas continuously
|
|
1455
1309
|
if (textBlocks.length > 0) {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
// More strict pattern: look for blocks with 8+ consecutive uppercase/numbers/< characters AND
|
|
1459
|
-
// must contain at least one '<' character (true MRZ characteristic)
|
|
1460
|
-
const mrzPattern = /[A-Z0-9<]{8,}.*</i;
|
|
1461
|
-
const bottomHalf = frame.height * 0.5; // Increased from 0.67 to catch more MRZ blocks
|
|
1462
|
-
|
|
1463
|
-
// Log bottom area blocks for debugging
|
|
1464
|
-
const bottomBlocks = textBlocks.filter(block => block.blockFrame && block.blockFrame.y > bottomHalf);
|
|
1465
|
-
if (bottomBlocks.length > 0) {
|
|
1466
|
-
console.log('[Debug] Bottom area blocks:', bottomBlocks.map(b => b.text.substring(0, 30)));
|
|
1467
|
-
}
|
|
1468
|
-
const mrzBlocks = textBlocks.filter(block => block.blockFrame && block.blockFrame.y > bottomHalf && mrzPattern.test(block.text));
|
|
1469
|
-
console.log('[Debug] MRZ blocks found:', mrzBlocks.length);
|
|
1310
|
+
const bottomHalf = frame.height * 0.5;
|
|
1311
|
+
const mrzBlocks = textBlocks.filter(block => block.blockFrame && block.blockFrame.y > bottomHalf && MRZ_BLOCK_PATTERN.test(block.text));
|
|
1470
1312
|
if (mrzBlocks.length > 0) {
|
|
1471
|
-
// Extract MRZ-only text from detected blocks (sorted by Y position for correct line order)
|
|
1472
1313
|
const sortedMrzBlocks = [...mrzBlocks].sort((a, b) => (a.blockFrame?.y || 0) - (b.blockFrame?.y || 0));
|
|
1473
1314
|
scannedText.mrzOnlyText = sortedMrzBlocks.map(b => b.text).join('\n');
|
|
1474
|
-
if (isDebugEnabled()) {
|
|
1475
|
-
console.log('[MRZ Extraction] Using only MRZ blocks:', scannedText.mrzOnlyText.substring(0, 100));
|
|
1476
|
-
}
|
|
1477
1315
|
const minX = Math.min(...mrzBlocks.map(b => b.blockFrame.x));
|
|
1478
1316
|
const minY = Math.min(...mrzBlocks.map(b => b.blockFrame.y));
|
|
1479
1317
|
const maxX = Math.max(...mrzBlocks.map(b => b.blockFrame.x + b.blockFrame.width));
|
|
1480
1318
|
const maxY = Math.max(...mrzBlocks.map(b => b.blockFrame.y + b.blockFrame.height));
|
|
1481
|
-
|
|
1482
|
-
// Collect all corner points from MRZ blocks
|
|
1483
1319
|
const allCornerPoints = mrzBlocks.flatMap(b => b.cornerPoints || []).map(c => ({
|
|
1484
1320
|
x: c.x * scale - offsetX,
|
|
1485
1321
|
y: c.y * scale - offsetY
|
|
1486
1322
|
}));
|
|
1487
|
-
|
|
1488
|
-
if (allCornerPoints.length >= 2) {
|
|
1489
|
-
// Calculate angle from first two points
|
|
1490
|
-
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
1491
|
-
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
1492
|
-
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1493
|
-
}
|
|
1494
|
-
console.log('[Debug] MRZ bounds:', {
|
|
1495
|
-
minX,
|
|
1496
|
-
minY,
|
|
1497
|
-
maxX,
|
|
1498
|
-
maxY,
|
|
1499
|
-
angle
|
|
1500
|
-
});
|
|
1323
|
+
const angle = allCornerPoints.length >= 2 ? angleBetweenPoints(allCornerPoints[0], allCornerPoints[1]) : 0;
|
|
1501
1324
|
setMrzBounds({
|
|
1502
1325
|
x: minX * scale - offsetX,
|
|
1503
1326
|
y: minY * scale - offsetY,
|
|
@@ -1509,31 +1332,17 @@ const IdentityDocumentCamera = ({
|
|
|
1509
1332
|
} else {
|
|
1510
1333
|
setMrzBounds(null);
|
|
1511
1334
|
}
|
|
1512
|
-
|
|
1513
|
-
// Detect signature area
|
|
1514
|
-
const signaturePattern = /signature|imza|İmza/i;
|
|
1515
|
-
const signatureBlocks = textBlocks.filter(block => block.blockFrame && signaturePattern.test(block.text));
|
|
1516
|
-
if (textBlocks.length > 0 && signatureBlocks.length === 0) {
|
|
1517
|
-
console.log(`[Signature Debug] No signature blocks found. All blocks (${textBlocks.length}):`, textBlocks.map(b => b.text).join(' | '));
|
|
1518
|
-
}
|
|
1335
|
+
const signatureBlocks = textBlocks.filter(block => block.blockFrame && SIGNATURE_TEXT_REGEX.test(block.text));
|
|
1519
1336
|
if (signatureBlocks.length > 0) {
|
|
1520
1337
|
const minX = Math.min(...signatureBlocks.map(b => b.blockFrame.x));
|
|
1521
1338
|
const minY = Math.min(...signatureBlocks.map(b => b.blockFrame.y));
|
|
1522
1339
|
const maxX = Math.max(...signatureBlocks.map(b => b.blockFrame.x + b.blockFrame.width));
|
|
1523
1340
|
const maxY = Math.max(...signatureBlocks.map(b => b.blockFrame.y + b.blockFrame.height));
|
|
1524
|
-
|
|
1525
|
-
// Collect all corner points from signature blocks
|
|
1526
1341
|
const allCornerPoints = signatureBlocks.flatMap(b => b.cornerPoints || []).map(c => ({
|
|
1527
1342
|
x: c.x * scale - offsetX,
|
|
1528
1343
|
y: c.y * scale - offsetY
|
|
1529
1344
|
}));
|
|
1530
|
-
|
|
1531
|
-
if (allCornerPoints.length >= 2) {
|
|
1532
|
-
// Calculate angle from first two points
|
|
1533
|
-
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
1534
|
-
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
1535
|
-
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1536
|
-
}
|
|
1345
|
+
const angle = allCornerPoints.length >= 2 ? angleBetweenPoints(allCornerPoints[0], allCornerPoints[1]) : 0;
|
|
1537
1346
|
setSignatureBounds({
|
|
1538
1347
|
x: minX * scale - offsetX,
|
|
1539
1348
|
y: minY * scale - offsetY,
|
|
@@ -1545,67 +1354,9 @@ const IdentityDocumentCamera = ({
|
|
|
1545
1354
|
} else {
|
|
1546
1355
|
setSignatureBounds(null);
|
|
1547
1356
|
}
|
|
1548
|
-
|
|
1549
|
-
// Check if all required elements are detected based on document type
|
|
1550
|
-
if (nextStep === 'SCAN_ID_BACK') {
|
|
1551
|
-
// ID Back: MRZ + barcode (barcode optional but preferred)
|
|
1552
|
-
const hasMRZ = mrzBlocks.length > 0;
|
|
1553
|
-
const hasBarcode = barcodes.length > 0 || cachedBarcode.current !== null;
|
|
1554
|
-
const allPresent = hasMRZ && hasBarcode;
|
|
1555
|
-
setAllElementsDetected(allPresent);
|
|
1556
|
-
|
|
1557
|
-
// Don't block based on bounds - allow elements even if slightly outside
|
|
1558
|
-
setElementsOutsideScanArea([]);
|
|
1559
|
-
if (!allPresent) {
|
|
1560
|
-
const missing = [];
|
|
1561
|
-
if (!hasMRZ) missing.push('MRZ');
|
|
1562
|
-
if (!hasBarcode) missing.push('Barcode');
|
|
1563
|
-
console.log(`[Frame Check] Missing elements: ${missing.join(', ')}`);
|
|
1564
|
-
} else {
|
|
1565
|
-
console.log('[Frame Check] ✓ All elements detected in frame');
|
|
1566
|
-
}
|
|
1567
|
-
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1568
|
-
// Check if it's passport (has MRZ) or ID front (no MRZ)
|
|
1569
|
-
const hasMRZ = mrzBlocks.length > 0;
|
|
1570
|
-
const hasFace = detectedFaces.length > 0;
|
|
1571
|
-
const hasSignature = signatureBlocks.length > 0;
|
|
1572
|
-
|
|
1573
|
-
// Don't block based on bounds - allow elements even if slightly outside
|
|
1574
|
-
setElementsOutsideScanArea([]);
|
|
1575
|
-
let allPresent = false;
|
|
1576
|
-
if (hasMRZ) {
|
|
1577
|
-
// Passport: face + MRZ
|
|
1578
|
-
allPresent = hasFace && hasMRZ;
|
|
1579
|
-
if (!allPresent) {
|
|
1580
|
-
const missing = [];
|
|
1581
|
-
if (!hasFace) missing.push('Face');
|
|
1582
|
-
if (!hasMRZ) missing.push('MRZ');
|
|
1583
|
-
console.log(`[Frame Check] Passport - Missing elements: ${missing.join(', ')}`);
|
|
1584
|
-
} else {
|
|
1585
|
-
console.log('[Frame Check] ✓ Passport - All elements detected (face + MRZ)');
|
|
1586
|
-
}
|
|
1587
|
-
} else {
|
|
1588
|
-
// ID Front: face + signature
|
|
1589
|
-
allPresent = hasFace && hasSignature;
|
|
1590
|
-
if (!allPresent) {
|
|
1591
|
-
const missing = [];
|
|
1592
|
-
if (!hasFace) missing.push('Face');
|
|
1593
|
-
if (!hasSignature) missing.push('Signature');
|
|
1594
|
-
console.log(`[Frame Check] ID Front - Missing elements: ${missing.join(', ')}`);
|
|
1595
|
-
} else {
|
|
1596
|
-
console.log('[Frame Check] ✓ ID Front - All elements detected (face + signature)');
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
setAllElementsDetected(allPresent);
|
|
1600
|
-
} else {
|
|
1601
|
-
setAllElementsDetected(false);
|
|
1602
|
-
setElementsOutsideScanArea([]);
|
|
1603
|
-
}
|
|
1604
1357
|
} else {
|
|
1605
1358
|
setMrzBounds(null);
|
|
1606
1359
|
setSignatureBounds(null);
|
|
1607
|
-
setAllElementsDetected(false);
|
|
1608
|
-
setElementsOutsideScanArea([]);
|
|
1609
1360
|
}
|
|
1610
1361
|
} else if (!isDebugEnabled()) {
|
|
1611
1362
|
// Clear all bounds when debug mode is disabled
|
|
@@ -1616,39 +1367,28 @@ const IdentityDocumentCamera = ({
|
|
|
1616
1367
|
setSignatureBounds(null);
|
|
1617
1368
|
}
|
|
1618
1369
|
|
|
1619
|
-
// Update allElementsDetected for status text display
|
|
1370
|
+
// Update allElementsDetected for status text display
|
|
1620
1371
|
if (nextStep === 'SCAN_ID_BACK') {
|
|
1621
|
-
const hasMRZ = textBlocks.some(b =>
|
|
1372
|
+
const hasMRZ = textBlocks.some(b => MRZ_BLOCK_PATTERN.test(b.text));
|
|
1622
1373
|
const hasBarcode = barcodes.length > 0 || cachedBarcode.current !== null;
|
|
1623
1374
|
setAllElementsDetected(hasMRZ && hasBarcode);
|
|
1624
1375
|
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1625
|
-
const hasMRZ = textBlocks.some(b =>
|
|
1376
|
+
const hasMRZ = textBlocks.some(b => MRZ_BLOCK_PATTERN.test(b.text));
|
|
1626
1377
|
const hasFace = detectedFaces.length > 0;
|
|
1627
|
-
const hasSignature = textBlocks.some(b =>
|
|
1378
|
+
const hasSignature = textBlocks.some(b => SIGNATURE_TEXT_REGEX.test(b.text));
|
|
1628
1379
|
setAllElementsDetected(hasMRZ ? hasFace && hasMRZ : hasFace && hasSignature);
|
|
1629
1380
|
} else {
|
|
1630
1381
|
setAllElementsDetected(false);
|
|
1631
1382
|
}
|
|
1632
1383
|
|
|
1633
1384
|
// Check if detected elements are inside the scan area
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
scanScale = scanScreen.height / frame.height;
|
|
1642
|
-
scanOffsetX = (frame.width * scanScale - scanScreen.width) / 2;
|
|
1643
|
-
} else {
|
|
1644
|
-
scanScale = scanScreen.width / frame.width;
|
|
1645
|
-
scanOffsetY = (frame.height * scanScale - scanScreen.height) / 2;
|
|
1646
|
-
}
|
|
1647
|
-
const scanLeft = (scanScreen.width * 0.05 + scanOffsetX) / scanScale;
|
|
1648
|
-
const scanTop = (scanScreen.height * 0.36 + scanOffsetY) / scanScale;
|
|
1649
|
-
const scanRight = (scanScreen.width * 0.95 + scanOffsetX) / scanScale;
|
|
1650
|
-
const scanBottom = (scanScreen.height * 0.64 + scanOffsetY) / scanScale;
|
|
1651
|
-
const isInsideScan = (x, y, w, h) => x >= scanLeft && y >= scanTop && x + w <= scanRight && y + h <= scanBottom;
|
|
1385
|
+
const {
|
|
1386
|
+
scanLeft,
|
|
1387
|
+
scanTop,
|
|
1388
|
+
scanRight,
|
|
1389
|
+
scanBottom,
|
|
1390
|
+
isInsideScan
|
|
1391
|
+
} = getScanAreaBounds(frame.width, frame.height);
|
|
1652
1392
|
const outsideElements = [];
|
|
1653
1393
|
|
|
1654
1394
|
// Collect all detected element bounds
|
|
@@ -1678,8 +1418,8 @@ const IdentityDocumentCamera = ({
|
|
|
1678
1418
|
y2: bf.y + bf.height
|
|
1679
1419
|
});
|
|
1680
1420
|
}
|
|
1681
|
-
const isMRZ =
|
|
1682
|
-
const isSignature =
|
|
1421
|
+
const isMRZ = MRZ_BLOCK_PATTERN.test(block.text);
|
|
1422
|
+
const isSignature = SIGNATURE_TEXT_REGEX.test(block.text);
|
|
1683
1423
|
if ((isMRZ || isSignature) && !isInsideScan(bf.x, bf.y, bf.width, bf.height)) {
|
|
1684
1424
|
outsideElements.push('text');
|
|
1685
1425
|
}
|
|
@@ -1803,345 +1543,6 @@ const IdentityDocumentCamera = ({
|
|
|
1803
1543
|
onFrameAvailable: handleFrame,
|
|
1804
1544
|
onCameraReady: handleCameraReady,
|
|
1805
1545
|
onCameraError: handleCameraError
|
|
1806
|
-
}), isDebugEnabled() && documentPlaneBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsxs(_Fragment, {
|
|
1807
|
-
children: [!!documentPlaneBounds.cropPadding && /*#__PURE__*/_jsx(View, {
|
|
1808
|
-
style: {
|
|
1809
|
-
position: 'absolute',
|
|
1810
|
-
left: documentPlaneBounds.x - documentPlaneBounds.cropPadding,
|
|
1811
|
-
top: documentPlaneBounds.y - documentPlaneBounds.cropPadding,
|
|
1812
|
-
width: documentPlaneBounds.width + 2 * documentPlaneBounds.cropPadding,
|
|
1813
|
-
height: documentPlaneBounds.height + 2 * documentPlaneBounds.cropPadding,
|
|
1814
|
-
borderWidth: 2,
|
|
1815
|
-
borderColor: 'rgba(76, 175, 80, 0.5)',
|
|
1816
|
-
borderStyle: 'dashed',
|
|
1817
|
-
borderRadius: 8,
|
|
1818
|
-
backgroundColor: 'transparent',
|
|
1819
|
-
transform: [{
|
|
1820
|
-
rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg`
|
|
1821
|
-
}],
|
|
1822
|
-
transformOrigin: 'center'
|
|
1823
|
-
}
|
|
1824
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
1825
|
-
style: {
|
|
1826
|
-
position: 'absolute',
|
|
1827
|
-
left: documentPlaneBounds.x,
|
|
1828
|
-
top: documentPlaneBounds.y,
|
|
1829
|
-
width: documentPlaneBounds.width,
|
|
1830
|
-
height: documentPlaneBounds.height,
|
|
1831
|
-
borderWidth: 3,
|
|
1832
|
-
borderColor: '#4CAF50',
|
|
1833
|
-
borderRadius: 8,
|
|
1834
|
-
backgroundColor: 'transparent',
|
|
1835
|
-
transform: [{
|
|
1836
|
-
rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg`
|
|
1837
|
-
}],
|
|
1838
|
-
transformOrigin: 'center'
|
|
1839
|
-
},
|
|
1840
|
-
children: !!documentPlaneBounds.rollAngle && Math.abs(documentPlaneBounds.rollAngle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1841
|
-
style: {
|
|
1842
|
-
position: 'absolute',
|
|
1843
|
-
top: -20,
|
|
1844
|
-
left: 0,
|
|
1845
|
-
color: '#4CAF50',
|
|
1846
|
-
fontSize: 10,
|
|
1847
|
-
fontWeight: 'bold',
|
|
1848
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1849
|
-
paddingHorizontal: 4,
|
|
1850
|
-
paddingVertical: 2,
|
|
1851
|
-
borderRadius: 2
|
|
1852
|
-
},
|
|
1853
|
-
children: [documentPlaneBounds.rollAngle.toFixed(1), "\xB0"]
|
|
1854
|
-
})
|
|
1855
|
-
})]
|
|
1856
|
-
}), isDebugEnabled() && secondaryFaceBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsxs(_Fragment, {
|
|
1857
|
-
children: [!!secondaryFaceBounds.cropPadding && /*#__PURE__*/_jsx(View, {
|
|
1858
|
-
style: {
|
|
1859
|
-
position: 'absolute',
|
|
1860
|
-
left: secondaryFaceBounds.x - secondaryFaceBounds.cropPadding,
|
|
1861
|
-
top: secondaryFaceBounds.y - secondaryFaceBounds.cropPadding,
|
|
1862
|
-
width: secondaryFaceBounds.width + 2 * secondaryFaceBounds.cropPadding,
|
|
1863
|
-
height: secondaryFaceBounds.height + 2 * secondaryFaceBounds.cropPadding,
|
|
1864
|
-
borderWidth: 2,
|
|
1865
|
-
borderColor: 'rgba(33, 150, 243, 0.5)',
|
|
1866
|
-
borderStyle: 'dashed',
|
|
1867
|
-
borderRadius: 8,
|
|
1868
|
-
backgroundColor: 'transparent',
|
|
1869
|
-
transform: [{
|
|
1870
|
-
rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg`
|
|
1871
|
-
}],
|
|
1872
|
-
transformOrigin: 'center'
|
|
1873
|
-
}
|
|
1874
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
1875
|
-
style: {
|
|
1876
|
-
position: 'absolute',
|
|
1877
|
-
left: secondaryFaceBounds.x,
|
|
1878
|
-
top: secondaryFaceBounds.y,
|
|
1879
|
-
width: secondaryFaceBounds.width,
|
|
1880
|
-
height: secondaryFaceBounds.height,
|
|
1881
|
-
borderWidth: 3,
|
|
1882
|
-
borderColor: '#2196F3',
|
|
1883
|
-
borderRadius: 8,
|
|
1884
|
-
backgroundColor: 'transparent',
|
|
1885
|
-
transform: [{
|
|
1886
|
-
rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg`
|
|
1887
|
-
}],
|
|
1888
|
-
transformOrigin: 'center'
|
|
1889
|
-
},
|
|
1890
|
-
children: !!secondaryFaceBounds.rollAngle && Math.abs(secondaryFaceBounds.rollAngle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1891
|
-
style: {
|
|
1892
|
-
position: 'absolute',
|
|
1893
|
-
top: -20,
|
|
1894
|
-
left: 0,
|
|
1895
|
-
color: '#2196F3',
|
|
1896
|
-
fontSize: 10,
|
|
1897
|
-
fontWeight: 'bold',
|
|
1898
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1899
|
-
paddingHorizontal: 4,
|
|
1900
|
-
paddingVertical: 2,
|
|
1901
|
-
borderRadius: 2
|
|
1902
|
-
},
|
|
1903
|
-
children: [secondaryFaceBounds.rollAngle.toFixed(1), "\xB0"]
|
|
1904
|
-
})
|
|
1905
|
-
})]
|
|
1906
|
-
}), isDebugEnabled() && barcodeBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(_Fragment, {
|
|
1907
|
-
children: barcodeBounds.corners && barcodeBounds.corners.length >= 4 ?
|
|
1908
|
-
/*#__PURE__*/
|
|
1909
|
-
// Render using corner points for precise rotated border
|
|
1910
|
-
_jsxs(_Fragment, {
|
|
1911
|
-
children: [[0, 1, 2, 3].map(i => {
|
|
1912
|
-
const start = barcodeBounds.corners[i];
|
|
1913
|
-
const end = barcodeBounds.corners[(i + 1) % 4];
|
|
1914
|
-
const dx = end.x - start.x;
|
|
1915
|
-
const dy = end.y - start.y;
|
|
1916
|
-
const length = Math.sqrt(dx * dx + dy * dy);
|
|
1917
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1918
|
-
return /*#__PURE__*/_jsx(View, {
|
|
1919
|
-
style: {
|
|
1920
|
-
position: 'absolute',
|
|
1921
|
-
left: start.x,
|
|
1922
|
-
top: start.y,
|
|
1923
|
-
width: length,
|
|
1924
|
-
height: 3,
|
|
1925
|
-
backgroundColor: '#FF9800',
|
|
1926
|
-
transform: [{
|
|
1927
|
-
rotate: `${angle}deg`
|
|
1928
|
-
}],
|
|
1929
|
-
transformOrigin: 'top left'
|
|
1930
|
-
}
|
|
1931
|
-
}, i);
|
|
1932
|
-
}), barcodeBounds.corners.map((corner, idx) => /*#__PURE__*/_jsx(View, {
|
|
1933
|
-
style: {
|
|
1934
|
-
position: 'absolute',
|
|
1935
|
-
left: corner.x - 4,
|
|
1936
|
-
top: corner.y - 4,
|
|
1937
|
-
width: 8,
|
|
1938
|
-
height: 8,
|
|
1939
|
-
borderRadius: 4,
|
|
1940
|
-
backgroundColor: '#FF9800'
|
|
1941
|
-
}
|
|
1942
|
-
}, `corner-${idx}`)), !!barcodeBounds.angle && Math.abs(barcodeBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1943
|
-
style: {
|
|
1944
|
-
position: 'absolute',
|
|
1945
|
-
left: barcodeBounds.x,
|
|
1946
|
-
top: barcodeBounds.y - 20,
|
|
1947
|
-
color: '#FF9800',
|
|
1948
|
-
fontSize: 10,
|
|
1949
|
-
fontWeight: 'bold',
|
|
1950
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1951
|
-
paddingHorizontal: 4,
|
|
1952
|
-
paddingVertical: 2,
|
|
1953
|
-
borderRadius: 2
|
|
1954
|
-
},
|
|
1955
|
-
children: [barcodeBounds.angle.toFixed(1), "\xB0"]
|
|
1956
|
-
})]
|
|
1957
|
-
}) :
|
|
1958
|
-
/*#__PURE__*/
|
|
1959
|
-
// Fallback to rotated rectangle if corners not available
|
|
1960
|
-
_jsx(View, {
|
|
1961
|
-
style: {
|
|
1962
|
-
position: 'absolute',
|
|
1963
|
-
left: barcodeBounds.x + barcodeBounds.width / 2,
|
|
1964
|
-
top: barcodeBounds.y + barcodeBounds.height / 2,
|
|
1965
|
-
width: barcodeBounds.width,
|
|
1966
|
-
height: barcodeBounds.height,
|
|
1967
|
-
marginLeft: -barcodeBounds.width / 2,
|
|
1968
|
-
marginTop: -barcodeBounds.height / 2,
|
|
1969
|
-
borderWidth: 3,
|
|
1970
|
-
borderColor: '#FF9800',
|
|
1971
|
-
borderRadius: 8,
|
|
1972
|
-
backgroundColor: 'transparent',
|
|
1973
|
-
transform: [{
|
|
1974
|
-
rotate: `${barcodeBounds.angle || 0}deg`
|
|
1975
|
-
}]
|
|
1976
|
-
},
|
|
1977
|
-
children: !!barcodeBounds.angle && Math.abs(barcodeBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1978
|
-
style: {
|
|
1979
|
-
position: 'absolute',
|
|
1980
|
-
top: -20,
|
|
1981
|
-
left: 0,
|
|
1982
|
-
color: '#FF9800',
|
|
1983
|
-
fontSize: 10,
|
|
1984
|
-
fontWeight: 'bold',
|
|
1985
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1986
|
-
paddingHorizontal: 4,
|
|
1987
|
-
paddingVertical: 2,
|
|
1988
|
-
borderRadius: 2
|
|
1989
|
-
},
|
|
1990
|
-
children: [barcodeBounds.angle.toFixed(1), "\xB0"]
|
|
1991
|
-
})
|
|
1992
|
-
})
|
|
1993
|
-
}), isDebugEnabled() && mrzBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(_Fragment, {
|
|
1994
|
-
children: mrzBounds.corners && mrzBounds.corners.length >= 2 ?
|
|
1995
|
-
/*#__PURE__*/
|
|
1996
|
-
// Render using corner points for precise rotated border
|
|
1997
|
-
_jsxs(_Fragment, {
|
|
1998
|
-
children: [mrzBounds.corners.map((corner, idx) => {
|
|
1999
|
-
const nextCorner = mrzBounds.corners[(idx + 1) % mrzBounds.corners.length];
|
|
2000
|
-
const dx = nextCorner.x - corner.x;
|
|
2001
|
-
const dy = nextCorner.y - corner.y;
|
|
2002
|
-
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2003
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2004
|
-
return /*#__PURE__*/_jsx(View, {
|
|
2005
|
-
style: {
|
|
2006
|
-
position: 'absolute',
|
|
2007
|
-
left: corner.x,
|
|
2008
|
-
top: corner.y,
|
|
2009
|
-
width: length,
|
|
2010
|
-
height: 3,
|
|
2011
|
-
backgroundColor: '#9C27B0',
|
|
2012
|
-
transform: [{
|
|
2013
|
-
rotate: `${angle}deg`
|
|
2014
|
-
}],
|
|
2015
|
-
transformOrigin: 'top left'
|
|
2016
|
-
}
|
|
2017
|
-
}, idx);
|
|
2018
|
-
}), !!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
2019
|
-
style: {
|
|
2020
|
-
position: 'absolute',
|
|
2021
|
-
left: mrzBounds.x,
|
|
2022
|
-
top: mrzBounds.y - 20,
|
|
2023
|
-
color: '#9C27B0',
|
|
2024
|
-
fontSize: 10,
|
|
2025
|
-
fontWeight: 'bold',
|
|
2026
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2027
|
-
paddingHorizontal: 4,
|
|
2028
|
-
paddingVertical: 2,
|
|
2029
|
-
borderRadius: 2
|
|
2030
|
-
},
|
|
2031
|
-
children: [mrzBounds.angle.toFixed(1), "\xB0"]
|
|
2032
|
-
})]
|
|
2033
|
-
}) :
|
|
2034
|
-
/*#__PURE__*/
|
|
2035
|
-
// Fallback to rotated rectangle if corners not available
|
|
2036
|
-
_jsx(View, {
|
|
2037
|
-
style: {
|
|
2038
|
-
position: 'absolute',
|
|
2039
|
-
left: mrzBounds.x + mrzBounds.width / 2,
|
|
2040
|
-
top: mrzBounds.y + mrzBounds.height / 2,
|
|
2041
|
-
width: mrzBounds.width,
|
|
2042
|
-
height: mrzBounds.height,
|
|
2043
|
-
marginLeft: -mrzBounds.width / 2,
|
|
2044
|
-
marginTop: -mrzBounds.height / 2,
|
|
2045
|
-
borderWidth: 3,
|
|
2046
|
-
borderColor: '#9C27B0',
|
|
2047
|
-
borderRadius: 8,
|
|
2048
|
-
backgroundColor: 'transparent',
|
|
2049
|
-
transform: [{
|
|
2050
|
-
rotate: `${mrzBounds.angle || 0}deg`
|
|
2051
|
-
}]
|
|
2052
|
-
},
|
|
2053
|
-
children: !!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
2054
|
-
style: {
|
|
2055
|
-
position: 'absolute',
|
|
2056
|
-
top: -20,
|
|
2057
|
-
left: 0,
|
|
2058
|
-
color: '#9C27B0',
|
|
2059
|
-
fontSize: 10,
|
|
2060
|
-
fontWeight: 'bold',
|
|
2061
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2062
|
-
paddingHorizontal: 4,
|
|
2063
|
-
paddingVertical: 2,
|
|
2064
|
-
borderRadius: 2
|
|
2065
|
-
},
|
|
2066
|
-
children: [mrzBounds.angle.toFixed(1), "\xB0"]
|
|
2067
|
-
})
|
|
2068
|
-
})
|
|
2069
|
-
}), isDebugEnabled() && signatureBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(_Fragment, {
|
|
2070
|
-
children: signatureBounds.corners && signatureBounds.corners.length >= 2 ?
|
|
2071
|
-
/*#__PURE__*/
|
|
2072
|
-
// Render using corner points for precise rotated border
|
|
2073
|
-
_jsxs(_Fragment, {
|
|
2074
|
-
children: [signatureBounds.corners.map((corner, idx) => {
|
|
2075
|
-
const nextCorner = signatureBounds.corners[(idx + 1) % signatureBounds.corners.length];
|
|
2076
|
-
const dx = nextCorner.x - corner.x;
|
|
2077
|
-
const dy = nextCorner.y - corner.y;
|
|
2078
|
-
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2079
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2080
|
-
return /*#__PURE__*/_jsx(View, {
|
|
2081
|
-
style: {
|
|
2082
|
-
position: 'absolute',
|
|
2083
|
-
left: corner.x,
|
|
2084
|
-
top: corner.y,
|
|
2085
|
-
width: length,
|
|
2086
|
-
height: 3,
|
|
2087
|
-
backgroundColor: '#00BCD4',
|
|
2088
|
-
transform: [{
|
|
2089
|
-
rotate: `${angle}deg`
|
|
2090
|
-
}],
|
|
2091
|
-
transformOrigin: 'top left'
|
|
2092
|
-
}
|
|
2093
|
-
}, idx);
|
|
2094
|
-
}), !!signatureBounds.angle && Math.abs(signatureBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
2095
|
-
style: {
|
|
2096
|
-
position: 'absolute',
|
|
2097
|
-
left: signatureBounds.x,
|
|
2098
|
-
top: signatureBounds.y - 20,
|
|
2099
|
-
color: '#00BCD4',
|
|
2100
|
-
fontSize: 10,
|
|
2101
|
-
fontWeight: 'bold',
|
|
2102
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2103
|
-
paddingHorizontal: 4,
|
|
2104
|
-
paddingVertical: 2,
|
|
2105
|
-
borderRadius: 2
|
|
2106
|
-
},
|
|
2107
|
-
children: [signatureBounds.angle.toFixed(1), "\xB0"]
|
|
2108
|
-
})]
|
|
2109
|
-
}) :
|
|
2110
|
-
/*#__PURE__*/
|
|
2111
|
-
// Fallback to rotated rectangle if corners not available
|
|
2112
|
-
_jsx(View, {
|
|
2113
|
-
style: {
|
|
2114
|
-
position: 'absolute',
|
|
2115
|
-
left: signatureBounds.x + signatureBounds.width / 2,
|
|
2116
|
-
top: signatureBounds.y + signatureBounds.height / 2,
|
|
2117
|
-
width: signatureBounds.width,
|
|
2118
|
-
height: signatureBounds.height,
|
|
2119
|
-
marginLeft: -signatureBounds.width / 2,
|
|
2120
|
-
marginTop: -signatureBounds.height / 2,
|
|
2121
|
-
borderWidth: 3,
|
|
2122
|
-
borderColor: '#00BCD4',
|
|
2123
|
-
borderRadius: 8,
|
|
2124
|
-
backgroundColor: 'transparent',
|
|
2125
|
-
transform: [{
|
|
2126
|
-
rotate: `${signatureBounds.angle || 0}deg`
|
|
2127
|
-
}]
|
|
2128
|
-
},
|
|
2129
|
-
children: !!signatureBounds.angle && Math.abs(signatureBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
2130
|
-
style: {
|
|
2131
|
-
position: 'absolute',
|
|
2132
|
-
top: -20,
|
|
2133
|
-
left: 0,
|
|
2134
|
-
color: '#00BCD4',
|
|
2135
|
-
fontSize: 10,
|
|
2136
|
-
fontWeight: 'bold',
|
|
2137
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2138
|
-
paddingHorizontal: 4,
|
|
2139
|
-
paddingVertical: 2,
|
|
2140
|
-
borderRadius: 2
|
|
2141
|
-
},
|
|
2142
|
-
children: [signatureBounds.angle.toFixed(1), "\xB0"]
|
|
2143
|
-
})
|
|
2144
|
-
})
|
|
2145
1546
|
}), /*#__PURE__*/_jsxs(View, {
|
|
2146
1547
|
style: [styles.topZone, {
|
|
2147
1548
|
paddingTop: insets.top
|
|
@@ -2158,174 +1559,35 @@ const IdentityDocumentCamera = ({
|
|
|
2158
1559
|
current: 3,
|
|
2159
1560
|
total: 3
|
|
2160
1561
|
})}` : ''
|
|
2161
|
-
}), /*#__PURE__*/_jsx(
|
|
1562
|
+
}), /*#__PURE__*/_jsx(AnimatedText, {
|
|
2162
1563
|
style: [styles.topZoneText,
|
|
2163
1564
|
// Priority order for coloring (later styles override earlier ones)
|
|
2164
1565
|
// 1. Success (green) - scan completed
|
|
2165
|
-
|
|
2166
|
-
// 2. Error (red) - wrong side
|
|
2167
|
-
status === 'INCORRECT' && styles.topZoneTextError,
|
|
1566
|
+
nextStep === 'COMPLETED' && styles.topZoneTextSuccess,
|
|
1567
|
+
// 2. Error (red) - wrong side - with flash opacity
|
|
1568
|
+
status === 'INCORRECT' && styles.topZoneTextError, status === 'INCORRECT' && {
|
|
1569
|
+
opacity: errorFlashAnim
|
|
1570
|
+
},
|
|
2168
1571
|
// 3. Warning (yellow) - quality issues
|
|
2169
1572
|
(isBrightnessLow || isFrameBlurry) && styles.topZoneTextWarning,
|
|
2170
1573
|
// 4. Scanning (green) - all elements detected AND inside scan area
|
|
2171
1574
|
status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
|
|
2172
1575
|
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
2173
1576
|
],
|
|
2174
|
-
children:
|
|
2175
|
-
: t('identityDocumentCamera.alignPhotoSide') : isBrightnessLow ? t('identityDocumentCamera.lowBrightness') : isFrameBlurry ? t('identityDocumentCamera.avoidBlur') : status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 ? nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.idCardBackDetected') : detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportDetected') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.idCardFrontDetected') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument') : elementsOutsideScanArea.length > 0 ? t('identityDocumentCamera.centerDocument') : (status === 'SCANNING' || status === 'SEARCHING') && !allElementsDetected ? nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.alignIDBack') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.alignPassport') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.alignIDFront') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : ''
|
|
1577
|
+
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t)
|
|
2176
1578
|
})]
|
|
2177
1579
|
}), /*#__PURE__*/_jsx(View, {
|
|
2178
1580
|
style: styles.leftZone
|
|
2179
1581
|
}), /*#__PURE__*/_jsx(View, {
|
|
2180
1582
|
style: styles.rightZone
|
|
2181
1583
|
}), /*#__PURE__*/_jsx(View, {
|
|
2182
|
-
style: styles.bottomZone
|
|
2183
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
2184
|
-
style: styles.debugImagesRow,
|
|
2185
|
-
children: [isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2186
|
-
style: styles.imageContainer,
|
|
2187
|
-
children: [currentFaceImage ? /*#__PURE__*/_jsx(Image, {
|
|
2188
|
-
source: {
|
|
2189
|
-
uri: `data:image/jpeg;base64,${currentFaceImage}`
|
|
2190
|
-
},
|
|
2191
|
-
style: styles.faceImage
|
|
2192
|
-
}) : /*#__PURE__*/_jsx(View, {
|
|
2193
|
-
style: [styles.faceImage, {
|
|
2194
|
-
backgroundColor: '#333',
|
|
2195
|
-
justifyContent: 'center'
|
|
2196
|
-
}],
|
|
2197
|
-
children: /*#__PURE__*/_jsx(TextView, {
|
|
2198
|
-
style: {
|
|
2199
|
-
color: '#666',
|
|
2200
|
-
fontSize: 10,
|
|
2201
|
-
textAlign: 'center'
|
|
2202
|
-
},
|
|
2203
|
-
children: "Waiting..."
|
|
2204
|
-
})
|
|
2205
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2206
|
-
style: [styles.imageContainerText, currentFaceImage && {
|
|
2207
|
-
color: '#4CAF50'
|
|
2208
|
-
}],
|
|
2209
|
-
children: `${currentFaceImage ? '✓ ' : ''}Face`
|
|
2210
|
-
})]
|
|
2211
|
-
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2212
|
-
style: styles.imageContainer,
|
|
2213
|
-
children: [currentSecondaryFaceImage ? /*#__PURE__*/_jsx(Image, {
|
|
2214
|
-
source: {
|
|
2215
|
-
uri: `data:image/jpeg;base64,${currentSecondaryFaceImage}`
|
|
2216
|
-
},
|
|
2217
|
-
style: styles.faceImage
|
|
2218
|
-
}) : /*#__PURE__*/_jsx(View, {
|
|
2219
|
-
style: [styles.faceImage, {
|
|
2220
|
-
backgroundColor: '#333',
|
|
2221
|
-
justifyContent: 'center'
|
|
2222
|
-
}],
|
|
2223
|
-
children: /*#__PURE__*/_jsx(TextView, {
|
|
2224
|
-
style: {
|
|
2225
|
-
color: '#666',
|
|
2226
|
-
fontSize: 10,
|
|
2227
|
-
textAlign: 'center'
|
|
2228
|
-
},
|
|
2229
|
-
children: "Waiting..."
|
|
2230
|
-
})
|
|
2231
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2232
|
-
style: [styles.imageContainerText, currentSecondaryFaceImage && {
|
|
2233
|
-
color: '#4CAF50'
|
|
2234
|
-
}],
|
|
2235
|
-
children: `${currentSecondaryFaceImage ? '✓ ' : ''}2nd Face`
|
|
2236
|
-
})]
|
|
2237
|
-
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2238
|
-
style: styles.imageContainer,
|
|
2239
|
-
children: [currentHologramImage ? /*#__PURE__*/_jsx(Image, {
|
|
2240
|
-
source: {
|
|
2241
|
-
uri: `data:image/jpeg;base64,${currentHologramImage}`
|
|
2242
|
-
},
|
|
2243
|
-
style: styles.faceImage
|
|
2244
|
-
}) : latestHologramFaceImage && hologramImageCount > 0 ? /*#__PURE__*/_jsxs(View, {
|
|
2245
|
-
style: {
|
|
2246
|
-
position: 'relative'
|
|
2247
|
-
},
|
|
2248
|
-
children: [/*#__PURE__*/_jsx(Image, {
|
|
2249
|
-
source: {
|
|
2250
|
-
uri: `data:image/jpeg;base64,${latestHologramFaceImage}`
|
|
2251
|
-
},
|
|
2252
|
-
style: [styles.faceImage, {
|
|
2253
|
-
opacity: 0.7
|
|
2254
|
-
}]
|
|
2255
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
2256
|
-
style: {
|
|
2257
|
-
position: 'absolute',
|
|
2258
|
-
bottom: 0,
|
|
2259
|
-
left: 0,
|
|
2260
|
-
right: 0,
|
|
2261
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2262
|
-
padding: 2
|
|
2263
|
-
},
|
|
2264
|
-
children: /*#__PURE__*/_jsxs(TextView, {
|
|
2265
|
-
style: {
|
|
2266
|
-
color: '#FFA500',
|
|
2267
|
-
fontSize: 8,
|
|
2268
|
-
textAlign: 'center',
|
|
2269
|
-
fontWeight: 'bold'
|
|
2270
|
-
},
|
|
2271
|
-
children: [hologramImageCount, "/", HOLOGRAM_IMAGE_COUNT]
|
|
2272
|
-
})
|
|
2273
|
-
})]
|
|
2274
|
-
}) : /*#__PURE__*/_jsx(View, {
|
|
2275
|
-
style: [styles.faceImage, {
|
|
2276
|
-
backgroundColor: '#333',
|
|
2277
|
-
justifyContent: 'center'
|
|
2278
|
-
}],
|
|
2279
|
-
children: /*#__PURE__*/_jsx(TextView, {
|
|
2280
|
-
style: {
|
|
2281
|
-
color: '#666',
|
|
2282
|
-
fontSize: 10,
|
|
2283
|
-
textAlign: 'center'
|
|
2284
|
-
},
|
|
2285
|
-
children: "Waiting..."
|
|
2286
|
-
})
|
|
2287
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2288
|
-
style: [styles.imageContainerText, currentHologramImage && {
|
|
2289
|
-
color: '#4CAF50'
|
|
2290
|
-
}, latestHologramFaceImage && !currentHologramImage && {
|
|
2291
|
-
color: '#FFA500'
|
|
2292
|
-
}],
|
|
2293
|
-
children: `${currentHologramImage ? '✓ ' : latestHologramFaceImage ? '⏳ ' : ''}Hologram`
|
|
2294
|
-
})]
|
|
2295
|
-
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2296
|
-
style: styles.imageContainer,
|
|
2297
|
-
children: [_currentHologramMaskImage ? /*#__PURE__*/_jsx(Image, {
|
|
2298
|
-
source: {
|
|
2299
|
-
uri: `data:image/jpeg;base64,${_currentHologramMaskImage}`
|
|
2300
|
-
},
|
|
2301
|
-
style: styles.faceImage
|
|
2302
|
-
}) : /*#__PURE__*/_jsx(View, {
|
|
2303
|
-
style: [styles.faceImage, {
|
|
2304
|
-
backgroundColor: '#333',
|
|
2305
|
-
justifyContent: 'center'
|
|
2306
|
-
}],
|
|
2307
|
-
children: /*#__PURE__*/_jsx(TextView, {
|
|
2308
|
-
style: {
|
|
2309
|
-
color: '#666',
|
|
2310
|
-
fontSize: 10,
|
|
2311
|
-
textAlign: 'center'
|
|
2312
|
-
},
|
|
2313
|
-
children: "Waiting..."
|
|
2314
|
-
})
|
|
2315
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2316
|
-
style: [styles.imageContainerText, _currentHologramMaskImage && {
|
|
2317
|
-
color: '#4CAF50'
|
|
2318
|
-
}],
|
|
2319
|
-
children: `${_currentHologramMaskImage ? '✓ ' : ''}Mask`
|
|
2320
|
-
})]
|
|
2321
|
-
})]
|
|
2322
|
-
})
|
|
1584
|
+
style: styles.bottomZone
|
|
2323
1585
|
}), /*#__PURE__*/_jsx(View, {
|
|
2324
1586
|
style: [styles.scanArea, {
|
|
2325
|
-
borderColor:
|
|
1587
|
+
borderColor: nextStep === 'COMPLETED' ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
|
|
2326
1588
|
borderWidth: status === 'SCANNING' ? 3 : 2
|
|
2327
1589
|
}],
|
|
2328
|
-
children: nextStep === 'COMPLETED'
|
|
1590
|
+
children: nextStep === 'COMPLETED' ? /*#__PURE__*/_jsx(LottieView, {
|
|
2329
1591
|
source: require('../../Shared/Animations/success.json'),
|
|
2330
1592
|
style: styles.animation,
|
|
2331
1593
|
loop: false,
|
|
@@ -2340,137 +1602,34 @@ const IdentityDocumentCamera = ({
|
|
|
2340
1602
|
style: styles.animation,
|
|
2341
1603
|
loop: true,
|
|
2342
1604
|
autoPlay: true
|
|
2343
|
-
}) : status === 'SCANNING' ? /*#__PURE__*/_jsx(LottieView, {
|
|
2344
|
-
source: require('../../Shared/Animations/scanning.json'),
|
|
2345
|
-
style: styles.animation,
|
|
2346
|
-
loop: true,
|
|
2347
|
-
autoPlay: true
|
|
2348
1605
|
}) : null
|
|
2349
|
-
}), isDebugEnabled() && /*#__PURE__*/_jsx(
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
fontSize: 11,
|
|
2372
|
-
fontWeight: 'bold',
|
|
2373
|
-
marginBottom: 6,
|
|
2374
|
-
textAlign: 'center'
|
|
2375
|
-
},
|
|
2376
|
-
children: "DEBUG MODE"
|
|
2377
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2378
|
-
style: {
|
|
2379
|
-
color: '#88D8B0',
|
|
2380
|
-
fontSize: 9,
|
|
2381
|
-
marginBottom: 2
|
|
2382
|
-
},
|
|
2383
|
-
children: `Step: ${nextStep}`
|
|
2384
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2385
|
-
style: {
|
|
2386
|
-
color: '#88D8B0',
|
|
2387
|
-
fontSize: 9,
|
|
2388
|
-
marginBottom: 2
|
|
2389
|
-
},
|
|
2390
|
-
children: `Status: ${status}`
|
|
2391
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2392
|
-
style: {
|
|
2393
|
-
color: '#88D8B0',
|
|
2394
|
-
fontSize: 9,
|
|
2395
|
-
marginBottom: 2
|
|
2396
|
-
},
|
|
2397
|
-
children: `Doc Type: ${detectedDocumentType}`
|
|
2398
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2399
|
-
style: {
|
|
2400
|
-
color: '#88D8B0',
|
|
2401
|
-
fontSize: 9,
|
|
2402
|
-
marginBottom: 2
|
|
2403
|
-
},
|
|
2404
|
-
children: `Brightness: ${isBrightnessLow ? '⚠️ LOW' : '✓'}`
|
|
2405
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2406
|
-
style: {
|
|
2407
|
-
color: '#88D8B0',
|
|
2408
|
-
fontSize: 9,
|
|
2409
|
-
marginBottom: 2
|
|
2410
|
-
},
|
|
2411
|
-
children: `Blur: ${isFrameBlurry ? '⚠️' : '✓'}`
|
|
2412
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2413
|
-
style: {
|
|
2414
|
-
color: '#88D8B0',
|
|
2415
|
-
fontSize: 9,
|
|
2416
|
-
marginBottom: 2
|
|
2417
|
-
},
|
|
2418
|
-
children: `Flash: ${isTorchOn ? '🔦' : '○'}`
|
|
2419
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2420
|
-
style: {
|
|
2421
|
-
color: '#88D8B0',
|
|
2422
|
-
fontSize: 9
|
|
2423
|
-
},
|
|
2424
|
-
children: `Face Detection: ${faceDetectionEnabled ? '✓' : '✗'}`
|
|
2425
|
-
})]
|
|
2426
|
-
})
|
|
2427
|
-
}), testMode && testModeData && /*#__PURE__*/_jsx(SafeAreaView, {
|
|
2428
|
-
style: {
|
|
2429
|
-
position: 'absolute',
|
|
2430
|
-
bottom: 0,
|
|
2431
|
-
left: 0,
|
|
2432
|
-
right: 0,
|
|
2433
|
-
maxHeight: '40%',
|
|
2434
|
-
backgroundColor: 'rgba(0, 0, 0, 0.95)',
|
|
2435
|
-
borderTopWidth: 2,
|
|
2436
|
-
borderTopColor: '#FFA500'
|
|
2437
|
-
},
|
|
2438
|
-
children: /*#__PURE__*/_jsx(ScrollView, {
|
|
2439
|
-
style: {
|
|
2440
|
-
flex: 1
|
|
2441
|
-
},
|
|
2442
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
2443
|
-
style: {
|
|
2444
|
-
padding: 10
|
|
2445
|
-
},
|
|
2446
|
-
children: [/*#__PURE__*/_jsx(TextView, {
|
|
2447
|
-
style: {
|
|
2448
|
-
color: '#FFA500',
|
|
2449
|
-
fontSize: 12,
|
|
2450
|
-
fontWeight: 'bold',
|
|
2451
|
-
marginBottom: 8,
|
|
2452
|
-
textAlign: 'center'
|
|
2453
|
-
},
|
|
2454
|
-
children: "MRZ Text Read"
|
|
2455
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
2456
|
-
style: {
|
|
2457
|
-
color: '#FFFFFF',
|
|
2458
|
-
fontSize: 9,
|
|
2459
|
-
fontFamily: 'monospace',
|
|
2460
|
-
lineHeight: 16
|
|
2461
|
-
},
|
|
2462
|
-
children: testModeData.mrzText.split('\n').map((line, i) => `Line ${i + 1}: ${line} (${line.length} chars)`).join('\n')
|
|
2463
|
-
})]
|
|
2464
|
-
})
|
|
2465
|
-
})
|
|
1606
|
+
}), isDebugEnabled() && /*#__PURE__*/_jsx(DebugOverlay, {
|
|
1607
|
+
nextStep: nextStep,
|
|
1608
|
+
status: status,
|
|
1609
|
+
detectedDocumentType: detectedDocumentType,
|
|
1610
|
+
isBrightnessLow: isBrightnessLow,
|
|
1611
|
+
isFrameBlurry: isFrameBlurry,
|
|
1612
|
+
isTorchOn: isTorchOn,
|
|
1613
|
+
documentPlaneBounds: documentPlaneBounds,
|
|
1614
|
+
secondaryFaceBounds: secondaryFaceBounds,
|
|
1615
|
+
barcodeBounds: barcodeBounds,
|
|
1616
|
+
mrzBounds: mrzBounds,
|
|
1617
|
+
signatureBounds: signatureBounds,
|
|
1618
|
+
currentFaceImage: currentFaceImage,
|
|
1619
|
+
currentSecondaryFaceImage: currentSecondaryFaceImage,
|
|
1620
|
+
currentHologramImage: currentHologramImage,
|
|
1621
|
+
currentHologramMaskImage: _currentHologramMaskImage,
|
|
1622
|
+
latestHologramFaceImage: latestHologramFaceImage,
|
|
1623
|
+
hologramImageCount: hologramImageCount,
|
|
1624
|
+
allElementsDetected: allElementsDetected,
|
|
1625
|
+
elementsOutsideScanArea: elementsOutsideScanArea
|
|
1626
|
+
}), testMode && testModeData && /*#__PURE__*/_jsx(TestModePanel, {
|
|
1627
|
+
mrzText: testModeData.mrzText
|
|
2466
1628
|
})]
|
|
2467
1629
|
})]
|
|
2468
1630
|
});
|
|
2469
1631
|
};
|
|
2470
1632
|
const styles = StyleSheet.create({
|
|
2471
|
-
container: {
|
|
2472
|
-
flex: 1
|
|
2473
|
-
},
|
|
2474
1633
|
permissionContainer: {
|
|
2475
1634
|
flex: 1,
|
|
2476
1635
|
justifyContent: 'center',
|
|
@@ -2567,50 +1726,6 @@ const styles = StyleSheet.create({
|
|
|
2567
1726
|
gap: 10,
|
|
2568
1727
|
justifyContent: 'flex-start'
|
|
2569
1728
|
},
|
|
2570
|
-
debugImagesRow: {
|
|
2571
|
-
display: 'flex',
|
|
2572
|
-
flexDirection: 'row',
|
|
2573
|
-
gap: 10,
|
|
2574
|
-
justifyContent: 'center',
|
|
2575
|
-
flexWrap: 'wrap'
|
|
2576
|
-
},
|
|
2577
|
-
cardDetectionRow: {
|
|
2578
|
-
display: 'flex',
|
|
2579
|
-
flexDirection: 'row',
|
|
2580
|
-
justifyContent: 'center',
|
|
2581
|
-
marginTop: 5
|
|
2582
|
-
},
|
|
2583
|
-
imageContainer: {
|
|
2584
|
-
display: 'flex',
|
|
2585
|
-
flexDirection: 'column',
|
|
2586
|
-
alignItems: 'center'
|
|
2587
|
-
},
|
|
2588
|
-
imageContainerText: {
|
|
2589
|
-
color: 'white',
|
|
2590
|
-
fontSize: 8,
|
|
2591
|
-
textAlign: 'center',
|
|
2592
|
-
fontWeight: 'bold',
|
|
2593
|
-
marginTop: 2
|
|
2594
|
-
},
|
|
2595
|
-
faceImage: {
|
|
2596
|
-
width: 60,
|
|
2597
|
-
height: 80,
|
|
2598
|
-
borderRadius: 4,
|
|
2599
|
-
borderWidth: 1,
|
|
2600
|
-
borderColor: 'white'
|
|
2601
|
-
},
|
|
2602
|
-
cardDetectionImage: {
|
|
2603
|
-
width: 160,
|
|
2604
|
-
height: 120,
|
|
2605
|
-
borderRadius: 8,
|
|
2606
|
-
borderWidth: 2,
|
|
2607
|
-
borderColor: '#FF9800'
|
|
2608
|
-
},
|
|
2609
|
-
cardDetectionContainer: {
|
|
2610
|
-
display: 'flex',
|
|
2611
|
-
flexDirection: 'column',
|
|
2612
|
-
alignItems: 'center'
|
|
2613
|
-
},
|
|
2614
1729
|
guide: {
|
|
2615
1730
|
flex: 1,
|
|
2616
1731
|
display: 'flex',
|