@trustchex/react-native-sdk 1.355.1 → 1.357.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/README.md +2 -9
- package/TrustchexSDK.podspec +5 -4
- package/android/build.gradle +6 -4
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +45 -25
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraManager.kt +168 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +871 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +245 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidationModule.kt +785 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidator.kt +419 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +818 -0
- package/ios/Camera/TrustchexCameraManager.m +37 -0
- package/ios/Camera/TrustchexCameraManager.swift +125 -0
- package/ios/Camera/TrustchexCameraView.swift +1176 -0
- package/ios/MLKit/MLKitModule.m +23 -0
- package/ios/MLKit/MLKitModule.swift +250 -0
- package/ios/MRZValidation.m +39 -0
- package/ios/MRZValidation.swift +802 -0
- package/ios/MRZValidator.swift +466 -0
- package/ios/OpenCV/OpenCVModule.h +4 -0
- package/ios/OpenCV/OpenCVModule.mm +810 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +2 -3
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +1 -2
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +418 -193
- package/lib/module/Screens/Static/OTPVerificationScreen.js +11 -11
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +5 -1
- package/lib/module/Screens/Static/ResultScreen.js +25 -2
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +25 -7
- package/lib/module/Shared/Components/DebugNavigationPanel.js +234 -24
- package/lib/module/Shared/Components/EIDScanner.js +99 -9
- package/lib/module/Shared/Components/FaceCamera.js +170 -179
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +2151 -771
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +109 -107
- package/lib/module/Shared/Components/TrustchexCamera.js +122 -0
- package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +91 -0
- package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +2 -124
- package/lib/module/Shared/EIDReader/tlv/tlvInputStream.js +4 -4
- package/lib/module/Shared/EIDReader/tlv/tlvOutputState.js +4 -4
- package/lib/module/Shared/EIDReader/tlv/tlvOutputStream.js +4 -4
- package/lib/module/Shared/Libs/analytics.utils.js +2 -2
- package/lib/module/Shared/Libs/debug.utils.js +132 -0
- package/lib/module/Shared/Libs/deeplink.utils.js +6 -5
- package/lib/module/Shared/Libs/demo.utils.js +13 -3
- package/lib/module/Shared/Libs/mrz.utils.js +1 -175
- package/lib/module/Shared/Libs/native-device-info.utils.js +12 -6
- package/lib/module/Shared/Libs/tts.utils.js +40 -6
- package/lib/module/Shared/Services/AnalyticsService.js +9 -8
- package/lib/module/Shared/Types/mrzFields.js +1 -0
- package/lib/module/Translation/Resources/en.js +87 -88
- package/lib/module/Translation/Resources/tr.js +84 -85
- package/lib/module/Trustchex.js +9 -2
- package/lib/module/index.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +2 -2
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +18 -4
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -4
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts +2 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +124 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts +11 -0
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts +2 -39
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/debug.utils.d.ts +42 -0
- package/lib/typescript/src/Shared/Libs/debug.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +0 -4
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/tts.utils.d.ts +4 -3
- package/lib/typescript/src/Shared/Libs/tts.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +2 -2
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts +11 -0
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +4 -5
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +4 -5
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +2 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +4 -35
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +7 -5
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +2 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +498 -216
- package/src/Screens/Static/OTPVerificationScreen.tsx +37 -31
- package/src/Screens/Static/QrCodeScanningScreen.tsx +8 -1
- package/src/Screens/Static/ResultScreen.tsx +136 -88
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +46 -13
- package/src/Shared/Components/DebugNavigationPanel.tsx +290 -34
- package/src/Shared/Components/EIDScanner.tsx +94 -16
- package/src/Shared/Components/FaceCamera.tsx +236 -203
- package/src/Shared/Components/IdentityDocumentCamera.tsx +3073 -1030
- package/src/Shared/Components/QrCodeScannerCamera.tsx +133 -127
- package/src/Shared/Components/TrustchexCamera.tsx +289 -0
- package/src/Shared/Config/camera-enhancement.config.ts +2 -2
- package/src/Shared/EIDReader/tlv/tlv.helpers.ts +96 -0
- package/src/Shared/EIDReader/tlv/tlv.utils.ts +2 -125
- package/src/Shared/EIDReader/tlv/tlvInputStream.ts +4 -4
- package/src/Shared/EIDReader/tlv/tlvOutputState.ts +4 -4
- package/src/Shared/EIDReader/tlv/tlvOutputStream.ts +4 -4
- package/src/Shared/Libs/analytics.utils.ts +48 -20
- package/src/Shared/Libs/debug.utils.ts +149 -0
- package/src/Shared/Libs/deeplink.utils.ts +7 -5
- package/src/Shared/Libs/demo.utils.ts +4 -0
- package/src/Shared/Libs/http-client.ts +12 -8
- package/src/Shared/Libs/mrz.utils.ts +1 -163
- package/src/Shared/Libs/native-device-info.utils.ts +12 -6
- package/src/Shared/Libs/tts.utils.ts +48 -6
- package/src/Shared/Services/AnalyticsService.ts +69 -24
- package/src/Shared/Types/identificationInfo.ts +2 -2
- package/src/Shared/Types/mrzFields.ts +29 -0
- package/src/Translation/Resources/en.ts +90 -100
- package/src/Translation/Resources/tr.ts +89 -97
- package/src/Translation/index.ts +1 -1
- package/src/Trustchex.tsx +21 -4
- package/src/index.tsx +14 -0
- package/src/version.ts +1 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/barcodescanner/BarcodeScannerFrameProcessorPlugin.kt +0 -301
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/BitmapUtils.kt +0 -205
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/CropperPlugin.kt +0 -72
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/FrameMetadata.kt +0 -4
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/facedetector/FaceDetectorFrameProcessorPlugin.kt +0 -303
- package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/textrecognition/TextRecognitionFrameProcessorPlugin.kt +0 -115
- package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin-Bridging-Header.h +0 -9
- package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.mm +0 -22
- package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.swift +0 -188
- package/ios/VisionCameraPlugins/Cropper/Cropper-Bridging-Header.h +0 -13
- package/ios/VisionCameraPlugins/Cropper/Cropper.h +0 -20
- package/ios/VisionCameraPlugins/Cropper/Cropper.mm +0 -22
- package/ios/VisionCameraPlugins/Cropper/Cropper.swift +0 -145
- package/ios/VisionCameraPlugins/Cropper/CropperUtils.swift +0 -49
- package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin-Bridging-Header.h +0 -4
- package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.mm +0 -22
- package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.swift +0 -320
- package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin-Bridging-Header.h +0 -4
- package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.mm +0 -27
- package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.swift +0 -144
- package/lib/module/Shared/Libs/camera.utils.js +0 -308
- package/lib/module/Shared/Libs/frame-enhancement.utils.js +0 -133
- package/lib/module/Shared/Libs/opencv.utils.js +0 -21
- package/lib/module/Shared/Libs/worklet.utils.js +0 -68
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.js +0 -46
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +0 -35
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/index.js +0 -19
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.js +0 -26
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/types.js +0 -3
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.js +0 -197
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.js +0 -101
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.js +0 -60
- package/lib/module/Shared/VisionCameraPlugins/Cropper/index.js +0 -47
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/Camera.js +0 -42
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/detectFaces.js +0 -35
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/index.js +0 -4
- package/lib/module/Shared/VisionCameraPlugins/FaceDetector/types.js +0 -3
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/Camera.js +0 -56
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.js +0 -20
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.js +0 -9
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/index.js +0 -6
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/scanText.js +0 -20
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/translateText.js +0 -19
- package/lib/module/Shared/VisionCameraPlugins/TextRecognition/types.js +0 -3
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +0 -87
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts +0 -25
- package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts +0 -3
- package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts +0 -9
- package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts +0 -13
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts +0 -6
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts +0 -12
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts +0 -52
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts +0 -62
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts +0 -34
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts +0 -32
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts +0 -23
- package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts +0 -9
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts +0 -79
- package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts +0 -6
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts +0 -5
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts +0 -3
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts.map +0 -1
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts +0 -67
- package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts.map +0 -1
- package/src/Shared/Libs/camera.utils.ts +0 -345
- package/src/Shared/Libs/frame-enhancement.utils.ts +0 -217
- package/src/Shared/Libs/opencv.utils.ts +0 -40
- package/src/Shared/Libs/worklet.utils.ts +0 -72
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.ts +0 -79
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +0 -46
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/index.ts +0 -60
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.ts +0 -32
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/types.ts +0 -82
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.ts +0 -195
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.ts +0 -135
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.ts +0 -84
- package/src/Shared/VisionCameraPlugins/Cropper/index.ts +0 -78
- package/src/Shared/VisionCameraPlugins/FaceDetector/Camera.tsx +0 -63
- package/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.ts +0 -44
- package/src/Shared/VisionCameraPlugins/FaceDetector/index.ts +0 -3
- package/src/Shared/VisionCameraPlugins/FaceDetector/types.ts +0 -99
- package/src/Shared/VisionCameraPlugins/TextRecognition/Camera.tsx +0 -76
- package/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.ts +0 -18
- package/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.ts +0 -7
- package/src/Shared/VisionCameraPlugins/TextRecognition/index.ts +0 -7
- package/src/Shared/VisionCameraPlugins/TextRecognition/scanText.ts +0 -27
- package/src/Shared/VisionCameraPlugins/TextRecognition/translateText.ts +0 -21
- package/src/Shared/VisionCameraPlugins/TextRecognition/types.ts +0 -141
|
@@ -1,60 +1,41 @@
|
|
|
1
1
|
/* eslint-disable react-native/no-inline-styles */
|
|
2
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
View,
|
|
5
5
|
StyleSheet,
|
|
6
6
|
Text as TextView,
|
|
7
7
|
Platform,
|
|
8
|
+
StatusBar,
|
|
8
9
|
Vibration,
|
|
9
|
-
TouchableOpacity,
|
|
10
|
-
type GestureResponderEvent,
|
|
11
|
-
Text,
|
|
12
10
|
Linking,
|
|
13
11
|
Image,
|
|
14
12
|
ActivityIndicator,
|
|
13
|
+
PermissionsAndroid,
|
|
14
|
+
Dimensions,
|
|
15
|
+
type NativeSyntheticEvent,
|
|
16
|
+
type ViewStyle,
|
|
15
17
|
} from 'react-native';
|
|
16
18
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
19
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from 'react-native-vision-camera';
|
|
25
|
-
import type { Frame } from 'react-native-vision-camera';
|
|
26
|
-
import { runAsync } from '../Libs/worklet.utils';
|
|
27
|
-
import { useRunOnJS, useSharedValue } from 'react-native-worklets-core';
|
|
28
|
-
import { useTextRecognition } from '../VisionCameraPlugins/TextRecognition';
|
|
29
|
-
import type { FieldRecords } from 'mrz';
|
|
30
|
-
import {
|
|
31
|
-
type Face,
|
|
32
|
-
useFaceDetector,
|
|
33
|
-
} from '../VisionCameraPlugins/FaceDetector';
|
|
34
|
-
import mrzUtils from '../Libs/mrz.utils';
|
|
35
|
-
import { crop, type CropResult } from '../VisionCameraPlugins/Cropper';
|
|
20
|
+
TrustchexCamera,
|
|
21
|
+
type TrustchexCameraHandle,
|
|
22
|
+
type Frame,
|
|
23
|
+
} from './TrustchexCamera';
|
|
24
|
+
import { NativeModules } from 'react-native';
|
|
25
|
+
import type { MRZFields } from '../Types/mrzFields';
|
|
36
26
|
import { useKeepAwake } from '../Libs/native-keep-awake.utils';
|
|
37
|
-
import ImageEditor from '@react-native-community/image-editor';
|
|
38
27
|
import { useIsFocused } from '@react-navigation/native';
|
|
39
|
-
import {
|
|
40
|
-
AdaptiveThresholdTypes,
|
|
41
|
-
ColorConversionCodes,
|
|
42
|
-
DataTypes,
|
|
43
|
-
type Mat,
|
|
44
|
-
ObjectType,
|
|
45
|
-
OpenCV,
|
|
46
|
-
ThresholdTypes,
|
|
47
|
-
} from 'react-native-fast-opencv';
|
|
48
|
-
import { getAverageBrightness, getScanAreaCenterPoint, calculateExposureStep, isBlurry as checkBlurry } from '../Libs/camera.utils';
|
|
49
28
|
import { useTranslation } from 'react-i18next';
|
|
29
|
+
import { debugLog, logError, isDebugEnabled } from '../Libs/debug.utils';
|
|
50
30
|
import LottieView from 'lottie-react-native';
|
|
51
31
|
import StyledButton from './StyledButton';
|
|
52
32
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
53
|
-
import {
|
|
54
|
-
import { speakWithDebounce } from '../Libs/tts.utils';
|
|
33
|
+
import { speak, resetLastMessage } from '../Libs/tts.utils';
|
|
55
34
|
import AppContext from '../Contexts/AppContext';
|
|
56
35
|
import { useTheme } from '../Contexts/ThemeContext';
|
|
57
36
|
|
|
37
|
+
const { OpenCVModule } = NativeModules;
|
|
38
|
+
|
|
58
39
|
export type DocumentScannedData = {
|
|
59
40
|
documentType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN';
|
|
60
41
|
image: string;
|
|
@@ -63,7 +44,7 @@ export type DocumentScannedData = {
|
|
|
63
44
|
hologramImage?: string;
|
|
64
45
|
barcodeValue?: string;
|
|
65
46
|
mrzText?: string;
|
|
66
|
-
mrzFields?:
|
|
47
|
+
mrzFields?: MRZFields;
|
|
67
48
|
};
|
|
68
49
|
|
|
69
50
|
export type BlockText = {
|
|
@@ -107,63 +88,64 @@ type ElementsData = [
|
|
|
107
88
|
export type PhotoOptions = {
|
|
108
89
|
uri: string;
|
|
109
90
|
orientation?:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
91
|
+
| 'landscapeRight'
|
|
92
|
+
| 'portrait'
|
|
93
|
+
| 'portraitUpsideDown'
|
|
94
|
+
| 'landscapeLeft';
|
|
114
95
|
};
|
|
115
96
|
|
|
116
97
|
export interface IdentityDocumentCameraProps {
|
|
117
98
|
onlyMRZScan: boolean;
|
|
118
99
|
onIdentityDocumentScanned: (scannedData: DocumentScannedData) => void;
|
|
119
|
-
showDebugImages?: boolean; // For development: show detected face and other images
|
|
120
100
|
}
|
|
121
101
|
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
interface Face {
|
|
103
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
104
|
+
rollAngle?: number;
|
|
105
|
+
pitchAngle?: number;
|
|
106
|
+
yawAngle?: number;
|
|
107
|
+
leftEyeOpenProbability?: number;
|
|
108
|
+
rightEyeOpenProbability?: number;
|
|
109
|
+
smilingProbability?: number;
|
|
110
|
+
}
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
112
|
+
interface Barcode {
|
|
113
|
+
rawValue: string;
|
|
114
|
+
displayValue: string;
|
|
115
|
+
format: number;
|
|
116
|
+
boundingBox: { left: number; top: number; right: number; bottom: number };
|
|
117
|
+
cornerPoints: Array<{ x: number; y: number }>;
|
|
118
|
+
value?: string;
|
|
119
|
+
}
|
|
130
120
|
|
|
131
|
-
|
|
121
|
+
const HOLOGRAM_IMAGE_COUNT = 12;
|
|
122
|
+
const HOLOGRAM_DETECTION_THRESHOLD = 1000; // Lowered for better sensitivity while still filtering noise
|
|
123
|
+
const HOLOGRAM_DETECTION_RETRY_COUNT = 3; // More attempts but faster each (~1s per attempt)
|
|
124
|
+
const SECOND_FACE_DETECTION_RETRY_COUNT = 10;
|
|
125
|
+
const MIN_BRIGHTNESS_THRESHOLD = 60;
|
|
126
|
+
const MAX_CONSECUTIVE_QUALITY_FAILURES = 30;
|
|
132
127
|
|
|
133
128
|
const IdentityDocumentCamera = ({
|
|
134
129
|
onlyMRZScan,
|
|
135
130
|
onIdentityDocumentScanned,
|
|
136
|
-
showDebugImages = false,
|
|
137
131
|
}: IdentityDocumentCameraProps) => {
|
|
138
132
|
useKeepAwake();
|
|
139
133
|
const theme = useTheme();
|
|
140
134
|
const insets = useSafeAreaInsets();
|
|
141
135
|
const appContext = React.useContext(AppContext);
|
|
142
|
-
const cameraRef =
|
|
143
|
-
const
|
|
144
|
-
const [permissionsRequested, setPermissionsRequested] =
|
|
145
|
-
const [isActive, setIsActive] =
|
|
136
|
+
const cameraRef = useRef<TrustchexCameraHandle>(null);
|
|
137
|
+
const [hasPermission, setHasPermission] = useState(false);
|
|
138
|
+
const [permissionsRequested, setPermissionsRequested] = useState(false);
|
|
139
|
+
const [isActive, setIsActive] = useState(false);
|
|
146
140
|
const isFocused = useIsFocused();
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
videoResolution: {
|
|
156
|
-
width: 1920,
|
|
157
|
-
height: 1080,
|
|
158
|
-
},
|
|
159
|
-
iso: 'max',
|
|
160
|
-
photoHdr: false,
|
|
161
|
-
videoHdr: false,
|
|
162
|
-
videoStabilizationMode: 'standard',
|
|
163
|
-
autoFocusSystem: 'phase-detection',
|
|
164
|
-
},
|
|
165
|
-
]);
|
|
166
|
-
const isCameraInitialized = useSharedValue(false);
|
|
141
|
+
const isTorchOnRef = useRef(false);
|
|
142
|
+
const [isTorchOn, _setIsTorchOn] = useState(false);
|
|
143
|
+
const setIsTorchOn = useCallback((val: boolean) => {
|
|
144
|
+
isTorchOnRef.current = val;
|
|
145
|
+
_setIsTorchOn(val);
|
|
146
|
+
}, []);
|
|
147
|
+
const [_exposure, _setExposure] = useState(0);
|
|
148
|
+
const isCameraInitialized = useRef(false);
|
|
167
149
|
const [currentFaceImage, setCurrentFaceImage] = useState<string | undefined>(
|
|
168
150
|
undefined
|
|
169
151
|
);
|
|
@@ -191,152 +173,354 @@ const IdentityDocumentCamera = ({
|
|
|
191
173
|
const [detectedDocumentType, setDetectedDocumentType] = useState<
|
|
192
174
|
'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN'
|
|
193
175
|
>('UNKNOWN');
|
|
194
|
-
const hologramDetectionCurrentRetryCount =
|
|
195
|
-
const secondaryFaceDetectionCurrentRetryCount =
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
176
|
+
const hologramDetectionCurrentRetryCount = useRef(0);
|
|
177
|
+
const secondaryFaceDetectionCurrentRetryCount = useRef(0);
|
|
178
|
+
const consecutiveQualityFailures = useRef(0);
|
|
179
|
+
const mrzDetectionCurrentRetryCount = useRef(0);
|
|
180
|
+
|
|
181
|
+
// MRZ stability tracking - require consistent valid reads
|
|
182
|
+
const lastValidMRZText = useRef<string | null>(null);
|
|
183
|
+
const lastValidMRZFields = useRef<any>(null);
|
|
184
|
+
const validMRZConsecutiveCount = useRef(0);
|
|
185
|
+
const REQUIRED_CONSISTENT_MRZ_READS = 2; // Require 2 consistent valid reads
|
|
186
|
+
|
|
187
|
+
// Document type stability tracking - require consistent detections from good quality frames
|
|
188
|
+
const lastDetectedDocType = useRef<
|
|
189
|
+
'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN'
|
|
190
|
+
>('UNKNOWN');
|
|
191
|
+
const consistentDocTypeCount = useRef(0);
|
|
192
|
+
const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3; // Require 3 consistent detections from quality frames
|
|
193
|
+
|
|
194
|
+
// Frame quality tracking - persist across callbacks
|
|
195
|
+
const lastFrameQuality = useRef({
|
|
196
|
+
hasAcceptableQuality: true,
|
|
197
|
+
isBlurry: false,
|
|
198
|
+
brightness: 128,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Barcode caching - persist detected barcode across frames for reliability
|
|
202
|
+
const cachedBarcode = useRef<Barcode | null>(null);
|
|
203
|
+
|
|
204
|
+
// Helper to compare MRZ field values (ignore raw text variations)
|
|
205
|
+
const areMRZFieldsEqual = useCallback(
|
|
206
|
+
(fields1: any, fields2: any): boolean => {
|
|
207
|
+
if (!fields1 || !fields2) return false;
|
|
208
|
+
// Compare critical fields that define document identity
|
|
209
|
+
return (
|
|
210
|
+
fields1.documentNumber === fields2.documentNumber &&
|
|
211
|
+
fields1.birthDate === fields2.birthDate &&
|
|
212
|
+
fields1.expirationDate === fields2.expirationDate &&
|
|
213
|
+
fields1.firstName === fields2.firstName &&
|
|
214
|
+
fields1.lastName === fields2.lastName &&
|
|
215
|
+
fields1.issuingState === fields2.issuingState
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
[]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Helper functions to reduce duplication
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if all required MRZ fields are present
|
|
225
|
+
*/
|
|
226
|
+
const hasRequiredMRZFields = useCallback(
|
|
227
|
+
(fields: any): boolean =>
|
|
228
|
+
!!fields?.firstName &&
|
|
229
|
+
!!fields?.lastName &&
|
|
230
|
+
!!fields?.documentNumber &&
|
|
231
|
+
!!fields?.birthDate,
|
|
232
|
+
[]
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Log detailed MRZ information for debugging and verification
|
|
237
|
+
*/
|
|
238
|
+
const logMRZDetails = useCallback(
|
|
239
|
+
(
|
|
240
|
+
stepName: string,
|
|
241
|
+
fields: any,
|
|
242
|
+
mrzText: string | null,
|
|
243
|
+
consecutiveReads: number,
|
|
244
|
+
isDebugMode: boolean
|
|
245
|
+
) => {
|
|
246
|
+
if (isDebugMode) {
|
|
247
|
+
debugLog(
|
|
248
|
+
'IdentityDocumentCamera',
|
|
249
|
+
`[${stepName}] MRZ validated successfully (consistent reads: ${consecutiveReads})`
|
|
250
|
+
);
|
|
251
|
+
debugLog('IdentityDocumentCamera', `[${stepName}] Final MRZ Fields:`, {
|
|
252
|
+
documentNumber: fields?.documentNumber,
|
|
253
|
+
name: `${fields?.lastName} ${fields?.firstName}`,
|
|
254
|
+
birthDate: fields?.birthDate,
|
|
255
|
+
expirationDate: fields?.expirationDate,
|
|
256
|
+
nationality: fields?.nationality || fields?.issuingState,
|
|
257
|
+
sex: fields?.sex,
|
|
258
|
+
personalId: fields?.optional1,
|
|
259
|
+
});
|
|
260
|
+
if (mrzText) {
|
|
261
|
+
const mrzLines = mrzText
|
|
262
|
+
.split('\n')
|
|
263
|
+
.map((l) => l.replace(/\s/g, ''))
|
|
264
|
+
.filter((l) => l.length >= 20 && /^[A-Z0-9<]+$/.test(l));
|
|
265
|
+
debugLog(
|
|
266
|
+
'IdentityDocumentCamera',
|
|
267
|
+
`[${stepName}] MRZ lines (${mrzLines.length}):`
|
|
268
|
+
);
|
|
269
|
+
mrzLines.forEach((line, idx) => {
|
|
270
|
+
debugLog('IdentityDocumentCamera', ` ${idx + 1}: ${line}`);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
[]
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Log MRZ validation failure details for debugging
|
|
280
|
+
*/
|
|
281
|
+
const logMRZValidationFailure = useCallback(
|
|
282
|
+
(
|
|
283
|
+
stepName: string,
|
|
284
|
+
hasRequiredFields: boolean,
|
|
285
|
+
parsedData: any,
|
|
286
|
+
retryCount: number,
|
|
287
|
+
isDebugMode: boolean
|
|
288
|
+
) => {
|
|
289
|
+
if (isDebugMode) {
|
|
290
|
+
const debugInfo: any = {
|
|
291
|
+
hasRequiredFields,
|
|
292
|
+
isValid: parsedData?.valid,
|
|
293
|
+
retryCount,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
if (parsedData?.valid) {
|
|
297
|
+
debugInfo.consistentReads = validMRZConsecutiveCount.current;
|
|
298
|
+
debugInfo.requiredReads = REQUIRED_CONSISTENT_MRZ_READS;
|
|
299
|
+
debugInfo.fieldsMatch = areMRZFieldsEqual(
|
|
300
|
+
lastValidMRZFields.current,
|
|
301
|
+
parsedData?.fields
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
debugLog(
|
|
306
|
+
'IdentityDocumentCamera',
|
|
307
|
+
`[${stepName}] MRZ detected but validation failed - retrying`,
|
|
308
|
+
debugInfo
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
[areMRZFieldsEqual]
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const lastHologramCaptureTime = useRef(0);
|
|
316
|
+
const HOLOGRAM_CAPTURE_INTERVAL = 250; // ms between captures - allows flash to fully stabilize for accurate diff
|
|
317
|
+
const hologramFramesWithoutFace = useRef(0); // Track consecutive frames without face during hologram collection
|
|
318
|
+
const HOLOGRAM_MAX_FRAMES_WITHOUT_FACE = 30; // ~1 second at 30fps - safety timeout
|
|
319
|
+
|
|
320
|
+
const faceDetectionErrorCount = useRef(0);
|
|
321
|
+
const brightnessHistory = useRef<number[]>([]);
|
|
199
322
|
const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
|
|
323
|
+
const faceImages = useRef<string[]>([]);
|
|
324
|
+
const hologramImageCountRef = useRef(0);
|
|
325
|
+
const [hologramImageCount, setHologramImageCount] = useState(0);
|
|
326
|
+
const lastVoiceGuidanceMessage = useRef<string>('');
|
|
327
|
+
const [latestHologramFaceImage, setLatestHologramFaceImage] = useState<
|
|
328
|
+
string | undefined
|
|
329
|
+
>(undefined);
|
|
330
|
+
const lastFacePosition = useRef<{
|
|
331
|
+
x: number;
|
|
332
|
+
y: number;
|
|
333
|
+
width: number;
|
|
334
|
+
height: number;
|
|
335
|
+
} | null>(null);
|
|
336
|
+
const [documentPlaneBounds, setDocumentPlaneBounds] = useState<{
|
|
337
|
+
x: number;
|
|
338
|
+
y: number;
|
|
339
|
+
width: number;
|
|
340
|
+
height: number;
|
|
341
|
+
rollAngle?: number;
|
|
342
|
+
cropPadding?: number;
|
|
343
|
+
} | null>(null);
|
|
344
|
+
const [secondaryFaceBounds, setSecondaryFaceBounds] = useState<{
|
|
345
|
+
x: number;
|
|
346
|
+
y: number;
|
|
347
|
+
width: number;
|
|
348
|
+
height: number;
|
|
349
|
+
rollAngle?: number;
|
|
350
|
+
cropPadding?: number;
|
|
351
|
+
} | null>(null);
|
|
352
|
+
const [barcodeBounds, setBarcodeBounds] = useState<{
|
|
353
|
+
x: number;
|
|
354
|
+
y: number;
|
|
355
|
+
width: number;
|
|
356
|
+
height: number;
|
|
357
|
+
angle?: number;
|
|
358
|
+
corners?: Array<{ x: number; y: number }>;
|
|
359
|
+
} | null>(null);
|
|
360
|
+
const [mrzBounds, setMrzBounds] = useState<{
|
|
361
|
+
x: number;
|
|
362
|
+
y: number;
|
|
363
|
+
width: number;
|
|
364
|
+
height: number;
|
|
365
|
+
angle?: number;
|
|
366
|
+
corners?: Array<{ x: number; y: number }>;
|
|
367
|
+
} | null>(null);
|
|
368
|
+
const [signatureBounds, setSignatureBounds] = useState<{
|
|
369
|
+
x: number;
|
|
370
|
+
y: number;
|
|
371
|
+
width: number;
|
|
372
|
+
height: number;
|
|
373
|
+
angle?: number;
|
|
374
|
+
corners?: Array<{ x: number; y: number }>;
|
|
375
|
+
} | null>(null);
|
|
376
|
+
const [frameDimensions, setFrameDimensions] = useState<{
|
|
377
|
+
width: number;
|
|
378
|
+
height: number;
|
|
379
|
+
} | null>(null);
|
|
380
|
+
|
|
381
|
+
// Track if all required elements are detected in current frame
|
|
382
|
+
const [allElementsDetected, setAllElementsDetected] = useState(false);
|
|
383
|
+
// Track if detected elements are within scan area
|
|
384
|
+
const [elementsOutsideScanArea, setElementsOutsideScanArea] = useState<
|
|
385
|
+
string[]
|
|
386
|
+
>([]);
|
|
200
387
|
const { t } = useTranslation();
|
|
201
|
-
// const [boundingBox, setBoundingBox] = useState<Bounds>({
|
|
202
|
-
// x: 0,
|
|
203
|
-
// y: 0,
|
|
204
|
-
// width: 0,
|
|
205
|
-
// height: 0,
|
|
206
|
-
// });
|
|
207
|
-
|
|
208
|
-
const { scanText } = useTextRecognition({
|
|
209
|
-
language: 'latin',
|
|
210
|
-
});
|
|
211
|
-
const { detectFaces } = useFaceDetector({
|
|
212
|
-
contourMode: 'none',
|
|
213
|
-
landmarkMode: 'none',
|
|
214
|
-
classificationMode: 'all',
|
|
215
|
-
performanceMode: 'accurate',
|
|
216
|
-
trackingEnabled: false,
|
|
217
|
-
minFaceSize: 0.1,
|
|
218
|
-
autoScale: false,
|
|
219
|
-
});
|
|
220
388
|
|
|
221
389
|
useEffect(() => {
|
|
222
390
|
const requestPermissions = async () => {
|
|
223
|
-
if (
|
|
224
|
-
await
|
|
391
|
+
if (Platform.OS === 'android') {
|
|
392
|
+
const granted = await PermissionsAndroid.request(
|
|
393
|
+
PermissionsAndroid.PERMISSIONS.CAMERA
|
|
394
|
+
);
|
|
395
|
+
setHasPermission(granted === PermissionsAndroid.RESULTS.GRANTED);
|
|
396
|
+
} else {
|
|
397
|
+
setHasPermission(true);
|
|
225
398
|
}
|
|
226
399
|
setPermissionsRequested(true);
|
|
227
400
|
};
|
|
228
401
|
requestPermissions();
|
|
229
|
-
}, [
|
|
402
|
+
}, []);
|
|
230
403
|
|
|
231
404
|
useEffect(() => {
|
|
232
|
-
if (
|
|
405
|
+
if (isFocused && hasPermission && hasGuideShown) {
|
|
233
406
|
setIsActive(true);
|
|
234
407
|
} else {
|
|
235
408
|
setIsActive(false);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
409
|
+
faceImages.current = [];
|
|
410
|
+
hologramImageCountRef.current = 0;
|
|
411
|
+
setHologramImageCount(0);
|
|
412
|
+
setLatestHologramFaceImage(undefined);
|
|
413
|
+
hologramDetectionCurrentRetryCount.current = 0;
|
|
414
|
+
secondaryFaceDetectionCurrentRetryCount.current = 0;
|
|
415
|
+
mrzDetectionCurrentRetryCount.current = 0;
|
|
416
|
+
lastValidMRZText.current = null;
|
|
417
|
+
lastValidMRZFields.current = null;
|
|
418
|
+
validMRZConsecutiveCount.current = 0;
|
|
419
|
+
lastValidMRZText.current = null;
|
|
420
|
+
lastValidMRZFields.current = null;
|
|
421
|
+
validMRZConsecutiveCount.current = 0;
|
|
422
|
+
cachedBarcode.current = null; // Clear cached barcode on new scan
|
|
423
|
+
lastVoiceGuidanceMessage.current = '';
|
|
424
|
+
resetLastMessage();
|
|
250
425
|
}
|
|
251
426
|
|
|
252
427
|
return () => {
|
|
253
428
|
setIsActive(false);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// Clear face images array
|
|
262
|
-
faceImages = [];
|
|
429
|
+
faceImages.current = [];
|
|
430
|
+
hologramImageCountRef.current = 0;
|
|
431
|
+
setHologramImageCount(0);
|
|
432
|
+
setLatestHologramFaceImage(undefined);
|
|
433
|
+
lastVoiceGuidanceMessage.current = '';
|
|
434
|
+
resetLastMessage();
|
|
263
435
|
};
|
|
264
|
-
}, [
|
|
265
|
-
device,
|
|
266
|
-
format,
|
|
267
|
-
isFocused,
|
|
268
|
-
hologramDetectionCurrentRetryCount,
|
|
269
|
-
secondaryFaceDetectionCurrentRetryCount,
|
|
270
|
-
mrzDetectionCurrentRetryCount,
|
|
271
|
-
faceDetectionErrorCount,
|
|
272
|
-
]);
|
|
436
|
+
}, [isFocused, hasPermission, hasGuideShown]);
|
|
273
437
|
|
|
274
438
|
useEffect(() => {
|
|
275
439
|
if (hasGuideShown) {
|
|
440
|
+
// Generate message - match UI display logic exactly for consistency
|
|
276
441
|
let message = '';
|
|
277
442
|
|
|
278
|
-
// Priority: scanned > incorrect > blur during scanning > brightness > blur > step-specific
|
|
279
443
|
if (status === 'SCANNED') {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
message = t('identityDocumentCamera.scanCompleted');
|
|
291
|
-
}
|
|
444
|
+
message =
|
|
445
|
+
completedStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
446
|
+
? detectedDocumentType === 'PASSPORT'
|
|
447
|
+
? t('identityDocumentCamera.passportScanned')
|
|
448
|
+
: t('identityDocumentCamera.frontSideScanned')
|
|
449
|
+
: completedStep === 'SCAN_ID_BACK'
|
|
450
|
+
? t('identityDocumentCamera.backSideScanned')
|
|
451
|
+
: completedStep === 'SCAN_HOLOGRAM'
|
|
452
|
+
? t('identityDocumentCamera.hologramVerified')
|
|
453
|
+
: t('identityDocumentCamera.scanCompleted');
|
|
292
454
|
} else if (status === 'INCORRECT') {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
455
|
+
message =
|
|
456
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
457
|
+
? t('identityDocumentCamera.wrongSideFront')
|
|
458
|
+
: nextStep === 'SCAN_ID_BACK'
|
|
459
|
+
? t('identityDocumentCamera.wrongSideBack')
|
|
460
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
461
|
+
? t('identityDocumentCamera.wrongSideFront')
|
|
462
|
+
: t('identityDocumentCamera.alignPhotoSide');
|
|
299
463
|
} else if (isBrightnessLow) {
|
|
300
|
-
// Brightness warning takes priority over blur
|
|
301
464
|
message = t('identityDocumentCamera.lowBrightness');
|
|
302
465
|
} else if (isFrameBlurry) {
|
|
303
|
-
// Show blur warning only when brightness is sufficient
|
|
304
466
|
message = t('identityDocumentCamera.avoidBlur');
|
|
305
|
-
} else if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
467
|
+
} else if (
|
|
468
|
+
status === 'SCANNING' &&
|
|
469
|
+
allElementsDetected &&
|
|
470
|
+
elementsOutsideScanArea.length === 0
|
|
471
|
+
) {
|
|
472
|
+
message =
|
|
473
|
+
nextStep === 'SCAN_ID_BACK'
|
|
474
|
+
? t('identityDocumentCamera.idCardBackDetected')
|
|
475
|
+
: detectedDocumentType === 'PASSPORT'
|
|
476
|
+
? t('identityDocumentCamera.passportDetected')
|
|
477
|
+
: detectedDocumentType === 'ID_FRONT'
|
|
478
|
+
? t('identityDocumentCamera.idCardFrontDetected')
|
|
479
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
480
|
+
? t('identityDocumentCamera.alignHologram')
|
|
481
|
+
: t('identityDocumentCamera.readingDocument');
|
|
482
|
+
} else if (elementsOutsideScanArea.length > 0) {
|
|
483
|
+
message = t('identityDocumentCamera.centerDocument');
|
|
484
|
+
} else if (
|
|
485
|
+
(status === 'SCANNING' || status === 'SEARCHING') &&
|
|
486
|
+
!allElementsDetected
|
|
487
|
+
) {
|
|
488
|
+
message =
|
|
489
|
+
nextStep === 'SCAN_ID_BACK'
|
|
490
|
+
? t('identityDocumentCamera.alignIDBack')
|
|
491
|
+
: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
492
|
+
? detectedDocumentType === 'PASSPORT'
|
|
493
|
+
? t('identityDocumentCamera.alignPassport')
|
|
494
|
+
: detectedDocumentType === 'ID_FRONT'
|
|
495
|
+
? t('identityDocumentCamera.alignIDFront')
|
|
496
|
+
: t('identityDocumentCamera.alignPhotoSide')
|
|
497
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
498
|
+
? t('identityDocumentCamera.alignHologram')
|
|
499
|
+
: t('identityDocumentCamera.readingDocument');
|
|
500
|
+
} else {
|
|
501
|
+
message =
|
|
502
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
503
|
+
? status === 'SCANNING'
|
|
504
|
+
? t('identityDocumentCamera.readingDocument')
|
|
505
|
+
: t('identityDocumentCamera.alignPhotoSide')
|
|
506
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
507
|
+
? t('identityDocumentCamera.alignHologram')
|
|
508
|
+
: nextStep === 'SCAN_ID_BACK'
|
|
509
|
+
? status === 'SCANNING'
|
|
510
|
+
? t('identityDocumentCamera.readingDocument')
|
|
511
|
+
: t('identityDocumentCamera.alignIDBackSide')
|
|
512
|
+
: nextStep === 'COMPLETED'
|
|
513
|
+
? t('identityDocumentCamera.scanCompleted')
|
|
514
|
+
: '';
|
|
333
515
|
}
|
|
334
516
|
|
|
335
517
|
if (
|
|
336
518
|
appContext.currentWorkflowStep?.data?.voiceGuidanceActive &&
|
|
337
|
-
message
|
|
519
|
+
message &&
|
|
520
|
+
message !== lastVoiceGuidanceMessage.current
|
|
338
521
|
) {
|
|
339
|
-
|
|
522
|
+
lastVoiceGuidanceMessage.current = message;
|
|
523
|
+
speak(message, true);
|
|
340
524
|
}
|
|
341
525
|
}
|
|
342
526
|
}, [
|
|
@@ -349,402 +533,287 @@ const IdentityDocumentCamera = ({
|
|
|
349
533
|
completedStep,
|
|
350
534
|
currentFaceImage,
|
|
351
535
|
detectedDocumentType,
|
|
536
|
+
allElementsDetected,
|
|
537
|
+
elementsOutsideScanArea,
|
|
352
538
|
t,
|
|
353
539
|
]);
|
|
354
540
|
|
|
355
|
-
// Auto-reset INCORRECT status after showing warning briefly
|
|
356
541
|
useEffect(() => {
|
|
357
542
|
if (status === 'INCORRECT') {
|
|
358
543
|
const timeout = setTimeout(() => {
|
|
359
544
|
setStatus('SEARCHING');
|
|
360
|
-
}, 1500);
|
|
545
|
+
}, 1500);
|
|
361
546
|
return () => clearTimeout(timeout);
|
|
362
547
|
}
|
|
363
548
|
}, [status]);
|
|
364
549
|
|
|
365
|
-
//
|
|
550
|
+
// Disable face detection when scanning back side (no face expected, avoids false positives)
|
|
366
551
|
useEffect(() => {
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Only autofocus during searching and scanning states
|
|
372
|
-
if (status !== 'SEARCHING' && status !== 'SCANNING') {
|
|
373
|
-
return;
|
|
552
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
553
|
+
setFaceDetectionEnabled(false);
|
|
554
|
+
} else {
|
|
555
|
+
setFaceDetectionEnabled(true);
|
|
374
556
|
}
|
|
557
|
+
}, [nextStep]);
|
|
375
558
|
|
|
376
|
-
|
|
559
|
+
// Native OpenCV: detect hologram from sequence of face images
|
|
560
|
+
const detectHologramNative = useCallback(
|
|
561
|
+
async (images: string[]): Promise<[string, string] | []> => {
|
|
377
562
|
try {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
563
|
+
if (isDebugEnabled()) {
|
|
564
|
+
debugLog(
|
|
565
|
+
'IdentityDocumentCamera',
|
|
566
|
+
`[Hologram] Detecting hologram from ${images.length} images`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
// Limit images to prevent memory issues
|
|
570
|
+
const limitedImages = images.slice(0, HOLOGRAM_IMAGE_COUNT);
|
|
571
|
+
const result = await OpenCVModule.detectHologram(
|
|
572
|
+
limitedImages,
|
|
573
|
+
HOLOGRAM_DETECTION_THRESHOLD
|
|
574
|
+
);
|
|
575
|
+
if (result) {
|
|
576
|
+
return [result.hologramMask, result.hologramImage];
|
|
577
|
+
}
|
|
390
578
|
} catch (error) {
|
|
391
|
-
|
|
579
|
+
logError('[Hologram] Detection error:', error);
|
|
392
580
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const detectDocumentType = (
|
|
399
|
-
faces: Face[],
|
|
400
|
-
ocrText: string,
|
|
401
|
-
mrzFields?: FieldRecords
|
|
402
|
-
) => {
|
|
403
|
-
if (
|
|
404
|
-
faces.length > 0 &&
|
|
405
|
-
!mrzFields &&
|
|
406
|
-
ocrText?.includes('Signature')
|
|
407
|
-
// ocrText?.includes('Surname') &&
|
|
408
|
-
// ocrText?.includes('Given Name(s)') &&
|
|
409
|
-
// ocrText?.includes('Date of Birth') &&
|
|
410
|
-
// ocrText?.includes('Document No') &&
|
|
411
|
-
// ocrText?.includes('Valid Until')
|
|
412
|
-
) {
|
|
413
|
-
return 'ID_FRONT';
|
|
414
|
-
} else if (
|
|
415
|
-
faces.length === 0 &&
|
|
416
|
-
mrzFields?.documentCode === 'I'
|
|
417
|
-
// ocrText?.includes("Father's Name") &&
|
|
418
|
-
// ocrText?.includes("Mother's Name") &&
|
|
419
|
-
// ocrText?.includes('Issued By')
|
|
420
|
-
) {
|
|
421
|
-
return 'ID_BACK';
|
|
422
|
-
} else if (faces.length > 0 && mrzFields?.documentCode === 'P') {
|
|
423
|
-
return 'PASSPORT';
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return 'UNKNOWN';
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
// const setBoundingBoxInJS = useRunOnJS(
|
|
430
|
-
// (bounds: Bounds) => {
|
|
431
|
-
// setBoundingBox(bounds);
|
|
432
|
-
// },
|
|
433
|
-
// [setBoundingBox],
|
|
434
|
-
// );
|
|
435
|
-
|
|
436
|
-
// const isBlockInFrame = (block: BlocksData) => {
|
|
437
|
-
// 'worklet';
|
|
438
|
-
// const scanningFrame = {
|
|
439
|
-
// x: 0.03 * 1080,
|
|
440
|
-
// y: 0.35 * 1920,
|
|
441
|
-
// width: 0.94 * 1080,
|
|
442
|
-
// height: 0.3 * 1920,
|
|
443
|
-
// } as Bounds;
|
|
444
|
-
// const bounds = {
|
|
445
|
-
// x: block.blockFrame.x,
|
|
446
|
-
// y: block.blockFrame.y,
|
|
447
|
-
// width: block.blockFrame.width,
|
|
448
|
-
// height: block.blockFrame.height,
|
|
449
|
-
// } as Bounds;
|
|
450
|
-
|
|
451
|
-
// if (
|
|
452
|
-
// bounds.x >= scanningFrame.x &&
|
|
453
|
-
// bounds.y >= scanningFrame.y &&
|
|
454
|
-
// bounds.x + bounds.width <= scanningFrame.x + scanningFrame.width &&
|
|
455
|
-
// bounds.y + bounds.height <= scanningFrame.y + scanningFrame.height
|
|
456
|
-
// ) {
|
|
457
|
-
// return true;
|
|
458
|
-
// }
|
|
459
|
-
|
|
460
|
-
// setBoundingBoxInJS({
|
|
461
|
-
// x: (bounds.x / 1080) * windowWidth,
|
|
462
|
-
// y: (bounds.y / 1920) * windowHeight,
|
|
463
|
-
// width: (bounds.width / 1080) * windowWidth,
|
|
464
|
-
// height: (bounds.height / 1920) * windowHeight,
|
|
465
|
-
// });
|
|
466
|
-
|
|
467
|
-
// return false;
|
|
468
|
-
// };
|
|
469
|
-
|
|
470
|
-
const applyThreshold = (image: Mat) => {
|
|
471
|
-
const gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
472
|
-
|
|
473
|
-
// Convert to grayscale
|
|
474
|
-
OpenCV.invoke('cvtColor', image, gray, ColorConversionCodes.COLOR_RGB2GRAY);
|
|
475
|
-
|
|
476
|
-
// Apply GaussianBlur to reduce noise
|
|
477
|
-
const kSize = OpenCV.createObject(ObjectType.Size, 5, 5);
|
|
478
|
-
OpenCV.invoke('GaussianBlur', gray, gray, kSize, 0);
|
|
479
|
-
|
|
480
|
-
// Apply Otsu's thresholding
|
|
481
|
-
OpenCV.invoke(
|
|
482
|
-
'threshold',
|
|
483
|
-
gray,
|
|
484
|
-
gray,
|
|
485
|
-
0,
|
|
486
|
-
255,
|
|
487
|
-
ThresholdTypes.THRESH_BINARY + ThresholdTypes.THRESH_OTSU
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
return gray;
|
|
491
|
-
};
|
|
581
|
+
return [];
|
|
582
|
+
},
|
|
583
|
+
[]
|
|
584
|
+
);
|
|
492
585
|
|
|
493
|
-
|
|
586
|
+
// Native OpenCV: compare two images for similarity
|
|
587
|
+
const areImagesSimilarNative = async (
|
|
494
588
|
image1: string,
|
|
495
589
|
image2: string,
|
|
496
|
-
threshold =
|
|
497
|
-
) => {
|
|
590
|
+
threshold = 20000 // Relaxed threshold for better tolerance of lighting/angle variations
|
|
591
|
+
): Promise<boolean> => {
|
|
498
592
|
try {
|
|
499
|
-
if (!image1 || !image2)
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
const mat1 = OpenCV.base64ToMat(image1);
|
|
503
|
-
const mat2 = OpenCV.base64ToMat(image2);
|
|
504
|
-
const diff = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
505
|
-
OpenCV.invoke(
|
|
506
|
-
'absdiff',
|
|
507
|
-
applyThreshold(mat1),
|
|
508
|
-
applyThreshold(mat2),
|
|
509
|
-
diff
|
|
510
|
-
);
|
|
511
|
-
const count = OpenCV.invoke('countNonZero', diff);
|
|
512
|
-
|
|
513
|
-
return count.value < threshold;
|
|
593
|
+
if (!image1 || !image2) return false;
|
|
594
|
+
return await OpenCVModule.areImagesSimilar(image1, image2, threshold);
|
|
514
595
|
} catch (error) {
|
|
515
|
-
// console.log('Error while comparing images:', error);
|
|
516
596
|
return false;
|
|
517
597
|
}
|
|
518
598
|
};
|
|
519
599
|
|
|
520
|
-
|
|
521
|
-
const detectHologram = (images: string[]) => {
|
|
522
|
-
try {
|
|
523
|
-
const lowerBound = OpenCV.createObject(ObjectType.Scalar, 40, 90, 90);
|
|
524
|
-
const upperBound = OpenCV.createObject(ObjectType.Scalar, 179, 255, 255);
|
|
525
|
-
const diffs = [];
|
|
526
|
-
const hologram = OpenCV.base64ToMat(images[0]);
|
|
527
|
-
for (let i = 0; i < images.length - 1; i++) {
|
|
528
|
-
const mat1 = OpenCV.base64ToMat(images[i]);
|
|
529
|
-
const mat2 = OpenCV.base64ToMat(images[i + 1]);
|
|
530
|
-
let diff = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
531
|
-
OpenCV.invoke('absdiff', mat1, mat2, diff);
|
|
532
|
-
OpenCV.invoke(
|
|
533
|
-
'cvtColor',
|
|
534
|
-
diff,
|
|
535
|
-
diff,
|
|
536
|
-
ColorConversionCodes.COLOR_RGB2HSV
|
|
537
|
-
);
|
|
538
|
-
OpenCV.invoke('inRange', diff, lowerBound, upperBound, diff);
|
|
539
|
-
if (OpenCV.invoke('countNonZero', diff).value > 500) {
|
|
540
|
-
OpenCV.invoke('addWeighted', hologram, 0.5, mat2, 0.5, 0, hologram);
|
|
541
|
-
diffs.push(diff);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const hologramMask = diffs[0];
|
|
546
|
-
for (let i = 1; i < diffs.length; i++) {
|
|
547
|
-
OpenCV.invoke(
|
|
548
|
-
'addWeighted',
|
|
549
|
-
hologramMask,
|
|
550
|
-
0.5,
|
|
551
|
-
diffs[i],
|
|
552
|
-
0.5,
|
|
553
|
-
0,
|
|
554
|
-
hologramMask
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
OpenCV.invoke(
|
|
558
|
-
'adaptiveThreshold',
|
|
559
|
-
hologramMask,
|
|
560
|
-
hologramMask,
|
|
561
|
-
255,
|
|
562
|
-
AdaptiveThresholdTypes.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
563
|
-
ThresholdTypes.THRESH_BINARY_INV,
|
|
564
|
-
21,
|
|
565
|
-
2
|
|
566
|
-
);
|
|
567
|
-
const count = OpenCV.invoke('countNonZero', hologramMask);
|
|
568
|
-
if (count.value > HOLOGRAM_DETECTION_THRESHOLD) {
|
|
569
|
-
const hologramMaskJs = OpenCV.toJSValue(hologramMask);
|
|
570
|
-
const hologramJs = OpenCV.toJSValue(hologram);
|
|
571
|
-
return [hologramMaskJs.base64, hologramJs.base64];
|
|
572
|
-
}
|
|
573
|
-
} catch (error) {
|
|
574
|
-
// console.log('Error while detecting hologram:', error);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return [];
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
const getFaceImagesOrderedByLocation = (faces: Face[]) => {
|
|
581
|
-
return faces.sort((a, b) => {
|
|
582
|
-
if (a.bounds.x < b.bounds.x) {
|
|
583
|
-
return -1;
|
|
584
|
-
} else if (a.bounds.x > b.bounds.x) {
|
|
585
|
-
return 1;
|
|
586
|
-
}
|
|
587
|
-
return 0;
|
|
588
|
-
});
|
|
589
|
-
};
|
|
590
|
-
|
|
600
|
+
// Native OpenCV: crop face images from full frame
|
|
591
601
|
const getFaceImages = async (
|
|
592
|
-
|
|
602
|
+
facesToDetect: Face[],
|
|
593
603
|
image: string,
|
|
594
604
|
width: number,
|
|
595
605
|
height: number
|
|
596
|
-
) => {
|
|
597
|
-
if (!
|
|
606
|
+
): Promise<string[]> => {
|
|
607
|
+
if (!facesToDetect.length || !image || width <= 0 || height <= 0) {
|
|
598
608
|
return [];
|
|
599
609
|
}
|
|
600
|
-
|
|
601
|
-
const croppedFaces = [];
|
|
602
|
-
|
|
603
610
|
try {
|
|
604
|
-
|
|
605
|
-
|
|
611
|
+
const faceBounds = facesToDetect.map((f) => ({
|
|
612
|
+
x: f.bounds.x,
|
|
613
|
+
y: f.bounds.y,
|
|
614
|
+
width: f.bounds.width,
|
|
615
|
+
height: f.bounds.height,
|
|
616
|
+
}));
|
|
617
|
+
const croppedFaces: string[] = await OpenCVModule.cropFaceImages(
|
|
618
|
+
image,
|
|
619
|
+
faceBounds,
|
|
620
|
+
width,
|
|
621
|
+
height
|
|
622
|
+
);
|
|
623
|
+
return croppedFaces ?? [];
|
|
624
|
+
} catch (error) {
|
|
625
|
+
logError('[getFaceImages] Native face crop failed:', error);
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
};
|
|
606
629
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
// );
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
630
|
+
const setNextStepAndVibrate = useCallback(
|
|
631
|
+
(
|
|
632
|
+
nextStepType:
|
|
633
|
+
| 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
634
|
+
| 'SCAN_ID_BACK'
|
|
635
|
+
| 'SCAN_HOLOGRAM'
|
|
636
|
+
| 'COMPLETED',
|
|
637
|
+
fromStep?: 'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM'
|
|
638
|
+
) => {
|
|
639
|
+
if (fromStep) {
|
|
640
|
+
setCompletedStep(fromStep);
|
|
641
|
+
}
|
|
622
642
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
y: offsetY,
|
|
636
|
-
},
|
|
637
|
-
size: {
|
|
638
|
-
width: expandedWidth,
|
|
639
|
-
height: expandedHeight,
|
|
640
|
-
},
|
|
641
|
-
displaySize: {
|
|
642
|
-
width: 240,
|
|
643
|
-
height: 320,
|
|
644
|
-
},
|
|
645
|
-
includeBase64: true,
|
|
646
|
-
quality: 1,
|
|
647
|
-
});
|
|
643
|
+
// Turn flash on and reset hologram counters when entering SCAN_HOLOGRAM
|
|
644
|
+
if (nextStepType === 'SCAN_HOLOGRAM' && fromStep !== 'SCAN_HOLOGRAM') {
|
|
645
|
+
setIsTorchOn(true);
|
|
646
|
+
// Reset hologram detection counters for fresh start
|
|
647
|
+
hologramDetectionCurrentRetryCount.current = 0;
|
|
648
|
+
secondaryFaceDetectionCurrentRetryCount.current = 0;
|
|
649
|
+
hologramFramesWithoutFace.current = 0;
|
|
650
|
+
faceImages.current = [];
|
|
651
|
+
hologramImageCountRef.current = 0;
|
|
652
|
+
setHologramImageCount(0);
|
|
653
|
+
setLatestHologramFaceImage(undefined);
|
|
654
|
+
}
|
|
648
655
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
ObjectType.Rect,
|
|
665
|
-
0,
|
|
666
|
-
0,
|
|
667
|
-
matCropWidth,
|
|
668
|
-
matCropHeight
|
|
669
|
-
)
|
|
670
|
-
);
|
|
671
|
-
croppedFaces.push(OpenCV.toJSValue(croppedFaceMat).base64);
|
|
672
|
-
} else {
|
|
673
|
-
// Fallback to original base64 if crop dimensions are invalid
|
|
674
|
-
croppedFaces.push(croppedFace.base64);
|
|
675
|
-
}
|
|
676
|
-
} catch (cropError) {
|
|
677
|
-
console.warn('OpenCV crop operation failed:', cropError);
|
|
678
|
-
// Fallback to original image if OpenCV crop fails
|
|
679
|
-
croppedFaces.push(croppedFace.base64);
|
|
680
|
-
}
|
|
681
|
-
} else {
|
|
682
|
-
croppedFaces.push(croppedFace.base64);
|
|
656
|
+
// Clean up flash and hologram state when leaving SCAN_HOLOGRAM step
|
|
657
|
+
if (fromStep === 'SCAN_HOLOGRAM' && nextStepType !== 'SCAN_HOLOGRAM') {
|
|
658
|
+
setIsTorchOn(false);
|
|
659
|
+
faceImages.current = [];
|
|
660
|
+
hologramImageCountRef.current = 0;
|
|
661
|
+
setHologramImageCount(0);
|
|
662
|
+
setLatestHologramFaceImage(undefined);
|
|
663
|
+
lastFacePosition.current = null; // Reset document plane reference
|
|
664
|
+
cachedBarcode.current = null; // Clear cached barcode
|
|
665
|
+
setDocumentPlaneBounds(null); // Clear visual overlay
|
|
666
|
+
setSecondaryFaceBounds(null); // Clear secondary face overlay
|
|
667
|
+
if (isDebugEnabled()) {
|
|
668
|
+
console.log(
|
|
669
|
+
'[Flash] Turning off flash and clearing hologram images when leaving step'
|
|
670
|
+
);
|
|
683
671
|
}
|
|
684
672
|
}
|
|
685
|
-
} catch (error) {
|
|
686
|
-
console.warn('Error while cropping face:', error);
|
|
687
|
-
}
|
|
688
673
|
|
|
689
|
-
|
|
690
|
-
|
|
674
|
+
setNextStep(nextStepType);
|
|
675
|
+
Vibration.vibrate(100);
|
|
691
676
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
| 'COMPLETED',
|
|
698
|
-
fromStep?:
|
|
699
|
-
| 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
700
|
-
| 'SCAN_ID_BACK'
|
|
701
|
-
| 'SCAN_HOLOGRAM'
|
|
702
|
-
) => {
|
|
703
|
-
// Track which step was just completed for showing specific message
|
|
704
|
-
if (fromStep) {
|
|
705
|
-
setCompletedStep(fromStep);
|
|
706
|
-
}
|
|
707
|
-
setNextStep(nextStepType);
|
|
708
|
-
Vibration.vibrate(100);
|
|
677
|
+
// Reset MRZ retry counter for each new step so retries start fresh
|
|
678
|
+
mrzDetectionCurrentRetryCount.current = 0;
|
|
679
|
+
lastValidMRZText.current = null;
|
|
680
|
+
validMRZConsecutiveCount.current = 0;
|
|
681
|
+
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
709
682
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
const handleBrightness = useRunOnJS(
|
|
720
|
-
(isBright: boolean) => {
|
|
721
|
-
setIsBrightnessLow(!isBright);
|
|
722
|
-
},
|
|
723
|
-
[setIsBrightnessLow]
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
const handleBlurStatus = useRunOnJS(
|
|
727
|
-
(blurry: boolean) => {
|
|
728
|
-
setIsFrameBlurry(blurry);
|
|
683
|
+
if (nextStepType !== 'COMPLETED') {
|
|
684
|
+
setTimeout(() => {
|
|
685
|
+
setStatus('SEARCHING');
|
|
686
|
+
setCompletedStep(null);
|
|
687
|
+
}, 1000);
|
|
688
|
+
}
|
|
729
689
|
},
|
|
730
|
-
[
|
|
690
|
+
[setIsTorchOn]
|
|
731
691
|
);
|
|
732
692
|
|
|
733
|
-
const handleFaceAndText =
|
|
693
|
+
const handleFaceAndText = useCallback(
|
|
734
694
|
async (
|
|
735
695
|
text: string,
|
|
736
696
|
faces: Face[],
|
|
737
697
|
frameWidth: number,
|
|
738
698
|
frameHeight: number,
|
|
739
699
|
barcode?: Barcode,
|
|
740
|
-
image?: string
|
|
700
|
+
image?: string,
|
|
701
|
+
elementsOutside?: boolean,
|
|
702
|
+
nativeMrzResult?: Frame['mrzResult']
|
|
741
703
|
) => {
|
|
704
|
+
const detectDocumentType = (
|
|
705
|
+
facesParam: Face[],
|
|
706
|
+
ocrText: string,
|
|
707
|
+
mrzFields?: MRZFields,
|
|
708
|
+
frameWidthParam?: number,
|
|
709
|
+
mrzTextParam?: string | null
|
|
710
|
+
) => {
|
|
711
|
+
// Relaxed signature detection: matches signature/imza variants and OCR errors
|
|
712
|
+
const hasSignatureMatch = /s[gi]g?n[au]?t[u]?r|imz[a]s?/i.test(ocrText);
|
|
713
|
+
|
|
714
|
+
if (isDebugEnabled()) {
|
|
715
|
+
console.log(
|
|
716
|
+
'[DocType] faces:',
|
|
717
|
+
facesParam.length,
|
|
718
|
+
'mrzFields:',
|
|
719
|
+
!!mrzFields,
|
|
720
|
+
'mrzText:',
|
|
721
|
+
!!mrzTextParam,
|
|
722
|
+
'textLen:',
|
|
723
|
+
ocrText?.length,
|
|
724
|
+
'hasSignature:',
|
|
725
|
+
hasSignatureMatch
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ID Back: no face + ID MRZ
|
|
730
|
+
if (facesParam.length === 0 && mrzFields?.documentCode === 'I') {
|
|
731
|
+
return 'ID_BACK';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Passport: face + passport MRZ
|
|
735
|
+
if (facesParam.length > 0 && mrzFields?.documentCode === 'P') {
|
|
736
|
+
return 'PASSPORT';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ID Front: face detected with signature text
|
|
740
|
+
if (facesParam.length > 0 && ocrText?.length >= 5) {
|
|
741
|
+
const hasSignature = hasSignatureMatch;
|
|
742
|
+
// Only turn off torch during initial scan step, not during SCAN_HOLOGRAM
|
|
743
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
744
|
+
setIsTorchOn(false);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Filter to card-sized faces only (min 5% of frame width to exclude tiny background faces)
|
|
748
|
+
const cardSizedFaces = frameWidthParam
|
|
749
|
+
? facesParam.filter(
|
|
750
|
+
(face) =>
|
|
751
|
+
face.bounds.width >= frameWidthParam * 0.05 &&
|
|
752
|
+
face.bounds.height >= frameWidthParam * 0.05
|
|
753
|
+
)
|
|
754
|
+
: facesParam;
|
|
755
|
+
|
|
756
|
+
// CRITICAL: If passport MRZ pattern is detected but not parsed yet,
|
|
757
|
+
// return UNKNOWN instead of ID_FRONT to avoid misclassifying passports
|
|
758
|
+
// Passports always have MRZ visible on front starting with P<TUR or similar
|
|
759
|
+
if (
|
|
760
|
+
cardSizedFaces.length > 0 &&
|
|
761
|
+
!mrzFields?.documentCode &&
|
|
762
|
+
hasSignature
|
|
763
|
+
) {
|
|
764
|
+
if (
|
|
765
|
+
mrzTextParam &&
|
|
766
|
+
mrzTextParam.length > 20 &&
|
|
767
|
+
/P<[A-Z]{3}/.test(mrzTextParam)
|
|
768
|
+
) {
|
|
769
|
+
// Passport MRZ pattern detected (P<TUR, P<USA, etc.) but not parsed
|
|
770
|
+
// Could be passport with OCR errors - wait for proper parsing
|
|
771
|
+
if (isDebugEnabled()) {
|
|
772
|
+
console.log(
|
|
773
|
+
'[DocType] Passport MRZ pattern (P<XXX) detected but not parsed - returning UNKNOWN to wait for proper classification'
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return 'UNKNOWN';
|
|
777
|
+
}
|
|
778
|
+
return 'ID_FRONT';
|
|
779
|
+
}
|
|
780
|
+
// Also ensure flash is off when scan is completed
|
|
781
|
+
if (nextStep === 'COMPLETED' && isTorchOn) {
|
|
782
|
+
setIsTorchOn(false);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return 'UNKNOWN';
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// Filter to card-sized faces only (min 5% of frame width to exclude tiny background faces)
|
|
790
|
+
const cardSizedFaces = faces.filter(
|
|
791
|
+
(face) =>
|
|
792
|
+
face.bounds.width >= frameWidth * 0.05 &&
|
|
793
|
+
face.bounds.height >= frameWidth * 0.05
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
// Cache barcode when detected, use cached value if current frame has no barcode
|
|
797
|
+
// This handles inconsistent barcode detection across frames
|
|
798
|
+
if (barcode?.rawValue && nextStep === 'SCAN_ID_BACK') {
|
|
799
|
+
cachedBarcode.current = barcode;
|
|
800
|
+
}
|
|
801
|
+
const barcodeToUse = barcode || cachedBarcode.current;
|
|
802
|
+
|
|
803
|
+
// Store frame dimensions for coordinate conversion
|
|
742
804
|
if (
|
|
743
|
-
|
|
744
|
-
|
|
805
|
+
frameDimensions?.width !== frameWidth ||
|
|
806
|
+
frameDimensions.height !== frameHeight
|
|
807
|
+
) {
|
|
808
|
+
setFrameDimensions({ width: frameWidth, height: frameHeight });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (
|
|
812
|
+
nextStep !== 'SCAN_HOLOGRAM' &&
|
|
813
|
+
isTorchOnRef.current &&
|
|
745
814
|
(currentHologramImage ||
|
|
746
|
-
hologramDetectionCurrentRetryCount.
|
|
747
|
-
|
|
815
|
+
hologramDetectionCurrentRetryCount.current >=
|
|
816
|
+
HOLOGRAM_DETECTION_RETRY_COUNT)
|
|
748
817
|
) {
|
|
749
818
|
setIsTorchOn(false);
|
|
750
819
|
}
|
|
@@ -754,30 +823,450 @@ const IdentityDocumentCamera = ({
|
|
|
754
823
|
return;
|
|
755
824
|
}
|
|
756
825
|
|
|
757
|
-
|
|
758
|
-
|
|
826
|
+
if (elementsOutside) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (nextStep === 'SCAN_ID_BACK' && cardSizedFaces.length > 0) {
|
|
759
831
|
setStatus('INCORRECT');
|
|
760
832
|
return;
|
|
761
833
|
}
|
|
762
834
|
|
|
763
|
-
|
|
835
|
+
// Only crop and lock face when ID_FRONT or PASSPORT is confirmed
|
|
836
|
+
const shouldCropFaces =
|
|
837
|
+
detectedDocumentType === 'ID_FRONT' ||
|
|
838
|
+
detectedDocumentType === 'PASSPORT' ||
|
|
839
|
+
nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
840
|
+
const croppedFaces = shouldCropFaces
|
|
841
|
+
? await getFaceImages(
|
|
842
|
+
cardSizedFaces,
|
|
843
|
+
image ?? '',
|
|
844
|
+
frameWidth,
|
|
845
|
+
frameHeight
|
|
846
|
+
)
|
|
847
|
+
: [];
|
|
848
|
+
|
|
849
|
+
// Validate document plane consistency across all captures
|
|
850
|
+
let facePositionValid = true;
|
|
851
|
+
if (cardSizedFaces.length > 0 && cardSizedFaces[0]) {
|
|
852
|
+
const currentFaceBounds = cardSizedFaces[0].bounds;
|
|
853
|
+
if (lastFacePosition.current) {
|
|
854
|
+
// Check if face position is within acceptable range
|
|
855
|
+
// Use looser tolerance during hologram step since flash toggling causes position jitter
|
|
856
|
+
const xDiff = Math.abs(
|
|
857
|
+
currentFaceBounds.x - lastFacePosition.current.x
|
|
858
|
+
);
|
|
859
|
+
const yDiff = Math.abs(
|
|
860
|
+
currentFaceBounds.y - lastFacePosition.current.y
|
|
861
|
+
);
|
|
862
|
+
const widthDiff = Math.abs(
|
|
863
|
+
currentFaceBounds.width - lastFacePosition.current.width
|
|
864
|
+
);
|
|
865
|
+
const heightDiff = Math.abs(
|
|
866
|
+
currentFaceBounds.height - lastFacePosition.current.height
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const tolerance = nextStep === 'SCAN_HOLOGRAM' ? 0.5 : 0.2;
|
|
870
|
+
const xTolerance = lastFacePosition.current.width * tolerance;
|
|
871
|
+
const yTolerance = lastFacePosition.current.height * tolerance;
|
|
872
|
+
const sizeTolerance = lastFacePosition.current.width * tolerance;
|
|
873
|
+
|
|
874
|
+
facePositionValid =
|
|
875
|
+
xDiff <= xTolerance &&
|
|
876
|
+
yDiff <= yTolerance &&
|
|
877
|
+
widthDiff <= sizeTolerance &&
|
|
878
|
+
heightDiff <= sizeTolerance;
|
|
879
|
+
|
|
880
|
+
if (!facePositionValid) {
|
|
881
|
+
if (isDebugEnabled()) {
|
|
882
|
+
console.log(
|
|
883
|
+
`[DocPlane] Face position changed - document moved (xDiff=${xDiff.toFixed(1)}, yDiff=${yDiff.toFixed(1)}) - rejecting frame`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Update reference position to follow gradual movement (sliding window)
|
|
889
|
+
lastFacePosition.current = {
|
|
890
|
+
x: currentFaceBounds.x,
|
|
891
|
+
y: currentFaceBounds.y,
|
|
892
|
+
width: currentFaceBounds.width,
|
|
893
|
+
height: currentFaceBounds.height,
|
|
894
|
+
};
|
|
895
|
+
} else {
|
|
896
|
+
// First capture - store reference position
|
|
897
|
+
lastFacePosition.current = {
|
|
898
|
+
x: currentFaceBounds.x,
|
|
899
|
+
y: currentFaceBounds.y,
|
|
900
|
+
width: currentFaceBounds.width,
|
|
901
|
+
height: currentFaceBounds.height,
|
|
902
|
+
};
|
|
903
|
+
console.log(
|
|
904
|
+
'[DocPlane] Stored reference face position for document plane validation'
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Update visual bounds for debug overlay
|
|
909
|
+
// Transform face bounds from image coordinates to screen coordinates
|
|
910
|
+
if (facePositionValid && frameDimensions) {
|
|
911
|
+
const screen = Dimensions.get('window');
|
|
912
|
+
|
|
913
|
+
// Camera uses FILL_CENTER: scale to fill screen while maintaining aspect ratio
|
|
914
|
+
const frameAspect = frameDimensions.width / frameDimensions.height;
|
|
915
|
+
const screenAspect = screen.width / screen.height;
|
|
916
|
+
|
|
917
|
+
let scale: number;
|
|
918
|
+
let offsetX = 0;
|
|
919
|
+
let offsetY = 0;
|
|
920
|
+
|
|
921
|
+
if (frameAspect > screenAspect) {
|
|
922
|
+
// Frame is wider - scale by height, crop width
|
|
923
|
+
scale = screen.height / frameDimensions.height;
|
|
924
|
+
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
925
|
+
} else {
|
|
926
|
+
// Frame is taller - scale by width, crop height
|
|
927
|
+
scale = screen.width / frameDimensions.width;
|
|
928
|
+
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const cropPadding = Math.max(
|
|
932
|
+
currentFaceBounds.width * 0.15,
|
|
933
|
+
currentFaceBounds.height * 0.15
|
|
934
|
+
);
|
|
935
|
+
setDocumentPlaneBounds({
|
|
936
|
+
x: currentFaceBounds.x * scale - offsetX,
|
|
937
|
+
y: currentFaceBounds.y * scale - offsetY,
|
|
938
|
+
width: currentFaceBounds.width * scale,
|
|
939
|
+
height: currentFaceBounds.height * scale,
|
|
940
|
+
cropPadding: cropPadding * scale,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Capture and persist face only after document type is confirmed
|
|
946
|
+
// This prevents locking a face before we know what document we're scanning
|
|
947
|
+
let faceImageToUse = currentFaceImage;
|
|
948
|
+
if (
|
|
949
|
+
shouldCropFaces &&
|
|
950
|
+
croppedFaces.length > 0 &&
|
|
951
|
+
croppedFaces[0] &&
|
|
952
|
+
facePositionValid
|
|
953
|
+
) {
|
|
954
|
+
if (!currentFaceImage) {
|
|
955
|
+
// First face detection after doc type confirmed - lock it for all subsequent steps
|
|
956
|
+
faceImageToUse = croppedFaces[0];
|
|
957
|
+
setCurrentFaceImage(croppedFaces[0]);
|
|
958
|
+
if (isDebugEnabled()) {
|
|
959
|
+
console.log(
|
|
960
|
+
'[DocPlane] Locked primary face from validated document plane (docType: ' +
|
|
961
|
+
detectedDocumentType +
|
|
962
|
+
')'
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (!text || text.length < 5 || !image) {
|
|
764
969
|
setStatus('SEARCHING');
|
|
765
970
|
return;
|
|
766
971
|
}
|
|
767
972
|
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
973
|
+
const parsedMRZData =
|
|
974
|
+
nativeMrzResult?.valid && nativeMrzResult.documentCode
|
|
975
|
+
? { valid: true, fields: nativeMrzResult as MRZFields }
|
|
976
|
+
: nativeMrzResult?.documentCode
|
|
977
|
+
? { valid: false, fields: nativeMrzResult as MRZFields }
|
|
978
|
+
: { valid: false, fields: null };
|
|
979
|
+
const mrzText = parsedMRZData.valid ? nativeMrzResult?.rawLines : null;
|
|
980
|
+
|
|
981
|
+
// MRZ stability check - require consistent valid reads to avoid OCR noise
|
|
982
|
+
// Compare parsed field values instead of raw text to handle OCR variations in filler characters
|
|
983
|
+
// Only proceed with MRZ if it's actually valid and has all required fields
|
|
984
|
+
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
985
|
+
|
|
986
|
+
if (
|
|
987
|
+
mrzText &&
|
|
988
|
+
parsedMRZData?.valid === true &&
|
|
989
|
+
parsedMRZData?.fields &&
|
|
990
|
+
mrzHasRequiredFields
|
|
991
|
+
) {
|
|
992
|
+
const currentFields = parsedMRZData.fields;
|
|
993
|
+
|
|
994
|
+
if (areMRZFieldsEqual(lastValidMRZFields.current, currentFields)) {
|
|
995
|
+
// Same MRZ data detected again - increment counter
|
|
996
|
+
validMRZConsecutiveCount.current++;
|
|
997
|
+
} else {
|
|
998
|
+
// Different MRZ data - reset counter and store new data
|
|
999
|
+
if (isDebugEnabled()) {
|
|
1000
|
+
console.log(
|
|
1001
|
+
`[MRZ Stability] New MRZ detected, resetting counter (was ${validMRZConsecutiveCount.current}, doc: ${currentFields.documentNumber?.slice(-4)})`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
lastValidMRZFields.current = currentFields;
|
|
1005
|
+
lastValidMRZText.current = mrzText;
|
|
1006
|
+
validMRZConsecutiveCount.current = 1;
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
// Invalid or no MRZ - don't reset completely, just skip this frame
|
|
1010
|
+
// This allows temporary OCR noise without losing progress
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Check if we have enough consistent valid reads
|
|
1014
|
+
const mrzStableAndValid =
|
|
1015
|
+
validMRZConsecutiveCount.current >= REQUIRED_CONSISTENT_MRZ_READS &&
|
|
1016
|
+
parsedMRZData?.valid === true &&
|
|
1017
|
+
areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
|
|
1018
|
+
|
|
1019
|
+
// During SCAN_ID_BACK, handle MRZ/barcode directly without full document type detection
|
|
1020
|
+
// This avoids the chicken-and-egg problem where detectDocumentType requires
|
|
1021
|
+
// mrzFields.documentCode === 'I' but MRZ parsing may return different codes
|
|
1022
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
1023
|
+
// CRITICAL: Always check if wrong side is shown (front or passport when back is expected)
|
|
1024
|
+
// ID_BACK should have NO faces and NO signature text
|
|
1025
|
+
// Multiple indicators for robust detection:
|
|
1026
|
+
const hasFaces = cardSizedFaces.length > 0;
|
|
1027
|
+
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
1028
|
+
const hasPassportMRZ = parsedMRZData?.fields?.documentCode === 'P';
|
|
1029
|
+
const hasPassportMRZPattern = mrzText && /P<[A-Z]{3}/.test(mrzText);
|
|
1030
|
+
|
|
1031
|
+
if (
|
|
1032
|
+
hasFaces ||
|
|
1033
|
+
hasSignature ||
|
|
1034
|
+
hasPassportMRZ ||
|
|
1035
|
+
hasPassportMRZPattern
|
|
1036
|
+
) {
|
|
1037
|
+
if (isDebugEnabled()) {
|
|
1038
|
+
console.log(
|
|
1039
|
+
`[ID_BACK Scan] Wrong side detected - faces: ${hasFaces}, signature: ${hasSignature}, passport MRZ: ${hasPassportMRZ}, passport pattern: ${hasPassportMRZPattern}`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
setStatus('INCORRECT');
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// SAFETY CHECK: If we detect passport during ID_BACK scan via state, skip this step
|
|
1047
|
+
// This shouldn't happen but protects against edge cases
|
|
1048
|
+
if (detectedDocumentType === 'PASSPORT') {
|
|
1049
|
+
if (isDebugEnabled()) {
|
|
1050
|
+
console.log(
|
|
1051
|
+
'[ID_BACK Scan] ERROR: Passport detected in ID_BACK step - skipping to COMPLETED'
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
1055
|
+
setTimeout(() => {
|
|
1056
|
+
onIdentityDocumentScanned({
|
|
1057
|
+
image,
|
|
1058
|
+
documentType: 'PASSPORT',
|
|
1059
|
+
mrzText: mrzText ?? undefined,
|
|
1060
|
+
mrzFields: parsedMRZData?.fields,
|
|
1061
|
+
});
|
|
1062
|
+
}, 1000);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const hasMRZ = !!mrzText;
|
|
1067
|
+
const hasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
1068
|
+
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
1069
|
+
// AND ensure all required fields are present
|
|
1070
|
+
const mrzAccepted =
|
|
1071
|
+
parsedMRZData?.valid === true &&
|
|
1072
|
+
hasRequiredFields &&
|
|
1073
|
+
mrzStableAndValid;
|
|
1074
|
+
const barcodeMatchesMRZ =
|
|
1075
|
+
barcodeToUse?.rawValue?.trim() ===
|
|
1076
|
+
parsedMRZData?.fields?.optional1?.trim();
|
|
1077
|
+
// Require barcode for all documents (no special card fallback)
|
|
1078
|
+
const barcodeAccepted = onlyMRZScan || barcodeMatchesMRZ;
|
|
1079
|
+
|
|
1080
|
+
// CRITICAL: Require all document elements to be in frame before accepting
|
|
1081
|
+
// For ID_BACK: MRZ + barcode (check directly, not via state to avoid timing issues)
|
|
1082
|
+
const hasBarcode = !!barcodeToUse?.rawValue;
|
|
1083
|
+
const allRequiredElementsInFrame =
|
|
1084
|
+
(hasMRZ && hasBarcode) || onlyMRZScan;
|
|
1085
|
+
|
|
1086
|
+
// Don't block based on bounds - just ensure elements are present
|
|
1087
|
+
setElementsOutsideScanArea([]);
|
|
1088
|
+
|
|
1089
|
+
if (!allRequiredElementsInFrame && hasMRZ && mrzAccepted) {
|
|
1090
|
+
if (isDebugEnabled()) {
|
|
1091
|
+
console.log(
|
|
1092
|
+
'[ID_BACK Scan] MRZ valid but waiting for all elements in frame (MRZ + barcode)'
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
setStatus('SCANNING');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (
|
|
1100
|
+
hasMRZ &&
|
|
1101
|
+
mrzAccepted &&
|
|
1102
|
+
barcodeAccepted &&
|
|
1103
|
+
allRequiredElementsInFrame
|
|
1104
|
+
) {
|
|
1105
|
+
logMRZDetails(
|
|
1106
|
+
'ID_BACK Scan',
|
|
1107
|
+
parsedMRZData?.fields,
|
|
1108
|
+
mrzText,
|
|
1109
|
+
validMRZConsecutiveCount.current,
|
|
1110
|
+
isDebugEnabled()
|
|
1111
|
+
);
|
|
1112
|
+
const scannedData: DocumentScannedData = {
|
|
1113
|
+
image,
|
|
1114
|
+
documentType: 'ID_BACK',
|
|
1115
|
+
mrzText: mrzText ?? undefined,
|
|
1116
|
+
mrzFields: parsedMRZData?.fields,
|
|
1117
|
+
barcodeValue: barcodeToUse?.rawValue ?? undefined,
|
|
1118
|
+
};
|
|
1119
|
+
setDetectedDocumentType('ID_BACK');
|
|
1120
|
+
setStatus('SCANNED');
|
|
1121
|
+
setIsTorchOn(false);
|
|
1122
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
1123
|
+
setTimeout(() => {
|
|
1124
|
+
onIdentityDocumentScanned(scannedData);
|
|
1125
|
+
}, 1000);
|
|
1126
|
+
} else {
|
|
1127
|
+
if (hasMRZ && !mrzAccepted) {
|
|
1128
|
+
logMRZValidationFailure(
|
|
1129
|
+
'ID_BACK Scan',
|
|
1130
|
+
hasRequiredFields,
|
|
1131
|
+
parsedMRZData,
|
|
1132
|
+
mrzDetectionCurrentRetryCount.current,
|
|
1133
|
+
isDebugEnabled()
|
|
1134
|
+
);
|
|
1135
|
+
} else if (hasMRZ && mrzAccepted && !barcodeAccepted) {
|
|
1136
|
+
if (isDebugEnabled()) {
|
|
1137
|
+
console.log(
|
|
1138
|
+
'[ID_BACK Scan] MRZ valid but barcode check failed - retrying',
|
|
1139
|
+
{
|
|
1140
|
+
onlyMRZScan,
|
|
1141
|
+
hasBarcodeValue: !!barcodeToUse?.rawValue,
|
|
1142
|
+
barcodeMatchesMRZ,
|
|
1143
|
+
mrzOptional1: parsedMRZData?.fields?.optional1,
|
|
1144
|
+
barcodeValue: barcodeToUse?.rawValue,
|
|
1145
|
+
barcodeSource:
|
|
1146
|
+
barcodeToUse === cachedBarcode.current
|
|
1147
|
+
? 'cached'
|
|
1148
|
+
: 'current',
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1154
|
+
setStatus(hasMRZ ? 'SCANNING' : 'SEARCHING');
|
|
1155
|
+
}
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
776
1159
|
const documentType = detectDocumentType(
|
|
777
|
-
|
|
1160
|
+
cardSizedFaces,
|
|
778
1161
|
text,
|
|
779
|
-
parsedMRZData?.fields
|
|
1162
|
+
parsedMRZData?.fields,
|
|
1163
|
+
frameWidth,
|
|
1164
|
+
mrzText
|
|
780
1165
|
);
|
|
1166
|
+
|
|
1167
|
+
// Update detected document type only during initial scan step
|
|
1168
|
+
// CRITICAL: Only set document type from non-blurry, stable frames
|
|
1169
|
+
// Once set to PASSPORT or definitively identified, preserve it to avoid incorrect flow transitions
|
|
1170
|
+
if (
|
|
1171
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
1172
|
+
detectedDocumentType === 'UNKNOWN'
|
|
1173
|
+
) {
|
|
1174
|
+
// Determine the document type to set based on current frame analysis
|
|
1175
|
+
let docTypeToSet: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN' =
|
|
1176
|
+
documentType;
|
|
1177
|
+
|
|
1178
|
+
if (documentType === 'PASSPORT') {
|
|
1179
|
+
// Passport detected definitively - candidate for locking in
|
|
1180
|
+
docTypeToSet = 'PASSPORT';
|
|
1181
|
+
} else if (
|
|
1182
|
+
documentType === 'UNKNOWN' &&
|
|
1183
|
+
cardSizedFaces.length > 0 &&
|
|
1184
|
+
parsedMRZData?.fields?.documentCode === 'P'
|
|
1185
|
+
) {
|
|
1186
|
+
// Early passport detection: face detected + passport MRZ code (even if not fully valid yet)
|
|
1187
|
+
docTypeToSet = 'PASSPORT';
|
|
1188
|
+
} else if (documentType === 'ID_FRONT') {
|
|
1189
|
+
// Check if this is actually a passport based on MRZ code
|
|
1190
|
+
// Passports can be misdetected as ID_FRONT when signature-like text is visible
|
|
1191
|
+
if (parsedMRZData?.fields?.documentCode === 'P') {
|
|
1192
|
+
if (isDebugEnabled()) {
|
|
1193
|
+
console.log(
|
|
1194
|
+
'[DocType] Correcting misdetection: ID_FRONT → PASSPORT (MRZ code P)'
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
docTypeToSet = 'PASSPORT';
|
|
1198
|
+
} else if (parsedMRZData?.fields?.documentCode === 'I') {
|
|
1199
|
+
// MRZ confirms it's an ID card
|
|
1200
|
+
docTypeToSet = 'ID_FRONT';
|
|
1201
|
+
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
1202
|
+
// Passport MRZ pattern visible but not parsed yet - wait for proper classification
|
|
1203
|
+
if (isDebugEnabled()) {
|
|
1204
|
+
console.log(
|
|
1205
|
+
'[DocType] Passport MRZ pattern detected but not parsed - waiting instead of locking as ID_FRONT'
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
docTypeToSet = 'UNKNOWN';
|
|
1209
|
+
} else {
|
|
1210
|
+
// No MRZ code and no passport pattern - safe to classify as ID_FRONT
|
|
1211
|
+
// ID cards typically don't have MRZ on front (only on back)
|
|
1212
|
+
docTypeToSet = 'ID_FRONT';
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
docTypeToSet = 'UNKNOWN';
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Only update document type state if:
|
|
1219
|
+
// 1. Frame quality is acceptable (not blurry, good brightness)
|
|
1220
|
+
// 2. Document type has been detected consistently for multiple frames
|
|
1221
|
+
if (
|
|
1222
|
+
lastFrameQuality.current.hasAcceptableQuality &&
|
|
1223
|
+
docTypeToSet !== 'UNKNOWN'
|
|
1224
|
+
) {
|
|
1225
|
+
if (docTypeToSet === lastDetectedDocType.current) {
|
|
1226
|
+
consistentDocTypeCount.current++;
|
|
1227
|
+
if (isDebugEnabled()) {
|
|
1228
|
+
console.log(
|
|
1229
|
+
`[DocType Stability] Consistent detection: ${docTypeToSet} (${consistentDocTypeCount.current}/${REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS})`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (
|
|
1234
|
+
consistentDocTypeCount.current >=
|
|
1235
|
+
REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS
|
|
1236
|
+
) {
|
|
1237
|
+
// Stable detection confirmed - lock it in
|
|
1238
|
+
if (isDebugEnabled()) {
|
|
1239
|
+
console.log(
|
|
1240
|
+
`[DocType] Locked document type: ${docTypeToSet} (stable for ${consistentDocTypeCount.current} quality frames)`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
setDetectedDocumentType(docTypeToSet);
|
|
1244
|
+
}
|
|
1245
|
+
} else {
|
|
1246
|
+
// Document type changed - reset counter
|
|
1247
|
+
if (isDebugEnabled()) {
|
|
1248
|
+
console.log(
|
|
1249
|
+
`[DocType Stability] Type changed: ${lastDetectedDocType.current} → ${docTypeToSet}, resetting counter`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
lastDetectedDocType.current = docTypeToSet;
|
|
1253
|
+
consistentDocTypeCount.current = 1;
|
|
1254
|
+
}
|
|
1255
|
+
} else if (
|
|
1256
|
+
!lastFrameQuality.current.hasAcceptableQuality &&
|
|
1257
|
+
docTypeToSet !== 'UNKNOWN'
|
|
1258
|
+
) {
|
|
1259
|
+
// Poor quality frame - don't use for document type detection
|
|
1260
|
+
if (isDebugEnabled()) {
|
|
1261
|
+
console.log(
|
|
1262
|
+
`[DocType Stability] Skipping poor quality frame (blurry: ${lastFrameQuality.current.isBlurry}, brightness: ${lastFrameQuality.current.brightness.toFixed(0)})`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
// Document type is now locked and won't be changed after initial scan
|
|
1268
|
+
// Hologram and subsequent steps use the preserved detectedDocumentType state
|
|
1269
|
+
|
|
781
1270
|
const scannedData: DocumentScannedData = {
|
|
782
1271
|
image,
|
|
783
1272
|
documentType,
|
|
@@ -785,420 +1274,1281 @@ const IdentityDocumentCamera = ({
|
|
|
785
1274
|
mrzFields: parsedMRZData?.fields,
|
|
786
1275
|
};
|
|
787
1276
|
|
|
788
|
-
scannedData.faceImage = croppedFaces[0];
|
|
789
|
-
setCurrentFaceImage(croppedFaces[0]);
|
|
790
|
-
|
|
791
|
-
// Track detected document type for UI feedback
|
|
792
|
-
if (documentType !== 'UNKNOWN') {
|
|
793
|
-
setDetectedDocumentType(documentType);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Detect wrong side based on document type or face presence (works for both normal and eID scan)
|
|
797
|
-
// For ID_BACK step: if faces are detected, it's likely the front side (wrong)
|
|
798
|
-
// For FRONT step: if ID_BACK is detected, it's the wrong side
|
|
799
1277
|
const isWrongSide =
|
|
800
|
-
|
|
801
|
-
(nextStep === 'SCAN_ID_BACK' && (documentType === 'ID_FRONT' || documentType === 'PASSPORT' || croppedFaces.length > 0));
|
|
1278
|
+
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && documentType === 'ID_BACK';
|
|
802
1279
|
|
|
803
1280
|
if (isWrongSide) {
|
|
804
1281
|
setStatus('INCORRECT');
|
|
805
1282
|
return;
|
|
806
1283
|
}
|
|
807
1284
|
|
|
1285
|
+
// Always use locked face if available
|
|
1286
|
+
if (faceImageToUse) {
|
|
1287
|
+
scannedData.faceImage = faceImageToUse;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
808
1290
|
if (!onlyMRZScan) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1291
|
+
// Hologram detection during SCAN_HOLOGRAM step - ALWAYS use first/leftmost face ONLY
|
|
1292
|
+
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
1293
|
+
if (isDebugEnabled()) {
|
|
1294
|
+
console.log(
|
|
1295
|
+
`[Hologram] In SCAN_HOLOGRAM step - currentHologramImage: ${!!currentHologramImage}, collected: ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT}`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Always crop to the same face region across all hologram frames so
|
|
1300
|
+
// OpenCV receives consistently-sized images for comparison.
|
|
1301
|
+
// Use current face bounds if available, otherwise fall back to last known position.
|
|
1302
|
+
const hologramFaceBounds =
|
|
1303
|
+
cardSizedFaces.length > 0 && cardSizedFaces[0]
|
|
1304
|
+
? cardSizedFaces[0].bounds
|
|
1305
|
+
: lastFacePosition.current;
|
|
1306
|
+
let primaryFaceOnly: string | undefined;
|
|
1307
|
+
if (hologramFaceBounds && image) {
|
|
1308
|
+
const hologramCropped = await getFaceImages(
|
|
1309
|
+
[{ bounds: hologramFaceBounds, rollAngle: 0, yawAngle: 0 }],
|
|
1310
|
+
image,
|
|
1311
|
+
frameWidth,
|
|
1312
|
+
frameHeight
|
|
1313
|
+
);
|
|
1314
|
+
primaryFaceOnly = hologramCropped[0] ?? faceImageToUse;
|
|
812
1315
|
} else {
|
|
813
|
-
|
|
814
|
-
setCurrentFaceImage(croppedFaces[0]);
|
|
1316
|
+
primaryFaceOnly = faceImageToUse;
|
|
815
1317
|
}
|
|
816
1318
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1319
|
+
// Skip face position validation for hologram — flash toggling causes position jitter
|
|
1320
|
+
if (primaryFaceOnly) {
|
|
1321
|
+
// Reset consecutive no-face counter since we have a face
|
|
1322
|
+
hologramFramesWithoutFace.current = 0;
|
|
1323
|
+
|
|
1324
|
+
if (currentHologramImage) {
|
|
1325
|
+
scannedData.hologramImage = currentHologramImage;
|
|
1326
|
+
} else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
|
|
1327
|
+
// Add timing control to space out captures for better variation
|
|
1328
|
+
const now = Date.now();
|
|
1329
|
+
const timeSinceLastCapture =
|
|
1330
|
+
now - lastHologramCaptureTime.current;
|
|
1331
|
+
|
|
1332
|
+
if (
|
|
1333
|
+
faceImages.current.length === 0 ||
|
|
1334
|
+
timeSinceLastCapture >= HOLOGRAM_CAPTURE_INTERVAL
|
|
1335
|
+
) {
|
|
1336
|
+
// Collect PRIMARY face image ONLY (always index 0) from same document plane
|
|
1337
|
+
faceImages.current.push(primaryFaceOnly);
|
|
1338
|
+
lastHologramCaptureTime.current = now;
|
|
1339
|
+
hologramImageCountRef.current = faceImages.current.length;
|
|
1340
|
+
|
|
1341
|
+
// Only update state at first and last frame to minimize re-renders
|
|
1342
|
+
if (
|
|
1343
|
+
faceImages.current.length === 1 ||
|
|
1344
|
+
faceImages.current.length === HOLOGRAM_IMAGE_COUNT
|
|
1345
|
+
) {
|
|
1346
|
+
setHologramImageCount(faceImages.current.length);
|
|
1347
|
+
setLatestHologramFaceImage(primaryFaceOnly);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (isDebugEnabled()) {
|
|
1351
|
+
console.log(
|
|
1352
|
+
`[Hologram] Collected ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT} face images`
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Keep flash on during processing - will turn off when step changes
|
|
1357
|
+
}
|
|
1358
|
+
} else if (faceImages.current.length >= HOLOGRAM_IMAGE_COUNT) {
|
|
1359
|
+
// Process collected full document images
|
|
1360
|
+
if (isDebugEnabled()) {
|
|
1361
|
+
console.log(
|
|
1362
|
+
`[Hologram] Processing ${faceImages.current.length} full document images`
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const [hologramMask, hologram] = await detectHologramNative(
|
|
1367
|
+
faceImages.current
|
|
1368
|
+
);
|
|
1369
|
+
if (hologram) {
|
|
1370
|
+
setCurrentHologramMaskImage(hologramMask);
|
|
1371
|
+
scannedData.hologramImage = hologram;
|
|
1372
|
+
setCurrentHologramImage(hologram);
|
|
1373
|
+
if (isDebugEnabled()) {
|
|
1374
|
+
console.log('[Hologram] Detection successful');
|
|
1375
|
+
}
|
|
1376
|
+
} else {
|
|
1377
|
+
if (isDebugEnabled()) {
|
|
1378
|
+
console.log('[Hologram] No hologram detected');
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
console.error('[Hologram] Processing error:', error);
|
|
1383
|
+
} finally {
|
|
1384
|
+
// Keep flash on - will turn off when step changes
|
|
1385
|
+
faceImages.current = [];
|
|
1386
|
+
hologramImageCountRef.current = 0;
|
|
1387
|
+
setHologramImageCount(0);
|
|
1388
|
+
setLatestHologramFaceImage(undefined);
|
|
1389
|
+
hologramDetectionCurrentRetryCount.current++;
|
|
1390
|
+
if (isDebugEnabled()) {
|
|
1391
|
+
console.log(
|
|
1392
|
+
`[Hologram] Retry ${hologramDetectionCurrentRetryCount.current}/${HOLOGRAM_DETECTION_RETRY_COUNT}`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
822
1396
|
}
|
|
823
|
-
faceImages.push(croppedFaces[0]);
|
|
824
1397
|
} else {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
setCurrentHologramImage(hologram);
|
|
1398
|
+
// No face detected for hologram collection
|
|
1399
|
+
// Track consecutive frames without face for safety timeout
|
|
1400
|
+
hologramFramesWithoutFace.current++;
|
|
1401
|
+
if (isDebugEnabled()) {
|
|
1402
|
+
console.log(
|
|
1403
|
+
`[Hologram] No face detected - frame ${hologramFramesWithoutFace.current}/${HOLOGRAM_MAX_FRAMES_WITHOUT_FACE}`
|
|
1404
|
+
);
|
|
833
1405
|
}
|
|
834
|
-
faceImages = [];
|
|
835
|
-
hologramDetectionCurrentRetryCount.value++;
|
|
836
1406
|
}
|
|
1407
|
+
} else if (currentHologramImage) {
|
|
1408
|
+
scannedData.hologramImage = currentHologramImage;
|
|
1409
|
+
} else if (faceImages.current.length > 0) {
|
|
1410
|
+
// Safety cleanup: not in hologram step but have images collected
|
|
1411
|
+
faceImages.current = [];
|
|
1412
|
+
hologramImageCountRef.current = 0;
|
|
1413
|
+
setHologramImageCount(0);
|
|
1414
|
+
setLatestHologramFaceImage(undefined);
|
|
1415
|
+
if (isDebugEnabled()) {
|
|
1416
|
+
console.log(
|
|
1417
|
+
'[Hologram] Defensive cleanup - cleared images outside hologram step'
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
837
1421
|
|
|
1422
|
+
// SKIP secondary face detection during SCAN_HOLOGRAM - only focus on primary face
|
|
1423
|
+
// Secondary face was already captured during initial scan (SCAN_ID_FRONT_OR_PASSPORT)
|
|
1424
|
+
// During hologram, we only collect hologram images from primary face
|
|
1425
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1426
|
+
// Capture secondary face - must be similar to main face AND from same document plane
|
|
838
1427
|
if (currentSecondaryFaceImage) {
|
|
839
1428
|
scannedData.secondaryFaceImage = currentSecondaryFaceImage;
|
|
840
1429
|
} else if (
|
|
841
1430
|
!!scannedData.faceImage &&
|
|
842
1431
|
croppedFaces.length > 1 &&
|
|
843
1432
|
!!croppedFaces[1] &&
|
|
844
|
-
|
|
1433
|
+
facePositionValid
|
|
845
1434
|
) {
|
|
846
|
-
|
|
847
|
-
|
|
1435
|
+
// Always validate similarity to ensure it's the same person on the same document
|
|
1436
|
+
const isSimilar = await areImagesSimilarNative(
|
|
1437
|
+
scannedData.faceImage,
|
|
1438
|
+
croppedFaces[1],
|
|
1439
|
+
15000 // Default threshold from main branch
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
if (isSimilar) {
|
|
1443
|
+
scannedData.secondaryFaceImage = croppedFaces[1];
|
|
1444
|
+
setCurrentSecondaryFaceImage(scannedData.secondaryFaceImage);
|
|
1445
|
+
|
|
1446
|
+
// Update secondary face bounds for debug overlay
|
|
1447
|
+
if (faces.length > 1 && faces[1] && frameDimensions) {
|
|
1448
|
+
const screen = Dimensions.get('window');
|
|
1449
|
+
const frameAspect =
|
|
1450
|
+
frameDimensions.width / frameDimensions.height;
|
|
1451
|
+
const screenAspect = screen.width / screen.height;
|
|
1452
|
+
|
|
1453
|
+
let scale: number;
|
|
1454
|
+
let offsetX = 0;
|
|
1455
|
+
let offsetY = 0;
|
|
1456
|
+
|
|
1457
|
+
if (frameAspect > screenAspect) {
|
|
1458
|
+
scale = screen.height / frameDimensions.height;
|
|
1459
|
+
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
1460
|
+
} else {
|
|
1461
|
+
scale = screen.width / frameDimensions.width;
|
|
1462
|
+
offsetY =
|
|
1463
|
+
(frameDimensions.height * scale - screen.height) / 2;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const scanLeft = (screen.width * 0.05 + offsetX) / scale;
|
|
1467
|
+
const scanTop = (screen.height * 0.36 + offsetY) / scale;
|
|
1468
|
+
const scanRight = (screen.width * 0.95 + offsetX) / scale;
|
|
1469
|
+
const scanBottom = (screen.height * 0.64 + offsetY) / scale;
|
|
1470
|
+
const isInsideScan = (
|
|
1471
|
+
x: number,
|
|
1472
|
+
y: number,
|
|
1473
|
+
w: number,
|
|
1474
|
+
h: number
|
|
1475
|
+
) =>
|
|
1476
|
+
x >= scanLeft &&
|
|
1477
|
+
y >= scanTop &&
|
|
1478
|
+
x + w <= scanRight &&
|
|
1479
|
+
y + h <= scanBottom;
|
|
1480
|
+
|
|
1481
|
+
const secondaryBounds = faces[1].bounds;
|
|
1482
|
+
if (
|
|
1483
|
+
isInsideScan(
|
|
1484
|
+
secondaryBounds.x,
|
|
1485
|
+
secondaryBounds.y,
|
|
1486
|
+
secondaryBounds.width,
|
|
1487
|
+
secondaryBounds.height
|
|
1488
|
+
)
|
|
1489
|
+
) {
|
|
1490
|
+
setSecondaryFaceBounds({
|
|
1491
|
+
x: secondaryBounds.x * scale - offsetX,
|
|
1492
|
+
y: secondaryBounds.y * scale - offsetY,
|
|
1493
|
+
width: secondaryBounds.width * scale,
|
|
1494
|
+
height: secondaryBounds.height * scale,
|
|
1495
|
+
});
|
|
1496
|
+
} else {
|
|
1497
|
+
setSecondaryFaceBounds(null);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (isDebugEnabled()) {
|
|
1502
|
+
console.log(
|
|
1503
|
+
'[SecondaryFace] ✓ Captured and validated as similar to main face (same document plane)'
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
} else {
|
|
1507
|
+
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
1508
|
+
if (isDebugEnabled()) {
|
|
1509
|
+
console.log(
|
|
1510
|
+
'[SecondaryFace] ✗ Rejected - not similar enough to main face'
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
848
1514
|
} else {
|
|
849
|
-
secondaryFaceDetectionCurrentRetryCount.
|
|
1515
|
+
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
1516
|
+
if (!facePositionValid && croppedFaces.length > 1) {
|
|
1517
|
+
if (isDebugEnabled()) {
|
|
1518
|
+
console.log(
|
|
1519
|
+
'[SecondaryFace] ✗ Rejected - document plane changed'
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
850
1523
|
}
|
|
1524
|
+
} else if (currentSecondaryFaceImage) {
|
|
1525
|
+
// Already have secondary face from earlier - just use it
|
|
1526
|
+
scannedData.secondaryFaceImage = currentSecondaryFaceImage;
|
|
851
1527
|
}
|
|
852
1528
|
}
|
|
853
1529
|
|
|
1530
|
+
// UNIFIED SCAN_HOLOGRAM completion - ONLY check primary face and hologram collection
|
|
1531
|
+
// Document type is already definitively determined before entering this step
|
|
1532
|
+
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
1533
|
+
// CRITICAL: Verify correct side is shown - hologram is ALWAYS on the front side with photo
|
|
1534
|
+
// If wrong side detected, warn user immediately
|
|
1535
|
+
const hasFaces = cardSizedFaces.length > 0;
|
|
1536
|
+
const hasBarcode = !!barcode?.rawValue; // ID back has barcode, front doesn't
|
|
1537
|
+
|
|
1538
|
+
// For passport: back side has no photo and different text pattern
|
|
1539
|
+
// For ID card: back side has no photo, has barcode
|
|
1540
|
+
const isWrongSideForHologram = !hasFaces || hasBarcode;
|
|
1541
|
+
|
|
1542
|
+
if (isWrongSideForHologram) {
|
|
1543
|
+
if (isDebugEnabled()) {
|
|
1544
|
+
console.log(
|
|
1545
|
+
`[SCAN_HOLOGRAM] Wrong side detected - no faces: ${!hasFaces}, has barcode: ${hasBarcode} - expecting front side with photo`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
setStatus('INCORRECT');
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Safety timeout: if we can't detect face for too many consecutive frames, give up
|
|
1553
|
+
const faceDetectionTimeout =
|
|
1554
|
+
hologramFramesWithoutFace.current >= HOLOGRAM_MAX_FRAMES_WITHOUT_FACE;
|
|
1555
|
+
|
|
1556
|
+
// Don't skip if actively collecting images
|
|
1557
|
+
const isActivelyCollecting =
|
|
1558
|
+
faceImages.current.length > 0 &&
|
|
1559
|
+
faceImages.current.length < HOLOGRAM_IMAGE_COUNT;
|
|
1560
|
+
|
|
1561
|
+
const hologramConditionMet =
|
|
1562
|
+
!!scannedData.hologramImage ||
|
|
1563
|
+
(hologramDetectionCurrentRetryCount.current >=
|
|
1564
|
+
HOLOGRAM_DETECTION_RETRY_COUNT &&
|
|
1565
|
+
!isActivelyCollecting) || // Don't skip if mid-collection
|
|
1566
|
+
(faceDetectionTimeout && !isActivelyCollecting); // Don't timeout if mid-collection
|
|
1567
|
+
|
|
1568
|
+
// During hologram scan, we ONLY care about hologram collection - no other checks
|
|
1569
|
+
// Secondary face, MRZ, document type checks are all skipped
|
|
1570
|
+
// Document type was already definitively determined in the initial scan phase
|
|
1571
|
+
|
|
1572
|
+
// Log detailed state for debugging
|
|
1573
|
+
if (isActivelyCollecting && isDebugEnabled()) {
|
|
1574
|
+
console.log(
|
|
1575
|
+
`[SCAN_HOLOGRAM] Actively collecting: ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT} - continuing collection`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if (hologramConditionMet) {
|
|
1580
|
+
if (faceDetectionTimeout && isDebugEnabled()) {
|
|
1581
|
+
console.log(
|
|
1582
|
+
'[SCAN_HOLOGRAM] Face detection timeout - proceeding without hologram'
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
setStatus('SCANNED');
|
|
1586
|
+
if (nextStep !== 'SCAN_HOLOGRAM') {
|
|
1587
|
+
setIsTorchOn(false);
|
|
1588
|
+
}
|
|
1589
|
+
// Route based on PRESERVED detectedDocumentType state (set during initial scan)
|
|
1590
|
+
// Also check current frame's documentType and MRZ code as fallback
|
|
1591
|
+
// Passport has no back side - go directly to COMPLETED
|
|
1592
|
+
const isPassport =
|
|
1593
|
+
detectedDocumentType === 'PASSPORT' ||
|
|
1594
|
+
documentType === 'PASSPORT' ||
|
|
1595
|
+
parsedMRZData?.fields?.documentCode === 'P';
|
|
1596
|
+
if (isDebugEnabled()) {
|
|
1597
|
+
console.log('[SCAN_HOLOGRAM] Document type check:', {
|
|
1598
|
+
detectedDocumentType,
|
|
1599
|
+
documentType,
|
|
1600
|
+
mrzCode: parsedMRZData?.fields?.documentCode,
|
|
1601
|
+
isPassport,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
if (isPassport) {
|
|
1605
|
+
if (isDebugEnabled()) {
|
|
1606
|
+
console.log(
|
|
1607
|
+
'[SCAN_HOLOGRAM] Passport detected - completing scan (no back side)'
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
|
|
1611
|
+
} else {
|
|
1612
|
+
if (isDebugEnabled()) {
|
|
1613
|
+
console.log(
|
|
1614
|
+
'[SCAN_HOLOGRAM] ID card detected - proceeding to back scan'
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
|
|
1618
|
+
}
|
|
1619
|
+
setTimeout(() => {
|
|
1620
|
+
onIdentityDocumentScanned(scannedData);
|
|
1621
|
+
}, 1000);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
// Still collecting or conditions not met - stay in SCAN_HOLOGRAM
|
|
1625
|
+
// Don't fall through to document type branching
|
|
1626
|
+
setStatus('SCANNING');
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
854
1630
|
if (documentType === 'ID_FRONT') {
|
|
855
1631
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1632
|
+
// CRITICAL: Verify this is actually an ID card, not a passport misdetected as ID_FRONT
|
|
1633
|
+
// Passports can show signature-like text and be temporarily classified as ID_FRONT
|
|
1634
|
+
if (parsedMRZData?.fields?.documentCode === 'P') {
|
|
1635
|
+
if (isDebugEnabled()) {
|
|
1636
|
+
console.log(
|
|
1637
|
+
'[ID_FRONT Scan] Detected as ID_FRONT but MRZ shows passport (code P) - waiting for passport branch'
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
setStatus('SCANNING');
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const hasFace = cardSizedFaces.length > 0;
|
|
1645
|
+
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
1646
|
+
const retryThreshold = 60;
|
|
1647
|
+
const allowFaceOnly =
|
|
1648
|
+
mrzDetectionCurrentRetryCount.current > retryThreshold;
|
|
1649
|
+
const allRequiredElementsInFrame =
|
|
1650
|
+
hasFace && (hasSignature || allowFaceOnly);
|
|
1651
|
+
|
|
1652
|
+
setElementsOutsideScanArea([]);
|
|
1653
|
+
|
|
1654
|
+
if (!allRequiredElementsInFrame) {
|
|
1655
|
+
console.log(
|
|
1656
|
+
'[ID_FRONT Scan] Valid but waiting for all elements in frame (face + signature)'
|
|
1657
|
+
);
|
|
1658
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1659
|
+
setStatus('SCANNING');
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// CRITICAL: Final verification that this is definitively an ID card before proceeding
|
|
1664
|
+
// Check if we have MRZ and if it indicates ID card (not passport)
|
|
1665
|
+
if (parsedMRZData?.fields?.documentCode) {
|
|
1666
|
+
if (parsedMRZData.fields.documentCode === 'I') {
|
|
1667
|
+
if (isDebugEnabled()) {
|
|
1668
|
+
console.log('[ID_FRONT Scan] MRZ confirms ID card (code I)');
|
|
1669
|
+
}
|
|
1670
|
+
} else if (parsedMRZData.fields.documentCode === 'P') {
|
|
1671
|
+
if (isDebugEnabled()) {
|
|
1672
|
+
console.log(
|
|
1673
|
+
'[ID_FRONT Scan] MRZ shows passport (code P) - rejecting as ID_FRONT'
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
setStatus('SCANNING');
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
1680
|
+
// No parsed MRZ BUT passport MRZ pattern visible (P<TUR, P<USA, etc.)
|
|
1681
|
+
// This is likely a passport with OCR errors - wait for proper parsing
|
|
1682
|
+
if (isDebugEnabled()) {
|
|
1683
|
+
console.log(
|
|
1684
|
+
'[ID_FRONT Scan] Passport MRZ pattern (P<XXX) visible but not parsed - waiting for passport classification'
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1688
|
+
setStatus('SCANNING');
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
// No MRZ or no passport pattern - proceed as ID card
|
|
1692
|
+
// ID cards typically don't have MRZ on front side (only on back)
|
|
1693
|
+
|
|
1694
|
+
// CRITICAL: Lock document type state to ID_FRONT before proceeding
|
|
1695
|
+
// This ensures hologram completion knows it's an ID card (needs ID_BACK step)
|
|
1696
|
+
setDetectedDocumentType('ID_FRONT');
|
|
856
1697
|
setStatus('SCANNED');
|
|
1698
|
+
setIsTorchOn(false);
|
|
857
1699
|
if (onlyMRZScan) {
|
|
858
|
-
|
|
859
|
-
|
|
1700
|
+
// Passport has no back side - go directly to COMPLETED
|
|
1701
|
+
// At this point detectedDocumentType is definitively set
|
|
1702
|
+
if (detectedDocumentType === 'PASSPORT') {
|
|
1703
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1704
|
+
} else {
|
|
1705
|
+
setNextStepAndVibrate(
|
|
1706
|
+
'SCAN_ID_BACK',
|
|
1707
|
+
'SCAN_ID_FRONT_OR_PASSPORT'
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
setTimeout(() => {
|
|
1711
|
+
onIdentityDocumentScanned(scannedData);
|
|
1712
|
+
}, 1000);
|
|
860
1713
|
} else {
|
|
1714
|
+
if (isDebugEnabled()) {
|
|
1715
|
+
console.log(
|
|
1716
|
+
'[ID_FRONT Scan] Confirmed as ID card - proceeding to hologram'
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
861
1719
|
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
hologramDetectionCurrentRetryCount.value >=
|
|
867
|
-
HOLOGRAM_DETECTION_RETRY_COUNT) &&
|
|
868
|
-
(!!scannedData.secondaryFaceImage ||
|
|
869
|
-
secondaryFaceDetectionCurrentRetryCount.value >=
|
|
870
|
-
SECOND_FACE_DETECTION_RETRY_COUNT)
|
|
871
|
-
) {
|
|
872
|
-
setStatus('SCANNED');
|
|
873
|
-
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
|
|
874
|
-
onIdentityDocumentScanned(scannedData);
|
|
1720
|
+
setTimeout(() => {
|
|
1721
|
+
onIdentityDocumentScanned(scannedData);
|
|
1722
|
+
}, 1000);
|
|
1723
|
+
}
|
|
875
1724
|
}
|
|
1725
|
+
// Note: SCAN_HOLOGRAM completion is now handled in the unified block above
|
|
876
1726
|
} else if (documentType === 'PASSPORT') {
|
|
877
1727
|
if (
|
|
878
1728
|
nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
|
|
879
1729
|
!scannedData.hologramImage
|
|
880
1730
|
) {
|
|
881
|
-
// For passport, require valid MRZ before proceeding
|
|
882
1731
|
if (onlyMRZScan) {
|
|
883
|
-
|
|
1732
|
+
const hasRequiredFields = hasRequiredMRZFields(
|
|
1733
|
+
parsedMRZData?.fields
|
|
1734
|
+
);
|
|
1735
|
+
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
884
1736
|
if (
|
|
885
1737
|
!!scannedData.mrzText &&
|
|
886
|
-
|
|
887
|
-
|
|
1738
|
+
hasRequiredFields &&
|
|
1739
|
+
mrzStableAndValid
|
|
888
1740
|
) {
|
|
1741
|
+
const hasFace = cardSizedFaces.length > 0;
|
|
1742
|
+
const hasMRZ = !!mrzText;
|
|
1743
|
+
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1744
|
+
|
|
1745
|
+
setElementsOutsideScanArea([]);
|
|
1746
|
+
|
|
1747
|
+
if (!allRequiredElementsInFrame) {
|
|
1748
|
+
console.log(
|
|
1749
|
+
'[Passport Scan] MRZ valid but waiting for all elements in frame (face + MRZ)'
|
|
1750
|
+
);
|
|
1751
|
+
setStatus('SCANNING');
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
logMRZDetails(
|
|
1755
|
+
'Passport Scan',
|
|
1756
|
+
parsedMRZData?.fields,
|
|
1757
|
+
mrzText,
|
|
1758
|
+
validMRZConsecutiveCount.current,
|
|
1759
|
+
isDebugEnabled()
|
|
1760
|
+
);
|
|
1761
|
+
setDetectedDocumentType('PASSPORT');
|
|
889
1762
|
setStatus('SCANNED');
|
|
1763
|
+
setIsTorchOn(false);
|
|
890
1764
|
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1765
|
+
setTimeout(() => {
|
|
1766
|
+
onIdentityDocumentScanned(scannedData);
|
|
1767
|
+
}, 1000);
|
|
1768
|
+
return; // CRITICAL: Exit after MRZ-only scan completes to prevent fall-through
|
|
1769
|
+
} else {
|
|
1770
|
+
if (!!scannedData.mrzText && !mrzStableAndValid) {
|
|
1771
|
+
logMRZValidationFailure(
|
|
1772
|
+
'Passport Scan',
|
|
1773
|
+
hasRequiredFields,
|
|
1774
|
+
parsedMRZData,
|
|
1775
|
+
mrzDetectionCurrentRetryCount.current,
|
|
1776
|
+
isDebugEnabled()
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
894
1780
|
setStatus('SCANNING');
|
|
1781
|
+
return; // Don't fall through to else-if
|
|
895
1782
|
}
|
|
896
1783
|
} else {
|
|
897
|
-
// Normal scan
|
|
1784
|
+
// Normal passport scan (with hologram) - require MRZ to be detected before proceeding
|
|
1785
|
+
const hasFace = cardSizedFaces.length > 0;
|
|
1786
|
+
const hasMRZ = !!mrzText;
|
|
1787
|
+
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1788
|
+
|
|
1789
|
+
setElementsOutsideScanArea([]);
|
|
1790
|
+
|
|
1791
|
+
if (!allRequiredElementsInFrame) {
|
|
1792
|
+
console.log(
|
|
1793
|
+
'[Passport Scan] Valid but waiting for all elements in frame (face + MRZ)'
|
|
1794
|
+
);
|
|
1795
|
+
setStatus('SCANNING');
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// CRITICAL: Final verification - ensure MRZ definitively identifies this as passport
|
|
1800
|
+
// This must pass before we can proceed to hologram
|
|
1801
|
+
if (
|
|
1802
|
+
!parsedMRZData?.fields?.documentCode ||
|
|
1803
|
+
parsedMRZData.fields.documentCode !== 'P'
|
|
1804
|
+
) {
|
|
1805
|
+
console.log(
|
|
1806
|
+
'[Passport Scan] MRZ detected but not confirmed as passport (code:',
|
|
1807
|
+
parsedMRZData?.fields?.documentCode || 'none',
|
|
1808
|
+
') - waiting for valid passport MRZ'
|
|
1809
|
+
);
|
|
1810
|
+
setStatus('SCANNING');
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
console.log(
|
|
1815
|
+
'[Passport Scan] MRZ confirmed passport (code P) - definitively identified, proceeding to hologram'
|
|
1816
|
+
);
|
|
1817
|
+
// CRITICAL: Lock document type state to PASSPORT before proceeding to hologram
|
|
1818
|
+
// This ensures hologram completion knows it's a passport (no ID_BACK step)
|
|
1819
|
+
setDetectedDocumentType('PASSPORT');
|
|
898
1820
|
setStatus('SCANNED');
|
|
1821
|
+
setIsTorchOn(false);
|
|
899
1822
|
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1823
|
+
setTimeout(() => {
|
|
1824
|
+
onIdentityDocumentScanned(scannedData);
|
|
1825
|
+
}, 1000);
|
|
900
1826
|
}
|
|
901
|
-
} else if (
|
|
902
|
-
((nextStep === 'SCAN_HOLOGRAM' &&
|
|
903
|
-
(!!scannedData.hologramImage ||
|
|
904
|
-
hologramDetectionCurrentRetryCount.value >=
|
|
905
|
-
HOLOGRAM_DETECTION_RETRY_COUNT) &&
|
|
906
|
-
(!!scannedData.secondaryFaceImage ||
|
|
907
|
-
secondaryFaceDetectionCurrentRetryCount.value >=
|
|
908
|
-
SECOND_FACE_DETECTION_RETRY_COUNT)) ||
|
|
909
|
-
onlyMRZScan) &&
|
|
910
|
-
!!scannedData.mrzText &&
|
|
911
|
-
(parsedMRZData?.valid ||
|
|
912
|
-
mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)
|
|
913
|
-
) {
|
|
914
|
-
setStatus('SCANNED');
|
|
915
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
|
|
916
|
-
onIdentityDocumentScanned(scannedData);
|
|
917
|
-
} else if (!parsedMRZData?.valid) {
|
|
918
|
-
mrzDetectionCurrentRetryCount.value++;
|
|
919
1827
|
}
|
|
1828
|
+
// Note: SCAN_HOLOGRAM completion is now handled in the unified block above
|
|
920
1829
|
} else if (documentType === 'ID_BACK') {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
parsedMRZData?.fields?.issuingState !== 'TUR' ||
|
|
926
|
-
onlyMRZScan) &&
|
|
927
|
-
nextStep === 'SCAN_ID_BACK' &&
|
|
928
|
-
!!scannedData.mrzText &&
|
|
929
|
-
(parsedMRZData?.valid ||
|
|
930
|
-
mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)
|
|
931
|
-
) {
|
|
932
|
-
scannedData.barcodeValue = barcode?.value ?? undefined;
|
|
933
|
-
setStatus('SCANNED');
|
|
934
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
935
|
-
onIdentityDocumentScanned(scannedData);
|
|
936
|
-
} else if (!parsedMRZData?.valid) {
|
|
937
|
-
mrzDetectionCurrentRetryCount.value++;
|
|
938
|
-
}
|
|
1830
|
+
// ID_BACK is now handled in the early-return path above (SCAN_ID_BACK)
|
|
1831
|
+
// This branch only triggers if somehow ID_BACK is detected during a non-back-scan step
|
|
1832
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1833
|
+
setStatus('SCANNING');
|
|
939
1834
|
} else {
|
|
1835
|
+
// Document type UNKNOWN - continue scanning until we can classify it
|
|
1836
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1837
|
+
console.log(
|
|
1838
|
+
'[Document Scan] Type UNKNOWN - waiting for clearer detection (faces:',
|
|
1839
|
+
cardSizedFaces.length,
|
|
1840
|
+
'mrzCode:',
|
|
1841
|
+
parsedMRZData?.fields?.documentCode || 'none',
|
|
1842
|
+
'text length:',
|
|
1843
|
+
text.length,
|
|
1844
|
+
')'
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
940
1847
|
setStatus('SCANNING');
|
|
941
1848
|
}
|
|
942
|
-
|
|
943
|
-
// Clear OpenCV buffers to prevent memory leaks
|
|
944
|
-
try {
|
|
945
|
-
OpenCV.clearBuffers();
|
|
946
|
-
} catch (bufferError) {
|
|
947
|
-
// Ignore buffer cleanup errors
|
|
948
|
-
console.warn('Buffer cleanup error:', bufferError);
|
|
949
|
-
}
|
|
950
1849
|
},
|
|
951
1850
|
[
|
|
952
|
-
currentFaceImage,
|
|
953
|
-
currentHologramImage,
|
|
954
|
-
currentSecondaryFaceImage,
|
|
955
|
-
device,
|
|
956
1851
|
nextStep,
|
|
1852
|
+
frameDimensions,
|
|
1853
|
+
currentHologramImage,
|
|
1854
|
+
currentFaceImage,
|
|
1855
|
+
hasRequiredMRZFields,
|
|
1856
|
+
areMRZFieldsEqual,
|
|
1857
|
+
detectedDocumentType,
|
|
957
1858
|
onlyMRZScan,
|
|
1859
|
+
isTorchOn,
|
|
1860
|
+
setIsTorchOn,
|
|
1861
|
+
setNextStepAndVibrate,
|
|
1862
|
+
onIdentityDocumentScanned,
|
|
1863
|
+
logMRZDetails,
|
|
1864
|
+
logMRZValidationFailure,
|
|
1865
|
+
currentSecondaryFaceImage,
|
|
1866
|
+
detectHologramNative,
|
|
958
1867
|
]
|
|
959
1868
|
);
|
|
960
1869
|
|
|
961
|
-
const
|
|
962
|
-
(
|
|
963
|
-
|
|
964
|
-
},
|
|
965
|
-
[exposure]
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
// Focus trigger for when blur is detected (called from worklet)
|
|
969
|
-
const triggerFocus = useRunOnJS(
|
|
970
|
-
async () => {
|
|
971
|
-
if (!cameraRef.current || !device?.supportsFocus) {
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
try {
|
|
975
|
-
const width = format?.videoWidth ?? 1920;
|
|
976
|
-
const height = format?.videoHeight ?? 1080;
|
|
977
|
-
const centerPoint = getScanAreaCenterPoint(width, height);
|
|
978
|
-
await cameraRef.current.focus({
|
|
979
|
-
x: centerPoint.x,
|
|
980
|
-
y: centerPoint.y,
|
|
981
|
-
});
|
|
982
|
-
} catch (error) {
|
|
983
|
-
// Ignore focus errors
|
|
984
|
-
}
|
|
985
|
-
},
|
|
986
|
-
[device, format]
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
const handleExposureAndBrightness = (frame: Frame) => {
|
|
990
|
-
'worklet';
|
|
991
|
-
const averageBrightness = getAverageBrightness(frame);
|
|
992
|
-
const minExposure = device?.minExposure ?? 0;
|
|
993
|
-
const maxExposure = device?.maxExposure ?? 0;
|
|
994
|
-
|
|
995
|
-
// Dynamic thresholds based on scanning state using config values
|
|
996
|
-
// Face detection requires higher minimum brightness for reliable detection
|
|
997
|
-
const isFrontOrPassport = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
998
|
-
const isBack = nextStep === 'SCAN_ID_BACK';
|
|
999
|
-
|
|
1000
|
-
// Using config values: faceDetection { low: 50, high: 110, target: 85 }, mrzScanning { low: 45, high: 130, target: 80 }
|
|
1001
|
-
const lowerBrightnessBound = isFrontOrPassport ? 50 : 40;
|
|
1002
|
-
const upperBrightnessBound = isBack ? 130 : 120;
|
|
1003
|
-
const targetBrightness = isFrontOrPassport ? 85 : 80;
|
|
1004
|
-
|
|
1005
|
-
// Smooth exposure adjustment with hysteresis to prevent oscillation
|
|
1006
|
-
// Only adjust if brightness is significantly outside the acceptable range
|
|
1007
|
-
const hysteresis = 5; // Dead zone to prevent jitter
|
|
1008
|
-
|
|
1009
|
-
if (
|
|
1010
|
-
averageBrightness < (lowerBrightnessBound - hysteresis) &&
|
|
1011
|
-
exposureValue.value < maxExposure
|
|
1012
|
-
) {
|
|
1013
|
-
// Increase exposure smoothly when too dark
|
|
1014
|
-
const step = calculateExposureStep(averageBrightness, targetBrightness);
|
|
1015
|
-
exposureValue.value = Math.min(maxExposure, exposureValue.value + step);
|
|
1016
|
-
} else if (
|
|
1017
|
-
averageBrightness > (upperBrightnessBound + hysteresis) &&
|
|
1018
|
-
exposureValue.value > minExposure
|
|
1019
|
-
) {
|
|
1020
|
-
// Decrease exposure smoothly when too bright
|
|
1021
|
-
const step = calculateExposureStep(averageBrightness, targetBrightness);
|
|
1022
|
-
exposureValue.value = Math.max(minExposure, exposureValue.value - step);
|
|
1023
|
-
}
|
|
1024
|
-
// When within acceptable range (with hysteresis), don't adjust - prevents oscillation
|
|
1025
|
-
|
|
1026
|
-
const isBright = averageBrightness > lowerBrightnessBound;
|
|
1027
|
-
handleExposureValue(exposureValue.value);
|
|
1028
|
-
handleBrightness(isBright);
|
|
1029
|
-
|
|
1030
|
-
return isBright;
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
const handleWorklet = (frame: Frame) => {
|
|
1034
|
-
'worklet';
|
|
1035
|
-
try {
|
|
1036
|
-
const isBright = handleExposureAndBrightness(frame);
|
|
1037
|
-
if (!isBright) {
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
// Check for blur before processing - skip blurry frames
|
|
1044
|
-
// Use different thresholds: 25 for front (face detection), 30 for back (MRZ/text)
|
|
1045
|
-
// Higher thresholds with improved Laplacian algorithm using H+V gradients
|
|
1046
|
-
const isFront = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
1047
|
-
const blurThreshold = isFront ? 25 : 30;
|
|
1048
|
-
const blurry = checkBlurry(frame, blurThreshold);
|
|
1049
|
-
handleBlurStatus(blurry);
|
|
1050
|
-
if (blurry) {
|
|
1051
|
-
consecutiveBlurCount.value++;
|
|
1052
|
-
// Only trigger focus after 2 consecutive blurry frames (matching Flutter)
|
|
1053
|
-
if (consecutiveBlurCount.value >= 2) {
|
|
1054
|
-
triggerFocus();
|
|
1055
|
-
consecutiveBlurCount.value = 0;
|
|
1056
|
-
}
|
|
1870
|
+
const handleFrame = useCallback(
|
|
1871
|
+
async (event: NativeSyntheticEvent<{ frame: Frame }>) => {
|
|
1872
|
+
if (!isCameraInitialized.current) {
|
|
1057
1873
|
return;
|
|
1058
1874
|
}
|
|
1059
|
-
// Reset blur count on sharp frame
|
|
1060
|
-
consecutiveBlurCount.value = 0;
|
|
1061
1875
|
|
|
1876
|
+
const { frame } = event.nativeEvent;
|
|
1062
1877
|
|
|
1063
|
-
// Validate frame dimensions before processing
|
|
1064
1878
|
if (
|
|
1065
1879
|
!frame.width ||
|
|
1066
1880
|
!frame.height ||
|
|
1067
1881
|
frame.width <= 0 ||
|
|
1068
1882
|
frame.height <= 0
|
|
1069
1883
|
) {
|
|
1070
|
-
console.warn('Invalid frame dimensions:', {
|
|
1071
|
-
width: frame.width,
|
|
1072
|
-
height: frame.height,
|
|
1073
|
-
});
|
|
1074
1884
|
return;
|
|
1075
1885
|
}
|
|
1076
1886
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
try {
|
|
1080
|
-
if (faceDetectionEnabled) {
|
|
1081
|
-
detectedFaces = detectFaces(frame);
|
|
1082
|
-
// Reset error count on successful detection
|
|
1083
|
-
faceDetectionErrorCount.value = 0;
|
|
1084
|
-
}
|
|
1085
|
-
} catch (faceError) {
|
|
1086
|
-
console.warn('Face detection failed:', faceError);
|
|
1087
|
-
faceDetectionErrorCount.value += 1;
|
|
1088
|
-
|
|
1089
|
-
// Disable face detection temporarily after 5 consecutive errors
|
|
1090
|
-
if (faceDetectionErrorCount.value >= 5) {
|
|
1091
|
-
setFaceDetectionEnabled(false);
|
|
1092
|
-
// Re-enable after 10 seconds
|
|
1093
|
-
setTimeout(() => {
|
|
1094
|
-
setFaceDetectionEnabled(true);
|
|
1095
|
-
faceDetectionErrorCount.value = 0;
|
|
1096
|
-
}, 10000);
|
|
1097
|
-
}
|
|
1887
|
+
const base64Image = frame.base64Image;
|
|
1888
|
+
if (!base64Image) return;
|
|
1098
1889
|
|
|
1099
|
-
|
|
1890
|
+
const frameBrightness = frame.brightness ?? 128;
|
|
1891
|
+
brightnessHistory.current.push(frameBrightness);
|
|
1892
|
+
if (brightnessHistory.current.length > 5) {
|
|
1893
|
+
brightnessHistory.current.shift();
|
|
1100
1894
|
}
|
|
1895
|
+
const avgBrightness =
|
|
1896
|
+
brightnessHistory.current.reduce((a, b) => a + b, 0) /
|
|
1897
|
+
brightnessHistory.current.length;
|
|
1898
|
+
const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
|
|
1101
1899
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1900
|
+
setIsBrightnessLow(!isOverallBright);
|
|
1901
|
+
|
|
1902
|
+
// Check blur only in center region (area of interest) to avoid false positives
|
|
1903
|
+
// from iOS depth-of-field background blur
|
|
1904
|
+
let isNotBlurry = true;
|
|
1905
|
+
let isBlurry = false; // Track blur state for quality metrics
|
|
1104
1906
|
try {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1907
|
+
// Check blur in center 60% of frame (0.6 width x 0.6 height)
|
|
1908
|
+
// Center position: 50% x, 50% y
|
|
1909
|
+
isBlurry = await OpenCVModule.checkBlurryInRegion(
|
|
1910
|
+
base64Image,
|
|
1911
|
+
0.5, // centerXPercent
|
|
1912
|
+
0.5, // centerYPercent
|
|
1913
|
+
0.6, // widthPercent
|
|
1914
|
+
0.6, // heightPercent
|
|
1915
|
+
60 // threshold
|
|
1916
|
+
);
|
|
1917
|
+
isNotBlurry = !isBlurry;
|
|
1918
|
+
setIsFrameBlurry(isBlurry);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
setIsFrameBlurry(false);
|
|
1118
1921
|
}
|
|
1119
1922
|
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1923
|
+
// Only proceed if image quality is acceptable
|
|
1924
|
+
const hasAcceptableQuality = isOverallBright && isNotBlurry;
|
|
1925
|
+
|
|
1926
|
+
// Store quality metrics in ref for access in handleFaceAndText callback
|
|
1927
|
+
lastFrameQuality.current = {
|
|
1928
|
+
hasAcceptableQuality,
|
|
1929
|
+
isBlurry, // Use local variable, not state (which is from previous frame)
|
|
1930
|
+
brightness: avgBrightness,
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
if (!hasAcceptableQuality) {
|
|
1934
|
+
consecutiveQualityFailures.current++;
|
|
1935
|
+
// After max failures, allow capture to prevent indefinite waiting
|
|
1936
|
+
if (
|
|
1937
|
+
consecutiveQualityFailures.current < MAX_CONSECUTIVE_QUALITY_FAILURES
|
|
1938
|
+
) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
console.warn(
|
|
1942
|
+
'Max quality failures reached, proceeding with current frame'
|
|
1943
|
+
);
|
|
1944
|
+
} else {
|
|
1945
|
+
consecutiveQualityFailures.current = 0;
|
|
1129
1946
|
}
|
|
1130
1947
|
|
|
1131
|
-
// Barcode scanning with error handling
|
|
1132
|
-
let barcodes: any[] = [];
|
|
1133
1948
|
try {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1949
|
+
// Read faces directly from native ML Kit results
|
|
1950
|
+
let detectedFaces: Face[] = [];
|
|
1951
|
+
if (faceDetectionEnabled && frame.faces) {
|
|
1952
|
+
detectedFaces = frame.faces.map((f) => ({
|
|
1953
|
+
bounds: {
|
|
1954
|
+
x: f.bounds.x,
|
|
1955
|
+
y: f.bounds.y,
|
|
1956
|
+
width: f.bounds.width,
|
|
1957
|
+
height: f.bounds.height,
|
|
1958
|
+
},
|
|
1959
|
+
rollAngle: f.rollAngle,
|
|
1960
|
+
yawAngle: f.yawAngle,
|
|
1961
|
+
}));
|
|
1962
|
+
faceDetectionErrorCount.current = 0;
|
|
1963
|
+
}
|
|
1141
1964
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1965
|
+
// Read text directly from native ML Kit results
|
|
1966
|
+
const textBlocks = frame.textBlocks ?? [];
|
|
1967
|
+
const resultText = textBlocks.map((b) => b.text).join('\n');
|
|
1968
|
+
const scannedText: BlockText = {
|
|
1969
|
+
resultText,
|
|
1970
|
+
blocks: textBlocks.map((block) => ({
|
|
1971
|
+
blockText: block.text || '',
|
|
1972
|
+
blockFrame: block.blockFrame ?? {
|
|
1973
|
+
x: 0,
|
|
1974
|
+
y: 0,
|
|
1975
|
+
width: 0,
|
|
1976
|
+
height: 0,
|
|
1977
|
+
boundingCenterX: 0,
|
|
1978
|
+
boundingCenterY: 0,
|
|
1979
|
+
},
|
|
1980
|
+
blockCornerPoints: [] as unknown as CornerPointsType,
|
|
1981
|
+
lines: [] as unknown as LinesData,
|
|
1982
|
+
blockLanguages: [],
|
|
1983
|
+
})),
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
// Read barcodes directly from native ML Kit results
|
|
1987
|
+
let barcodes: Barcode[] = [];
|
|
1988
|
+
if (frame.barcodes) {
|
|
1989
|
+
barcodes = frame.barcodes.map((b) => ({
|
|
1990
|
+
rawValue: b.rawValue,
|
|
1991
|
+
displayValue: b.displayValue,
|
|
1992
|
+
format: b.format,
|
|
1993
|
+
boundingBox: b.boundingBox ?? {
|
|
1994
|
+
left: 0,
|
|
1995
|
+
top: 0,
|
|
1996
|
+
right: 0,
|
|
1997
|
+
bottom: 0,
|
|
1998
|
+
},
|
|
1999
|
+
cornerPoints: b.cornerPoints ?? [],
|
|
2000
|
+
}));
|
|
2001
|
+
|
|
2002
|
+
// Log barcode detection for debugging (only when scanning ID back)
|
|
2003
|
+
if (
|
|
2004
|
+
barcodes.length > 0 &&
|
|
2005
|
+
nextStep === 'SCAN_ID_BACK' &&
|
|
2006
|
+
isDebugEnabled()
|
|
2007
|
+
) {
|
|
2008
|
+
console.log(`[Barcode JS] Detected ${barcodes.length} barcode(s):`);
|
|
2009
|
+
barcodes.forEach((b, idx) => {
|
|
2010
|
+
const formatNames: { [key: number]: string } = {
|
|
2011
|
+
5: 'PDF417',
|
|
2012
|
+
64: 'QR_CODE',
|
|
2013
|
+
1: 'CODE_128',
|
|
2014
|
+
2: 'CODE_39',
|
|
2015
|
+
13: 'EAN_13',
|
|
2016
|
+
8: 'EAN_8',
|
|
2017
|
+
4096: 'AZTEC',
|
|
2018
|
+
16: 'DATA_MATRIX',
|
|
2019
|
+
};
|
|
2020
|
+
const formatName =
|
|
2021
|
+
formatNames[b.format] || `UNKNOWN(${b.format})`;
|
|
2022
|
+
console.log(
|
|
2023
|
+
` [${idx + 1}] ${formatName}: ${b.rawValue.substring(0, 50)}`
|
|
2024
|
+
);
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
1154
2028
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
2029
|
+
// Update all debug overlay bounds continuously when debug mode is enabled
|
|
2030
|
+
if (isDebugEnabled() && frameDimensions) {
|
|
2031
|
+
const screen = Dimensions.get('window');
|
|
2032
|
+
const frameAspect = frameDimensions.width / frameDimensions.height;
|
|
2033
|
+
const screenAspect = screen.width / screen.height;
|
|
1158
2034
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
2035
|
+
let scale: number;
|
|
2036
|
+
let offsetX = 0;
|
|
2037
|
+
let offsetY = 0;
|
|
1162
2038
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
} catch (error: any) {
|
|
1170
|
-
console.warn('iOS Frame processor error:', error?.message || error?.name || String(error));
|
|
2039
|
+
if (frameAspect > screenAspect) {
|
|
2040
|
+
scale = screen.height / frameDimensions.height;
|
|
2041
|
+
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
2042
|
+
} else {
|
|
2043
|
+
scale = screen.width / frameDimensions.width;
|
|
2044
|
+
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
1171
2045
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2046
|
+
|
|
2047
|
+
const scanLeft = (screen.width * 0.05 + offsetX) / scale;
|
|
2048
|
+
const scanTop = (screen.height * 0.36 + offsetY) / scale;
|
|
2049
|
+
const scanRight = (screen.width * 0.95 + offsetX) / scale;
|
|
2050
|
+
const scanBottom = (screen.height * 0.64 + offsetY) / scale;
|
|
2051
|
+
const isInsideScan = (x: number, y: number, w: number, h: number) =>
|
|
2052
|
+
x >= scanLeft &&
|
|
2053
|
+
y >= scanTop &&
|
|
2054
|
+
x + w <= scanRight &&
|
|
2055
|
+
y + h <= scanBottom;
|
|
2056
|
+
|
|
2057
|
+
// Update barcode bounds
|
|
2058
|
+
if (barcodes.length > 0 && barcodes[0]) {
|
|
2059
|
+
const bbox = barcodes[0].boundingBox;
|
|
2060
|
+
const corners = barcodes[0].cornerPoints;
|
|
2061
|
+
let angle = 0;
|
|
2062
|
+
|
|
2063
|
+
// Calculate angle from corner points if available
|
|
2064
|
+
if (corners && corners.length >= 2) {
|
|
2065
|
+
const transformedCorners = corners.map((c) => ({
|
|
2066
|
+
x: c.x * scale - offsetX,
|
|
2067
|
+
y: c.y * scale - offsetY,
|
|
2068
|
+
}));
|
|
2069
|
+
// Calculate angle from first two corners (bottom edge)
|
|
2070
|
+
const dx = transformedCorners[1].x - transformedCorners[0].x;
|
|
2071
|
+
const dy = transformedCorners[1].y - transformedCorners[0].y;
|
|
2072
|
+
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (isDebugEnabled()) {
|
|
2076
|
+
console.log('[Debug] Barcode detected:', { bbox, angle });
|
|
2077
|
+
}
|
|
2078
|
+
setBarcodeBounds({
|
|
2079
|
+
x: bbox.left * scale - offsetX,
|
|
2080
|
+
y: bbox.top * scale - offsetY,
|
|
2081
|
+
width: (bbox.right - bbox.left) * scale,
|
|
2082
|
+
height: (bbox.bottom - bbox.top) * scale,
|
|
2083
|
+
angle,
|
|
2084
|
+
corners: corners?.map((c) => ({
|
|
2085
|
+
x: c.x * scale - offsetX,
|
|
2086
|
+
y: c.y * scale - offsetY,
|
|
2087
|
+
})),
|
|
2088
|
+
});
|
|
2089
|
+
} else {
|
|
2090
|
+
setBarcodeBounds(null);
|
|
1181
2091
|
}
|
|
1182
|
-
|
|
2092
|
+
|
|
2093
|
+
// Update face bounds continuously
|
|
2094
|
+
if (detectedFaces.length > 0 && detectedFaces[0]) {
|
|
2095
|
+
const faceBounds = detectedFaces[0].bounds;
|
|
2096
|
+
const rollAngle = detectedFaces[0].rollAngle;
|
|
2097
|
+
const faceWidth = faceBounds.width * scale;
|
|
2098
|
+
const faceHeight = faceBounds.height * scale;
|
|
2099
|
+
const cropPadding = Math.max(faceWidth * 0.15, faceHeight * 0.15);
|
|
2100
|
+
setDocumentPlaneBounds({
|
|
2101
|
+
x: faceBounds.x * scale - offsetX,
|
|
2102
|
+
y: faceBounds.y * scale - offsetY,
|
|
2103
|
+
width: faceWidth,
|
|
2104
|
+
height: faceHeight,
|
|
2105
|
+
rollAngle,
|
|
2106
|
+
cropPadding,
|
|
2107
|
+
});
|
|
2108
|
+
} else {
|
|
2109
|
+
setDocumentPlaneBounds(null);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Update secondary face bounds
|
|
2113
|
+
if (detectedFaces.length > 1 && detectedFaces[1]) {
|
|
2114
|
+
const secondaryBounds = detectedFaces[1].bounds;
|
|
2115
|
+
const rollAngle = detectedFaces[1].rollAngle;
|
|
2116
|
+
const secondaryWidth = secondaryBounds.width * scale;
|
|
2117
|
+
const secondaryHeight = secondaryBounds.height * scale;
|
|
2118
|
+
const cropPadding = Math.max(
|
|
2119
|
+
secondaryWidth * 0.15,
|
|
2120
|
+
secondaryHeight * 0.15
|
|
2121
|
+
);
|
|
2122
|
+
if (
|
|
2123
|
+
isInsideScan(
|
|
2124
|
+
secondaryBounds.x,
|
|
2125
|
+
secondaryBounds.y,
|
|
2126
|
+
secondaryBounds.width,
|
|
2127
|
+
secondaryBounds.height
|
|
2128
|
+
)
|
|
2129
|
+
) {
|
|
2130
|
+
setSecondaryFaceBounds({
|
|
2131
|
+
x: secondaryBounds.x * scale - offsetX,
|
|
2132
|
+
y: secondaryBounds.y * scale - offsetY,
|
|
2133
|
+
width: secondaryWidth,
|
|
2134
|
+
height: secondaryHeight,
|
|
2135
|
+
rollAngle,
|
|
2136
|
+
cropPadding,
|
|
2137
|
+
});
|
|
2138
|
+
} else {
|
|
2139
|
+
setSecondaryFaceBounds(null);
|
|
2140
|
+
}
|
|
2141
|
+
} else {
|
|
2142
|
+
setSecondaryFaceBounds(null);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// Detect MRZ and signature text areas continuously
|
|
2146
|
+
if (textBlocks.length > 0) {
|
|
2147
|
+
console.log('[Debug] Text blocks count:', textBlocks.length);
|
|
2148
|
+
// Find MRZ-like text blocks (bottom area, contains MRZ-like characters)
|
|
2149
|
+
// More strict pattern: look for blocks with 8+ consecutive uppercase/numbers/< characters AND
|
|
2150
|
+
// must contain at least one '<' character (true MRZ characteristic)
|
|
2151
|
+
const mrzPattern = /[A-Z0-9<]{8,}.*</i;
|
|
2152
|
+
const bottomHalf = frame.height * 0.5; // Increased from 0.67 to catch more MRZ blocks
|
|
2153
|
+
|
|
2154
|
+
// Log bottom area blocks for debugging
|
|
2155
|
+
const bottomBlocks = textBlocks.filter(
|
|
2156
|
+
(block) => block.blockFrame && block.blockFrame.y > bottomHalf
|
|
2157
|
+
);
|
|
2158
|
+
if (bottomBlocks.length > 0) {
|
|
2159
|
+
console.log(
|
|
2160
|
+
'[Debug] Bottom area blocks:',
|
|
2161
|
+
bottomBlocks.map((b) => b.text.substring(0, 30))
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const mrzBlocks = textBlocks.filter(
|
|
2166
|
+
(block) =>
|
|
2167
|
+
block.blockFrame &&
|
|
2168
|
+
block.blockFrame.y > bottomHalf &&
|
|
2169
|
+
mrzPattern.test(block.text)
|
|
2170
|
+
);
|
|
2171
|
+
|
|
2172
|
+
console.log('[Debug] MRZ blocks found:', mrzBlocks.length);
|
|
2173
|
+
if (mrzBlocks.length > 0) {
|
|
2174
|
+
const minX = Math.min(...mrzBlocks.map((b) => b.blockFrame!.x));
|
|
2175
|
+
const minY = Math.min(...mrzBlocks.map((b) => b.blockFrame!.y));
|
|
2176
|
+
const maxX = Math.max(
|
|
2177
|
+
...mrzBlocks.map((b) => b.blockFrame!.x + b.blockFrame!.width)
|
|
2178
|
+
);
|
|
2179
|
+
const maxY = Math.max(
|
|
2180
|
+
...mrzBlocks.map((b) => b.blockFrame!.y + b.blockFrame!.height)
|
|
2181
|
+
);
|
|
2182
|
+
|
|
2183
|
+
// Collect all corner points from MRZ blocks
|
|
2184
|
+
const allCornerPoints = mrzBlocks
|
|
2185
|
+
.flatMap((b) => b.cornerPoints || [])
|
|
2186
|
+
.map((c) => ({
|
|
2187
|
+
x: c.x * scale - offsetX,
|
|
2188
|
+
y: c.y * scale - offsetY,
|
|
2189
|
+
}));
|
|
2190
|
+
|
|
2191
|
+
let angle = 0;
|
|
2192
|
+
if (allCornerPoints.length >= 2) {
|
|
2193
|
+
// Calculate angle from first two points
|
|
2194
|
+
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
2195
|
+
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
2196
|
+
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
console.log('[Debug] MRZ bounds:', {
|
|
2200
|
+
minX,
|
|
2201
|
+
minY,
|
|
2202
|
+
maxX,
|
|
2203
|
+
maxY,
|
|
2204
|
+
angle,
|
|
2205
|
+
});
|
|
2206
|
+
setMrzBounds({
|
|
2207
|
+
x: minX * scale - offsetX,
|
|
2208
|
+
y: minY * scale - offsetY,
|
|
2209
|
+
width: (maxX - minX) * scale,
|
|
2210
|
+
height: (maxY - minY) * scale,
|
|
2211
|
+
angle,
|
|
2212
|
+
corners:
|
|
2213
|
+
allCornerPoints.length > 0 ? allCornerPoints : undefined,
|
|
2214
|
+
});
|
|
2215
|
+
} else {
|
|
2216
|
+
setMrzBounds(null);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// Detect signature area
|
|
2220
|
+
const signaturePattern = /signature|imza|İmza/i;
|
|
2221
|
+
const signatureBlocks = textBlocks.filter(
|
|
2222
|
+
(block) => block.blockFrame && signaturePattern.test(block.text)
|
|
2223
|
+
);
|
|
2224
|
+
|
|
2225
|
+
if (textBlocks.length > 0 && signatureBlocks.length === 0) {
|
|
2226
|
+
console.log(
|
|
2227
|
+
`[Signature Debug] No signature blocks found. All blocks (${textBlocks.length}):`,
|
|
2228
|
+
textBlocks.map((b) => b.text).join(' | ')
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (signatureBlocks.length > 0) {
|
|
2233
|
+
const minX = Math.min(
|
|
2234
|
+
...signatureBlocks.map((b) => b.blockFrame!.x)
|
|
2235
|
+
);
|
|
2236
|
+
const minY = Math.min(
|
|
2237
|
+
...signatureBlocks.map((b) => b.blockFrame!.y)
|
|
2238
|
+
);
|
|
2239
|
+
const maxX = Math.max(
|
|
2240
|
+
...signatureBlocks.map(
|
|
2241
|
+
(b) => b.blockFrame!.x + b.blockFrame!.width
|
|
2242
|
+
)
|
|
2243
|
+
);
|
|
2244
|
+
const maxY = Math.max(
|
|
2245
|
+
...signatureBlocks.map(
|
|
2246
|
+
(b) => b.blockFrame!.y + b.blockFrame!.height
|
|
2247
|
+
)
|
|
2248
|
+
);
|
|
2249
|
+
|
|
2250
|
+
// Collect all corner points from signature blocks
|
|
2251
|
+
const allCornerPoints = signatureBlocks
|
|
2252
|
+
.flatMap((b) => b.cornerPoints || [])
|
|
2253
|
+
.map((c) => ({
|
|
2254
|
+
x: c.x * scale - offsetX,
|
|
2255
|
+
y: c.y * scale - offsetY,
|
|
2256
|
+
}));
|
|
2257
|
+
|
|
2258
|
+
let angle = 0;
|
|
2259
|
+
if (allCornerPoints.length >= 2) {
|
|
2260
|
+
// Calculate angle from first two points
|
|
2261
|
+
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
2262
|
+
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
2263
|
+
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
setSignatureBounds({
|
|
2267
|
+
x: minX * scale - offsetX,
|
|
2268
|
+
y: minY * scale - offsetY,
|
|
2269
|
+
width: (maxX - minX) * scale,
|
|
2270
|
+
height: (maxY - minY) * scale,
|
|
2271
|
+
angle,
|
|
2272
|
+
corners:
|
|
2273
|
+
allCornerPoints.length > 0 ? allCornerPoints : undefined,
|
|
2274
|
+
});
|
|
2275
|
+
} else {
|
|
2276
|
+
setSignatureBounds(null);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// Check if all required elements are detected based on document type
|
|
2280
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
2281
|
+
// ID Back: MRZ + barcode (barcode optional but preferred)
|
|
2282
|
+
const hasMRZ = mrzBlocks.length > 0;
|
|
2283
|
+
const hasBarcode =
|
|
2284
|
+
barcodes.length > 0 || cachedBarcode.current !== null;
|
|
2285
|
+
const allPresent = hasMRZ && hasBarcode;
|
|
2286
|
+
setAllElementsDetected(allPresent);
|
|
2287
|
+
|
|
2288
|
+
// Don't block based on bounds - allow elements even if slightly outside
|
|
2289
|
+
setElementsOutsideScanArea([]);
|
|
2290
|
+
|
|
2291
|
+
if (!allPresent) {
|
|
2292
|
+
const missing = [];
|
|
2293
|
+
if (!hasMRZ) missing.push('MRZ');
|
|
2294
|
+
if (!hasBarcode) missing.push('Barcode');
|
|
2295
|
+
console.log(
|
|
2296
|
+
`[Frame Check] Missing elements: ${missing.join(', ')}`
|
|
2297
|
+
);
|
|
2298
|
+
} else {
|
|
2299
|
+
console.log('[Frame Check] ✓ All elements detected in frame');
|
|
2300
|
+
}
|
|
2301
|
+
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
2302
|
+
// Check if it's passport (has MRZ) or ID front (no MRZ)
|
|
2303
|
+
const hasMRZ = mrzBlocks.length > 0;
|
|
2304
|
+
const hasFace = detectedFaces.length > 0;
|
|
2305
|
+
const hasSignature = signatureBlocks.length > 0;
|
|
2306
|
+
|
|
2307
|
+
// Don't block based on bounds - allow elements even if slightly outside
|
|
2308
|
+
setElementsOutsideScanArea([]);
|
|
2309
|
+
|
|
2310
|
+
let allPresent = false;
|
|
2311
|
+
if (hasMRZ) {
|
|
2312
|
+
// Passport: face + MRZ
|
|
2313
|
+
allPresent = hasFace && hasMRZ;
|
|
2314
|
+
if (!allPresent) {
|
|
2315
|
+
const missing = [];
|
|
2316
|
+
if (!hasFace) missing.push('Face');
|
|
2317
|
+
if (!hasMRZ) missing.push('MRZ');
|
|
2318
|
+
console.log(
|
|
2319
|
+
`[Frame Check] Passport - Missing elements: ${missing.join(', ')}`
|
|
2320
|
+
);
|
|
2321
|
+
} else {
|
|
2322
|
+
console.log(
|
|
2323
|
+
'[Frame Check] ✓ Passport - All elements detected (face + MRZ)'
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
} else {
|
|
2327
|
+
// ID Front: face + signature
|
|
2328
|
+
allPresent = hasFace && hasSignature;
|
|
2329
|
+
if (!allPresent) {
|
|
2330
|
+
const missing = [];
|
|
2331
|
+
if (!hasFace) missing.push('Face');
|
|
2332
|
+
if (!hasSignature) missing.push('Signature');
|
|
2333
|
+
console.log(
|
|
2334
|
+
`[Frame Check] ID Front - Missing elements: ${missing.join(', ')}`
|
|
2335
|
+
);
|
|
2336
|
+
} else {
|
|
2337
|
+
console.log(
|
|
2338
|
+
'[Frame Check] ✓ ID Front - All elements detected (face + signature)'
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
setAllElementsDetected(allPresent);
|
|
2344
|
+
} else {
|
|
2345
|
+
setAllElementsDetected(false);
|
|
2346
|
+
setElementsOutsideScanArea([]);
|
|
2347
|
+
}
|
|
2348
|
+
} else {
|
|
2349
|
+
setMrzBounds(null);
|
|
2350
|
+
setSignatureBounds(null);
|
|
2351
|
+
setAllElementsDetected(false);
|
|
2352
|
+
setElementsOutsideScanArea([]);
|
|
2353
|
+
}
|
|
2354
|
+
} else if (!isDebugEnabled()) {
|
|
2355
|
+
// Clear all bounds when debug mode is disabled
|
|
2356
|
+
setBarcodeBounds(null);
|
|
2357
|
+
setDocumentPlaneBounds(null);
|
|
2358
|
+
setSecondaryFaceBounds(null);
|
|
2359
|
+
setMrzBounds(null);
|
|
2360
|
+
setSignatureBounds(null);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// Update allElementsDetected for status text display (regardless of debug mode)
|
|
2364
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
2365
|
+
const hasMRZ = textBlocks.some((b) =>
|
|
2366
|
+
/[A-Z0-9<]{8,}.*</i.test(b.text)
|
|
2367
|
+
);
|
|
2368
|
+
const hasBarcode =
|
|
2369
|
+
barcodes.length > 0 || cachedBarcode.current !== null;
|
|
2370
|
+
setAllElementsDetected(hasMRZ && hasBarcode);
|
|
2371
|
+
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
2372
|
+
const hasMRZ = textBlocks.some((b) =>
|
|
2373
|
+
/[A-Z0-9<]{8,}.*</i.test(b.text)
|
|
2374
|
+
);
|
|
2375
|
+
const hasFace = detectedFaces.length > 0;
|
|
2376
|
+
const hasSignature = textBlocks.some((b) =>
|
|
2377
|
+
/signature|imza|İmza/i.test(b.text)
|
|
2378
|
+
);
|
|
2379
|
+
setAllElementsDetected(
|
|
2380
|
+
hasMRZ ? hasFace && hasMRZ : hasFace && hasSignature
|
|
2381
|
+
);
|
|
2382
|
+
} else {
|
|
2383
|
+
setAllElementsDetected(false);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// Check if detected elements are inside the scan area
|
|
2387
|
+
const scanScreen = Dimensions.get('window');
|
|
2388
|
+
const scanFrameAspect = frame.width / frame.height;
|
|
2389
|
+
const scanScreenAspect = scanScreen.width / scanScreen.height;
|
|
2390
|
+
let scanScale: number;
|
|
2391
|
+
let scanOffsetX = 0;
|
|
2392
|
+
let scanOffsetY = 0;
|
|
2393
|
+
if (scanFrameAspect > scanScreenAspect) {
|
|
2394
|
+
scanScale = scanScreen.height / frame.height;
|
|
2395
|
+
scanOffsetX = (frame.width * scanScale - scanScreen.width) / 2;
|
|
2396
|
+
} else {
|
|
2397
|
+
scanScale = scanScreen.width / frame.width;
|
|
2398
|
+
scanOffsetY = (frame.height * scanScale - scanScreen.height) / 2;
|
|
2399
|
+
}
|
|
2400
|
+
const scanLeft = (scanScreen.width * 0.05 + scanOffsetX) / scanScale;
|
|
2401
|
+
const scanTop = (scanScreen.height * 0.36 + scanOffsetY) / scanScale;
|
|
2402
|
+
const scanRight = (scanScreen.width * 0.95 + scanOffsetX) / scanScale;
|
|
2403
|
+
const scanBottom = (scanScreen.height * 0.64 + scanOffsetY) / scanScale;
|
|
2404
|
+
|
|
2405
|
+
const isInsideScan = (x: number, y: number, w: number, h: number) =>
|
|
2406
|
+
x >= scanLeft &&
|
|
2407
|
+
y >= scanTop &&
|
|
2408
|
+
x + w <= scanRight &&
|
|
2409
|
+
y + h <= scanBottom;
|
|
2410
|
+
|
|
2411
|
+
const outsideElements: string[] = [];
|
|
2412
|
+
|
|
2413
|
+
// Collect all detected element bounds
|
|
2414
|
+
const allBounds: Array<{
|
|
2415
|
+
x: number;
|
|
2416
|
+
y: number;
|
|
2417
|
+
x2: number;
|
|
2418
|
+
y2: number;
|
|
2419
|
+
}> = [];
|
|
2420
|
+
const primaryFace = detectedFaces[0];
|
|
2421
|
+
if (primaryFace) {
|
|
2422
|
+
if (
|
|
2423
|
+
primaryFace.bounds.width >= frame.width * 0.05 &&
|
|
2424
|
+
primaryFace.bounds.height >= frame.width * 0.05
|
|
2425
|
+
) {
|
|
2426
|
+
allBounds.push({
|
|
2427
|
+
x: primaryFace.bounds.x,
|
|
2428
|
+
y: primaryFace.bounds.y,
|
|
2429
|
+
x2: primaryFace.bounds.x + primaryFace.bounds.width,
|
|
2430
|
+
y2: primaryFace.bounds.y + primaryFace.bounds.height,
|
|
2431
|
+
});
|
|
2432
|
+
if (
|
|
2433
|
+
!isInsideScan(
|
|
2434
|
+
primaryFace.bounds.x,
|
|
2435
|
+
primaryFace.bounds.y,
|
|
2436
|
+
primaryFace.bounds.width,
|
|
2437
|
+
primaryFace.bounds.height
|
|
2438
|
+
)
|
|
2439
|
+
) {
|
|
2440
|
+
outsideElements.push('face');
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const block of textBlocks) {
|
|
2445
|
+
if (block.blockFrame) {
|
|
2446
|
+
const bf = block.blockFrame;
|
|
2447
|
+
if (bf.width > 0 && bf.height > 0) {
|
|
2448
|
+
allBounds.push({
|
|
2449
|
+
x: bf.x,
|
|
2450
|
+
y: bf.y,
|
|
2451
|
+
x2: bf.x + bf.width,
|
|
2452
|
+
y2: bf.y + bf.height,
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
const isMRZ = /[A-Z0-9<]{8,}.*</i.test(block.text);
|
|
2456
|
+
const isSignature = /signature|imza|İmza/i.test(block.text);
|
|
2457
|
+
if (
|
|
2458
|
+
(isMRZ || isSignature) &&
|
|
2459
|
+
!isInsideScan(bf.x, bf.y, bf.width, bf.height)
|
|
2460
|
+
) {
|
|
2461
|
+
outsideElements.push('text');
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
for (const bc of barcodes) {
|
|
2466
|
+
if (bc.boundingBox) {
|
|
2467
|
+
const bb = bc.boundingBox;
|
|
2468
|
+
if (
|
|
2469
|
+
!isInsideScan(
|
|
2470
|
+
bb.left,
|
|
2471
|
+
bb.top,
|
|
2472
|
+
bb.right - bb.left,
|
|
2473
|
+
bb.bottom - bb.top
|
|
2474
|
+
)
|
|
2475
|
+
) {
|
|
2476
|
+
outsideElements.push('barcode');
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// Check that detected content spans enough of the scan area horizontally and vertically
|
|
2482
|
+
// This catches cases where one side of the card is off-screen (elements on that side won't be detected)
|
|
2483
|
+
if (allBounds.length > 0 && outsideElements.length === 0) {
|
|
2484
|
+
const minX = Math.min(...allBounds.map((b) => b.x));
|
|
2485
|
+
const maxX = Math.max(...allBounds.map((b) => b.x2));
|
|
2486
|
+
const minY = Math.min(...allBounds.map((b) => b.y));
|
|
2487
|
+
const maxY = Math.max(...allBounds.map((b) => b.y2));
|
|
2488
|
+
const spanWidth = maxX - minX;
|
|
2489
|
+
const spanHeight = maxY - minY;
|
|
2490
|
+
const scanWidth = scanRight - scanLeft;
|
|
2491
|
+
const scanHeight = scanBottom - scanTop;
|
|
2492
|
+
// Require content to span at least 55% of scan area in both dimensions
|
|
2493
|
+
if (spanWidth < scanWidth * 0.55 || spanHeight < scanHeight * 0.55) {
|
|
2494
|
+
outsideElements.push('span');
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
setElementsOutsideScanArea(outsideElements);
|
|
2499
|
+
|
|
2500
|
+
handleFaceAndText(
|
|
2501
|
+
scannedText.resultText ?? '',
|
|
2502
|
+
detectedFaces,
|
|
2503
|
+
frame.width,
|
|
2504
|
+
frame.height,
|
|
2505
|
+
barcodes.length ? barcodes[0] : undefined,
|
|
2506
|
+
base64Image,
|
|
2507
|
+
outsideElements.length > 0,
|
|
2508
|
+
frame.mrzResult
|
|
2509
|
+
);
|
|
2510
|
+
} catch (error: any) {
|
|
2511
|
+
console.warn('Frame processing error:', error?.message);
|
|
1183
2512
|
}
|
|
1184
2513
|
},
|
|
1185
|
-
[handleFaceAndText,
|
|
2514
|
+
[faceDetectionEnabled, frameDimensions, handleFaceAndText, nextStep]
|
|
2515
|
+
);
|
|
2516
|
+
|
|
2517
|
+
const handleCameraReady = useCallback(
|
|
2518
|
+
(
|
|
2519
|
+
_event: NativeSyntheticEvent<{
|
|
2520
|
+
minExposureOffset: number;
|
|
2521
|
+
maxExposureOffset: number;
|
|
2522
|
+
}>
|
|
2523
|
+
) => {
|
|
2524
|
+
isCameraInitialized.current = true;
|
|
2525
|
+
},
|
|
2526
|
+
[]
|
|
2527
|
+
);
|
|
2528
|
+
|
|
2529
|
+
const handleCameraError = useCallback(
|
|
2530
|
+
(event: NativeSyntheticEvent<{ error: string }>) => {
|
|
2531
|
+
console.error('Camera error:', event.nativeEvent.error);
|
|
2532
|
+
},
|
|
2533
|
+
[]
|
|
1186
2534
|
);
|
|
1187
2535
|
|
|
1188
2536
|
if (!permissionsRequested) {
|
|
1189
2537
|
return (
|
|
1190
2538
|
<SafeAreaView style={styles.permissionContainer}>
|
|
2539
|
+
<StatusBar barStyle="dark-content" />
|
|
1191
2540
|
<ActivityIndicator size="large" color={theme.colors.primary} />
|
|
1192
2541
|
</SafeAreaView>
|
|
1193
2542
|
);
|
|
1194
2543
|
}
|
|
1195
2544
|
|
|
1196
|
-
if (!
|
|
2545
|
+
if (!hasPermission) {
|
|
1197
2546
|
return (
|
|
1198
2547
|
<SafeAreaView style={styles.permissionContainer}>
|
|
1199
|
-
<
|
|
2548
|
+
<StatusBar barStyle="dark-content" />
|
|
2549
|
+
<TextView style={styles.permissionText}>
|
|
1200
2550
|
{t('general.noCameraPermissionGiven')}
|
|
1201
|
-
</
|
|
2551
|
+
</TextView>
|
|
1202
2552
|
<StyledButton
|
|
1203
2553
|
mode="contained"
|
|
1204
2554
|
onPress={() => {
|
|
@@ -1211,32 +2561,13 @@ const IdentityDocumentCamera = ({
|
|
|
1211
2561
|
);
|
|
1212
2562
|
}
|
|
1213
2563
|
|
|
1214
|
-
if (device == null) {
|
|
1215
|
-
return (
|
|
1216
|
-
<SafeAreaView style={styles.permissionContainer}>
|
|
1217
|
-
<TextView style={styles.permissionText}>
|
|
1218
|
-
{t('general.noCameraDetected')}
|
|
1219
|
-
</TextView>
|
|
1220
|
-
</SafeAreaView>
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const handleFocus = async (event: GestureResponderEvent) => {
|
|
1225
|
-
if (cameraRef.current && device.supportsFocus) {
|
|
1226
|
-
try {
|
|
1227
|
-
const { locationX, locationY } = event.nativeEvent;
|
|
1228
|
-
await cameraRef.current.focus({
|
|
1229
|
-
x: locationX,
|
|
1230
|
-
y: locationY,
|
|
1231
|
-
});
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
// console.log('Error while focusing:', error);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
};
|
|
1237
|
-
|
|
1238
2564
|
return (
|
|
1239
2565
|
<View style={StyleSheet.absoluteFill}>
|
|
2566
|
+
<StatusBar
|
|
2567
|
+
barStyle="light-content"
|
|
2568
|
+
backgroundColor="transparent"
|
|
2569
|
+
translucent
|
|
2570
|
+
/>
|
|
1240
2571
|
{!hasGuideShown ? (
|
|
1241
2572
|
<SafeAreaView style={styles.guide}>
|
|
1242
2573
|
<LottieView
|
|
@@ -1273,52 +2604,513 @@ const IdentityDocumentCamera = ({
|
|
|
1273
2604
|
</SafeAreaView>
|
|
1274
2605
|
) : (
|
|
1275
2606
|
<>
|
|
1276
|
-
<
|
|
2607
|
+
<TrustchexCamera
|
|
1277
2608
|
ref={cameraRef}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}}
|
|
2609
|
+
style={StyleSheet.absoluteFill as ViewStyle}
|
|
2610
|
+
cameraType="back"
|
|
2611
|
+
enableFrameProcessing={isActive}
|
|
2612
|
+
enableFaceDetection={isActive && faceDetectionEnabled}
|
|
2613
|
+
enableTextRecognition={isActive}
|
|
2614
|
+
enableMrzValidation={isActive}
|
|
2615
|
+
enableBarcodeScanning={isActive && nextStep === 'SCAN_ID_BACK'}
|
|
2616
|
+
includeBase64={isActive}
|
|
2617
|
+
targetFps={10}
|
|
2618
|
+
torchEnabled={isTorchOn}
|
|
2619
|
+
onFrameAvailable={handleFrame}
|
|
2620
|
+
onCameraReady={handleCameraReady}
|
|
2621
|
+
onCameraError={handleCameraError}
|
|
1292
2622
|
/>
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
{
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
:
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
2623
|
+
{isDebugEnabled() &&
|
|
2624
|
+
documentPlaneBounds &&
|
|
2625
|
+
nextStep !== 'COMPLETED' && (
|
|
2626
|
+
<>
|
|
2627
|
+
{/* Crop area border (with padding) */}
|
|
2628
|
+
{!!documentPlaneBounds.cropPadding && (
|
|
2629
|
+
<View
|
|
2630
|
+
style={{
|
|
2631
|
+
position: 'absolute',
|
|
2632
|
+
left:
|
|
2633
|
+
documentPlaneBounds.x - documentPlaneBounds.cropPadding,
|
|
2634
|
+
top:
|
|
2635
|
+
documentPlaneBounds.y - documentPlaneBounds.cropPadding,
|
|
2636
|
+
width:
|
|
2637
|
+
documentPlaneBounds.width +
|
|
2638
|
+
2 * documentPlaneBounds.cropPadding,
|
|
2639
|
+
height:
|
|
2640
|
+
documentPlaneBounds.height +
|
|
2641
|
+
2 * documentPlaneBounds.cropPadding,
|
|
2642
|
+
borderWidth: 2,
|
|
2643
|
+
borderColor: 'rgba(76, 175, 80, 0.5)',
|
|
2644
|
+
borderStyle: 'dashed',
|
|
2645
|
+
borderRadius: 8,
|
|
2646
|
+
backgroundColor: 'transparent',
|
|
2647
|
+
transform: [
|
|
2648
|
+
{
|
|
2649
|
+
rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg`,
|
|
2650
|
+
},
|
|
2651
|
+
],
|
|
2652
|
+
transformOrigin: 'center',
|
|
2653
|
+
}}
|
|
2654
|
+
/>
|
|
2655
|
+
)}
|
|
2656
|
+
{/* Actual face border */}
|
|
2657
|
+
<View
|
|
2658
|
+
style={{
|
|
2659
|
+
position: 'absolute',
|
|
2660
|
+
left: documentPlaneBounds.x,
|
|
2661
|
+
top: documentPlaneBounds.y,
|
|
2662
|
+
width: documentPlaneBounds.width,
|
|
2663
|
+
height: documentPlaneBounds.height,
|
|
2664
|
+
borderWidth: 3,
|
|
2665
|
+
borderColor: '#4CAF50',
|
|
2666
|
+
borderRadius: 8,
|
|
2667
|
+
backgroundColor: 'transparent',
|
|
2668
|
+
transform: [
|
|
2669
|
+
{ rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg` },
|
|
2670
|
+
],
|
|
2671
|
+
transformOrigin: 'center',
|
|
2672
|
+
}}
|
|
2673
|
+
>
|
|
2674
|
+
{!!documentPlaneBounds.rollAngle &&
|
|
2675
|
+
Math.abs(documentPlaneBounds.rollAngle) > 5 && (
|
|
2676
|
+
<TextView
|
|
2677
|
+
style={{
|
|
2678
|
+
position: 'absolute',
|
|
2679
|
+
top: -20,
|
|
2680
|
+
left: 0,
|
|
2681
|
+
color: '#4CAF50',
|
|
2682
|
+
fontSize: 10,
|
|
2683
|
+
fontWeight: 'bold',
|
|
2684
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2685
|
+
paddingHorizontal: 4,
|
|
2686
|
+
paddingVertical: 2,
|
|
2687
|
+
borderRadius: 2,
|
|
2688
|
+
}}
|
|
2689
|
+
>
|
|
2690
|
+
{documentPlaneBounds.rollAngle.toFixed(1)}°
|
|
2691
|
+
</TextView>
|
|
2692
|
+
)}
|
|
2693
|
+
</View>
|
|
2694
|
+
</>
|
|
2695
|
+
)}
|
|
2696
|
+
{isDebugEnabled() &&
|
|
2697
|
+
secondaryFaceBounds &&
|
|
2698
|
+
nextStep !== 'COMPLETED' && (
|
|
2699
|
+
<>
|
|
2700
|
+
{/* Crop area border (with padding) */}
|
|
2701
|
+
{!!secondaryFaceBounds.cropPadding && (
|
|
2702
|
+
<View
|
|
2703
|
+
style={{
|
|
2704
|
+
position: 'absolute',
|
|
2705
|
+
left:
|
|
2706
|
+
secondaryFaceBounds.x - secondaryFaceBounds.cropPadding,
|
|
2707
|
+
top:
|
|
2708
|
+
secondaryFaceBounds.y - secondaryFaceBounds.cropPadding,
|
|
2709
|
+
width:
|
|
2710
|
+
secondaryFaceBounds.width +
|
|
2711
|
+
2 * secondaryFaceBounds.cropPadding,
|
|
2712
|
+
height:
|
|
2713
|
+
secondaryFaceBounds.height +
|
|
2714
|
+
2 * secondaryFaceBounds.cropPadding,
|
|
2715
|
+
borderWidth: 2,
|
|
2716
|
+
borderColor: 'rgba(33, 150, 243, 0.5)',
|
|
2717
|
+
borderStyle: 'dashed',
|
|
2718
|
+
borderRadius: 8,
|
|
2719
|
+
backgroundColor: 'transparent',
|
|
2720
|
+
transform: [
|
|
2721
|
+
{
|
|
2722
|
+
rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg`,
|
|
2723
|
+
},
|
|
2724
|
+
],
|
|
2725
|
+
transformOrigin: 'center',
|
|
2726
|
+
}}
|
|
2727
|
+
/>
|
|
2728
|
+
)}
|
|
2729
|
+
{/* Actual face border */}
|
|
2730
|
+
<View
|
|
2731
|
+
style={{
|
|
2732
|
+
position: 'absolute',
|
|
2733
|
+
left: secondaryFaceBounds.x,
|
|
2734
|
+
top: secondaryFaceBounds.y,
|
|
2735
|
+
width: secondaryFaceBounds.width,
|
|
2736
|
+
height: secondaryFaceBounds.height,
|
|
2737
|
+
borderWidth: 3,
|
|
2738
|
+
borderColor: '#2196F3',
|
|
2739
|
+
borderRadius: 8,
|
|
2740
|
+
backgroundColor: 'transparent',
|
|
2741
|
+
transform: [
|
|
2742
|
+
{ rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg` },
|
|
2743
|
+
],
|
|
2744
|
+
transformOrigin: 'center',
|
|
2745
|
+
}}
|
|
2746
|
+
>
|
|
2747
|
+
{!!secondaryFaceBounds.rollAngle &&
|
|
2748
|
+
Math.abs(secondaryFaceBounds.rollAngle) > 5 && (
|
|
2749
|
+
<TextView
|
|
2750
|
+
style={{
|
|
2751
|
+
position: 'absolute',
|
|
2752
|
+
top: -20,
|
|
2753
|
+
left: 0,
|
|
2754
|
+
color: '#2196F3',
|
|
2755
|
+
fontSize: 10,
|
|
2756
|
+
fontWeight: 'bold',
|
|
2757
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2758
|
+
paddingHorizontal: 4,
|
|
2759
|
+
paddingVertical: 2,
|
|
2760
|
+
borderRadius: 2,
|
|
2761
|
+
}}
|
|
2762
|
+
>
|
|
2763
|
+
{secondaryFaceBounds.rollAngle.toFixed(1)}°
|
|
2764
|
+
</TextView>
|
|
2765
|
+
)}
|
|
2766
|
+
</View>
|
|
2767
|
+
</>
|
|
1313
2768
|
)}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
2769
|
+
{isDebugEnabled() && barcodeBounds && nextStep !== 'COMPLETED' && (
|
|
2770
|
+
<>
|
|
2771
|
+
{barcodeBounds.corners && barcodeBounds.corners.length >= 4 ? (
|
|
2772
|
+
// Render using corner points for precise rotated border
|
|
2773
|
+
<>
|
|
2774
|
+
{/* Draw border lines between corners */}
|
|
2775
|
+
{[0, 1, 2, 3].map((i) => {
|
|
2776
|
+
const start = barcodeBounds.corners![i];
|
|
2777
|
+
const end = barcodeBounds.corners![(i + 1) % 4];
|
|
2778
|
+
const dx = end.x - start.x;
|
|
2779
|
+
const dy = end.y - start.y;
|
|
2780
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2781
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2782
|
+
|
|
2783
|
+
return (
|
|
2784
|
+
<View
|
|
2785
|
+
key={i}
|
|
2786
|
+
style={{
|
|
2787
|
+
position: 'absolute',
|
|
2788
|
+
left: start.x,
|
|
2789
|
+
top: start.y,
|
|
2790
|
+
width: length,
|
|
2791
|
+
height: 3,
|
|
2792
|
+
backgroundColor: '#FF9800',
|
|
2793
|
+
transform: [{ rotate: `${angle}deg` }],
|
|
2794
|
+
transformOrigin: 'top left',
|
|
2795
|
+
}}
|
|
2796
|
+
/>
|
|
2797
|
+
);
|
|
2798
|
+
})}
|
|
2799
|
+
{/* Draw corner markers */}
|
|
2800
|
+
{barcodeBounds.corners.map((corner, idx) => (
|
|
2801
|
+
<View
|
|
2802
|
+
key={`corner-${idx}`}
|
|
2803
|
+
style={{
|
|
2804
|
+
position: 'absolute',
|
|
2805
|
+
left: corner.x - 4,
|
|
2806
|
+
top: corner.y - 4,
|
|
2807
|
+
width: 8,
|
|
2808
|
+
height: 8,
|
|
2809
|
+
borderRadius: 4,
|
|
2810
|
+
backgroundColor: '#FF9800',
|
|
2811
|
+
}}
|
|
2812
|
+
/>
|
|
2813
|
+
))}
|
|
2814
|
+
{/* Angle indicator */}
|
|
2815
|
+
{!!barcodeBounds.angle &&
|
|
2816
|
+
Math.abs(barcodeBounds.angle) > 5 && (
|
|
2817
|
+
<TextView
|
|
2818
|
+
style={{
|
|
2819
|
+
position: 'absolute',
|
|
2820
|
+
left: barcodeBounds.x,
|
|
2821
|
+
top: barcodeBounds.y - 20,
|
|
2822
|
+
color: '#FF9800',
|
|
2823
|
+
fontSize: 10,
|
|
2824
|
+
fontWeight: 'bold',
|
|
2825
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2826
|
+
paddingHorizontal: 4,
|
|
2827
|
+
paddingVertical: 2,
|
|
2828
|
+
borderRadius: 2,
|
|
2829
|
+
}}
|
|
2830
|
+
>
|
|
2831
|
+
{barcodeBounds.angle.toFixed(1)}°
|
|
2832
|
+
</TextView>
|
|
2833
|
+
)}
|
|
2834
|
+
</>
|
|
2835
|
+
) : (
|
|
2836
|
+
// Fallback to rotated rectangle if corners not available
|
|
2837
|
+
<View
|
|
2838
|
+
style={{
|
|
2839
|
+
position: 'absolute',
|
|
2840
|
+
left: barcodeBounds.x + barcodeBounds.width / 2,
|
|
2841
|
+
top: barcodeBounds.y + barcodeBounds.height / 2,
|
|
2842
|
+
width: barcodeBounds.width,
|
|
2843
|
+
height: barcodeBounds.height,
|
|
2844
|
+
marginLeft: -barcodeBounds.width / 2,
|
|
2845
|
+
marginTop: -barcodeBounds.height / 2,
|
|
2846
|
+
borderWidth: 3,
|
|
2847
|
+
borderColor: '#FF9800',
|
|
2848
|
+
borderRadius: 8,
|
|
2849
|
+
backgroundColor: 'transparent',
|
|
2850
|
+
transform: [{ rotate: `${barcodeBounds.angle || 0}deg` }],
|
|
2851
|
+
}}
|
|
2852
|
+
>
|
|
2853
|
+
{!!barcodeBounds.angle &&
|
|
2854
|
+
Math.abs(barcodeBounds.angle) > 5 && (
|
|
2855
|
+
<TextView
|
|
2856
|
+
style={{
|
|
2857
|
+
position: 'absolute',
|
|
2858
|
+
top: -20,
|
|
2859
|
+
left: 0,
|
|
2860
|
+
color: '#FF9800',
|
|
2861
|
+
fontSize: 10,
|
|
2862
|
+
fontWeight: 'bold',
|
|
2863
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2864
|
+
paddingHorizontal: 4,
|
|
2865
|
+
paddingVertical: 2,
|
|
2866
|
+
borderRadius: 2,
|
|
2867
|
+
}}
|
|
2868
|
+
>
|
|
2869
|
+
{barcodeBounds.angle.toFixed(1)}°
|
|
2870
|
+
</TextView>
|
|
2871
|
+
)}
|
|
2872
|
+
</View>
|
|
2873
|
+
)}
|
|
2874
|
+
</>
|
|
2875
|
+
)}
|
|
2876
|
+
{isDebugEnabled() && mrzBounds && nextStep !== 'COMPLETED' && (
|
|
2877
|
+
<>
|
|
2878
|
+
{mrzBounds.corners && mrzBounds.corners.length >= 2 ? (
|
|
2879
|
+
// Render using corner points for precise rotated border
|
|
2880
|
+
<>
|
|
2881
|
+
{/* Draw border lines between consecutive corners */}
|
|
2882
|
+
{mrzBounds.corners.map((corner, idx) => {
|
|
2883
|
+
const nextCorner =
|
|
2884
|
+
mrzBounds.corners![(idx + 1) % mrzBounds.corners!.length];
|
|
2885
|
+
const dx = nextCorner.x - corner.x;
|
|
2886
|
+
const dy = nextCorner.y - corner.y;
|
|
2887
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2888
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2889
|
+
|
|
2890
|
+
return (
|
|
2891
|
+
<View
|
|
2892
|
+
key={idx}
|
|
2893
|
+
style={{
|
|
2894
|
+
position: 'absolute',
|
|
2895
|
+
left: corner.x,
|
|
2896
|
+
top: corner.y,
|
|
2897
|
+
width: length,
|
|
2898
|
+
height: 3,
|
|
2899
|
+
backgroundColor: '#9C27B0',
|
|
2900
|
+
transform: [{ rotate: `${angle}deg` }],
|
|
2901
|
+
transformOrigin: 'top left',
|
|
2902
|
+
}}
|
|
2903
|
+
/>
|
|
2904
|
+
);
|
|
2905
|
+
})}
|
|
2906
|
+
{/* Angle indicator */}
|
|
2907
|
+
{!!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && (
|
|
2908
|
+
<TextView
|
|
2909
|
+
style={{
|
|
2910
|
+
position: 'absolute',
|
|
2911
|
+
left: mrzBounds.x,
|
|
2912
|
+
top: mrzBounds.y - 20,
|
|
2913
|
+
color: '#9C27B0',
|
|
2914
|
+
fontSize: 10,
|
|
2915
|
+
fontWeight: 'bold',
|
|
2916
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2917
|
+
paddingHorizontal: 4,
|
|
2918
|
+
paddingVertical: 2,
|
|
2919
|
+
borderRadius: 2,
|
|
2920
|
+
}}
|
|
2921
|
+
>
|
|
2922
|
+
{mrzBounds.angle.toFixed(1)}°
|
|
2923
|
+
</TextView>
|
|
2924
|
+
)}
|
|
2925
|
+
</>
|
|
2926
|
+
) : (
|
|
2927
|
+
// Fallback to rotated rectangle if corners not available
|
|
2928
|
+
<View
|
|
2929
|
+
style={{
|
|
2930
|
+
position: 'absolute',
|
|
2931
|
+
left: mrzBounds.x + mrzBounds.width / 2,
|
|
2932
|
+
top: mrzBounds.y + mrzBounds.height / 2,
|
|
2933
|
+
width: mrzBounds.width,
|
|
2934
|
+
height: mrzBounds.height,
|
|
2935
|
+
marginLeft: -mrzBounds.width / 2,
|
|
2936
|
+
marginTop: -mrzBounds.height / 2,
|
|
2937
|
+
borderWidth: 3,
|
|
2938
|
+
borderColor: '#9C27B0',
|
|
2939
|
+
borderRadius: 8,
|
|
2940
|
+
backgroundColor: 'transparent',
|
|
2941
|
+
transform: [{ rotate: `${mrzBounds.angle || 0}deg` }],
|
|
2942
|
+
}}
|
|
2943
|
+
>
|
|
2944
|
+
{!!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && (
|
|
2945
|
+
<TextView
|
|
2946
|
+
style={{
|
|
2947
|
+
position: 'absolute',
|
|
2948
|
+
top: -20,
|
|
2949
|
+
left: 0,
|
|
2950
|
+
color: '#9C27B0',
|
|
2951
|
+
fontSize: 10,
|
|
2952
|
+
fontWeight: 'bold',
|
|
2953
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2954
|
+
paddingHorizontal: 4,
|
|
2955
|
+
paddingVertical: 2,
|
|
2956
|
+
borderRadius: 2,
|
|
2957
|
+
}}
|
|
2958
|
+
>
|
|
2959
|
+
{mrzBounds.angle.toFixed(1)}°
|
|
2960
|
+
</TextView>
|
|
2961
|
+
)}
|
|
2962
|
+
</View>
|
|
2963
|
+
)}
|
|
2964
|
+
</>
|
|
2965
|
+
)}
|
|
2966
|
+
{isDebugEnabled() && signatureBounds && nextStep !== 'COMPLETED' && (
|
|
2967
|
+
<>
|
|
2968
|
+
{signatureBounds.corners &&
|
|
2969
|
+
signatureBounds.corners.length >= 2 ? (
|
|
2970
|
+
// Render using corner points for precise rotated border
|
|
2971
|
+
<>
|
|
2972
|
+
{/* Draw border lines between consecutive corners */}
|
|
2973
|
+
{signatureBounds.corners.map((corner, idx) => {
|
|
2974
|
+
const nextCorner =
|
|
2975
|
+
signatureBounds.corners![
|
|
2976
|
+
(idx + 1) % signatureBounds.corners!.length
|
|
2977
|
+
];
|
|
2978
|
+
const dx = nextCorner.x - corner.x;
|
|
2979
|
+
const dy = nextCorner.y - corner.y;
|
|
2980
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2981
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2982
|
+
|
|
2983
|
+
return (
|
|
2984
|
+
<View
|
|
2985
|
+
key={idx}
|
|
2986
|
+
style={{
|
|
2987
|
+
position: 'absolute',
|
|
2988
|
+
left: corner.x,
|
|
2989
|
+
top: corner.y,
|
|
2990
|
+
width: length,
|
|
2991
|
+
height: 3,
|
|
2992
|
+
backgroundColor: '#00BCD4',
|
|
2993
|
+
transform: [{ rotate: `${angle}deg` }],
|
|
2994
|
+
transformOrigin: 'top left',
|
|
2995
|
+
}}
|
|
2996
|
+
/>
|
|
2997
|
+
);
|
|
2998
|
+
})}
|
|
2999
|
+
{/* Angle indicator */}
|
|
3000
|
+
{!!signatureBounds.angle &&
|
|
3001
|
+
Math.abs(signatureBounds.angle) > 5 && (
|
|
3002
|
+
<TextView
|
|
3003
|
+
style={{
|
|
3004
|
+
position: 'absolute',
|
|
3005
|
+
left: signatureBounds.x,
|
|
3006
|
+
top: signatureBounds.y - 20,
|
|
3007
|
+
color: '#00BCD4',
|
|
3008
|
+
fontSize: 10,
|
|
3009
|
+
fontWeight: 'bold',
|
|
3010
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3011
|
+
paddingHorizontal: 4,
|
|
3012
|
+
paddingVertical: 2,
|
|
3013
|
+
borderRadius: 2,
|
|
3014
|
+
}}
|
|
3015
|
+
>
|
|
3016
|
+
{signatureBounds.angle.toFixed(1)}°
|
|
3017
|
+
</TextView>
|
|
3018
|
+
)}
|
|
3019
|
+
</>
|
|
3020
|
+
) : (
|
|
3021
|
+
// Fallback to rotated rectangle if corners not available
|
|
3022
|
+
<View
|
|
3023
|
+
style={{
|
|
3024
|
+
position: 'absolute',
|
|
3025
|
+
left: signatureBounds.x + signatureBounds.width / 2,
|
|
3026
|
+
top: signatureBounds.y + signatureBounds.height / 2,
|
|
3027
|
+
width: signatureBounds.width,
|
|
3028
|
+
height: signatureBounds.height,
|
|
3029
|
+
marginLeft: -signatureBounds.width / 2,
|
|
3030
|
+
marginTop: -signatureBounds.height / 2,
|
|
3031
|
+
borderWidth: 3,
|
|
3032
|
+
borderColor: '#00BCD4',
|
|
3033
|
+
borderRadius: 8,
|
|
3034
|
+
backgroundColor: 'transparent',
|
|
3035
|
+
transform: [{ rotate: `${signatureBounds.angle || 0}deg` }],
|
|
3036
|
+
}}
|
|
3037
|
+
>
|
|
3038
|
+
{!!signatureBounds.angle &&
|
|
3039
|
+
Math.abs(signatureBounds.angle) > 5 && (
|
|
3040
|
+
<TextView
|
|
3041
|
+
style={{
|
|
3042
|
+
position: 'absolute',
|
|
3043
|
+
top: -20,
|
|
3044
|
+
left: 0,
|
|
3045
|
+
color: '#00BCD4',
|
|
3046
|
+
fontSize: 10,
|
|
3047
|
+
fontWeight: 'bold',
|
|
3048
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3049
|
+
paddingHorizontal: 4,
|
|
3050
|
+
paddingVertical: 2,
|
|
3051
|
+
borderRadius: 2,
|
|
3052
|
+
}}
|
|
3053
|
+
>
|
|
3054
|
+
{signatureBounds.angle.toFixed(1)}°
|
|
3055
|
+
</TextView>
|
|
3056
|
+
)}
|
|
3057
|
+
</View>
|
|
3058
|
+
)}
|
|
3059
|
+
</>
|
|
3060
|
+
)}
|
|
3061
|
+
<View style={[styles.topZone, { paddingTop: insets.top }]}>
|
|
3062
|
+
{nextStep !== 'COMPLETED' &&
|
|
3063
|
+
status !== 'SCANNED' &&
|
|
3064
|
+
detectedDocumentType !== 'UNKNOWN' && (
|
|
3065
|
+
<TextView style={styles.stepIndicator}>
|
|
3066
|
+
{nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
3067
|
+
? `${t('identityDocumentCamera.frontSide')} • ${t(
|
|
3068
|
+
'identityDocumentCamera.stepProgress',
|
|
3069
|
+
{
|
|
3070
|
+
current: 1,
|
|
3071
|
+
total: onlyMRZScan
|
|
3072
|
+
? detectedDocumentType === 'PASSPORT'
|
|
3073
|
+
? 1
|
|
3074
|
+
: 2
|
|
3075
|
+
: detectedDocumentType === 'PASSPORT'
|
|
3076
|
+
? 2
|
|
3077
|
+
: 3,
|
|
3078
|
+
}
|
|
3079
|
+
)}`
|
|
3080
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
3081
|
+
? `${t('identityDocumentCamera.hologramCheck')} • ${t(
|
|
3082
|
+
'identityDocumentCamera.stepProgress',
|
|
3083
|
+
{
|
|
3084
|
+
current: 2,
|
|
3085
|
+
total: detectedDocumentType === 'PASSPORT' ? 2 : 3,
|
|
3086
|
+
}
|
|
3087
|
+
)}`
|
|
3088
|
+
: nextStep === 'SCAN_ID_BACK'
|
|
3089
|
+
? `${t('identityDocumentCamera.backSide')} • ${t('identityDocumentCamera.stepProgress', { current: 3, total: 3 })}`
|
|
3090
|
+
: ''}
|
|
3091
|
+
</TextView>
|
|
3092
|
+
)}
|
|
3093
|
+
|
|
3094
|
+
<TextView
|
|
3095
|
+
style={[
|
|
3096
|
+
styles.topZoneText,
|
|
3097
|
+
// Priority order for coloring (later styles override earlier ones)
|
|
3098
|
+
// 1. Success (green) - scan completed
|
|
3099
|
+
status === 'SCANNED' && styles.topZoneTextSuccess,
|
|
3100
|
+
// 2. Error (red) - wrong side
|
|
3101
|
+
status === 'INCORRECT' && styles.topZoneTextError,
|
|
3102
|
+
// 3. Warning (yellow) - quality issues
|
|
3103
|
+
(isBrightnessLow || isFrameBlurry) && styles.topZoneTextWarning,
|
|
3104
|
+
// 4. Scanning (green) - all elements detected AND inside scan area
|
|
3105
|
+
status === 'SCANNING' &&
|
|
3106
|
+
allElementsDetected &&
|
|
3107
|
+
elementsOutsideScanArea.length === 0 &&
|
|
3108
|
+
!isBrightnessLow &&
|
|
3109
|
+
!isFrameBlurry &&
|
|
3110
|
+
styles.topZoneTextScanning,
|
|
3111
|
+
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
3112
|
+
]}
|
|
3113
|
+
>
|
|
1322
3114
|
{status === 'SCANNED'
|
|
1323
3115
|
? completedStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
1324
3116
|
? detectedDocumentType === 'PASSPORT'
|
|
@@ -1334,109 +3126,272 @@ const IdentityDocumentCamera = ({
|
|
|
1334
3126
|
? t('identityDocumentCamera.wrongSideFront')
|
|
1335
3127
|
: nextStep === 'SCAN_ID_BACK'
|
|
1336
3128
|
? t('identityDocumentCamera.wrongSideBack')
|
|
1337
|
-
:
|
|
3129
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
3130
|
+
? t('identityDocumentCamera.wrongSideFront') // Hologram is on front, show same message as front scan
|
|
3131
|
+
: t('identityDocumentCamera.alignPhotoSide')
|
|
1338
3132
|
: isBrightnessLow
|
|
1339
3133
|
? t('identityDocumentCamera.lowBrightness')
|
|
1340
3134
|
: isFrameBlurry
|
|
1341
3135
|
? t('identityDocumentCamera.avoidBlur')
|
|
1342
|
-
:
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
3136
|
+
: status === 'SCANNING' &&
|
|
3137
|
+
allElementsDetected &&
|
|
3138
|
+
elementsOutsideScanArea.length === 0
|
|
3139
|
+
? nextStep === 'SCAN_ID_BACK'
|
|
3140
|
+
? t('identityDocumentCamera.idCardBackDetected')
|
|
3141
|
+
: detectedDocumentType === 'PASSPORT'
|
|
3142
|
+
? t('identityDocumentCamera.passportDetected')
|
|
3143
|
+
: detectedDocumentType === 'ID_FRONT'
|
|
3144
|
+
? t('identityDocumentCamera.idCardFrontDetected')
|
|
3145
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
3146
|
+
? t('identityDocumentCamera.alignHologram')
|
|
1349
3147
|
: t('identityDocumentCamera.readingDocument')
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
3148
|
+
: elementsOutsideScanArea.length > 0
|
|
3149
|
+
? t('identityDocumentCamera.centerDocument')
|
|
3150
|
+
: (status === 'SCANNING' || status === 'SEARCHING') &&
|
|
3151
|
+
!allElementsDetected
|
|
3152
|
+
? nextStep === 'SCAN_ID_BACK'
|
|
3153
|
+
? t('identityDocumentCamera.alignIDBack')
|
|
3154
|
+
: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
3155
|
+
? detectedDocumentType === 'PASSPORT'
|
|
3156
|
+
? t('identityDocumentCamera.alignPassport')
|
|
3157
|
+
: detectedDocumentType === 'ID_FRONT'
|
|
3158
|
+
? t('identityDocumentCamera.alignIDFront')
|
|
3159
|
+
: t('identityDocumentCamera.alignPhotoSide')
|
|
3160
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
3161
|
+
? t('identityDocumentCamera.alignHologram')
|
|
3162
|
+
: t('identityDocumentCamera.readingDocument')
|
|
3163
|
+
: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
|
|
3164
|
+
? status === 'SCANNING'
|
|
3165
|
+
? t('identityDocumentCamera.readingDocument')
|
|
3166
|
+
: t('identityDocumentCamera.alignPhotoSide')
|
|
3167
|
+
: nextStep === 'SCAN_HOLOGRAM'
|
|
3168
|
+
? t('identityDocumentCamera.alignHologram')
|
|
3169
|
+
: nextStep === 'SCAN_ID_BACK'
|
|
3170
|
+
? status === 'SCANNING'
|
|
3171
|
+
? t(
|
|
3172
|
+
'identityDocumentCamera.readingDocument'
|
|
3173
|
+
)
|
|
3174
|
+
: t(
|
|
3175
|
+
'identityDocumentCamera.alignIDBackSide'
|
|
3176
|
+
)
|
|
3177
|
+
: nextStep === 'COMPLETED'
|
|
3178
|
+
? t('identityDocumentCamera.scanCompleted')
|
|
3179
|
+
: ''}
|
|
1361
3180
|
</TextView>
|
|
1362
3181
|
</View>
|
|
1363
3182
|
<View style={styles.leftZone} />
|
|
1364
3183
|
<View style={styles.rightZone} />
|
|
1365
3184
|
<View style={styles.bottomZone}>
|
|
1366
|
-
{
|
|
1367
|
-
|
|
1368
|
-
<
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
</
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
3185
|
+
<View style={styles.debugImagesRow}>
|
|
3186
|
+
{isDebugEnabled() && (
|
|
3187
|
+
<View style={styles.imageContainer}>
|
|
3188
|
+
{currentFaceImage ? (
|
|
3189
|
+
<Image
|
|
3190
|
+
source={{
|
|
3191
|
+
uri: `data:image/jpeg;base64,${currentFaceImage}`,
|
|
3192
|
+
}}
|
|
3193
|
+
style={styles.faceImage}
|
|
3194
|
+
/>
|
|
3195
|
+
) : (
|
|
3196
|
+
<View
|
|
3197
|
+
style={[
|
|
3198
|
+
styles.faceImage,
|
|
3199
|
+
{ backgroundColor: '#333', justifyContent: 'center' },
|
|
3200
|
+
]}
|
|
3201
|
+
>
|
|
3202
|
+
<TextView
|
|
3203
|
+
style={{
|
|
3204
|
+
color: '#666',
|
|
3205
|
+
fontSize: 10,
|
|
3206
|
+
textAlign: 'center',
|
|
3207
|
+
}}
|
|
3208
|
+
>
|
|
3209
|
+
Waiting...
|
|
3210
|
+
</TextView>
|
|
3211
|
+
</View>
|
|
3212
|
+
)}
|
|
3213
|
+
<TextView
|
|
3214
|
+
style={[
|
|
3215
|
+
styles.imageContainerText,
|
|
3216
|
+
currentFaceImage && { color: '#4CAF50' },
|
|
3217
|
+
]}
|
|
3218
|
+
>
|
|
3219
|
+
{`${currentFaceImage ? '✓ ' : ''}Face`}
|
|
3220
|
+
</TextView>
|
|
3221
|
+
</View>
|
|
3222
|
+
)}
|
|
3223
|
+
{isDebugEnabled() && (
|
|
3224
|
+
<View style={styles.imageContainer}>
|
|
3225
|
+
{currentSecondaryFaceImage ? (
|
|
3226
|
+
<Image
|
|
3227
|
+
source={{
|
|
3228
|
+
uri: `data:image/jpeg;base64,${currentSecondaryFaceImage}`,
|
|
3229
|
+
}}
|
|
3230
|
+
style={styles.faceImage}
|
|
3231
|
+
/>
|
|
3232
|
+
) : (
|
|
3233
|
+
<View
|
|
3234
|
+
style={[
|
|
3235
|
+
styles.faceImage,
|
|
3236
|
+
{ backgroundColor: '#333', justifyContent: 'center' },
|
|
3237
|
+
]}
|
|
3238
|
+
>
|
|
3239
|
+
<TextView
|
|
3240
|
+
style={{
|
|
3241
|
+
color: '#666',
|
|
3242
|
+
fontSize: 10,
|
|
3243
|
+
textAlign: 'center',
|
|
3244
|
+
}}
|
|
3245
|
+
>
|
|
3246
|
+
Waiting...
|
|
3247
|
+
</TextView>
|
|
3248
|
+
</View>
|
|
3249
|
+
)}
|
|
3250
|
+
<TextView
|
|
3251
|
+
style={[
|
|
3252
|
+
styles.imageContainerText,
|
|
3253
|
+
currentSecondaryFaceImage && { color: '#4CAF50' },
|
|
3254
|
+
]}
|
|
3255
|
+
>
|
|
3256
|
+
{`${currentSecondaryFaceImage ? '✓ ' : ''}2nd Face`}
|
|
3257
|
+
</TextView>
|
|
3258
|
+
</View>
|
|
3259
|
+
)}
|
|
3260
|
+
{isDebugEnabled() && (
|
|
3261
|
+
<View style={styles.imageContainer}>
|
|
3262
|
+
{_currentHologramMaskImage ? (
|
|
3263
|
+
<Image
|
|
3264
|
+
source={{
|
|
3265
|
+
uri: `data:image/jpeg;base64,${_currentHologramMaskImage}`,
|
|
3266
|
+
}}
|
|
3267
|
+
style={styles.faceImage}
|
|
3268
|
+
/>
|
|
3269
|
+
) : (
|
|
3270
|
+
<View
|
|
3271
|
+
style={[
|
|
3272
|
+
styles.faceImage,
|
|
3273
|
+
{ backgroundColor: '#333', justifyContent: 'center' },
|
|
3274
|
+
]}
|
|
3275
|
+
>
|
|
3276
|
+
<TextView
|
|
3277
|
+
style={{
|
|
3278
|
+
color: '#666',
|
|
3279
|
+
fontSize: 10,
|
|
3280
|
+
textAlign: 'center',
|
|
3281
|
+
}}
|
|
3282
|
+
>
|
|
3283
|
+
Waiting...
|
|
3284
|
+
</TextView>
|
|
3285
|
+
</View>
|
|
3286
|
+
)}
|
|
3287
|
+
<TextView
|
|
3288
|
+
style={[
|
|
3289
|
+
styles.imageContainerText,
|
|
3290
|
+
_currentHologramMaskImage && { color: '#4CAF50' },
|
|
3291
|
+
]}
|
|
3292
|
+
>
|
|
3293
|
+
{`${_currentHologramMaskImage ? '✓ ' : ''}Mask`}
|
|
3294
|
+
</TextView>
|
|
3295
|
+
</View>
|
|
3296
|
+
)}
|
|
3297
|
+
{isDebugEnabled() && (
|
|
3298
|
+
<View style={styles.imageContainer}>
|
|
3299
|
+
{currentHologramImage ? (
|
|
3300
|
+
<Image
|
|
3301
|
+
source={{
|
|
3302
|
+
uri: `data:image/jpeg;base64,${currentHologramImage}`,
|
|
3303
|
+
}}
|
|
3304
|
+
style={styles.faceImage}
|
|
3305
|
+
/>
|
|
3306
|
+
) : latestHologramFaceImage && hologramImageCount > 0 ? (
|
|
3307
|
+
<View style={{ position: 'relative' }}>
|
|
3308
|
+
<Image
|
|
3309
|
+
source={{
|
|
3310
|
+
uri: `data:image/jpeg;base64,${latestHologramFaceImage}`,
|
|
3311
|
+
}}
|
|
3312
|
+
style={[styles.faceImage, { opacity: 0.7 }]}
|
|
3313
|
+
/>
|
|
3314
|
+
<View
|
|
3315
|
+
style={{
|
|
3316
|
+
position: 'absolute',
|
|
3317
|
+
bottom: 0,
|
|
3318
|
+
left: 0,
|
|
3319
|
+
right: 0,
|
|
3320
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
3321
|
+
padding: 2,
|
|
3322
|
+
}}
|
|
3323
|
+
>
|
|
3324
|
+
<TextView
|
|
3325
|
+
style={{
|
|
3326
|
+
color: '#FFA500',
|
|
3327
|
+
fontSize: 8,
|
|
3328
|
+
textAlign: 'center',
|
|
3329
|
+
fontWeight: 'bold',
|
|
3330
|
+
}}
|
|
3331
|
+
>
|
|
3332
|
+
{hologramImageCount}/{HOLOGRAM_IMAGE_COUNT}
|
|
3333
|
+
</TextView>
|
|
3334
|
+
</View>
|
|
3335
|
+
</View>
|
|
3336
|
+
) : (
|
|
3337
|
+
<View
|
|
3338
|
+
style={[
|
|
3339
|
+
styles.faceImage,
|
|
3340
|
+
{ backgroundColor: '#333', justifyContent: 'center' },
|
|
3341
|
+
]}
|
|
3342
|
+
>
|
|
3343
|
+
<TextView
|
|
3344
|
+
style={{
|
|
3345
|
+
color: '#666',
|
|
3346
|
+
fontSize: 10,
|
|
3347
|
+
textAlign: 'center',
|
|
3348
|
+
}}
|
|
3349
|
+
>
|
|
3350
|
+
Waiting...
|
|
3351
|
+
</TextView>
|
|
3352
|
+
</View>
|
|
3353
|
+
)}
|
|
3354
|
+
<TextView
|
|
3355
|
+
style={[
|
|
3356
|
+
styles.imageContainerText,
|
|
3357
|
+
currentHologramImage && { color: '#4CAF50' },
|
|
3358
|
+
latestHologramFaceImage &&
|
|
3359
|
+
!currentHologramImage && { color: '#FFA500' },
|
|
3360
|
+
]}
|
|
3361
|
+
>
|
|
3362
|
+
{`${currentHologramImage ? '✓ ' : latestHologramFaceImage ? '⏳ ' : ''}Hologram`}
|
|
3363
|
+
</TextView>
|
|
3364
|
+
</View>
|
|
3365
|
+
)}
|
|
3366
|
+
{isDebugEnabled() && (
|
|
3367
|
+
<View style={styles.debugInfoContainer}>
|
|
3368
|
+
<TextView style={styles.debugInfoText}>
|
|
3369
|
+
{`Step: ${nextStep}`}
|
|
3370
|
+
</TextView>
|
|
3371
|
+
<TextView style={styles.debugInfoText}>
|
|
3372
|
+
{`Status: ${status}`}
|
|
3373
|
+
</TextView>
|
|
3374
|
+
<TextView style={styles.debugInfoText}>
|
|
3375
|
+
{`Face: ${currentFaceImage ? '✓ CAPTURED' : '✗ WAITING'} ${!faceDetectionEnabled ? '(DISABLED)' : ''}`}
|
|
3376
|
+
</TextView>
|
|
3377
|
+
<TextView style={styles.debugInfoText}>
|
|
3378
|
+
{`Hologram: ${currentHologramImage ? '✓ CAPTURED' : `${hologramImageCount}/${HOLOGRAM_IMAGE_COUNT} imgs`} (retry ${hologramDetectionCurrentRetryCount.current}/${HOLOGRAM_DETECTION_RETRY_COUNT})`}
|
|
3379
|
+
</TextView>
|
|
3380
|
+
<TextView style={styles.debugInfoText}>
|
|
3381
|
+
{`2nd Face: ${currentSecondaryFaceImage ? '✓ CAPTURED' : '✗ WAITING'} (retry ${secondaryFaceDetectionCurrentRetryCount.current}/${SECOND_FACE_DETECTION_RETRY_COUNT})`}
|
|
3382
|
+
</TextView>
|
|
3383
|
+
<TextView style={styles.debugInfoText}>
|
|
3384
|
+
{`Flash: ${isTorchOn ? '🔦 ON' : '🔦 OFF'}`}
|
|
3385
|
+
</TextView>
|
|
3386
|
+
<TextView style={styles.debugInfoText}>
|
|
3387
|
+
{`Brightness: ${isBrightnessLow ? '⚠️ LOW' : '✓ OK'}`}
|
|
3388
|
+
</TextView>
|
|
3389
|
+
<TextView style={styles.debugInfoText}>
|
|
3390
|
+
{`Blur: ${isFrameBlurry ? '⚠️ BLURRY' : '✓ OK'}`}
|
|
3391
|
+
</TextView>
|
|
3392
|
+
</View>
|
|
3393
|
+
)}
|
|
3394
|
+
</View>
|
|
1440
3395
|
</View>
|
|
1441
3396
|
<View
|
|
1442
3397
|
style={[
|
|
@@ -1444,19 +3399,18 @@ const IdentityDocumentCamera = ({
|
|
|
1444
3399
|
{
|
|
1445
3400
|
borderColor:
|
|
1446
3401
|
status === 'SCANNED' || nextStep === 'COMPLETED'
|
|
1447
|
-
? '#4CAF50'
|
|
3402
|
+
? '#4CAF50'
|
|
1448
3403
|
: status === 'INCORRECT'
|
|
1449
|
-
? '#f44336'
|
|
3404
|
+
? '#f44336'
|
|
1450
3405
|
: status === 'SCANNING'
|
|
1451
|
-
? '#2196F3'
|
|
3406
|
+
? '#2196F3'
|
|
1452
3407
|
: isBrightnessLow || isFrameBlurry
|
|
1453
|
-
? '#FFC107'
|
|
3408
|
+
? '#FFC107'
|
|
1454
3409
|
: 'white',
|
|
1455
3410
|
borderWidth: status === 'SCANNING' ? 3 : 2,
|
|
1456
3411
|
},
|
|
1457
3412
|
]}
|
|
1458
3413
|
>
|
|
1459
|
-
{/* Only show ONE animation at a time - priority order: completed/scanned > brightness > hologram > scanning */}
|
|
1460
3414
|
{nextStep === 'COMPLETED' || status === 'SCANNED' ? (
|
|
1461
3415
|
<LottieView
|
|
1462
3416
|
source={require('../../Shared/Animations/success.json')}
|
|
@@ -1487,11 +3441,84 @@ const IdentityDocumentCamera = ({
|
|
|
1487
3441
|
/>
|
|
1488
3442
|
) : null}
|
|
1489
3443
|
</View>
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
3444
|
+
{isDebugEnabled() && (
|
|
3445
|
+
<View
|
|
3446
|
+
style={{
|
|
3447
|
+
position: 'absolute',
|
|
3448
|
+
top: 10,
|
|
3449
|
+
right: 10,
|
|
3450
|
+
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
3451
|
+
padding: 10,
|
|
3452
|
+
borderRadius: 8,
|
|
3453
|
+
borderWidth: 1,
|
|
3454
|
+
borderColor: '#FF6B6B',
|
|
3455
|
+
maxWidth: 200,
|
|
3456
|
+
}}
|
|
3457
|
+
>
|
|
3458
|
+
<TextView
|
|
3459
|
+
style={{
|
|
3460
|
+
color: '#FF6B6B',
|
|
3461
|
+
fontSize: 11,
|
|
3462
|
+
fontWeight: 'bold',
|
|
3463
|
+
marginBottom: 6,
|
|
3464
|
+
}}
|
|
3465
|
+
>
|
|
3466
|
+
🐛 DEBUG MODE
|
|
3467
|
+
</TextView>
|
|
3468
|
+
<TextView
|
|
3469
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3470
|
+
>
|
|
3471
|
+
{`Step: ${nextStep}`}
|
|
3472
|
+
</TextView>
|
|
3473
|
+
<TextView
|
|
3474
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3475
|
+
>
|
|
3476
|
+
{`Status: ${status}`}
|
|
3477
|
+
</TextView>
|
|
3478
|
+
<TextView
|
|
3479
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3480
|
+
>
|
|
3481
|
+
{`Doc Type: ${detectedDocumentType}`}
|
|
3482
|
+
</TextView>
|
|
3483
|
+
<TextView
|
|
3484
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3485
|
+
>
|
|
3486
|
+
{`Face: ${currentFaceImage ? '✓' : '✗'}`}
|
|
3487
|
+
</TextView>
|
|
3488
|
+
{!onlyMRZScan && (
|
|
3489
|
+
<>
|
|
3490
|
+
<TextView
|
|
3491
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3492
|
+
>
|
|
3493
|
+
{`2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'}`}
|
|
3494
|
+
</TextView>
|
|
3495
|
+
<TextView
|
|
3496
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3497
|
+
>
|
|
3498
|
+
{`Hologram: ${hologramImageCount}/${HOLOGRAM_IMAGE_COUNT}`}
|
|
3499
|
+
</TextView>
|
|
3500
|
+
</>
|
|
3501
|
+
)}
|
|
3502
|
+
<TextView
|
|
3503
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3504
|
+
>
|
|
3505
|
+
{`Brightness: ${isBrightnessLow ? '⚠️ LOW' : '✓'}`}
|
|
3506
|
+
</TextView>
|
|
3507
|
+
<TextView
|
|
3508
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3509
|
+
>
|
|
3510
|
+
{`Blur: ${isFrameBlurry ? '⚠️' : '✓'}`}
|
|
3511
|
+
</TextView>
|
|
3512
|
+
<TextView
|
|
3513
|
+
style={{ color: '#88D8B0', fontSize: 9, marginBottom: 2 }}
|
|
3514
|
+
>
|
|
3515
|
+
{`Flash: ${isTorchOn ? '🔦' : '○'}`}
|
|
3516
|
+
</TextView>
|
|
3517
|
+
<TextView style={{ color: '#88D8B0', fontSize: 9 }}>
|
|
3518
|
+
{`Face Detection: ${faceDetectionEnabled ? '✓' : '✗'}`}
|
|
3519
|
+
</TextView>
|
|
3520
|
+
</View>
|
|
3521
|
+
)}
|
|
1495
3522
|
</>
|
|
1496
3523
|
)}
|
|
1497
3524
|
</View>
|
|
@@ -1528,14 +3555,6 @@ const styles = StyleSheet.create({
|
|
|
1528
3555
|
alignItems: 'center',
|
|
1529
3556
|
paddingHorizontal: 5,
|
|
1530
3557
|
},
|
|
1531
|
-
focusArea: {
|
|
1532
|
-
position: 'absolute',
|
|
1533
|
-
top: 0,
|
|
1534
|
-
left: 0,
|
|
1535
|
-
width: '100%',
|
|
1536
|
-
height: '100%',
|
|
1537
|
-
zIndex: 2,
|
|
1538
|
-
},
|
|
1539
3558
|
animation: {
|
|
1540
3559
|
width: '100%',
|
|
1541
3560
|
height: '100%',
|
|
@@ -1566,16 +3585,16 @@ const styles = StyleSheet.create({
|
|
|
1566
3585
|
padding: 20,
|
|
1567
3586
|
},
|
|
1568
3587
|
topZoneTextScanning: {
|
|
1569
|
-
color: '#2196F3',
|
|
3588
|
+
color: '#2196F3',
|
|
1570
3589
|
},
|
|
1571
3590
|
topZoneTextSuccess: {
|
|
1572
|
-
color: '#4CAF50',
|
|
3591
|
+
color: '#4CAF50',
|
|
1573
3592
|
},
|
|
1574
3593
|
topZoneTextWarning: {
|
|
1575
|
-
color: '#FFC107',
|
|
3594
|
+
color: '#FFC107',
|
|
1576
3595
|
},
|
|
1577
3596
|
topZoneTextError: {
|
|
1578
|
-
color: '#f44336',
|
|
3597
|
+
color: '#f44336',
|
|
1579
3598
|
},
|
|
1580
3599
|
leftZone: {
|
|
1581
3600
|
position: 'absolute',
|
|
@@ -1601,12 +3620,24 @@ const styles = StyleSheet.create({
|
|
|
1601
3620
|
bottom: 0,
|
|
1602
3621
|
backgroundColor: '#00000099',
|
|
1603
3622
|
padding: 20,
|
|
3623
|
+
display: 'flex',
|
|
3624
|
+
flexDirection: 'column',
|
|
3625
|
+
gap: 10,
|
|
3626
|
+
justifyContent: 'flex-start',
|
|
3627
|
+
},
|
|
3628
|
+
debugImagesRow: {
|
|
1604
3629
|
display: 'flex',
|
|
1605
3630
|
flexDirection: 'row',
|
|
1606
3631
|
gap: 10,
|
|
1607
3632
|
justifyContent: 'flex-start',
|
|
1608
3633
|
flexWrap: 'wrap',
|
|
1609
3634
|
},
|
|
3635
|
+
cardDetectionRow: {
|
|
3636
|
+
display: 'flex',
|
|
3637
|
+
flexDirection: 'row',
|
|
3638
|
+
justifyContent: 'center',
|
|
3639
|
+
marginTop: 5,
|
|
3640
|
+
},
|
|
1610
3641
|
imageContainer: {
|
|
1611
3642
|
display: 'flex',
|
|
1612
3643
|
flexDirection: 'column',
|
|
@@ -1626,6 +3657,18 @@ const styles = StyleSheet.create({
|
|
|
1626
3657
|
borderWidth: 1,
|
|
1627
3658
|
borderColor: 'white',
|
|
1628
3659
|
},
|
|
3660
|
+
cardDetectionImage: {
|
|
3661
|
+
width: 160,
|
|
3662
|
+
height: 120,
|
|
3663
|
+
borderRadius: 8,
|
|
3664
|
+
borderWidth: 2,
|
|
3665
|
+
borderColor: '#FF9800',
|
|
3666
|
+
},
|
|
3667
|
+
cardDetectionContainer: {
|
|
3668
|
+
display: 'flex',
|
|
3669
|
+
flexDirection: 'column',
|
|
3670
|
+
alignItems: 'center',
|
|
3671
|
+
},
|
|
1629
3672
|
debugInfoContainer: {
|
|
1630
3673
|
flex: 1,
|
|
1631
3674
|
paddingLeft: 10,
|