@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.
Files changed (254) hide show
  1. package/README.md +2 -9
  2. package/TrustchexSDK.podspec +5 -4
  3. package/android/build.gradle +6 -4
  4. package/android/src/main/AndroidManifest.xml +1 -1
  5. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +45 -25
  6. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraManager.kt +168 -0
  7. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +871 -0
  8. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +245 -0
  9. package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidationModule.kt +785 -0
  10. package/android/src/main/java/com/trustchex/reactnativesdk/mrz/MRZValidator.kt +419 -0
  11. package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +818 -0
  12. package/ios/Camera/TrustchexCameraManager.m +37 -0
  13. package/ios/Camera/TrustchexCameraManager.swift +125 -0
  14. package/ios/Camera/TrustchexCameraView.swift +1176 -0
  15. package/ios/MLKit/MLKitModule.m +23 -0
  16. package/ios/MLKit/MLKitModule.swift +250 -0
  17. package/ios/MRZValidation.m +39 -0
  18. package/ios/MRZValidation.swift +802 -0
  19. package/ios/MRZValidator.swift +466 -0
  20. package/ios/OpenCV/OpenCVModule.h +4 -0
  21. package/ios/OpenCV/OpenCVModule.mm +810 -0
  22. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +2 -3
  23. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +1 -2
  24. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +418 -193
  25. package/lib/module/Screens/Static/OTPVerificationScreen.js +11 -11
  26. package/lib/module/Screens/Static/QrCodeScanningScreen.js +5 -1
  27. package/lib/module/Screens/Static/ResultScreen.js +25 -2
  28. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +25 -7
  29. package/lib/module/Shared/Components/DebugNavigationPanel.js +234 -24
  30. package/lib/module/Shared/Components/EIDScanner.js +99 -9
  31. package/lib/module/Shared/Components/FaceCamera.js +170 -179
  32. package/lib/module/Shared/Components/IdentityDocumentCamera.js +2151 -771
  33. package/lib/module/Shared/Components/QrCodeScannerCamera.js +109 -107
  34. package/lib/module/Shared/Components/TrustchexCamera.js +122 -0
  35. package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +91 -0
  36. package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +2 -124
  37. package/lib/module/Shared/EIDReader/tlv/tlvInputStream.js +4 -4
  38. package/lib/module/Shared/EIDReader/tlv/tlvOutputState.js +4 -4
  39. package/lib/module/Shared/EIDReader/tlv/tlvOutputStream.js +4 -4
  40. package/lib/module/Shared/Libs/analytics.utils.js +2 -2
  41. package/lib/module/Shared/Libs/debug.utils.js +132 -0
  42. package/lib/module/Shared/Libs/deeplink.utils.js +6 -5
  43. package/lib/module/Shared/Libs/demo.utils.js +13 -3
  44. package/lib/module/Shared/Libs/mrz.utils.js +1 -175
  45. package/lib/module/Shared/Libs/native-device-info.utils.js +12 -6
  46. package/lib/module/Shared/Libs/tts.utils.js +40 -6
  47. package/lib/module/Shared/Services/AnalyticsService.js +9 -8
  48. package/lib/module/Shared/Types/mrzFields.js +1 -0
  49. package/lib/module/Translation/Resources/en.js +87 -88
  50. package/lib/module/Translation/Resources/tr.js +84 -85
  51. package/lib/module/Trustchex.js +9 -2
  52. package/lib/module/index.js +1 -0
  53. package/lib/module/version.js +1 -1
  54. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  55. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  56. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  57. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
  58. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  59. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  60. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  62. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +2 -2
  63. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +18 -4
  65. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  66. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -4
  67. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  68. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts +2 -1
  69. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +124 -0
  71. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -0
  72. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts +11 -0
  73. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts.map +1 -0
  74. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts +2 -39
  75. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
  76. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  77. package/lib/typescript/src/Shared/Libs/debug.utils.d.ts +42 -0
  78. package/lib/typescript/src/Shared/Libs/debug.utils.d.ts.map +1 -0
  79. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  80. package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
  81. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  82. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +0 -4
  83. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  84. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
  85. package/lib/typescript/src/Shared/Libs/tts.utils.d.ts +4 -3
  86. package/lib/typescript/src/Shared/Libs/tts.utils.d.ts.map +1 -1
  87. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  88. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +2 -2
  89. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  90. package/lib/typescript/src/Shared/Types/mrzFields.d.ts +11 -0
  91. package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -0
  92. package/lib/typescript/src/Translation/Resources/en.d.ts +4 -5
  93. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  94. package/lib/typescript/src/Translation/Resources/tr.d.ts +4 -5
  95. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  96. package/lib/typescript/src/Trustchex.d.ts +2 -0
  97. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  98. package/lib/typescript/src/index.d.ts +1 -0
  99. package/lib/typescript/src/index.d.ts.map +1 -1
  100. package/lib/typescript/src/version.d.ts +1 -1
  101. package/package.json +4 -35
  102. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -1
  103. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +7 -5
  104. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +2 -3
  105. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +498 -216
  106. package/src/Screens/Static/OTPVerificationScreen.tsx +37 -31
  107. package/src/Screens/Static/QrCodeScanningScreen.tsx +8 -1
  108. package/src/Screens/Static/ResultScreen.tsx +136 -88
  109. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +46 -13
  110. package/src/Shared/Components/DebugNavigationPanel.tsx +290 -34
  111. package/src/Shared/Components/EIDScanner.tsx +94 -16
  112. package/src/Shared/Components/FaceCamera.tsx +236 -203
  113. package/src/Shared/Components/IdentityDocumentCamera.tsx +3073 -1030
  114. package/src/Shared/Components/QrCodeScannerCamera.tsx +133 -127
  115. package/src/Shared/Components/TrustchexCamera.tsx +289 -0
  116. package/src/Shared/Config/camera-enhancement.config.ts +2 -2
  117. package/src/Shared/EIDReader/tlv/tlv.helpers.ts +96 -0
  118. package/src/Shared/EIDReader/tlv/tlv.utils.ts +2 -125
  119. package/src/Shared/EIDReader/tlv/tlvInputStream.ts +4 -4
  120. package/src/Shared/EIDReader/tlv/tlvOutputState.ts +4 -4
  121. package/src/Shared/EIDReader/tlv/tlvOutputStream.ts +4 -4
  122. package/src/Shared/Libs/analytics.utils.ts +48 -20
  123. package/src/Shared/Libs/debug.utils.ts +149 -0
  124. package/src/Shared/Libs/deeplink.utils.ts +7 -5
  125. package/src/Shared/Libs/demo.utils.ts +4 -0
  126. package/src/Shared/Libs/http-client.ts +12 -8
  127. package/src/Shared/Libs/mrz.utils.ts +1 -163
  128. package/src/Shared/Libs/native-device-info.utils.ts +12 -6
  129. package/src/Shared/Libs/tts.utils.ts +48 -6
  130. package/src/Shared/Services/AnalyticsService.ts +69 -24
  131. package/src/Shared/Types/identificationInfo.ts +2 -2
  132. package/src/Shared/Types/mrzFields.ts +29 -0
  133. package/src/Translation/Resources/en.ts +90 -100
  134. package/src/Translation/Resources/tr.ts +89 -97
  135. package/src/Translation/index.ts +1 -1
  136. package/src/Trustchex.tsx +21 -4
  137. package/src/index.tsx +14 -0
  138. package/src/version.ts +1 -1
  139. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/barcodescanner/BarcodeScannerFrameProcessorPlugin.kt +0 -301
  140. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/BitmapUtils.kt +0 -205
  141. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/CropperPlugin.kt +0 -72
  142. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/FrameMetadata.kt +0 -4
  143. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/facedetector/FaceDetectorFrameProcessorPlugin.kt +0 -303
  144. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/textrecognition/TextRecognitionFrameProcessorPlugin.kt +0 -115
  145. package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin-Bridging-Header.h +0 -9
  146. package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.mm +0 -22
  147. package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.swift +0 -188
  148. package/ios/VisionCameraPlugins/Cropper/Cropper-Bridging-Header.h +0 -13
  149. package/ios/VisionCameraPlugins/Cropper/Cropper.h +0 -20
  150. package/ios/VisionCameraPlugins/Cropper/Cropper.mm +0 -22
  151. package/ios/VisionCameraPlugins/Cropper/Cropper.swift +0 -145
  152. package/ios/VisionCameraPlugins/Cropper/CropperUtils.swift +0 -49
  153. package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin-Bridging-Header.h +0 -4
  154. package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.mm +0 -22
  155. package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.swift +0 -320
  156. package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin-Bridging-Header.h +0 -4
  157. package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.mm +0 -27
  158. package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.swift +0 -144
  159. package/lib/module/Shared/Libs/camera.utils.js +0 -308
  160. package/lib/module/Shared/Libs/frame-enhancement.utils.js +0 -133
  161. package/lib/module/Shared/Libs/opencv.utils.js +0 -21
  162. package/lib/module/Shared/Libs/worklet.utils.js +0 -68
  163. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.js +0 -46
  164. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +0 -35
  165. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/index.js +0 -19
  166. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.js +0 -26
  167. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/types.js +0 -3
  168. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.js +0 -197
  169. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.js +0 -101
  170. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.js +0 -60
  171. package/lib/module/Shared/VisionCameraPlugins/Cropper/index.js +0 -47
  172. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/Camera.js +0 -42
  173. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/detectFaces.js +0 -35
  174. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/index.js +0 -4
  175. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/types.js +0 -3
  176. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/Camera.js +0 -56
  177. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.js +0 -20
  178. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.js +0 -9
  179. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/index.js +0 -6
  180. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/scanText.js +0 -20
  181. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/translateText.js +0 -19
  182. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/types.js +0 -3
  183. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +0 -87
  184. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +0 -1
  185. package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts +0 -25
  186. package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts.map +0 -1
  187. package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts +0 -3
  188. package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts.map +0 -1
  189. package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts +0 -9
  190. package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts.map +0 -1
  191. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts +0 -13
  192. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts.map +0 -1
  193. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts +0 -6
  194. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +0 -1
  195. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts +0 -12
  196. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts.map +0 -1
  197. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts +0 -3
  198. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts.map +0 -1
  199. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts +0 -52
  200. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts.map +0 -1
  201. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts +0 -62
  202. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts.map +0 -1
  203. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts +0 -34
  204. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts.map +0 -1
  205. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts +0 -32
  206. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts.map +0 -1
  207. package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts +0 -23
  208. package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts.map +0 -1
  209. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts +0 -9
  210. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts.map +0 -1
  211. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts +0 -3
  212. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts.map +0 -1
  213. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts +0 -3
  214. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts.map +0 -1
  215. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts +0 -79
  216. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts.map +0 -1
  217. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts +0 -6
  218. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts.map +0 -1
  219. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts +0 -3
  220. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts.map +0 -1
  221. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts +0 -3
  222. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts.map +0 -1
  223. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts +0 -5
  224. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts.map +0 -1
  225. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts +0 -3
  226. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts.map +0 -1
  227. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts +0 -3
  228. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts.map +0 -1
  229. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts +0 -67
  230. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts.map +0 -1
  231. package/src/Shared/Libs/camera.utils.ts +0 -345
  232. package/src/Shared/Libs/frame-enhancement.utils.ts +0 -217
  233. package/src/Shared/Libs/opencv.utils.ts +0 -40
  234. package/src/Shared/Libs/worklet.utils.ts +0 -72
  235. package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.ts +0 -79
  236. package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +0 -46
  237. package/src/Shared/VisionCameraPlugins/BarcodeScanner/index.ts +0 -60
  238. package/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.ts +0 -32
  239. package/src/Shared/VisionCameraPlugins/BarcodeScanner/types.ts +0 -82
  240. package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.ts +0 -195
  241. package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.ts +0 -135
  242. package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.ts +0 -84
  243. package/src/Shared/VisionCameraPlugins/Cropper/index.ts +0 -78
  244. package/src/Shared/VisionCameraPlugins/FaceDetector/Camera.tsx +0 -63
  245. package/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.ts +0 -44
  246. package/src/Shared/VisionCameraPlugins/FaceDetector/index.ts +0 -3
  247. package/src/Shared/VisionCameraPlugins/FaceDetector/types.ts +0 -99
  248. package/src/Shared/VisionCameraPlugins/TextRecognition/Camera.tsx +0 -76
  249. package/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.ts +0 -18
  250. package/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.ts +0 -7
  251. package/src/Shared/VisionCameraPlugins/TextRecognition/index.ts +0 -7
  252. package/src/Shared/VisionCameraPlugins/TextRecognition/scanText.ts +0 -27
  253. package/src/Shared/VisionCameraPlugins/TextRecognition/translateText.ts +0 -21
  254. 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
- Dimensions,
12
- Text,
13
- View,
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 { Camera } from 'react-native-vision-camera';
22
- import type { Face } from '../../Shared/VisionCameraPlugins/FaceDetector';
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 { speakWithDebounce } from '../../Shared/Libs/tts.utils';
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<Camera | null>(null);
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 [instructions, _setInstructions] = useState<{
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
- if (isRecording) {
182
- try {
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
- setTimeout(() => {
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
- videoCodec: 'h265',
196
- onRecordingError() {
197
- // Recording errors are retried automatically, no need to track them
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
- setTimeout(() => {
202
- dispatch({ type: 'VIDEO_RECORDED', payload: video.path });
203
-
204
- // Track liveness check completion
205
- trackVerificationComplete('LIVENESS_CHECK', true, 1);
206
-
207
- trackFunnelStep(
208
- 'Liveness Check Completed',
209
- 3,
210
- 5,
211
- 'document_scanning',
212
- true
213
- );
214
- setIsRecording(false);
215
- }, 500);
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
- }, 500);
219
- setIsRecording(true);
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
- // Stop recording can fail due to race conditions when user navigates away
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
- face.leftEyeOpenProbability >= 0.8 && face.rightEyeOpenProbability >= 0.8;
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 { ...state, faceTooBig: action.payload };
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: 100 / (state.instructionList.length + 1),
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
- return initialState;
275
-
276
- case 'VIDEO_RECORDED':
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
- processComplete: true,
284
- progressFill: 100,
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
- const nextInstructionIndex =
289
- instructionList.findIndex(
290
- (instruction) => instruction === action.payload
291
- ) + 1;
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
- const progressMultiplier = nextInstructionIndex + 1;
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
- (100 / (state.instructionList.length + 1)) * progressMultiplier;
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 (faces: Face[], image: string, isImageBright: boolean) => {
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: PREVIEW_RECT,
520
+ outside: previewRectInFrame,
320
521
  inside: faceRectSmaller,
321
522
  });
322
523
  const multipleFacesDetected = faces.length > 1;
323
524
 
324
525
  if (!isImageBright) {
325
- dispatch({ type: 'BRIGHTNESS_LOW', payload: true });
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
- dispatch({ type: 'FACE_DETECTED', payload: false });
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
- dispatch({ type: 'MULTIPLE_FACES_DETECTED', payload: true });
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 (!state.faceDetected) {
348
- if (
349
- face.bounds.width >= PREVIEW_SIZE &&
350
- face.bounds.height >= PREVIEW_SIZE
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
- if (state.faceTooBig) {
357
- dispatch({ type: 'FACE_TOO_BIG', payload: false });
358
- return;
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
- if (face.smilingProbability >= instructions.SMILE.minProbability) {
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
- if (Platform.OS === 'ios') {
421
- if (
422
- face.yawAngle <= instructions.TURN_HEAD_LEFT.maxAngle &&
423
- areEyesOpen(face)
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
- case 'TURN_HEAD_RIGHT':
439
- if (Platform.OS === 'ios') {
440
- if (
441
- face.yawAngle >= instructions.TURN_HEAD_RIGHT.maxAngle &&
442
- areEyesOpen(face)
443
- ) {
444
- instructions.TURN_HEAD_RIGHT.photo = image;
445
- dispatch({
446
- type: 'NEXT_INSTRUCTION',
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
- await stopRecording();
465
- dispatch({ type: 'NEXT_INSTRUCTION', payload: 'FINISH' });
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
- !!state.videoPath &&
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
- ...state.instructionList.map((instruction) => ({
498
- instruction: instruction,
499
- photo: instructions[instruction].photo ?? '',
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
- {!hasGuideShown ? (
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 style={[styles.instructionsContainerTop, { paddingTop: insets.top }]}>
599
- <Text style={styles.instructions}>
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
- <View style={[styles.instructionsContainerBottom, { paddingBottom: insets.bottom }]}>
616
- <Text style={styles.action}>
617
- {state.faceDetected &&
618
- !state.faceTooBig &&
619
- !state.multipleFacesDetected &&
620
- !state.brightnessLow &&
621
- instructions[state.currentInstruction]?.instruction}
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
- height: windowHeight / 4,
965
+ maxHeight: windowHeight / 4,
677
966
  justifyContent: 'flex-start',
678
967
  alignItems: 'center',
679
968
  zIndex: 1,
680
- padding: 20,
681
- gap: 20,
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
- display: 'flex',
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',