@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,71 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/* eslint-disable react-native/no-inline-styles */
|
|
4
|
-
import React, { useEffect, useState } from 'react';
|
|
5
|
-
import { View, StyleSheet, Text as TextView, Platform,
|
|
4
|
+
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
5
|
+
import { View, StyleSheet, Text as TextView, Platform, StatusBar, Vibration, Linking, Image, ActivityIndicator, PermissionsAndroid, Dimensions } from 'react-native';
|
|
6
6
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { useRunOnJS, useSharedValue } from 'react-native-worklets-core';
|
|
10
|
-
import { useTextRecognition } from "../VisionCameraPlugins/TextRecognition/index.js";
|
|
11
|
-
import { useFaceDetector } from "../VisionCameraPlugins/FaceDetector/index.js";
|
|
12
|
-
import mrzUtils from "../Libs/mrz.utils.js";
|
|
13
|
-
import { crop } from "../VisionCameraPlugins/Cropper/index.js";
|
|
7
|
+
import { TrustchexCamera } from "./TrustchexCamera.js";
|
|
8
|
+
import { NativeModules } from 'react-native';
|
|
14
9
|
import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
|
|
15
|
-
import ImageEditor from '@react-native-community/image-editor';
|
|
16
10
|
import { useIsFocused } from '@react-navigation/native';
|
|
17
|
-
import { AdaptiveThresholdTypes, ColorConversionCodes, DataTypes, ObjectType, OpenCV, ThresholdTypes } from 'react-native-fast-opencv';
|
|
18
|
-
import { getAverageBrightness, getScanAreaCenterPoint, calculateExposureStep, isBlurry as checkBlurry } from "../Libs/camera.utils.js";
|
|
19
11
|
import { useTranslation } from 'react-i18next';
|
|
12
|
+
import { debugLog, logError, isDebugEnabled } from "../Libs/debug.utils.js";
|
|
20
13
|
import LottieView from 'lottie-react-native';
|
|
21
14
|
import StyledButton from "./StyledButton.js";
|
|
22
15
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
23
|
-
import {
|
|
24
|
-
import { speakWithDebounce } from "../Libs/tts.utils.js";
|
|
16
|
+
import { speak, resetLastMessage } from "../Libs/tts.utils.js";
|
|
25
17
|
import AppContext from "../Contexts/AppContext.js";
|
|
26
18
|
import { useTheme } from "../Contexts/ThemeContext.js";
|
|
27
19
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const HOLOGRAM_IMAGE_COUNT =
|
|
32
|
-
const HOLOGRAM_DETECTION_THRESHOLD =
|
|
33
|
-
const HOLOGRAM_DETECTION_RETRY_COUNT = 3;
|
|
20
|
+
const {
|
|
21
|
+
OpenCVModule
|
|
22
|
+
} = NativeModules;
|
|
23
|
+
const HOLOGRAM_IMAGE_COUNT = 12;
|
|
24
|
+
const HOLOGRAM_DETECTION_THRESHOLD = 1000; // Lowered for better sensitivity while still filtering noise
|
|
25
|
+
const HOLOGRAM_DETECTION_RETRY_COUNT = 3; // More attempts but faster each (~1s per attempt)
|
|
34
26
|
const SECOND_FACE_DETECTION_RETRY_COUNT = 10;
|
|
35
|
-
const
|
|
36
|
-
|
|
27
|
+
const MIN_BRIGHTNESS_THRESHOLD = 60;
|
|
28
|
+
const MAX_CONSECUTIVE_QUALITY_FAILURES = 30;
|
|
37
29
|
const IdentityDocumentCamera = ({
|
|
38
30
|
onlyMRZScan,
|
|
39
|
-
onIdentityDocumentScanned
|
|
40
|
-
showDebugImages = false
|
|
31
|
+
onIdentityDocumentScanned
|
|
41
32
|
}) => {
|
|
42
33
|
useKeepAwake();
|
|
43
34
|
const theme = useTheme();
|
|
44
35
|
const insets = useSafeAreaInsets();
|
|
45
36
|
const appContext = React.useContext(AppContext);
|
|
46
|
-
const cameraRef =
|
|
47
|
-
const
|
|
48
|
-
const [permissionsRequested, setPermissionsRequested] =
|
|
49
|
-
const [isActive, setIsActive] =
|
|
37
|
+
const cameraRef = useRef(null);
|
|
38
|
+
const [hasPermission, setHasPermission] = useState(false);
|
|
39
|
+
const [permissionsRequested, setPermissionsRequested] = useState(false);
|
|
40
|
+
const [isActive, setIsActive] = useState(false);
|
|
50
41
|
const isFocused = useIsFocused();
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
width: 1920,
|
|
60
|
-
height: 1080
|
|
61
|
-
},
|
|
62
|
-
iso: 'max',
|
|
63
|
-
photoHdr: false,
|
|
64
|
-
videoHdr: false,
|
|
65
|
-
videoStabilizationMode: 'standard',
|
|
66
|
-
autoFocusSystem: 'phase-detection'
|
|
67
|
-
}]);
|
|
68
|
-
const isCameraInitialized = useSharedValue(false);
|
|
42
|
+
const isTorchOnRef = useRef(false);
|
|
43
|
+
const [isTorchOn, _setIsTorchOn] = useState(false);
|
|
44
|
+
const setIsTorchOn = useCallback(val => {
|
|
45
|
+
isTorchOnRef.current = val;
|
|
46
|
+
_setIsTorchOn(val);
|
|
47
|
+
}, []);
|
|
48
|
+
const [_exposure, _setExposure] = useState(0);
|
|
49
|
+
const isCameraInitialized = useRef(false);
|
|
69
50
|
const [currentFaceImage, setCurrentFaceImage] = useState(undefined);
|
|
70
51
|
const [_currentHologramMaskImage, setCurrentHologramMaskImage] = useState(undefined);
|
|
71
52
|
const [currentHologramImage, setCurrentHologramImage] = useState(undefined);
|
|
@@ -77,762 +58,1628 @@ const IdentityDocumentCamera = ({
|
|
|
77
58
|
const [nextStep, setNextStep] = useState('SCAN_ID_FRONT_OR_PASSPORT');
|
|
78
59
|
const [completedStep, setCompletedStep] = useState(null);
|
|
79
60
|
const [detectedDocumentType, setDetectedDocumentType] = useState('UNKNOWN');
|
|
80
|
-
const hologramDetectionCurrentRetryCount =
|
|
81
|
-
const secondaryFaceDetectionCurrentRetryCount =
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
61
|
+
const hologramDetectionCurrentRetryCount = useRef(0);
|
|
62
|
+
const secondaryFaceDetectionCurrentRetryCount = useRef(0);
|
|
63
|
+
const consecutiveQualityFailures = useRef(0);
|
|
64
|
+
const mrzDetectionCurrentRetryCount = useRef(0);
|
|
65
|
+
|
|
66
|
+
// MRZ stability tracking - require consistent valid reads
|
|
67
|
+
const lastValidMRZText = useRef(null);
|
|
68
|
+
const lastValidMRZFields = useRef(null);
|
|
69
|
+
const validMRZConsecutiveCount = useRef(0);
|
|
70
|
+
const REQUIRED_CONSISTENT_MRZ_READS = 2; // Require 2 consistent valid reads
|
|
71
|
+
|
|
72
|
+
// Document type stability tracking - require consistent detections from good quality frames
|
|
73
|
+
const lastDetectedDocType = useRef('UNKNOWN');
|
|
74
|
+
const consistentDocTypeCount = useRef(0);
|
|
75
|
+
const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3; // Require 3 consistent detections from quality frames
|
|
76
|
+
|
|
77
|
+
// Frame quality tracking - persist across callbacks
|
|
78
|
+
const lastFrameQuality = useRef({
|
|
79
|
+
hasAcceptableQuality: true,
|
|
80
|
+
isBlurry: false,
|
|
81
|
+
brightness: 128
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Barcode caching - persist detected barcode across frames for reliability
|
|
85
|
+
const cachedBarcode = useRef(null);
|
|
86
|
+
|
|
87
|
+
// Helper to compare MRZ field values (ignore raw text variations)
|
|
88
|
+
const areMRZFieldsEqual = useCallback((fields1, fields2) => {
|
|
89
|
+
if (!fields1 || !fields2) return false;
|
|
90
|
+
// Compare critical fields that define document identity
|
|
91
|
+
return fields1.documentNumber === fields2.documentNumber && fields1.birthDate === fields2.birthDate && fields1.expirationDate === fields2.expirationDate && fields1.firstName === fields2.firstName && fields1.lastName === fields2.lastName && fields1.issuingState === fields2.issuingState;
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
// Helper functions to reduce duplication
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if all required MRZ fields are present
|
|
98
|
+
*/
|
|
99
|
+
const hasRequiredMRZFields = useCallback(fields => !!fields?.firstName && !!fields?.lastName && !!fields?.documentNumber && !!fields?.birthDate, []);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Log detailed MRZ information for debugging and verification
|
|
103
|
+
*/
|
|
104
|
+
const logMRZDetails = useCallback((stepName, fields, mrzText, consecutiveReads, isDebugMode) => {
|
|
105
|
+
if (isDebugMode) {
|
|
106
|
+
debugLog('IdentityDocumentCamera', `[${stepName}] MRZ validated successfully (consistent reads: ${consecutiveReads})`);
|
|
107
|
+
debugLog('IdentityDocumentCamera', `[${stepName}] Final MRZ Fields:`, {
|
|
108
|
+
documentNumber: fields?.documentNumber,
|
|
109
|
+
name: `${fields?.lastName} ${fields?.firstName}`,
|
|
110
|
+
birthDate: fields?.birthDate,
|
|
111
|
+
expirationDate: fields?.expirationDate,
|
|
112
|
+
nationality: fields?.nationality || fields?.issuingState,
|
|
113
|
+
sex: fields?.sex,
|
|
114
|
+
personalId: fields?.optional1
|
|
115
|
+
});
|
|
116
|
+
if (mrzText) {
|
|
117
|
+
const mrzLines = mrzText.split('\n').map(l => l.replace(/\s/g, '')).filter(l => l.length >= 20 && /^[A-Z0-9<]+$/.test(l));
|
|
118
|
+
debugLog('IdentityDocumentCamera', `[${stepName}] MRZ lines (${mrzLines.length}):`);
|
|
119
|
+
mrzLines.forEach((line, idx) => {
|
|
120
|
+
debugLog('IdentityDocumentCamera', ` ${idx + 1}: ${line}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Log MRZ validation failure details for debugging
|
|
128
|
+
*/
|
|
129
|
+
const logMRZValidationFailure = useCallback((stepName, hasRequiredFields, parsedData, retryCount, isDebugMode) => {
|
|
130
|
+
if (isDebugMode) {
|
|
131
|
+
const debugInfo = {
|
|
132
|
+
hasRequiredFields,
|
|
133
|
+
isValid: parsedData?.valid,
|
|
134
|
+
retryCount
|
|
135
|
+
};
|
|
136
|
+
if (parsedData?.valid) {
|
|
137
|
+
debugInfo.consistentReads = validMRZConsecutiveCount.current;
|
|
138
|
+
debugInfo.requiredReads = REQUIRED_CONSISTENT_MRZ_READS;
|
|
139
|
+
debugInfo.fieldsMatch = areMRZFieldsEqual(lastValidMRZFields.current, parsedData?.fields);
|
|
140
|
+
}
|
|
141
|
+
debugLog('IdentityDocumentCamera', `[${stepName}] MRZ detected but validation failed - retrying`, debugInfo);
|
|
142
|
+
}
|
|
143
|
+
}, [areMRZFieldsEqual]);
|
|
144
|
+
const lastHologramCaptureTime = useRef(0);
|
|
145
|
+
const HOLOGRAM_CAPTURE_INTERVAL = 250; // ms between captures - allows flash to fully stabilize for accurate diff
|
|
146
|
+
const hologramFramesWithoutFace = useRef(0); // Track consecutive frames without face during hologram collection
|
|
147
|
+
const HOLOGRAM_MAX_FRAMES_WITHOUT_FACE = 30; // ~1 second at 30fps - safety timeout
|
|
148
|
+
|
|
149
|
+
const faceDetectionErrorCount = useRef(0);
|
|
150
|
+
const brightnessHistory = useRef([]);
|
|
85
151
|
const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
|
|
152
|
+
const faceImages = useRef([]);
|
|
153
|
+
const hologramImageCountRef = useRef(0);
|
|
154
|
+
const [hologramImageCount, setHologramImageCount] = useState(0);
|
|
155
|
+
const lastVoiceGuidanceMessage = useRef('');
|
|
156
|
+
const [latestHologramFaceImage, setLatestHologramFaceImage] = useState(undefined);
|
|
157
|
+
const lastFacePosition = useRef(null);
|
|
158
|
+
const [documentPlaneBounds, setDocumentPlaneBounds] = useState(null);
|
|
159
|
+
const [secondaryFaceBounds, setSecondaryFaceBounds] = useState(null);
|
|
160
|
+
const [barcodeBounds, setBarcodeBounds] = useState(null);
|
|
161
|
+
const [mrzBounds, setMrzBounds] = useState(null);
|
|
162
|
+
const [signatureBounds, setSignatureBounds] = useState(null);
|
|
163
|
+
const [frameDimensions, setFrameDimensions] = useState(null);
|
|
164
|
+
|
|
165
|
+
// Track if all required elements are detected in current frame
|
|
166
|
+
const [allElementsDetected, setAllElementsDetected] = useState(false);
|
|
167
|
+
// Track if detected elements are within scan area
|
|
168
|
+
const [elementsOutsideScanArea, setElementsOutsideScanArea] = useState([]);
|
|
86
169
|
const {
|
|
87
170
|
t
|
|
88
171
|
} = useTranslation();
|
|
89
|
-
// const [boundingBox, setBoundingBox] = useState<Bounds>({
|
|
90
|
-
// x: 0,
|
|
91
|
-
// y: 0,
|
|
92
|
-
// width: 0,
|
|
93
|
-
// height: 0,
|
|
94
|
-
// });
|
|
95
|
-
|
|
96
|
-
const {
|
|
97
|
-
scanText
|
|
98
|
-
} = useTextRecognition({
|
|
99
|
-
language: 'latin'
|
|
100
|
-
});
|
|
101
|
-
const {
|
|
102
|
-
detectFaces
|
|
103
|
-
} = useFaceDetector({
|
|
104
|
-
contourMode: 'none',
|
|
105
|
-
landmarkMode: 'none',
|
|
106
|
-
classificationMode: 'all',
|
|
107
|
-
performanceMode: 'accurate',
|
|
108
|
-
trackingEnabled: false,
|
|
109
|
-
minFaceSize: 0.1,
|
|
110
|
-
autoScale: false
|
|
111
|
-
});
|
|
112
172
|
useEffect(() => {
|
|
113
173
|
const requestPermissions = async () => {
|
|
114
|
-
if (
|
|
115
|
-
await
|
|
174
|
+
if (Platform.OS === 'android') {
|
|
175
|
+
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
176
|
+
setHasPermission(granted === PermissionsAndroid.RESULTS.GRANTED);
|
|
177
|
+
} else {
|
|
178
|
+
setHasPermission(true);
|
|
116
179
|
}
|
|
117
180
|
setPermissionsRequested(true);
|
|
118
181
|
};
|
|
119
182
|
requestPermissions();
|
|
120
|
-
}, [
|
|
183
|
+
}, []);
|
|
121
184
|
useEffect(() => {
|
|
122
|
-
if (
|
|
185
|
+
if (isFocused && hasPermission && hasGuideShown) {
|
|
123
186
|
setIsActive(true);
|
|
124
187
|
} else {
|
|
125
188
|
setIsActive(false);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
faceImages.current = [];
|
|
190
|
+
hologramImageCountRef.current = 0;
|
|
191
|
+
setHologramImageCount(0);
|
|
192
|
+
setLatestHologramFaceImage(undefined);
|
|
193
|
+
hologramDetectionCurrentRetryCount.current = 0;
|
|
194
|
+
secondaryFaceDetectionCurrentRetryCount.current = 0;
|
|
195
|
+
mrzDetectionCurrentRetryCount.current = 0;
|
|
196
|
+
lastValidMRZText.current = null;
|
|
197
|
+
lastValidMRZFields.current = null;
|
|
198
|
+
validMRZConsecutiveCount.current = 0;
|
|
199
|
+
lastValidMRZText.current = null;
|
|
200
|
+
lastValidMRZFields.current = null;
|
|
201
|
+
validMRZConsecutiveCount.current = 0;
|
|
202
|
+
cachedBarcode.current = null; // Clear cached barcode on new scan
|
|
203
|
+
lastVoiceGuidanceMessage.current = '';
|
|
204
|
+
resetLastMessage();
|
|
140
205
|
}
|
|
141
206
|
return () => {
|
|
142
207
|
setIsActive(false);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Clear face images array
|
|
151
|
-
faceImages = [];
|
|
208
|
+
faceImages.current = [];
|
|
209
|
+
hologramImageCountRef.current = 0;
|
|
210
|
+
setHologramImageCount(0);
|
|
211
|
+
setLatestHologramFaceImage(undefined);
|
|
212
|
+
lastVoiceGuidanceMessage.current = '';
|
|
213
|
+
resetLastMessage();
|
|
152
214
|
};
|
|
153
|
-
}, [
|
|
215
|
+
}, [isFocused, hasPermission, hasGuideShown]);
|
|
154
216
|
useEffect(() => {
|
|
155
217
|
if (hasGuideShown) {
|
|
218
|
+
// Generate message - match UI display logic exactly for consistency
|
|
156
219
|
let message = '';
|
|
157
|
-
|
|
158
|
-
// Priority: scanned > incorrect > blur during scanning > brightness > blur > step-specific
|
|
159
220
|
if (status === 'SCANNED') {
|
|
160
|
-
|
|
161
|
-
if (completedStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
162
|
-
message = detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportScanned') : t('identityDocumentCamera.frontSideScanned');
|
|
163
|
-
} else if (completedStep === 'SCAN_ID_BACK') {
|
|
164
|
-
message = t('identityDocumentCamera.backSideScanned');
|
|
165
|
-
} else if (completedStep === 'SCAN_HOLOGRAM') {
|
|
166
|
-
message = t('identityDocumentCamera.hologramVerified');
|
|
167
|
-
} else {
|
|
168
|
-
message = t('identityDocumentCamera.scanCompleted');
|
|
169
|
-
}
|
|
221
|
+
message = completedStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportScanned') : t('identityDocumentCamera.frontSideScanned') : completedStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.backSideScanned') : completedStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.hologramVerified') : t('identityDocumentCamera.scanCompleted');
|
|
170
222
|
} else if (status === 'INCORRECT') {
|
|
171
|
-
|
|
172
|
-
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
173
|
-
message = t('identityDocumentCamera.wrongSideFront');
|
|
174
|
-
} else if (nextStep === 'SCAN_ID_BACK') {
|
|
175
|
-
message = t('identityDocumentCamera.wrongSideBack');
|
|
176
|
-
}
|
|
223
|
+
message = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? t('identityDocumentCamera.wrongSideFront') : nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.wrongSideBack') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.wrongSideFront') : t('identityDocumentCamera.alignPhotoSide');
|
|
177
224
|
} else if (isBrightnessLow) {
|
|
178
|
-
// Brightness warning takes priority over blur
|
|
179
225
|
message = t('identityDocumentCamera.lowBrightness');
|
|
180
226
|
} else if (isFrameBlurry) {
|
|
181
|
-
// Show blur warning only when brightness is sufficient
|
|
182
227
|
message = t('identityDocumentCamera.avoidBlur');
|
|
183
|
-
} else if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
message = t('identityDocumentCamera.idCardFrontDetected');
|
|
192
|
-
} else {
|
|
193
|
-
message = t('identityDocumentCamera.readingDocument');
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
message = t('identityDocumentCamera.readingDocument');
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
message = t('identityDocumentCamera.alignPhotoSide');
|
|
200
|
-
}
|
|
201
|
-
} else if (nextStep === 'SCAN_HOLOGRAM') {
|
|
202
|
-
message = t('identityDocumentCamera.alignHologram');
|
|
203
|
-
} else if (nextStep === 'SCAN_ID_BACK') {
|
|
204
|
-
if (status === 'SCANNING') {
|
|
205
|
-
message = t('identityDocumentCamera.readingDocument');
|
|
206
|
-
} else {
|
|
207
|
-
message = t('identityDocumentCamera.alignIDBackSide');
|
|
208
|
-
}
|
|
209
|
-
} else if (nextStep === 'COMPLETED') {
|
|
210
|
-
message = t('identityDocumentCamera.scanCompleted');
|
|
228
|
+
} else if (status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0) {
|
|
229
|
+
message = nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.idCardBackDetected') : detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportDetected') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.idCardFrontDetected') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument');
|
|
230
|
+
} else if (elementsOutsideScanArea.length > 0) {
|
|
231
|
+
message = t('identityDocumentCamera.centerDocument');
|
|
232
|
+
} else if ((status === 'SCANNING' || status === 'SEARCHING') && !allElementsDetected) {
|
|
233
|
+
message = nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.alignIDBack') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.alignPassport') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.alignIDFront') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument');
|
|
234
|
+
} else {
|
|
235
|
+
message = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : '';
|
|
211
236
|
}
|
|
212
|
-
if (appContext.currentWorkflowStep?.data?.voiceGuidanceActive && message) {
|
|
213
|
-
|
|
237
|
+
if (appContext.currentWorkflowStep?.data?.voiceGuidanceActive && message && message !== lastVoiceGuidanceMessage.current) {
|
|
238
|
+
lastVoiceGuidanceMessage.current = message;
|
|
239
|
+
speak(message, true);
|
|
214
240
|
}
|
|
215
241
|
}
|
|
216
|
-
}, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, isFrameBlurry, nextStep, status, completedStep, currentFaceImage, detectedDocumentType, t]);
|
|
217
|
-
|
|
218
|
-
// Auto-reset INCORRECT status after showing warning briefly
|
|
242
|
+
}, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, isFrameBlurry, nextStep, status, completedStep, currentFaceImage, detectedDocumentType, allElementsDetected, elementsOutsideScanArea, t]);
|
|
219
243
|
useEffect(() => {
|
|
220
244
|
if (status === 'INCORRECT') {
|
|
221
245
|
const timeout = setTimeout(() => {
|
|
222
246
|
setStatus('SEARCHING');
|
|
223
|
-
}, 1500);
|
|
247
|
+
}, 1500);
|
|
224
248
|
return () => clearTimeout(timeout);
|
|
225
249
|
}
|
|
226
250
|
}, [status]);
|
|
227
251
|
|
|
228
|
-
//
|
|
252
|
+
// Disable face detection when scanning back side (no face expected, avoids false positives)
|
|
229
253
|
useEffect(() => {
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Only autofocus during searching and scanning states
|
|
235
|
-
if (status !== 'SEARCHING' && status !== 'SCANNING') {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const autofocusInterval = setInterval(async () => {
|
|
239
|
-
try {
|
|
240
|
-
// Get camera dimensions (assuming format dimensions)
|
|
241
|
-
const width = format?.videoWidth ?? 1920;
|
|
242
|
-
const height = format?.videoHeight ?? 1080;
|
|
243
|
-
|
|
244
|
-
// Calculate center point of scan area
|
|
245
|
-
const centerPoint = getScanAreaCenterPoint(width, height);
|
|
246
|
-
|
|
247
|
-
// Focus on the center of the scan area
|
|
248
|
-
await cameraRef.current?.focus({
|
|
249
|
-
x: centerPoint.x,
|
|
250
|
-
y: centerPoint.y
|
|
251
|
-
});
|
|
252
|
-
} catch (error) {
|
|
253
|
-
// Ignore autofocus errors
|
|
254
|
-
}
|
|
255
|
-
}, 2500); // Every 2.5 seconds
|
|
256
|
-
|
|
257
|
-
return () => clearInterval(autofocusInterval);
|
|
258
|
-
}, [isActive, device, format, status]);
|
|
259
|
-
const detectDocumentType = (faces, ocrText, mrzFields) => {
|
|
260
|
-
if (faces.length > 0 && !mrzFields && ocrText?.includes('Signature')
|
|
261
|
-
// ocrText?.includes('Surname') &&
|
|
262
|
-
// ocrText?.includes('Given Name(s)') &&
|
|
263
|
-
// ocrText?.includes('Date of Birth') &&
|
|
264
|
-
// ocrText?.includes('Document No') &&
|
|
265
|
-
// ocrText?.includes('Valid Until')
|
|
266
|
-
) {
|
|
267
|
-
return 'ID_FRONT';
|
|
268
|
-
} else if (faces.length === 0 && mrzFields?.documentCode === 'I'
|
|
269
|
-
// ocrText?.includes("Father's Name") &&
|
|
270
|
-
// ocrText?.includes("Mother's Name") &&
|
|
271
|
-
// ocrText?.includes('Issued By')
|
|
272
|
-
) {
|
|
273
|
-
return 'ID_BACK';
|
|
274
|
-
} else if (faces.length > 0 && mrzFields?.documentCode === 'P') {
|
|
275
|
-
return 'PASSPORT';
|
|
254
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
255
|
+
setFaceDetectionEnabled(false);
|
|
256
|
+
} else {
|
|
257
|
+
setFaceDetectionEnabled(true);
|
|
276
258
|
}
|
|
277
|
-
|
|
278
|
-
};
|
|
259
|
+
}, [nextStep]);
|
|
279
260
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
// setBoundingBox(bounds);
|
|
283
|
-
// },
|
|
284
|
-
// [setBoundingBox],
|
|
285
|
-
// );
|
|
286
|
-
|
|
287
|
-
// const isBlockInFrame = (block: BlocksData) => {
|
|
288
|
-
// 'worklet';
|
|
289
|
-
// const scanningFrame = {
|
|
290
|
-
// x: 0.03 * 1080,
|
|
291
|
-
// y: 0.35 * 1920,
|
|
292
|
-
// width: 0.94 * 1080,
|
|
293
|
-
// height: 0.3 * 1920,
|
|
294
|
-
// } as Bounds;
|
|
295
|
-
// const bounds = {
|
|
296
|
-
// x: block.blockFrame.x,
|
|
297
|
-
// y: block.blockFrame.y,
|
|
298
|
-
// width: block.blockFrame.width,
|
|
299
|
-
// height: block.blockFrame.height,
|
|
300
|
-
// } as Bounds;
|
|
301
|
-
|
|
302
|
-
// if (
|
|
303
|
-
// bounds.x >= scanningFrame.x &&
|
|
304
|
-
// bounds.y >= scanningFrame.y &&
|
|
305
|
-
// bounds.x + bounds.width <= scanningFrame.x + scanningFrame.width &&
|
|
306
|
-
// bounds.y + bounds.height <= scanningFrame.y + scanningFrame.height
|
|
307
|
-
// ) {
|
|
308
|
-
// return true;
|
|
309
|
-
// }
|
|
310
|
-
|
|
311
|
-
// setBoundingBoxInJS({
|
|
312
|
-
// x: (bounds.x / 1080) * windowWidth,
|
|
313
|
-
// y: (bounds.y / 1920) * windowHeight,
|
|
314
|
-
// width: (bounds.width / 1080) * windowWidth,
|
|
315
|
-
// height: (bounds.height / 1920) * windowHeight,
|
|
316
|
-
// });
|
|
317
|
-
|
|
318
|
-
// return false;
|
|
319
|
-
// };
|
|
320
|
-
|
|
321
|
-
const applyThreshold = image => {
|
|
322
|
-
const gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
323
|
-
|
|
324
|
-
// Convert to grayscale
|
|
325
|
-
OpenCV.invoke('cvtColor', image, gray, ColorConversionCodes.COLOR_RGB2GRAY);
|
|
326
|
-
|
|
327
|
-
// Apply GaussianBlur to reduce noise
|
|
328
|
-
const kSize = OpenCV.createObject(ObjectType.Size, 5, 5);
|
|
329
|
-
OpenCV.invoke('GaussianBlur', gray, gray, kSize, 0);
|
|
330
|
-
|
|
331
|
-
// Apply Otsu's thresholding
|
|
332
|
-
OpenCV.invoke('threshold', gray, gray, 0, 255, ThresholdTypes.THRESH_BINARY + ThresholdTypes.THRESH_OTSU);
|
|
333
|
-
return gray;
|
|
334
|
-
};
|
|
335
|
-
const areImagesSimilar = (image1, image2, threshold = 15000) => {
|
|
261
|
+
// Native OpenCV: detect hologram from sequence of face images
|
|
262
|
+
const detectHologramNative = useCallback(async images => {
|
|
336
263
|
try {
|
|
337
|
-
if (
|
|
338
|
-
|
|
264
|
+
if (isDebugEnabled()) {
|
|
265
|
+
debugLog('IdentityDocumentCamera', `[Hologram] Detecting hologram from ${images.length} images`);
|
|
266
|
+
}
|
|
267
|
+
// Limit images to prevent memory issues
|
|
268
|
+
const limitedImages = images.slice(0, HOLOGRAM_IMAGE_COUNT);
|
|
269
|
+
const result = await OpenCVModule.detectHologram(limitedImages, HOLOGRAM_DETECTION_THRESHOLD);
|
|
270
|
+
if (result) {
|
|
271
|
+
return [result.hologramMask, result.hologramImage];
|
|
339
272
|
}
|
|
340
|
-
const mat1 = OpenCV.base64ToMat(image1);
|
|
341
|
-
const mat2 = OpenCV.base64ToMat(image2);
|
|
342
|
-
const diff = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
343
|
-
OpenCV.invoke('absdiff', applyThreshold(mat1), applyThreshold(mat2), diff);
|
|
344
|
-
const count = OpenCV.invoke('countNonZero', diff);
|
|
345
|
-
return count.value < threshold;
|
|
346
273
|
} catch (error) {
|
|
347
|
-
|
|
348
|
-
return false;
|
|
274
|
+
logError('[Hologram] Detection error:', error);
|
|
349
275
|
}
|
|
350
|
-
|
|
351
|
-
|
|
276
|
+
return [];
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
// Native OpenCV: compare two images for similarity
|
|
280
|
+
const areImagesSimilarNative = async (image1, image2, threshold = 20000 // Relaxed threshold for better tolerance of lighting/angle variations
|
|
281
|
+
) => {
|
|
352
282
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const diffs = [];
|
|
356
|
-
const hologram = OpenCV.base64ToMat(images[0]);
|
|
357
|
-
for (let i = 0; i < images.length - 1; i++) {
|
|
358
|
-
const mat1 = OpenCV.base64ToMat(images[i]);
|
|
359
|
-
const mat2 = OpenCV.base64ToMat(images[i + 1]);
|
|
360
|
-
let diff = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
361
|
-
OpenCV.invoke('absdiff', mat1, mat2, diff);
|
|
362
|
-
OpenCV.invoke('cvtColor', diff, diff, ColorConversionCodes.COLOR_RGB2HSV);
|
|
363
|
-
OpenCV.invoke('inRange', diff, lowerBound, upperBound, diff);
|
|
364
|
-
if (OpenCV.invoke('countNonZero', diff).value > 500) {
|
|
365
|
-
OpenCV.invoke('addWeighted', hologram, 0.5, mat2, 0.5, 0, hologram);
|
|
366
|
-
diffs.push(diff);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
const hologramMask = diffs[0];
|
|
370
|
-
for (let i = 1; i < diffs.length; i++) {
|
|
371
|
-
OpenCV.invoke('addWeighted', hologramMask, 0.5, diffs[i], 0.5, 0, hologramMask);
|
|
372
|
-
}
|
|
373
|
-
OpenCV.invoke('adaptiveThreshold', hologramMask, hologramMask, 255, AdaptiveThresholdTypes.ADAPTIVE_THRESH_GAUSSIAN_C, ThresholdTypes.THRESH_BINARY_INV, 21, 2);
|
|
374
|
-
const count = OpenCV.invoke('countNonZero', hologramMask);
|
|
375
|
-
if (count.value > HOLOGRAM_DETECTION_THRESHOLD) {
|
|
376
|
-
const hologramMaskJs = OpenCV.toJSValue(hologramMask);
|
|
377
|
-
const hologramJs = OpenCV.toJSValue(hologram);
|
|
378
|
-
return [hologramMaskJs.base64, hologramJs.base64];
|
|
379
|
-
}
|
|
283
|
+
if (!image1 || !image2) return false;
|
|
284
|
+
return await OpenCVModule.areImagesSimilar(image1, image2, threshold);
|
|
380
285
|
} catch (error) {
|
|
381
|
-
|
|
286
|
+
return false;
|
|
382
287
|
}
|
|
383
|
-
return [];
|
|
384
288
|
};
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
} else if (a.bounds.x > b.bounds.x) {
|
|
390
|
-
return 1;
|
|
391
|
-
}
|
|
392
|
-
return 0;
|
|
393
|
-
});
|
|
394
|
-
};
|
|
395
|
-
const getFaceImages = async (faces, image, width, height) => {
|
|
396
|
-
if (!faces.length || !image || width <= 0 || height <= 0) {
|
|
289
|
+
|
|
290
|
+
// Native OpenCV: crop face images from full frame
|
|
291
|
+
const getFaceImages = async (facesToDetect, image, width, height) => {
|
|
292
|
+
if (!facesToDetect.length || !image || width <= 0 || height <= 0) {
|
|
397
293
|
return [];
|
|
398
294
|
}
|
|
399
|
-
const croppedFaces = [];
|
|
400
295
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// );
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Calculate crop area with bounds checking
|
|
414
|
-
const expandedWidth = face.bounds.width * 1.5;
|
|
415
|
-
const expandedHeight = face.bounds.height * 1.5;
|
|
416
|
-
const offsetX = Math.max(0, face.bounds.x - expandedWidth / 6);
|
|
417
|
-
const offsetY = Platform.OS === 'ios' ? Math.max(0, face.bounds.y - expandedHeight / 6) : Math.max(0, width - face.bounds.y - expandedHeight / 1.2);
|
|
418
|
-
const croppedFace = await ImageEditor.cropImage(uri, {
|
|
419
|
-
offset: {
|
|
420
|
-
x: offsetX,
|
|
421
|
-
y: offsetY
|
|
422
|
-
},
|
|
423
|
-
size: {
|
|
424
|
-
width: expandedWidth,
|
|
425
|
-
height: expandedHeight
|
|
426
|
-
},
|
|
427
|
-
displaySize: {
|
|
428
|
-
width: 240,
|
|
429
|
-
height: 320
|
|
430
|
-
},
|
|
431
|
-
includeBase64: true,
|
|
432
|
-
quality: 1
|
|
433
|
-
});
|
|
434
|
-
if (croppedFace.width !== 240 || croppedFace.height !== 320) {
|
|
435
|
-
try {
|
|
436
|
-
const croppedFaceMat = OpenCV.base64ToMat(croppedFace.base64);
|
|
437
|
-
|
|
438
|
-
// Ensure crop dimensions are valid for the matrix
|
|
439
|
-
const matCropWidth = Math.min(240, croppedFace.width);
|
|
440
|
-
const matCropHeight = Math.min(320, croppedFace.height);
|
|
441
|
-
|
|
442
|
-
// Only crop if we have valid dimensions
|
|
443
|
-
if (matCropWidth > 0 && matCropHeight > 0) {
|
|
444
|
-
OpenCV.invoke('crop', croppedFaceMat, croppedFaceMat, OpenCV.createObject(ObjectType.Rect, 0, 0, matCropWidth, matCropHeight));
|
|
445
|
-
croppedFaces.push(OpenCV.toJSValue(croppedFaceMat).base64);
|
|
446
|
-
} else {
|
|
447
|
-
// Fallback to original base64 if crop dimensions are invalid
|
|
448
|
-
croppedFaces.push(croppedFace.base64);
|
|
449
|
-
}
|
|
450
|
-
} catch (cropError) {
|
|
451
|
-
console.warn('OpenCV crop operation failed:', cropError);
|
|
452
|
-
// Fallback to original image if OpenCV crop fails
|
|
453
|
-
croppedFaces.push(croppedFace.base64);
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
croppedFaces.push(croppedFace.base64);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
296
|
+
const faceBounds = facesToDetect.map(f => ({
|
|
297
|
+
x: f.bounds.x,
|
|
298
|
+
y: f.bounds.y,
|
|
299
|
+
width: f.bounds.width,
|
|
300
|
+
height: f.bounds.height
|
|
301
|
+
}));
|
|
302
|
+
const croppedFaces = await OpenCVModule.cropFaceImages(image, faceBounds, width, height);
|
|
303
|
+
return croppedFaces ?? [];
|
|
459
304
|
} catch (error) {
|
|
460
|
-
|
|
305
|
+
logError('[getFaceImages] Native face crop failed:', error);
|
|
306
|
+
return [];
|
|
461
307
|
}
|
|
462
|
-
return croppedFaces;
|
|
463
308
|
};
|
|
464
|
-
const setNextStepAndVibrate = (nextStepType, fromStep) => {
|
|
465
|
-
// Track which step was just completed for showing specific message
|
|
309
|
+
const setNextStepAndVibrate = useCallback((nextStepType, fromStep) => {
|
|
466
310
|
if (fromStep) {
|
|
467
311
|
setCompletedStep(fromStep);
|
|
468
312
|
}
|
|
313
|
+
|
|
314
|
+
// Turn flash on and reset hologram counters when entering SCAN_HOLOGRAM
|
|
315
|
+
if (nextStepType === 'SCAN_HOLOGRAM' && fromStep !== 'SCAN_HOLOGRAM') {
|
|
316
|
+
setIsTorchOn(true);
|
|
317
|
+
// Reset hologram detection counters for fresh start
|
|
318
|
+
hologramDetectionCurrentRetryCount.current = 0;
|
|
319
|
+
secondaryFaceDetectionCurrentRetryCount.current = 0;
|
|
320
|
+
hologramFramesWithoutFace.current = 0;
|
|
321
|
+
faceImages.current = [];
|
|
322
|
+
hologramImageCountRef.current = 0;
|
|
323
|
+
setHologramImageCount(0);
|
|
324
|
+
setLatestHologramFaceImage(undefined);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Clean up flash and hologram state when leaving SCAN_HOLOGRAM step
|
|
328
|
+
if (fromStep === 'SCAN_HOLOGRAM' && nextStepType !== 'SCAN_HOLOGRAM') {
|
|
329
|
+
setIsTorchOn(false);
|
|
330
|
+
faceImages.current = [];
|
|
331
|
+
hologramImageCountRef.current = 0;
|
|
332
|
+
setHologramImageCount(0);
|
|
333
|
+
setLatestHologramFaceImage(undefined);
|
|
334
|
+
lastFacePosition.current = null; // Reset document plane reference
|
|
335
|
+
cachedBarcode.current = null; // Clear cached barcode
|
|
336
|
+
setDocumentPlaneBounds(null); // Clear visual overlay
|
|
337
|
+
setSecondaryFaceBounds(null); // Clear secondary face overlay
|
|
338
|
+
if (isDebugEnabled()) {
|
|
339
|
+
console.log('[Flash] Turning off flash and clearing hologram images when leaving step');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
469
342
|
setNextStep(nextStepType);
|
|
470
343
|
Vibration.vibrate(100);
|
|
471
344
|
|
|
472
|
-
// Reset
|
|
345
|
+
// Reset MRZ retry counter for each new step so retries start fresh
|
|
346
|
+
mrzDetectionCurrentRetryCount.current = 0;
|
|
347
|
+
lastValidMRZText.current = null;
|
|
348
|
+
validMRZConsecutiveCount.current = 0;
|
|
349
|
+
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
350
|
+
|
|
473
351
|
if (nextStepType !== 'COMPLETED') {
|
|
474
352
|
setTimeout(() => {
|
|
475
353
|
setStatus('SEARCHING');
|
|
476
354
|
setCompletedStep(null);
|
|
477
|
-
},
|
|
355
|
+
}, 1000);
|
|
478
356
|
}
|
|
479
|
-
};
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
357
|
+
}, [setIsTorchOn]);
|
|
358
|
+
const handleFaceAndText = useCallback(async (text, faces, frameWidth, frameHeight, barcode, image, elementsOutside, nativeMrzResult) => {
|
|
359
|
+
const detectDocumentType = (facesParam, ocrText, mrzFields, frameWidthParam, mrzTextParam) => {
|
|
360
|
+
// Relaxed signature detection: matches signature/imza variants and OCR errors
|
|
361
|
+
const hasSignatureMatch = /s[gi]g?n[au]?t[u]?r|imz[a]s?/i.test(ocrText);
|
|
362
|
+
if (isDebugEnabled()) {
|
|
363
|
+
console.log('[DocType] faces:', facesParam.length, 'mrzFields:', !!mrzFields, 'mrzText:', !!mrzTextParam, 'textLen:', ocrText?.length, 'hasSignature:', hasSignatureMatch);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ID Back: no face + ID MRZ
|
|
367
|
+
if (facesParam.length === 0 && mrzFields?.documentCode === 'I') {
|
|
368
|
+
return 'ID_BACK';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Passport: face + passport MRZ
|
|
372
|
+
if (facesParam.length > 0 && mrzFields?.documentCode === 'P') {
|
|
373
|
+
return 'PASSPORT';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ID Front: face detected with signature text
|
|
377
|
+
if (facesParam.length > 0 && ocrText?.length >= 5) {
|
|
378
|
+
const hasSignature = hasSignatureMatch;
|
|
379
|
+
// Only turn off torch during initial scan step, not during SCAN_HOLOGRAM
|
|
380
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
381
|
+
setIsTorchOn(false);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Filter to card-sized faces only (min 5% of frame width to exclude tiny background faces)
|
|
385
|
+
const cardSizedFaces = frameWidthParam ? facesParam.filter(face => face.bounds.width >= frameWidthParam * 0.05 && face.bounds.height >= frameWidthParam * 0.05) : facesParam;
|
|
386
|
+
|
|
387
|
+
// CRITICAL: If passport MRZ pattern is detected but not parsed yet,
|
|
388
|
+
// return UNKNOWN instead of ID_FRONT to avoid misclassifying passports
|
|
389
|
+
// Passports always have MRZ visible on front starting with P<TUR or similar
|
|
390
|
+
if (cardSizedFaces.length > 0 && !mrzFields?.documentCode && hasSignature) {
|
|
391
|
+
if (mrzTextParam && mrzTextParam.length > 20 && /P<[A-Z]{3}/.test(mrzTextParam)) {
|
|
392
|
+
// Passport MRZ pattern detected (P<TUR, P<USA, etc.) but not parsed
|
|
393
|
+
// Could be passport with OCR errors - wait for proper parsing
|
|
394
|
+
if (isDebugEnabled()) {
|
|
395
|
+
console.log('[DocType] Passport MRZ pattern (P<XXX) detected but not parsed - returning UNKNOWN to wait for proper classification');
|
|
396
|
+
}
|
|
397
|
+
return 'UNKNOWN';
|
|
398
|
+
}
|
|
399
|
+
return 'ID_FRONT';
|
|
400
|
+
}
|
|
401
|
+
// Also ensure flash is off when scan is completed
|
|
402
|
+
if (nextStep === 'COMPLETED' && isTorchOn) {
|
|
403
|
+
setIsTorchOn(false);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return 'UNKNOWN';
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// Filter to card-sized faces only (min 5% of frame width to exclude tiny background faces)
|
|
410
|
+
const cardSizedFaces = faces.filter(face => face.bounds.width >= frameWidth * 0.05 && face.bounds.height >= frameWidth * 0.05);
|
|
411
|
+
|
|
412
|
+
// Cache barcode when detected, use cached value if current frame has no barcode
|
|
413
|
+
// This handles inconsistent barcode detection across frames
|
|
414
|
+
if (barcode?.rawValue && nextStep === 'SCAN_ID_BACK') {
|
|
415
|
+
cachedBarcode.current = barcode;
|
|
416
|
+
}
|
|
417
|
+
const barcodeToUse = barcode || cachedBarcode.current;
|
|
418
|
+
|
|
419
|
+
// Store frame dimensions for coordinate conversion
|
|
420
|
+
if (frameDimensions?.width !== frameWidth || frameDimensions.height !== frameHeight) {
|
|
421
|
+
setFrameDimensions({
|
|
422
|
+
width: frameWidth,
|
|
423
|
+
height: frameHeight
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (nextStep !== 'SCAN_HOLOGRAM' && isTorchOnRef.current && (currentHologramImage || hologramDetectionCurrentRetryCount.current >= HOLOGRAM_DETECTION_RETRY_COUNT)) {
|
|
488
427
|
setIsTorchOn(false);
|
|
489
428
|
}
|
|
490
429
|
if (nextStep === 'COMPLETED') {
|
|
491
430
|
setStatus('SCANNED');
|
|
492
431
|
return;
|
|
493
432
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
433
|
+
if (elementsOutside) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (nextStep === 'SCAN_ID_BACK' && cardSizedFaces.length > 0) {
|
|
497
437
|
setStatus('INCORRECT');
|
|
498
438
|
return;
|
|
499
439
|
}
|
|
500
|
-
|
|
440
|
+
|
|
441
|
+
// Only crop and lock face when ID_FRONT or PASSPORT is confirmed
|
|
442
|
+
const shouldCropFaces = detectedDocumentType === 'ID_FRONT' || detectedDocumentType === 'PASSPORT' || nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
443
|
+
const croppedFaces = shouldCropFaces ? await getFaceImages(cardSizedFaces, image ?? '', frameWidth, frameHeight) : [];
|
|
444
|
+
|
|
445
|
+
// Validate document plane consistency across all captures
|
|
446
|
+
let facePositionValid = true;
|
|
447
|
+
if (cardSizedFaces.length > 0 && cardSizedFaces[0]) {
|
|
448
|
+
const currentFaceBounds = cardSizedFaces[0].bounds;
|
|
449
|
+
if (lastFacePosition.current) {
|
|
450
|
+
// Check if face position is within acceptable range
|
|
451
|
+
// Use looser tolerance during hologram step since flash toggling causes position jitter
|
|
452
|
+
const xDiff = Math.abs(currentFaceBounds.x - lastFacePosition.current.x);
|
|
453
|
+
const yDiff = Math.abs(currentFaceBounds.y - lastFacePosition.current.y);
|
|
454
|
+
const widthDiff = Math.abs(currentFaceBounds.width - lastFacePosition.current.width);
|
|
455
|
+
const heightDiff = Math.abs(currentFaceBounds.height - lastFacePosition.current.height);
|
|
456
|
+
const tolerance = nextStep === 'SCAN_HOLOGRAM' ? 0.5 : 0.2;
|
|
457
|
+
const xTolerance = lastFacePosition.current.width * tolerance;
|
|
458
|
+
const yTolerance = lastFacePosition.current.height * tolerance;
|
|
459
|
+
const sizeTolerance = lastFacePosition.current.width * tolerance;
|
|
460
|
+
facePositionValid = xDiff <= xTolerance && yDiff <= yTolerance && widthDiff <= sizeTolerance && heightDiff <= sizeTolerance;
|
|
461
|
+
if (!facePositionValid) {
|
|
462
|
+
if (isDebugEnabled()) {
|
|
463
|
+
console.log(`[DocPlane] Face position changed - document moved (xDiff=${xDiff.toFixed(1)}, yDiff=${yDiff.toFixed(1)}) - rejecting frame`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Update reference position to follow gradual movement (sliding window)
|
|
468
|
+
lastFacePosition.current = {
|
|
469
|
+
x: currentFaceBounds.x,
|
|
470
|
+
y: currentFaceBounds.y,
|
|
471
|
+
width: currentFaceBounds.width,
|
|
472
|
+
height: currentFaceBounds.height
|
|
473
|
+
};
|
|
474
|
+
} else {
|
|
475
|
+
// First capture - store reference position
|
|
476
|
+
lastFacePosition.current = {
|
|
477
|
+
x: currentFaceBounds.x,
|
|
478
|
+
y: currentFaceBounds.y,
|
|
479
|
+
width: currentFaceBounds.width,
|
|
480
|
+
height: currentFaceBounds.height
|
|
481
|
+
};
|
|
482
|
+
console.log('[DocPlane] Stored reference face position for document plane validation');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Update visual bounds for debug overlay
|
|
486
|
+
// Transform face bounds from image coordinates to screen coordinates
|
|
487
|
+
if (facePositionValid && frameDimensions) {
|
|
488
|
+
const screen = Dimensions.get('window');
|
|
489
|
+
|
|
490
|
+
// Camera uses FILL_CENTER: scale to fill screen while maintaining aspect ratio
|
|
491
|
+
const frameAspect = frameDimensions.width / frameDimensions.height;
|
|
492
|
+
const screenAspect = screen.width / screen.height;
|
|
493
|
+
let scale;
|
|
494
|
+
let offsetX = 0;
|
|
495
|
+
let offsetY = 0;
|
|
496
|
+
if (frameAspect > screenAspect) {
|
|
497
|
+
// Frame is wider - scale by height, crop width
|
|
498
|
+
scale = screen.height / frameDimensions.height;
|
|
499
|
+
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
500
|
+
} else {
|
|
501
|
+
// Frame is taller - scale by width, crop height
|
|
502
|
+
scale = screen.width / frameDimensions.width;
|
|
503
|
+
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
504
|
+
}
|
|
505
|
+
const cropPadding = Math.max(currentFaceBounds.width * 0.15, currentFaceBounds.height * 0.15);
|
|
506
|
+
setDocumentPlaneBounds({
|
|
507
|
+
x: currentFaceBounds.x * scale - offsetX,
|
|
508
|
+
y: currentFaceBounds.y * scale - offsetY,
|
|
509
|
+
width: currentFaceBounds.width * scale,
|
|
510
|
+
height: currentFaceBounds.height * scale,
|
|
511
|
+
cropPadding: cropPadding * scale
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Capture and persist face only after document type is confirmed
|
|
517
|
+
// This prevents locking a face before we know what document we're scanning
|
|
518
|
+
let faceImageToUse = currentFaceImage;
|
|
519
|
+
if (shouldCropFaces && croppedFaces.length > 0 && croppedFaces[0] && facePositionValid) {
|
|
520
|
+
if (!currentFaceImage) {
|
|
521
|
+
// First face detection after doc type confirmed - lock it for all subsequent steps
|
|
522
|
+
faceImageToUse = croppedFaces[0];
|
|
523
|
+
setCurrentFaceImage(croppedFaces[0]);
|
|
524
|
+
if (isDebugEnabled()) {
|
|
525
|
+
console.log('[DocPlane] Locked primary face from validated document plane (docType: ' + detectedDocumentType + ')');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!text || text.length < 5 || !image) {
|
|
501
530
|
setStatus('SEARCHING');
|
|
502
531
|
return;
|
|
503
532
|
}
|
|
504
|
-
const {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
533
|
+
const parsedMRZData = nativeMrzResult?.valid && nativeMrzResult.documentCode ? {
|
|
534
|
+
valid: true,
|
|
535
|
+
fields: nativeMrzResult
|
|
536
|
+
} : nativeMrzResult?.documentCode ? {
|
|
537
|
+
valid: false,
|
|
538
|
+
fields: nativeMrzResult
|
|
539
|
+
} : {
|
|
540
|
+
valid: false,
|
|
541
|
+
fields: null
|
|
542
|
+
};
|
|
543
|
+
const mrzText = parsedMRZData.valid ? nativeMrzResult?.rawLines : null;
|
|
544
|
+
|
|
545
|
+
// MRZ stability check - require consistent valid reads to avoid OCR noise
|
|
546
|
+
// Compare parsed field values instead of raw text to handle OCR variations in filler characters
|
|
547
|
+
// Only proceed with MRZ if it's actually valid and has all required fields
|
|
548
|
+
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
549
|
+
if (mrzText && parsedMRZData?.valid === true && parsedMRZData?.fields && mrzHasRequiredFields) {
|
|
550
|
+
const currentFields = parsedMRZData.fields;
|
|
551
|
+
if (areMRZFieldsEqual(lastValidMRZFields.current, currentFields)) {
|
|
552
|
+
// Same MRZ data detected again - increment counter
|
|
553
|
+
validMRZConsecutiveCount.current++;
|
|
554
|
+
} else {
|
|
555
|
+
// Different MRZ data - reset counter and store new data
|
|
556
|
+
if (isDebugEnabled()) {
|
|
557
|
+
console.log(`[MRZ Stability] New MRZ detected, resetting counter (was ${validMRZConsecutiveCount.current}, doc: ${currentFields.documentNumber?.slice(-4)})`);
|
|
558
|
+
}
|
|
559
|
+
lastValidMRZFields.current = currentFields;
|
|
560
|
+
lastValidMRZText.current = mrzText;
|
|
561
|
+
validMRZConsecutiveCount.current = 1;
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
// Invalid or no MRZ - don't reset completely, just skip this frame
|
|
565
|
+
// This allows temporary OCR noise without losing progress
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check if we have enough consistent valid reads
|
|
569
|
+
const mrzStableAndValid = validMRZConsecutiveCount.current >= REQUIRED_CONSISTENT_MRZ_READS && parsedMRZData?.valid === true && areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
|
|
570
|
+
|
|
571
|
+
// During SCAN_ID_BACK, handle MRZ/barcode directly without full document type detection
|
|
572
|
+
// This avoids the chicken-and-egg problem where detectDocumentType requires
|
|
573
|
+
// mrzFields.documentCode === 'I' but MRZ parsing may return different codes
|
|
574
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
575
|
+
// CRITICAL: Always check if wrong side is shown (front or passport when back is expected)
|
|
576
|
+
// ID_BACK should have NO faces and NO signature text
|
|
577
|
+
// Multiple indicators for robust detection:
|
|
578
|
+
const hasFaces = cardSizedFaces.length > 0;
|
|
579
|
+
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
580
|
+
const hasPassportMRZ = parsedMRZData?.fields?.documentCode === 'P';
|
|
581
|
+
const hasPassportMRZPattern = mrzText && /P<[A-Z]{3}/.test(mrzText);
|
|
582
|
+
if (hasFaces || hasSignature || hasPassportMRZ || hasPassportMRZPattern) {
|
|
583
|
+
if (isDebugEnabled()) {
|
|
584
|
+
console.log(`[ID_BACK Scan] Wrong side detected - faces: ${hasFaces}, signature: ${hasSignature}, passport MRZ: ${hasPassportMRZ}, passport pattern: ${hasPassportMRZPattern}`);
|
|
585
|
+
}
|
|
586
|
+
setStatus('INCORRECT');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// SAFETY CHECK: If we detect passport during ID_BACK scan via state, skip this step
|
|
591
|
+
// This shouldn't happen but protects against edge cases
|
|
592
|
+
if (detectedDocumentType === 'PASSPORT') {
|
|
593
|
+
if (isDebugEnabled()) {
|
|
594
|
+
console.log('[ID_BACK Scan] ERROR: Passport detected in ID_BACK step - skipping to COMPLETED');
|
|
595
|
+
}
|
|
596
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
597
|
+
setTimeout(() => {
|
|
598
|
+
onIdentityDocumentScanned({
|
|
599
|
+
image,
|
|
600
|
+
documentType: 'PASSPORT',
|
|
601
|
+
mrzText: mrzText ?? undefined,
|
|
602
|
+
mrzFields: parsedMRZData?.fields
|
|
603
|
+
});
|
|
604
|
+
}, 1000);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const hasMRZ = !!mrzText;
|
|
608
|
+
const hasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
609
|
+
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
610
|
+
// AND ensure all required fields are present
|
|
611
|
+
const mrzAccepted = parsedMRZData?.valid === true && hasRequiredFields && mrzStableAndValid;
|
|
612
|
+
const barcodeMatchesMRZ = barcodeToUse?.rawValue?.trim() === parsedMRZData?.fields?.optional1?.trim();
|
|
613
|
+
// Require barcode for all documents (no special card fallback)
|
|
614
|
+
const barcodeAccepted = onlyMRZScan || barcodeMatchesMRZ;
|
|
615
|
+
|
|
616
|
+
// CRITICAL: Require all document elements to be in frame before accepting
|
|
617
|
+
// For ID_BACK: MRZ + barcode (check directly, not via state to avoid timing issues)
|
|
618
|
+
const hasBarcode = !!barcodeToUse?.rawValue;
|
|
619
|
+
const allRequiredElementsInFrame = hasMRZ && hasBarcode || onlyMRZScan;
|
|
620
|
+
|
|
621
|
+
// Don't block based on bounds - just ensure elements are present
|
|
622
|
+
setElementsOutsideScanArea([]);
|
|
623
|
+
if (!allRequiredElementsInFrame && hasMRZ && mrzAccepted) {
|
|
624
|
+
if (isDebugEnabled()) {
|
|
625
|
+
console.log('[ID_BACK Scan] MRZ valid but waiting for all elements in frame (MRZ + barcode)');
|
|
626
|
+
}
|
|
627
|
+
setStatus('SCANNING');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (hasMRZ && mrzAccepted && barcodeAccepted && allRequiredElementsInFrame) {
|
|
631
|
+
logMRZDetails('ID_BACK Scan', parsedMRZData?.fields, mrzText, validMRZConsecutiveCount.current, isDebugEnabled());
|
|
632
|
+
const scannedData = {
|
|
633
|
+
image,
|
|
634
|
+
documentType: 'ID_BACK',
|
|
635
|
+
mrzText: mrzText ?? undefined,
|
|
636
|
+
mrzFields: parsedMRZData?.fields,
|
|
637
|
+
barcodeValue: barcodeToUse?.rawValue ?? undefined
|
|
638
|
+
};
|
|
639
|
+
setDetectedDocumentType('ID_BACK');
|
|
640
|
+
setStatus('SCANNED');
|
|
641
|
+
setIsTorchOn(false);
|
|
642
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
|
|
643
|
+
setTimeout(() => {
|
|
644
|
+
onIdentityDocumentScanned(scannedData);
|
|
645
|
+
}, 1000);
|
|
646
|
+
} else {
|
|
647
|
+
if (hasMRZ && !mrzAccepted) {
|
|
648
|
+
logMRZValidationFailure('ID_BACK Scan', hasRequiredFields, parsedMRZData, mrzDetectionCurrentRetryCount.current, isDebugEnabled());
|
|
649
|
+
} else if (hasMRZ && mrzAccepted && !barcodeAccepted) {
|
|
650
|
+
if (isDebugEnabled()) {
|
|
651
|
+
console.log('[ID_BACK Scan] MRZ valid but barcode check failed - retrying', {
|
|
652
|
+
onlyMRZScan,
|
|
653
|
+
hasBarcodeValue: !!barcodeToUse?.rawValue,
|
|
654
|
+
barcodeMatchesMRZ,
|
|
655
|
+
mrzOptional1: parsedMRZData?.fields?.optional1,
|
|
656
|
+
barcodeValue: barcodeToUse?.rawValue,
|
|
657
|
+
barcodeSource: barcodeToUse === cachedBarcode.current ? 'cached' : 'current'
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
662
|
+
setStatus(hasMRZ ? 'SCANNING' : 'SEARCHING');
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const documentType = detectDocumentType(cardSizedFaces, text, parsedMRZData?.fields, frameWidth, mrzText);
|
|
667
|
+
|
|
668
|
+
// Update detected document type only during initial scan step
|
|
669
|
+
// CRITICAL: Only set document type from non-blurry, stable frames
|
|
670
|
+
// Once set to PASSPORT or definitively identified, preserve it to avoid incorrect flow transitions
|
|
671
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && detectedDocumentType === 'UNKNOWN') {
|
|
672
|
+
// Determine the document type to set based on current frame analysis
|
|
673
|
+
let docTypeToSet = documentType;
|
|
674
|
+
if (documentType === 'PASSPORT') {
|
|
675
|
+
// Passport detected definitively - candidate for locking in
|
|
676
|
+
docTypeToSet = 'PASSPORT';
|
|
677
|
+
} else if (documentType === 'UNKNOWN' && cardSizedFaces.length > 0 && parsedMRZData?.fields?.documentCode === 'P') {
|
|
678
|
+
// Early passport detection: face detected + passport MRZ code (even if not fully valid yet)
|
|
679
|
+
docTypeToSet = 'PASSPORT';
|
|
680
|
+
} else if (documentType === 'ID_FRONT') {
|
|
681
|
+
// Check if this is actually a passport based on MRZ code
|
|
682
|
+
// Passports can be misdetected as ID_FRONT when signature-like text is visible
|
|
683
|
+
if (parsedMRZData?.fields?.documentCode === 'P') {
|
|
684
|
+
if (isDebugEnabled()) {
|
|
685
|
+
console.log('[DocType] Correcting misdetection: ID_FRONT → PASSPORT (MRZ code P)');
|
|
686
|
+
}
|
|
687
|
+
docTypeToSet = 'PASSPORT';
|
|
688
|
+
} else if (parsedMRZData?.fields?.documentCode === 'I') {
|
|
689
|
+
// MRZ confirms it's an ID card
|
|
690
|
+
docTypeToSet = 'ID_FRONT';
|
|
691
|
+
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
692
|
+
// Passport MRZ pattern visible but not parsed yet - wait for proper classification
|
|
693
|
+
if (isDebugEnabled()) {
|
|
694
|
+
console.log('[DocType] Passport MRZ pattern detected but not parsed - waiting instead of locking as ID_FRONT');
|
|
695
|
+
}
|
|
696
|
+
docTypeToSet = 'UNKNOWN';
|
|
697
|
+
} else {
|
|
698
|
+
// No MRZ code and no passport pattern - safe to classify as ID_FRONT
|
|
699
|
+
// ID cards typically don't have MRZ on front (only on back)
|
|
700
|
+
docTypeToSet = 'ID_FRONT';
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
docTypeToSet = 'UNKNOWN';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Only update document type state if:
|
|
707
|
+
// 1. Frame quality is acceptable (not blurry, good brightness)
|
|
708
|
+
// 2. Document type has been detected consistently for multiple frames
|
|
709
|
+
if (lastFrameQuality.current.hasAcceptableQuality && docTypeToSet !== 'UNKNOWN') {
|
|
710
|
+
if (docTypeToSet === lastDetectedDocType.current) {
|
|
711
|
+
consistentDocTypeCount.current++;
|
|
712
|
+
if (isDebugEnabled()) {
|
|
713
|
+
console.log(`[DocType Stability] Consistent detection: ${docTypeToSet} (${consistentDocTypeCount.current}/${REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS})`);
|
|
714
|
+
}
|
|
715
|
+
if (consistentDocTypeCount.current >= REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS) {
|
|
716
|
+
// Stable detection confirmed - lock it in
|
|
717
|
+
if (isDebugEnabled()) {
|
|
718
|
+
console.log(`[DocType] Locked document type: ${docTypeToSet} (stable for ${consistentDocTypeCount.current} quality frames)`);
|
|
719
|
+
}
|
|
720
|
+
setDetectedDocumentType(docTypeToSet);
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
// Document type changed - reset counter
|
|
724
|
+
if (isDebugEnabled()) {
|
|
725
|
+
console.log(`[DocType Stability] Type changed: ${lastDetectedDocType.current} → ${docTypeToSet}, resetting counter`);
|
|
726
|
+
}
|
|
727
|
+
lastDetectedDocType.current = docTypeToSet;
|
|
728
|
+
consistentDocTypeCount.current = 1;
|
|
729
|
+
}
|
|
730
|
+
} else if (!lastFrameQuality.current.hasAcceptableQuality && docTypeToSet !== 'UNKNOWN') {
|
|
731
|
+
// Poor quality frame - don't use for document type detection
|
|
732
|
+
if (isDebugEnabled()) {
|
|
733
|
+
console.log(`[DocType Stability] Skipping poor quality frame (blurry: ${lastFrameQuality.current.isBlurry}, brightness: ${lastFrameQuality.current.brightness.toFixed(0)})`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// Document type is now locked and won't be changed after initial scan
|
|
738
|
+
// Hologram and subsequent steps use the preserved detectedDocumentType state
|
|
739
|
+
|
|
510
740
|
const scannedData = {
|
|
511
741
|
image,
|
|
512
742
|
documentType,
|
|
513
743
|
mrzText: mrzText ?? undefined,
|
|
514
744
|
mrzFields: parsedMRZData?.fields
|
|
515
745
|
};
|
|
516
|
-
|
|
517
|
-
setCurrentFaceImage(croppedFaces[0]);
|
|
518
|
-
|
|
519
|
-
// Track detected document type for UI feedback
|
|
520
|
-
if (documentType !== 'UNKNOWN') {
|
|
521
|
-
setDetectedDocumentType(documentType);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Detect wrong side based on document type or face presence (works for both normal and eID scan)
|
|
525
|
-
// For ID_BACK step: if faces are detected, it's likely the front side (wrong)
|
|
526
|
-
// For FRONT step: if ID_BACK is detected, it's the wrong side
|
|
527
|
-
const isWrongSide = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && documentType === 'ID_BACK' || nextStep === 'SCAN_ID_BACK' && (documentType === 'ID_FRONT' || documentType === 'PASSPORT' || croppedFaces.length > 0);
|
|
746
|
+
const isWrongSide = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && documentType === 'ID_BACK';
|
|
528
747
|
if (isWrongSide) {
|
|
529
748
|
setStatus('INCORRECT');
|
|
530
749
|
return;
|
|
531
750
|
}
|
|
751
|
+
|
|
752
|
+
// Always use locked face if available
|
|
753
|
+
if (faceImageToUse) {
|
|
754
|
+
scannedData.faceImage = faceImageToUse;
|
|
755
|
+
}
|
|
532
756
|
if (!onlyMRZScan) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
757
|
+
// Hologram detection during SCAN_HOLOGRAM step - ALWAYS use first/leftmost face ONLY
|
|
758
|
+
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
759
|
+
if (isDebugEnabled()) {
|
|
760
|
+
console.log(`[Hologram] In SCAN_HOLOGRAM step - currentHologramImage: ${!!currentHologramImage}, collected: ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT}`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Always crop to the same face region across all hologram frames so
|
|
764
|
+
// OpenCV receives consistently-sized images for comparison.
|
|
765
|
+
// Use current face bounds if available, otherwise fall back to last known position.
|
|
766
|
+
const hologramFaceBounds = cardSizedFaces.length > 0 && cardSizedFaces[0] ? cardSizedFaces[0].bounds : lastFacePosition.current;
|
|
767
|
+
let primaryFaceOnly;
|
|
768
|
+
if (hologramFaceBounds && image) {
|
|
769
|
+
const hologramCropped = await getFaceImages([{
|
|
770
|
+
bounds: hologramFaceBounds,
|
|
771
|
+
rollAngle: 0,
|
|
772
|
+
yawAngle: 0
|
|
773
|
+
}], image, frameWidth, frameHeight);
|
|
774
|
+
primaryFaceOnly = hologramCropped[0] ?? faceImageToUse;
|
|
536
775
|
} else {
|
|
537
|
-
|
|
538
|
-
setCurrentFaceImage(croppedFaces[0]);
|
|
776
|
+
primaryFaceOnly = faceImageToUse;
|
|
539
777
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
778
|
+
|
|
779
|
+
// Skip face position validation for hologram — flash toggling causes position jitter
|
|
780
|
+
if (primaryFaceOnly) {
|
|
781
|
+
// Reset consecutive no-face counter since we have a face
|
|
782
|
+
hologramFramesWithoutFace.current = 0;
|
|
783
|
+
if (currentHologramImage) {
|
|
784
|
+
scannedData.hologramImage = currentHologramImage;
|
|
785
|
+
} else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
|
|
786
|
+
// Add timing control to space out captures for better variation
|
|
787
|
+
const now = Date.now();
|
|
788
|
+
const timeSinceLastCapture = now - lastHologramCaptureTime.current;
|
|
789
|
+
if (faceImages.current.length === 0 || timeSinceLastCapture >= HOLOGRAM_CAPTURE_INTERVAL) {
|
|
790
|
+
// Collect PRIMARY face image ONLY (always index 0) from same document plane
|
|
791
|
+
faceImages.current.push(primaryFaceOnly);
|
|
792
|
+
lastHologramCaptureTime.current = now;
|
|
793
|
+
hologramImageCountRef.current = faceImages.current.length;
|
|
794
|
+
|
|
795
|
+
// Only update state at first and last frame to minimize re-renders
|
|
796
|
+
if (faceImages.current.length === 1 || faceImages.current.length === HOLOGRAM_IMAGE_COUNT) {
|
|
797
|
+
setHologramImageCount(faceImages.current.length);
|
|
798
|
+
setLatestHologramFaceImage(primaryFaceOnly);
|
|
799
|
+
}
|
|
800
|
+
if (isDebugEnabled()) {
|
|
801
|
+
console.log(`[Hologram] Collected ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT} face images`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Keep flash on during processing - will turn off when step changes
|
|
805
|
+
}
|
|
806
|
+
} else if (faceImages.current.length >= HOLOGRAM_IMAGE_COUNT) {
|
|
807
|
+
// Process collected full document images
|
|
808
|
+
if (isDebugEnabled()) {
|
|
809
|
+
console.log(`[Hologram] Processing ${faceImages.current.length} full document images`);
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
const [hologramMask, hologram] = await detectHologramNative(faceImages.current);
|
|
813
|
+
if (hologram) {
|
|
814
|
+
setCurrentHologramMaskImage(hologramMask);
|
|
815
|
+
scannedData.hologramImage = hologram;
|
|
816
|
+
setCurrentHologramImage(hologram);
|
|
817
|
+
if (isDebugEnabled()) {
|
|
818
|
+
console.log('[Hologram] Detection successful');
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
if (isDebugEnabled()) {
|
|
822
|
+
console.log('[Hologram] No hologram detected');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error('[Hologram] Processing error:', error);
|
|
827
|
+
} finally {
|
|
828
|
+
// Keep flash on - will turn off when step changes
|
|
829
|
+
faceImages.current = [];
|
|
830
|
+
hologramImageCountRef.current = 0;
|
|
831
|
+
setHologramImageCount(0);
|
|
832
|
+
setLatestHologramFaceImage(undefined);
|
|
833
|
+
hologramDetectionCurrentRetryCount.current++;
|
|
834
|
+
if (isDebugEnabled()) {
|
|
835
|
+
console.log(`[Hologram] Retry ${hologramDetectionCurrentRetryCount.current}/${HOLOGRAM_DETECTION_RETRY_COUNT}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
545
838
|
}
|
|
546
|
-
faceImages.push(croppedFaces[0]);
|
|
547
839
|
} else {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
840
|
+
// No face detected for hologram collection
|
|
841
|
+
// Track consecutive frames without face for safety timeout
|
|
842
|
+
hologramFramesWithoutFace.current++;
|
|
843
|
+
if (isDebugEnabled()) {
|
|
844
|
+
console.log(`[Hologram] No face detected - frame ${hologramFramesWithoutFace.current}/${HOLOGRAM_MAX_FRAMES_WITHOUT_FACE}`);
|
|
553
845
|
}
|
|
554
|
-
faceImages = [];
|
|
555
|
-
hologramDetectionCurrentRetryCount.value++;
|
|
556
846
|
}
|
|
847
|
+
} else if (currentHologramImage) {
|
|
848
|
+
scannedData.hologramImage = currentHologramImage;
|
|
849
|
+
} else if (faceImages.current.length > 0) {
|
|
850
|
+
// Safety cleanup: not in hologram step but have images collected
|
|
851
|
+
faceImages.current = [];
|
|
852
|
+
hologramImageCountRef.current = 0;
|
|
853
|
+
setHologramImageCount(0);
|
|
854
|
+
setLatestHologramFaceImage(undefined);
|
|
855
|
+
if (isDebugEnabled()) {
|
|
856
|
+
console.log('[Hologram] Defensive cleanup - cleared images outside hologram step');
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// SKIP secondary face detection during SCAN_HOLOGRAM - only focus on primary face
|
|
861
|
+
// Secondary face was already captured during initial scan (SCAN_ID_FRONT_OR_PASSPORT)
|
|
862
|
+
// During hologram, we only collect hologram images from primary face
|
|
863
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
864
|
+
// Capture secondary face - must be similar to main face AND from same document plane
|
|
557
865
|
if (currentSecondaryFaceImage) {
|
|
558
866
|
scannedData.secondaryFaceImage = currentSecondaryFaceImage;
|
|
559
|
-
} else if (!!scannedData.faceImage && croppedFaces.length > 1 && !!croppedFaces[1] &&
|
|
560
|
-
|
|
561
|
-
|
|
867
|
+
} else if (!!scannedData.faceImage && croppedFaces.length > 1 && !!croppedFaces[1] && facePositionValid) {
|
|
868
|
+
// Always validate similarity to ensure it's the same person on the same document
|
|
869
|
+
const isSimilar = await areImagesSimilarNative(scannedData.faceImage, croppedFaces[1], 15000 // Default threshold from main branch
|
|
870
|
+
);
|
|
871
|
+
if (isSimilar) {
|
|
872
|
+
scannedData.secondaryFaceImage = croppedFaces[1];
|
|
873
|
+
setCurrentSecondaryFaceImage(scannedData.secondaryFaceImage);
|
|
874
|
+
|
|
875
|
+
// Update secondary face bounds for debug overlay
|
|
876
|
+
if (faces.length > 1 && faces[1] && frameDimensions) {
|
|
877
|
+
const screen = Dimensions.get('window');
|
|
878
|
+
const frameAspect = frameDimensions.width / frameDimensions.height;
|
|
879
|
+
const screenAspect = screen.width / screen.height;
|
|
880
|
+
let scale;
|
|
881
|
+
let offsetX = 0;
|
|
882
|
+
let offsetY = 0;
|
|
883
|
+
if (frameAspect > screenAspect) {
|
|
884
|
+
scale = screen.height / frameDimensions.height;
|
|
885
|
+
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
886
|
+
} else {
|
|
887
|
+
scale = screen.width / frameDimensions.width;
|
|
888
|
+
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
889
|
+
}
|
|
890
|
+
const scanLeft = (screen.width * 0.05 + offsetX) / scale;
|
|
891
|
+
const scanTop = (screen.height * 0.36 + offsetY) / scale;
|
|
892
|
+
const scanRight = (screen.width * 0.95 + offsetX) / scale;
|
|
893
|
+
const scanBottom = (screen.height * 0.64 + offsetY) / scale;
|
|
894
|
+
const isInsideScan = (x, y, w, h) => x >= scanLeft && y >= scanTop && x + w <= scanRight && y + h <= scanBottom;
|
|
895
|
+
const secondaryBounds = faces[1].bounds;
|
|
896
|
+
if (isInsideScan(secondaryBounds.x, secondaryBounds.y, secondaryBounds.width, secondaryBounds.height)) {
|
|
897
|
+
setSecondaryFaceBounds({
|
|
898
|
+
x: secondaryBounds.x * scale - offsetX,
|
|
899
|
+
y: secondaryBounds.y * scale - offsetY,
|
|
900
|
+
width: secondaryBounds.width * scale,
|
|
901
|
+
height: secondaryBounds.height * scale
|
|
902
|
+
});
|
|
903
|
+
} else {
|
|
904
|
+
setSecondaryFaceBounds(null);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (isDebugEnabled()) {
|
|
908
|
+
console.log('[SecondaryFace] ✓ Captured and validated as similar to main face (same document plane)');
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
912
|
+
if (isDebugEnabled()) {
|
|
913
|
+
console.log('[SecondaryFace] ✗ Rejected - not similar enough to main face');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
562
916
|
} else {
|
|
563
|
-
secondaryFaceDetectionCurrentRetryCount.
|
|
917
|
+
secondaryFaceDetectionCurrentRetryCount.current++;
|
|
918
|
+
if (!facePositionValid && croppedFaces.length > 1) {
|
|
919
|
+
if (isDebugEnabled()) {
|
|
920
|
+
console.log('[SecondaryFace] ✗ Rejected - document plane changed');
|
|
921
|
+
}
|
|
922
|
+
}
|
|
564
923
|
}
|
|
924
|
+
} else if (currentSecondaryFaceImage) {
|
|
925
|
+
// Already have secondary face from earlier - just use it
|
|
926
|
+
scannedData.secondaryFaceImage = currentSecondaryFaceImage;
|
|
565
927
|
}
|
|
566
928
|
}
|
|
929
|
+
|
|
930
|
+
// UNIFIED SCAN_HOLOGRAM completion - ONLY check primary face and hologram collection
|
|
931
|
+
// Document type is already definitively determined before entering this step
|
|
932
|
+
if (nextStep === 'SCAN_HOLOGRAM') {
|
|
933
|
+
// CRITICAL: Verify correct side is shown - hologram is ALWAYS on the front side with photo
|
|
934
|
+
// If wrong side detected, warn user immediately
|
|
935
|
+
const hasFaces = cardSizedFaces.length > 0;
|
|
936
|
+
const hasBarcode = !!barcode?.rawValue; // ID back has barcode, front doesn't
|
|
937
|
+
|
|
938
|
+
// For passport: back side has no photo and different text pattern
|
|
939
|
+
// For ID card: back side has no photo, has barcode
|
|
940
|
+
const isWrongSideForHologram = !hasFaces || hasBarcode;
|
|
941
|
+
if (isWrongSideForHologram) {
|
|
942
|
+
if (isDebugEnabled()) {
|
|
943
|
+
console.log(`[SCAN_HOLOGRAM] Wrong side detected - no faces: ${!hasFaces}, has barcode: ${hasBarcode} - expecting front side with photo`);
|
|
944
|
+
}
|
|
945
|
+
setStatus('INCORRECT');
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Safety timeout: if we can't detect face for too many consecutive frames, give up
|
|
950
|
+
const faceDetectionTimeout = hologramFramesWithoutFace.current >= HOLOGRAM_MAX_FRAMES_WITHOUT_FACE;
|
|
951
|
+
|
|
952
|
+
// Don't skip if actively collecting images
|
|
953
|
+
const isActivelyCollecting = faceImages.current.length > 0 && faceImages.current.length < HOLOGRAM_IMAGE_COUNT;
|
|
954
|
+
const hologramConditionMet = !!scannedData.hologramImage || hologramDetectionCurrentRetryCount.current >= HOLOGRAM_DETECTION_RETRY_COUNT && !isActivelyCollecting ||
|
|
955
|
+
// Don't skip if mid-collection
|
|
956
|
+
faceDetectionTimeout && !isActivelyCollecting; // Don't timeout if mid-collection
|
|
957
|
+
|
|
958
|
+
// During hologram scan, we ONLY care about hologram collection - no other checks
|
|
959
|
+
// Secondary face, MRZ, document type checks are all skipped
|
|
960
|
+
// Document type was already definitively determined in the initial scan phase
|
|
961
|
+
|
|
962
|
+
// Log detailed state for debugging
|
|
963
|
+
if (isActivelyCollecting && isDebugEnabled()) {
|
|
964
|
+
console.log(`[SCAN_HOLOGRAM] Actively collecting: ${faceImages.current.length}/${HOLOGRAM_IMAGE_COUNT} - continuing collection`);
|
|
965
|
+
}
|
|
966
|
+
if (hologramConditionMet) {
|
|
967
|
+
if (faceDetectionTimeout && isDebugEnabled()) {
|
|
968
|
+
console.log('[SCAN_HOLOGRAM] Face detection timeout - proceeding without hologram');
|
|
969
|
+
}
|
|
970
|
+
setStatus('SCANNED');
|
|
971
|
+
if (nextStep !== 'SCAN_HOLOGRAM') {
|
|
972
|
+
setIsTorchOn(false);
|
|
973
|
+
}
|
|
974
|
+
// Route based on PRESERVED detectedDocumentType state (set during initial scan)
|
|
975
|
+
// Also check current frame's documentType and MRZ code as fallback
|
|
976
|
+
// Passport has no back side - go directly to COMPLETED
|
|
977
|
+
const isPassport = detectedDocumentType === 'PASSPORT' || documentType === 'PASSPORT' || parsedMRZData?.fields?.documentCode === 'P';
|
|
978
|
+
if (isDebugEnabled()) {
|
|
979
|
+
console.log('[SCAN_HOLOGRAM] Document type check:', {
|
|
980
|
+
detectedDocumentType,
|
|
981
|
+
documentType,
|
|
982
|
+
mrzCode: parsedMRZData?.fields?.documentCode,
|
|
983
|
+
isPassport
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
if (isPassport) {
|
|
987
|
+
if (isDebugEnabled()) {
|
|
988
|
+
console.log('[SCAN_HOLOGRAM] Passport detected - completing scan (no back side)');
|
|
989
|
+
}
|
|
990
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
|
|
991
|
+
} else {
|
|
992
|
+
if (isDebugEnabled()) {
|
|
993
|
+
console.log('[SCAN_HOLOGRAM] ID card detected - proceeding to back scan');
|
|
994
|
+
}
|
|
995
|
+
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
|
|
996
|
+
}
|
|
997
|
+
setTimeout(() => {
|
|
998
|
+
onIdentityDocumentScanned(scannedData);
|
|
999
|
+
}, 1000);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// Still collecting or conditions not met - stay in SCAN_HOLOGRAM
|
|
1003
|
+
// Don't fall through to document type branching
|
|
1004
|
+
setStatus('SCANNING');
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
567
1007
|
if (documentType === 'ID_FRONT') {
|
|
568
1008
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1009
|
+
// CRITICAL: Verify this is actually an ID card, not a passport misdetected as ID_FRONT
|
|
1010
|
+
// Passports can show signature-like text and be temporarily classified as ID_FRONT
|
|
1011
|
+
if (parsedMRZData?.fields?.documentCode === 'P') {
|
|
1012
|
+
if (isDebugEnabled()) {
|
|
1013
|
+
console.log('[ID_FRONT Scan] Detected as ID_FRONT but MRZ shows passport (code P) - waiting for passport branch');
|
|
1014
|
+
}
|
|
1015
|
+
setStatus('SCANNING');
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const hasFace = cardSizedFaces.length > 0;
|
|
1019
|
+
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
1020
|
+
const retryThreshold = 60;
|
|
1021
|
+
const allowFaceOnly = mrzDetectionCurrentRetryCount.current > retryThreshold;
|
|
1022
|
+
const allRequiredElementsInFrame = hasFace && (hasSignature || allowFaceOnly);
|
|
1023
|
+
setElementsOutsideScanArea([]);
|
|
1024
|
+
if (!allRequiredElementsInFrame) {
|
|
1025
|
+
console.log('[ID_FRONT Scan] Valid but waiting for all elements in frame (face + signature)');
|
|
1026
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1027
|
+
setStatus('SCANNING');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// CRITICAL: Final verification that this is definitively an ID card before proceeding
|
|
1032
|
+
// Check if we have MRZ and if it indicates ID card (not passport)
|
|
1033
|
+
if (parsedMRZData?.fields?.documentCode) {
|
|
1034
|
+
if (parsedMRZData.fields.documentCode === 'I') {
|
|
1035
|
+
if (isDebugEnabled()) {
|
|
1036
|
+
console.log('[ID_FRONT Scan] MRZ confirms ID card (code I)');
|
|
1037
|
+
}
|
|
1038
|
+
} else if (parsedMRZData.fields.documentCode === 'P') {
|
|
1039
|
+
if (isDebugEnabled()) {
|
|
1040
|
+
console.log('[ID_FRONT Scan] MRZ shows passport (code P) - rejecting as ID_FRONT');
|
|
1041
|
+
}
|
|
1042
|
+
setStatus('SCANNING');
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
} else if (mrzText && /P<[A-Z]{3}/.test(mrzText)) {
|
|
1046
|
+
// No parsed MRZ BUT passport MRZ pattern visible (P<TUR, P<USA, etc.)
|
|
1047
|
+
// This is likely a passport with OCR errors - wait for proper parsing
|
|
1048
|
+
if (isDebugEnabled()) {
|
|
1049
|
+
console.log('[ID_FRONT Scan] Passport MRZ pattern (P<XXX) visible but not parsed - waiting for passport classification');
|
|
1050
|
+
}
|
|
1051
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1052
|
+
setStatus('SCANNING');
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
// No MRZ or no passport pattern - proceed as ID card
|
|
1056
|
+
// ID cards typically don't have MRZ on front side (only on back)
|
|
1057
|
+
|
|
1058
|
+
// CRITICAL: Lock document type state to ID_FRONT before proceeding
|
|
1059
|
+
// This ensures hologram completion knows it's an ID card (needs ID_BACK step)
|
|
1060
|
+
setDetectedDocumentType('ID_FRONT');
|
|
569
1061
|
setStatus('SCANNED');
|
|
1062
|
+
setIsTorchOn(false);
|
|
570
1063
|
if (onlyMRZScan) {
|
|
571
|
-
|
|
572
|
-
|
|
1064
|
+
// Passport has no back side - go directly to COMPLETED
|
|
1065
|
+
// At this point detectedDocumentType is definitively set
|
|
1066
|
+
if (detectedDocumentType === 'PASSPORT') {
|
|
1067
|
+
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1068
|
+
} else {
|
|
1069
|
+
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1070
|
+
}
|
|
1071
|
+
setTimeout(() => {
|
|
1072
|
+
onIdentityDocumentScanned(scannedData);
|
|
1073
|
+
}, 1000);
|
|
573
1074
|
} else {
|
|
1075
|
+
if (isDebugEnabled()) {
|
|
1076
|
+
console.log('[ID_FRONT Scan] Confirmed as ID card - proceeding to hologram');
|
|
1077
|
+
}
|
|
574
1078
|
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1079
|
+
setTimeout(() => {
|
|
1080
|
+
onIdentityDocumentScanned(scannedData);
|
|
1081
|
+
}, 1000);
|
|
575
1082
|
}
|
|
576
|
-
} else if (nextStep === 'SCAN_HOLOGRAM' && (!!scannedData.hologramImage || hologramDetectionCurrentRetryCount.value >= HOLOGRAM_DETECTION_RETRY_COUNT) && (!!scannedData.secondaryFaceImage || secondaryFaceDetectionCurrentRetryCount.value >= SECOND_FACE_DETECTION_RETRY_COUNT)) {
|
|
577
|
-
setStatus('SCANNED');
|
|
578
|
-
setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
|
|
579
|
-
onIdentityDocumentScanned(scannedData);
|
|
580
1083
|
}
|
|
1084
|
+
// Note: SCAN_HOLOGRAM completion is now handled in the unified block above
|
|
581
1085
|
} else if (documentType === 'PASSPORT') {
|
|
582
1086
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && !scannedData.hologramImage) {
|
|
583
|
-
// For passport, require valid MRZ before proceeding
|
|
584
1087
|
if (onlyMRZScan) {
|
|
585
|
-
|
|
586
|
-
|
|
1088
|
+
const hasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
1089
|
+
// CRITICAL: Only accept MRZ with valid checksums AND consistent reads
|
|
1090
|
+
if (!!scannedData.mrzText && hasRequiredFields && mrzStableAndValid) {
|
|
1091
|
+
const hasFace = cardSizedFaces.length > 0;
|
|
1092
|
+
const hasMRZ = !!mrzText;
|
|
1093
|
+
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1094
|
+
setElementsOutsideScanArea([]);
|
|
1095
|
+
if (!allRequiredElementsInFrame) {
|
|
1096
|
+
console.log('[Passport Scan] MRZ valid but waiting for all elements in frame (face + MRZ)');
|
|
1097
|
+
setStatus('SCANNING');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
logMRZDetails('Passport Scan', parsedMRZData?.fields, mrzText, validMRZConsecutiveCount.current, isDebugEnabled());
|
|
1101
|
+
setDetectedDocumentType('PASSPORT');
|
|
587
1102
|
setStatus('SCANNED');
|
|
1103
|
+
setIsTorchOn(false);
|
|
588
1104
|
setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1105
|
+
setTimeout(() => {
|
|
1106
|
+
onIdentityDocumentScanned(scannedData);
|
|
1107
|
+
}, 1000);
|
|
1108
|
+
return; // CRITICAL: Exit after MRZ-only scan completes to prevent fall-through
|
|
1109
|
+
} else {
|
|
1110
|
+
if (!!scannedData.mrzText && !mrzStableAndValid) {
|
|
1111
|
+
logMRZValidationFailure('Passport Scan', hasRequiredFields, parsedMRZData, mrzDetectionCurrentRetryCount.current, isDebugEnabled());
|
|
1112
|
+
}
|
|
1113
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
592
1114
|
setStatus('SCANNING');
|
|
1115
|
+
return; // Don't fall through to else-if
|
|
593
1116
|
}
|
|
594
1117
|
} else {
|
|
595
|
-
// Normal scan
|
|
1118
|
+
// Normal passport scan (with hologram) - require MRZ to be detected before proceeding
|
|
1119
|
+
const hasFace = cardSizedFaces.length > 0;
|
|
1120
|
+
const hasMRZ = !!mrzText;
|
|
1121
|
+
const allRequiredElementsInFrame = hasFace && hasMRZ;
|
|
1122
|
+
setElementsOutsideScanArea([]);
|
|
1123
|
+
if (!allRequiredElementsInFrame) {
|
|
1124
|
+
console.log('[Passport Scan] Valid but waiting for all elements in frame (face + MRZ)');
|
|
1125
|
+
setStatus('SCANNING');
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// CRITICAL: Final verification - ensure MRZ definitively identifies this as passport
|
|
1130
|
+
// This must pass before we can proceed to hologram
|
|
1131
|
+
if (!parsedMRZData?.fields?.documentCode || parsedMRZData.fields.documentCode !== 'P') {
|
|
1132
|
+
console.log('[Passport Scan] MRZ detected but not confirmed as passport (code:', parsedMRZData?.fields?.documentCode || 'none', ') - waiting for valid passport MRZ');
|
|
1133
|
+
setStatus('SCANNING');
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
console.log('[Passport Scan] MRZ confirmed passport (code P) - definitively identified, proceeding to hologram');
|
|
1137
|
+
// CRITICAL: Lock document type state to PASSPORT before proceeding to hologram
|
|
1138
|
+
// This ensures hologram completion knows it's a passport (no ID_BACK step)
|
|
1139
|
+
setDetectedDocumentType('PASSPORT');
|
|
596
1140
|
setStatus('SCANNED');
|
|
1141
|
+
setIsTorchOn(false);
|
|
597
1142
|
setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
|
|
1143
|
+
setTimeout(() => {
|
|
1144
|
+
onIdentityDocumentScanned(scannedData);
|
|
1145
|
+
}, 1000);
|
|
598
1146
|
}
|
|
599
|
-
} else if ((nextStep === 'SCAN_HOLOGRAM' && (!!scannedData.hologramImage || hologramDetectionCurrentRetryCount.value >= HOLOGRAM_DETECTION_RETRY_COUNT) && (!!scannedData.secondaryFaceImage || secondaryFaceDetectionCurrentRetryCount.value >= SECOND_FACE_DETECTION_RETRY_COUNT) || onlyMRZScan) && !!scannedData.mrzText && (parsedMRZData?.valid || mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)) {
|
|
600
|
-
setStatus('SCANNED');
|
|
601
|
-
setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
|
|
602
|
-
onIdentityDocumentScanned(scannedData);
|
|
603
|
-
} else if (!parsedMRZData?.valid) {
|
|
604
|
-
mrzDetectionCurrentRetryCount.value++;
|
|
605
1147
|
}
|
|
1148
|
+
// Note: SCAN_HOLOGRAM completion is now handled in the unified block above
|
|
606
1149
|
} else if (documentType === 'ID_BACK') {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
onIdentityDocumentScanned(scannedData);
|
|
612
|
-
} else if (!parsedMRZData?.valid) {
|
|
613
|
-
mrzDetectionCurrentRetryCount.value++;
|
|
614
|
-
}
|
|
1150
|
+
// ID_BACK is now handled in the early-return path above (SCAN_ID_BACK)
|
|
1151
|
+
// This branch only triggers if somehow ID_BACK is detected during a non-back-scan step
|
|
1152
|
+
mrzDetectionCurrentRetryCount.current++;
|
|
1153
|
+
setStatus('SCANNING');
|
|
615
1154
|
} else {
|
|
1155
|
+
// Document type UNKNOWN - continue scanning until we can classify it
|
|
1156
|
+
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1157
|
+
console.log('[Document Scan] Type UNKNOWN - waiting for clearer detection (faces:', cardSizedFaces.length, 'mrzCode:', parsedMRZData?.fields?.documentCode || 'none', 'text length:', text.length, ')');
|
|
1158
|
+
}
|
|
616
1159
|
setStatus('SCANNING');
|
|
617
1160
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
} catch (bufferError) {
|
|
623
|
-
// Ignore buffer cleanup errors
|
|
624
|
-
console.warn('Buffer cleanup error:', bufferError);
|
|
1161
|
+
}, [nextStep, frameDimensions, currentHologramImage, currentFaceImage, hasRequiredMRZFields, areMRZFieldsEqual, detectedDocumentType, onlyMRZScan, isTorchOn, setIsTorchOn, setNextStepAndVibrate, onIdentityDocumentScanned, logMRZDetails, logMRZValidationFailure, currentSecondaryFaceImage, detectHologramNative]);
|
|
1162
|
+
const handleFrame = useCallback(async event => {
|
|
1163
|
+
if (!isCameraInitialized.current) {
|
|
1164
|
+
return;
|
|
625
1165
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// Focus trigger for when blur is detected (called from worklet)
|
|
632
|
-
const triggerFocus = useRunOnJS(async () => {
|
|
633
|
-
if (!cameraRef.current || !device?.supportsFocus) {
|
|
1166
|
+
const {
|
|
1167
|
+
frame
|
|
1168
|
+
} = event.nativeEvent;
|
|
1169
|
+
if (!frame.width || !frame.height || frame.width <= 0 || frame.height <= 0) {
|
|
634
1170
|
return;
|
|
635
1171
|
}
|
|
1172
|
+
const base64Image = frame.base64Image;
|
|
1173
|
+
if (!base64Image) return;
|
|
1174
|
+
const frameBrightness = frame.brightness ?? 128;
|
|
1175
|
+
brightnessHistory.current.push(frameBrightness);
|
|
1176
|
+
if (brightnessHistory.current.length > 5) {
|
|
1177
|
+
brightnessHistory.current.shift();
|
|
1178
|
+
}
|
|
1179
|
+
const avgBrightness = brightnessHistory.current.reduce((a, b) => a + b, 0) / brightnessHistory.current.length;
|
|
1180
|
+
const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
|
|
1181
|
+
setIsBrightnessLow(!isOverallBright);
|
|
1182
|
+
|
|
1183
|
+
// Check blur only in center region (area of interest) to avoid false positives
|
|
1184
|
+
// from iOS depth-of-field background blur
|
|
1185
|
+
let isNotBlurry = true;
|
|
1186
|
+
let isBlurry = false; // Track blur state for quality metrics
|
|
636
1187
|
try {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1188
|
+
// Check blur in center 60% of frame (0.6 width x 0.6 height)
|
|
1189
|
+
// Center position: 50% x, 50% y
|
|
1190
|
+
isBlurry = await OpenCVModule.checkBlurryInRegion(base64Image, 0.5,
|
|
1191
|
+
// centerXPercent
|
|
1192
|
+
0.5,
|
|
1193
|
+
// centerYPercent
|
|
1194
|
+
0.6,
|
|
1195
|
+
// widthPercent
|
|
1196
|
+
0.6,
|
|
1197
|
+
// heightPercent
|
|
1198
|
+
60 // threshold
|
|
1199
|
+
);
|
|
1200
|
+
isNotBlurry = !isBlurry;
|
|
1201
|
+
setIsFrameBlurry(isBlurry);
|
|
644
1202
|
} catch (error) {
|
|
645
|
-
|
|
1203
|
+
setIsFrameBlurry(false);
|
|
646
1204
|
}
|
|
647
|
-
}, [device, format]);
|
|
648
|
-
const handleExposureAndBrightness = frame => {
|
|
649
|
-
'worklet';
|
|
650
|
-
|
|
651
|
-
const averageBrightness = getAverageBrightness(frame);
|
|
652
|
-
const minExposure = device?.minExposure ?? 0;
|
|
653
|
-
const maxExposure = device?.maxExposure ?? 0;
|
|
654
|
-
|
|
655
|
-
// Dynamic thresholds based on scanning state using config values
|
|
656
|
-
// Face detection requires higher minimum brightness for reliable detection
|
|
657
|
-
const isFrontOrPassport = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
658
|
-
const isBack = nextStep === 'SCAN_ID_BACK';
|
|
659
|
-
|
|
660
|
-
// Using config values: faceDetection { low: 50, high: 110, target: 85 }, mrzScanning { low: 45, high: 130, target: 80 }
|
|
661
|
-
const lowerBrightnessBound = isFrontOrPassport ? 50 : 40;
|
|
662
|
-
const upperBrightnessBound = isBack ? 130 : 120;
|
|
663
|
-
const targetBrightness = isFrontOrPassport ? 85 : 80;
|
|
664
|
-
|
|
665
|
-
// Smooth exposure adjustment with hysteresis to prevent oscillation
|
|
666
|
-
// Only adjust if brightness is significantly outside the acceptable range
|
|
667
|
-
const hysteresis = 5; // Dead zone to prevent jitter
|
|
668
|
-
|
|
669
|
-
if (averageBrightness < lowerBrightnessBound - hysteresis && exposureValue.value < maxExposure) {
|
|
670
|
-
// Increase exposure smoothly when too dark
|
|
671
|
-
const step = calculateExposureStep(averageBrightness, targetBrightness);
|
|
672
|
-
exposureValue.value = Math.min(maxExposure, exposureValue.value + step);
|
|
673
|
-
} else if (averageBrightness > upperBrightnessBound + hysteresis && exposureValue.value > minExposure) {
|
|
674
|
-
// Decrease exposure smoothly when too bright
|
|
675
|
-
const step = calculateExposureStep(averageBrightness, targetBrightness);
|
|
676
|
-
exposureValue.value = Math.max(minExposure, exposureValue.value - step);
|
|
677
|
-
}
|
|
678
|
-
// When within acceptable range (with hysteresis), don't adjust - prevents oscillation
|
|
679
1205
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
handleBrightness(isBright);
|
|
683
|
-
return isBright;
|
|
684
|
-
};
|
|
685
|
-
const handleWorklet = frame => {
|
|
686
|
-
'worklet';
|
|
1206
|
+
// Only proceed if image quality is acceptable
|
|
1207
|
+
const hasAcceptableQuality = isOverallBright && isNotBlurry;
|
|
687
1208
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1209
|
+
// Store quality metrics in ref for access in handleFaceAndText callback
|
|
1210
|
+
lastFrameQuality.current = {
|
|
1211
|
+
hasAcceptableQuality,
|
|
1212
|
+
isBlurry,
|
|
1213
|
+
// Use local variable, not state (which is from previous frame)
|
|
1214
|
+
brightness: avgBrightness
|
|
1215
|
+
};
|
|
1216
|
+
if (!hasAcceptableQuality) {
|
|
1217
|
+
consecutiveQualityFailures.current++;
|
|
1218
|
+
// After max failures, allow capture to prevent indefinite waiting
|
|
1219
|
+
if (consecutiveQualityFailures.current < MAX_CONSECUTIVE_QUALITY_FAILURES) {
|
|
691
1220
|
return;
|
|
692
1221
|
}
|
|
1222
|
+
console.warn('Max quality failures reached, proceeding with current frame');
|
|
1223
|
+
} else {
|
|
1224
|
+
consecutiveQualityFailures.current = 0;
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
// Read faces directly from native ML Kit results
|
|
1228
|
+
let detectedFaces = [];
|
|
1229
|
+
if (faceDetectionEnabled && frame.faces) {
|
|
1230
|
+
detectedFaces = frame.faces.map(f => ({
|
|
1231
|
+
bounds: {
|
|
1232
|
+
x: f.bounds.x,
|
|
1233
|
+
y: f.bounds.y,
|
|
1234
|
+
width: f.bounds.width,
|
|
1235
|
+
height: f.bounds.height
|
|
1236
|
+
},
|
|
1237
|
+
rollAngle: f.rollAngle,
|
|
1238
|
+
yawAngle: f.yawAngle
|
|
1239
|
+
}));
|
|
1240
|
+
faceDetectionErrorCount.current = 0;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Read text directly from native ML Kit results
|
|
1244
|
+
const textBlocks = frame.textBlocks ?? [];
|
|
1245
|
+
const resultText = textBlocks.map(b => b.text).join('\n');
|
|
1246
|
+
const scannedText = {
|
|
1247
|
+
resultText,
|
|
1248
|
+
blocks: textBlocks.map(block => ({
|
|
1249
|
+
blockText: block.text || '',
|
|
1250
|
+
blockFrame: block.blockFrame ?? {
|
|
1251
|
+
x: 0,
|
|
1252
|
+
y: 0,
|
|
1253
|
+
width: 0,
|
|
1254
|
+
height: 0,
|
|
1255
|
+
boundingCenterX: 0,
|
|
1256
|
+
boundingCenterY: 0
|
|
1257
|
+
},
|
|
1258
|
+
blockCornerPoints: [],
|
|
1259
|
+
lines: [],
|
|
1260
|
+
blockLanguages: []
|
|
1261
|
+
}))
|
|
1262
|
+
};
|
|
693
1263
|
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1264
|
+
// Read barcodes directly from native ML Kit results
|
|
1265
|
+
let barcodes = [];
|
|
1266
|
+
if (frame.barcodes) {
|
|
1267
|
+
barcodes = frame.barcodes.map(b => ({
|
|
1268
|
+
rawValue: b.rawValue,
|
|
1269
|
+
displayValue: b.displayValue,
|
|
1270
|
+
format: b.format,
|
|
1271
|
+
boundingBox: b.boundingBox ?? {
|
|
1272
|
+
left: 0,
|
|
1273
|
+
top: 0,
|
|
1274
|
+
right: 0,
|
|
1275
|
+
bottom: 0
|
|
1276
|
+
},
|
|
1277
|
+
cornerPoints: b.cornerPoints ?? []
|
|
1278
|
+
}));
|
|
1279
|
+
|
|
1280
|
+
// Log barcode detection for debugging (only when scanning ID back)
|
|
1281
|
+
if (barcodes.length > 0 && nextStep === 'SCAN_ID_BACK' && isDebugEnabled()) {
|
|
1282
|
+
console.log(`[Barcode JS] Detected ${barcodes.length} barcode(s):`);
|
|
1283
|
+
barcodes.forEach((b, idx) => {
|
|
1284
|
+
const formatNames = {
|
|
1285
|
+
5: 'PDF417',
|
|
1286
|
+
64: 'QR_CODE',
|
|
1287
|
+
1: 'CODE_128',
|
|
1288
|
+
2: 'CODE_39',
|
|
1289
|
+
13: 'EAN_13',
|
|
1290
|
+
8: 'EAN_8',
|
|
1291
|
+
4096: 'AZTEC',
|
|
1292
|
+
16: 'DATA_MATRIX'
|
|
1293
|
+
};
|
|
1294
|
+
const formatName = formatNames[b.format] || `UNKNOWN(${b.format})`;
|
|
1295
|
+
console.log(` [${idx + 1}] ${formatName}: ${b.rawValue.substring(0, 50)}`);
|
|
1296
|
+
});
|
|
707
1297
|
}
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
// Reset blur count on sharp frame
|
|
711
|
-
consecutiveBlurCount.value = 0;
|
|
712
|
-
|
|
713
|
-
// Validate frame dimensions before processing
|
|
714
|
-
if (!frame.width || !frame.height || frame.width <= 0 || frame.height <= 0) {
|
|
715
|
-
console.warn('Invalid frame dimensions:', {
|
|
716
|
-
width: frame.width,
|
|
717
|
-
height: frame.height
|
|
718
|
-
});
|
|
719
|
-
return;
|
|
720
1298
|
}
|
|
721
1299
|
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1300
|
+
// Update all debug overlay bounds continuously when debug mode is enabled
|
|
1301
|
+
if (isDebugEnabled() && frameDimensions) {
|
|
1302
|
+
const screen = Dimensions.get('window');
|
|
1303
|
+
const frameAspect = frameDimensions.width / frameDimensions.height;
|
|
1304
|
+
const screenAspect = screen.width / screen.height;
|
|
1305
|
+
let scale;
|
|
1306
|
+
let offsetX = 0;
|
|
1307
|
+
let offsetY = 0;
|
|
1308
|
+
if (frameAspect > screenAspect) {
|
|
1309
|
+
scale = screen.height / frameDimensions.height;
|
|
1310
|
+
offsetX = (frameDimensions.width * scale - screen.width) / 2;
|
|
1311
|
+
} else {
|
|
1312
|
+
scale = screen.width / frameDimensions.width;
|
|
1313
|
+
offsetY = (frameDimensions.height * scale - screen.height) / 2;
|
|
729
1314
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1315
|
+
const scanLeft = (screen.width * 0.05 + offsetX) / scale;
|
|
1316
|
+
const scanTop = (screen.height * 0.36 + offsetY) / scale;
|
|
1317
|
+
const scanRight = (screen.width * 0.95 + offsetX) / scale;
|
|
1318
|
+
const scanBottom = (screen.height * 0.64 + offsetY) / scale;
|
|
1319
|
+
const isInsideScan = (x, y, w, h) => x >= scanLeft && y >= scanTop && x + w <= scanRight && y + h <= scanBottom;
|
|
1320
|
+
|
|
1321
|
+
// Update barcode bounds
|
|
1322
|
+
if (barcodes.length > 0 && barcodes[0]) {
|
|
1323
|
+
const bbox = barcodes[0].boundingBox;
|
|
1324
|
+
const corners = barcodes[0].cornerPoints;
|
|
1325
|
+
let angle = 0;
|
|
1326
|
+
|
|
1327
|
+
// Calculate angle from corner points if available
|
|
1328
|
+
if (corners && corners.length >= 2) {
|
|
1329
|
+
const transformedCorners = corners.map(c => ({
|
|
1330
|
+
x: c.x * scale - offsetX,
|
|
1331
|
+
y: c.y * scale - offsetY
|
|
1332
|
+
}));
|
|
1333
|
+
// Calculate angle from first two corners (bottom edge)
|
|
1334
|
+
const dx = transformedCorners[1].x - transformedCorners[0].x;
|
|
1335
|
+
const dy = transformedCorners[1].y - transformedCorners[0].y;
|
|
1336
|
+
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1337
|
+
}
|
|
1338
|
+
if (isDebugEnabled()) {
|
|
1339
|
+
console.log('[Debug] Barcode detected:', {
|
|
1340
|
+
bbox,
|
|
1341
|
+
angle
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
setBarcodeBounds({
|
|
1345
|
+
x: bbox.left * scale - offsetX,
|
|
1346
|
+
y: bbox.top * scale - offsetY,
|
|
1347
|
+
width: (bbox.right - bbox.left) * scale,
|
|
1348
|
+
height: (bbox.bottom - bbox.top) * scale,
|
|
1349
|
+
angle,
|
|
1350
|
+
corners: corners?.map(c => ({
|
|
1351
|
+
x: c.x * scale - offsetX,
|
|
1352
|
+
y: c.y * scale - offsetY
|
|
1353
|
+
}))
|
|
1354
|
+
});
|
|
1355
|
+
} else {
|
|
1356
|
+
setBarcodeBounds(null);
|
|
742
1357
|
}
|
|
743
|
-
detectedFaces = []; // Continue without face detection
|
|
744
|
-
}
|
|
745
1358
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1359
|
+
// Update face bounds continuously
|
|
1360
|
+
if (detectedFaces.length > 0 && detectedFaces[0]) {
|
|
1361
|
+
const faceBounds = detectedFaces[0].bounds;
|
|
1362
|
+
const rollAngle = detectedFaces[0].rollAngle;
|
|
1363
|
+
const faceWidth = faceBounds.width * scale;
|
|
1364
|
+
const faceHeight = faceBounds.height * scale;
|
|
1365
|
+
const cropPadding = Math.max(faceWidth * 0.15, faceHeight * 0.15);
|
|
1366
|
+
setDocumentPlaneBounds({
|
|
1367
|
+
x: faceBounds.x * scale - offsetX,
|
|
1368
|
+
y: faceBounds.y * scale - offsetY,
|
|
1369
|
+
width: faceWidth,
|
|
1370
|
+
height: faceHeight,
|
|
1371
|
+
rollAngle,
|
|
1372
|
+
cropPadding
|
|
1373
|
+
});
|
|
1374
|
+
} else {
|
|
1375
|
+
setDocumentPlaneBounds(null);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Update secondary face bounds
|
|
1379
|
+
if (detectedFaces.length > 1 && detectedFaces[1]) {
|
|
1380
|
+
const secondaryBounds = detectedFaces[1].bounds;
|
|
1381
|
+
const rollAngle = detectedFaces[1].rollAngle;
|
|
1382
|
+
const secondaryWidth = secondaryBounds.width * scale;
|
|
1383
|
+
const secondaryHeight = secondaryBounds.height * scale;
|
|
1384
|
+
const cropPadding = Math.max(secondaryWidth * 0.15, secondaryHeight * 0.15);
|
|
1385
|
+
if (isInsideScan(secondaryBounds.x, secondaryBounds.y, secondaryBounds.width, secondaryBounds.height)) {
|
|
1386
|
+
setSecondaryFaceBounds({
|
|
1387
|
+
x: secondaryBounds.x * scale - offsetX,
|
|
1388
|
+
y: secondaryBounds.y * scale - offsetY,
|
|
1389
|
+
width: secondaryWidth,
|
|
1390
|
+
height: secondaryHeight,
|
|
1391
|
+
rollAngle,
|
|
1392
|
+
cropPadding
|
|
1393
|
+
});
|
|
1394
|
+
} else {
|
|
1395
|
+
setSecondaryFaceBounds(null);
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
setSecondaryFaceBounds(null);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Detect MRZ and signature text areas continuously
|
|
1402
|
+
if (textBlocks.length > 0) {
|
|
1403
|
+
console.log('[Debug] Text blocks count:', textBlocks.length);
|
|
1404
|
+
// Find MRZ-like text blocks (bottom area, contains MRZ-like characters)
|
|
1405
|
+
// More strict pattern: look for blocks with 8+ consecutive uppercase/numbers/< characters AND
|
|
1406
|
+
// must contain at least one '<' character (true MRZ characteristic)
|
|
1407
|
+
const mrzPattern = /[A-Z0-9<]{8,}.*</i;
|
|
1408
|
+
const bottomHalf = frame.height * 0.5; // Increased from 0.67 to catch more MRZ blocks
|
|
1409
|
+
|
|
1410
|
+
// Log bottom area blocks for debugging
|
|
1411
|
+
const bottomBlocks = textBlocks.filter(block => block.blockFrame && block.blockFrame.y > bottomHalf);
|
|
1412
|
+
if (bottomBlocks.length > 0) {
|
|
1413
|
+
console.log('[Debug] Bottom area blocks:', bottomBlocks.map(b => b.text.substring(0, 30)));
|
|
1414
|
+
}
|
|
1415
|
+
const mrzBlocks = textBlocks.filter(block => block.blockFrame && block.blockFrame.y > bottomHalf && mrzPattern.test(block.text));
|
|
1416
|
+
console.log('[Debug] MRZ blocks found:', mrzBlocks.length);
|
|
1417
|
+
if (mrzBlocks.length > 0) {
|
|
1418
|
+
const minX = Math.min(...mrzBlocks.map(b => b.blockFrame.x));
|
|
1419
|
+
const minY = Math.min(...mrzBlocks.map(b => b.blockFrame.y));
|
|
1420
|
+
const maxX = Math.max(...mrzBlocks.map(b => b.blockFrame.x + b.blockFrame.width));
|
|
1421
|
+
const maxY = Math.max(...mrzBlocks.map(b => b.blockFrame.y + b.blockFrame.height));
|
|
1422
|
+
|
|
1423
|
+
// Collect all corner points from MRZ blocks
|
|
1424
|
+
const allCornerPoints = mrzBlocks.flatMap(b => b.cornerPoints || []).map(c => ({
|
|
1425
|
+
x: c.x * scale - offsetX,
|
|
1426
|
+
y: c.y * scale - offsetY
|
|
1427
|
+
}));
|
|
1428
|
+
let angle = 0;
|
|
1429
|
+
if (allCornerPoints.length >= 2) {
|
|
1430
|
+
// Calculate angle from first two points
|
|
1431
|
+
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
1432
|
+
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
1433
|
+
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1434
|
+
}
|
|
1435
|
+
console.log('[Debug] MRZ bounds:', {
|
|
1436
|
+
minX,
|
|
1437
|
+
minY,
|
|
1438
|
+
maxX,
|
|
1439
|
+
maxY,
|
|
1440
|
+
angle
|
|
1441
|
+
});
|
|
1442
|
+
setMrzBounds({
|
|
1443
|
+
x: minX * scale - offsetX,
|
|
1444
|
+
y: minY * scale - offsetY,
|
|
1445
|
+
width: (maxX - minX) * scale,
|
|
1446
|
+
height: (maxY - minY) * scale,
|
|
1447
|
+
angle,
|
|
1448
|
+
corners: allCornerPoints.length > 0 ? allCornerPoints : undefined
|
|
1449
|
+
});
|
|
1450
|
+
} else {
|
|
1451
|
+
setMrzBounds(null);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Detect signature area
|
|
1455
|
+
const signaturePattern = /signature|imza|İmza/i;
|
|
1456
|
+
const signatureBlocks = textBlocks.filter(block => block.blockFrame && signaturePattern.test(block.text));
|
|
1457
|
+
if (textBlocks.length > 0 && signatureBlocks.length === 0) {
|
|
1458
|
+
console.log(`[Signature Debug] No signature blocks found. All blocks (${textBlocks.length}):`, textBlocks.map(b => b.text).join(' | '));
|
|
1459
|
+
}
|
|
1460
|
+
if (signatureBlocks.length > 0) {
|
|
1461
|
+
const minX = Math.min(...signatureBlocks.map(b => b.blockFrame.x));
|
|
1462
|
+
const minY = Math.min(...signatureBlocks.map(b => b.blockFrame.y));
|
|
1463
|
+
const maxX = Math.max(...signatureBlocks.map(b => b.blockFrame.x + b.blockFrame.width));
|
|
1464
|
+
const maxY = Math.max(...signatureBlocks.map(b => b.blockFrame.y + b.blockFrame.height));
|
|
1465
|
+
|
|
1466
|
+
// Collect all corner points from signature blocks
|
|
1467
|
+
const allCornerPoints = signatureBlocks.flatMap(b => b.cornerPoints || []).map(c => ({
|
|
1468
|
+
x: c.x * scale - offsetX,
|
|
1469
|
+
y: c.y * scale - offsetY
|
|
1470
|
+
}));
|
|
1471
|
+
let angle = 0;
|
|
1472
|
+
if (allCornerPoints.length >= 2) {
|
|
1473
|
+
// Calculate angle from first two points
|
|
1474
|
+
const dx = allCornerPoints[1].x - allCornerPoints[0].x;
|
|
1475
|
+
const dy = allCornerPoints[1].y - allCornerPoints[0].y;
|
|
1476
|
+
angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1477
|
+
}
|
|
1478
|
+
setSignatureBounds({
|
|
1479
|
+
x: minX * scale - offsetX,
|
|
1480
|
+
y: minY * scale - offsetY,
|
|
1481
|
+
width: (maxX - minX) * scale,
|
|
1482
|
+
height: (maxY - minY) * scale,
|
|
1483
|
+
angle,
|
|
1484
|
+
corners: allCornerPoints.length > 0 ? allCornerPoints : undefined
|
|
1485
|
+
});
|
|
1486
|
+
} else {
|
|
1487
|
+
setSignatureBounds(null);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Check if all required elements are detected based on document type
|
|
1491
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
1492
|
+
// ID Back: MRZ + barcode (barcode optional but preferred)
|
|
1493
|
+
const hasMRZ = mrzBlocks.length > 0;
|
|
1494
|
+
const hasBarcode = barcodes.length > 0 || cachedBarcode.current !== null;
|
|
1495
|
+
const allPresent = hasMRZ && hasBarcode;
|
|
1496
|
+
setAllElementsDetected(allPresent);
|
|
1497
|
+
|
|
1498
|
+
// Don't block based on bounds - allow elements even if slightly outside
|
|
1499
|
+
setElementsOutsideScanArea([]);
|
|
1500
|
+
if (!allPresent) {
|
|
1501
|
+
const missing = [];
|
|
1502
|
+
if (!hasMRZ) missing.push('MRZ');
|
|
1503
|
+
if (!hasBarcode) missing.push('Barcode');
|
|
1504
|
+
console.log(`[Frame Check] Missing elements: ${missing.join(', ')}`);
|
|
1505
|
+
} else {
|
|
1506
|
+
console.log('[Frame Check] ✓ All elements detected in frame');
|
|
1507
|
+
}
|
|
1508
|
+
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1509
|
+
// Check if it's passport (has MRZ) or ID front (no MRZ)
|
|
1510
|
+
const hasMRZ = mrzBlocks.length > 0;
|
|
1511
|
+
const hasFace = detectedFaces.length > 0;
|
|
1512
|
+
const hasSignature = signatureBlocks.length > 0;
|
|
1513
|
+
|
|
1514
|
+
// Don't block based on bounds - allow elements even if slightly outside
|
|
1515
|
+
setElementsOutsideScanArea([]);
|
|
1516
|
+
let allPresent = false;
|
|
1517
|
+
if (hasMRZ) {
|
|
1518
|
+
// Passport: face + MRZ
|
|
1519
|
+
allPresent = hasFace && hasMRZ;
|
|
1520
|
+
if (!allPresent) {
|
|
1521
|
+
const missing = [];
|
|
1522
|
+
if (!hasFace) missing.push('Face');
|
|
1523
|
+
if (!hasMRZ) missing.push('MRZ');
|
|
1524
|
+
console.log(`[Frame Check] Passport - Missing elements: ${missing.join(', ')}`);
|
|
1525
|
+
} else {
|
|
1526
|
+
console.log('[Frame Check] ✓ Passport - All elements detected (face + MRZ)');
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
// ID Front: face + signature
|
|
1530
|
+
allPresent = hasFace && hasSignature;
|
|
1531
|
+
if (!allPresent) {
|
|
1532
|
+
const missing = [];
|
|
1533
|
+
if (!hasFace) missing.push('Face');
|
|
1534
|
+
if (!hasSignature) missing.push('Signature');
|
|
1535
|
+
console.log(`[Frame Check] ID Front - Missing elements: ${missing.join(', ')}`);
|
|
1536
|
+
} else {
|
|
1537
|
+
console.log('[Frame Check] ✓ ID Front - All elements detected (face + signature)');
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
setAllElementsDetected(allPresent);
|
|
1541
|
+
} else {
|
|
1542
|
+
setAllElementsDetected(false);
|
|
1543
|
+
setElementsOutsideScanArea([]);
|
|
1544
|
+
}
|
|
1545
|
+
} else {
|
|
1546
|
+
setMrzBounds(null);
|
|
1547
|
+
setSignatureBounds(null);
|
|
1548
|
+
setAllElementsDetected(false);
|
|
1549
|
+
setElementsOutsideScanArea([]);
|
|
1550
|
+
}
|
|
1551
|
+
} else if (!isDebugEnabled()) {
|
|
1552
|
+
// Clear all bounds when debug mode is disabled
|
|
1553
|
+
setBarcodeBounds(null);
|
|
1554
|
+
setDocumentPlaneBounds(null);
|
|
1555
|
+
setSecondaryFaceBounds(null);
|
|
1556
|
+
setMrzBounds(null);
|
|
1557
|
+
setSignatureBounds(null);
|
|
762
1558
|
}
|
|
763
1559
|
|
|
764
|
-
//
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1560
|
+
// Update allElementsDetected for status text display (regardless of debug mode)
|
|
1561
|
+
if (nextStep === 'SCAN_ID_BACK') {
|
|
1562
|
+
const hasMRZ = textBlocks.some(b => /[A-Z0-9<]{8,}.*</i.test(b.text));
|
|
1563
|
+
const hasBarcode = barcodes.length > 0 || cachedBarcode.current !== null;
|
|
1564
|
+
setAllElementsDetected(hasMRZ && hasBarcode);
|
|
1565
|
+
} else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1566
|
+
const hasMRZ = textBlocks.some(b => /[A-Z0-9<]{8,}.*</i.test(b.text));
|
|
1567
|
+
const hasFace = detectedFaces.length > 0;
|
|
1568
|
+
const hasSignature = textBlocks.some(b => /signature|imza|İmza/i.test(b.text));
|
|
1569
|
+
setAllElementsDetected(hasMRZ ? hasFace && hasMRZ : hasFace && hasSignature);
|
|
1570
|
+
} else {
|
|
1571
|
+
setAllElementsDetected(false);
|
|
776
1572
|
}
|
|
777
1573
|
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1574
|
+
// Check if detected elements are inside the scan area
|
|
1575
|
+
const scanScreen = Dimensions.get('window');
|
|
1576
|
+
const scanFrameAspect = frame.width / frame.height;
|
|
1577
|
+
const scanScreenAspect = scanScreen.width / scanScreen.height;
|
|
1578
|
+
let scanScale;
|
|
1579
|
+
let scanOffsetX = 0;
|
|
1580
|
+
let scanOffsetY = 0;
|
|
1581
|
+
if (scanFrameAspect > scanScreenAspect) {
|
|
1582
|
+
scanScale = scanScreen.height / frame.height;
|
|
1583
|
+
scanOffsetX = (frame.width * scanScale - scanScreen.width) / 2;
|
|
1584
|
+
} else {
|
|
1585
|
+
scanScale = scanScreen.width / frame.width;
|
|
1586
|
+
scanOffsetY = (frame.height * scanScale - scanScreen.height) / 2;
|
|
787
1587
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
'worklet';
|
|
1588
|
+
const scanLeft = (scanScreen.width * 0.05 + scanOffsetX) / scanScale;
|
|
1589
|
+
const scanTop = (scanScreen.height * 0.36 + scanOffsetY) / scanScale;
|
|
1590
|
+
const scanRight = (scanScreen.width * 0.95 + scanOffsetX) / scanScale;
|
|
1591
|
+
const scanBottom = (scanScreen.height * 0.64 + scanOffsetY) / scanScale;
|
|
1592
|
+
const isInsideScan = (x, y, w, h) => x >= scanLeft && y >= scanTop && x + w <= scanRight && y + h <= scanBottom;
|
|
1593
|
+
const outsideElements = [];
|
|
795
1594
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1595
|
+
// Collect all detected element bounds
|
|
1596
|
+
const allBounds = [];
|
|
1597
|
+
const primaryFace = detectedFaces[0];
|
|
1598
|
+
if (primaryFace) {
|
|
1599
|
+
if (primaryFace.bounds.width >= frame.width * 0.05 && primaryFace.bounds.height >= frame.width * 0.05) {
|
|
1600
|
+
allBounds.push({
|
|
1601
|
+
x: primaryFace.bounds.x,
|
|
1602
|
+
y: primaryFace.bounds.y,
|
|
1603
|
+
x2: primaryFace.bounds.x + primaryFace.bounds.width,
|
|
1604
|
+
y2: primaryFace.bounds.y + primaryFace.bounds.height
|
|
1605
|
+
});
|
|
1606
|
+
if (!isInsideScan(primaryFace.bounds.x, primaryFace.bounds.y, primaryFace.bounds.width, primaryFace.bounds.height)) {
|
|
1607
|
+
outsideElements.push('face');
|
|
1608
|
+
}
|
|
808
1609
|
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1610
|
+
}
|
|
1611
|
+
for (const block of textBlocks) {
|
|
1612
|
+
if (block.blockFrame) {
|
|
1613
|
+
const bf = block.blockFrame;
|
|
1614
|
+
if (bf.width > 0 && bf.height > 0) {
|
|
1615
|
+
allBounds.push({
|
|
1616
|
+
x: bf.x,
|
|
1617
|
+
y: bf.y,
|
|
1618
|
+
x2: bf.x + bf.width,
|
|
1619
|
+
y2: bf.y + bf.height
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
const isMRZ = /[A-Z0-9<]{8,}.*</i.test(block.text);
|
|
1623
|
+
const isSignature = /signature|imza|İmza/i.test(block.text);
|
|
1624
|
+
if ((isMRZ || isSignature) && !isInsideScan(bf.x, bf.y, bf.width, bf.height)) {
|
|
1625
|
+
outsideElements.push('text');
|
|
1626
|
+
}
|
|
819
1627
|
}
|
|
820
|
-
}
|
|
1628
|
+
}
|
|
1629
|
+
for (const bc of barcodes) {
|
|
1630
|
+
if (bc.boundingBox) {
|
|
1631
|
+
const bb = bc.boundingBox;
|
|
1632
|
+
if (!isInsideScan(bb.left, bb.top, bb.right - bb.left, bb.bottom - bb.top)) {
|
|
1633
|
+
outsideElements.push('barcode');
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Check that detected content spans enough of the scan area horizontally and vertically
|
|
1639
|
+
// This catches cases where one side of the card is off-screen (elements on that side won't be detected)
|
|
1640
|
+
if (allBounds.length > 0 && outsideElements.length === 0) {
|
|
1641
|
+
const minX = Math.min(...allBounds.map(b => b.x));
|
|
1642
|
+
const maxX = Math.max(...allBounds.map(b => b.x2));
|
|
1643
|
+
const minY = Math.min(...allBounds.map(b => b.y));
|
|
1644
|
+
const maxY = Math.max(...allBounds.map(b => b.y2));
|
|
1645
|
+
const spanWidth = maxX - minX;
|
|
1646
|
+
const spanHeight = maxY - minY;
|
|
1647
|
+
const scanWidth = scanRight - scanLeft;
|
|
1648
|
+
const scanHeight = scanBottom - scanTop;
|
|
1649
|
+
// Require content to span at least 55% of scan area in both dimensions
|
|
1650
|
+
if (spanWidth < scanWidth * 0.55 || spanHeight < scanHeight * 0.55) {
|
|
1651
|
+
outsideElements.push('span');
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
setElementsOutsideScanArea(outsideElements);
|
|
1655
|
+
handleFaceAndText(scannedText.resultText ?? '', detectedFaces, frame.width, frame.height, barcodes.length ? barcodes[0] : undefined, base64Image, outsideElements.length > 0, frame.mrzResult);
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
console.warn('Frame processing error:', error?.message);
|
|
821
1658
|
}
|
|
822
|
-
}, [handleFaceAndText,
|
|
1659
|
+
}, [faceDetectionEnabled, frameDimensions, handleFaceAndText, nextStep]);
|
|
1660
|
+
const handleCameraReady = useCallback(_event => {
|
|
1661
|
+
isCameraInitialized.current = true;
|
|
1662
|
+
}, []);
|
|
1663
|
+
const handleCameraError = useCallback(event => {
|
|
1664
|
+
console.error('Camera error:', event.nativeEvent.error);
|
|
1665
|
+
}, []);
|
|
823
1666
|
if (!permissionsRequested) {
|
|
824
|
-
return /*#__PURE__*/
|
|
1667
|
+
return /*#__PURE__*/_jsxs(SafeAreaView, {
|
|
825
1668
|
style: styles.permissionContainer,
|
|
826
|
-
children: /*#__PURE__*/_jsx(
|
|
1669
|
+
children: [/*#__PURE__*/_jsx(StatusBar, {
|
|
1670
|
+
barStyle: "dark-content"
|
|
1671
|
+
}), /*#__PURE__*/_jsx(ActivityIndicator, {
|
|
827
1672
|
size: "large",
|
|
828
1673
|
color: theme.colors.primary
|
|
829
|
-
})
|
|
1674
|
+
})]
|
|
830
1675
|
});
|
|
831
1676
|
}
|
|
832
|
-
if (!
|
|
1677
|
+
if (!hasPermission) {
|
|
833
1678
|
return /*#__PURE__*/_jsxs(SafeAreaView, {
|
|
834
1679
|
style: styles.permissionContainer,
|
|
835
|
-
children: [/*#__PURE__*/_jsx(
|
|
1680
|
+
children: [/*#__PURE__*/_jsx(StatusBar, {
|
|
1681
|
+
barStyle: "dark-content"
|
|
1682
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
836
1683
|
style: styles.permissionText,
|
|
837
1684
|
children: t('general.noCameraPermissionGiven')
|
|
838
1685
|
}), /*#__PURE__*/_jsx(StyledButton, {
|
|
@@ -844,34 +1691,13 @@ const IdentityDocumentCamera = ({
|
|
|
844
1691
|
})]
|
|
845
1692
|
});
|
|
846
1693
|
}
|
|
847
|
-
|
|
848
|
-
return /*#__PURE__*/_jsx(SafeAreaView, {
|
|
849
|
-
style: styles.permissionContainer,
|
|
850
|
-
children: /*#__PURE__*/_jsx(TextView, {
|
|
851
|
-
style: styles.permissionText,
|
|
852
|
-
children: t('general.noCameraDetected')
|
|
853
|
-
})
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
const handleFocus = async event => {
|
|
857
|
-
if (cameraRef.current && device.supportsFocus) {
|
|
858
|
-
try {
|
|
859
|
-
const {
|
|
860
|
-
locationX,
|
|
861
|
-
locationY
|
|
862
|
-
} = event.nativeEvent;
|
|
863
|
-
await cameraRef.current.focus({
|
|
864
|
-
x: locationX,
|
|
865
|
-
y: locationY
|
|
866
|
-
});
|
|
867
|
-
} catch (error) {
|
|
868
|
-
// console.log('Error while focusing:', error);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
return /*#__PURE__*/_jsx(View, {
|
|
1694
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
873
1695
|
style: StyleSheet.absoluteFill,
|
|
874
|
-
children:
|
|
1696
|
+
children: [/*#__PURE__*/_jsx(StatusBar, {
|
|
1697
|
+
barStyle: "light-content",
|
|
1698
|
+
backgroundColor: "transparent",
|
|
1699
|
+
translucent: true
|
|
1700
|
+
}), !hasGuideShown ? /*#__PURE__*/_jsxs(SafeAreaView, {
|
|
875
1701
|
style: styles.guide,
|
|
876
1702
|
children: [/*#__PURE__*/_jsx(LottieView, {
|
|
877
1703
|
source: require('../../Shared/Animations/id-or-passport.json'),
|
|
@@ -904,22 +1730,360 @@ const IdentityDocumentCamera = ({
|
|
|
904
1730
|
children: t('general.letsGo')
|
|
905
1731
|
})]
|
|
906
1732
|
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
907
|
-
children: [/*#__PURE__*/_jsx(
|
|
1733
|
+
children: [/*#__PURE__*/_jsx(TrustchexCamera, {
|
|
908
1734
|
ref: cameraRef,
|
|
909
|
-
frameProcessor: frameProcessor,
|
|
910
1735
|
style: StyleSheet.absoluteFill,
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1736
|
+
cameraType: "back",
|
|
1737
|
+
enableFrameProcessing: isActive,
|
|
1738
|
+
enableFaceDetection: isActive && faceDetectionEnabled,
|
|
1739
|
+
enableTextRecognition: isActive,
|
|
1740
|
+
enableMrzValidation: isActive,
|
|
1741
|
+
enableBarcodeScanning: isActive && nextStep === 'SCAN_ID_BACK',
|
|
1742
|
+
includeBase64: isActive,
|
|
1743
|
+
targetFps: 10,
|
|
1744
|
+
torchEnabled: isTorchOn,
|
|
1745
|
+
onFrameAvailable: handleFrame,
|
|
1746
|
+
onCameraReady: handleCameraReady,
|
|
1747
|
+
onCameraError: handleCameraError
|
|
1748
|
+
}), isDebugEnabled() && documentPlaneBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsxs(_Fragment, {
|
|
1749
|
+
children: [!!documentPlaneBounds.cropPadding && /*#__PURE__*/_jsx(View, {
|
|
1750
|
+
style: {
|
|
1751
|
+
position: 'absolute',
|
|
1752
|
+
left: documentPlaneBounds.x - documentPlaneBounds.cropPadding,
|
|
1753
|
+
top: documentPlaneBounds.y - documentPlaneBounds.cropPadding,
|
|
1754
|
+
width: documentPlaneBounds.width + 2 * documentPlaneBounds.cropPadding,
|
|
1755
|
+
height: documentPlaneBounds.height + 2 * documentPlaneBounds.cropPadding,
|
|
1756
|
+
borderWidth: 2,
|
|
1757
|
+
borderColor: 'rgba(76, 175, 80, 0.5)',
|
|
1758
|
+
borderStyle: 'dashed',
|
|
1759
|
+
borderRadius: 8,
|
|
1760
|
+
backgroundColor: 'transparent',
|
|
1761
|
+
transform: [{
|
|
1762
|
+
rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg`
|
|
1763
|
+
}],
|
|
1764
|
+
transformOrigin: 'center'
|
|
1765
|
+
}
|
|
1766
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
1767
|
+
style: {
|
|
1768
|
+
position: 'absolute',
|
|
1769
|
+
left: documentPlaneBounds.x,
|
|
1770
|
+
top: documentPlaneBounds.y,
|
|
1771
|
+
width: documentPlaneBounds.width,
|
|
1772
|
+
height: documentPlaneBounds.height,
|
|
1773
|
+
borderWidth: 3,
|
|
1774
|
+
borderColor: '#4CAF50',
|
|
1775
|
+
borderRadius: 8,
|
|
1776
|
+
backgroundColor: 'transparent',
|
|
1777
|
+
transform: [{
|
|
1778
|
+
rotate: `${-(documentPlaneBounds.rollAngle || 0)}deg`
|
|
1779
|
+
}],
|
|
1780
|
+
transformOrigin: 'center'
|
|
1781
|
+
},
|
|
1782
|
+
children: !!documentPlaneBounds.rollAngle && Math.abs(documentPlaneBounds.rollAngle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1783
|
+
style: {
|
|
1784
|
+
position: 'absolute',
|
|
1785
|
+
top: -20,
|
|
1786
|
+
left: 0,
|
|
1787
|
+
color: '#4CAF50',
|
|
1788
|
+
fontSize: 10,
|
|
1789
|
+
fontWeight: 'bold',
|
|
1790
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1791
|
+
paddingHorizontal: 4,
|
|
1792
|
+
paddingVertical: 2,
|
|
1793
|
+
borderRadius: 2
|
|
1794
|
+
},
|
|
1795
|
+
children: [documentPlaneBounds.rollAngle.toFixed(1), "\xB0"]
|
|
1796
|
+
})
|
|
1797
|
+
})]
|
|
1798
|
+
}), isDebugEnabled() && secondaryFaceBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsxs(_Fragment, {
|
|
1799
|
+
children: [!!secondaryFaceBounds.cropPadding && /*#__PURE__*/_jsx(View, {
|
|
1800
|
+
style: {
|
|
1801
|
+
position: 'absolute',
|
|
1802
|
+
left: secondaryFaceBounds.x - secondaryFaceBounds.cropPadding,
|
|
1803
|
+
top: secondaryFaceBounds.y - secondaryFaceBounds.cropPadding,
|
|
1804
|
+
width: secondaryFaceBounds.width + 2 * secondaryFaceBounds.cropPadding,
|
|
1805
|
+
height: secondaryFaceBounds.height + 2 * secondaryFaceBounds.cropPadding,
|
|
1806
|
+
borderWidth: 2,
|
|
1807
|
+
borderColor: 'rgba(33, 150, 243, 0.5)',
|
|
1808
|
+
borderStyle: 'dashed',
|
|
1809
|
+
borderRadius: 8,
|
|
1810
|
+
backgroundColor: 'transparent',
|
|
1811
|
+
transform: [{
|
|
1812
|
+
rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg`
|
|
1813
|
+
}],
|
|
1814
|
+
transformOrigin: 'center'
|
|
1815
|
+
}
|
|
1816
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
1817
|
+
style: {
|
|
1818
|
+
position: 'absolute',
|
|
1819
|
+
left: secondaryFaceBounds.x,
|
|
1820
|
+
top: secondaryFaceBounds.y,
|
|
1821
|
+
width: secondaryFaceBounds.width,
|
|
1822
|
+
height: secondaryFaceBounds.height,
|
|
1823
|
+
borderWidth: 3,
|
|
1824
|
+
borderColor: '#2196F3',
|
|
1825
|
+
borderRadius: 8,
|
|
1826
|
+
backgroundColor: 'transparent',
|
|
1827
|
+
transform: [{
|
|
1828
|
+
rotate: `${-(secondaryFaceBounds.rollAngle || 0)}deg`
|
|
1829
|
+
}],
|
|
1830
|
+
transformOrigin: 'center'
|
|
1831
|
+
},
|
|
1832
|
+
children: !!secondaryFaceBounds.rollAngle && Math.abs(secondaryFaceBounds.rollAngle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1833
|
+
style: {
|
|
1834
|
+
position: 'absolute',
|
|
1835
|
+
top: -20,
|
|
1836
|
+
left: 0,
|
|
1837
|
+
color: '#2196F3',
|
|
1838
|
+
fontSize: 10,
|
|
1839
|
+
fontWeight: 'bold',
|
|
1840
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1841
|
+
paddingHorizontal: 4,
|
|
1842
|
+
paddingVertical: 2,
|
|
1843
|
+
borderRadius: 2
|
|
1844
|
+
},
|
|
1845
|
+
children: [secondaryFaceBounds.rollAngle.toFixed(1), "\xB0"]
|
|
1846
|
+
})
|
|
1847
|
+
})]
|
|
1848
|
+
}), isDebugEnabled() && barcodeBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(_Fragment, {
|
|
1849
|
+
children: barcodeBounds.corners && barcodeBounds.corners.length >= 4 ?
|
|
1850
|
+
/*#__PURE__*/
|
|
1851
|
+
// Render using corner points for precise rotated border
|
|
1852
|
+
_jsxs(_Fragment, {
|
|
1853
|
+
children: [[0, 1, 2, 3].map(i => {
|
|
1854
|
+
const start = barcodeBounds.corners[i];
|
|
1855
|
+
const end = barcodeBounds.corners[(i + 1) % 4];
|
|
1856
|
+
const dx = end.x - start.x;
|
|
1857
|
+
const dy = end.y - start.y;
|
|
1858
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
1859
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1860
|
+
return /*#__PURE__*/_jsx(View, {
|
|
1861
|
+
style: {
|
|
1862
|
+
position: 'absolute',
|
|
1863
|
+
left: start.x,
|
|
1864
|
+
top: start.y,
|
|
1865
|
+
width: length,
|
|
1866
|
+
height: 3,
|
|
1867
|
+
backgroundColor: '#FF9800',
|
|
1868
|
+
transform: [{
|
|
1869
|
+
rotate: `${angle}deg`
|
|
1870
|
+
}],
|
|
1871
|
+
transformOrigin: 'top left'
|
|
1872
|
+
}
|
|
1873
|
+
}, i);
|
|
1874
|
+
}), barcodeBounds.corners.map((corner, idx) => /*#__PURE__*/_jsx(View, {
|
|
1875
|
+
style: {
|
|
1876
|
+
position: 'absolute',
|
|
1877
|
+
left: corner.x - 4,
|
|
1878
|
+
top: corner.y - 4,
|
|
1879
|
+
width: 8,
|
|
1880
|
+
height: 8,
|
|
1881
|
+
borderRadius: 4,
|
|
1882
|
+
backgroundColor: '#FF9800'
|
|
1883
|
+
}
|
|
1884
|
+
}, `corner-${idx}`)), !!barcodeBounds.angle && Math.abs(barcodeBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1885
|
+
style: {
|
|
1886
|
+
position: 'absolute',
|
|
1887
|
+
left: barcodeBounds.x,
|
|
1888
|
+
top: barcodeBounds.y - 20,
|
|
1889
|
+
color: '#FF9800',
|
|
1890
|
+
fontSize: 10,
|
|
1891
|
+
fontWeight: 'bold',
|
|
1892
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1893
|
+
paddingHorizontal: 4,
|
|
1894
|
+
paddingVertical: 2,
|
|
1895
|
+
borderRadius: 2
|
|
1896
|
+
},
|
|
1897
|
+
children: [barcodeBounds.angle.toFixed(1), "\xB0"]
|
|
1898
|
+
})]
|
|
1899
|
+
}) :
|
|
1900
|
+
/*#__PURE__*/
|
|
1901
|
+
// Fallback to rotated rectangle if corners not available
|
|
1902
|
+
_jsx(View, {
|
|
1903
|
+
style: {
|
|
1904
|
+
position: 'absolute',
|
|
1905
|
+
left: barcodeBounds.x + barcodeBounds.width / 2,
|
|
1906
|
+
top: barcodeBounds.y + barcodeBounds.height / 2,
|
|
1907
|
+
width: barcodeBounds.width,
|
|
1908
|
+
height: barcodeBounds.height,
|
|
1909
|
+
marginLeft: -barcodeBounds.width / 2,
|
|
1910
|
+
marginTop: -barcodeBounds.height / 2,
|
|
1911
|
+
borderWidth: 3,
|
|
1912
|
+
borderColor: '#FF9800',
|
|
1913
|
+
borderRadius: 8,
|
|
1914
|
+
backgroundColor: 'transparent',
|
|
1915
|
+
transform: [{
|
|
1916
|
+
rotate: `${barcodeBounds.angle || 0}deg`
|
|
1917
|
+
}]
|
|
1918
|
+
},
|
|
1919
|
+
children: !!barcodeBounds.angle && Math.abs(barcodeBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1920
|
+
style: {
|
|
1921
|
+
position: 'absolute',
|
|
1922
|
+
top: -20,
|
|
1923
|
+
left: 0,
|
|
1924
|
+
color: '#FF9800',
|
|
1925
|
+
fontSize: 10,
|
|
1926
|
+
fontWeight: 'bold',
|
|
1927
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1928
|
+
paddingHorizontal: 4,
|
|
1929
|
+
paddingVertical: 2,
|
|
1930
|
+
borderRadius: 2
|
|
1931
|
+
},
|
|
1932
|
+
children: [barcodeBounds.angle.toFixed(1), "\xB0"]
|
|
1933
|
+
})
|
|
1934
|
+
})
|
|
1935
|
+
}), isDebugEnabled() && mrzBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(_Fragment, {
|
|
1936
|
+
children: mrzBounds.corners && mrzBounds.corners.length >= 2 ?
|
|
1937
|
+
/*#__PURE__*/
|
|
1938
|
+
// Render using corner points for precise rotated border
|
|
1939
|
+
_jsxs(_Fragment, {
|
|
1940
|
+
children: [mrzBounds.corners.map((corner, idx) => {
|
|
1941
|
+
const nextCorner = mrzBounds.corners[(idx + 1) % mrzBounds.corners.length];
|
|
1942
|
+
const dx = nextCorner.x - corner.x;
|
|
1943
|
+
const dy = nextCorner.y - corner.y;
|
|
1944
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
1945
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1946
|
+
return /*#__PURE__*/_jsx(View, {
|
|
1947
|
+
style: {
|
|
1948
|
+
position: 'absolute',
|
|
1949
|
+
left: corner.x,
|
|
1950
|
+
top: corner.y,
|
|
1951
|
+
width: length,
|
|
1952
|
+
height: 3,
|
|
1953
|
+
backgroundColor: '#9C27B0',
|
|
1954
|
+
transform: [{
|
|
1955
|
+
rotate: `${angle}deg`
|
|
1956
|
+
}],
|
|
1957
|
+
transformOrigin: 'top left'
|
|
1958
|
+
}
|
|
1959
|
+
}, idx);
|
|
1960
|
+
}), !!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1961
|
+
style: {
|
|
1962
|
+
position: 'absolute',
|
|
1963
|
+
left: mrzBounds.x,
|
|
1964
|
+
top: mrzBounds.y - 20,
|
|
1965
|
+
color: '#9C27B0',
|
|
1966
|
+
fontSize: 10,
|
|
1967
|
+
fontWeight: 'bold',
|
|
1968
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
1969
|
+
paddingHorizontal: 4,
|
|
1970
|
+
paddingVertical: 2,
|
|
1971
|
+
borderRadius: 2
|
|
1972
|
+
},
|
|
1973
|
+
children: [mrzBounds.angle.toFixed(1), "\xB0"]
|
|
1974
|
+
})]
|
|
1975
|
+
}) :
|
|
1976
|
+
/*#__PURE__*/
|
|
1977
|
+
// Fallback to rotated rectangle if corners not available
|
|
1978
|
+
_jsx(View, {
|
|
1979
|
+
style: {
|
|
1980
|
+
position: 'absolute',
|
|
1981
|
+
left: mrzBounds.x + mrzBounds.width / 2,
|
|
1982
|
+
top: mrzBounds.y + mrzBounds.height / 2,
|
|
1983
|
+
width: mrzBounds.width,
|
|
1984
|
+
height: mrzBounds.height,
|
|
1985
|
+
marginLeft: -mrzBounds.width / 2,
|
|
1986
|
+
marginTop: -mrzBounds.height / 2,
|
|
1987
|
+
borderWidth: 3,
|
|
1988
|
+
borderColor: '#9C27B0',
|
|
1989
|
+
borderRadius: 8,
|
|
1990
|
+
backgroundColor: 'transparent',
|
|
1991
|
+
transform: [{
|
|
1992
|
+
rotate: `${mrzBounds.angle || 0}deg`
|
|
1993
|
+
}]
|
|
1994
|
+
},
|
|
1995
|
+
children: !!mrzBounds.angle && Math.abs(mrzBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
1996
|
+
style: {
|
|
1997
|
+
position: 'absolute',
|
|
1998
|
+
top: -20,
|
|
1999
|
+
left: 0,
|
|
2000
|
+
color: '#9C27B0',
|
|
2001
|
+
fontSize: 10,
|
|
2002
|
+
fontWeight: 'bold',
|
|
2003
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2004
|
+
paddingHorizontal: 4,
|
|
2005
|
+
paddingVertical: 2,
|
|
2006
|
+
borderRadius: 2
|
|
2007
|
+
},
|
|
2008
|
+
children: [mrzBounds.angle.toFixed(1), "\xB0"]
|
|
2009
|
+
})
|
|
2010
|
+
})
|
|
2011
|
+
}), isDebugEnabled() && signatureBounds && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(_Fragment, {
|
|
2012
|
+
children: signatureBounds.corners && signatureBounds.corners.length >= 2 ?
|
|
2013
|
+
/*#__PURE__*/
|
|
2014
|
+
// Render using corner points for precise rotated border
|
|
2015
|
+
_jsxs(_Fragment, {
|
|
2016
|
+
children: [signatureBounds.corners.map((corner, idx) => {
|
|
2017
|
+
const nextCorner = signatureBounds.corners[(idx + 1) % signatureBounds.corners.length];
|
|
2018
|
+
const dx = nextCorner.x - corner.x;
|
|
2019
|
+
const dy = nextCorner.y - corner.y;
|
|
2020
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
2021
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
2022
|
+
return /*#__PURE__*/_jsx(View, {
|
|
2023
|
+
style: {
|
|
2024
|
+
position: 'absolute',
|
|
2025
|
+
left: corner.x,
|
|
2026
|
+
top: corner.y,
|
|
2027
|
+
width: length,
|
|
2028
|
+
height: 3,
|
|
2029
|
+
backgroundColor: '#00BCD4',
|
|
2030
|
+
transform: [{
|
|
2031
|
+
rotate: `${angle}deg`
|
|
2032
|
+
}],
|
|
2033
|
+
transformOrigin: 'top left'
|
|
2034
|
+
}
|
|
2035
|
+
}, idx);
|
|
2036
|
+
}), !!signatureBounds.angle && Math.abs(signatureBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
2037
|
+
style: {
|
|
2038
|
+
position: 'absolute',
|
|
2039
|
+
left: signatureBounds.x,
|
|
2040
|
+
top: signatureBounds.y - 20,
|
|
2041
|
+
color: '#00BCD4',
|
|
2042
|
+
fontSize: 10,
|
|
2043
|
+
fontWeight: 'bold',
|
|
2044
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2045
|
+
paddingHorizontal: 4,
|
|
2046
|
+
paddingVertical: 2,
|
|
2047
|
+
borderRadius: 2
|
|
2048
|
+
},
|
|
2049
|
+
children: [signatureBounds.angle.toFixed(1), "\xB0"]
|
|
2050
|
+
})]
|
|
2051
|
+
}) :
|
|
2052
|
+
/*#__PURE__*/
|
|
2053
|
+
// Fallback to rotated rectangle if corners not available
|
|
2054
|
+
_jsx(View, {
|
|
2055
|
+
style: {
|
|
2056
|
+
position: 'absolute',
|
|
2057
|
+
left: signatureBounds.x + signatureBounds.width / 2,
|
|
2058
|
+
top: signatureBounds.y + signatureBounds.height / 2,
|
|
2059
|
+
width: signatureBounds.width,
|
|
2060
|
+
height: signatureBounds.height,
|
|
2061
|
+
marginLeft: -signatureBounds.width / 2,
|
|
2062
|
+
marginTop: -signatureBounds.height / 2,
|
|
2063
|
+
borderWidth: 3,
|
|
2064
|
+
borderColor: '#00BCD4',
|
|
2065
|
+
borderRadius: 8,
|
|
2066
|
+
backgroundColor: 'transparent',
|
|
2067
|
+
transform: [{
|
|
2068
|
+
rotate: `${signatureBounds.angle || 0}deg`
|
|
2069
|
+
}]
|
|
2070
|
+
},
|
|
2071
|
+
children: !!signatureBounds.angle && Math.abs(signatureBounds.angle) > 5 && /*#__PURE__*/_jsxs(TextView, {
|
|
2072
|
+
style: {
|
|
2073
|
+
position: 'absolute',
|
|
2074
|
+
top: -20,
|
|
2075
|
+
left: 0,
|
|
2076
|
+
color: '#00BCD4',
|
|
2077
|
+
fontSize: 10,
|
|
2078
|
+
fontWeight: 'bold',
|
|
2079
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2080
|
+
paddingHorizontal: 4,
|
|
2081
|
+
paddingVertical: 2,
|
|
2082
|
+
borderRadius: 2
|
|
2083
|
+
},
|
|
2084
|
+
children: [signatureBounds.angle.toFixed(1), "\xB0"]
|
|
2085
|
+
})
|
|
2086
|
+
})
|
|
923
2087
|
}), /*#__PURE__*/_jsxs(View, {
|
|
924
2088
|
style: [styles.topZone, {
|
|
925
2089
|
paddingTop: insets.top
|
|
@@ -937,85 +2101,197 @@ const IdentityDocumentCamera = ({
|
|
|
937
2101
|
total: 3
|
|
938
2102
|
})}` : ''
|
|
939
2103
|
}), /*#__PURE__*/_jsx(TextView, {
|
|
940
|
-
style: [styles.topZoneText,
|
|
941
|
-
|
|
2104
|
+
style: [styles.topZoneText,
|
|
2105
|
+
// Priority order for coloring (later styles override earlier ones)
|
|
2106
|
+
// 1. Success (green) - scan completed
|
|
2107
|
+
status === 'SCANNED' && styles.topZoneTextSuccess,
|
|
2108
|
+
// 2. Error (red) - wrong side
|
|
2109
|
+
status === 'INCORRECT' && styles.topZoneTextError,
|
|
2110
|
+
// 3. Warning (yellow) - quality issues
|
|
2111
|
+
(isBrightnessLow || isFrameBlurry) && styles.topZoneTextWarning,
|
|
2112
|
+
// 4. Scanning (green) - all elements detected AND inside scan area
|
|
2113
|
+
status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
|
|
2114
|
+
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
2115
|
+
],
|
|
2116
|
+
children: status === 'SCANNED' ? completedStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportScanned') : t('identityDocumentCamera.frontSideScanned') : completedStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.backSideScanned') : completedStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.hologramVerified') : t('identityDocumentCamera.scanCompleted') : status === 'INCORRECT' ? nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? t('identityDocumentCamera.wrongSideFront') : nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.wrongSideBack') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.wrongSideFront') // Hologram is on front, show same message as front scan
|
|
2117
|
+
: t('identityDocumentCamera.alignPhotoSide') : isBrightnessLow ? t('identityDocumentCamera.lowBrightness') : isFrameBlurry ? t('identityDocumentCamera.avoidBlur') : status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 ? nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.idCardBackDetected') : detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportDetected') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.idCardFrontDetected') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument') : elementsOutsideScanArea.length > 0 ? t('identityDocumentCamera.centerDocument') : (status === 'SCANNING' || status === 'SEARCHING') && !allElementsDetected ? nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.alignIDBack') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.alignPassport') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.alignIDFront') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : t('identityDocumentCamera.readingDocument') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : ''
|
|
942
2118
|
})]
|
|
943
2119
|
}), /*#__PURE__*/_jsx(View, {
|
|
944
2120
|
style: styles.leftZone
|
|
945
2121
|
}), /*#__PURE__*/_jsx(View, {
|
|
946
2122
|
style: styles.rightZone
|
|
947
|
-
}), /*#__PURE__*/
|
|
2123
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
948
2124
|
style: styles.bottomZone,
|
|
949
|
-
children:
|
|
950
|
-
style: styles.
|
|
951
|
-
children: [/*#__PURE__*/
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
2125
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
2126
|
+
style: styles.debugImagesRow,
|
|
2127
|
+
children: [isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2128
|
+
style: styles.imageContainer,
|
|
2129
|
+
children: [currentFaceImage ? /*#__PURE__*/_jsx(Image, {
|
|
2130
|
+
source: {
|
|
2131
|
+
uri: `data:image/jpeg;base64,${currentFaceImage}`
|
|
2132
|
+
},
|
|
2133
|
+
style: styles.faceImage
|
|
2134
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
2135
|
+
style: [styles.faceImage, {
|
|
2136
|
+
backgroundColor: '#333',
|
|
2137
|
+
justifyContent: 'center'
|
|
2138
|
+
}],
|
|
2139
|
+
children: /*#__PURE__*/_jsx(TextView, {
|
|
2140
|
+
style: {
|
|
2141
|
+
color: '#666',
|
|
2142
|
+
fontSize: 10,
|
|
2143
|
+
textAlign: 'center'
|
|
2144
|
+
},
|
|
2145
|
+
children: "Waiting..."
|
|
2146
|
+
})
|
|
2147
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2148
|
+
style: [styles.imageContainerText, currentFaceImage && {
|
|
2149
|
+
color: '#4CAF50'
|
|
2150
|
+
}],
|
|
2151
|
+
children: `${currentFaceImage ? '✓ ' : ''}Face`
|
|
2152
|
+
})]
|
|
2153
|
+
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2154
|
+
style: styles.imageContainer,
|
|
2155
|
+
children: [currentSecondaryFaceImage ? /*#__PURE__*/_jsx(Image, {
|
|
2156
|
+
source: {
|
|
2157
|
+
uri: `data:image/jpeg;base64,${currentSecondaryFaceImage}`
|
|
2158
|
+
},
|
|
2159
|
+
style: styles.faceImage
|
|
2160
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
2161
|
+
style: [styles.faceImage, {
|
|
2162
|
+
backgroundColor: '#333',
|
|
2163
|
+
justifyContent: 'center'
|
|
2164
|
+
}],
|
|
2165
|
+
children: /*#__PURE__*/_jsx(TextView, {
|
|
2166
|
+
style: {
|
|
2167
|
+
color: '#666',
|
|
2168
|
+
fontSize: 10,
|
|
2169
|
+
textAlign: 'center'
|
|
2170
|
+
},
|
|
2171
|
+
children: "Waiting..."
|
|
2172
|
+
})
|
|
2173
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2174
|
+
style: [styles.imageContainerText, currentSecondaryFaceImage && {
|
|
2175
|
+
color: '#4CAF50'
|
|
2176
|
+
}],
|
|
2177
|
+
children: `${currentSecondaryFaceImage ? '✓ ' : ''}2nd Face`
|
|
2178
|
+
})]
|
|
2179
|
+
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2180
|
+
style: styles.imageContainer,
|
|
2181
|
+
children: [_currentHologramMaskImage ? /*#__PURE__*/_jsx(Image, {
|
|
2182
|
+
source: {
|
|
2183
|
+
uri: `data:image/jpeg;base64,${_currentHologramMaskImage}`
|
|
2184
|
+
},
|
|
2185
|
+
style: styles.faceImage
|
|
2186
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
2187
|
+
style: [styles.faceImage, {
|
|
2188
|
+
backgroundColor: '#333',
|
|
2189
|
+
justifyContent: 'center'
|
|
2190
|
+
}],
|
|
2191
|
+
children: /*#__PURE__*/_jsx(TextView, {
|
|
2192
|
+
style: {
|
|
2193
|
+
color: '#666',
|
|
2194
|
+
fontSize: 10,
|
|
2195
|
+
textAlign: 'center'
|
|
2196
|
+
},
|
|
2197
|
+
children: "Waiting..."
|
|
2198
|
+
})
|
|
2199
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2200
|
+
style: [styles.imageContainerText, _currentHologramMaskImage && {
|
|
2201
|
+
color: '#4CAF50'
|
|
2202
|
+
}],
|
|
2203
|
+
children: `${_currentHologramMaskImage ? '✓ ' : ''}Mask`
|
|
2204
|
+
})]
|
|
2205
|
+
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2206
|
+
style: styles.imageContainer,
|
|
2207
|
+
children: [currentHologramImage ? /*#__PURE__*/_jsx(Image, {
|
|
2208
|
+
source: {
|
|
2209
|
+
uri: `data:image/jpeg;base64,${currentHologramImage}`
|
|
2210
|
+
},
|
|
2211
|
+
style: styles.faceImage
|
|
2212
|
+
}) : latestHologramFaceImage && hologramImageCount > 0 ? /*#__PURE__*/_jsxs(View, {
|
|
2213
|
+
style: {
|
|
2214
|
+
position: 'relative'
|
|
2215
|
+
},
|
|
2216
|
+
children: [/*#__PURE__*/_jsx(Image, {
|
|
2217
|
+
source: {
|
|
2218
|
+
uri: `data:image/jpeg;base64,${latestHologramFaceImage}`
|
|
2219
|
+
},
|
|
2220
|
+
style: [styles.faceImage, {
|
|
2221
|
+
opacity: 0.7
|
|
2222
|
+
}]
|
|
2223
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
2224
|
+
style: {
|
|
2225
|
+
position: 'absolute',
|
|
2226
|
+
bottom: 0,
|
|
2227
|
+
left: 0,
|
|
2228
|
+
right: 0,
|
|
2229
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
2230
|
+
padding: 2
|
|
2231
|
+
},
|
|
2232
|
+
children: /*#__PURE__*/_jsxs(TextView, {
|
|
2233
|
+
style: {
|
|
2234
|
+
color: '#FFA500',
|
|
2235
|
+
fontSize: 8,
|
|
2236
|
+
textAlign: 'center',
|
|
2237
|
+
fontWeight: 'bold'
|
|
2238
|
+
},
|
|
2239
|
+
children: [hologramImageCount, "/", HOLOGRAM_IMAGE_COUNT]
|
|
2240
|
+
})
|
|
2241
|
+
})]
|
|
2242
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
2243
|
+
style: [styles.faceImage, {
|
|
2244
|
+
backgroundColor: '#333',
|
|
2245
|
+
justifyContent: 'center'
|
|
2246
|
+
}],
|
|
2247
|
+
children: /*#__PURE__*/_jsx(TextView, {
|
|
2248
|
+
style: {
|
|
2249
|
+
color: '#666',
|
|
2250
|
+
fontSize: 10,
|
|
2251
|
+
textAlign: 'center'
|
|
2252
|
+
},
|
|
2253
|
+
children: "Waiting..."
|
|
2254
|
+
})
|
|
2255
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2256
|
+
style: [styles.imageContainerText, currentHologramImage && {
|
|
2257
|
+
color: '#4CAF50'
|
|
2258
|
+
}, latestHologramFaceImage && !currentHologramImage && {
|
|
2259
|
+
color: '#FFA500'
|
|
2260
|
+
}],
|
|
2261
|
+
children: `${currentHologramImage ? '✓ ' : latestHologramFaceImage ? '⏳ ' : ''}Hologram`
|
|
2262
|
+
})]
|
|
2263
|
+
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2264
|
+
style: styles.debugInfoContainer,
|
|
2265
|
+
children: [/*#__PURE__*/_jsx(TextView, {
|
|
2266
|
+
style: styles.debugInfoText,
|
|
2267
|
+
children: `Step: ${nextStep}`
|
|
2268
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2269
|
+
style: styles.debugInfoText,
|
|
2270
|
+
children: `Status: ${status}`
|
|
2271
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2272
|
+
style: styles.debugInfoText,
|
|
2273
|
+
children: `Face: ${currentFaceImage ? '✓ CAPTURED' : '✗ WAITING'} ${!faceDetectionEnabled ? '(DISABLED)' : ''}`
|
|
2274
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2275
|
+
style: styles.debugInfoText,
|
|
2276
|
+
children: `Hologram: ${currentHologramImage ? '✓ CAPTURED' : `${hologramImageCount}/${HOLOGRAM_IMAGE_COUNT} imgs`} (retry ${hologramDetectionCurrentRetryCount.current}/${HOLOGRAM_DETECTION_RETRY_COUNT})`
|
|
2277
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2278
|
+
style: styles.debugInfoText,
|
|
2279
|
+
children: `2nd Face: ${currentSecondaryFaceImage ? '✓ CAPTURED' : '✗ WAITING'} (retry ${secondaryFaceDetectionCurrentRetryCount.current}/${SECOND_FACE_DETECTION_RETRY_COUNT})`
|
|
2280
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2281
|
+
style: styles.debugInfoText,
|
|
2282
|
+
children: `Flash: ${isTorchOn ? '🔦 ON' : '🔦 OFF'}`
|
|
2283
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2284
|
+
style: styles.debugInfoText,
|
|
2285
|
+
children: `Brightness: ${isBrightnessLow ? '⚠️ LOW' : '✓ OK'}`
|
|
2286
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2287
|
+
style: styles.debugInfoText,
|
|
2288
|
+
children: `Blur: ${isFrameBlurry ? '⚠️ BLURRY' : '✓ OK'}`
|
|
2289
|
+
})]
|
|
970
2290
|
})]
|
|
971
|
-
})
|
|
972
|
-
style: styles.imageContainer,
|
|
973
|
-
children: [/*#__PURE__*/_jsx(Image, {
|
|
974
|
-
source: {
|
|
975
|
-
uri: `data:image/jpeg;base64,${_currentHologramMaskImage}`
|
|
976
|
-
},
|
|
977
|
-
style: styles.faceImage
|
|
978
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
979
|
-
style: styles.imageContainerText,
|
|
980
|
-
children: "Hologram Mask"
|
|
981
|
-
})]
|
|
982
|
-
}), showDebugImages && currentHologramImage && /*#__PURE__*/_jsxs(View, {
|
|
983
|
-
style: styles.imageContainer,
|
|
984
|
-
children: [/*#__PURE__*/_jsx(Image, {
|
|
985
|
-
source: {
|
|
986
|
-
uri: `data:image/jpeg;base64,${currentHologramImage}`
|
|
987
|
-
},
|
|
988
|
-
style: styles.faceImage
|
|
989
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
990
|
-
style: styles.imageContainerText,
|
|
991
|
-
children: "Hologram"
|
|
992
|
-
})]
|
|
993
|
-
}), showDebugImages && /*#__PURE__*/_jsxs(View, {
|
|
994
|
-
style: styles.debugInfoContainer,
|
|
995
|
-
children: [/*#__PURE__*/_jsxs(TextView, {
|
|
996
|
-
style: styles.debugInfoText,
|
|
997
|
-
children: ["Step: ", nextStep]
|
|
998
|
-
}), /*#__PURE__*/_jsxs(TextView, {
|
|
999
|
-
style: styles.debugInfoText,
|
|
1000
|
-
children: ["Status: ", status]
|
|
1001
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
1002
|
-
style: styles.debugInfoText,
|
|
1003
|
-
children: `Face: ${currentFaceImage ? '✓' : '✗'} ${!faceDetectionEnabled ? '(DISABLED)' : ''}`
|
|
1004
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
1005
|
-
style: styles.debugInfoText,
|
|
1006
|
-
children: `Hologram: ${currentHologramImage ? '✓' : '✗'} (${hologramDetectionCurrentRetryCount.value}/${HOLOGRAM_DETECTION_RETRY_COUNT})`
|
|
1007
|
-
}), /*#__PURE__*/_jsx(TextView, {
|
|
1008
|
-
style: styles.debugInfoText,
|
|
1009
|
-
children: `2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'} (${secondaryFaceDetectionCurrentRetryCount.value}/${SECOND_FACE_DETECTION_RETRY_COUNT})`
|
|
1010
|
-
})]
|
|
1011
|
-
})]
|
|
2291
|
+
})
|
|
1012
2292
|
}), /*#__PURE__*/_jsx(View, {
|
|
1013
2293
|
style: [styles.scanArea, {
|
|
1014
|
-
borderColor: status === 'SCANNED' || nextStep === 'COMPLETED' ? '#4CAF50'
|
|
1015
|
-
: status === 'INCORRECT' ? '#f44336' // Red - error
|
|
1016
|
-
: status === 'SCANNING' ? '#2196F3' // Blue - processing
|
|
1017
|
-
: isBrightnessLow || isFrameBlurry ? '#FFC107' // Yellow - warning
|
|
1018
|
-
: 'white',
|
|
2294
|
+
borderColor: status === 'SCANNED' || nextStep === 'COMPLETED' ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
|
|
1019
2295
|
borderWidth: status === 'SCANNING' ? 3 : 2
|
|
1020
2296
|
}],
|
|
1021
2297
|
children: nextStep === 'COMPLETED' || status === 'SCANNED' ? /*#__PURE__*/_jsx(LottieView, {
|
|
@@ -1039,12 +2315,100 @@ const IdentityDocumentCamera = ({
|
|
|
1039
2315
|
loop: true,
|
|
1040
2316
|
autoPlay: true
|
|
1041
2317
|
}) : null
|
|
1042
|
-
}), /*#__PURE__*/
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
2318
|
+
}), isDebugEnabled() && /*#__PURE__*/_jsxs(View, {
|
|
2319
|
+
style: {
|
|
2320
|
+
position: 'absolute',
|
|
2321
|
+
top: 10,
|
|
2322
|
+
right: 10,
|
|
2323
|
+
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
2324
|
+
padding: 10,
|
|
2325
|
+
borderRadius: 8,
|
|
2326
|
+
borderWidth: 1,
|
|
2327
|
+
borderColor: '#FF6B6B',
|
|
2328
|
+
maxWidth: 200
|
|
2329
|
+
},
|
|
2330
|
+
children: [/*#__PURE__*/_jsx(TextView, {
|
|
2331
|
+
style: {
|
|
2332
|
+
color: '#FF6B6B',
|
|
2333
|
+
fontSize: 11,
|
|
2334
|
+
fontWeight: 'bold',
|
|
2335
|
+
marginBottom: 6
|
|
2336
|
+
},
|
|
2337
|
+
children: "\uD83D\uDC1B DEBUG MODE"
|
|
2338
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2339
|
+
style: {
|
|
2340
|
+
color: '#88D8B0',
|
|
2341
|
+
fontSize: 9,
|
|
2342
|
+
marginBottom: 2
|
|
2343
|
+
},
|
|
2344
|
+
children: `Step: ${nextStep}`
|
|
2345
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2346
|
+
style: {
|
|
2347
|
+
color: '#88D8B0',
|
|
2348
|
+
fontSize: 9,
|
|
2349
|
+
marginBottom: 2
|
|
2350
|
+
},
|
|
2351
|
+
children: `Status: ${status}`
|
|
2352
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2353
|
+
style: {
|
|
2354
|
+
color: '#88D8B0',
|
|
2355
|
+
fontSize: 9,
|
|
2356
|
+
marginBottom: 2
|
|
2357
|
+
},
|
|
2358
|
+
children: `Doc Type: ${detectedDocumentType}`
|
|
2359
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2360
|
+
style: {
|
|
2361
|
+
color: '#88D8B0',
|
|
2362
|
+
fontSize: 9,
|
|
2363
|
+
marginBottom: 2
|
|
2364
|
+
},
|
|
2365
|
+
children: `Face: ${currentFaceImage ? '✓' : '✗'}`
|
|
2366
|
+
}), !onlyMRZScan && /*#__PURE__*/_jsxs(_Fragment, {
|
|
2367
|
+
children: [/*#__PURE__*/_jsx(TextView, {
|
|
2368
|
+
style: {
|
|
2369
|
+
color: '#88D8B0',
|
|
2370
|
+
fontSize: 9,
|
|
2371
|
+
marginBottom: 2
|
|
2372
|
+
},
|
|
2373
|
+
children: `2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'}`
|
|
2374
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2375
|
+
style: {
|
|
2376
|
+
color: '#88D8B0',
|
|
2377
|
+
fontSize: 9,
|
|
2378
|
+
marginBottom: 2
|
|
2379
|
+
},
|
|
2380
|
+
children: `Hologram: ${hologramImageCount}/${HOLOGRAM_IMAGE_COUNT}`
|
|
2381
|
+
})]
|
|
2382
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2383
|
+
style: {
|
|
2384
|
+
color: '#88D8B0',
|
|
2385
|
+
fontSize: 9,
|
|
2386
|
+
marginBottom: 2
|
|
2387
|
+
},
|
|
2388
|
+
children: `Brightness: ${isBrightnessLow ? '⚠️ LOW' : '✓'}`
|
|
2389
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2390
|
+
style: {
|
|
2391
|
+
color: '#88D8B0',
|
|
2392
|
+
fontSize: 9,
|
|
2393
|
+
marginBottom: 2
|
|
2394
|
+
},
|
|
2395
|
+
children: `Blur: ${isFrameBlurry ? '⚠️' : '✓'}`
|
|
2396
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2397
|
+
style: {
|
|
2398
|
+
color: '#88D8B0',
|
|
2399
|
+
fontSize: 9,
|
|
2400
|
+
marginBottom: 2
|
|
2401
|
+
},
|
|
2402
|
+
children: `Flash: ${isTorchOn ? '🔦' : '○'}`
|
|
2403
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2404
|
+
style: {
|
|
2405
|
+
color: '#88D8B0',
|
|
2406
|
+
fontSize: 9
|
|
2407
|
+
},
|
|
2408
|
+
children: `Face Detection: ${faceDetectionEnabled ? '✓' : '✗'}`
|
|
2409
|
+
})]
|
|
1046
2410
|
})]
|
|
1047
|
-
})
|
|
2411
|
+
})]
|
|
1048
2412
|
});
|
|
1049
2413
|
};
|
|
1050
2414
|
const styles = StyleSheet.create({
|
|
@@ -1077,14 +2441,6 @@ const styles = StyleSheet.create({
|
|
|
1077
2441
|
alignItems: 'center',
|
|
1078
2442
|
paddingHorizontal: 5
|
|
1079
2443
|
},
|
|
1080
|
-
focusArea: {
|
|
1081
|
-
position: 'absolute',
|
|
1082
|
-
top: 0,
|
|
1083
|
-
left: 0,
|
|
1084
|
-
width: '100%',
|
|
1085
|
-
height: '100%',
|
|
1086
|
-
zIndex: 2
|
|
1087
|
-
},
|
|
1088
2444
|
animation: {
|
|
1089
2445
|
width: '100%',
|
|
1090
2446
|
height: '100%',
|
|
@@ -1115,16 +2471,16 @@ const styles = StyleSheet.create({
|
|
|
1115
2471
|
padding: 20
|
|
1116
2472
|
},
|
|
1117
2473
|
topZoneTextScanning: {
|
|
1118
|
-
color: '#2196F3'
|
|
2474
|
+
color: '#2196F3'
|
|
1119
2475
|
},
|
|
1120
2476
|
topZoneTextSuccess: {
|
|
1121
|
-
color: '#4CAF50'
|
|
2477
|
+
color: '#4CAF50'
|
|
1122
2478
|
},
|
|
1123
2479
|
topZoneTextWarning: {
|
|
1124
|
-
color: '#FFC107'
|
|
2480
|
+
color: '#FFC107'
|
|
1125
2481
|
},
|
|
1126
2482
|
topZoneTextError: {
|
|
1127
|
-
color: '#f44336'
|
|
2483
|
+
color: '#f44336'
|
|
1128
2484
|
},
|
|
1129
2485
|
leftZone: {
|
|
1130
2486
|
position: 'absolute',
|
|
@@ -1150,12 +2506,24 @@ const styles = StyleSheet.create({
|
|
|
1150
2506
|
bottom: 0,
|
|
1151
2507
|
backgroundColor: '#00000099',
|
|
1152
2508
|
padding: 20,
|
|
2509
|
+
display: 'flex',
|
|
2510
|
+
flexDirection: 'column',
|
|
2511
|
+
gap: 10,
|
|
2512
|
+
justifyContent: 'flex-start'
|
|
2513
|
+
},
|
|
2514
|
+
debugImagesRow: {
|
|
1153
2515
|
display: 'flex',
|
|
1154
2516
|
flexDirection: 'row',
|
|
1155
2517
|
gap: 10,
|
|
1156
2518
|
justifyContent: 'flex-start',
|
|
1157
2519
|
flexWrap: 'wrap'
|
|
1158
2520
|
},
|
|
2521
|
+
cardDetectionRow: {
|
|
2522
|
+
display: 'flex',
|
|
2523
|
+
flexDirection: 'row',
|
|
2524
|
+
justifyContent: 'center',
|
|
2525
|
+
marginTop: 5
|
|
2526
|
+
},
|
|
1159
2527
|
imageContainer: {
|
|
1160
2528
|
display: 'flex',
|
|
1161
2529
|
flexDirection: 'column',
|
|
@@ -1175,6 +2543,18 @@ const styles = StyleSheet.create({
|
|
|
1175
2543
|
borderWidth: 1,
|
|
1176
2544
|
borderColor: 'white'
|
|
1177
2545
|
},
|
|
2546
|
+
cardDetectionImage: {
|
|
2547
|
+
width: 160,
|
|
2548
|
+
height: 120,
|
|
2549
|
+
borderRadius: 8,
|
|
2550
|
+
borderWidth: 2,
|
|
2551
|
+
borderColor: '#FF9800'
|
|
2552
|
+
},
|
|
2553
|
+
cardDetectionContainer: {
|
|
2554
|
+
display: 'flex',
|
|
2555
|
+
flexDirection: 'column',
|
|
2556
|
+
alignItems: 'center'
|
|
2557
|
+
},
|
|
1178
2558
|
debugInfoContainer: {
|
|
1179
2559
|
flex: 1,
|
|
1180
2560
|
paddingLeft: 10
|