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