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