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