@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
|
@@ -6,21 +6,16 @@ import React, {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
8
|
useCallback,
|
|
9
|
+
useRef,
|
|
9
10
|
} from 'react';
|
|
11
|
+
import { StyleSheet, Text, View, Dimensions, Vibration } from 'react-native';
|
|
10
12
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
StyleSheet,
|
|
15
|
-
Platform,
|
|
16
|
-
Vibration,
|
|
17
|
-
} from 'react-native';
|
|
18
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
13
|
+
useSafeAreaInsets,
|
|
14
|
+
SafeAreaView,
|
|
15
|
+
} from 'react-native-safe-area-context';
|
|
20
16
|
import NativeCircularProgress from '../../Shared/Components/NativeCircularProgress';
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
23
|
-
import FaceCamera from '../../Shared/Components/FaceCamera';
|
|
17
|
+
import type { TrustchexCameraHandle } from '../../Shared/Components/TrustchexCamera';
|
|
18
|
+
import FaceCamera, { type Face } from '../../Shared/Components/FaceCamera';
|
|
24
19
|
import NavigationManager, {
|
|
25
20
|
type NavigationManagerRef,
|
|
26
21
|
} from '../../Shared/Components/NavigationManager';
|
|
@@ -29,7 +24,7 @@ import { contains, type Rect } from '../../Shared/Libs/contains';
|
|
|
29
24
|
import { useTranslation } from 'react-i18next';
|
|
30
25
|
import StyledButton from '../../Shared/Components/StyledButton';
|
|
31
26
|
import LottieView from 'lottie-react-native';
|
|
32
|
-
import {
|
|
27
|
+
import { speak, resetLastMessage } from '../../Shared/Libs/tts.utils';
|
|
33
28
|
import {
|
|
34
29
|
trackFunnelStep,
|
|
35
30
|
useScreenTracking,
|
|
@@ -70,6 +65,7 @@ interface Actions {
|
|
|
70
65
|
FACE_TOO_BIG: boolean;
|
|
71
66
|
VIDEO_RECORDED: string;
|
|
72
67
|
NEXT_INSTRUCTION: string;
|
|
68
|
+
RESET: undefined;
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
interface Action<T extends keyof Actions> {
|
|
@@ -82,16 +78,18 @@ type PossibleActions = {
|
|
|
82
78
|
}[keyof Actions];
|
|
83
79
|
|
|
84
80
|
const LivenessDetectionScreen = () => {
|
|
85
|
-
const [camera, setCamera] = useState<
|
|
81
|
+
const [camera, setCamera] = useState<TrustchexCameraHandle | null>(null);
|
|
86
82
|
const navigation = useNavigation();
|
|
87
83
|
const appContext = useContext(AppContext);
|
|
88
84
|
const navigationManagerRef = React.useRef<NavigationManagerRef>(null);
|
|
89
85
|
const { t } = useTranslation();
|
|
90
86
|
const [isRecording, setIsRecording] = useState(false);
|
|
91
87
|
const insets = useSafeAreaInsets();
|
|
88
|
+
const referenceFaceTrackingId = useRef<number | null>(null);
|
|
92
89
|
|
|
93
90
|
// Track screen view and exit
|
|
94
91
|
useScreenTracking('liveness_detection');
|
|
92
|
+
|
|
95
93
|
const [initialState, setInitialState] = useState<StateType>({
|
|
96
94
|
brightnessLow: false,
|
|
97
95
|
faceDetected: false,
|
|
@@ -104,7 +102,8 @@ const LivenessDetectionScreen = () => {
|
|
|
104
102
|
processComplete: false,
|
|
105
103
|
videoPath: '',
|
|
106
104
|
});
|
|
107
|
-
const
|
|
105
|
+
const hasNavigatedRef = useRef(false);
|
|
106
|
+
const [instructions] = useState<{
|
|
108
107
|
[type: string]: Record<string, any> & {
|
|
109
108
|
instruction: string;
|
|
110
109
|
photo?: string;
|
|
@@ -114,6 +113,8 @@ const LivenessDetectionScreen = () => {
|
|
|
114
113
|
SMILE: {
|
|
115
114
|
instruction: t('livenessDetectionScreen.smile'),
|
|
116
115
|
minProbability: 0.7,
|
|
116
|
+
maxAngle: LOOK_STRAIGHT_ANGLE_LIMIT,
|
|
117
|
+
minAngle: -LOOK_STRAIGHT_ANGLE_LIMIT,
|
|
117
118
|
},
|
|
118
119
|
LOOK_STRAIGHT_AND_BLINK: {
|
|
119
120
|
instruction: t('livenessDetectionScreen.lookStraightAndBlink'),
|
|
@@ -140,6 +141,8 @@ const LivenessDetectionScreen = () => {
|
|
|
140
141
|
});
|
|
141
142
|
const [instructionList, setInstructionList] = useState<string[]>([]);
|
|
142
143
|
const [hasGuideShown, setHasGuideShown] = useState(false);
|
|
144
|
+
const stoppingRecordingRef = useRef(false); // Track if we're already stopping to prevent multiple calls
|
|
145
|
+
const lastVoiceGuidanceMessage = useRef<string>('');
|
|
143
146
|
|
|
144
147
|
useEffect(() => {
|
|
145
148
|
const il = Object.keys(instructions)
|
|
@@ -172,71 +175,102 @@ const LivenessDetectionScreen = () => {
|
|
|
172
175
|
processComplete: false,
|
|
173
176
|
videoPath: '',
|
|
174
177
|
});
|
|
178
|
+
lastVoiceGuidanceMessage.current = '';
|
|
179
|
+
resetLastMessage();
|
|
175
180
|
}, [
|
|
176
181
|
instructions,
|
|
177
182
|
appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes,
|
|
178
183
|
]);
|
|
179
184
|
|
|
185
|
+
const isCommandInProgress = useRef(false);
|
|
186
|
+
|
|
180
187
|
const startRecording = useCallback(async () => {
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
console.log(
|
|
189
|
+
'[LivenessDetection] startRecording called, current state isRecording:',
|
|
190
|
+
isRecording
|
|
191
|
+
);
|
|
192
|
+
if (isCommandInProgress.current) {
|
|
193
|
+
console.log(
|
|
194
|
+
'[LivenessDetection] Skipping startRecording: Command already in progress'
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
isCommandInProgress.current = true;
|
|
200
|
+
try {
|
|
201
|
+
if (isRecording) {
|
|
202
|
+
console.log(
|
|
203
|
+
'[LivenessDetection] isRecording is true, cancelling existing recording first'
|
|
204
|
+
);
|
|
183
205
|
await camera?.cancelRecording();
|
|
184
206
|
setIsRecording(false);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
// User cancelled recording - expected behavior, no need to track
|
|
187
207
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Track liveness check started
|
|
208
|
+
|
|
209
|
+
console.log('[LivenessDetection] Starting new recording');
|
|
191
210
|
trackVerificationStart('LIVENESS_CHECK');
|
|
192
211
|
|
|
193
212
|
camera?.startRecording({
|
|
194
213
|
fileType: 'mp4',
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
onRecordingError(error) {
|
|
215
|
+
console.error(
|
|
216
|
+
'[LivenessDetection] *** Recording error callback ***:',
|
|
217
|
+
error
|
|
218
|
+
);
|
|
198
219
|
setIsRecording(false);
|
|
220
|
+
isCommandInProgress.current = false;
|
|
199
221
|
},
|
|
200
222
|
onRecordingFinished(video) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
// Only process if we actually called stopRecording (i.e., reached FINISH)
|
|
224
|
+
if (!stoppingRecordingRef.current) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
dispatch({ type: 'VIDEO_RECORDED', payload: video.path });
|
|
229
|
+
trackVerificationComplete('LIVENESS_CHECK', true, 1);
|
|
230
|
+
trackFunnelStep(
|
|
231
|
+
'Liveness Check Completed',
|
|
232
|
+
3,
|
|
233
|
+
5,
|
|
234
|
+
'document_scanning',
|
|
235
|
+
true
|
|
236
|
+
);
|
|
237
|
+
setIsRecording(false);
|
|
238
|
+
isCommandInProgress.current = false;
|
|
216
239
|
},
|
|
217
240
|
});
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
setIsRecording(true);
|
|
242
|
+
console.log('[LivenessDetection] startRecording command sent to camera');
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('[LivenessDetection] Error in startRecording:', error);
|
|
245
|
+
isCommandInProgress.current = false;
|
|
246
|
+
}
|
|
220
247
|
}, [camera, isRecording]);
|
|
221
248
|
|
|
222
249
|
const stopRecording = useCallback(async () => {
|
|
223
250
|
try {
|
|
224
251
|
await camera?.stopRecording();
|
|
225
|
-
setIsRecording(false);
|
|
226
252
|
} catch (error) {
|
|
227
|
-
|
|
228
|
-
// This is expected behavior and not actionable
|
|
253
|
+
console.error('[LivenessDetection] Error in stopRecording:', error);
|
|
229
254
|
}
|
|
230
255
|
}, [camera]);
|
|
231
256
|
|
|
232
|
-
const areEyesOpen = (face: Face) =>
|
|
233
|
-
|
|
257
|
+
const areEyesOpen = (face: Face) => {
|
|
258
|
+
// Handle undefined probabilities (when ML Kit can't detect eyes)
|
|
259
|
+
const leftOpen = face.leftEyeOpenProbability ?? 0;
|
|
260
|
+
const rightOpen = face.rightEyeOpenProbability ?? 0;
|
|
261
|
+
return leftOpen >= 0.8 && rightOpen >= 0.8;
|
|
262
|
+
};
|
|
234
263
|
|
|
235
264
|
const instructionReducer = (
|
|
236
265
|
state: StateType,
|
|
237
266
|
action: PossibleActions
|
|
238
267
|
): StateType => {
|
|
239
268
|
switch (action.type) {
|
|
269
|
+
case 'RESET':
|
|
270
|
+
console.log('[LivenessDetection] Resetting to initial state');
|
|
271
|
+
referenceFaceTrackingId.current = null;
|
|
272
|
+
return initialState;
|
|
273
|
+
|
|
240
274
|
case 'BRIGHTNESS_LOW':
|
|
241
275
|
if (action.payload) {
|
|
242
276
|
return {
|
|
@@ -259,7 +293,11 @@ const LivenessDetectionScreen = () => {
|
|
|
259
293
|
|
|
260
294
|
case 'FACE_TOO_BIG':
|
|
261
295
|
if (action.payload) {
|
|
262
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
...state,
|
|
298
|
+
faceTooBig: action.payload,
|
|
299
|
+
progressFill: 0,
|
|
300
|
+
};
|
|
263
301
|
}
|
|
264
302
|
return initialState;
|
|
265
303
|
|
|
@@ -268,37 +306,67 @@ const LivenessDetectionScreen = () => {
|
|
|
268
306
|
return {
|
|
269
307
|
...state,
|
|
270
308
|
faceDetected: action.payload,
|
|
271
|
-
progressFill:
|
|
309
|
+
progressFill:
|
|
310
|
+
state.currentInstruction === 'START'
|
|
311
|
+
? 0 // No actions completed yet
|
|
312
|
+
: state.progressFill, // Keep progress if we're past START
|
|
272
313
|
};
|
|
273
314
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return { ...state, videoPath: action.payload };
|
|
278
|
-
|
|
279
|
-
case 'NEXT_INSTRUCTION':
|
|
280
|
-
if (action.payload === 'FINISH') {
|
|
315
|
+
// Face lost - reset to START if we haven't begun the flow yet
|
|
316
|
+
console.log('[LivenessDetection] Face lost');
|
|
317
|
+
if (state.currentInstruction === 'START') {
|
|
281
318
|
return {
|
|
282
319
|
...state,
|
|
283
|
-
|
|
284
|
-
progressFill:
|
|
320
|
+
faceDetected: false,
|
|
321
|
+
progressFill: 0,
|
|
285
322
|
};
|
|
286
323
|
}
|
|
324
|
+
// If past START, this should have been caught by onFacesDetected
|
|
325
|
+
// but just in case, keep state stable
|
|
326
|
+
return {
|
|
327
|
+
...state,
|
|
328
|
+
faceDetected: false,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
case 'VIDEO_RECORDED':
|
|
332
|
+
// Only finalize if we're at FINISH instruction - prevents premature completion from stale callbacks
|
|
333
|
+
if (state.currentInstruction !== 'FINISH') {
|
|
334
|
+
return state;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
...state,
|
|
338
|
+
videoPath: action.payload,
|
|
339
|
+
processComplete: true,
|
|
340
|
+
progressFill: 100,
|
|
341
|
+
};
|
|
287
342
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
343
|
+
case 'NEXT_INSTRUCTION': {
|
|
344
|
+
const currentInstructionIndex = state.instructionList.indexOf(
|
|
345
|
+
action.payload
|
|
346
|
+
);
|
|
347
|
+
const nextInstructionIndex = currentInstructionIndex + 1;
|
|
292
348
|
const nextInstruction = state.instructionList[nextInstructionIndex];
|
|
293
|
-
|
|
349
|
+
|
|
350
|
+
// Reset TTS state when moving to any new instruction to ensure it speaks
|
|
351
|
+
lastVoiceGuidanceMessage.current = '';
|
|
352
|
+
resetLastMessage();
|
|
353
|
+
|
|
354
|
+
// Calculate progress based on actual action steps (excluding START and FINISH)
|
|
355
|
+
// When last action is completed (moving to FINISH), show 100%
|
|
356
|
+
const totalActionSteps = state.instructionList.length - 2; // Exclude START and FINISH
|
|
357
|
+
const completedActionSteps = currentInstructionIndex; // Steps completed before current (START is index 0)
|
|
358
|
+
|
|
294
359
|
const newProgressFill =
|
|
295
|
-
|
|
360
|
+
nextInstruction === 'FINISH'
|
|
361
|
+
? 100 // Last action completed - show 100%
|
|
362
|
+
: (completedActionSteps / totalActionSteps) * 100;
|
|
296
363
|
|
|
297
364
|
return {
|
|
298
365
|
...state,
|
|
299
366
|
currentInstruction: nextInstruction,
|
|
300
367
|
progressFill: newProgressFill,
|
|
301
368
|
};
|
|
369
|
+
}
|
|
302
370
|
default:
|
|
303
371
|
throw new Error('Unexpected action type.');
|
|
304
372
|
}
|
|
@@ -306,9 +374,142 @@ const LivenessDetectionScreen = () => {
|
|
|
306
374
|
|
|
307
375
|
const [state, dispatch] = useReducer(instructionReducer, initialState);
|
|
308
376
|
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
if (
|
|
379
|
+
!appContext.currentWorkflowStep?.data?.voiceGuidanceActive ||
|
|
380
|
+
!hasGuideShown
|
|
381
|
+
)
|
|
382
|
+
return;
|
|
383
|
+
|
|
384
|
+
let text: string = '';
|
|
385
|
+
|
|
386
|
+
// Priority order: errors first, then face placement, then instructions
|
|
387
|
+
if (state.brightnessLow) {
|
|
388
|
+
text = t('livenessDetectionScreen.brightnessLow');
|
|
389
|
+
} else if (state.multipleFacesDetected) {
|
|
390
|
+
text = t('livenessDetectionScreen.multipleFacesDetected');
|
|
391
|
+
} else if (state.faceTooBig) {
|
|
392
|
+
text = t('livenessDetectionScreen.faceTooBig');
|
|
393
|
+
} else if (!state.faceDetected) {
|
|
394
|
+
// Only speak "place face" message when face is not detected
|
|
395
|
+
text = t('livenessDetectionScreen.placeFaceInsideCircle');
|
|
396
|
+
} else if (state.faceDetected && state.currentInstruction !== 'START') {
|
|
397
|
+
// Face is detected and we've moved past START - speak the actual instruction
|
|
398
|
+
// Don't speak START instruction, wait for first actual liveness instruction
|
|
399
|
+
text = instructions[state.currentInstruction]?.instruction ?? '';
|
|
400
|
+
}
|
|
401
|
+
// If currentInstruction is 'START' and face is detected, don't speak anything
|
|
402
|
+
// Let the instruction advance first, then speak the next instruction
|
|
403
|
+
|
|
404
|
+
// Only speak if message changed and is not empty
|
|
405
|
+
if (text && text !== lastVoiceGuidanceMessage.current) {
|
|
406
|
+
lastVoiceGuidanceMessage.current = text;
|
|
407
|
+
// Bypass interval for liveness instructions to ensure all instructions are spoken
|
|
408
|
+
speak(text, true);
|
|
409
|
+
}
|
|
410
|
+
}, [
|
|
411
|
+
appContext.currentWorkflowStep?.data?.voiceGuidanceActive,
|
|
412
|
+
hasGuideShown,
|
|
413
|
+
state.brightnessLow,
|
|
414
|
+
state.multipleFacesDetected,
|
|
415
|
+
state.faceTooBig,
|
|
416
|
+
state.faceDetected,
|
|
417
|
+
state.currentInstruction,
|
|
418
|
+
instructions,
|
|
419
|
+
t,
|
|
420
|
+
]);
|
|
421
|
+
|
|
309
422
|
const onFacesDetected = useCallback(
|
|
310
|
-
async (
|
|
423
|
+
async (
|
|
424
|
+
faces: Face[],
|
|
425
|
+
image: string,
|
|
426
|
+
isImageBright: boolean,
|
|
427
|
+
frameWidth: number,
|
|
428
|
+
frameHeight: number
|
|
429
|
+
) => {
|
|
430
|
+
// Skip processing if recording is being finalized or process is already complete
|
|
431
|
+
if (stoppingRecordingRef.current || state.processComplete) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check if no faces detected
|
|
436
|
+
if (faces.length === 0) {
|
|
437
|
+
// Face not detected - reset progress if we've started the flow
|
|
438
|
+
if (state.currentInstruction === 'START') {
|
|
439
|
+
// Just mark face as not detected
|
|
440
|
+
dispatch({ type: 'FACE_DETECTED', payload: false });
|
|
441
|
+
} else {
|
|
442
|
+
console.log(
|
|
443
|
+
'[LivenessDetection] No face detected after START, resetting to beginning'
|
|
444
|
+
);
|
|
445
|
+
if (isRecording) {
|
|
446
|
+
await camera?.cancelRecording();
|
|
447
|
+
setIsRecording(false);
|
|
448
|
+
}
|
|
449
|
+
isCommandInProgress.current = false;
|
|
450
|
+
stoppingRecordingRef.current = false;
|
|
451
|
+
dispatch({ type: 'RESET', payload: undefined });
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
311
456
|
const face = faces[0];
|
|
457
|
+
|
|
458
|
+
// Track face identity - ensure same person throughout liveness check
|
|
459
|
+
if (face.trackingId !== undefined) {
|
|
460
|
+
if (referenceFaceTrackingId.current === null) {
|
|
461
|
+
// First face detected - store tracking ID
|
|
462
|
+
referenceFaceTrackingId.current = face.trackingId;
|
|
463
|
+
console.log(
|
|
464
|
+
'[LivenessDetection] Stored reference face tracking ID:',
|
|
465
|
+
face.trackingId
|
|
466
|
+
);
|
|
467
|
+
} else if (referenceFaceTrackingId.current !== face.trackingId) {
|
|
468
|
+
// Different person detected - reset
|
|
469
|
+
console.log(
|
|
470
|
+
'[LivenessDetection] Different person detected (tracking ID changed from',
|
|
471
|
+
referenceFaceTrackingId.current,
|
|
472
|
+
'to',
|
|
473
|
+
face.trackingId,
|
|
474
|
+
'), resetting'
|
|
475
|
+
);
|
|
476
|
+
if (isRecording) {
|
|
477
|
+
await camera?.cancelRecording();
|
|
478
|
+
setIsRecording(false);
|
|
479
|
+
}
|
|
480
|
+
isCommandInProgress.current = false;
|
|
481
|
+
stoppingRecordingRef.current = false;
|
|
482
|
+
referenceFaceTrackingId.current = null;
|
|
483
|
+
dispatch({ type: 'RESET', payload: undefined });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check if frame orientation is correct (should be portrait: width < height)
|
|
489
|
+
if (frameWidth > frameHeight) {
|
|
490
|
+
console.warn(
|
|
491
|
+
'[LivenessDetection] WARNING: Frame is landscape but expected portrait!',
|
|
492
|
+
{
|
|
493
|
+
frameWidth,
|
|
494
|
+
frameHeight,
|
|
495
|
+
faceX: face.bounds.x,
|
|
496
|
+
faceY: face.bounds.y,
|
|
497
|
+
faceWidth: face.bounds.width,
|
|
498
|
+
faceHeight: face.bounds.height,
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Calculate preview rect in frame coordinates (not screen coordinates)
|
|
504
|
+
// Preview circle is 80% of frame width, centered
|
|
505
|
+
const previewSizeInFrame = frameWidth * 0.8;
|
|
506
|
+
const previewRectInFrame: Rect = {
|
|
507
|
+
minX: (frameWidth - previewSizeInFrame) / 2,
|
|
508
|
+
minY: (frameHeight - previewSizeInFrame) / 2,
|
|
509
|
+
width: previewSizeInFrame,
|
|
510
|
+
height: previewSizeInFrame,
|
|
511
|
+
};
|
|
512
|
+
|
|
312
513
|
const faceRectSmaller: Rect = {
|
|
313
514
|
width: face.bounds.width - PREVIEW_EDGE_OFFSET,
|
|
314
515
|
height: face.bounds.height - PREVIEW_EDGE_OFFSET,
|
|
@@ -316,18 +517,46 @@ const LivenessDetectionScreen = () => {
|
|
|
316
517
|
minX: face.bounds.x + PREVIEW_EDGE_OFFSET / 2,
|
|
317
518
|
};
|
|
318
519
|
const previewContainsFace = contains({
|
|
319
|
-
outside:
|
|
520
|
+
outside: previewRectInFrame,
|
|
320
521
|
inside: faceRectSmaller,
|
|
321
522
|
});
|
|
322
523
|
const multipleFacesDetected = faces.length > 1;
|
|
323
524
|
|
|
324
525
|
if (!isImageBright) {
|
|
325
|
-
|
|
526
|
+
// Brightness too low - reset progress if we've started the flow
|
|
527
|
+
if (state.currentInstruction === 'START') {
|
|
528
|
+
dispatch({ type: 'BRIGHTNESS_LOW', payload: true });
|
|
529
|
+
} else {
|
|
530
|
+
console.log(
|
|
531
|
+
'[LivenessDetection] Brightness low after START, resetting to beginning'
|
|
532
|
+
);
|
|
533
|
+
if (isRecording) {
|
|
534
|
+
await camera?.cancelRecording();
|
|
535
|
+
setIsRecording(false);
|
|
536
|
+
}
|
|
537
|
+
isCommandInProgress.current = false;
|
|
538
|
+
stoppingRecordingRef.current = false;
|
|
539
|
+
dispatch({ type: 'RESET', payload: undefined });
|
|
540
|
+
}
|
|
326
541
|
return;
|
|
327
542
|
}
|
|
328
543
|
|
|
329
544
|
if (!previewContainsFace) {
|
|
330
|
-
|
|
545
|
+
// Face went outside circle - reset progress if we've started the flow
|
|
546
|
+
if (state.currentInstruction === 'START') {
|
|
547
|
+
dispatch({ type: 'FACE_DETECTED', payload: false });
|
|
548
|
+
} else {
|
|
549
|
+
console.log(
|
|
550
|
+
'[LivenessDetection] Face outside circle after START, resetting to beginning'
|
|
551
|
+
);
|
|
552
|
+
if (isRecording) {
|
|
553
|
+
await camera?.cancelRecording();
|
|
554
|
+
setIsRecording(false);
|
|
555
|
+
}
|
|
556
|
+
isCommandInProgress.current = false;
|
|
557
|
+
stoppingRecordingRef.current = false;
|
|
558
|
+
dispatch({ type: 'RESET', payload: undefined });
|
|
559
|
+
}
|
|
331
560
|
return;
|
|
332
561
|
}
|
|
333
562
|
|
|
@@ -336,7 +565,21 @@ const LivenessDetectionScreen = () => {
|
|
|
336
565
|
}
|
|
337
566
|
|
|
338
567
|
if (!state.multipleFacesDetected && multipleFacesDetected) {
|
|
339
|
-
|
|
568
|
+
// Multiple faces detected - reset progress if we've started the flow
|
|
569
|
+
if (state.currentInstruction === 'START') {
|
|
570
|
+
dispatch({ type: 'MULTIPLE_FACES_DETECTED', payload: true });
|
|
571
|
+
} else {
|
|
572
|
+
console.log(
|
|
573
|
+
'[LivenessDetection] Multiple faces after START, resetting to beginning'
|
|
574
|
+
);
|
|
575
|
+
if (isRecording) {
|
|
576
|
+
await camera?.cancelRecording();
|
|
577
|
+
setIsRecording(false);
|
|
578
|
+
}
|
|
579
|
+
isCommandInProgress.current = false;
|
|
580
|
+
stoppingRecordingRef.current = false;
|
|
581
|
+
dispatch({ type: 'RESET', payload: undefined });
|
|
582
|
+
}
|
|
340
583
|
return;
|
|
341
584
|
}
|
|
342
585
|
|
|
@@ -344,21 +587,36 @@ const LivenessDetectionScreen = () => {
|
|
|
344
587
|
dispatch({ type: 'MULTIPLE_FACES_DETECTED', payload: false });
|
|
345
588
|
}
|
|
346
589
|
|
|
347
|
-
if
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
590
|
+
// Check if face is too big
|
|
591
|
+
const faceTooBig =
|
|
592
|
+
face.bounds.width >= previewSizeInFrame &&
|
|
593
|
+
face.bounds.height >= previewSizeInFrame;
|
|
594
|
+
|
|
595
|
+
if (faceTooBig) {
|
|
596
|
+
// Face too big - reset progress if we've started the flow
|
|
597
|
+
if (state.currentInstruction !== 'START') {
|
|
598
|
+
console.log(
|
|
599
|
+
'[LivenessDetection] Face too big after START, resetting to beginning'
|
|
600
|
+
);
|
|
601
|
+
if (isRecording) {
|
|
602
|
+
await camera?.cancelRecording();
|
|
603
|
+
setIsRecording(false);
|
|
604
|
+
}
|
|
605
|
+
isCommandInProgress.current = false;
|
|
606
|
+
stoppingRecordingRef.current = false;
|
|
607
|
+
dispatch({ type: 'RESET', payload: undefined });
|
|
608
|
+
} else if (!state.faceTooBig) {
|
|
352
609
|
dispatch({ type: 'FACE_TOO_BIG', payload: true });
|
|
353
|
-
return;
|
|
354
610
|
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
355
613
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
614
|
+
// Face is good size now, clear too big flag if set
|
|
615
|
+
if (state.faceTooBig) {
|
|
616
|
+
dispatch({ type: 'FACE_TOO_BIG', payload: false });
|
|
360
617
|
}
|
|
361
618
|
|
|
619
|
+
// Face is detected and good size
|
|
362
620
|
if (!state.faceDetected) {
|
|
363
621
|
dispatch({ type: 'FACE_DETECTED', payload: true });
|
|
364
622
|
}
|
|
@@ -366,16 +624,12 @@ const LivenessDetectionScreen = () => {
|
|
|
366
624
|
if (state.currentInstruction !== state.previousInstruction) {
|
|
367
625
|
state.previousInstruction = state.currentInstruction;
|
|
368
626
|
Vibration.vibrate(100);
|
|
369
|
-
if (appContext.currentWorkflowStep?.data?.voiceGuidanceActive) {
|
|
370
|
-
speakWithDebounce(
|
|
371
|
-
instructions[state.currentInstruction]?.instruction ?? ''
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
627
|
}
|
|
375
628
|
|
|
376
629
|
switch (state.currentInstruction) {
|
|
377
630
|
case 'START':
|
|
378
631
|
if (state.faceDetected) {
|
|
632
|
+
console.log('[LivenessDetection] Starting flow...');
|
|
379
633
|
await startRecording();
|
|
380
634
|
dispatch({ type: 'NEXT_INSTRUCTION', payload: 'START' });
|
|
381
635
|
}
|
|
@@ -389,6 +643,9 @@ const LivenessDetectionScreen = () => {
|
|
|
389
643
|
) {
|
|
390
644
|
if (instructions.LOOK_STRAIGHT_AND_BLINK.eyesClosed) {
|
|
391
645
|
if (areEyesOpen(face)) {
|
|
646
|
+
console.log(
|
|
647
|
+
'[LivenessDetection] LOOK_STRAIGHT_AND_BLINK: Eyes opened, completing instruction'
|
|
648
|
+
);
|
|
392
649
|
instructions.LOOK_STRAIGHT_AND_BLINK.eyesClosed = false;
|
|
393
650
|
instructions.LOOK_STRAIGHT_AND_BLINK.photo = image;
|
|
394
651
|
dispatch({
|
|
@@ -401,12 +658,27 @@ const LivenessDetectionScreen = () => {
|
|
|
401
658
|
}
|
|
402
659
|
}
|
|
403
660
|
return;
|
|
404
|
-
case 'SMILE':
|
|
405
|
-
|
|
661
|
+
case 'SMILE': {
|
|
662
|
+
// Handle undefined smilingProbability (when ML Kit can't detect smile)
|
|
663
|
+
const smilingProb = face.smilingProbability ?? 0;
|
|
664
|
+
// Ensure user is looking at camera (face direction check)
|
|
665
|
+
const isFacingCamera =
|
|
666
|
+
instructions.SMILE.minAngle < face.yawAngle &&
|
|
667
|
+
face.yawAngle < instructions.SMILE.maxAngle &&
|
|
668
|
+
instructions.SMILE.minAngle < face.pitchAngle &&
|
|
669
|
+
face.pitchAngle < instructions.SMILE.maxAngle;
|
|
670
|
+
|
|
671
|
+
// Check if smiling with sufficient probability AND looking at camera AND eyes open
|
|
672
|
+
if (
|
|
673
|
+
smilingProb >= instructions.SMILE.minProbability &&
|
|
674
|
+
isFacingCamera &&
|
|
675
|
+
areEyesOpen(face)
|
|
676
|
+
) {
|
|
406
677
|
instructions.SMILE.photo = image;
|
|
407
678
|
dispatch({ type: 'NEXT_INSTRUCTION', payload: 'SMILE' });
|
|
408
679
|
}
|
|
409
680
|
return;
|
|
681
|
+
}
|
|
410
682
|
case 'LOOK_UP':
|
|
411
683
|
if (
|
|
412
684
|
face.pitchAngle >= instructions.LOOK_UP.minAngle &&
|
|
@@ -416,53 +688,38 @@ const LivenessDetectionScreen = () => {
|
|
|
416
688
|
dispatch({ type: 'NEXT_INSTRUCTION', payload: 'LOOK_UP' });
|
|
417
689
|
}
|
|
418
690
|
return;
|
|
419
|
-
case 'TURN_HEAD_LEFT':
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
) {
|
|
425
|
-
instructions.TURN_HEAD_LEFT.photo = image;
|
|
426
|
-
dispatch({ type: 'NEXT_INSTRUCTION', payload: 'TURN_HEAD_LEFT' });
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
if (
|
|
430
|
-
face.yawAngle >= instructions.TURN_HEAD_LEFT.minAngle &&
|
|
431
|
-
areEyesOpen(face)
|
|
432
|
-
) {
|
|
433
|
-
instructions.TURN_HEAD_LEFT.photo = image;
|
|
434
|
-
dispatch({ type: 'NEXT_INSTRUCTION', payload: 'TURN_HEAD_LEFT' });
|
|
435
|
-
}
|
|
691
|
+
case 'TURN_HEAD_LEFT': {
|
|
692
|
+
const isLeftTurn = face.yawAngle >= TURN_ANGLE_LIMIT;
|
|
693
|
+
if (isLeftTurn && areEyesOpen(face)) {
|
|
694
|
+
instructions.TURN_HEAD_LEFT.photo = image;
|
|
695
|
+
dispatch({ type: 'NEXT_INSTRUCTION', payload: 'TURN_HEAD_LEFT' });
|
|
436
696
|
}
|
|
437
697
|
return;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
payload: 'TURN_HEAD_RIGHT',
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
} else {
|
|
451
|
-
if (
|
|
452
|
-
face.yawAngle <= instructions.TURN_HEAD_RIGHT.minAngle &&
|
|
453
|
-
areEyesOpen(face)
|
|
454
|
-
) {
|
|
455
|
-
instructions.TURN_HEAD_RIGHT.photo = image;
|
|
456
|
-
dispatch({
|
|
457
|
-
type: 'NEXT_INSTRUCTION',
|
|
458
|
-
payload: 'TURN_HEAD_RIGHT',
|
|
459
|
-
});
|
|
460
|
-
}
|
|
698
|
+
}
|
|
699
|
+
case 'TURN_HEAD_RIGHT': {
|
|
700
|
+
const isRightTurn = face.yawAngle <= -TURN_ANGLE_LIMIT;
|
|
701
|
+
if (isRightTurn && areEyesOpen(face)) {
|
|
702
|
+
instructions.TURN_HEAD_RIGHT.photo = image;
|
|
703
|
+
dispatch({
|
|
704
|
+
type: 'NEXT_INSTRUCTION',
|
|
705
|
+
payload: 'TURN_HEAD_RIGHT',
|
|
706
|
+
});
|
|
461
707
|
}
|
|
462
708
|
return;
|
|
709
|
+
}
|
|
463
710
|
case 'FINISH':
|
|
464
|
-
|
|
465
|
-
|
|
711
|
+
// Prevent multiple calls to stopRecording
|
|
712
|
+
if (stoppingRecordingRef.current) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
stoppingRecordingRef.current = true;
|
|
717
|
+
stopRecording().catch((error) => {
|
|
718
|
+
console.error(
|
|
719
|
+
'[LivenessDetection] Error stopping recording:',
|
|
720
|
+
error
|
|
721
|
+
);
|
|
722
|
+
});
|
|
466
723
|
return;
|
|
467
724
|
}
|
|
468
725
|
},
|
|
@@ -480,27 +737,43 @@ const LivenessDetectionScreen = () => {
|
|
|
480
737
|
instructions.TURN_HEAD_RIGHT,
|
|
481
738
|
stopRecording,
|
|
482
739
|
startRecording,
|
|
740
|
+
isRecording,
|
|
741
|
+
camera,
|
|
742
|
+
state.processComplete,
|
|
483
743
|
]
|
|
484
744
|
);
|
|
485
745
|
|
|
486
746
|
useEffect(() => {
|
|
747
|
+
console.log('[LivenessDetection] Navigation check:', {
|
|
748
|
+
processComplete: state.processComplete,
|
|
749
|
+
hasVideoPath: !!state.videoPath,
|
|
750
|
+
videoPath: state.videoPath,
|
|
751
|
+
hasInstructionList: !!state.instructionList,
|
|
752
|
+
hasIdentificationInfo: !!appContext.identificationInfo,
|
|
753
|
+
hasNavigation: !!navigation,
|
|
754
|
+
hasNavigated: hasNavigatedRef.current,
|
|
755
|
+
});
|
|
487
756
|
if (
|
|
488
757
|
state.processComplete &&
|
|
489
|
-
|
|
758
|
+
state.videoPath &&
|
|
490
759
|
!!state.instructionList &&
|
|
491
760
|
!!appContext.identificationInfo &&
|
|
492
761
|
!!navigation &&
|
|
493
|
-
!!instructions
|
|
762
|
+
!!instructions &&
|
|
763
|
+
!hasNavigatedRef.current
|
|
494
764
|
) {
|
|
765
|
+
console.log(
|
|
766
|
+
'[LivenessDetection] All conditions met, finalizing liveness data'
|
|
767
|
+
);
|
|
768
|
+
hasNavigatedRef.current = true;
|
|
495
769
|
appContext.identificationInfo.livenessDetection = {
|
|
496
|
-
instructions:
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
})),
|
|
501
|
-
],
|
|
770
|
+
instructions: state.instructionList.map((instruction) => ({
|
|
771
|
+
instruction: instruction,
|
|
772
|
+
photo: instructions[instruction].photo ?? '',
|
|
773
|
+
})),
|
|
502
774
|
videoPath: state.videoPath,
|
|
503
775
|
};
|
|
776
|
+
console.log('[LivenessDetection] Navigating to next step');
|
|
504
777
|
navigationManagerRef.current?.navigateToNextStep();
|
|
505
778
|
}
|
|
506
779
|
}, [
|
|
@@ -512,48 +785,40 @@ const LivenessDetectionScreen = () => {
|
|
|
512
785
|
instructions,
|
|
513
786
|
]);
|
|
514
787
|
|
|
788
|
+
// Cleanup: Cancel recording when component unmounts
|
|
789
|
+
// IMPORTANT: Empty dependency array ensures this only runs on actual unmount
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
// Capture the ref itself (not its value) so we can read .current at cleanup time
|
|
792
|
+
const stoppingRef = stoppingRecordingRef;
|
|
793
|
+
const cameraRef = camera;
|
|
794
|
+
|
|
795
|
+
return () => {
|
|
796
|
+
console.log(
|
|
797
|
+
'[LivenessDetection] Component unmounting, checking if should cancel recording'
|
|
798
|
+
);
|
|
799
|
+
console.log(
|
|
800
|
+
'[LivenessDetection] stoppingRecordingRef.current:',
|
|
801
|
+
stoppingRef.current
|
|
802
|
+
);
|
|
803
|
+
// Don't cancel if we're already stopping/finishing - let the recording finalize
|
|
804
|
+
if (stoppingRef.current) {
|
|
805
|
+
console.log(
|
|
806
|
+
'[LivenessDetection] Recording is finishing, not cancelling'
|
|
807
|
+
);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
console.log('[LivenessDetection] Cancelling incomplete recording');
|
|
811
|
+
// Cancel any ongoing recording when component unmounts
|
|
812
|
+
cameraRef?.cancelRecording().catch(() => {
|
|
813
|
+
// Ignore errors during cleanup
|
|
814
|
+
});
|
|
815
|
+
};
|
|
816
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
817
|
+
}, []); // Empty array = only run on mount/unmount
|
|
818
|
+
|
|
515
819
|
return (
|
|
516
820
|
<>
|
|
517
|
-
{
|
|
518
|
-
<SafeAreaView style={styles.guide}>
|
|
519
|
-
<LottieView
|
|
520
|
-
source={require('../../Shared/Animations/face-scan.json')}
|
|
521
|
-
style={styles.guideAnimation}
|
|
522
|
-
loop={true}
|
|
523
|
-
autoPlay
|
|
524
|
-
/>
|
|
525
|
-
<Text style={styles.guideHeader}>
|
|
526
|
-
{t('livenessDetectionScreen.guideHeader')}
|
|
527
|
-
</Text>
|
|
528
|
-
<View style={styles.guidePoints}>
|
|
529
|
-
<Text style={styles.guideText}>
|
|
530
|
-
{t('livenessDetectionScreen.guideText')}
|
|
531
|
-
</Text>
|
|
532
|
-
<Text style={styles.guideText}>
|
|
533
|
-
• {t('livenessDetectionScreen.guidePoint1')}
|
|
534
|
-
</Text>
|
|
535
|
-
<Text style={styles.guideText}>
|
|
536
|
-
• {t('livenessDetectionScreen.guidePoint2')}
|
|
537
|
-
</Text>
|
|
538
|
-
<Text style={styles.guideText}>
|
|
539
|
-
• {t('livenessDetectionScreen.guidePoint3')}
|
|
540
|
-
</Text>
|
|
541
|
-
<Text style={styles.guideText}>
|
|
542
|
-
• {t('livenessDetectionScreen.guidePoint4')}
|
|
543
|
-
</Text>
|
|
544
|
-
</View>
|
|
545
|
-
<View style={{ paddingBottom: insets.bottom }}>
|
|
546
|
-
<StyledButton
|
|
547
|
-
mode="contained"
|
|
548
|
-
onPress={() => {
|
|
549
|
-
setHasGuideShown(true);
|
|
550
|
-
}}
|
|
551
|
-
>
|
|
552
|
-
{t('general.letsGo')}
|
|
553
|
-
</StyledButton>
|
|
554
|
-
</View>
|
|
555
|
-
</SafeAreaView>
|
|
556
|
-
) : (
|
|
821
|
+
{hasGuideShown ? (
|
|
557
822
|
<>
|
|
558
823
|
<FaceCamera
|
|
559
824
|
onFacesDetected={onFacesDetected}
|
|
@@ -595,8 +860,13 @@ const LivenessDetectionScreen = () => {
|
|
|
595
860
|
mask="url(#hole-mask)"
|
|
596
861
|
/>
|
|
597
862
|
</Svg>
|
|
598
|
-
<View
|
|
599
|
-
|
|
863
|
+
<View
|
|
864
|
+
style={[
|
|
865
|
+
styles.instructionsContainerBottom,
|
|
866
|
+
{ top: PREVIEW_RECT.minY + PREVIEW_SIZE + 20 },
|
|
867
|
+
]}
|
|
868
|
+
>
|
|
869
|
+
<Text style={styles.action} numberOfLines={3} adjustsFontSizeToFit>
|
|
600
870
|
{(() => {
|
|
601
871
|
if (state.brightnessLow) {
|
|
602
872
|
return t('livenessDetectionScreen.brightnessLow');
|
|
@@ -604,24 +874,56 @@ const LivenessDetectionScreen = () => {
|
|
|
604
874
|
return t('livenessDetectionScreen.multipleFacesDetected');
|
|
605
875
|
} else if (state.faceTooBig) {
|
|
606
876
|
return t('livenessDetectionScreen.faceTooBig');
|
|
607
|
-
} else if (state.faceDetected) {
|
|
608
|
-
return t('livenessDetectionScreen.followInstructions');
|
|
609
|
-
} else {
|
|
877
|
+
} else if (!state.faceDetected) {
|
|
610
878
|
return t('livenessDetectionScreen.placeFaceInsideCircle');
|
|
879
|
+
} else {
|
|
880
|
+
return (
|
|
881
|
+
instructions[state.currentInstruction]?.instruction ?? ''
|
|
882
|
+
);
|
|
611
883
|
}
|
|
612
884
|
})()}
|
|
613
885
|
</Text>
|
|
614
886
|
</View>
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
887
|
+
</>
|
|
888
|
+
) : (
|
|
889
|
+
<SafeAreaView style={styles.guide}>
|
|
890
|
+
<LottieView
|
|
891
|
+
source={require('../../Shared/Animations/face-scan.json')}
|
|
892
|
+
style={styles.guideAnimation}
|
|
893
|
+
loop={true}
|
|
894
|
+
autoPlay
|
|
895
|
+
/>
|
|
896
|
+
<Text style={styles.guideHeader}>
|
|
897
|
+
{t('livenessDetectionScreen.guideHeader')}
|
|
898
|
+
</Text>
|
|
899
|
+
<View style={styles.guidePoints}>
|
|
900
|
+
<Text style={styles.guideText}>
|
|
901
|
+
{t('livenessDetectionScreen.guideText')}
|
|
902
|
+
</Text>
|
|
903
|
+
<Text style={styles.guideText}>
|
|
904
|
+
• {t('livenessDetectionScreen.guidePoint1')}
|
|
905
|
+
</Text>
|
|
906
|
+
<Text style={styles.guideText}>
|
|
907
|
+
• {t('livenessDetectionScreen.guidePoint2')}
|
|
908
|
+
</Text>
|
|
909
|
+
<Text style={styles.guideText}>
|
|
910
|
+
• {t('livenessDetectionScreen.guidePoint3')}
|
|
911
|
+
</Text>
|
|
912
|
+
<Text style={styles.guideText}>
|
|
913
|
+
• {t('livenessDetectionScreen.guidePoint4')}
|
|
622
914
|
</Text>
|
|
623
915
|
</View>
|
|
624
|
-
|
|
916
|
+
<View style={{ paddingBottom: insets.bottom }}>
|
|
917
|
+
<StyledButton
|
|
918
|
+
mode="contained"
|
|
919
|
+
onPress={() => {
|
|
920
|
+
setHasGuideShown(true);
|
|
921
|
+
}}
|
|
922
|
+
>
|
|
923
|
+
{t('general.letsGo')}
|
|
924
|
+
</StyledButton>
|
|
925
|
+
</View>
|
|
926
|
+
</SafeAreaView>
|
|
625
927
|
)}
|
|
626
928
|
<View style={[styles.footer, { bottom: insets.bottom }]}>
|
|
627
929
|
<NavigationManager ref={navigationManagerRef} />
|
|
@@ -657,43 +959,23 @@ const styles = StyleSheet.create({
|
|
|
657
959
|
left: 0,
|
|
658
960
|
zIndex: 1,
|
|
659
961
|
},
|
|
660
|
-
instructionsContainerTop: {
|
|
661
|
-
flex: 1,
|
|
662
|
-
position: 'absolute',
|
|
663
|
-
top: 0,
|
|
664
|
-
width: '100%',
|
|
665
|
-
height: windowHeight / 4,
|
|
666
|
-
justifyContent: 'flex-end',
|
|
667
|
-
alignItems: 'center',
|
|
668
|
-
zIndex: 1,
|
|
669
|
-
padding: 20,
|
|
670
|
-
},
|
|
671
962
|
instructionsContainerBottom: {
|
|
672
|
-
flex: 1,
|
|
673
963
|
position: 'absolute',
|
|
674
|
-
bottom: 0,
|
|
675
964
|
width: '100%',
|
|
676
|
-
|
|
965
|
+
maxHeight: windowHeight / 4,
|
|
677
966
|
justifyContent: 'flex-start',
|
|
678
967
|
alignItems: 'center',
|
|
679
968
|
zIndex: 1,
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
},
|
|
683
|
-
instructions: {
|
|
684
|
-
display: 'flex',
|
|
685
|
-
alignItems: 'center',
|
|
686
|
-
justifyContent: 'center',
|
|
687
|
-
fontSize: 20,
|
|
688
|
-
color: 'black',
|
|
969
|
+
paddingHorizontal: 20,
|
|
970
|
+
paddingVertical: 10,
|
|
689
971
|
},
|
|
690
972
|
action: {
|
|
691
|
-
|
|
692
|
-
alignItems: 'center',
|
|
693
|
-
justifyContent: 'center',
|
|
694
|
-
fontSize: 24,
|
|
973
|
+
fontSize: 22,
|
|
695
974
|
fontWeight: 'bold',
|
|
696
975
|
color: 'black',
|
|
976
|
+
textAlign: 'center',
|
|
977
|
+
lineHeight: 30,
|
|
978
|
+
paddingHorizontal: 10,
|
|
697
979
|
},
|
|
698
980
|
footer: {
|
|
699
981
|
position: 'absolute',
|