@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
|
@@ -8,11 +8,8 @@ import {
|
|
|
8
8
|
StatusBar,
|
|
9
9
|
Vibration,
|
|
10
10
|
Linking,
|
|
11
|
-
Image,
|
|
12
11
|
ActivityIndicator,
|
|
13
12
|
PermissionsAndroid,
|
|
14
|
-
Dimensions,
|
|
15
|
-
ScrollView,
|
|
16
13
|
Animated,
|
|
17
14
|
type NativeSyntheticEvent,
|
|
18
15
|
type ViewStyle,
|
|
@@ -24,7 +21,6 @@ import {
|
|
|
24
21
|
type Frame,
|
|
25
22
|
} from './TrustchexCamera';
|
|
26
23
|
import { NativeModules } from 'react-native';
|
|
27
|
-
import type { MRZFields } from '../Types/mrzFields';
|
|
28
24
|
import mrzUtils from '../Libs/mrz.utils';
|
|
29
25
|
import { useKeepAwake } from '../Libs/native-keep-awake.utils';
|
|
30
26
|
import { useIsFocused } from '@react-navigation/native';
|
|
@@ -36,101 +32,60 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
36
32
|
import { speak, resetLastMessage } from '../Libs/tts.utils';
|
|
37
33
|
import AppContext from '../Contexts/AppContext';
|
|
38
34
|
import { useTheme } from '../Contexts/ThemeContext';
|
|
35
|
+
import DebugOverlay, { TestModePanel } from './DebugOverlay';
|
|
36
|
+
import {
|
|
37
|
+
getStatusMessage,
|
|
38
|
+
getFrameToScreenTransform,
|
|
39
|
+
transformBoundsToScreen,
|
|
40
|
+
getScanAreaBounds,
|
|
41
|
+
angleBetweenPoints,
|
|
42
|
+
detectDocumentType,
|
|
43
|
+
determineDocumentTypeToSet,
|
|
44
|
+
areMRZFieldsEqual,
|
|
45
|
+
hasRequiredMRZFields,
|
|
46
|
+
validateFacePosition,
|
|
47
|
+
} from './IdentityDocumentCamera.utils';
|
|
48
|
+
import {
|
|
49
|
+
handlePassportFlow,
|
|
50
|
+
handleIDFrontFlow,
|
|
51
|
+
handleIDBackFlow,
|
|
52
|
+
getNextStepAfterHologram,
|
|
53
|
+
} from './IdentityDocumentCamera.flows';
|
|
54
|
+
import {
|
|
55
|
+
HOLOGRAM_IMAGE_COUNT,
|
|
56
|
+
HOLOGRAM_DETECTION_THRESHOLD,
|
|
57
|
+
HOLOGRAM_DETECTION_RETRY_COUNT,
|
|
58
|
+
HOLOGRAM_CAPTURE_INTERVAL,
|
|
59
|
+
HOLOGRAM_MAX_FRAMES_WITHOUT_FACE,
|
|
60
|
+
MIN_BRIGHTNESS_THRESHOLD,
|
|
61
|
+
MAX_BRIGHTNESS_THRESHOLD,
|
|
62
|
+
FACE_EDGE_MARGIN_PERCENT,
|
|
63
|
+
MAX_CONSECUTIVE_QUALITY_FAILURES,
|
|
64
|
+
REQUIRED_CONSISTENT_MRZ_READS,
|
|
65
|
+
REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS,
|
|
66
|
+
SIGNATURE_TEXT_REGEX,
|
|
67
|
+
MRZ_BLOCK_PATTERN,
|
|
68
|
+
PASSPORT_MRZ_PATTERN,
|
|
69
|
+
MIN_CARD_FACE_SIZE_PERCENT,
|
|
70
|
+
} from './IdentityDocumentCamera.constants';
|
|
71
|
+
import type {
|
|
72
|
+
DocumentScannedData,
|
|
73
|
+
BlockText,
|
|
74
|
+
IdentityDocumentCameraProps,
|
|
75
|
+
Face,
|
|
76
|
+
Barcode,
|
|
77
|
+
CornerPointsType,
|
|
78
|
+
LinesData,
|
|
79
|
+
} from './IdentityDocumentCamera.types';
|
|
80
|
+
|
|
81
|
+
// Re-export types for backward compatibility
|
|
82
|
+
export type { DocumentScannedData, BlockText, IdentityDocumentCameraProps };
|
|
83
|
+
export type { PhotoOptions } from './IdentityDocumentCamera.types';
|
|
39
84
|
|
|
40
85
|
const { OpenCVModule } = NativeModules;
|
|
41
86
|
|
|
42
87
|
const AnimatedText = Animated.createAnimatedComponent(TextView);
|
|
43
88
|
|
|
44
|
-
export type DocumentScannedData = {
|
|
45
|
-
documentType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN';
|
|
46
|
-
image: string;
|
|
47
|
-
faceImage?: string;
|
|
48
|
-
secondaryFaceImage?: string;
|
|
49
|
-
hologramImage?: string;
|
|
50
|
-
barcodeValue?: string;
|
|
51
|
-
mrzText?: string;
|
|
52
|
-
mrzFields?: MRZFields;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export type BlockText = {
|
|
56
|
-
blocks: BlocksData[];
|
|
57
|
-
resultText: string;
|
|
58
|
-
mrzOnlyText?: string; // MRZ-specific text from detected MRZ blocks only
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
type BlocksData = {
|
|
62
|
-
blockFrame: FrameType;
|
|
63
|
-
blockCornerPoints: CornerPointsType;
|
|
64
|
-
lines: LinesData;
|
|
65
|
-
blockLanguages: string[] | [];
|
|
66
|
-
blockText: string;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type CornerPointsType = [{ x: number; y: number }];
|
|
70
|
-
|
|
71
|
-
type FrameType = {
|
|
72
|
-
boundingCenterX: number;
|
|
73
|
-
boundingCenterY: number;
|
|
74
|
-
height: number;
|
|
75
|
-
width: number;
|
|
76
|
-
x: number;
|
|
77
|
-
y: number;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type LinesData = [
|
|
81
|
-
lineCornerPoints: CornerPointsType,
|
|
82
|
-
elements: ElementsData,
|
|
83
|
-
lineFrame: FrameType,
|
|
84
|
-
lineLanguages: string[] | [],
|
|
85
|
-
lineText: string,
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
type ElementsData = [
|
|
89
|
-
elementCornerPoints: CornerPointsType,
|
|
90
|
-
elementFrame: FrameType,
|
|
91
|
-
elementText: string,
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
export type PhotoOptions = {
|
|
95
|
-
uri: string;
|
|
96
|
-
orientation?:
|
|
97
|
-
| 'landscapeRight'
|
|
98
|
-
| 'portrait'
|
|
99
|
-
| 'portraitUpsideDown'
|
|
100
|
-
| 'landscapeLeft';
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export interface IdentityDocumentCameraProps {
|
|
104
|
-
onlyMRZScan: boolean;
|
|
105
|
-
onIdentityDocumentScanned: (scannedData: DocumentScannedData) => void;
|
|
106
|
-
testMode?: boolean;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface Face {
|
|
110
|
-
bounds: { x: number; y: number; width: number; height: number };
|
|
111
|
-
rollAngle?: number;
|
|
112
|
-
pitchAngle?: number;
|
|
113
|
-
yawAngle?: number;
|
|
114
|
-
leftEyeOpenProbability?: number;
|
|
115
|
-
rightEyeOpenProbability?: number;
|
|
116
|
-
smilingProbability?: number;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface Barcode {
|
|
120
|
-
rawValue: string;
|
|
121
|
-
displayValue: string;
|
|
122
|
-
format: number;
|
|
123
|
-
boundingBox: { left: number; top: number; right: number; bottom: number };
|
|
124
|
-
cornerPoints: Array<{ x: number; y: number }>;
|
|
125
|
-
value?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const HOLOGRAM_IMAGE_COUNT = 12;
|
|
129
|
-
const HOLOGRAM_DETECTION_THRESHOLD = 1000; // Lowered for better sensitivity while still filtering noise
|
|
130
|
-
const HOLOGRAM_DETECTION_RETRY_COUNT = 3; // More attempts but faster each (~1s per attempt)
|
|
131
|
-
const MIN_BRIGHTNESS_THRESHOLD = 45;
|
|
132
|
-
const MAX_CONSECUTIVE_QUALITY_FAILURES = 30;
|
|
133
|
-
|
|
134
89
|
const IdentityDocumentCamera = ({
|
|
135
90
|
onlyMRZScan,
|
|
136
91
|
onIdentityDocumentScanned,
|
|
@@ -151,7 +106,6 @@ const IdentityDocumentCamera = ({
|
|
|
151
106
|
isTorchOnRef.current = val;
|
|
152
107
|
_setIsTorchOn(val);
|
|
153
108
|
}, []);
|
|
154
|
-
const [_exposure, _setExposure] = useState(0);
|
|
155
109
|
const isCameraInitialized = useRef(false);
|
|
156
110
|
const [currentFaceImage, setCurrentFaceImage] = useState<string | undefined>(
|
|
157
111
|
undefined
|
|
@@ -174,7 +128,7 @@ const IdentityDocumentCamera = ({
|
|
|
174
128
|
const [nextStep, setNextStep] = useState<
|
|
175
129
|
'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM' | 'COMPLETED'
|
|
176
130
|
>('SCAN_ID_FRONT_OR_PASSPORT');
|
|
177
|
-
const [
|
|
131
|
+
const [_completedStep, setCompletedStep] = useState<
|
|
178
132
|
'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM' | null
|
|
179
133
|
>(null);
|
|
180
134
|
const [detectedDocumentType, setDetectedDocumentType] = useState<
|
|
@@ -189,14 +143,12 @@ const IdentityDocumentCamera = ({
|
|
|
189
143
|
const lastValidMRZText = useRef<string | null>(null);
|
|
190
144
|
const lastValidMRZFields = useRef<any>(null);
|
|
191
145
|
const validMRZConsecutiveCount = useRef(0);
|
|
192
|
-
const REQUIRED_CONSISTENT_MRZ_READS = 2; // Require 2 consistent valid reads
|
|
193
146
|
|
|
194
147
|
// Document type stability tracking - require consistent detections from good quality frames
|
|
195
148
|
const lastDetectedDocType = useRef<
|
|
196
149
|
'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN'
|
|
197
150
|
>('UNKNOWN');
|
|
198
151
|
const consistentDocTypeCount = useRef(0);
|
|
199
|
-
const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3; // Require 3 consistent detections from quality frames
|
|
200
152
|
|
|
201
153
|
// Frame quality tracking - persist across callbacks
|
|
202
154
|
const lastFrameQuality = useRef({
|
|
@@ -217,121 +169,12 @@ const IdentityDocumentCamera = ({
|
|
|
217
169
|
timestamp: number;
|
|
218
170
|
} | null>(null);
|
|
219
171
|
|
|
220
|
-
// Helper to compare MRZ field values (ignore raw text variations)
|
|
221
|
-
const areMRZFieldsEqual = useCallback(
|
|
222
|
-
(fields1: any, fields2: any): boolean => {
|
|
223
|
-
if (!fields1 || !fields2) return false;
|
|
224
|
-
// Compare critical fields that define document identity
|
|
225
|
-
return (
|
|
226
|
-
fields1.documentNumber === fields2.documentNumber &&
|
|
227
|
-
fields1.birthDate === fields2.birthDate &&
|
|
228
|
-
fields1.expirationDate === fields2.expirationDate &&
|
|
229
|
-
fields1.firstName === fields2.firstName &&
|
|
230
|
-
fields1.lastName === fields2.lastName &&
|
|
231
|
-
fields1.issuingState === fields2.issuingState
|
|
232
|
-
);
|
|
233
|
-
},
|
|
234
|
-
[]
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
// Helper functions to reduce duplication
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Check if all required MRZ fields are present
|
|
241
|
-
*/
|
|
242
|
-
const hasRequiredMRZFields = useCallback(
|
|
243
|
-
(fields: any): boolean =>
|
|
244
|
-
!!fields?.firstName &&
|
|
245
|
-
!!fields?.lastName &&
|
|
246
|
-
!!fields?.documentNumber &&
|
|
247
|
-
!!fields?.birthDate,
|
|
248
|
-
[]
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Log detailed MRZ information for debugging and verification
|
|
253
|
-
*/
|
|
254
|
-
const logMRZDetails = useCallback(
|
|
255
|
-
(
|
|
256
|
-
stepName: string,
|
|
257
|
-
fields: any,
|
|
258
|
-
mrzText: string | null,
|
|
259
|
-
consecutiveReads: number,
|
|
260
|
-
isDebugMode: boolean
|
|
261
|
-
) => {
|
|
262
|
-
if (isDebugMode) {
|
|
263
|
-
debugLog(
|
|
264
|
-
'IdentityDocumentCamera',
|
|
265
|
-
`[${stepName}] MRZ validated successfully (consistent reads: ${consecutiveReads})`
|
|
266
|
-
);
|
|
267
|
-
debugLog('IdentityDocumentCamera', `[${stepName}] Final MRZ Fields:`, {
|
|
268
|
-
documentNumber: fields?.documentNumber,
|
|
269
|
-
name: `${fields?.lastName} ${fields?.firstName}`,
|
|
270
|
-
birthDate: fields?.birthDate,
|
|
271
|
-
expirationDate: fields?.expirationDate,
|
|
272
|
-
nationality: fields?.nationality || fields?.issuingState,
|
|
273
|
-
sex: fields?.sex,
|
|
274
|
-
personalId: fields?.optional1,
|
|
275
|
-
});
|
|
276
|
-
if (mrzText) {
|
|
277
|
-
const mrzLines = mrzText
|
|
278
|
-
.split('\n')
|
|
279
|
-
.map((l) => l.replace(/\s/g, ''))
|
|
280
|
-
.filter((l) => l.length >= 20 && /^[A-Z0-9<]+$/.test(l));
|
|
281
|
-
debugLog(
|
|
282
|
-
'IdentityDocumentCamera',
|
|
283
|
-
`[${stepName}] MRZ lines (${mrzLines.length}):`
|
|
284
|
-
);
|
|
285
|
-
mrzLines.forEach((line, idx) => {
|
|
286
|
-
debugLog('IdentityDocumentCamera', ` ${idx + 1}: ${line}`);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
[]
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Log MRZ validation failure details for debugging
|
|
296
|
-
*/
|
|
297
|
-
const logMRZValidationFailure = useCallback(
|
|
298
|
-
(
|
|
299
|
-
stepName: string,
|
|
300
|
-
hasRequiredFields: boolean,
|
|
301
|
-
parsedData: any,
|
|
302
|
-
retryCount: number,
|
|
303
|
-
isDebugMode: boolean
|
|
304
|
-
) => {
|
|
305
|
-
if (isDebugMode) {
|
|
306
|
-
const debugInfo: any = {
|
|
307
|
-
hasRequiredFields,
|
|
308
|
-
isValid: parsedData?.valid,
|
|
309
|
-
retryCount,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
if (parsedData?.valid) {
|
|
313
|
-
debugInfo.consistentReads = validMRZConsecutiveCount.current;
|
|
314
|
-
debugInfo.requiredReads = REQUIRED_CONSISTENT_MRZ_READS;
|
|
315
|
-
debugInfo.fieldsMatch = areMRZFieldsEqual(
|
|
316
|
-
lastValidMRZFields.current,
|
|
317
|
-
parsedData?.fields
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
debugLog(
|
|
322
|
-
'IdentityDocumentCamera',
|
|
323
|
-
`[${stepName}] MRZ detected but validation failed - retrying`,
|
|
324
|
-
debugInfo
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
[areMRZFieldsEqual]
|
|
329
|
-
);
|
|
330
|
-
|
|
331
172
|
const lastHologramCaptureTime = useRef(0);
|
|
332
|
-
const HOLOGRAM_CAPTURE_INTERVAL = 250; // ms between captures - allows flash to fully stabilize for accurate diff
|
|
333
173
|
const hologramFramesWithoutFace = useRef(0); // Track consecutive frames without face during hologram collection
|
|
334
|
-
const
|
|
174
|
+
const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
|
|
175
|
+
const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
|
|
176
|
+
const isStepTransitionInProgress = useRef(false); // Prevent duplicate step transitions from rapid frames
|
|
177
|
+
const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
|
|
335
178
|
|
|
336
179
|
const faceDetectionErrorCount = useRef(0);
|
|
337
180
|
const brightnessHistory = useRef<number[]>([]);
|
|
@@ -339,7 +182,6 @@ const IdentityDocumentCamera = ({
|
|
|
339
182
|
const faceImages = useRef<string[]>([]);
|
|
340
183
|
const hologramImageCountRef = useRef(0);
|
|
341
184
|
const [hologramImageCount, setHologramImageCount] = useState(0);
|
|
342
|
-
const lastVoiceGuidanceMessage = useRef<string>('');
|
|
343
185
|
const [latestHologramFaceImage, setLatestHologramFaceImage] = useState<
|
|
344
186
|
string | undefined
|
|
345
187
|
>(undefined);
|
|
@@ -420,6 +262,9 @@ const IdentityDocumentCamera = ({
|
|
|
420
262
|
useEffect(() => {
|
|
421
263
|
if (isFocused && hasPermission && hasGuideShown) {
|
|
422
264
|
setIsActive(true);
|
|
265
|
+
isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
|
|
266
|
+
isStepTransitionInProgress.current = false;
|
|
267
|
+
isCompletionAnnouncementSpoken.current = false;
|
|
423
268
|
} else {
|
|
424
269
|
setIsActive(false);
|
|
425
270
|
faceImages.current = [];
|
|
@@ -432,11 +277,14 @@ const IdentityDocumentCamera = ({
|
|
|
432
277
|
lastValidMRZText.current = null;
|
|
433
278
|
lastValidMRZFields.current = null;
|
|
434
279
|
validMRZConsecutiveCount.current = 0;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
280
|
+
cachedBarcode.current = null;
|
|
281
|
+
isCompletionCallbackInvoked.current = false;
|
|
282
|
+
isStepTransitionInProgress.current = false;
|
|
283
|
+
isCompletionAnnouncementSpoken.current = false;
|
|
284
|
+
// Clear all captured image states from previous scan
|
|
285
|
+
setCurrentFaceImage(undefined);
|
|
286
|
+
setCurrentHologramImage(undefined);
|
|
287
|
+
setCurrentSecondaryFaceImage(undefined);
|
|
440
288
|
resetLastMessage();
|
|
441
289
|
}
|
|
442
290
|
|
|
@@ -446,89 +294,46 @@ const IdentityDocumentCamera = ({
|
|
|
446
294
|
hologramImageCountRef.current = 0;
|
|
447
295
|
setHologramImageCount(0);
|
|
448
296
|
setLatestHologramFaceImage(undefined);
|
|
449
|
-
|
|
297
|
+
isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
|
|
298
|
+
isStepTransitionInProgress.current = false;
|
|
299
|
+
isCompletionAnnouncementSpoken.current = false;
|
|
300
|
+
// Clear all captured image states on unmount
|
|
301
|
+
setCurrentFaceImage(undefined);
|
|
302
|
+
setCurrentHologramImage(undefined);
|
|
303
|
+
setCurrentSecondaryFaceImage(undefined);
|
|
450
304
|
resetLastMessage();
|
|
451
305
|
};
|
|
452
306
|
}, [isFocused, hasPermission, hasGuideShown]);
|
|
453
307
|
|
|
454
308
|
useEffect(() => {
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
309
|
+
if (
|
|
310
|
+
!hasGuideShown ||
|
|
311
|
+
!appContext.currentWorkflowStep?.data?.voiceGuidanceActive
|
|
312
|
+
)
|
|
313
|
+
return;
|
|
314
|
+
|
|
315
|
+
const message = getStatusMessage(
|
|
316
|
+
nextStep,
|
|
317
|
+
status,
|
|
318
|
+
detectedDocumentType,
|
|
319
|
+
isBrightnessLow,
|
|
320
|
+
isFrameBlurry,
|
|
321
|
+
allElementsDetected,
|
|
322
|
+
elementsOutsideScanArea,
|
|
323
|
+
t
|
|
324
|
+
);
|
|
458
325
|
|
|
326
|
+
if (message) {
|
|
459
327
|
if (nextStep === 'COMPLETED') {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
? t('identityDocumentCamera.alignIDFront')
|
|
465
|
-
: nextStep === 'SCAN_ID_BACK'
|
|
466
|
-
? t('identityDocumentCamera.alignIDBack')
|
|
467
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
468
|
-
? t('identityDocumentCamera.alignIDFront')
|
|
469
|
-
: t('identityDocumentCamera.alignPhotoSide');
|
|
470
|
-
} else if (isBrightnessLow) {
|
|
471
|
-
message = t('identityDocumentCamera.lowBrightness');
|
|
472
|
-
} else if (isFrameBlurry) {
|
|
473
|
-
message = t('identityDocumentCamera.avoidBlur');
|
|
474
|
-
} else if (
|
|
475
|
-
status === 'SCANNING' &&
|
|
476
|
-
allElementsDetected &&
|
|
477
|
-
elementsOutsideScanArea.length === 0
|
|
478
|
-
) {
|
|
479
|
-
message =
|
|
480
|
-
nextStep === 'SCAN_ID_BACK'
|
|
481
|
-
? t('identityDocumentCamera.idCardBackDetected')
|
|
482
|
-
: detectedDocumentType === 'PASSPORT'
|
|
483
|
-
? t('identityDocumentCamera.passportDetected')
|
|
484
|
-
: detectedDocumentType === 'ID_FRONT'
|
|
485
|
-
? t('identityDocumentCamera.idCardFrontDetected')
|
|
486
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
487
|
-
? t('identityDocumentCamera.alignHologram')
|
|
488
|
-
: t('identityDocumentCamera.readingDocument');
|
|
489
|
-
} else if (elementsOutsideScanArea.length > 0) {
|
|
490
|
-
message = t('identityDocumentCamera.centerDocument');
|
|
491
|
-
} else if (
|
|
492
|
-
(status === 'SCANNING' || status === 'SEARCHING') &&
|
|
493
|
-
!allElementsDetected
|
|
494
|
-
) {
|
|
495
|
-
message =
|
|
496
|
-
nextStep === 'SCAN_ID_BACK'
|
|
497
|
-
? t('identityDocumentCamera.alignIDBack')
|
|
498
|
-
: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
499
|
-
? detectedDocumentType === 'PASSPORT'
|
|
500
|
-
? t('identityDocumentCamera.alignPassport')
|
|
501
|
-
: detectedDocumentType === 'ID_FRONT'
|
|
502
|
-
? t('identityDocumentCamera.alignIDFront')
|
|
503
|
-
: t('identityDocumentCamera.alignPhotoSide')
|
|
504
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
505
|
-
? t('identityDocumentCamera.alignHologram')
|
|
506
|
-
: t('identityDocumentCamera.readingDocument');
|
|
328
|
+
if (isCompletionAnnouncementSpoken.current) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
isCompletionAnnouncementSpoken.current = true;
|
|
507
332
|
} else {
|
|
508
|
-
|
|
509
|
-
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
510
|
-
? status === 'SCANNING'
|
|
511
|
-
? t('identityDocumentCamera.readingDocument')
|
|
512
|
-
: t('identityDocumentCamera.alignPhotoSide')
|
|
513
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
514
|
-
? t('identityDocumentCamera.alignHologram')
|
|
515
|
-
: nextStep === 'SCAN_ID_BACK'
|
|
516
|
-
? status === 'SCANNING'
|
|
517
|
-
? t('identityDocumentCamera.readingDocument')
|
|
518
|
-
: t('identityDocumentCamera.alignIDBackSide')
|
|
519
|
-
: nextStep === 'COMPLETED'
|
|
520
|
-
? t('identityDocumentCamera.scanCompleted')
|
|
521
|
-
: '';
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
appContext.currentWorkflowStep?.data?.voiceGuidanceActive &&
|
|
526
|
-
message &&
|
|
527
|
-
message !== lastVoiceGuidanceMessage.current
|
|
528
|
-
) {
|
|
529
|
-
lastVoiceGuidanceMessage.current = message;
|
|
530
|
-
speak(message, true);
|
|
333
|
+
isCompletionAnnouncementSpoken.current = false;
|
|
531
334
|
}
|
|
335
|
+
const shouldBypassInterval = nextStep !== 'COMPLETED';
|
|
336
|
+
speak(message, shouldBypassInterval);
|
|
532
337
|
}
|
|
533
338
|
}, [
|
|
534
339
|
appContext.currentWorkflowStep?.data?.voiceGuidanceActive,
|
|
@@ -537,8 +342,6 @@ const IdentityDocumentCamera = ({
|
|
|
537
342
|
isFrameBlurry,
|
|
538
343
|
nextStep,
|
|
539
344
|
status,
|
|
540
|
-
completedStep,
|
|
541
|
-
currentFaceImage,
|
|
542
345
|
detectedDocumentType,
|
|
543
346
|
allElementsDetected,
|
|
544
347
|
elementsOutsideScanArea,
|
|
@@ -563,6 +366,26 @@ const IdentityDocumentCamera = ({
|
|
|
563
366
|
}
|
|
564
367
|
}, [nextStep]);
|
|
565
368
|
|
|
369
|
+
// Reset completion callback flag when transitioning away from COMPLETED
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (nextStep !== 'COMPLETED') {
|
|
372
|
+
isCompletionCallbackInvoked.current = false;
|
|
373
|
+
}
|
|
374
|
+
isStepTransitionInProgress.current = false;
|
|
375
|
+
}, [nextStep]);
|
|
376
|
+
|
|
377
|
+
// Update status bar based on guide visibility
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (hasGuideShown) {
|
|
380
|
+
// Scanning camera view - use light icons on dark background
|
|
381
|
+
StatusBar.setBarStyle('light-content', true);
|
|
382
|
+
} else {
|
|
383
|
+
// Guide screen with white background - use dark icons
|
|
384
|
+
StatusBar.setBarStyle('dark-content', true);
|
|
385
|
+
StatusBar.setBackgroundColor('#ffffff', true);
|
|
386
|
+
}
|
|
387
|
+
}, [hasGuideShown]);
|
|
388
|
+
|
|
566
389
|
// Error flash animation - flash red text when wrong side detected
|
|
567
390
|
useEffect(() => {
|
|
568
391
|
if (status === 'INCORRECT') {
|
|
@@ -611,17 +434,22 @@ const IdentityDocumentCamera = ({
|
|
|
611
434
|
[]
|
|
612
435
|
);
|
|
613
436
|
|
|
614
|
-
// Native OpenCV: compare
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
): Promise<boolean> => {
|
|
437
|
+
// Native OpenCV: compare face visual similarity (device-side validation before backend FaceNet)
|
|
438
|
+
const compareFaceVisualSimilarityNative = async (
|
|
439
|
+
faceImage1: string,
|
|
440
|
+
faceImage2: string
|
|
441
|
+
): Promise<{ similarity: number } | null> => {
|
|
620
442
|
try {
|
|
621
|
-
if (!
|
|
622
|
-
return await OpenCVModule.
|
|
443
|
+
if (!faceImage1 || !faceImage2) return null;
|
|
444
|
+
return await OpenCVModule.compareFaceVisualSimilarity(
|
|
445
|
+
faceImage1,
|
|
446
|
+
faceImage2
|
|
447
|
+
);
|
|
623
448
|
} catch (error) {
|
|
624
|
-
|
|
449
|
+
if (isDebugEnabled()) {
|
|
450
|
+
logError('[Face Visual Similarity] Comparison error:', error);
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
625
453
|
}
|
|
626
454
|
};
|
|
627
455
|
|
|
@@ -630,7 +458,8 @@ const IdentityDocumentCamera = ({
|
|
|
630
458
|
facesToDetect: Face[],
|
|
631
459
|
image: string,
|
|
632
460
|
width: number,
|
|
633
|
-
height: number
|
|
461
|
+
height: number,
|
|
462
|
+
widerRightPadding = false
|
|
634
463
|
): Promise<string[]> => {
|
|
635
464
|
if (!facesToDetect.length || !image || width <= 0 || height <= 0) {
|
|
636
465
|
return [];
|
|
@@ -646,7 +475,8 @@ const IdentityDocumentCamera = ({
|
|
|
646
475
|
image,
|
|
647
476
|
faceBounds,
|
|
648
477
|
width,
|
|
649
|
-
height
|
|
478
|
+
height,
|
|
479
|
+
widerRightPadding
|
|
650
480
|
);
|
|
651
481
|
return croppedFaces ?? [];
|
|
652
482
|
} catch (error) {
|
|
@@ -655,6 +485,62 @@ const IdentityDocumentCamera = ({
|
|
|
655
485
|
}
|
|
656
486
|
};
|
|
657
487
|
|
|
488
|
+
// Check if face image has glare (brightness exceeds threshold)
|
|
489
|
+
const checkFaceGlare = async (faceImage: string): Promise<boolean> => {
|
|
490
|
+
try {
|
|
491
|
+
// Check entire face region for glare
|
|
492
|
+
const hasGlare = await OpenCVModule.isRectangularRegionBright(
|
|
493
|
+
faceImage,
|
|
494
|
+
0,
|
|
495
|
+
0,
|
|
496
|
+
100, // Full face width
|
|
497
|
+
100, // Full face height
|
|
498
|
+
MAX_BRIGHTNESS_THRESHOLD
|
|
499
|
+
);
|
|
500
|
+
return hasGlare;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return false; // Assume no glare on error
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Check if face is fully visible (not cropped at edges)
|
|
507
|
+
const isFaceFullyVisible = (
|
|
508
|
+
face: Face,
|
|
509
|
+
frameWidth: number,
|
|
510
|
+
frameHeight: number
|
|
511
|
+
): boolean => {
|
|
512
|
+
const margin = FACE_EDGE_MARGIN_PERCENT;
|
|
513
|
+
const bounds = face.bounds;
|
|
514
|
+
return (
|
|
515
|
+
bounds.x >= frameWidth * margin &&
|
|
516
|
+
bounds.y >= frameHeight * margin &&
|
|
517
|
+
bounds.x + bounds.width <= frameWidth * (1 - margin) &&
|
|
518
|
+
bounds.y + bounds.height <= frameHeight * (1 - margin)
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// Check if document image has glare
|
|
523
|
+
const checkDocumentGlare = async (
|
|
524
|
+
image: string,
|
|
525
|
+
width: number,
|
|
526
|
+
height: number
|
|
527
|
+
): Promise<boolean> => {
|
|
528
|
+
try {
|
|
529
|
+
// Check center 80% region for glare (document area)
|
|
530
|
+
const hasGlare = await OpenCVModule.isRectangularRegionBright(
|
|
531
|
+
image,
|
|
532
|
+
Math.round(width * 0.1),
|
|
533
|
+
Math.round(height * 0.1),
|
|
534
|
+
Math.round(width * 0.8),
|
|
535
|
+
Math.round(height * 0.8),
|
|
536
|
+
MAX_BRIGHTNESS_THRESHOLD
|
|
537
|
+
);
|
|
538
|
+
return hasGlare;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
658
544
|
const setNextStepAndVibrate = useCallback(
|
|
659
545
|
(
|
|
660
546
|
nextStepType:
|
|
@@ -670,15 +556,23 @@ const IdentityDocumentCamera = ({
|
|
|
670
556
|
|
|
671
557
|
// Turn flash on and reset hologram counters when entering SCAN_HOLOGRAM
|
|
672
558
|
if (nextStepType === 'SCAN_HOLOGRAM' && fromStep !== 'SCAN_HOLOGRAM') {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
559
|
+
const isMidCollection =
|
|
560
|
+
faceImages.current.length > 0 &&
|
|
561
|
+
faceImages.current.length < HOLOGRAM_IMAGE_COUNT;
|
|
562
|
+
|
|
563
|
+
if (!isMidCollection) {
|
|
564
|
+
setIsTorchOn(true);
|
|
565
|
+
hologramDetectionCurrentRetryCount.current = 0;
|
|
566
|
+
secondaryFaceDetectionCurrentRetryCount.current = 0;
|
|
567
|
+
hologramFramesWithoutFace.current = 0;
|
|
568
|
+
faceImages.current = [];
|
|
569
|
+
hologramImageCountRef.current = 0;
|
|
570
|
+
setHologramImageCount(0);
|
|
571
|
+
setLatestHologramFaceImage(undefined);
|
|
572
|
+
// Clear previous hologram state to prevent premature completion
|
|
573
|
+
setCurrentHologramImage(undefined);
|
|
574
|
+
setCurrentHologramMaskImage(undefined);
|
|
575
|
+
}
|
|
682
576
|
}
|
|
683
577
|
|
|
684
578
|
// Clean up flash and hologram state when leaving SCAN_HOLOGRAM step
|
|
@@ -688,15 +582,11 @@ const IdentityDocumentCamera = ({
|
|
|
688
582
|
hologramImageCountRef.current = 0;
|
|
689
583
|
setHologramImageCount(0);
|
|
690
584
|
setLatestHologramFaceImage(undefined);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
console.log(
|
|
697
|
-
'[Flash] Turning off flash and clearing hologram images when leaving step'
|
|
698
|
-
);
|
|
699
|
-
}
|
|
585
|
+
isHologramDetectionInProgress.current = false;
|
|
586
|
+
lastFacePosition.current = null;
|
|
587
|
+
cachedBarcode.current = null;
|
|
588
|
+
setDocumentPlaneBounds(null);
|
|
589
|
+
setSecondaryFaceBounds(null);
|
|
700
590
|
}
|
|
701
591
|
|
|
702
592
|
setNextStep(nextStepType);
|
|
@@ -704,7 +594,12 @@ const IdentityDocumentCamera = ({
|
|
|
704
594
|
|
|
705
595
|
// Reset MRZ retry counter for each new step so retries start fresh
|
|
706
596
|
mrzDetectionCurrentRetryCount.current = 0;
|
|
707
|
-
|
|
597
|
+
// Only clear MRZ text when entering SCAN_ID_BACK (new MRZ expected).
|
|
598
|
+
// Preserve across SCAN_HOLOGRAM so passport completion has MRZ data.
|
|
599
|
+
if (nextStepType === 'SCAN_ID_BACK') {
|
|
600
|
+
lastValidMRZText.current = null;
|
|
601
|
+
lastValidMRZFields.current = null;
|
|
602
|
+
}
|
|
708
603
|
validMRZConsecutiveCount.current = 0;
|
|
709
604
|
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
710
605
|
|
|
@@ -718,6 +613,50 @@ const IdentityDocumentCamera = ({
|
|
|
718
613
|
[setIsTorchOn]
|
|
719
614
|
);
|
|
720
615
|
|
|
616
|
+
const transitionStepWithCallback = useCallback(
|
|
617
|
+
(
|
|
618
|
+
nextStepType:
|
|
619
|
+
| 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
620
|
+
| 'SCAN_ID_BACK'
|
|
621
|
+
| 'SCAN_HOLOGRAM'
|
|
622
|
+
| 'COMPLETED',
|
|
623
|
+
fromStep: 'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM',
|
|
624
|
+
scannedData: DocumentScannedData
|
|
625
|
+
) => {
|
|
626
|
+
if (isStepTransitionInProgress.current) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
isStepTransitionInProgress.current = true;
|
|
631
|
+
|
|
632
|
+
// Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
|
|
633
|
+
if (nextStepType !== 'SCAN_HOLOGRAM') {
|
|
634
|
+
setIsTorchOn(false);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
setNextStepAndVibrate(nextStepType, fromStep);
|
|
638
|
+
|
|
639
|
+
// Prevent the COMPLETED handler from firing a duplicate callback with
|
|
640
|
+
// potentially wrong detectedDocumentType. This transition already sends
|
|
641
|
+
// the correct scannedData below.
|
|
642
|
+
if (nextStepType === 'COMPLETED') {
|
|
643
|
+
isCompletionCallbackInvoked.current = true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Only notify parent for step completions, not intermediate transitions.
|
|
647
|
+
// The COMPLETED handler constructs final data from accumulated state.
|
|
648
|
+
// For ID cards, front/back data must be sent incrementally since parent stores them separately.
|
|
649
|
+
const isIntermediatePassportStep =
|
|
650
|
+
scannedData.documentType === 'PASSPORT' && nextStepType !== 'COMPLETED';
|
|
651
|
+
if (!isIntermediatePassportStep) {
|
|
652
|
+
setTimeout(() => {
|
|
653
|
+
onIdentityDocumentScanned(scannedData);
|
|
654
|
+
}, 1000);
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
[onIdentityDocumentScanned, setNextStepAndVibrate, setIsTorchOn]
|
|
658
|
+
);
|
|
659
|
+
|
|
721
660
|
const handleFaceAndText = useCallback(
|
|
722
661
|
async (
|
|
723
662
|
text: string,
|
|
@@ -729,97 +668,98 @@ const IdentityDocumentCamera = ({
|
|
|
729
668
|
elementsOutside?: boolean,
|
|
730
669
|
scannedText?: BlockText
|
|
731
670
|
) => {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
) => {
|
|
739
|
-
// Relaxed signature detection: matches signature/imza variants and OCR errors
|
|
740
|
-
const hasSignatureMatch = /s[gi]g?n[au]?t[u]?r|imz[a]s?/i.test(ocrText);
|
|
741
|
-
|
|
742
|
-
if (isDebugEnabled()) {
|
|
743
|
-
console.log(
|
|
744
|
-
'[DocType] faces:',
|
|
745
|
-
facesParam.length,
|
|
746
|
-
'mrzFields:',
|
|
747
|
-
!!mrzFields,
|
|
748
|
-
'mrzText:',
|
|
749
|
-
!!mrzTextParam,
|
|
750
|
-
'textLen:',
|
|
751
|
-
ocrText?.length,
|
|
752
|
-
'hasSignature:',
|
|
753
|
-
hasSignatureMatch
|
|
754
|
-
);
|
|
755
|
-
}
|
|
671
|
+
// Classify faces: Primary (>= 5% of frame) vs Secondary (< 5%)
|
|
672
|
+
const primaryFaces = faces.filter(
|
|
673
|
+
(face) =>
|
|
674
|
+
face.bounds.width >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT &&
|
|
675
|
+
face.bounds.height >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT
|
|
676
|
+
);
|
|
756
677
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
678
|
+
const secondaryFaces = faces.filter(
|
|
679
|
+
(face) =>
|
|
680
|
+
face.bounds.width < frameWidth * MIN_CARD_FACE_SIZE_PERCENT ||
|
|
681
|
+
face.bounds.height < frameWidth * MIN_CARD_FACE_SIZE_PERCENT
|
|
682
|
+
);
|
|
761
683
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
684
|
+
// All faces for processing
|
|
685
|
+
const allDetectedFaces = faces;
|
|
686
|
+
|
|
687
|
+
// Validate primary face meets ICAO standards (face height 70-80% of image, aspect ratio ~1:1.25)
|
|
688
|
+
let primaryFaceICAOCompliant = false;
|
|
689
|
+
if (primaryFaces.length > 0) {
|
|
690
|
+
const face = primaryFaces[0];
|
|
691
|
+
const faceHeightPercent = (face.bounds.height / frameHeight) * 100;
|
|
692
|
+
const aspectRatio = face.bounds.width / face.bounds.height;
|
|
693
|
+
|
|
694
|
+
// ICAO: face height 70-80% of image, width:height ratio between 0.75 and 0.85
|
|
695
|
+
primaryFaceICAOCompliant =
|
|
696
|
+
faceHeightPercent >= 70 &&
|
|
697
|
+
faceHeightPercent <= 80 &&
|
|
698
|
+
aspectRatio >= 0.75 &&
|
|
699
|
+
aspectRatio <= 0.85;
|
|
700
|
+
}
|
|
766
701
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
702
|
+
if (
|
|
703
|
+
isDebugEnabled() &&
|
|
704
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
705
|
+
faces.length > 0
|
|
706
|
+
) {
|
|
707
|
+
debugLog('IdentityDocumentCamera', '[FACE DETECTION] All faces', {
|
|
708
|
+
totalFaces: faces.length,
|
|
709
|
+
primaryFacesCount: primaryFaces.length,
|
|
710
|
+
secondaryFacesCount: secondaryFaces.length,
|
|
711
|
+
frameWidth,
|
|
712
|
+
frameHeight,
|
|
713
|
+
minPrimarySize: frameWidth * MIN_CARD_FACE_SIZE_PERCENT,
|
|
714
|
+
primaryFaceICAOCompliant,
|
|
715
|
+
faceDetails: faces.map((f, i) => ({
|
|
716
|
+
index: i,
|
|
717
|
+
width: Math.round(f.bounds.width),
|
|
718
|
+
height: Math.round(f.bounds.height),
|
|
719
|
+
x: Math.round(f.bounds.x),
|
|
720
|
+
y: Math.round(f.bounds.y),
|
|
721
|
+
widthPercent:
|
|
722
|
+
((f.bounds.width / frameWidth) * 100).toFixed(1) + '%',
|
|
723
|
+
heightPercent:
|
|
724
|
+
((f.bounds.height / frameHeight) * 100).toFixed(1) + '%',
|
|
725
|
+
aspectRatio: (f.bounds.width / f.bounds.height).toFixed(2),
|
|
726
|
+
category:
|
|
727
|
+
f.bounds.width >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT &&
|
|
728
|
+
f.bounds.height >= frameWidth * MIN_CARD_FACE_SIZE_PERCENT
|
|
729
|
+
? 'primary'
|
|
730
|
+
: 'secondary',
|
|
731
|
+
})),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
774
734
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
735
|
+
// Get scan area bounds for face filtering
|
|
736
|
+
const { isInsideScan } = getScanAreaBounds(frameWidth, frameHeight);
|
|
737
|
+
|
|
738
|
+
// Filter to only faces inside scan area (for hologram, exclude passport secondary faces)
|
|
739
|
+
const facesInsideScanArea = primaryFaces.filter((face) =>
|
|
740
|
+
isInsideScan(
|
|
741
|
+
face.bounds.x,
|
|
742
|
+
face.bounds.y,
|
|
743
|
+
face.bounds.width,
|
|
744
|
+
face.bounds.height
|
|
745
|
+
)
|
|
746
|
+
);
|
|
783
747
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
) {
|
|
797
|
-
// Passport MRZ pattern detected (P<TUR, P<USA, etc.) but not parsed
|
|
798
|
-
// Could be passport with OCR errors - wait for proper parsing
|
|
799
|
-
if (isDebugEnabled()) {
|
|
800
|
-
console.log(
|
|
801
|
-
'[DocType] Passport MRZ pattern (P<XXX) detected but not parsed - returning UNKNOWN to wait for proper classification'
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
return 'UNKNOWN';
|
|
805
|
-
}
|
|
806
|
-
return 'ID_FRONT';
|
|
807
|
-
}
|
|
808
|
-
// Also ensure flash is off when scan is completed
|
|
809
|
-
if (nextStep === 'COMPLETED' && isTorchOn) {
|
|
810
|
-
setIsTorchOn(false);
|
|
748
|
+
if (
|
|
749
|
+
isDebugEnabled() &&
|
|
750
|
+
nextStep === 'SCAN_HOLOGRAM' &&
|
|
751
|
+
primaryFaces.length > facesInsideScanArea.length
|
|
752
|
+
) {
|
|
753
|
+
debugLog(
|
|
754
|
+
'IdentityDocumentCamera',
|
|
755
|
+
'[HOLOGRAM] Filtered out faces outside scan area',
|
|
756
|
+
{
|
|
757
|
+
totalFaces: primaryFaces.length,
|
|
758
|
+
facesInside: facesInsideScanArea.length,
|
|
759
|
+
filtered: primaryFaces.length - facesInsideScanArea.length,
|
|
811
760
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return 'UNKNOWN';
|
|
815
|
-
};
|
|
816
|
-
|
|
817
|
-
// Filter to card-sized faces only (min 5% of frame width to exclude tiny background faces)
|
|
818
|
-
const cardSizedFaces = faces.filter(
|
|
819
|
-
(face) =>
|
|
820
|
-
face.bounds.width >= frameWidth * 0.05 &&
|
|
821
|
-
face.bounds.height >= frameWidth * 0.05
|
|
822
|
-
);
|
|
761
|
+
);
|
|
762
|
+
}
|
|
823
763
|
|
|
824
764
|
// Cache barcode when detected, use cached value if current frame has no barcode
|
|
825
765
|
// This handles inconsistent barcode detection across frames
|
|
@@ -847,27 +787,100 @@ const IdentityDocumentCamera = ({
|
|
|
847
787
|
}
|
|
848
788
|
|
|
849
789
|
if (nextStep === 'COMPLETED') {
|
|
790
|
+
// Prevent multiple callback invocations from repeated frames
|
|
791
|
+
if (isCompletionCallbackInvoked.current) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
isCompletionCallbackInvoked.current = true;
|
|
795
|
+
|
|
850
796
|
setStatus('SCANNED');
|
|
851
|
-
|
|
852
|
-
|
|
797
|
+
// Construct scanned data from available state and invoke callback
|
|
798
|
+
// Use MRZ document code as ultimate authority for document type —
|
|
799
|
+
// detectedDocumentType may be wrong if locked from early noisy frames
|
|
800
|
+
const completedDocType =
|
|
801
|
+
lastValidMRZFields.current?.documentCode === 'P'
|
|
802
|
+
? 'PASSPORT'
|
|
803
|
+
: (detectedDocumentType as
|
|
804
|
+
| 'ID_FRONT'
|
|
805
|
+
| 'ID_BACK'
|
|
806
|
+
| 'PASSPORT'
|
|
807
|
+
| 'UNKNOWN');
|
|
808
|
+
const scannedData: DocumentScannedData = {
|
|
809
|
+
documentType: completedDocType,
|
|
810
|
+
image: image ?? '',
|
|
811
|
+
faceImage: currentFaceImage,
|
|
812
|
+
secondaryFaceImage: currentSecondaryFaceImage,
|
|
813
|
+
hologramImage: currentHologramImage,
|
|
814
|
+
mrzText: lastValidMRZText.current ?? undefined,
|
|
815
|
+
mrzFields: lastValidMRZFields.current ?? undefined,
|
|
816
|
+
barcodeValue: barcodeToUse?.rawValue ?? undefined,
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
if (isDebugEnabled()) {
|
|
820
|
+
debugLog('IdentityDocumentCamera', '[COMPLETED] Final scanned data', {
|
|
821
|
+
documentType: completedDocType,
|
|
822
|
+
hasFaceImage: !!scannedData.faceImage,
|
|
823
|
+
hasSecondaryFace: !!scannedData.secondaryFaceImage,
|
|
824
|
+
secondaryFaceImageLength:
|
|
825
|
+
scannedData.secondaryFaceImage?.length || 0,
|
|
826
|
+
currentSecondaryFaceLength: currentSecondaryFaceImage?.length || 0,
|
|
827
|
+
hasHologramImage: !!scannedData.hologramImage,
|
|
828
|
+
hasMRZ: !!scannedData.mrzFields,
|
|
829
|
+
hasBarcode: !!scannedData.barcodeValue,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
853
832
|
|
|
854
|
-
|
|
833
|
+
setTimeout(() => {
|
|
834
|
+
onIdentityDocumentScanned(scannedData);
|
|
835
|
+
}, 500);
|
|
855
836
|
return;
|
|
856
837
|
}
|
|
857
838
|
|
|
858
|
-
|
|
859
|
-
|
|
839
|
+
// Skip elementsOutside check during SCAN_HOLOGRAM - allow document tilting for hologram capture
|
|
840
|
+
if (elementsOutside && nextStep !== 'SCAN_HOLOGRAM') {
|
|
860
841
|
return;
|
|
861
842
|
}
|
|
862
843
|
|
|
863
|
-
//
|
|
844
|
+
// Parse MRZ early to use for document type detection
|
|
845
|
+
// Use JavaScript MRZ validation with corrections
|
|
846
|
+
// Prefer MRZ-only text if available (from detected MRZ blocks),
|
|
847
|
+
// otherwise fall back to all text (for backward compatibility)
|
|
848
|
+
const textForValidation = scannedText?.mrzOnlyText || text;
|
|
849
|
+
const mrzValidationResult =
|
|
850
|
+
mrzUtils.validateMRZWithCorrections(textForValidation);
|
|
851
|
+
const parsedMRZData = {
|
|
852
|
+
valid: mrzValidationResult.valid,
|
|
853
|
+
fields: mrzValidationResult.fields || null,
|
|
854
|
+
};
|
|
855
|
+
// Extract raw MRZ lines from text if validation succeeded
|
|
856
|
+
const mrzText = parsedMRZData.valid ? mrzUtils.fixMRZ(text) : null;
|
|
857
|
+
|
|
858
|
+
// CRITICAL: Only detect document type during initial scan step
|
|
859
|
+
// For SCAN_HOLOGRAM and beyond, use the locked detectedDocumentType to avoid
|
|
860
|
+
// interruptions when user tilts document (MRZ may not be visible)
|
|
861
|
+
// However, if MRZ code 'P' is detected, always use PASSPORT — the lock may
|
|
862
|
+
// be wrong (ID_FRONT locked before passport MRZ became readable)
|
|
863
|
+
const documentType =
|
|
864
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
865
|
+
? detectDocumentType(
|
|
866
|
+
primaryFaces,
|
|
867
|
+
text,
|
|
868
|
+
parsedMRZData?.fields,
|
|
869
|
+
frameWidth,
|
|
870
|
+
mrzText
|
|
871
|
+
)
|
|
872
|
+
: parsedMRZData?.fields?.documentCode === 'P'
|
|
873
|
+
? 'PASSPORT'
|
|
874
|
+
: detectedDocumentType;
|
|
875
|
+
|
|
876
|
+
// Crop faces once document type is confirmed or we're past the initial step
|
|
864
877
|
const shouldCropFaces =
|
|
865
|
-
|
|
866
|
-
|
|
878
|
+
documentType === 'ID_FRONT' ||
|
|
879
|
+
documentType === 'PASSPORT' ||
|
|
867
880
|
nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
868
881
|
const croppedFaces = shouldCropFaces
|
|
869
882
|
? await getFaceImages(
|
|
870
|
-
|
|
883
|
+
primaryFaces,
|
|
871
884
|
image ?? '',
|
|
872
885
|
frameWidth,
|
|
873
886
|
frameHeight
|
|
@@ -876,95 +889,41 @@ const IdentityDocumentCamera = ({
|
|
|
876
889
|
|
|
877
890
|
// Validate document plane consistency across all captures
|
|
878
891
|
let facePositionValid = true;
|
|
879
|
-
if (
|
|
880
|
-
const currentFaceBounds =
|
|
892
|
+
if (primaryFaces.length > 0 && primaryFaces[0]) {
|
|
893
|
+
const currentFaceBounds = primaryFaces[0].bounds;
|
|
881
894
|
if (lastFacePosition.current) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
);
|
|
887
|
-
const yDiff = Math.abs(
|
|
888
|
-
currentFaceBounds.y - lastFacePosition.current.y
|
|
889
|
-
);
|
|
890
|
-
const widthDiff = Math.abs(
|
|
891
|
-
currentFaceBounds.width - lastFacePosition.current.width
|
|
892
|
-
);
|
|
893
|
-
const heightDiff = Math.abs(
|
|
894
|
-
currentFaceBounds.height - lastFacePosition.current.height
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
const tolerance = nextStep === 'SCAN_HOLOGRAM' ? 0.5 : 0.2;
|
|
898
|
-
const xTolerance = lastFacePosition.current.width * tolerance;
|
|
899
|
-
const yTolerance = lastFacePosition.current.height * tolerance;
|
|
900
|
-
const sizeTolerance = lastFacePosition.current.width * tolerance;
|
|
901
|
-
|
|
902
|
-
facePositionValid =
|
|
903
|
-
xDiff <= xTolerance &&
|
|
904
|
-
yDiff <= yTolerance &&
|
|
905
|
-
widthDiff <= sizeTolerance &&
|
|
906
|
-
heightDiff <= sizeTolerance;
|
|
907
|
-
|
|
908
|
-
if (!facePositionValid) {
|
|
909
|
-
if (isDebugEnabled()) {
|
|
910
|
-
console.log(
|
|
911
|
-
`[DocPlane] Face position changed - document moved (xDiff=${xDiff.toFixed(1)}, yDiff=${yDiff.toFixed(1)}) - rejecting frame`
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Update reference position to follow gradual movement (sliding window)
|
|
917
|
-
lastFacePosition.current = {
|
|
918
|
-
x: currentFaceBounds.x,
|
|
919
|
-
y: currentFaceBounds.y,
|
|
920
|
-
width: currentFaceBounds.width,
|
|
921
|
-
height: currentFaceBounds.height,
|
|
922
|
-
};
|
|
923
|
-
} else {
|
|
924
|
-
// First capture - store reference position
|
|
925
|
-
lastFacePosition.current = {
|
|
926
|
-
x: currentFaceBounds.x,
|
|
927
|
-
y: currentFaceBounds.y,
|
|
928
|
-
width: currentFaceBounds.width,
|
|
929
|
-
height: currentFaceBounds.height,
|
|
930
|
-
};
|
|
931
|
-
console.log(
|
|
932
|
-
'[DocPlane] Stored reference face position for document plane validation'
|
|
895
|
+
facePositionValid = validateFacePosition(
|
|
896
|
+
currentFaceBounds,
|
|
897
|
+
lastFacePosition.current,
|
|
898
|
+
nextStep === 'SCAN_HOLOGRAM'
|
|
933
899
|
);
|
|
934
900
|
}
|
|
935
901
|
|
|
936
|
-
// Update
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const screenAspect = screen.width / screen.height;
|
|
944
|
-
|
|
945
|
-
let scale: number;
|
|
946
|
-
let offsetX = 0;
|
|
947
|
-
let offsetY = 0;
|
|
948
|
-
|
|
949
|
-
if (frameAspect > screenAspect) {
|
|
950
|
-
// Frame is wider - scale by height, crop width
|
|
951
|
-
scale = screen.height / frameDimensions.height;
|
|
952
|
-
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
953
|
-
} else {
|
|
954
|
-
// Frame is taller - scale by width, crop height
|
|
955
|
-
scale = screen.width / frameDimensions.width;
|
|
956
|
-
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
957
|
-
}
|
|
902
|
+
// Update reference position (sliding window)
|
|
903
|
+
lastFacePosition.current = {
|
|
904
|
+
x: currentFaceBounds.x,
|
|
905
|
+
y: currentFaceBounds.y,
|
|
906
|
+
width: currentFaceBounds.width,
|
|
907
|
+
height: currentFaceBounds.height,
|
|
908
|
+
};
|
|
958
909
|
|
|
910
|
+
// Update debug overlay bounds
|
|
911
|
+
if (facePositionValid && frameDimensions) {
|
|
912
|
+
const { scale, offsetX, offsetY } = getFrameToScreenTransform(
|
|
913
|
+
frameDimensions.width,
|
|
914
|
+
frameDimensions.height
|
|
915
|
+
);
|
|
959
916
|
const cropPadding = Math.max(
|
|
960
917
|
currentFaceBounds.width * 0.15,
|
|
961
918
|
currentFaceBounds.height * 0.15
|
|
962
919
|
);
|
|
963
920
|
setDocumentPlaneBounds({
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
921
|
+
...transformBoundsToScreen(
|
|
922
|
+
currentFaceBounds,
|
|
923
|
+
scale,
|
|
924
|
+
offsetX,
|
|
925
|
+
offsetY
|
|
926
|
+
),
|
|
968
927
|
cropPadding: cropPadding * scale,
|
|
969
928
|
});
|
|
970
929
|
}
|
|
@@ -977,38 +936,40 @@ const IdentityDocumentCamera = ({
|
|
|
977
936
|
shouldCropFaces &&
|
|
978
937
|
croppedFaces.length > 0 &&
|
|
979
938
|
croppedFaces[0] &&
|
|
980
|
-
facePositionValid
|
|
939
|
+
facePositionValid &&
|
|
940
|
+
!currentFaceImage
|
|
981
941
|
) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
942
|
+
// Validate face quality before accepting
|
|
943
|
+
const faceFullyVisible =
|
|
944
|
+
primaryFaces[0] &&
|
|
945
|
+
isFaceFullyVisible(primaryFaces[0], frameWidth, frameHeight);
|
|
946
|
+
const hasGlare = await checkFaceGlare(croppedFaces[0]);
|
|
947
|
+
|
|
948
|
+
if (!faceFullyVisible || hasGlare) {
|
|
949
|
+
// Reject face with glare or partially visible
|
|
986
950
|
if (isDebugEnabled()) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
);
|
|
951
|
+
debugLog('IdentityDocumentCamera', '[FACE] Rejected', {
|
|
952
|
+
fullyVisible: faceFullyVisible,
|
|
953
|
+
hasGlare,
|
|
954
|
+
});
|
|
992
955
|
}
|
|
956
|
+
// Continue scanning without locking this face
|
|
957
|
+
} else {
|
|
958
|
+
faceImageToUse = croppedFaces[0];
|
|
959
|
+
setCurrentFaceImage(croppedFaces[0]);
|
|
993
960
|
}
|
|
994
961
|
}
|
|
995
962
|
|
|
963
|
+
// Skip OCR text checks during SCAN_HOLOGRAM - flash and tilting make text unreadable
|
|
964
|
+
// but we only need face detection for hologram collection
|
|
996
965
|
if (!text || text.length < 5 || !image) {
|
|
997
|
-
|
|
998
|
-
|
|
966
|
+
if (nextStep !== 'SCAN_HOLOGRAM') {
|
|
967
|
+
setStatus('SEARCHING');
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
// During SCAN_HOLOGRAM, allow processing even if text is not readable
|
|
999
971
|
}
|
|
1000
972
|
|
|
1001
|
-
// Use JavaScript MRZ validation with corrections
|
|
1002
|
-
// Prefer MRZ-only text if available (from detected MRZ blocks),
|
|
1003
|
-
// otherwise fall back to all text (for backward compatibility)
|
|
1004
|
-
const textForValidation = scannedText?.mrzOnlyText || text;
|
|
1005
|
-
const mrzValidationResult =
|
|
1006
|
-
mrzUtils.validateMRZWithCorrections(textForValidation);
|
|
1007
|
-
const parsedMRZData = {
|
|
1008
|
-
valid: mrzValidationResult.valid,
|
|
1009
|
-
fields: mrzValidationResult.fields || null,
|
|
1010
|
-
};
|
|
1011
|
-
|
|
1012
973
|
// Capture test mode data
|
|
1013
974
|
if (testMode && text && text.includes('<')) {
|
|
1014
975
|
const mrzOnlyText = scannedText?.mrzOnlyText || text;
|
|
@@ -1018,36 +979,12 @@ const IdentityDocumentCamera = ({
|
|
|
1018
979
|
});
|
|
1019
980
|
}
|
|
1020
981
|
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1023
|
-
const mrzLines = text
|
|
1024
|
-
.split('\n')
|
|
1025
|
-
.filter((line) => line.includes('<') && line.length > 20);
|
|
1026
|
-
if (mrzLines.length >= 2) {
|
|
1027
|
-
console.log(
|
|
1028
|
-
'[MRZ Debug] Raw OCR text lines:',
|
|
1029
|
-
mrzLines.map((l) => `"${l}"`)
|
|
1030
|
-
);
|
|
1031
|
-
console.log('[MRZ Debug] Validation result:', {
|
|
1032
|
-
valid: mrzValidationResult.valid,
|
|
1033
|
-
format: mrzValidationResult.format,
|
|
1034
|
-
documentCode: mrzValidationResult.fields?.documentCode,
|
|
1035
|
-
documentNumber: mrzValidationResult.fields?.documentNumber,
|
|
1036
|
-
optional1: mrzValidationResult.fields?.optional1,
|
|
1037
|
-
error: mrzValidationResult.error,
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Extract raw MRZ lines from text if validation succeeded
|
|
1043
|
-
const mrzText = parsedMRZData.valid ? mrzUtils.fixMRZ(text) : null;
|
|
1044
|
-
|
|
1045
|
-
// MRZ stability check - require consistent valid reads to avoid OCR noise
|
|
1046
|
-
// Compare parsed field values instead of raw text to handle OCR variations in filler characters
|
|
1047
|
-
// Only proceed with MRZ if it's actually valid and has all required fields
|
|
982
|
+
// MRZ stability: require consistent valid reads across frames
|
|
983
|
+
// Skip during SCAN_HOLOGRAM - document type already locked
|
|
1048
984
|
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
1049
985
|
|
|
1050
986
|
if (
|
|
987
|
+
nextStep !== 'SCAN_HOLOGRAM' &&
|
|
1051
988
|
mrzText &&
|
|
1052
989
|
parsedMRZData?.valid === true &&
|
|
1053
990
|
parsedMRZData?.fields &&
|
|
@@ -1056,23 +993,14 @@ const IdentityDocumentCamera = ({
|
|
|
1056
993
|
const currentFields = parsedMRZData.fields;
|
|
1057
994
|
|
|
1058
995
|
if (areMRZFieldsEqual(lastValidMRZFields.current, currentFields)) {
|
|
1059
|
-
// Same MRZ data detected again - increment counter
|
|
1060
996
|
validMRZConsecutiveCount.current++;
|
|
1061
997
|
} else {
|
|
1062
|
-
// Different MRZ data - reset counter and store new data
|
|
1063
|
-
if (isDebugEnabled()) {
|
|
1064
|
-
console.log(
|
|
1065
|
-
`[MRZ Stability] New MRZ detected, resetting counter (was ${validMRZConsecutiveCount.current}, doc: ${currentFields.documentNumber?.slice(-4)})`
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
998
|
lastValidMRZFields.current = currentFields;
|
|
1069
999
|
lastValidMRZText.current = mrzText;
|
|
1070
1000
|
validMRZConsecutiveCount.current = 1;
|
|
1071
1001
|
}
|
|
1072
|
-
} else {
|
|
1073
|
-
// Invalid or no MRZ - don't reset completely, just skip this frame
|
|
1074
|
-
// This allows temporary OCR noise without losing progress
|
|
1075
1002
|
}
|
|
1003
|
+
// else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
|
|
1076
1004
|
|
|
1077
1005
|
// Check if we have enough consistent valid reads
|
|
1078
1006
|
const mrzStableAndValid =
|
|
@@ -1080,276 +1008,149 @@ const IdentityDocumentCamera = ({
|
|
|
1080
1008
|
parsedMRZData?.valid === true &&
|
|
1081
1009
|
areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
|
|
1082
1010
|
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1011
|
+
// ============================================================================
|
|
1012
|
+
// SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
|
|
1013
|
+
// ============================================================================
|
|
1086
1014
|
if (nextStep === 'SCAN_ID_BACK') {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1015
|
+
const handleIdBackStep = async () => {
|
|
1016
|
+
// Guard: wrong side detection (front or passport when back is expected)
|
|
1017
|
+
const hasFaces = primaryFaces.length > 0;
|
|
1018
|
+
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
1019
|
+
const hasPassportMRZ = parsedMRZData?.fields?.documentCode === 'P';
|
|
1020
|
+
const hasPassportMRZPattern =
|
|
1021
|
+
mrzText && PASSPORT_MRZ_PATTERN.test(mrzText);
|
|
1094
1022
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
`[ID_BACK Scan] Wrong side detected - faces: ${hasFaces}, signature: ${hasSignature}, passport MRZ: ${hasPassportMRZ}, passport pattern: ${hasPassportMRZPattern}`
|
|
1104
|
-
);
|
|
1023
|
+
if (
|
|
1024
|
+
hasFaces ||
|
|
1025
|
+
hasSignature ||
|
|
1026
|
+
hasPassportMRZ ||
|
|
1027
|
+
hasPassportMRZPattern
|
|
1028
|
+
) {
|
|
1029
|
+
setStatus('INCORRECT');
|
|
1030
|
+
return;
|
|
1105
1031
|
}
|
|
1106
|
-
setStatus('INCORRECT');
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
1032
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (isDebugEnabled()) {
|
|
1114
|
-
console.log(
|
|
1115
|
-
'[ID_BACK Scan] ERROR: Passport detected in ID_BACK step - skipping to COMPLETED'
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
1119
|
-
setTimeout(() => {
|
|
1120
|
-
onIdentityDocumentScanned({
|
|
1033
|
+
// Safety: passport somehow reached ID_BACK step
|
|
1034
|
+
if (detectedDocumentType === 'PASSPORT') {
|
|
1035
|
+
transitionStepWithCallback('COMPLETED', 'SCAN_ID_BACK', {
|
|
1121
1036
|
image,
|
|
1122
1037
|
documentType: 'PASSPORT',
|
|
1123
1038
|
mrzText: mrzText ?? undefined,
|
|
1124
1039
|
mrzFields: parsedMRZData?.fields,
|
|
1125
1040
|
});
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1129
1043
|
|
|
1130
|
-
|
|
1131
|
-
const hasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
1132
|
-
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
1133
|
-
// AND ensure all required fields are present
|
|
1134
|
-
const mrzAccepted =
|
|
1135
|
-
parsedMRZData?.valid === true &&
|
|
1136
|
-
hasRequiredFields &&
|
|
1137
|
-
mrzStableAndValid;
|
|
1138
|
-
|
|
1139
|
-
// For Turkish ID cards, barcode should match MRZ optional1 (serial number)
|
|
1140
|
-
// But some cards have encoding differences, so be lenient
|
|
1141
|
-
const barcodeMatchesMRZ =
|
|
1142
|
-
barcodeToUse?.rawValue?.trim() ===
|
|
1143
|
-
parsedMRZData?.fields?.optional1?.trim();
|
|
1144
|
-
|
|
1145
|
-
// If barcode doesn't match exactly, check if it contains the optional1 value
|
|
1146
|
-
const barcodeContainsMRZ =
|
|
1147
|
-
barcodeToUse?.rawValue?.includes(
|
|
1148
|
-
parsedMRZData?.fields?.optional1?.trim() || ''
|
|
1149
|
-
) ||
|
|
1150
|
-
parsedMRZData?.fields?.optional1?.includes(
|
|
1151
|
-
barcodeToUse?.rawValue?.trim() || ''
|
|
1152
|
-
);
|
|
1044
|
+
setElementsOutsideScanArea([]);
|
|
1153
1045
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1046
|
+
const flowResult = handleIDBackFlow(
|
|
1047
|
+
mrzText,
|
|
1048
|
+
parsedMRZData?.fields,
|
|
1049
|
+
parsedMRZData?.valid === true,
|
|
1050
|
+
mrzStableAndValid,
|
|
1051
|
+
hasRequiredMRZFields(parsedMRZData?.fields),
|
|
1052
|
+
barcodeToUse?.rawValue,
|
|
1053
|
+
onlyMRZScan
|
|
1054
|
+
);
|
|
1160
1055
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1056
|
+
if (!flowResult.shouldProceed) {
|
|
1057
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1058
|
+
setStatus(mrzText ? 'SCANNING' : 'SEARCHING');
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1166
1061
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1062
|
+
// Check for glare on ID back before accepting
|
|
1063
|
+
const hasGlare = await checkDocumentGlare(
|
|
1064
|
+
image,
|
|
1065
|
+
frameWidth,
|
|
1066
|
+
frameHeight
|
|
1067
|
+
);
|
|
1068
|
+
if (hasGlare) {
|
|
1069
|
+
if (isDebugEnabled()) {
|
|
1070
|
+
debugLog(
|
|
1071
|
+
'IdentityDocumentCamera',
|
|
1072
|
+
'[ID_BACK] Rejected - glare detected'
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
setStatus('SCANNING');
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1169
1078
|
|
|
1170
|
-
if (!allRequiredElementsInFrame && hasMRZ && mrzAccepted) {
|
|
1171
1079
|
if (isDebugEnabled()) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1080
|
+
debugLog('IdentityDocumentCamera', '[ID_BACK] MRZ validated', {
|
|
1081
|
+
documentNumber: parsedMRZData?.fields?.documentNumber,
|
|
1082
|
+
reads: validMRZConsecutiveCount.current,
|
|
1083
|
+
});
|
|
1175
1084
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1181
|
-
hasMRZ &&
|
|
1182
|
-
mrzAccepted &&
|
|
1183
|
-
barcodeAccepted &&
|
|
1184
|
-
allRequiredElementsInFrame
|
|
1185
|
-
) {
|
|
1186
|
-
logMRZDetails(
|
|
1187
|
-
'ID_BACK Scan',
|
|
1188
|
-
parsedMRZData?.fields,
|
|
1189
|
-
mrzText,
|
|
1190
|
-
validMRZConsecutiveCount.current,
|
|
1191
|
-
isDebugEnabled()
|
|
1192
|
-
);
|
|
1193
|
-
const scannedData: DocumentScannedData = {
|
|
1085
|
+
setDetectedDocumentType('ID_BACK');
|
|
1086
|
+
setStatus('SCANNED');
|
|
1087
|
+
transitionStepWithCallback('COMPLETED', 'SCAN_ID_BACK', {
|
|
1194
1088
|
image,
|
|
1195
1089
|
documentType: 'ID_BACK',
|
|
1196
1090
|
mrzText: mrzText ?? undefined,
|
|
1197
1091
|
mrzFields: parsedMRZData?.fields,
|
|
1198
1092
|
barcodeValue: barcodeToUse?.rawValue ?? undefined,
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
1204
|
-
setTimeout(() => {
|
|
1205
|
-
onIdentityDocumentScanned(scannedData);
|
|
1206
|
-
}, 1000);
|
|
1207
|
-
} else {
|
|
1208
|
-
if (hasMRZ && !mrzAccepted) {
|
|
1209
|
-
logMRZValidationFailure(
|
|
1210
|
-
'ID_BACK Scan',
|
|
1211
|
-
hasRequiredFields,
|
|
1212
|
-
parsedMRZData,
|
|
1213
|
-
mrzDetectionCurrentRetryCount.current,
|
|
1214
|
-
isDebugEnabled()
|
|
1215
|
-
);
|
|
1216
|
-
} else if (hasMRZ && mrzAccepted && !barcodeAccepted) {
|
|
1217
|
-
if (isDebugEnabled()) {
|
|
1218
|
-
console.log(
|
|
1219
|
-
'[ID_BACK Scan] MRZ valid but barcode check failed - retrying',
|
|
1220
|
-
{
|
|
1221
|
-
onlyMRZScan,
|
|
1222
|
-
hasBarcodeValue: !!barcodeToUse?.rawValue,
|
|
1223
|
-
barcodeMatchesMRZ,
|
|
1224
|
-
barcodeContainsMRZ,
|
|
1225
|
-
mrzOptional1: parsedMRZData?.fields?.optional1,
|
|
1226
|
-
barcodeValue: barcodeToUse?.rawValue,
|
|
1227
|
-
barcodeValueTrimmed: barcodeToUse?.rawValue?.trim(),
|
|
1228
|
-
optional1Trimmed: parsedMRZData?.fields?.optional1?.trim(),
|
|
1229
|
-
barcodeSource:
|
|
1230
|
-
barcodeToUse === cachedBarcode.current
|
|
1231
|
-
? 'cached'
|
|
1232
|
-
: 'current',
|
|
1233
|
-
}
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
mrzDetectionCurrentRetryCount.current++;
|
|
1238
|
-
setStatus(hasMRZ ? 'SCANNING' : 'SEARCHING');
|
|
1239
|
-
}
|
|
1093
|
+
});
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
handleIdBackStep();
|
|
1240
1097
|
return;
|
|
1241
1098
|
}
|
|
1242
1099
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1100
|
+
// Turn off torch when ID_FRONT detected during initial scan
|
|
1101
|
+
if (
|
|
1102
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
1103
|
+
(documentType === 'ID_FRONT' || documentType === 'PASSPORT') &&
|
|
1104
|
+
isTorchOnRef.current
|
|
1105
|
+
) {
|
|
1106
|
+
setIsTorchOn(false);
|
|
1107
|
+
}
|
|
1250
1108
|
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1109
|
+
// Lock document type from stable quality frames during initial scan.
|
|
1110
|
+
// Also allow correcting ID_FRONT → PASSPORT when passport MRZ appears later.
|
|
1111
|
+
// Passport MRZ may not be visible in early frames (while signature is),
|
|
1112
|
+
// causing premature ID_FRONT lock that must be correctable.
|
|
1254
1113
|
if (
|
|
1255
1114
|
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
1256
|
-
detectedDocumentType === 'UNKNOWN'
|
|
1115
|
+
(detectedDocumentType === 'UNKNOWN' ||
|
|
1116
|
+
detectedDocumentType === 'ID_FRONT')
|
|
1257
1117
|
) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
docTypeToSet = 'PASSPORT';
|
|
1265
|
-
} else if (
|
|
1266
|
-
documentType === 'UNKNOWN' &&
|
|
1267
|
-
cardSizedFaces.length > 0 &&
|
|
1268
|
-
parsedMRZData?.fields?.documentCode === 'P'
|
|
1269
|
-
) {
|
|
1270
|
-
// Early passport detection: face detected + passport MRZ code (even if not fully valid yet)
|
|
1271
|
-
docTypeToSet = 'PASSPORT';
|
|
1272
|
-
} else if (documentType === 'ID_FRONT') {
|
|
1273
|
-
// Check if this is actually a passport based on MRZ code
|
|
1274
|
-
// Passports can be misdetected as ID_FRONT when signature-like text is visible
|
|
1275
|
-
if (parsedMRZData?.fields?.documentCode === 'P') {
|
|
1276
|
-
if (isDebugEnabled()) {
|
|
1277
|
-
console.log(
|
|
1278
|
-
'[DocType] Correcting misdetection: ID_FRONT → PASSPORT (MRZ code P)'
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
docTypeToSet = 'PASSPORT';
|
|
1282
|
-
} else if (parsedMRZData?.fields?.documentCode === 'I') {
|
|
1283
|
-
// MRZ confirms it's an ID card
|
|
1284
|
-
docTypeToSet = 'ID_FRONT';
|
|
1285
|
-
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
1286
|
-
// Passport MRZ pattern visible but not parsed yet - wait for proper classification
|
|
1287
|
-
if (isDebugEnabled()) {
|
|
1288
|
-
console.log(
|
|
1289
|
-
'[DocType] Passport MRZ pattern detected but not parsed - waiting instead of locking as ID_FRONT'
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
docTypeToSet = 'UNKNOWN';
|
|
1293
|
-
} else {
|
|
1294
|
-
// No MRZ code and no passport pattern - safe to classify as ID_FRONT
|
|
1295
|
-
// ID cards typically don't have MRZ on front (only on back)
|
|
1296
|
-
docTypeToSet = 'ID_FRONT';
|
|
1297
|
-
}
|
|
1298
|
-
} else {
|
|
1299
|
-
docTypeToSet = 'UNKNOWN';
|
|
1300
|
-
}
|
|
1118
|
+
const docTypeToSet = determineDocumentTypeToSet(
|
|
1119
|
+
documentType,
|
|
1120
|
+
primaryFaces,
|
|
1121
|
+
parsedMRZData?.fields,
|
|
1122
|
+
mrzText
|
|
1123
|
+
);
|
|
1301
1124
|
|
|
1302
|
-
//
|
|
1303
|
-
// 1. Frame quality is acceptable (not blurry, good brightness)
|
|
1304
|
-
// 2. Document type has been detected consistently for multiple frames
|
|
1125
|
+
// If already locked as ID_FRONT, only allow correction to PASSPORT
|
|
1305
1126
|
if (
|
|
1306
|
-
|
|
1307
|
-
docTypeToSet
|
|
1127
|
+
detectedDocumentType === 'ID_FRONT' &&
|
|
1128
|
+
docTypeToSet === 'PASSPORT'
|
|
1308
1129
|
) {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1130
|
+
setDetectedDocumentType('PASSPORT');
|
|
1131
|
+
consistentDocTypeCount.current = 0;
|
|
1132
|
+
lastDetectedDocType.current = 'PASSPORT';
|
|
1133
|
+
} else if (detectedDocumentType === 'UNKNOWN') {
|
|
1134
|
+
if (
|
|
1135
|
+
lastFrameQuality.current.hasAcceptableQuality &&
|
|
1136
|
+
docTypeToSet !== 'UNKNOWN'
|
|
1137
|
+
) {
|
|
1138
|
+
if (docTypeToSet === lastDetectedDocType.current) {
|
|
1139
|
+
consistentDocTypeCount.current++;
|
|
1140
|
+
} else {
|
|
1141
|
+
lastDetectedDocType.current = docTypeToSet;
|
|
1142
|
+
consistentDocTypeCount.current = 1;
|
|
1315
1143
|
}
|
|
1316
1144
|
|
|
1317
1145
|
if (
|
|
1318
1146
|
consistentDocTypeCount.current >=
|
|
1319
1147
|
REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS
|
|
1320
1148
|
) {
|
|
1321
|
-
// Stable detection confirmed - lock it in
|
|
1322
|
-
if (isDebugEnabled()) {
|
|
1323
|
-
console.log(
|
|
1324
|
-
`[DocType] Locked document type: ${docTypeToSet} (stable for ${consistentDocTypeCount.current} quality frames)`
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1327
1149
|
setDetectedDocumentType(docTypeToSet);
|
|
1328
1150
|
}
|
|
1329
|
-
} else {
|
|
1330
|
-
// Document type changed - reset counter
|
|
1331
|
-
if (isDebugEnabled()) {
|
|
1332
|
-
console.log(
|
|
1333
|
-
`[DocType Stability] Type changed: ${lastDetectedDocType.current} → ${docTypeToSet}, resetting counter`
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
lastDetectedDocType.current = docTypeToSet;
|
|
1337
|
-
consistentDocTypeCount.current = 1;
|
|
1338
|
-
}
|
|
1339
|
-
} else if (
|
|
1340
|
-
!lastFrameQuality.current.hasAcceptableQuality &&
|
|
1341
|
-
docTypeToSet !== 'UNKNOWN'
|
|
1342
|
-
) {
|
|
1343
|
-
// Poor quality frame - don't use for document type detection
|
|
1344
|
-
if (isDebugEnabled()) {
|
|
1345
|
-
console.log(
|
|
1346
|
-
`[DocType Stability] Skipping poor quality frame (blurry: ${lastFrameQuality.current.isBlurry}, brightness: ${lastFrameQuality.current.brightness.toFixed(0)})`
|
|
1347
|
-
);
|
|
1348
1151
|
}
|
|
1349
1152
|
}
|
|
1350
1153
|
}
|
|
1351
|
-
// Document type is now locked and won't be changed after initial scan
|
|
1352
|
-
// Hologram and subsequent steps use the preserved detectedDocumentType state
|
|
1353
1154
|
|
|
1354
1155
|
const scannedData: DocumentScannedData = {
|
|
1355
1156
|
image,
|
|
@@ -1358,10 +1159,10 @@ const IdentityDocumentCamera = ({
|
|
|
1358
1159
|
mrzFields: parsedMRZData?.fields,
|
|
1359
1160
|
};
|
|
1360
1161
|
|
|
1361
|
-
|
|
1362
|
-
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
1363
|
-
|
|
1364
|
-
|
|
1162
|
+
if (
|
|
1163
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
1164
|
+
documentType === 'ID_BACK'
|
|
1165
|
+
) {
|
|
1365
1166
|
setStatus('INCORRECT');
|
|
1366
1167
|
return;
|
|
1367
1168
|
}
|
|
@@ -1371,238 +1172,366 @@ const IdentityDocumentCamera = ({
|
|
|
1371
1172
|
scannedData.faceImage = faceImageToUse;
|
|
1372
1173
|
}
|
|
1373
1174
|
|
|
1175
|
+
const continueScanning = (incrementMrzRetry: boolean = false) => {
|
|
1176
|
+
if (incrementMrzRetry) {
|
|
1177
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1178
|
+
}
|
|
1179
|
+
setStatus('SCANNING');
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1374
1182
|
if (!onlyMRZScan) {
|
|
1375
|
-
// Hologram detection during SCAN_HOLOGRAM step
|
|
1183
|
+
// Hologram detection during SCAN_HOLOGRAM step
|
|
1376
1184
|
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
`[Hologram] In SCAN_HOLOGRAM step - currentHologramImage: ${!!currentHologramImage}, collected: ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT}`
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
// Always crop to the same face region across all hologram frames so
|
|
1384
|
-
// OpenCV receives consistently-sized images for comparison.
|
|
1385
|
-
// Use current face bounds if available, otherwise fall back to last known position.
|
|
1185
|
+
// CRITICAL: Only use faces inside scan area for hologram
|
|
1186
|
+
// This prevents passport secondary faces (outside frame or on right side) from being used
|
|
1386
1187
|
const hologramFaceBounds =
|
|
1387
|
-
|
|
1388
|
-
?
|
|
1188
|
+
facesInsideScanArea.length > 0 && facesInsideScanArea[0]
|
|
1189
|
+
? facesInsideScanArea[0].bounds
|
|
1389
1190
|
: lastFacePosition.current;
|
|
1390
1191
|
let primaryFaceOnly: string | undefined;
|
|
1391
1192
|
if (hologramFaceBounds && image) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1193
|
+
// Verify face is fully visible before using for hologram
|
|
1194
|
+
const faceFullyVisible = hologramFaceBounds
|
|
1195
|
+
? hologramFaceBounds.x >= frameWidth * FACE_EDGE_MARGIN_PERCENT &&
|
|
1196
|
+
hologramFaceBounds.y >=
|
|
1197
|
+
frameHeight * FACE_EDGE_MARGIN_PERCENT &&
|
|
1198
|
+
hologramFaceBounds.x + hologramFaceBounds.width <=
|
|
1199
|
+
frameWidth * (1 - FACE_EDGE_MARGIN_PERCENT) &&
|
|
1200
|
+
hologramFaceBounds.y + hologramFaceBounds.height <=
|
|
1201
|
+
frameHeight * (1 - FACE_EDGE_MARGIN_PERCENT)
|
|
1202
|
+
: false;
|
|
1203
|
+
|
|
1204
|
+
if (faceFullyVisible) {
|
|
1205
|
+
const hologramCropped = await getFaceImages(
|
|
1206
|
+
[{ bounds: hologramFaceBounds, rollAngle: 0, yawAngle: 0 }],
|
|
1207
|
+
image,
|
|
1208
|
+
frameWidth,
|
|
1209
|
+
frameHeight,
|
|
1210
|
+
true // widerRightPadding for hologram detection
|
|
1211
|
+
);
|
|
1212
|
+
primaryFaceOnly = hologramCropped[0] ?? faceImageToUse;
|
|
1213
|
+
} else {
|
|
1214
|
+
// Face not fully visible - skip this frame
|
|
1215
|
+
if (isDebugEnabled()) {
|
|
1216
|
+
debugLog(
|
|
1217
|
+
'IdentityDocumentCamera',
|
|
1218
|
+
'[HOLOGRAM] Face not fully visible',
|
|
1219
|
+
{
|
|
1220
|
+
faceX: hologramFaceBounds.x,
|
|
1221
|
+
faceY: hologramFaceBounds.y,
|
|
1222
|
+
faceWidth: hologramFaceBounds.width,
|
|
1223
|
+
faceHeight: hologramFaceBounds.height,
|
|
1224
|
+
frameWidth,
|
|
1225
|
+
frameHeight,
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
primaryFaceOnly = undefined;
|
|
1230
|
+
}
|
|
1399
1231
|
} else {
|
|
1400
1232
|
primaryFaceOnly = faceImageToUse;
|
|
1401
1233
|
}
|
|
1402
1234
|
|
|
1403
1235
|
// Skip face position validation for hologram — flash toggling causes position jitter
|
|
1404
1236
|
if (primaryFaceOnly) {
|
|
1405
|
-
// Reset consecutive no-face counter since we have a face
|
|
1406
1237
|
hologramFramesWithoutFace.current = 0;
|
|
1407
1238
|
|
|
1408
1239
|
if (currentHologramImage) {
|
|
1409
1240
|
scannedData.hologramImage = currentHologramImage;
|
|
1410
1241
|
} else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
|
|
1411
|
-
//
|
|
1412
|
-
const now = Date.now();
|
|
1242
|
+
// Space out captures for better variation
|
|
1413
1243
|
const timeSinceLastCapture =
|
|
1414
|
-
now - lastHologramCaptureTime.current;
|
|
1244
|
+
Date.now() - lastHologramCaptureTime.current;
|
|
1415
1245
|
|
|
1416
1246
|
if (
|
|
1417
1247
|
faceImages.current.length === 0 ||
|
|
1418
1248
|
timeSinceLastCapture >= HOLOGRAM_CAPTURE_INTERVAL
|
|
1419
1249
|
) {
|
|
1420
|
-
//
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
hologramImageCountRef.current = faceImages.current.length;
|
|
1424
|
-
|
|
1425
|
-
// Only update state at first and last frame to minimize re-renders
|
|
1426
|
-
if (
|
|
1427
|
-
faceImages.current.length === 1 ||
|
|
1428
|
-
faceImages.current.length === HOLOGRAM_IMAGE_COUNT
|
|
1429
|
-
) {
|
|
1430
|
-
setHologramImageCount(faceImages.current.length);
|
|
1431
|
-
setLatestHologramFaceImage(primaryFaceOnly);
|
|
1250
|
+
// Keep torch on during hologram scan for consistent lighting
|
|
1251
|
+
if (!isTorchOnRef.current) {
|
|
1252
|
+
setIsTorchOn(true);
|
|
1432
1253
|
}
|
|
1433
1254
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1255
|
+
// Check hologram face for glare
|
|
1256
|
+
const hasGlare = await checkFaceGlare(primaryFaceOnly);
|
|
1257
|
+
if (!hasGlare) {
|
|
1258
|
+
faceImages.current.push(primaryFaceOnly);
|
|
1259
|
+
lastHologramCaptureTime.current = Date.now();
|
|
1260
|
+
hologramImageCountRef.current = faceImages.current.length;
|
|
1261
|
+
|
|
1262
|
+
if (isDebugEnabled()) {
|
|
1263
|
+
debugLog(
|
|
1264
|
+
'IdentityDocumentCamera',
|
|
1265
|
+
'[HOLOGRAM] Frame captured',
|
|
1266
|
+
{
|
|
1267
|
+
frameIndex: faceImages.current.length - 1,
|
|
1268
|
+
totalFrames: faceImages.current.length,
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
} else if (isDebugEnabled()) {
|
|
1273
|
+
debugLog(
|
|
1274
|
+
'IdentityDocumentCamera',
|
|
1275
|
+
'[HOLOGRAM] Rejected glare frame',
|
|
1276
|
+
{
|
|
1277
|
+
collectedCount: faceImages.current.length,
|
|
1278
|
+
}
|
|
1437
1279
|
);
|
|
1438
1280
|
}
|
|
1439
1281
|
|
|
1440
|
-
|
|
1282
|
+
if (faceImages.current.length > 0) {
|
|
1283
|
+
// Update UI state at first and last frame only
|
|
1284
|
+
if (
|
|
1285
|
+
faceImages.current.length === 1 ||
|
|
1286
|
+
faceImages.current.length === HOLOGRAM_IMAGE_COUNT
|
|
1287
|
+
) {
|
|
1288
|
+
setHologramImageCount(faceImages.current.length);
|
|
1289
|
+
setLatestHologramFaceImage(primaryFaceOnly);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1441
1292
|
}
|
|
1442
1293
|
} else if (faceImages.current.length >= HOLOGRAM_IMAGE_COUNT) {
|
|
1443
|
-
//
|
|
1444
|
-
if (
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1294
|
+
// Guard: skip if already processing or max retries exhausted
|
|
1295
|
+
if (isHologramDetectionInProgress.current) return;
|
|
1296
|
+
if (
|
|
1297
|
+
hologramDetectionCurrentRetryCount.current >=
|
|
1298
|
+
HOLOGRAM_DETECTION_RETRY_COUNT
|
|
1299
|
+
) {
|
|
1300
|
+
faceImages.current = [];
|
|
1301
|
+
hologramImageCountRef.current = 0;
|
|
1302
|
+
setHologramImageCount(0);
|
|
1303
|
+
setLatestHologramFaceImage(undefined);
|
|
1304
|
+
return;
|
|
1448
1305
|
}
|
|
1306
|
+
|
|
1307
|
+
// Process collected face images for hologram detection
|
|
1308
|
+
isHologramDetectionInProgress.current = true;
|
|
1449
1309
|
try {
|
|
1450
1310
|
const [hologramMask, hologram] = await detectHologramNative(
|
|
1451
1311
|
faceImages.current
|
|
1452
1312
|
);
|
|
1313
|
+
if (isDebugEnabled()) {
|
|
1314
|
+
debugLog(
|
|
1315
|
+
'IdentityDocumentCamera',
|
|
1316
|
+
'[Hologram] Native result',
|
|
1317
|
+
{
|
|
1318
|
+
hasHologram: !!hologram,
|
|
1319
|
+
hasHologramMask: !!hologramMask,
|
|
1320
|
+
hologramLength: hologram?.length || 0,
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1453
1324
|
if (hologram) {
|
|
1454
1325
|
setCurrentHologramMaskImage(hologramMask);
|
|
1455
1326
|
scannedData.hologramImage = hologram;
|
|
1456
1327
|
setCurrentHologramImage(hologram);
|
|
1457
1328
|
if (isDebugEnabled()) {
|
|
1458
|
-
|
|
1329
|
+
debugLog(
|
|
1330
|
+
'IdentityDocumentCamera',
|
|
1331
|
+
'[Hologram] ✓ Saved hologram image'
|
|
1332
|
+
);
|
|
1459
1333
|
}
|
|
1460
1334
|
} else {
|
|
1461
1335
|
if (isDebugEnabled()) {
|
|
1462
|
-
|
|
1336
|
+
debugLog(
|
|
1337
|
+
'IdentityDocumentCamera',
|
|
1338
|
+
'[Hologram] ✗ No hologram detected'
|
|
1339
|
+
);
|
|
1463
1340
|
}
|
|
1464
1341
|
}
|
|
1465
1342
|
} catch (error) {
|
|
1466
1343
|
console.error('[Hologram] Processing error:', error);
|
|
1467
1344
|
} finally {
|
|
1468
|
-
// Keep flash on - will turn off when step changes
|
|
1469
1345
|
faceImages.current = [];
|
|
1470
1346
|
hologramImageCountRef.current = 0;
|
|
1471
1347
|
setHologramImageCount(0);
|
|
1472
1348
|
setLatestHologramFaceImage(undefined);
|
|
1473
1349
|
hologramDetectionCurrentRetryCount.current++;
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1350
|
+
isHologramDetectionInProgress.current = false;
|
|
1351
|
+
// Turn off torch after detection completes
|
|
1352
|
+
setIsTorchOn(false);
|
|
1479
1353
|
}
|
|
1480
1354
|
}
|
|
1481
1355
|
} else {
|
|
1482
|
-
// No face detected for hologram collection
|
|
1483
|
-
// Track consecutive frames without face for safety timeout
|
|
1484
1356
|
hologramFramesWithoutFace.current++;
|
|
1485
|
-
if (isDebugEnabled()) {
|
|
1486
|
-
console.log(
|
|
1487
|
-
`[Hologram] No face detected - frame ${hologramFramesWithoutFace.current}/${HOLOGRAM_MAX_FRAMES_WITHOUT_FACE}`
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
1357
|
}
|
|
1491
1358
|
} else if (currentHologramImage) {
|
|
1492
1359
|
scannedData.hologramImage = currentHologramImage;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Secondary face capture (continuous during initial scan and hologram detection)
|
|
1363
|
+
// MLI (Multi Layer Image) is small secondary face on Turkish ID cards
|
|
1364
|
+
if (
|
|
1365
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ||
|
|
1366
|
+
nextStep === 'SCAN_HOLOGRAM'
|
|
1367
|
+
) {
|
|
1368
|
+
if (isDebugEnabled() && allDetectedFaces.length > 1) {
|
|
1369
|
+
debugLog(
|
|
1370
|
+
'IdentityDocumentCamera',
|
|
1371
|
+
'[MLI FACE] Entry conditions check',
|
|
1372
|
+
{
|
|
1373
|
+
hasCurrentSecondary: !!currentSecondaryFaceImage,
|
|
1374
|
+
hasPrimaryFace: !!scannedData.faceImage,
|
|
1375
|
+
totalFaces: allDetectedFaces.length,
|
|
1376
|
+
facePositionValid,
|
|
1377
|
+
willAttemptDetection:
|
|
1378
|
+
!currentSecondaryFaceImage &&
|
|
1379
|
+
!!scannedData.faceImage &&
|
|
1380
|
+
allDetectedFaces.length > 1 &&
|
|
1381
|
+
facePositionValid,
|
|
1382
|
+
}
|
|
1502
1383
|
);
|
|
1503
1384
|
}
|
|
1504
|
-
}
|
|
1505
1385
|
|
|
1506
|
-
// SKIP secondary face detection during SCAN_HOLOGRAM - only focus on primary face
|
|
1507
|
-
// Secondary face was already captured during initial scan (SCAN_ID_FRONT_OR_PASSPORT)
|
|
1508
|
-
// During hologram, we only collect hologram images from primary face
|
|
1509
|
-
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1510
|
-
// Capture secondary face - must be similar to main face AND from same document plane
|
|
1511
1386
|
if (currentSecondaryFaceImage) {
|
|
1512
1387
|
scannedData.secondaryFaceImage = currentSecondaryFaceImage;
|
|
1513
1388
|
} else if (
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
!!croppedFaces[1] &&
|
|
1389
|
+
scannedData.faceImage &&
|
|
1390
|
+
allDetectedFaces.length > 1 &&
|
|
1517
1391
|
facePositionValid
|
|
1518
1392
|
) {
|
|
1519
|
-
//
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1393
|
+
// Detect MLI face (smaller than main face, to the right)
|
|
1394
|
+
const primaryFace = primaryFaces[0];
|
|
1395
|
+
|
|
1396
|
+
if (isDebugEnabled() && primaryFace) {
|
|
1397
|
+
debugLog(
|
|
1398
|
+
'IdentityDocumentCamera',
|
|
1399
|
+
'[MLI FACE] Starting detection',
|
|
1400
|
+
{
|
|
1401
|
+
totalFaces: allDetectedFaces.length,
|
|
1402
|
+
primaryFaceSize: `${Math.round(primaryFace.bounds.width)}x${Math.round(primaryFace.bounds.height)}`,
|
|
1403
|
+
primaryFacePos: `x:${Math.round(primaryFace.bounds.x)} y:${Math.round(primaryFace.bounds.y)}`,
|
|
1404
|
+
otherFaces: allDetectedFaces
|
|
1405
|
+
.filter((f) => f !== primaryFace)
|
|
1406
|
+
.map((f) => ({
|
|
1407
|
+
size: `${Math.round(f.bounds.width)}x${Math.round(f.bounds.height)}`,
|
|
1408
|
+
pos: `x:${Math.round(f.bounds.x)} y:${Math.round(f.bounds.y)}`,
|
|
1409
|
+
isRight: f.bounds.x > primaryFace.bounds.x,
|
|
1410
|
+
verticalRange: `${Math.round(primaryFace.bounds.y - primaryFace.bounds.height * 0.3)}-${Math.round(primaryFace.bounds.y + primaryFace.bounds.height * 1.3)}`,
|
|
1411
|
+
inVerticalRange:
|
|
1412
|
+
f.bounds.y >=
|
|
1413
|
+
primaryFace.bounds.y -
|
|
1414
|
+
primaryFace.bounds.height * 0.3 &&
|
|
1415
|
+
f.bounds.y <=
|
|
1416
|
+
primaryFace.bounds.y +
|
|
1417
|
+
primaryFace.bounds.height * 1.3,
|
|
1418
|
+
})),
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const potentialMLIFaces = allDetectedFaces.filter(
|
|
1424
|
+
(f) =>
|
|
1425
|
+
f !== primaryFace &&
|
|
1426
|
+
f.bounds.x > primaryFace.bounds.x && // MLI is to the right of main face
|
|
1427
|
+
f.bounds.y >=
|
|
1428
|
+
primaryFace.bounds.y - primaryFace.bounds.height * 0.3 && // Same vertical level (±30%)
|
|
1429
|
+
f.bounds.y <=
|
|
1430
|
+
primaryFace.bounds.y + primaryFace.bounds.height * 1.3
|
|
1524
1431
|
);
|
|
1525
1432
|
|
|
1526
|
-
if (
|
|
1527
|
-
|
|
1528
|
-
|
|
1433
|
+
if (isDebugEnabled()) {
|
|
1434
|
+
debugLog(
|
|
1435
|
+
'IdentityDocumentCamera',
|
|
1436
|
+
'[MLI FACE] Position filter result',
|
|
1437
|
+
{
|
|
1438
|
+
potentialMLICount: potentialMLIFaces.length,
|
|
1439
|
+
}
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1529
1442
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
const screen = Dimensions.get('window');
|
|
1533
|
-
const frameAspect =
|
|
1534
|
-
frameDimensions.width / frameDimensions.height;
|
|
1535
|
-
const screenAspect = screen.width / screen.height;
|
|
1443
|
+
if (potentialMLIFaces.length > 0 && potentialMLIFaces[0]) {
|
|
1444
|
+
const secondaryFace = potentialMLIFaces[0];
|
|
1536
1445
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1446
|
+
// Crop MLI face separately
|
|
1447
|
+
const mliFaceCropped = await getFaceImages(
|
|
1448
|
+
[primaryFace, secondaryFace],
|
|
1449
|
+
image ?? '',
|
|
1450
|
+
frameWidth,
|
|
1451
|
+
frameHeight
|
|
1452
|
+
);
|
|
1540
1453
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
(frameDimensions.height * scale - screen.height) / 2;
|
|
1548
|
-
}
|
|
1454
|
+
if (mliFaceCropped.length > 1 && mliFaceCropped[1]) {
|
|
1455
|
+
// Visual similarity check with lenient threshold
|
|
1456
|
+
const visualResult = await compareFaceVisualSimilarityNative(
|
|
1457
|
+
mliFaceCropped[0],
|
|
1458
|
+
mliFaceCropped[1]
|
|
1459
|
+
);
|
|
1549
1460
|
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
y + h <= scanBottom;
|
|
1564
|
-
|
|
1565
|
-
const secondaryBounds = faces[1].bounds;
|
|
1566
|
-
if (
|
|
1567
|
-
isInsideScan(
|
|
1568
|
-
secondaryBounds.x,
|
|
1569
|
-
secondaryBounds.y,
|
|
1570
|
-
secondaryBounds.width,
|
|
1571
|
-
secondaryBounds.height
|
|
1572
|
-
)
|
|
1573
|
-
) {
|
|
1574
|
-
setSecondaryFaceBounds({
|
|
1575
|
-
x: secondaryBounds.x * scale - offsetX,
|
|
1576
|
-
y: secondaryBounds.y * scale - offsetY,
|
|
1577
|
-
width: secondaryBounds.width * scale,
|
|
1578
|
-
height: secondaryBounds.height * scale,
|
|
1579
|
-
});
|
|
1580
|
-
} else {
|
|
1581
|
-
setSecondaryFaceBounds(null);
|
|
1461
|
+
const similarityScore = visualResult?.similarity || 0;
|
|
1462
|
+
const isLikelySamePerson = similarityScore >= 0.2; // Very lenient: 20%
|
|
1463
|
+
|
|
1464
|
+
if (isDebugEnabled()) {
|
|
1465
|
+
debugLog(
|
|
1466
|
+
'IdentityDocumentCamera',
|
|
1467
|
+
'[MLI FACE] Similarity check',
|
|
1468
|
+
{
|
|
1469
|
+
visualSimilarity: similarityScore.toFixed(3),
|
|
1470
|
+
isLikelySamePerson,
|
|
1471
|
+
threshold: 0.2,
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1582
1474
|
}
|
|
1583
|
-
}
|
|
1584
1475
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1476
|
+
if (isLikelySamePerson) {
|
|
1477
|
+
// Skip glare check for MLI - it's a small printed photo with different reflective properties
|
|
1478
|
+
// Backend will validate quality
|
|
1479
|
+
scannedData.secondaryFaceImage = mliFaceCropped[1];
|
|
1480
|
+
setCurrentSecondaryFaceImage(scannedData.secondaryFaceImage);
|
|
1481
|
+
|
|
1482
|
+
if (isDebugEnabled()) {
|
|
1483
|
+
debugLog(
|
|
1484
|
+
'IdentityDocumentCamera',
|
|
1485
|
+
'[MLI FACE] Captured successfully',
|
|
1486
|
+
{
|
|
1487
|
+
similarity: similarityScore.toFixed(3),
|
|
1488
|
+
imageLength: mliFaceCropped[1]?.length || 0,
|
|
1489
|
+
imageSet: !!scannedData.secondaryFaceImage,
|
|
1490
|
+
stateSet: !!currentSecondaryFaceImage,
|
|
1491
|
+
primarySize: `${Math.round(primaryFace.bounds.width)}x${Math.round(primaryFace.bounds.height)}`,
|
|
1492
|
+
secondarySize: `${Math.round(secondaryFace.bounds.width)}x${Math.round(secondaryFace.bounds.height)}`,
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (frameDimensions) {
|
|
1498
|
+
const { scale, offsetX, offsetY } =
|
|
1499
|
+
getFrameToScreenTransform(
|
|
1500
|
+
frameDimensions.width,
|
|
1501
|
+
frameDimensions.height
|
|
1502
|
+
);
|
|
1503
|
+
const secondaryBounds = secondaryFace.bounds;
|
|
1504
|
+
setSecondaryFaceBounds(
|
|
1505
|
+
transformBoundsToScreen(
|
|
1506
|
+
secondaryBounds,
|
|
1507
|
+
scale,
|
|
1508
|
+
offsetX,
|
|
1509
|
+
offsetY
|
|
1510
|
+
)
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
} else if (isDebugEnabled()) {
|
|
1514
|
+
debugLog(
|
|
1515
|
+
'IdentityDocumentCamera',
|
|
1516
|
+
'[MLI FACE] Rejected - similarity too low',
|
|
1517
|
+
{ similarity: similarityScore.toFixed(3), threshold: 0.2 }
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1596
1520
|
}
|
|
1597
1521
|
}
|
|
1598
1522
|
} else {
|
|
1599
1523
|
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
1600
|
-
if (
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1524
|
+
if (isDebugEnabled() && allDetectedFaces.length > 1) {
|
|
1525
|
+
debugLog(
|
|
1526
|
+
'IdentityDocumentCamera',
|
|
1527
|
+
'[MLI FACE] Conditions not met',
|
|
1528
|
+
{
|
|
1529
|
+
hasPrimaryFace: !!scannedData.faceImage,
|
|
1530
|
+
primaryFacesCount: primaryFaces.length,
|
|
1531
|
+
allDetectedFaces: allDetectedFaces.length,
|
|
1532
|
+
facePositionValid,
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1606
1535
|
}
|
|
1607
1536
|
}
|
|
1608
1537
|
} else if (currentSecondaryFaceImage) {
|
|
@@ -1611,324 +1540,198 @@ const IdentityDocumentCamera = ({
|
|
|
1611
1540
|
}
|
|
1612
1541
|
}
|
|
1613
1542
|
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1543
|
+
// ============================================================================
|
|
1544
|
+
// SCAN_HOLOGRAM STEP
|
|
1545
|
+
// ============================================================================
|
|
1616
1546
|
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
1617
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1547
|
+
// Guard: barcode visible = back side shown
|
|
1548
|
+
if (barcode?.rawValue) {
|
|
1549
|
+
setStatus('INCORRECT');
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1621
1552
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1553
|
+
const isCollecting = faceImages.current.length > 0;
|
|
1554
|
+
const maxRetriesReached =
|
|
1555
|
+
hologramDetectionCurrentRetryCount.current >=
|
|
1556
|
+
HOLOGRAM_DETECTION_RETRY_COUNT;
|
|
1625
1557
|
|
|
1626
|
-
if
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1558
|
+
// Wait for face if not yet started collecting
|
|
1559
|
+
if (!isCollecting && primaryFaces.length === 0) {
|
|
1560
|
+
hologramFramesWithoutFace.current++;
|
|
1561
|
+
if (
|
|
1562
|
+
hologramFramesWithoutFace.current >=
|
|
1563
|
+
HOLOGRAM_MAX_FRAMES_WITHOUT_FACE
|
|
1564
|
+
) {
|
|
1565
|
+
setStatus('INCORRECT');
|
|
1566
|
+
return;
|
|
1631
1567
|
}
|
|
1632
|
-
|
|
1568
|
+
continueScanning();
|
|
1633
1569
|
return;
|
|
1634
1570
|
}
|
|
1635
1571
|
|
|
1636
|
-
//
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1572
|
+
// Complete when hologram captured OR retries exhausted (not mid-collection)
|
|
1573
|
+
const stepComplete =
|
|
1574
|
+
(!!scannedData.hologramImage ||
|
|
1575
|
+
(maxRetriesReached && !isCollecting)) &&
|
|
1576
|
+
!!faceImageToUse; // Require face before completing
|
|
1577
|
+
|
|
1578
|
+
if (stepComplete) {
|
|
1579
|
+
// Ensure preserved MRZ data is included (current frame may not have
|
|
1580
|
+
// readable MRZ due to flash/tilting during hologram capture)
|
|
1581
|
+
if (!scannedData.mrzText && lastValidMRZText.current) {
|
|
1582
|
+
scannedData.mrzText = lastValidMRZText.current;
|
|
1583
|
+
}
|
|
1584
|
+
if (!scannedData.mrzFields && lastValidMRZFields.current) {
|
|
1585
|
+
scannedData.mrzFields = lastValidMRZFields.current;
|
|
1586
|
+
}
|
|
1639
1587
|
|
|
1640
|
-
|
|
1641
|
-
const isActivelyCollecting =
|
|
1642
|
-
faceImages.current.length > 0 &&
|
|
1643
|
-
faceImages.current.length < HOLOGRAM_IMAGE_COUNT;
|
|
1588
|
+
setStatus('SCANNED');
|
|
1644
1589
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
(
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
// During hologram scan, we ONLY care about hologram collection - no other checks
|
|
1653
|
-
// Secondary face, MRZ, document type checks are all skipped
|
|
1654
|
-
// Document type was already definitively determined in the initial scan phase
|
|
1655
|
-
|
|
1656
|
-
// Log detailed state for debugging
|
|
1657
|
-
if (isActivelyCollecting && isDebugEnabled()) {
|
|
1658
|
-
console.log(
|
|
1659
|
-
`[SCAN_HOLOGRAM] Actively collecting: ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT} - continuing collection`
|
|
1590
|
+
// Use scannedData.mrzFields which we just ensured has preserved MRZ
|
|
1591
|
+
const mrzDocCode = scannedData.mrzFields?.documentCode;
|
|
1592
|
+
const nextStepAfterHologram = getNextStepAfterHologram(
|
|
1593
|
+
detectedDocumentType,
|
|
1594
|
+
documentType,
|
|
1595
|
+
mrzDocCode
|
|
1660
1596
|
);
|
|
1661
|
-
}
|
|
1662
1597
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
setStatus('SCANNED');
|
|
1670
|
-
if (nextStep !== 'SCAN_HOLOGRAM') {
|
|
1671
|
-
setIsTorchOn(false);
|
|
1672
|
-
}
|
|
1673
|
-
// Route based on PRESERVED detectedDocumentType state (set during initial scan)
|
|
1674
|
-
// Also check current frame's documentType and MRZ code as fallback
|
|
1675
|
-
// Passport has no back side - go directly to COMPLETED
|
|
1676
|
-
const isPassport =
|
|
1677
|
-
detectedDocumentType === 'PASSPORT' ||
|
|
1678
|
-
documentType === 'PASSPORT' ||
|
|
1679
|
-
parsedMRZData?.fields?.documentCode === 'P';
|
|
1680
|
-
if (isDebugEnabled()) {
|
|
1681
|
-
console.log('[SCAN_HOLOGRAM] Document type check:', {
|
|
1682
|
-
detectedDocumentType,
|
|
1683
|
-
documentType,
|
|
1684
|
-
mrzCode: parsedMRZData?.fields?.documentCode,
|
|
1685
|
-
isPassport,
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
if (isPassport) {
|
|
1689
|
-
if (isDebugEnabled()) {
|
|
1690
|
-
console.log(
|
|
1691
|
-
'[SCAN_HOLOGRAM] Passport detected - completing scan (no back side)'
|
|
1692
|
-
);
|
|
1693
|
-
}
|
|
1694
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
|
|
1695
|
-
} else {
|
|
1696
|
-
if (isDebugEnabled()) {
|
|
1697
|
-
console.log(
|
|
1698
|
-
'[SCAN_HOLOGRAM] ID card detected - proceeding to back scan'
|
|
1699
|
-
);
|
|
1700
|
-
}
|
|
1701
|
-
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
|
|
1702
|
-
}
|
|
1703
|
-
setTimeout(() => {
|
|
1704
|
-
onIdentityDocumentScanned(scannedData);
|
|
1705
|
-
}, 1000);
|
|
1598
|
+
transitionStepWithCallback(
|
|
1599
|
+
nextStepAfterHologram,
|
|
1600
|
+
'SCAN_HOLOGRAM',
|
|
1601
|
+
scannedData
|
|
1602
|
+
);
|
|
1706
1603
|
return;
|
|
1707
1604
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
setStatus('SCANNING');
|
|
1605
|
+
|
|
1606
|
+
continueScanning();
|
|
1711
1607
|
return;
|
|
1712
1608
|
}
|
|
1713
1609
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1610
|
+
// ============================================================================
|
|
1611
|
+
// INITIAL SCAN STEP - Detect document type and validate
|
|
1612
|
+
// ============================================================================
|
|
1613
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1614
|
+
// Determine which flow handler to use.
|
|
1615
|
+
// Current-frame passport detection always takes precedence over a locked
|
|
1616
|
+
// ID_FRONT — passport MRZ may not appear until later frames.
|
|
1617
|
+
const flowDocumentType =
|
|
1618
|
+
documentType === 'PASSPORT'
|
|
1619
|
+
? 'PASSPORT'
|
|
1620
|
+
: detectedDocumentType !== 'UNKNOWN'
|
|
1621
|
+
? detectedDocumentType
|
|
1622
|
+
: documentType;
|
|
1623
|
+
|
|
1624
|
+
const handlePassportInitialStep = async () => {
|
|
1625
|
+
const flowResult = handlePassportFlow(
|
|
1626
|
+
primaryFaces,
|
|
1627
|
+
mrzText,
|
|
1628
|
+
parsedMRZData?.fields,
|
|
1629
|
+
mrzStableAndValid,
|
|
1630
|
+
onlyMRZScan,
|
|
1631
|
+
hasRequiredMRZFields(parsedMRZData?.fields),
|
|
1632
|
+
!!faceImageToUse
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
if (!flowResult.shouldProceed) {
|
|
1636
|
+
continueScanning(true);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Check for glare on passport before accepting
|
|
1641
|
+
const hasGlare = await checkDocumentGlare(
|
|
1642
|
+
image,
|
|
1643
|
+
frameWidth,
|
|
1644
|
+
frameHeight
|
|
1645
|
+
);
|
|
1646
|
+
if (hasGlare) {
|
|
1719
1647
|
if (isDebugEnabled()) {
|
|
1720
|
-
|
|
1721
|
-
'
|
|
1648
|
+
debugLog(
|
|
1649
|
+
'IdentityDocumentCamera',
|
|
1650
|
+
'[PASSPORT] Rejected - glare detected'
|
|
1722
1651
|
);
|
|
1723
1652
|
}
|
|
1724
|
-
|
|
1653
|
+
continueScanning(false);
|
|
1725
1654
|
return;
|
|
1726
1655
|
}
|
|
1727
1656
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
const retryThreshold = 60;
|
|
1731
|
-
const allowFaceOnly =
|
|
1732
|
-
mrzDetectionCurrentRetryCount.current > retryThreshold;
|
|
1733
|
-
const allRequiredElementsInFrame =
|
|
1734
|
-
hasFace && (hasSignature || allowFaceOnly);
|
|
1657
|
+
setDetectedDocumentType('PASSPORT');
|
|
1658
|
+
setStatus('SCANNED');
|
|
1735
1659
|
|
|
1736
|
-
|
|
1660
|
+
const nextPassportStep =
|
|
1661
|
+
flowResult.nextAction === 'PROCEED_TO_COMPLETED'
|
|
1662
|
+
? 'COMPLETED'
|
|
1663
|
+
: 'SCAN_HOLOGRAM';
|
|
1664
|
+
transitionStepWithCallback(
|
|
1665
|
+
nextPassportStep,
|
|
1666
|
+
'SCAN_ID_FRONT_OR_PASSPORT',
|
|
1667
|
+
scannedData
|
|
1668
|
+
);
|
|
1669
|
+
};
|
|
1737
1670
|
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1671
|
+
const handleIdFrontInitialStep = async () => {
|
|
1672
|
+
const flowResult = handleIDFrontFlow(
|
|
1673
|
+
primaryFaces,
|
|
1674
|
+
text,
|
|
1675
|
+
mrzText,
|
|
1676
|
+
parsedMRZData?.fields,
|
|
1677
|
+
mrzDetectionCurrentRetryCount.current
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
if (!flowResult.shouldProceed) {
|
|
1681
|
+
if (flowResult.nextAction === 'REJECT_AS_PASSPORT') {
|
|
1682
|
+
setDetectedDocumentType('UNKNOWN');
|
|
1683
|
+
consistentDocTypeCount.current = 0;
|
|
1684
|
+
lastDetectedDocType.current = 'UNKNOWN';
|
|
1685
|
+
}
|
|
1686
|
+
continueScanning(flowResult.nextAction !== 'REJECT_AS_PASSPORT');
|
|
1744
1687
|
return;
|
|
1745
1688
|
}
|
|
1746
1689
|
|
|
1747
|
-
//
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
} else if (parsedMRZData.fields.documentCode === 'P') {
|
|
1755
|
-
if (isDebugEnabled()) {
|
|
1756
|
-
console.log(
|
|
1757
|
-
'[ID_FRONT Scan] MRZ shows passport (code P) - rejecting as ID_FRONT'
|
|
1758
|
-
);
|
|
1759
|
-
}
|
|
1760
|
-
setStatus('SCANNING');
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
1764
|
-
// No parsed MRZ BUT passport MRZ pattern visible (P<TUR, P<USA, etc.)
|
|
1765
|
-
// This is likely a passport with OCR errors - wait for proper parsing
|
|
1690
|
+
// Check for glare on ID front before accepting
|
|
1691
|
+
const hasGlare = await checkDocumentGlare(
|
|
1692
|
+
image,
|
|
1693
|
+
frameWidth,
|
|
1694
|
+
frameHeight
|
|
1695
|
+
);
|
|
1696
|
+
if (hasGlare) {
|
|
1766
1697
|
if (isDebugEnabled()) {
|
|
1767
|
-
|
|
1768
|
-
'
|
|
1698
|
+
debugLog(
|
|
1699
|
+
'IdentityDocumentCamera',
|
|
1700
|
+
'[ID_FRONT] Rejected - glare detected'
|
|
1769
1701
|
);
|
|
1770
1702
|
}
|
|
1771
|
-
|
|
1772
|
-
setStatus('SCANNING');
|
|
1703
|
+
continueScanning(false);
|
|
1773
1704
|
return;
|
|
1774
1705
|
}
|
|
1775
|
-
// No MRZ or no passport pattern - proceed as ID card
|
|
1776
|
-
// ID cards typically don't have MRZ on front side (only on back)
|
|
1777
1706
|
|
|
1778
|
-
// CRITICAL: Lock document type state to ID_FRONT before proceeding
|
|
1779
|
-
// This ensures hologram completion knows it's an ID card (needs ID_BACK step)
|
|
1780
1707
|
setDetectedDocumentType('ID_FRONT');
|
|
1781
1708
|
setStatus('SCANNED');
|
|
1782
|
-
setIsTorchOn(false);
|
|
1783
|
-
if (onlyMRZScan) {
|
|
1784
|
-
// Passport has no back side - go directly to COMPLETED
|
|
1785
|
-
// At this point detectedDocumentType is definitively set
|
|
1786
|
-
if (detectedDocumentType === 'PASSPORT') {
|
|
1787
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1788
|
-
} else {
|
|
1789
|
-
setNextStepAndVibrate(
|
|
1790
|
-
'SCAN_ID_BACK',
|
|
1791
|
-
'SCAN_ID_FRONT_OR_PASSPORT'
|
|
1792
|
-
);
|
|
1793
|
-
}
|
|
1794
|
-
setTimeout(() => {
|
|
1795
|
-
onIdentityDocumentScanned(scannedData);
|
|
1796
|
-
}, 1000);
|
|
1797
|
-
} else {
|
|
1798
|
-
if (isDebugEnabled()) {
|
|
1799
|
-
console.log(
|
|
1800
|
-
'[ID_FRONT Scan] Confirmed as ID card - proceeding to hologram'
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1804
|
-
setTimeout(() => {
|
|
1805
|
-
onIdentityDocumentScanned(scannedData);
|
|
1806
|
-
}, 1000);
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
// Note: SCAN_HOLOGRAM completion is now handled in the unified block above
|
|
1810
|
-
} else if (documentType === 'PASSPORT') {
|
|
1811
|
-
if (
|
|
1812
|
-
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
1813
|
-
!scannedData.hologramImage
|
|
1814
|
-
) {
|
|
1815
|
-
if (onlyMRZScan) {
|
|
1816
|
-
const hasRequiredFields = hasRequiredMRZFields(
|
|
1817
|
-
parsedMRZData?.fields
|
|
1818
|
-
);
|
|
1819
|
-
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
1820
|
-
if (
|
|
1821
|
-
!!scannedData.mrzText &&
|
|
1822
|
-
hasRequiredFields &&
|
|
1823
|
-
mrzStableAndValid
|
|
1824
|
-
) {
|
|
1825
|
-
const hasFace = cardSizedFaces.length > 0;
|
|
1826
|
-
const hasMRZ = !!mrzText;
|
|
1827
|
-
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1828
|
-
|
|
1829
|
-
setElementsOutsideScanArea([]);
|
|
1830
1709
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
parsedMRZData?.fields,
|
|
1841
|
-
mrzText,
|
|
1842
|
-
validMRZConsecutiveCount.current,
|
|
1843
|
-
isDebugEnabled()
|
|
1844
|
-
);
|
|
1845
|
-
setDetectedDocumentType('PASSPORT');
|
|
1846
|
-
setStatus('SCANNED');
|
|
1847
|
-
setIsTorchOn(false);
|
|
1848
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1849
|
-
setTimeout(() => {
|
|
1850
|
-
onIdentityDocumentScanned(scannedData);
|
|
1851
|
-
}, 1000);
|
|
1852
|
-
return; // CRITICAL: Exit after MRZ-only scan completes to prevent fall-through
|
|
1853
|
-
} else {
|
|
1854
|
-
if (!!scannedData.mrzText && !mrzStableAndValid) {
|
|
1855
|
-
logMRZValidationFailure(
|
|
1856
|
-
'Passport Scan',
|
|
1857
|
-
hasRequiredFields,
|
|
1858
|
-
parsedMRZData,
|
|
1859
|
-
mrzDetectionCurrentRetryCount.current,
|
|
1860
|
-
isDebugEnabled()
|
|
1861
|
-
);
|
|
1862
|
-
}
|
|
1863
|
-
mrzDetectionCurrentRetryCount.current++;
|
|
1864
|
-
setStatus('SCANNING');
|
|
1865
|
-
return; // Don't fall through to else-if
|
|
1866
|
-
}
|
|
1867
|
-
} else {
|
|
1868
|
-
// Normal passport scan (with hologram) - require MRZ to be detected before proceeding
|
|
1869
|
-
const hasFace = cardSizedFaces.length > 0;
|
|
1870
|
-
const hasMRZ = !!mrzText;
|
|
1871
|
-
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1872
|
-
|
|
1873
|
-
setElementsOutsideScanArea([]);
|
|
1874
|
-
|
|
1875
|
-
if (!allRequiredElementsInFrame) {
|
|
1876
|
-
console.log(
|
|
1877
|
-
'[Passport Scan] Valid but waiting for all elements in frame (face + MRZ)'
|
|
1878
|
-
);
|
|
1879
|
-
setStatus('SCANNING');
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
// CRITICAL: Final verification - ensure MRZ definitively identifies this as passport
|
|
1884
|
-
// This must pass before we can proceed to hologram
|
|
1885
|
-
if (
|
|
1886
|
-
!parsedMRZData?.fields?.documentCode ||
|
|
1887
|
-
parsedMRZData.fields.documentCode !== 'P'
|
|
1888
|
-
) {
|
|
1889
|
-
console.log(
|
|
1890
|
-
'[Passport Scan] MRZ detected but not confirmed as passport (code:',
|
|
1891
|
-
parsedMRZData?.fields?.documentCode || 'none',
|
|
1892
|
-
') - waiting for valid passport MRZ'
|
|
1893
|
-
);
|
|
1894
|
-
setStatus('SCANNING');
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1710
|
+
const nextIdFrontStep = onlyMRZScan
|
|
1711
|
+
? 'SCAN_ID_BACK'
|
|
1712
|
+
: 'SCAN_HOLOGRAM';
|
|
1713
|
+
transitionStepWithCallback(
|
|
1714
|
+
nextIdFrontStep,
|
|
1715
|
+
'SCAN_ID_FRONT_OR_PASSPORT',
|
|
1716
|
+
scannedData
|
|
1717
|
+
);
|
|
1718
|
+
};
|
|
1897
1719
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
// This ensures hologram completion knows it's a passport (no ID_BACK step)
|
|
1903
|
-
setDetectedDocumentType('PASSPORT');
|
|
1904
|
-
setStatus('SCANNED');
|
|
1905
|
-
setIsTorchOn(false);
|
|
1906
|
-
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1907
|
-
setTimeout(() => {
|
|
1908
|
-
onIdentityDocumentScanned(scannedData);
|
|
1909
|
-
}, 1000);
|
|
1910
|
-
}
|
|
1720
|
+
// PASSPORT FLOW: Face + MRZ with code 'P'
|
|
1721
|
+
if (flowDocumentType === 'PASSPORT') {
|
|
1722
|
+
handlePassportInitialStep();
|
|
1723
|
+
return;
|
|
1911
1724
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
setStatus('SCANNING');
|
|
1918
|
-
} else {
|
|
1919
|
-
// Document type UNKNOWN - continue scanning until we can classify it
|
|
1920
|
-
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1921
|
-
console.log(
|
|
1922
|
-
'[Document Scan] Type UNKNOWN - waiting for clearer detection (faces:',
|
|
1923
|
-
cardSizedFaces.length,
|
|
1924
|
-
'mrzCode:',
|
|
1925
|
-
parsedMRZData?.fields?.documentCode || 'none',
|
|
1926
|
-
'text length:',
|
|
1927
|
-
text.length,
|
|
1928
|
-
')'
|
|
1929
|
-
);
|
|
1725
|
+
|
|
1726
|
+
// ID CARD FLOW: Face + No passport MRZ pattern
|
|
1727
|
+
if (flowDocumentType === 'ID_FRONT') {
|
|
1728
|
+
handleIdFrontInitialStep();
|
|
1729
|
+
return;
|
|
1930
1730
|
}
|
|
1931
|
-
|
|
1731
|
+
|
|
1732
|
+
// UNKNOWN - Continue scanning
|
|
1733
|
+
continueScanning();
|
|
1734
|
+
return;
|
|
1932
1735
|
}
|
|
1933
1736
|
},
|
|
1934
1737
|
[
|
|
@@ -1938,15 +1741,10 @@ const IdentityDocumentCamera = ({
|
|
|
1938
1741
|
detectedDocumentType,
|
|
1939
1742
|
currentFaceImage,
|
|
1940
1743
|
testMode,
|
|
1941
|
-
hasRequiredMRZFields,
|
|
1942
|
-
areMRZFieldsEqual,
|
|
1943
1744
|
onlyMRZScan,
|
|
1944
|
-
isTorchOn,
|
|
1945
1745
|
setIsTorchOn,
|
|
1946
|
-
|
|
1746
|
+
transitionStepWithCallback,
|
|
1947
1747
|
onIdentityDocumentScanned,
|
|
1948
|
-
logMRZDetails,
|
|
1949
|
-
logMRZValidationFailure,
|
|
1950
1748
|
currentSecondaryFaceImage,
|
|
1951
1749
|
detectHologramNative,
|
|
1952
1750
|
]
|
|
@@ -2099,61 +1897,18 @@ const IdentityDocumentCamera = ({
|
|
|
2099
1897
|
},
|
|
2100
1898
|
cornerPoints: b.cornerPoints ?? [],
|
|
2101
1899
|
}));
|
|
2102
|
-
|
|
2103
|
-
// Log barcode detection for debugging (only when scanning ID back)
|
|
2104
|
-
if (
|
|
2105
|
-
barcodes.length > 0 &&
|
|
2106
|
-
nextStep === 'SCAN_ID_BACK' &&
|
|
2107
|
-
isDebugEnabled()
|
|
2108
|
-
) {
|
|
2109
|
-
console.log(`[Barcode JS] Detected ${barcodes.length} barcode(s):`);
|
|
2110
|
-
barcodes.forEach((b, idx) => {
|
|
2111
|
-
const formatNames: { [key: number]: string } = {
|
|
2112
|
-
5: 'PDF417',
|
|
2113
|
-
64: 'QR_CODE',
|
|
2114
|
-
1: 'CODE_128',
|
|
2115
|
-
2: 'CODE_39',
|
|
2116
|
-
13: 'EAN_13',
|
|
2117
|
-
8: 'EAN_8',
|
|
2118
|
-
4096: 'AZTEC',
|
|
2119
|
-
16: 'DATA_MATRIX',
|
|
2120
|
-
};
|
|
2121
|
-
const formatName =
|
|
2122
|
-
formatNames[b.format] || `UNKNOWN(${b.format})`;
|
|
2123
|
-
console.log(
|
|
2124
|
-
` [${idx + 1}] ${formatName}: ${b.rawValue.substring(0, 50)}`
|
|
2125
|
-
);
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
1900
|
}
|
|
2129
1901
|
|
|
2130
1902
|
// Update all debug overlay bounds continuously when debug mode is enabled
|
|
2131
1903
|
if (isDebugEnabled() && frameDimensions) {
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
if (frameAspect > screenAspect) {
|
|
2141
|
-
scale = screen.height / frameDimensions.height;
|
|
2142
|
-
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
2143
|
-
} else {
|
|
2144
|
-
scale = screen.width / frameDimensions.width;
|
|
2145
|
-
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
const scanLeft = (screen.width * 0.05 + offsetX) / scale;
|
|
2149
|
-
const scanTop = (screen.height * 0.36 + offsetY) / scale;
|
|
2150
|
-
const scanRight = (screen.width * 0.95 + offsetX) / scale;
|
|
2151
|
-
const scanBottom = (screen.height * 0.64 + offsetY) / scale;
|
|
2152
|
-
const isInsideScan = (x: number, y: number, w: number, h: number) =>
|
|
2153
|
-
x >= scanLeft &&
|
|
2154
|
-
y >= scanTop &&
|
|
2155
|
-
x + w <= scanRight &&
|
|
2156
|
-
y + h <= scanBottom;
|
|
1904
|
+
const { scale, offsetX, offsetY } = getFrameToScreenTransform(
|
|
1905
|
+
frameDimensions.width,
|
|
1906
|
+
frameDimensions.height
|
|
1907
|
+
);
|
|
1908
|
+
const { isInsideScan } = getScanAreaBounds(
|
|
1909
|
+
frameDimensions.width,
|
|
1910
|
+
frameDimensions.height
|
|
1911
|
+
);
|
|
2157
1912
|
|
|
2158
1913
|
// Update barcode bounds
|
|
2159
1914
|
if (barcodes.length > 0 && barcodes[0]) {
|
|
@@ -2167,15 +1922,12 @@ const IdentityDocumentCamera = ({
|
|
|
2167
1922
|
x: c.x * scale - offsetX,
|
|
2168
1923
|
y: c.y * scale - offsetY,
|
|
2169
1924
|
}));
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
1925
|
+
angle = angleBetweenPoints(
|
|
1926
|
+
transformedCorners[0],
|
|
1927
|
+
transformedCorners[1]
|
|
1928
|
+
);
|
|
2174
1929
|
}
|
|
2175
1930
|
|
|
2176
|
-
if (isDebugEnabled()) {
|
|
2177
|
-
console.log('[Debug] Barcode detected:', { bbox, angle });
|
|
2178
|
-
}
|
|
2179
1931
|
setBarcodeBounds({
|
|
2180
1932
|
x: bbox.left * scale - offsetX,
|
|
2181
1933
|
y: bbox.top * scale - offsetY,
|
|
@@ -2245,46 +1997,21 @@ const IdentityDocumentCamera = ({
|
|
|
2245
1997
|
|
|
2246
1998
|
// Detect MRZ and signature text areas continuously
|
|
2247
1999
|
if (textBlocks.length > 0) {
|
|
2248
|
-
|
|
2249
|
-
// Find MRZ-like text blocks (bottom area, contains MRZ-like characters)
|
|
2250
|
-
// More strict pattern: look for blocks with 8+ consecutive uppercase/numbers/< characters AND
|
|
2251
|
-
// must contain at least one '<' character (true MRZ characteristic)
|
|
2252
|
-
const mrzPattern = /[A-Z0-9<]{8,}.*</i;
|
|
2253
|
-
const bottomHalf = frame.height * 0.5; // Increased from 0.67 to catch more MRZ blocks
|
|
2254
|
-
|
|
2255
|
-
// Log bottom area blocks for debugging
|
|
2256
|
-
const bottomBlocks = textBlocks.filter(
|
|
2257
|
-
(block) => block.blockFrame && block.blockFrame.y > bottomHalf
|
|
2258
|
-
);
|
|
2259
|
-
if (bottomBlocks.length > 0) {
|
|
2260
|
-
console.log(
|
|
2261
|
-
'[Debug] Bottom area blocks:',
|
|
2262
|
-
bottomBlocks.map((b) => b.text.substring(0, 30))
|
|
2263
|
-
);
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2000
|
+
const bottomHalf = frame.height * 0.5;
|
|
2266
2001
|
const mrzBlocks = textBlocks.filter(
|
|
2267
2002
|
(block) =>
|
|
2268
2003
|
block.blockFrame &&
|
|
2269
2004
|
block.blockFrame.y > bottomHalf &&
|
|
2270
|
-
|
|
2005
|
+
MRZ_BLOCK_PATTERN.test(block.text)
|
|
2271
2006
|
);
|
|
2272
2007
|
|
|
2273
|
-
console.log('[Debug] MRZ blocks found:', mrzBlocks.length);
|
|
2274
2008
|
if (mrzBlocks.length > 0) {
|
|
2275
|
-
// Extract MRZ-only text from detected blocks (sorted by Y position for correct line order)
|
|
2276
2009
|
const sortedMrzBlocks = [...mrzBlocks].sort(
|
|
2277
2010
|
(a, b) => (a.blockFrame?.y || 0) - (b.blockFrame?.y || 0)
|
|
2278
2011
|
);
|
|
2279
2012
|
scannedText.mrzOnlyText = sortedMrzBlocks
|
|
2280
2013
|
.map((b) => b.text)
|
|
2281
2014
|
.join('\n');
|
|
2282
|
-
if (isDebugEnabled()) {
|
|
2283
|
-
console.log(
|
|
2284
|
-
'[MRZ Extraction] Using only MRZ blocks:',
|
|
2285
|
-
scannedText.mrzOnlyText.substring(0, 100)
|
|
2286
|
-
);
|
|
2287
|
-
}
|
|
2288
2015
|
|
|
2289
2016
|
const minX = Math.min(...mrzBlocks.map((b) => b.blockFrame!.x));
|
|
2290
2017
|
const minY = Math.min(...mrzBlocks.map((b) => b.blockFrame!.y));
|
|
@@ -2294,30 +2021,17 @@ const IdentityDocumentCamera = ({
|
|
|
2294
2021
|
const maxY = Math.max(
|
|
2295
2022
|
...mrzBlocks.map((b) => b.blockFrame!.y + b.blockFrame!.height)
|
|
2296
2023
|
);
|
|
2297
|
-
|
|
2298
|
-
// Collect all corner points from MRZ blocks
|
|
2299
2024
|
const allCornerPoints = mrzBlocks
|
|
2300
2025
|
.flatMap((b) => b.cornerPoints || [])
|
|
2301
2026
|
.map((c) => ({
|
|
2302
2027
|
x: c.x * scale - offsetX,
|
|
2303
2028
|
y: c.y * scale - offsetY,
|
|
2304
2029
|
}));
|
|
2030
|
+
const angle =
|
|
2031
|
+
allCornerPoints.length >= 2
|
|
2032
|
+
? angleBetweenPoints(allCornerPoints[0], allCornerPoints[1])
|
|
2033
|
+
: 0;
|
|
2305
2034
|
|
|
2306
|
-
let angle = 0;
|
|
2307
|
-
if (allCornerPoints.length >= 2) {
|
|
2308
|
-
// Calculate angle from first two points
|
|
2309
|
-
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
2310
|
-
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
2311
|
-
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
console.log('[Debug] MRZ bounds:', {
|
|
2315
|
-
minX,
|
|
2316
|
-
minY,
|
|
2317
|
-
maxX,
|
|
2318
|
-
maxY,
|
|
2319
|
-
angle,
|
|
2320
|
-
});
|
|
2321
2035
|
setMrzBounds({
|
|
2322
2036
|
x: minX * scale - offsetX,
|
|
2323
2037
|
y: minY * scale - offsetY,
|
|
@@ -2331,19 +2045,11 @@ const IdentityDocumentCamera = ({
|
|
|
2331
2045
|
setMrzBounds(null);
|
|
2332
2046
|
}
|
|
2333
2047
|
|
|
2334
|
-
// Detect signature area
|
|
2335
|
-
const signaturePattern = /signature|imza|İmza/i;
|
|
2336
2048
|
const signatureBlocks = textBlocks.filter(
|
|
2337
|
-
(block) =>
|
|
2049
|
+
(block) =>
|
|
2050
|
+
block.blockFrame && SIGNATURE_TEXT_REGEX.test(block.text)
|
|
2338
2051
|
);
|
|
2339
2052
|
|
|
2340
|
-
if (textBlocks.length > 0 && signatureBlocks.length === 0) {
|
|
2341
|
-
console.log(
|
|
2342
|
-
`[Signature Debug] No signature blocks found. All blocks (${textBlocks.length}):`,
|
|
2343
|
-
textBlocks.map((b) => b.text).join(' | ')
|
|
2344
|
-
);
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
2053
|
if (signatureBlocks.length > 0) {
|
|
2348
2054
|
const minX = Math.min(
|
|
2349
2055
|
...signatureBlocks.map((b) => b.blockFrame!.x)
|
|
@@ -2361,22 +2067,16 @@ const IdentityDocumentCamera = ({
|
|
|
2361
2067
|
(b) => b.blockFrame!.y + b.blockFrame!.height
|
|
2362
2068
|
)
|
|
2363
2069
|
);
|
|
2364
|
-
|
|
2365
|
-
// Collect all corner points from signature blocks
|
|
2366
2070
|
const allCornerPoints = signatureBlocks
|
|
2367
2071
|
.flatMap((b) => b.cornerPoints || [])
|
|
2368
2072
|
.map((c) => ({
|
|
2369
2073
|
x: c.x * scale - offsetX,
|
|
2370
2074
|
y: c.y * scale - offsetY,
|
|
2371
2075
|
}));
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
2377
|
-
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
2378
|
-
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2379
|
-
}
|
|
2076
|
+
const angle =
|
|
2077
|
+
allCornerPoints.length >= 2
|
|
2078
|
+
? angleBetweenPoints(allCornerPoints[0], allCornerPoints[1])
|
|
2079
|
+
: 0;
|
|
2380
2080
|
|
|
2381
2081
|
setSignatureBounds({
|
|
2382
2082
|
x: minX * scale - offsetX,
|
|
@@ -2390,81 +2090,9 @@ const IdentityDocumentCamera = ({
|
|
|
2390
2090
|
} else {
|
|
2391
2091
|
setSignatureBounds(null);
|
|
2392
2092
|
}
|
|
2393
|
-
|
|
2394
|
-
// Check if all required elements are detected based on document type
|
|
2395
|
-
if (nextStep === 'SCAN_ID_BACK') {
|
|
2396
|
-
// ID Back: MRZ + barcode (barcode optional but preferred)
|
|
2397
|
-
const hasMRZ = mrzBlocks.length > 0;
|
|
2398
|
-
const hasBarcode =
|
|
2399
|
-
barcodes.length > 0 || cachedBarcode.current !== null;
|
|
2400
|
-
const allPresent = hasMRZ && hasBarcode;
|
|
2401
|
-
setAllElementsDetected(allPresent);
|
|
2402
|
-
|
|
2403
|
-
// Don't block based on bounds - allow elements even if slightly outside
|
|
2404
|
-
setElementsOutsideScanArea([]);
|
|
2405
|
-
|
|
2406
|
-
if (!allPresent) {
|
|
2407
|
-
const missing = [];
|
|
2408
|
-
if (!hasMRZ) missing.push('MRZ');
|
|
2409
|
-
if (!hasBarcode) missing.push('Barcode');
|
|
2410
|
-
console.log(
|
|
2411
|
-
`[Frame Check] Missing elements: ${missing.join(', ')}`
|
|
2412
|
-
);
|
|
2413
|
-
} else {
|
|
2414
|
-
console.log('[Frame Check] ✓ All elements detected in frame');
|
|
2415
|
-
}
|
|
2416
|
-
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
2417
|
-
// Check if it's passport (has MRZ) or ID front (no MRZ)
|
|
2418
|
-
const hasMRZ = mrzBlocks.length > 0;
|
|
2419
|
-
const hasFace = detectedFaces.length > 0;
|
|
2420
|
-
const hasSignature = signatureBlocks.length > 0;
|
|
2421
|
-
|
|
2422
|
-
// Don't block based on bounds - allow elements even if slightly outside
|
|
2423
|
-
setElementsOutsideScanArea([]);
|
|
2424
|
-
|
|
2425
|
-
let allPresent = false;
|
|
2426
|
-
if (hasMRZ) {
|
|
2427
|
-
// Passport: face + MRZ
|
|
2428
|
-
allPresent = hasFace && hasMRZ;
|
|
2429
|
-
if (!allPresent) {
|
|
2430
|
-
const missing = [];
|
|
2431
|
-
if (!hasFace) missing.push('Face');
|
|
2432
|
-
if (!hasMRZ) missing.push('MRZ');
|
|
2433
|
-
console.log(
|
|
2434
|
-
`[Frame Check] Passport - Missing elements: ${missing.join(', ')}`
|
|
2435
|
-
);
|
|
2436
|
-
} else {
|
|
2437
|
-
console.log(
|
|
2438
|
-
'[Frame Check] ✓ Passport - All elements detected (face + MRZ)'
|
|
2439
|
-
);
|
|
2440
|
-
}
|
|
2441
|
-
} else {
|
|
2442
|
-
// ID Front: face + signature
|
|
2443
|
-
allPresent = hasFace && hasSignature;
|
|
2444
|
-
if (!allPresent) {
|
|
2445
|
-
const missing = [];
|
|
2446
|
-
if (!hasFace) missing.push('Face');
|
|
2447
|
-
if (!hasSignature) missing.push('Signature');
|
|
2448
|
-
console.log(
|
|
2449
|
-
`[Frame Check] ID Front - Missing elements: ${missing.join(', ')}`
|
|
2450
|
-
);
|
|
2451
|
-
} else {
|
|
2452
|
-
console.log(
|
|
2453
|
-
'[Frame Check] ✓ ID Front - All elements detected (face + signature)'
|
|
2454
|
-
);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
setAllElementsDetected(allPresent);
|
|
2459
|
-
} else {
|
|
2460
|
-
setAllElementsDetected(false);
|
|
2461
|
-
setElementsOutsideScanArea([]);
|
|
2462
|
-
}
|
|
2463
2093
|
} else {
|
|
2464
2094
|
setMrzBounds(null);
|
|
2465
2095
|
setSignatureBounds(null);
|
|
2466
|
-
setAllElementsDetected(false);
|
|
2467
|
-
setElementsOutsideScanArea([]);
|
|
2468
2096
|
}
|
|
2469
2097
|
} else if (!isDebugEnabled()) {
|
|
2470
2098
|
// Clear all bounds when debug mode is disabled
|
|
@@ -2475,21 +2103,17 @@ const IdentityDocumentCamera = ({
|
|
|
2475
2103
|
setSignatureBounds(null);
|
|
2476
2104
|
}
|
|
2477
2105
|
|
|
2478
|
-
// Update allElementsDetected for status text display
|
|
2106
|
+
// Update allElementsDetected for status text display
|
|
2479
2107
|
if (nextStep === 'SCAN_ID_BACK') {
|
|
2480
|
-
const hasMRZ = textBlocks.some((b) =>
|
|
2481
|
-
/[A-Z0-9<]{8,}.*</i.test(b.text)
|
|
2482
|
-
);
|
|
2108
|
+
const hasMRZ = textBlocks.some((b) => MRZ_BLOCK_PATTERN.test(b.text));
|
|
2483
2109
|
const hasBarcode =
|
|
2484
2110
|
barcodes.length > 0 || cachedBarcode.current !== null;
|
|
2485
2111
|
setAllElementsDetected(hasMRZ && hasBarcode);
|
|
2486
2112
|
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
2487
|
-
const hasMRZ = textBlocks.some((b) =>
|
|
2488
|
-
/[A-Z0-9<]{8,}.*</i.test(b.text)
|
|
2489
|
-
);
|
|
2113
|
+
const hasMRZ = textBlocks.some((b) => MRZ_BLOCK_PATTERN.test(b.text));
|
|
2490
2114
|
const hasFace = detectedFaces.length > 0;
|
|
2491
2115
|
const hasSignature = textBlocks.some((b) =>
|
|
2492
|
-
|
|
2116
|
+
SIGNATURE_TEXT_REGEX.test(b.text)
|
|
2493
2117
|
);
|
|
2494
2118
|
setAllElementsDetected(
|
|
2495
2119
|
hasMRZ ? hasFace && hasMRZ : hasFace && hasSignature
|
|
@@ -2499,29 +2123,8 @@ const IdentityDocumentCamera = ({
|
|
|
2499
2123
|
}
|
|
2500
2124
|
|
|
2501
2125
|
// Check if detected elements are inside the scan area
|
|
2502
|
-
const
|
|
2503
|
-
|
|
2504
|
-
const scanScreenAspect = scanScreen.width / scanScreen.height;
|
|
2505
|
-
let scanScale: number;
|
|
2506
|
-
let scanOffsetX = 0;
|
|
2507
|
-
let scanOffsetY = 0;
|
|
2508
|
-
if (scanFrameAspect > scanScreenAspect) {
|
|
2509
|
-
scanScale = scanScreen.height / frame.height;
|
|
2510
|
-
scanOffsetX = (frame.width * scanScale - scanScreen.width) / 2;
|
|
2511
|
-
} else {
|
|
2512
|
-
scanScale = scanScreen.width / frame.width;
|
|
2513
|
-
scanOffsetY = (frame.height * scanScale - scanScreen.height) / 2;
|
|
2514
|
-
}
|
|
2515
|
-
const scanLeft = (scanScreen.width * 0.05 + scanOffsetX) / scanScale;
|
|
2516
|
-
const scanTop = (scanScreen.height * 0.36 + scanOffsetY) / scanScale;
|
|
2517
|
-
const scanRight = (scanScreen.width * 0.95 + scanOffsetX) / scanScale;
|
|
2518
|
-
const scanBottom = (scanScreen.height * 0.64 + scanOffsetY) / scanScale;
|
|
2519
|
-
|
|
2520
|
-
const isInsideScan = (x: number, y: number, w: number, h: number) =>
|
|
2521
|
-
x >= scanLeft &&
|
|
2522
|
-
y >= scanTop &&
|
|
2523
|
-
x + w <= scanRight &&
|
|
2524
|
-
y + h <= scanBottom;
|
|
2126
|
+
const { scanLeft, scanTop, scanRight, scanBottom, isInsideScan } =
|
|
2127
|
+
getScanAreaBounds(frame.width, frame.height);
|
|
2525
2128
|
|
|
2526
2129
|
const outsideElements: string[] = [];
|
|
2527
2130
|
|
|
@@ -2567,8 +2170,8 @@ const IdentityDocumentCamera = ({
|
|
|
2567
2170
|
y2: bf.y + bf.height,
|
|
2568
2171
|
});
|
|
2569
2172
|
}
|
|
2570
|
-
const isMRZ =
|
|
2571
|
-
const isSignature =
|
|
2173
|
+
const isMRZ = MRZ_BLOCK_PATTERN.test(block.text);
|
|
2174
|
+
const isSignature = SIGNATURE_TEXT_REGEX.test(block.text);
|
|
2572
2175
|
if (
|
|
2573
2176
|
(isMRZ || isSignature) &&
|
|
2574
2177
|
!isInsideScan(bf.x, bf.y, bf.width, bf.height)
|
|
@@ -2679,8 +2282,8 @@ const IdentityDocumentCamera = ({
|
|
|
2679
2282
|
return (
|
|
2680
2283
|
<View style={StyleSheet.absoluteFill}>
|
|
2681
2284
|
<StatusBar
|
|
2682
|
-
barStyle=
|
|
2683
|
-
backgroundColor=
|
|
2285
|
+
barStyle={hasGuideShown ? 'light-content' : 'dark-content'}
|
|
2286
|
+
backgroundColor={hasGuideShown ? 'transparent' : '#ffffff'}
|
|
2684
2287
|
translucent
|
|
2685
2288
|
/>
|
|
2686
2289
|
{!hasGuideShown ? (
|
|
@@ -2734,444 +2337,6 @@ const IdentityDocumentCamera = ({
|
|
|
2734
2337
|
onCameraReady={handleCameraReady}
|
|
2735
2338
|
onCameraError={handleCameraError}
|
|
2736
2339
|
/>
|
|
2737
|
-
{isDebugEnabled() &&
|
|
2738
|
-
documentPlaneBounds &&
|
|
2739
|
-
nextStep !== 'COMPLETED' && (
|
|
2740
|
-
<>
|
|
2741
|
-
{/* Crop area border (with padding) */}
|
|
2742
|
-
{!!documentPlaneBounds.cropPadding && (
|
|
2743
|
-
<View
|
|
2744
|
-
style={{
|
|
2745
|
-
position: 'absolute',
|
|
2746
|
-
left:
|
|
2747
|
-
documentPlaneBounds.x - documentPlaneBounds.cropPadding,
|
|
2748
|
-
top:
|
|
2749
|
-
documentPlaneBounds.y - documentPlaneBounds.cropPadding,
|
|
2750
|
-
width:
|
|
2751
|
-
documentPlaneBounds.width +
|
|
2752
|
-
2 * documentPlaneBounds.cropPadding,
|
|
2753
|
-
height:
|
|
2754
|
-
documentPlaneBounds.height +
|
|
2755
|
-
2 * documentPlaneBounds.cropPadding,
|
|
2756
|
-
borderWidth: 2,
|
|
2757
|
-
borderColor: 'rgba(76, 175, 80, 0.5)',
|
|
2758
|
-
borderStyle: 'dashed',
|
|
2759
|
-
borderRadius: 8,
|
|
2760
|
-
backgroundColor: 'transparent',
|
|
2761
|
-
transform: [
|
|
2762
|
-
{
|
|
2763
|
-
rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg`,
|
|
2764
|
-
},
|
|
2765
|
-
],
|
|
2766
|
-
transformOrigin: 'center',
|
|
2767
|
-
}}
|
|
2768
|
-
/>
|
|
2769
|
-
)}
|
|
2770
|
-
{/* Actual face border */}
|
|
2771
|
-
<View
|
|
2772
|
-
style={{
|
|
2773
|
-
position: 'absolute',
|
|
2774
|
-
left: documentPlaneBounds.x,
|
|
2775
|
-
top: documentPlaneBounds.y,
|
|
2776
|
-
width: documentPlaneBounds.width,
|
|
2777
|
-
height: documentPlaneBounds.height,
|
|
2778
|
-
borderWidth: 3,
|
|
2779
|
-
borderColor: '#4CAF50',
|
|
2780
|
-
borderRadius: 8,
|
|
2781
|
-
backgroundColor: 'transparent',
|
|
2782
|
-
transform: [
|
|
2783
|
-
{ rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg` },
|
|
2784
|
-
],
|
|
2785
|
-
transformOrigin: 'center',
|
|
2786
|
-
}}
|
|
2787
|
-
>
|
|
2788
|
-
{!!documentPlaneBounds.rollAngle &&
|
|
2789
|
-
Math.abs(documentPlaneBounds.rollAngle) > 5 && (
|
|
2790
|
-
<TextView
|
|
2791
|
-
style={{
|
|
2792
|
-
position: 'absolute',
|
|
2793
|
-
top: -20,
|
|
2794
|
-
left: 0,
|
|
2795
|
-
color: '#4CAF50',
|
|
2796
|
-
fontSize: 10,
|
|
2797
|
-
fontWeight: 'bold',
|
|
2798
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2799
|
-
paddingHorizontal: 4,
|
|
2800
|
-
paddingVertical: 2,
|
|
2801
|
-
borderRadius: 2,
|
|
2802
|
-
}}
|
|
2803
|
-
>
|
|
2804
|
-
{documentPlaneBounds.rollAngle.toFixed(1)}°
|
|
2805
|
-
</TextView>
|
|
2806
|
-
)}
|
|
2807
|
-
</View>
|
|
2808
|
-
</>
|
|
2809
|
-
)}
|
|
2810
|
-
{isDebugEnabled() &&
|
|
2811
|
-
secondaryFaceBounds &&
|
|
2812
|
-
nextStep !== 'COMPLETED' && (
|
|
2813
|
-
<>
|
|
2814
|
-
{/* Crop area border (with padding) */}
|
|
2815
|
-
{!!secondaryFaceBounds.cropPadding && (
|
|
2816
|
-
<View
|
|
2817
|
-
style={{
|
|
2818
|
-
position: 'absolute',
|
|
2819
|
-
left:
|
|
2820
|
-
secondaryFaceBounds.x - secondaryFaceBounds.cropPadding,
|
|
2821
|
-
top:
|
|
2822
|
-
secondaryFaceBounds.y - secondaryFaceBounds.cropPadding,
|
|
2823
|
-
width:
|
|
2824
|
-
secondaryFaceBounds.width +
|
|
2825
|
-
2 * secondaryFaceBounds.cropPadding,
|
|
2826
|
-
height:
|
|
2827
|
-
secondaryFaceBounds.height +
|
|
2828
|
-
2 * secondaryFaceBounds.cropPadding,
|
|
2829
|
-
borderWidth: 2,
|
|
2830
|
-
borderColor: 'rgba(33, 150, 243, 0.5)',
|
|
2831
|
-
borderStyle: 'dashed',
|
|
2832
|
-
borderRadius: 8,
|
|
2833
|
-
backgroundColor: 'transparent',
|
|
2834
|
-
transform: [
|
|
2835
|
-
{
|
|
2836
|
-
rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg`,
|
|
2837
|
-
},
|
|
2838
|
-
],
|
|
2839
|
-
transformOrigin: 'center',
|
|
2840
|
-
}}
|
|
2841
|
-
/>
|
|
2842
|
-
)}
|
|
2843
|
-
{/* Actual face border */}
|
|
2844
|
-
<View
|
|
2845
|
-
style={{
|
|
2846
|
-
position: 'absolute',
|
|
2847
|
-
left: secondaryFaceBounds.x,
|
|
2848
|
-
top: secondaryFaceBounds.y,
|
|
2849
|
-
width: secondaryFaceBounds.width,
|
|
2850
|
-
height: secondaryFaceBounds.height,
|
|
2851
|
-
borderWidth: 3,
|
|
2852
|
-
borderColor: '#2196F3',
|
|
2853
|
-
borderRadius: 8,
|
|
2854
|
-
backgroundColor: 'transparent',
|
|
2855
|
-
transform: [
|
|
2856
|
-
{ rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg` },
|
|
2857
|
-
],
|
|
2858
|
-
transformOrigin: 'center',
|
|
2859
|
-
}}
|
|
2860
|
-
>
|
|
2861
|
-
{!!secondaryFaceBounds.rollAngle &&
|
|
2862
|
-
Math.abs(secondaryFaceBounds.rollAngle) > 5 && (
|
|
2863
|
-
<TextView
|
|
2864
|
-
style={{
|
|
2865
|
-
position: 'absolute',
|
|
2866
|
-
top: -20,
|
|
2867
|
-
left: 0,
|
|
2868
|
-
color: '#2196F3',
|
|
2869
|
-
fontSize: 10,
|
|
2870
|
-
fontWeight: 'bold',
|
|
2871
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2872
|
-
paddingHorizontal: 4,
|
|
2873
|
-
paddingVertical: 2,
|
|
2874
|
-
borderRadius: 2,
|
|
2875
|
-
}}
|
|
2876
|
-
>
|
|
2877
|
-
{secondaryFaceBounds.rollAngle.toFixed(1)}°
|
|
2878
|
-
</TextView>
|
|
2879
|
-
)}
|
|
2880
|
-
</View>
|
|
2881
|
-
</>
|
|
2882
|
-
)}
|
|
2883
|
-
{isDebugEnabled() && barcodeBounds && nextStep !== 'COMPLETED' && (
|
|
2884
|
-
<>
|
|
2885
|
-
{barcodeBounds.corners && barcodeBounds.corners.length >= 4 ? (
|
|
2886
|
-
// Render using corner points for precise rotated border
|
|
2887
|
-
<>
|
|
2888
|
-
{/* Draw border lines between corners */}
|
|
2889
|
-
{[0, 1, 2, 3].map((i) => {
|
|
2890
|
-
const start = barcodeBounds.corners![i];
|
|
2891
|
-
const end = barcodeBounds.corners![(i + 1) % 4];
|
|
2892
|
-
const dx = end.x - start.x;
|
|
2893
|
-
const dy = end.y - start.y;
|
|
2894
|
-
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2895
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2896
|
-
|
|
2897
|
-
return (
|
|
2898
|
-
<View
|
|
2899
|
-
key={i}
|
|
2900
|
-
style={{
|
|
2901
|
-
position: 'absolute',
|
|
2902
|
-
left: start.x,
|
|
2903
|
-
top: start.y,
|
|
2904
|
-
width: length,
|
|
2905
|
-
height: 3,
|
|
2906
|
-
backgroundColor: '#FF9800',
|
|
2907
|
-
transform: [{ rotate: `${angle}deg` }],
|
|
2908
|
-
transformOrigin: 'top left',
|
|
2909
|
-
}}
|
|
2910
|
-
/>
|
|
2911
|
-
);
|
|
2912
|
-
})}
|
|
2913
|
-
{/* Draw corner markers */}
|
|
2914
|
-
{barcodeBounds.corners.map((corner, idx) => (
|
|
2915
|
-
<View
|
|
2916
|
-
key={`corner-${idx}`}
|
|
2917
|
-
style={{
|
|
2918
|
-
position: 'absolute',
|
|
2919
|
-
left: corner.x - 4,
|
|
2920
|
-
top: corner.y - 4,
|
|
2921
|
-
width: 8,
|
|
2922
|
-
height: 8,
|
|
2923
|
-
borderRadius: 4,
|
|
2924
|
-
backgroundColor: '#FF9800',
|
|
2925
|
-
}}
|
|
2926
|
-
/>
|
|
2927
|
-
))}
|
|
2928
|
-
{/* Angle indicator */}
|
|
2929
|
-
{!!barcodeBounds.angle &&
|
|
2930
|
-
Math.abs(barcodeBounds.angle) > 5 && (
|
|
2931
|
-
<TextView
|
|
2932
|
-
style={{
|
|
2933
|
-
position: 'absolute',
|
|
2934
|
-
left: barcodeBounds.x,
|
|
2935
|
-
top: barcodeBounds.y - 20,
|
|
2936
|
-
color: '#FF9800',
|
|
2937
|
-
fontSize: 10,
|
|
2938
|
-
fontWeight: 'bold',
|
|
2939
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2940
|
-
paddingHorizontal: 4,
|
|
2941
|
-
paddingVertical: 2,
|
|
2942
|
-
borderRadius: 2,
|
|
2943
|
-
}}
|
|
2944
|
-
>
|
|
2945
|
-
{barcodeBounds.angle.toFixed(1)}°
|
|
2946
|
-
</TextView>
|
|
2947
|
-
)}
|
|
2948
|
-
</>
|
|
2949
|
-
) : (
|
|
2950
|
-
// Fallback to rotated rectangle if corners not available
|
|
2951
|
-
<View
|
|
2952
|
-
style={{
|
|
2953
|
-
position: 'absolute',
|
|
2954
|
-
left: barcodeBounds.x + barcodeBounds.width / 2,
|
|
2955
|
-
top: barcodeBounds.y + barcodeBounds.height / 2,
|
|
2956
|
-
width: barcodeBounds.width,
|
|
2957
|
-
height: barcodeBounds.height,
|
|
2958
|
-
marginLeft: -barcodeBounds.width / 2,
|
|
2959
|
-
marginTop: -barcodeBounds.height / 2,
|
|
2960
|
-
borderWidth: 3,
|
|
2961
|
-
borderColor: '#FF9800',
|
|
2962
|
-
borderRadius: 8,
|
|
2963
|
-
backgroundColor: 'transparent',
|
|
2964
|
-
transform: [{ rotate: `${barcodeBounds.angle || 0}deg` }],
|
|
2965
|
-
}}
|
|
2966
|
-
>
|
|
2967
|
-
{!!barcodeBounds.angle &&
|
|
2968
|
-
Math.abs(barcodeBounds.angle) > 5 && (
|
|
2969
|
-
<TextView
|
|
2970
|
-
style={{
|
|
2971
|
-
position: 'absolute',
|
|
2972
|
-
top: -20,
|
|
2973
|
-
left: 0,
|
|
2974
|
-
color: '#FF9800',
|
|
2975
|
-
fontSize: 10,
|
|
2976
|
-
fontWeight: 'bold',
|
|
2977
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2978
|
-
paddingHorizontal: 4,
|
|
2979
|
-
paddingVertical: 2,
|
|
2980
|
-
borderRadius: 2,
|
|
2981
|
-
}}
|
|
2982
|
-
>
|
|
2983
|
-
{barcodeBounds.angle.toFixed(1)}°
|
|
2984
|
-
</TextView>
|
|
2985
|
-
)}
|
|
2986
|
-
</View>
|
|
2987
|
-
)}
|
|
2988
|
-
</>
|
|
2989
|
-
)}
|
|
2990
|
-
{isDebugEnabled() && mrzBounds && nextStep !== 'COMPLETED' && (
|
|
2991
|
-
<>
|
|
2992
|
-
{mrzBounds.corners && mrzBounds.corners.length >= 2 ? (
|
|
2993
|
-
// Render using corner points for precise rotated border
|
|
2994
|
-
<>
|
|
2995
|
-
{/* Draw border lines between consecutive corners */}
|
|
2996
|
-
{mrzBounds.corners.map((corner, idx) => {
|
|
2997
|
-
const nextCorner =
|
|
2998
|
-
mrzBounds.corners![(idx + 1) % mrzBounds.corners!.length];
|
|
2999
|
-
const dx = nextCorner.x - corner.x;
|
|
3000
|
-
const dy = nextCorner.y - corner.y;
|
|
3001
|
-
const length = Math.sqrt(dx * dx + dy * dy);
|
|
3002
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
3003
|
-
|
|
3004
|
-
return (
|
|
3005
|
-
<View
|
|
3006
|
-
key={idx}
|
|
3007
|
-
style={{
|
|
3008
|
-
position: 'absolute',
|
|
3009
|
-
left: corner.x,
|
|
3010
|
-
top: corner.y,
|
|
3011
|
-
width: length,
|
|
3012
|
-
height: 3,
|
|
3013
|
-
backgroundColor: '#9C27B0',
|
|
3014
|
-
transform: [{ rotate: `${angle}deg` }],
|
|
3015
|
-
transformOrigin: 'top left',
|
|
3016
|
-
}}
|
|
3017
|
-
/>
|
|
3018
|
-
);
|
|
3019
|
-
})}
|
|
3020
|
-
{/* Angle indicator */}
|
|
3021
|
-
{!!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && (
|
|
3022
|
-
<TextView
|
|
3023
|
-
style={{
|
|
3024
|
-
position: 'absolute',
|
|
3025
|
-
left: mrzBounds.x,
|
|
3026
|
-
top: mrzBounds.y - 20,
|
|
3027
|
-
color: '#9C27B0',
|
|
3028
|
-
fontSize: 10,
|
|
3029
|
-
fontWeight: 'bold',
|
|
3030
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3031
|
-
paddingHorizontal: 4,
|
|
3032
|
-
paddingVertical: 2,
|
|
3033
|
-
borderRadius: 2,
|
|
3034
|
-
}}
|
|
3035
|
-
>
|
|
3036
|
-
{mrzBounds.angle.toFixed(1)}°
|
|
3037
|
-
</TextView>
|
|
3038
|
-
)}
|
|
3039
|
-
</>
|
|
3040
|
-
) : (
|
|
3041
|
-
// Fallback to rotated rectangle if corners not available
|
|
3042
|
-
<View
|
|
3043
|
-
style={{
|
|
3044
|
-
position: 'absolute',
|
|
3045
|
-
left: mrzBounds.x + mrzBounds.width / 2,
|
|
3046
|
-
top: mrzBounds.y + mrzBounds.height / 2,
|
|
3047
|
-
width: mrzBounds.width,
|
|
3048
|
-
height: mrzBounds.height,
|
|
3049
|
-
marginLeft: -mrzBounds.width / 2,
|
|
3050
|
-
marginTop: -mrzBounds.height / 2,
|
|
3051
|
-
borderWidth: 3,
|
|
3052
|
-
borderColor: '#9C27B0',
|
|
3053
|
-
borderRadius: 8,
|
|
3054
|
-
backgroundColor: 'transparent',
|
|
3055
|
-
transform: [{ rotate: `${mrzBounds.angle || 0}deg` }],
|
|
3056
|
-
}}
|
|
3057
|
-
>
|
|
3058
|
-
{!!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && (
|
|
3059
|
-
<TextView
|
|
3060
|
-
style={{
|
|
3061
|
-
position: 'absolute',
|
|
3062
|
-
top: -20,
|
|
3063
|
-
left: 0,
|
|
3064
|
-
color: '#9C27B0',
|
|
3065
|
-
fontSize: 10,
|
|
3066
|
-
fontWeight: 'bold',
|
|
3067
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3068
|
-
paddingHorizontal: 4,
|
|
3069
|
-
paddingVertical: 2,
|
|
3070
|
-
borderRadius: 2,
|
|
3071
|
-
}}
|
|
3072
|
-
>
|
|
3073
|
-
{mrzBounds.angle.toFixed(1)}°
|
|
3074
|
-
</TextView>
|
|
3075
|
-
)}
|
|
3076
|
-
</View>
|
|
3077
|
-
)}
|
|
3078
|
-
</>
|
|
3079
|
-
)}
|
|
3080
|
-
{isDebugEnabled() && signatureBounds && nextStep !== 'COMPLETED' && (
|
|
3081
|
-
<>
|
|
3082
|
-
{signatureBounds.corners &&
|
|
3083
|
-
signatureBounds.corners.length >= 2 ? (
|
|
3084
|
-
// Render using corner points for precise rotated border
|
|
3085
|
-
<>
|
|
3086
|
-
{/* Draw border lines between consecutive corners */}
|
|
3087
|
-
{signatureBounds.corners.map((corner, idx) => {
|
|
3088
|
-
const nextCorner =
|
|
3089
|
-
signatureBounds.corners![
|
|
3090
|
-
(idx + 1) % signatureBounds.corners!.length
|
|
3091
|
-
];
|
|
3092
|
-
const dx = nextCorner.x - corner.x;
|
|
3093
|
-
const dy = nextCorner.y - corner.y;
|
|
3094
|
-
const length = Math.sqrt(dx * dx + dy * dy);
|
|
3095
|
-
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
3096
|
-
|
|
3097
|
-
return (
|
|
3098
|
-
<View
|
|
3099
|
-
key={idx}
|
|
3100
|
-
style={{
|
|
3101
|
-
position: 'absolute',
|
|
3102
|
-
left: corner.x,
|
|
3103
|
-
top: corner.y,
|
|
3104
|
-
width: length,
|
|
3105
|
-
height: 3,
|
|
3106
|
-
backgroundColor: '#00BCD4',
|
|
3107
|
-
transform: [{ rotate: `${angle}deg` }],
|
|
3108
|
-
transformOrigin: 'top left',
|
|
3109
|
-
}}
|
|
3110
|
-
/>
|
|
3111
|
-
);
|
|
3112
|
-
})}
|
|
3113
|
-
{/* Angle indicator */}
|
|
3114
|
-
{!!signatureBounds.angle &&
|
|
3115
|
-
Math.abs(signatureBounds.angle) > 5 && (
|
|
3116
|
-
<TextView
|
|
3117
|
-
style={{
|
|
3118
|
-
position: 'absolute',
|
|
3119
|
-
left: signatureBounds.x,
|
|
3120
|
-
top: signatureBounds.y - 20,
|
|
3121
|
-
color: '#00BCD4',
|
|
3122
|
-
fontSize: 10,
|
|
3123
|
-
fontWeight: 'bold',
|
|
3124
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3125
|
-
paddingHorizontal: 4,
|
|
3126
|
-
paddingVertical: 2,
|
|
3127
|
-
borderRadius: 2,
|
|
3128
|
-
}}
|
|
3129
|
-
>
|
|
3130
|
-
{signatureBounds.angle.toFixed(1)}°
|
|
3131
|
-
</TextView>
|
|
3132
|
-
)}
|
|
3133
|
-
</>
|
|
3134
|
-
) : (
|
|
3135
|
-
// Fallback to rotated rectangle if corners not available
|
|
3136
|
-
<View
|
|
3137
|
-
style={{
|
|
3138
|
-
position: 'absolute',
|
|
3139
|
-
left: signatureBounds.x + signatureBounds.width / 2,
|
|
3140
|
-
top: signatureBounds.y + signatureBounds.height / 2,
|
|
3141
|
-
width: signatureBounds.width,
|
|
3142
|
-
height: signatureBounds.height,
|
|
3143
|
-
marginLeft: -signatureBounds.width / 2,
|
|
3144
|
-
marginTop: -signatureBounds.height / 2,
|
|
3145
|
-
borderWidth: 3,
|
|
3146
|
-
borderColor: '#00BCD4',
|
|
3147
|
-
borderRadius: 8,
|
|
3148
|
-
backgroundColor: 'transparent',
|
|
3149
|
-
transform: [{ rotate: `${signatureBounds.angle || 0}deg` }],
|
|
3150
|
-
}}
|
|
3151
|
-
>
|
|
3152
|
-
{!!signatureBounds.angle &&
|
|
3153
|
-
Math.abs(signatureBounds.angle) > 5 && (
|
|
3154
|
-
<TextView
|
|
3155
|
-
style={{
|
|
3156
|
-
position: 'absolute',
|
|
3157
|
-
top: -20,
|
|
3158
|
-
left: 0,
|
|
3159
|
-
color: '#00BCD4',
|
|
3160
|
-
fontSize: 10,
|
|
3161
|
-
fontWeight: 'bold',
|
|
3162
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3163
|
-
paddingHorizontal: 4,
|
|
3164
|
-
paddingVertical: 2,
|
|
3165
|
-
borderRadius: 2,
|
|
3166
|
-
}}
|
|
3167
|
-
>
|
|
3168
|
-
{signatureBounds.angle.toFixed(1)}°
|
|
3169
|
-
</TextView>
|
|
3170
|
-
)}
|
|
3171
|
-
</View>
|
|
3172
|
-
)}
|
|
3173
|
-
</>
|
|
3174
|
-
)}
|
|
3175
2340
|
<View style={[styles.topZone, { paddingTop: insets.top }]}>
|
|
3176
2341
|
{nextStep !== 'COMPLETED' &&
|
|
3177
2342
|
status !== 'SCANNED' &&
|
|
@@ -3228,64 +2393,16 @@ const IdentityDocumentCamera = ({
|
|
|
3228
2393
|
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
3229
2394
|
]}
|
|
3230
2395
|
>
|
|
3231
|
-
{
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
: isBrightnessLow
|
|
3242
|
-
? t('identityDocumentCamera.lowBrightness')
|
|
3243
|
-
: isFrameBlurry
|
|
3244
|
-
? t('identityDocumentCamera.avoidBlur')
|
|
3245
|
-
: status === 'SCANNING' &&
|
|
3246
|
-
allElementsDetected &&
|
|
3247
|
-
elementsOutsideScanArea.length === 0
|
|
3248
|
-
? nextStep === 'SCAN_ID_BACK'
|
|
3249
|
-
? t('identityDocumentCamera.idCardBackDetected')
|
|
3250
|
-
: detectedDocumentType === 'PASSPORT'
|
|
3251
|
-
? t('identityDocumentCamera.passportDetected')
|
|
3252
|
-
: detectedDocumentType === 'ID_FRONT'
|
|
3253
|
-
? t('identityDocumentCamera.idCardFrontDetected')
|
|
3254
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
3255
|
-
? t('identityDocumentCamera.alignHologram')
|
|
3256
|
-
: t('identityDocumentCamera.readingDocument')
|
|
3257
|
-
: elementsOutsideScanArea.length > 0
|
|
3258
|
-
? t('identityDocumentCamera.centerDocument')
|
|
3259
|
-
: (status === 'SCANNING' || status === 'SEARCHING') &&
|
|
3260
|
-
!allElementsDetected
|
|
3261
|
-
? nextStep === 'SCAN_ID_BACK'
|
|
3262
|
-
? t('identityDocumentCamera.alignIDBack')
|
|
3263
|
-
: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
3264
|
-
? detectedDocumentType === 'PASSPORT'
|
|
3265
|
-
? t('identityDocumentCamera.alignPassport')
|
|
3266
|
-
: detectedDocumentType === 'ID_FRONT'
|
|
3267
|
-
? t('identityDocumentCamera.alignIDFront')
|
|
3268
|
-
: t('identityDocumentCamera.alignPhotoSide')
|
|
3269
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
3270
|
-
? t('identityDocumentCamera.alignHologram')
|
|
3271
|
-
: t('identityDocumentCamera.readingDocument')
|
|
3272
|
-
: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
3273
|
-
? status === 'SCANNING'
|
|
3274
|
-
? t('identityDocumentCamera.readingDocument')
|
|
3275
|
-
: t('identityDocumentCamera.alignPhotoSide')
|
|
3276
|
-
: nextStep === 'SCAN_HOLOGRAM'
|
|
3277
|
-
? t('identityDocumentCamera.alignHologram')
|
|
3278
|
-
: nextStep === 'SCAN_ID_BACK'
|
|
3279
|
-
? status === 'SCANNING'
|
|
3280
|
-
? t(
|
|
3281
|
-
'identityDocumentCamera.readingDocument'
|
|
3282
|
-
)
|
|
3283
|
-
: t(
|
|
3284
|
-
'identityDocumentCamera.alignIDBackSide'
|
|
3285
|
-
)
|
|
3286
|
-
: nextStep === 'COMPLETED'
|
|
3287
|
-
? t('identityDocumentCamera.scanCompleted')
|
|
3288
|
-
: ''}
|
|
2396
|
+
{getStatusMessage(
|
|
2397
|
+
nextStep,
|
|
2398
|
+
status,
|
|
2399
|
+
detectedDocumentType,
|
|
2400
|
+
isBrightnessLow,
|
|
2401
|
+
isFrameBlurry,
|
|
2402
|
+
allElementsDetected,
|
|
2403
|
+
elementsOutsideScanArea,
|
|
2404
|
+
t
|
|
2405
|
+
)}
|
|
3289
2406
|
</AnimatedText>
|
|
3290
2407
|
</View>
|
|
3291
2408
|
<View style={styles.leftZone} />
|
|
@@ -3333,476 +2450,30 @@ const IdentityDocumentCamera = ({
|
|
|
3333
2450
|
) : null}
|
|
3334
2451
|
</View>
|
|
3335
2452
|
{isDebugEnabled() && (
|
|
3336
|
-
<
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
}}
|
|
3358
|
-
>
|
|
3359
|
-
{/* Status & State Row */}
|
|
3360
|
-
<View
|
|
3361
|
-
style={{
|
|
3362
|
-
flexDirection: 'row',
|
|
3363
|
-
justifyContent: 'space-between',
|
|
3364
|
-
marginBottom: 4,
|
|
3365
|
-
}}
|
|
3366
|
-
>
|
|
3367
|
-
<TextView
|
|
3368
|
-
style={{
|
|
3369
|
-
color: '#88D8B0',
|
|
3370
|
-
fontSize: 10,
|
|
3371
|
-
fontWeight: 'bold',
|
|
3372
|
-
}}
|
|
3373
|
-
>
|
|
3374
|
-
{`Status: ${status}`}
|
|
3375
|
-
</TextView>
|
|
3376
|
-
<TextView
|
|
3377
|
-
style={{
|
|
3378
|
-
color: '#FFEB3B',
|
|
3379
|
-
fontSize: 10,
|
|
3380
|
-
fontWeight: 'bold',
|
|
3381
|
-
}}
|
|
3382
|
-
>
|
|
3383
|
-
{`Step: ${nextStep}`}
|
|
3384
|
-
</TextView>
|
|
3385
|
-
</View>
|
|
3386
|
-
|
|
3387
|
-
{/* Quality Info - Badges */}
|
|
3388
|
-
<View
|
|
3389
|
-
style={{
|
|
3390
|
-
flexDirection: 'row',
|
|
3391
|
-
justifyContent: 'space-between',
|
|
3392
|
-
marginBottom: 4,
|
|
3393
|
-
gap: 3,
|
|
3394
|
-
}}
|
|
3395
|
-
>
|
|
3396
|
-
<View
|
|
3397
|
-
style={{
|
|
3398
|
-
backgroundColor: isBrightnessLow ? '#f44336' : '#4CAF50',
|
|
3399
|
-
paddingHorizontal: 6,
|
|
3400
|
-
paddingVertical: 1,
|
|
3401
|
-
borderRadius: 3,
|
|
3402
|
-
}}
|
|
3403
|
-
>
|
|
3404
|
-
<TextView
|
|
3405
|
-
style={{
|
|
3406
|
-
color: '#FFFFFF',
|
|
3407
|
-
fontSize: 8,
|
|
3408
|
-
fontWeight: 'bold',
|
|
3409
|
-
}}
|
|
3410
|
-
>
|
|
3411
|
-
{isBrightnessLow ? 'Low Light' : 'Good Light'}
|
|
3412
|
-
</TextView>
|
|
3413
|
-
</View>
|
|
3414
|
-
<View
|
|
3415
|
-
style={{
|
|
3416
|
-
backgroundColor: isFrameBlurry ? '#f44336' : '#4CAF50',
|
|
3417
|
-
paddingHorizontal: 6,
|
|
3418
|
-
paddingVertical: 1,
|
|
3419
|
-
borderRadius: 3,
|
|
3420
|
-
}}
|
|
3421
|
-
>
|
|
3422
|
-
<TextView
|
|
3423
|
-
style={{
|
|
3424
|
-
color: '#FFFFFF',
|
|
3425
|
-
fontSize: 8,
|
|
3426
|
-
fontWeight: 'bold',
|
|
3427
|
-
}}
|
|
3428
|
-
>
|
|
3429
|
-
{isFrameBlurry ? 'Blurry' : 'Sharp'}
|
|
3430
|
-
</TextView>
|
|
3431
|
-
</View>
|
|
3432
|
-
</View>
|
|
3433
|
-
|
|
3434
|
-
{/* Elements Status */}
|
|
3435
|
-
<TextView
|
|
3436
|
-
style={{
|
|
3437
|
-
color:
|
|
3438
|
-
elementsOutsideScanArea.length === 0
|
|
3439
|
-
? '#4CAF50'
|
|
3440
|
-
: '#FFA500',
|
|
3441
|
-
fontSize: 8,
|
|
3442
|
-
marginBottom: 3,
|
|
3443
|
-
}}
|
|
3444
|
-
>
|
|
3445
|
-
{elementsOutsideScanArea.length === 0
|
|
3446
|
-
? 'All elements in frame'
|
|
3447
|
-
: `Outside: ${elementsOutsideScanArea.join(', ')}`}
|
|
3448
|
-
</TextView>
|
|
3449
|
-
|
|
3450
|
-
{/* Detection Info */}
|
|
3451
|
-
<TextView
|
|
3452
|
-
style={{
|
|
3453
|
-
color: 'rgba(255, 255, 255, 0.7)',
|
|
3454
|
-
fontSize: 8,
|
|
3455
|
-
marginBottom: 3,
|
|
3456
|
-
}}
|
|
3457
|
-
>
|
|
3458
|
-
{`Face: ${documentPlaneBounds ? '✓' : '✗'}, MRZ: ${mrzBounds ? '✓' : '✗'}, Sig: ${signatureBounds ? '✓' : '✗'}`}
|
|
3459
|
-
</TextView>
|
|
3460
|
-
|
|
3461
|
-
{/* Hologram Count */}
|
|
3462
|
-
{nextStep === 'SCAN_HOLOGRAM' && hologramImageCount > 0 && (
|
|
3463
|
-
<TextView
|
|
3464
|
-
style={{
|
|
3465
|
-
color: '#9C27B0',
|
|
3466
|
-
fontSize: 8,
|
|
3467
|
-
fontWeight: 'bold',
|
|
3468
|
-
marginBottom: 3,
|
|
3469
|
-
}}
|
|
3470
|
-
>
|
|
3471
|
-
{`Hologram: ${hologramImageCount}/${HOLOGRAM_IMAGE_COUNT}`}
|
|
3472
|
-
</TextView>
|
|
3473
|
-
)}
|
|
3474
|
-
|
|
3475
|
-
{/* Additional Info Row */}
|
|
3476
|
-
<View
|
|
3477
|
-
style={{
|
|
3478
|
-
flexDirection: 'row',
|
|
3479
|
-
gap: 8,
|
|
3480
|
-
paddingTop: 3,
|
|
3481
|
-
borderTopWidth: 1,
|
|
3482
|
-
borderTopColor: 'rgba(255, 255, 255, 0.2)',
|
|
3483
|
-
marginBottom: 4,
|
|
3484
|
-
}}
|
|
3485
|
-
>
|
|
3486
|
-
<TextView
|
|
3487
|
-
style={{
|
|
3488
|
-
color: 'rgba(255, 255, 255, 0.7)',
|
|
3489
|
-
fontSize: 8,
|
|
3490
|
-
marginTop: 3,
|
|
3491
|
-
}}
|
|
3492
|
-
>
|
|
3493
|
-
{`Doc: ${detectedDocumentType}`}
|
|
3494
|
-
</TextView>
|
|
3495
|
-
<TextView
|
|
3496
|
-
style={{
|
|
3497
|
-
color: 'rgba(255, 255, 255, 0.7)',
|
|
3498
|
-
fontSize: 8,
|
|
3499
|
-
marginTop: 3,
|
|
3500
|
-
}}
|
|
3501
|
-
>
|
|
3502
|
-
{`Flash: ${isTorchOn ? '🔦' : '○'}`}
|
|
3503
|
-
</TextView>
|
|
3504
|
-
</View>
|
|
3505
|
-
|
|
3506
|
-
{/* Debug Images Grid - Compact */}
|
|
3507
|
-
<View
|
|
3508
|
-
style={{
|
|
3509
|
-
flexDirection: 'row',
|
|
3510
|
-
flexWrap: 'wrap',
|
|
3511
|
-
gap: 4,
|
|
3512
|
-
justifyContent: 'center',
|
|
3513
|
-
}}
|
|
3514
|
-
>
|
|
3515
|
-
{/* Face Image */}
|
|
3516
|
-
<View style={{ alignItems: 'center' }}>
|
|
3517
|
-
{currentFaceImage ? (
|
|
3518
|
-
<Image
|
|
3519
|
-
source={{
|
|
3520
|
-
uri: `data:image/jpeg;base64,${currentFaceImage}`,
|
|
3521
|
-
}}
|
|
3522
|
-
style={{
|
|
3523
|
-
width: 40,
|
|
3524
|
-
height: 48,
|
|
3525
|
-
borderRadius: 2,
|
|
3526
|
-
borderWidth: 1,
|
|
3527
|
-
borderColor: '#4CAF50',
|
|
3528
|
-
}}
|
|
3529
|
-
/>
|
|
3530
|
-
) : (
|
|
3531
|
-
<View
|
|
3532
|
-
style={{
|
|
3533
|
-
width: 40,
|
|
3534
|
-
height: 48,
|
|
3535
|
-
borderRadius: 2,
|
|
3536
|
-
borderWidth: 1,
|
|
3537
|
-
borderColor: '#666',
|
|
3538
|
-
backgroundColor: '#222',
|
|
3539
|
-
justifyContent: 'center',
|
|
3540
|
-
alignItems: 'center',
|
|
3541
|
-
}}
|
|
3542
|
-
>
|
|
3543
|
-
<TextView
|
|
3544
|
-
style={{
|
|
3545
|
-
color: '#666',
|
|
3546
|
-
fontSize: 7,
|
|
3547
|
-
}}
|
|
3548
|
-
>
|
|
3549
|
-
—
|
|
3550
|
-
</TextView>
|
|
3551
|
-
</View>
|
|
3552
|
-
)}
|
|
3553
|
-
<TextView
|
|
3554
|
-
style={{
|
|
3555
|
-
color: currentFaceImage ? '#4CAF50' : '#999',
|
|
3556
|
-
fontSize: 7,
|
|
3557
|
-
marginTop: 1,
|
|
3558
|
-
fontWeight: 'bold',
|
|
3559
|
-
}}
|
|
3560
|
-
>
|
|
3561
|
-
{`${currentFaceImage ? '✓' : '○'} Face`}
|
|
3562
|
-
</TextView>
|
|
3563
|
-
</View>
|
|
3564
|
-
|
|
3565
|
-
{/* Secondary Face Image */}
|
|
3566
|
-
<View style={{ alignItems: 'center' }}>
|
|
3567
|
-
{currentSecondaryFaceImage ? (
|
|
3568
|
-
<Image
|
|
3569
|
-
source={{
|
|
3570
|
-
uri: `data:image/jpeg;base64,${currentSecondaryFaceImage}`,
|
|
3571
|
-
}}
|
|
3572
|
-
style={{
|
|
3573
|
-
width: 40,
|
|
3574
|
-
height: 48,
|
|
3575
|
-
borderRadius: 2,
|
|
3576
|
-
borderWidth: 1,
|
|
3577
|
-
borderColor: '#2196F3',
|
|
3578
|
-
}}
|
|
3579
|
-
/>
|
|
3580
|
-
) : (
|
|
3581
|
-
<View
|
|
3582
|
-
style={{
|
|
3583
|
-
width: 40,
|
|
3584
|
-
height: 48,
|
|
3585
|
-
borderRadius: 2,
|
|
3586
|
-
borderWidth: 1,
|
|
3587
|
-
borderColor: '#666',
|
|
3588
|
-
backgroundColor: '#222',
|
|
3589
|
-
justifyContent: 'center',
|
|
3590
|
-
alignItems: 'center',
|
|
3591
|
-
}}
|
|
3592
|
-
>
|
|
3593
|
-
<TextView
|
|
3594
|
-
style={{
|
|
3595
|
-
color: '#666',
|
|
3596
|
-
fontSize: 7,
|
|
3597
|
-
}}
|
|
3598
|
-
>
|
|
3599
|
-
—
|
|
3600
|
-
</TextView>
|
|
3601
|
-
</View>
|
|
3602
|
-
)}
|
|
3603
|
-
<TextView
|
|
3604
|
-
style={{
|
|
3605
|
-
color: currentSecondaryFaceImage ? '#2196F3' : '#999',
|
|
3606
|
-
fontSize: 7,
|
|
3607
|
-
marginTop: 1,
|
|
3608
|
-
fontWeight: 'bold',
|
|
3609
|
-
}}
|
|
3610
|
-
>
|
|
3611
|
-
{`${currentSecondaryFaceImage ? '✓' : '○'} 2nd`}
|
|
3612
|
-
</TextView>
|
|
3613
|
-
</View>
|
|
3614
|
-
|
|
3615
|
-
{/* Hologram Image */}
|
|
3616
|
-
<View style={{ alignItems: 'center' }}>
|
|
3617
|
-
{currentHologramImage ? (
|
|
3618
|
-
<Image
|
|
3619
|
-
source={{
|
|
3620
|
-
uri: `data:image/jpeg;base64,${currentHologramImage}`,
|
|
3621
|
-
}}
|
|
3622
|
-
style={{
|
|
3623
|
-
width: 40,
|
|
3624
|
-
height: 48,
|
|
3625
|
-
borderRadius: 2,
|
|
3626
|
-
borderWidth: 1,
|
|
3627
|
-
borderColor: '#9C27B0',
|
|
3628
|
-
}}
|
|
3629
|
-
/>
|
|
3630
|
-
) : latestHologramFaceImage && hologramImageCount > 0 ? (
|
|
3631
|
-
<View style={{ position: 'relative' }}>
|
|
3632
|
-
<Image
|
|
3633
|
-
source={{
|
|
3634
|
-
uri: `data:image/jpeg;base64,${latestHologramFaceImage}`,
|
|
3635
|
-
}}
|
|
3636
|
-
style={{
|
|
3637
|
-
width: 40,
|
|
3638
|
-
height: 48,
|
|
3639
|
-
borderRadius: 2,
|
|
3640
|
-
borderWidth: 1,
|
|
3641
|
-
borderColor: '#FFA500',
|
|
3642
|
-
opacity: 0.7,
|
|
3643
|
-
}}
|
|
3644
|
-
/>
|
|
3645
|
-
<View
|
|
3646
|
-
style={{
|
|
3647
|
-
position: 'absolute',
|
|
3648
|
-
bottom: 1,
|
|
3649
|
-
left: 1,
|
|
3650
|
-
right: 1,
|
|
3651
|
-
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
3652
|
-
paddingVertical: 0,
|
|
3653
|
-
borderRadius: 1,
|
|
3654
|
-
}}
|
|
3655
|
-
>
|
|
3656
|
-
<TextView
|
|
3657
|
-
style={{
|
|
3658
|
-
color: '#FFA500',
|
|
3659
|
-
fontSize: 6,
|
|
3660
|
-
textAlign: 'center',
|
|
3661
|
-
fontWeight: 'bold',
|
|
3662
|
-
}}
|
|
3663
|
-
>
|
|
3664
|
-
{hologramImageCount}/{HOLOGRAM_IMAGE_COUNT}
|
|
3665
|
-
</TextView>
|
|
3666
|
-
</View>
|
|
3667
|
-
</View>
|
|
3668
|
-
) : (
|
|
3669
|
-
<View
|
|
3670
|
-
style={{
|
|
3671
|
-
width: 40,
|
|
3672
|
-
height: 48,
|
|
3673
|
-
borderRadius: 2,
|
|
3674
|
-
borderWidth: 1,
|
|
3675
|
-
borderColor: '#666',
|
|
3676
|
-
backgroundColor: '#222',
|
|
3677
|
-
justifyContent: 'center',
|
|
3678
|
-
alignItems: 'center',
|
|
3679
|
-
}}
|
|
3680
|
-
>
|
|
3681
|
-
<TextView
|
|
3682
|
-
style={{
|
|
3683
|
-
color: '#666',
|
|
3684
|
-
fontSize: 7,
|
|
3685
|
-
}}
|
|
3686
|
-
>
|
|
3687
|
-
—
|
|
3688
|
-
</TextView>
|
|
3689
|
-
</View>
|
|
3690
|
-
)}
|
|
3691
|
-
<TextView
|
|
3692
|
-
style={{
|
|
3693
|
-
color: currentHologramImage
|
|
3694
|
-
? '#9C27B0'
|
|
3695
|
-
: latestHologramFaceImage
|
|
3696
|
-
? '#FFA500'
|
|
3697
|
-
: '#999',
|
|
3698
|
-
fontSize: 7,
|
|
3699
|
-
marginTop: 1,
|
|
3700
|
-
fontWeight: 'bold',
|
|
3701
|
-
}}
|
|
3702
|
-
>
|
|
3703
|
-
{`${currentHologramImage ? '✓' : latestHologramFaceImage ? '⏳' : '○'} Holo`}
|
|
3704
|
-
</TextView>
|
|
3705
|
-
</View>
|
|
3706
|
-
|
|
3707
|
-
{/* Hologram Mask Image */}
|
|
3708
|
-
<View style={{ alignItems: 'center' }}>
|
|
3709
|
-
{_currentHologramMaskImage ? (
|
|
3710
|
-
<Image
|
|
3711
|
-
source={{
|
|
3712
|
-
uri: `data:image/jpeg;base64,${_currentHologramMaskImage}`,
|
|
3713
|
-
}}
|
|
3714
|
-
style={{
|
|
3715
|
-
width: 40,
|
|
3716
|
-
height: 48,
|
|
3717
|
-
borderRadius: 2,
|
|
3718
|
-
borderWidth: 1,
|
|
3719
|
-
borderColor: '#FF6B6B',
|
|
3720
|
-
}}
|
|
3721
|
-
/>
|
|
3722
|
-
) : (
|
|
3723
|
-
<View
|
|
3724
|
-
style={{
|
|
3725
|
-
width: 40,
|
|
3726
|
-
height: 48,
|
|
3727
|
-
borderRadius: 2,
|
|
3728
|
-
borderWidth: 1,
|
|
3729
|
-
borderColor: '#666',
|
|
3730
|
-
backgroundColor: '#222',
|
|
3731
|
-
justifyContent: 'center',
|
|
3732
|
-
alignItems: 'center',
|
|
3733
|
-
}}
|
|
3734
|
-
>
|
|
3735
|
-
<TextView
|
|
3736
|
-
style={{
|
|
3737
|
-
color: '#666',
|
|
3738
|
-
fontSize: 7,
|
|
3739
|
-
}}
|
|
3740
|
-
>
|
|
3741
|
-
—
|
|
3742
|
-
</TextView>
|
|
3743
|
-
</View>
|
|
3744
|
-
)}
|
|
3745
|
-
<TextView
|
|
3746
|
-
style={{
|
|
3747
|
-
color: _currentHologramMaskImage ? '#FF6B6B' : '#999',
|
|
3748
|
-
fontSize: 7,
|
|
3749
|
-
marginTop: 1,
|
|
3750
|
-
fontWeight: 'bold',
|
|
3751
|
-
}}
|
|
3752
|
-
>
|
|
3753
|
-
{`${_currentHologramMaskImage ? '✓' : '○'} Mask`}
|
|
3754
|
-
</TextView>
|
|
3755
|
-
</View>
|
|
3756
|
-
</View>
|
|
3757
|
-
</View>
|
|
3758
|
-
</SafeAreaView>
|
|
2453
|
+
<DebugOverlay
|
|
2454
|
+
nextStep={nextStep}
|
|
2455
|
+
status={status}
|
|
2456
|
+
detectedDocumentType={detectedDocumentType}
|
|
2457
|
+
isBrightnessLow={isBrightnessLow}
|
|
2458
|
+
isFrameBlurry={isFrameBlurry}
|
|
2459
|
+
isTorchOn={isTorchOn}
|
|
2460
|
+
documentPlaneBounds={documentPlaneBounds}
|
|
2461
|
+
secondaryFaceBounds={secondaryFaceBounds}
|
|
2462
|
+
barcodeBounds={barcodeBounds}
|
|
2463
|
+
mrzBounds={mrzBounds}
|
|
2464
|
+
signatureBounds={signatureBounds}
|
|
2465
|
+
currentFaceImage={currentFaceImage}
|
|
2466
|
+
currentSecondaryFaceImage={currentSecondaryFaceImage}
|
|
2467
|
+
currentHologramImage={currentHologramImage}
|
|
2468
|
+
currentHologramMaskImage={_currentHologramMaskImage}
|
|
2469
|
+
latestHologramFaceImage={latestHologramFaceImage}
|
|
2470
|
+
hologramImageCount={hologramImageCount}
|
|
2471
|
+
allElementsDetected={allElementsDetected}
|
|
2472
|
+
elementsOutsideScanArea={elementsOutsideScanArea}
|
|
2473
|
+
/>
|
|
3759
2474
|
)}
|
|
3760
2475
|
{testMode && testModeData && (
|
|
3761
|
-
<
|
|
3762
|
-
style={{
|
|
3763
|
-
position: 'absolute',
|
|
3764
|
-
bottom: 0,
|
|
3765
|
-
left: 0,
|
|
3766
|
-
right: 0,
|
|
3767
|
-
maxHeight: '40%',
|
|
3768
|
-
backgroundColor: 'rgba(0, 0, 0, 0.95)',
|
|
3769
|
-
borderTopWidth: 2,
|
|
3770
|
-
borderTopColor: '#FFA500',
|
|
3771
|
-
}}
|
|
3772
|
-
>
|
|
3773
|
-
<ScrollView style={{ flex: 1 }}>
|
|
3774
|
-
<View style={{ padding: 10 }}>
|
|
3775
|
-
<TextView
|
|
3776
|
-
style={{
|
|
3777
|
-
color: '#FFA500',
|
|
3778
|
-
fontSize: 12,
|
|
3779
|
-
fontWeight: 'bold',
|
|
3780
|
-
marginBottom: 8,
|
|
3781
|
-
textAlign: 'center',
|
|
3782
|
-
}}
|
|
3783
|
-
>
|
|
3784
|
-
MRZ Text Read
|
|
3785
|
-
</TextView>
|
|
3786
|
-
|
|
3787
|
-
<TextView
|
|
3788
|
-
style={{
|
|
3789
|
-
color: '#FFFFFF',
|
|
3790
|
-
fontSize: 9,
|
|
3791
|
-
fontFamily: 'monospace',
|
|
3792
|
-
lineHeight: 16,
|
|
3793
|
-
}}
|
|
3794
|
-
>
|
|
3795
|
-
{testModeData.mrzText
|
|
3796
|
-
.split('\n')
|
|
3797
|
-
.map(
|
|
3798
|
-
(line, i) =>
|
|
3799
|
-
`Line ${i + 1}: ${line} (${line.length} chars)`
|
|
3800
|
-
)
|
|
3801
|
-
.join('\n')}
|
|
3802
|
-
</TextView>
|
|
3803
|
-
</View>
|
|
3804
|
-
</ScrollView>
|
|
3805
|
-
</SafeAreaView>
|
|
2476
|
+
<TestModePanel mrzText={testModeData.mrzText} />
|
|
3806
2477
|
)}
|
|
3807
2478
|
</>
|
|
3808
2479
|
)}
|
|
@@ -3811,9 +2482,6 @@ const IdentityDocumentCamera = ({
|
|
|
3811
2482
|
};
|
|
3812
2483
|
|
|
3813
2484
|
const styles = StyleSheet.create({
|
|
3814
|
-
container: {
|
|
3815
|
-
flex: 1,
|
|
3816
|
-
},
|
|
3817
2485
|
permissionContainer: {
|
|
3818
2486
|
flex: 1,
|
|
3819
2487
|
justifyContent: 'center',
|
|
@@ -3910,50 +2578,6 @@ const styles = StyleSheet.create({
|
|
|
3910
2578
|
gap: 10,
|
|
3911
2579
|
justifyContent: 'flex-start',
|
|
3912
2580
|
},
|
|
3913
|
-
debugImagesRow: {
|
|
3914
|
-
display: 'flex',
|
|
3915
|
-
flexDirection: 'row',
|
|
3916
|
-
gap: 10,
|
|
3917
|
-
justifyContent: 'center',
|
|
3918
|
-
flexWrap: 'wrap',
|
|
3919
|
-
},
|
|
3920
|
-
cardDetectionRow: {
|
|
3921
|
-
display: 'flex',
|
|
3922
|
-
flexDirection: 'row',
|
|
3923
|
-
justifyContent: 'center',
|
|
3924
|
-
marginTop: 5,
|
|
3925
|
-
},
|
|
3926
|
-
imageContainer: {
|
|
3927
|
-
display: 'flex',
|
|
3928
|
-
flexDirection: 'column',
|
|
3929
|
-
alignItems: 'center',
|
|
3930
|
-
},
|
|
3931
|
-
imageContainerText: {
|
|
3932
|
-
color: 'white',
|
|
3933
|
-
fontSize: 8,
|
|
3934
|
-
textAlign: 'center',
|
|
3935
|
-
fontWeight: 'bold',
|
|
3936
|
-
marginTop: 2,
|
|
3937
|
-
},
|
|
3938
|
-
faceImage: {
|
|
3939
|
-
width: 60,
|
|
3940
|
-
height: 80,
|
|
3941
|
-
borderRadius: 4,
|
|
3942
|
-
borderWidth: 1,
|
|
3943
|
-
borderColor: 'white',
|
|
3944
|
-
},
|
|
3945
|
-
cardDetectionImage: {
|
|
3946
|
-
width: 160,
|
|
3947
|
-
height: 120,
|
|
3948
|
-
borderRadius: 8,
|
|
3949
|
-
borderWidth: 2,
|
|
3950
|
-
borderColor: '#FF9800',
|
|
3951
|
-
},
|
|
3952
|
-
cardDetectionContainer: {
|
|
3953
|
-
display: 'flex',
|
|
3954
|
-
flexDirection: 'column',
|
|
3955
|
-
alignItems: 'center',
|
|
3956
|
-
},
|
|
3957
2581
|
guide: {
|
|
3958
2582
|
flex: 1,
|
|
3959
2583
|
display: 'flex',
|