@trustchex/react-native-sdk 1.354.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 (256) 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 -13
  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 +2149 -778
  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/http-client.js +1 -0
  45. package/lib/module/Shared/Libs/mrz.utils.js +1 -176
  46. package/lib/module/Shared/Libs/native-device-info.utils.js +12 -6
  47. package/lib/module/Shared/Libs/tts.utils.js +40 -6
  48. package/lib/module/Shared/Services/AnalyticsService.js +9 -8
  49. package/lib/module/Shared/Types/mrzFields.js +1 -0
  50. package/lib/module/Translation/Resources/en.js +87 -88
  51. package/lib/module/Translation/Resources/tr.js +84 -85
  52. package/lib/module/Trustchex.js +10 -19
  53. package/lib/module/index.js +1 -0
  54. package/lib/module/version.js +1 -1
  55. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  56. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  57. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  58. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
  59. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  60. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  61. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  62. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +2 -2
  64. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  65. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +18 -4
  66. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  67. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -4
  68. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  69. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts +2 -1
  70. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
  71. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +124 -0
  72. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -0
  73. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts +11 -0
  74. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.helpers.d.ts.map +1 -0
  75. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts +2 -39
  76. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
  77. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  78. package/lib/typescript/src/Shared/Libs/debug.utils.d.ts +42 -0
  79. package/lib/typescript/src/Shared/Libs/debug.utils.d.ts.map +1 -0
  80. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  81. package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
  82. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  83. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +0 -4
  84. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  85. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
  86. package/lib/typescript/src/Shared/Libs/tts.utils.d.ts +4 -3
  87. package/lib/typescript/src/Shared/Libs/tts.utils.d.ts.map +1 -1
  88. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  89. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +2 -2
  90. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  91. package/lib/typescript/src/Shared/Types/mrzFields.d.ts +11 -0
  92. package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -0
  93. package/lib/typescript/src/Translation/Resources/en.d.ts +4 -5
  94. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  95. package/lib/typescript/src/Translation/Resources/tr.d.ts +4 -5
  96. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  97. package/lib/typescript/src/Trustchex.d.ts +2 -0
  98. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  99. package/lib/typescript/src/index.d.ts +1 -0
  100. package/lib/typescript/src/index.d.ts.map +1 -1
  101. package/lib/typescript/src/version.d.ts +1 -1
  102. package/package.json +13 -36
  103. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -1
  104. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +7 -5
  105. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +2 -3
  106. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +498 -216
  107. package/src/Screens/Static/OTPVerificationScreen.tsx +37 -31
  108. package/src/Screens/Static/QrCodeScanningScreen.tsx +8 -1
  109. package/src/Screens/Static/ResultScreen.tsx +136 -104
  110. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +46 -13
  111. package/src/Shared/Components/DebugNavigationPanel.tsx +290 -34
  112. package/src/Shared/Components/EIDScanner.tsx +94 -16
  113. package/src/Shared/Components/FaceCamera.tsx +236 -203
  114. package/src/Shared/Components/IdentityDocumentCamera.tsx +3070 -1036
  115. package/src/Shared/Components/QrCodeScannerCamera.tsx +133 -127
  116. package/src/Shared/Components/TrustchexCamera.tsx +289 -0
  117. package/src/Shared/Config/camera-enhancement.config.ts +2 -2
  118. package/src/Shared/EIDReader/tlv/tlv.helpers.ts +96 -0
  119. package/src/Shared/EIDReader/tlv/tlv.utils.ts +2 -125
  120. package/src/Shared/EIDReader/tlv/tlvInputStream.ts +4 -4
  121. package/src/Shared/EIDReader/tlv/tlvOutputState.ts +4 -4
  122. package/src/Shared/EIDReader/tlv/tlvOutputStream.ts +4 -4
  123. package/src/Shared/Libs/analytics.utils.ts +48 -20
  124. package/src/Shared/Libs/debounce.utils.ts +1 -1
  125. package/src/Shared/Libs/debug.utils.ts +149 -0
  126. package/src/Shared/Libs/deeplink.utils.ts +7 -5
  127. package/src/Shared/Libs/demo.utils.ts +4 -0
  128. package/src/Shared/Libs/http-client.ts +13 -8
  129. package/src/Shared/Libs/mrz.utils.ts +1 -164
  130. package/src/Shared/Libs/native-device-info.utils.ts +12 -6
  131. package/src/Shared/Libs/tts.utils.ts +48 -6
  132. package/src/Shared/Services/AnalyticsService.ts +69 -24
  133. package/src/Shared/Types/identificationInfo.ts +2 -2
  134. package/src/Shared/Types/mrzFields.ts +29 -0
  135. package/src/Translation/Resources/en.ts +90 -100
  136. package/src/Translation/Resources/tr.ts +89 -97
  137. package/src/Translation/index.ts +1 -1
  138. package/src/Trustchex.tsx +22 -17
  139. package/src/index.tsx +14 -0
  140. package/src/version.ts +1 -1
  141. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/barcodescanner/BarcodeScannerFrameProcessorPlugin.kt +0 -301
  142. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/BitmapUtils.kt +0 -205
  143. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/CropperPlugin.kt +0 -72
  144. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/cropper/FrameMetadata.kt +0 -4
  145. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/facedetector/FaceDetectorFrameProcessorPlugin.kt +0 -303
  146. package/android/src/main/java/com/trustchex/reactnativesdk/visioncameraplugins/textrecognition/TextRecognitionFrameProcessorPlugin.kt +0 -115
  147. package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin-Bridging-Header.h +0 -9
  148. package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.mm +0 -22
  149. package/ios/VisionCameraPlugins/BarcodeScanner/BarcodeScannerFrameProcessorPlugin.swift +0 -188
  150. package/ios/VisionCameraPlugins/Cropper/Cropper-Bridging-Header.h +0 -13
  151. package/ios/VisionCameraPlugins/Cropper/Cropper.h +0 -20
  152. package/ios/VisionCameraPlugins/Cropper/Cropper.mm +0 -22
  153. package/ios/VisionCameraPlugins/Cropper/Cropper.swift +0 -145
  154. package/ios/VisionCameraPlugins/Cropper/CropperUtils.swift +0 -49
  155. package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin-Bridging-Header.h +0 -4
  156. package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.mm +0 -22
  157. package/ios/VisionCameraPlugins/FaceDetector/FaceDetectorFrameProcessorPlugin.swift +0 -320
  158. package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin-Bridging-Header.h +0 -4
  159. package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.mm +0 -27
  160. package/ios/VisionCameraPlugins/TextRecognition/TextRecognitionFrameProcessorPlugin.swift +0 -144
  161. package/lib/module/Shared/Libs/camera.utils.js +0 -308
  162. package/lib/module/Shared/Libs/frame-enhancement.utils.js +0 -133
  163. package/lib/module/Shared/Libs/opencv.utils.js +0 -21
  164. package/lib/module/Shared/Libs/worklet.utils.js +0 -53
  165. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.js +0 -46
  166. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +0 -35
  167. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/index.js +0 -19
  168. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.js +0 -26
  169. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/types.js +0 -3
  170. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.js +0 -197
  171. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.js +0 -101
  172. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.js +0 -60
  173. package/lib/module/Shared/VisionCameraPlugins/Cropper/index.js +0 -47
  174. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/Camera.js +0 -42
  175. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/detectFaces.js +0 -35
  176. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/index.js +0 -4
  177. package/lib/module/Shared/VisionCameraPlugins/FaceDetector/types.js +0 -3
  178. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/Camera.js +0 -56
  179. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.js +0 -20
  180. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.js +0 -9
  181. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/index.js +0 -6
  182. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/scanText.js +0 -20
  183. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/translateText.js +0 -19
  184. package/lib/module/Shared/VisionCameraPlugins/TextRecognition/types.js +0 -3
  185. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +0 -87
  186. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +0 -1
  187. package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts +0 -25
  188. package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts.map +0 -1
  189. package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts +0 -3
  190. package/lib/typescript/src/Shared/Libs/opencv.utils.d.ts.map +0 -1
  191. package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts +0 -3
  192. package/lib/typescript/src/Shared/Libs/worklet.utils.d.ts.map +0 -1
  193. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts +0 -13
  194. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.d.ts.map +0 -1
  195. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts +0 -6
  196. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +0 -1
  197. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts +0 -12
  198. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/index.d.ts.map +0 -1
  199. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts +0 -3
  200. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.d.ts.map +0 -1
  201. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts +0 -52
  202. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/types.d.ts.map +0 -1
  203. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts +0 -62
  204. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.d.ts.map +0 -1
  205. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts +0 -34
  206. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.d.ts.map +0 -1
  207. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts +0 -32
  208. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.d.ts.map +0 -1
  209. package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts +0 -23
  210. package/lib/typescript/src/Shared/VisionCameraPlugins/Cropper/index.d.ts.map +0 -1
  211. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts +0 -9
  212. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/Camera.d.ts.map +0 -1
  213. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts +0 -3
  214. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.d.ts.map +0 -1
  215. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts +0 -3
  216. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/index.d.ts.map +0 -1
  217. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts +0 -79
  218. package/lib/typescript/src/Shared/VisionCameraPlugins/FaceDetector/types.d.ts.map +0 -1
  219. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts +0 -6
  220. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/Camera.d.ts.map +0 -1
  221. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts +0 -3
  222. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.d.ts.map +0 -1
  223. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts +0 -3
  224. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.d.ts.map +0 -1
  225. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts +0 -5
  226. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/index.d.ts.map +0 -1
  227. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts +0 -3
  228. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/scanText.d.ts.map +0 -1
  229. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts +0 -3
  230. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/translateText.d.ts.map +0 -1
  231. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts +0 -67
  232. package/lib/typescript/src/Shared/VisionCameraPlugins/TextRecognition/types.d.ts.map +0 -1
  233. package/src/Shared/Libs/camera.utils.ts +0 -345
  234. package/src/Shared/Libs/frame-enhancement.utils.ts +0 -217
  235. package/src/Shared/Libs/opencv.utils.ts +0 -40
  236. package/src/Shared/Libs/worklet.utils.ts +0 -58
  237. package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useBarcodeScanner.ts +0 -79
  238. package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +0 -46
  239. package/src/Shared/VisionCameraPlugins/BarcodeScanner/index.ts +0 -60
  240. package/src/Shared/VisionCameraPlugins/BarcodeScanner/scanCodes.ts +0 -32
  241. package/src/Shared/VisionCameraPlugins/BarcodeScanner/types.ts +0 -82
  242. package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/convert.ts +0 -195
  243. package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/geometry.ts +0 -135
  244. package/src/Shared/VisionCameraPlugins/BarcodeScanner/utils/highlights.ts +0 -84
  245. package/src/Shared/VisionCameraPlugins/Cropper/index.ts +0 -78
  246. package/src/Shared/VisionCameraPlugins/FaceDetector/Camera.tsx +0 -63
  247. package/src/Shared/VisionCameraPlugins/FaceDetector/detectFaces.ts +0 -44
  248. package/src/Shared/VisionCameraPlugins/FaceDetector/index.ts +0 -3
  249. package/src/Shared/VisionCameraPlugins/FaceDetector/types.ts +0 -99
  250. package/src/Shared/VisionCameraPlugins/TextRecognition/Camera.tsx +0 -76
  251. package/src/Shared/VisionCameraPlugins/TextRecognition/PhotoRecognizer.ts +0 -18
  252. package/src/Shared/VisionCameraPlugins/TextRecognition/RemoveLanguageModel.ts +0 -7
  253. package/src/Shared/VisionCameraPlugins/TextRecognition/index.ts +0 -7
  254. package/src/Shared/VisionCameraPlugins/TextRecognition/scanText.ts +0 -27
  255. package/src/Shared/VisionCameraPlugins/TextRecognition/translateText.ts +0 -21
  256. 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,771 +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) {
497
- console.log('[WRONG_SIDE] Back side expected but faces detected:', faces.length);
433
+ if (elementsOutside) {
434
+ return;
435
+ }
436
+ if (nextStep === 'SCAN_ID_BACK' && cardSizedFaces.length > 0) {
498
437
  setStatus('INCORRECT');
499
438
  return;
500
439
  }
501
- if (!text || text.length < 10 || !image) {
502
- // Log when searching to help debug
503
- if (nextStep === 'SCAN_ID_BACK') {
504
- console.log('[SCAN_ID_BACK] Searching... faces:', faces.length, 'text length:', text?.length || 0);
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');
505
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) {
506
530
  setStatus('SEARCHING');
507
531
  return;
508
532
  }
509
- const {
510
- mrzText,
511
- parsedResult: parsedMRZData
512
- } = mrzUtils.getMRZData(text);
513
- const croppedFaces = await getFaceImages(faces, image, frameWidth, frameHeight);
514
- 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
+
515
740
  const scannedData = {
516
741
  image,
517
742
  documentType,
518
743
  mrzText: mrzText ?? undefined,
519
744
  mrzFields: parsedMRZData?.fields
520
745
  };
521
- scannedData.faceImage = croppedFaces[0];
522
- setCurrentFaceImage(croppedFaces[0]);
523
-
524
- // Track detected document type for UI feedback
525
- if (documentType !== 'UNKNOWN') {
526
- setDetectedDocumentType(documentType);
527
- }
528
-
529
- // Detect wrong side based on document type or face presence (works for both normal and eID scan)
530
- // For ID_BACK step: if faces are detected, it's likely the front side (wrong)
531
- // For FRONT step: if ID_BACK is detected, it's the wrong side
532
- 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';
533
747
  if (isWrongSide) {
534
748
  setStatus('INCORRECT');
535
749
  return;
536
750
  }
751
+
752
+ // Always use locked face if available
753
+ if (faceImageToUse) {
754
+ scannedData.faceImage = faceImageToUse;
755
+ }
537
756
  if (!onlyMRZScan) {
538
- if (croppedFaces.length > 0 && croppedFaces[0]) {
539
- if (currentFaceImage) {
540
- 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;
541
775
  } else {
542
- scannedData.faceImage = croppedFaces[0];
543
- setCurrentFaceImage(croppedFaces[0]);
776
+ primaryFaceOnly = faceImageToUse;
544
777
  }
545
- if (currentHologramImage) {
546
- scannedData.hologramImage = currentHologramImage;
547
- } else if (faceImages.length <= HOLOGRAM_IMAGE_COUNT) {
548
- if (device?.hasTorch) {
549
- 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
+ }
550
838
  }
551
- faceImages.push(croppedFaces[0]);
552
839
  } else {
553
- const [hologramMask, hologram] = detectHologram(faceImages);
554
- if (!!currentFaceImage && areImagesSimilar(currentFaceImage, hologram, 25000)) {
555
- setCurrentHologramMaskImage(hologramMask);
556
- scannedData.hologramImage = hologram;
557
- 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}`);
558
845
  }
559
- faceImages = [];
560
- hologramDetectionCurrentRetryCount.value++;
561
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
562
865
  if (currentSecondaryFaceImage) {
563
866
  scannedData.secondaryFaceImage = currentSecondaryFaceImage;
564
- } else if (!!scannedData.faceImage && croppedFaces.length > 1 && !!croppedFaces[1] && areImagesSimilar(scannedData.faceImage, croppedFaces[1])) {
565
- scannedData.secondaryFaceImage = croppedFaces[1];
566
- 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
+ }
916
+ } else {
917
+ secondaryFaceDetectionCurrentRetryCount.current++;
918
+ if (!facePositionValid && croppedFaces.length > 1) {
919
+ if (isDebugEnabled()) {
920
+ console.log('[SecondaryFace] ✗ Rejected - document plane changed');
921
+ }
922
+ }
923
+ }
924
+ } else if (currentSecondaryFaceImage) {
925
+ // Already have secondary face from earlier - just use it
926
+ scannedData.secondaryFaceImage = currentSecondaryFaceImage;
927
+ }
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');
567
991
  } else {
568
- secondaryFaceDetectionCurrentRetryCount.value++;
992
+ if (isDebugEnabled()) {
993
+ console.log('[SCAN_HOLOGRAM] ID card detected - proceeding to back scan');
994
+ }
995
+ setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
569
996
  }
997
+ setTimeout(() => {
998
+ onIdentityDocumentScanned(scannedData);
999
+ }, 1000);
1000
+ return;
570
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;
571
1006
  }
572
1007
  if (documentType === 'ID_FRONT') {
573
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');
574
1061
  setStatus('SCANNED');
1062
+ setIsTorchOn(false);
575
1063
  if (onlyMRZScan) {
576
- setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_ID_FRONT_OR_PASSPORT');
577
- 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);
578
1074
  } else {
1075
+ if (isDebugEnabled()) {
1076
+ console.log('[ID_FRONT Scan] Confirmed as ID card - proceeding to hologram');
1077
+ }
579
1078
  setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
1079
+ setTimeout(() => {
1080
+ onIdentityDocumentScanned(scannedData);
1081
+ }, 1000);
580
1082
  }
581
- } else if (nextStep === 'SCAN_HOLOGRAM' && (!!scannedData.hologramImage || hologramDetectionCurrentRetryCount.value >= HOLOGRAM_DETECTION_RETRY_COUNT) && (!!scannedData.secondaryFaceImage || secondaryFaceDetectionCurrentRetryCount.value >= SECOND_FACE_DETECTION_RETRY_COUNT)) {
582
- setStatus('SCANNED');
583
- setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
584
- onIdentityDocumentScanned(scannedData);
585
1083
  }
1084
+ // Note: SCAN_HOLOGRAM completion is now handled in the unified block above
586
1085
  } else if (documentType === 'PASSPORT') {
587
1086
  if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && !scannedData.hologramImage) {
588
- // For passport, require valid MRZ before proceeding
589
1087
  if (onlyMRZScan) {
590
- // eID scan: require valid MRZ
591
- 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');
592
1102
  setStatus('SCANNED');
1103
+ setIsTorchOn(false);
593
1104
  setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
594
- onIdentityDocumentScanned(scannedData);
595
- } else if (!parsedMRZData?.valid) {
596
- 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++;
597
1114
  setStatus('SCANNING');
1115
+ return; // Don't fall through to else-if
598
1116
  }
599
1117
  } else {
600
- // 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');
601
1140
  setStatus('SCANNED');
1141
+ setIsTorchOn(false);
602
1142
  setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
1143
+ setTimeout(() => {
1144
+ onIdentityDocumentScanned(scannedData);
1145
+ }, 1000);
603
1146
  }
604
- } 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)) {
605
- setStatus('SCANNED');
606
- setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
607
- onIdentityDocumentScanned(scannedData);
608
- } else if (!parsedMRZData?.valid) {
609
- mrzDetectionCurrentRetryCount.value++;
610
1147
  }
1148
+ // Note: SCAN_HOLOGRAM completion is now handled in the unified block above
611
1149
  } else if (documentType === 'ID_BACK') {
612
- 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)) {
613
- scannedData.barcodeValue = barcode?.value ?? undefined;
614
- setStatus('SCANNED');
615
- setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
616
- onIdentityDocumentScanned(scannedData);
617
- } else if (!parsedMRZData?.valid) {
618
- mrzDetectionCurrentRetryCount.value++;
619
- }
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');
620
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
+ }
621
1159
  setStatus('SCANNING');
622
1160
  }
623
-
624
- // Clear OpenCV buffers to prevent memory leaks
625
- try {
626
- OpenCV.clearBuffers();
627
- } catch (bufferError) {
628
- // Ignore buffer cleanup errors
629
- 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;
630
1165
  }
631
- }, [currentFaceImage, currentHologramImage, currentSecondaryFaceImage, device, nextStep, onlyMRZScan]);
632
- const handleExposureValue = useRunOnJS(value => {
633
- setExposure(value);
634
- }, [exposure]);
635
-
636
- // Focus trigger for when blur is detected (called from worklet)
637
- const triggerFocus = useRunOnJS(async () => {
638
- if (!cameraRef.current || !device?.supportsFocus) {
1166
+ const {
1167
+ frame
1168
+ } = event.nativeEvent;
1169
+ if (!frame.width || !frame.height || frame.width <= 0 || frame.height <= 0) {
639
1170
  return;
640
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
641
1187
  try {
642
- const width = format?.videoWidth ?? 1920;
643
- const height = format?.videoHeight ?? 1080;
644
- const centerPoint = getScanAreaCenterPoint(width, height);
645
- await cameraRef.current.focus({
646
- x: centerPoint.x,
647
- y: centerPoint.y
648
- });
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);
649
1202
  } catch (error) {
650
- // Ignore focus errors
651
- }
652
- }, [device, format]);
653
- const handleExposureAndBrightness = frame => {
654
- 'worklet';
655
-
656
- const averageBrightness = getAverageBrightness(frame);
657
- const minExposure = device?.minExposure ?? 0;
658
- const maxExposure = device?.maxExposure ?? 0;
659
-
660
- // Dynamic thresholds based on scanning state using config values
661
- // Face detection requires higher minimum brightness for reliable detection
662
- const isFrontOrPassport = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
663
- const isBack = nextStep === 'SCAN_ID_BACK';
664
-
665
- // Using config values: faceDetection { low: 50, high: 110, target: 85 }, mrzScanning { low: 45, high: 130, target: 80 }
666
- const lowerBrightnessBound = isFrontOrPassport ? 50 : 40;
667
- const upperBrightnessBound = isBack ? 130 : 120;
668
- const targetBrightness = isFrontOrPassport ? 85 : 80;
669
-
670
- // Smooth exposure adjustment with hysteresis to prevent oscillation
671
- // Only adjust if brightness is significantly outside the acceptable range
672
- const hysteresis = 5; // Dead zone to prevent jitter
673
-
674
- if (averageBrightness < lowerBrightnessBound - hysteresis && exposureValue.value < maxExposure) {
675
- // Increase exposure smoothly when too dark
676
- const step = calculateExposureStep(averageBrightness, targetBrightness);
677
- exposureValue.value = Math.min(maxExposure, exposureValue.value + step);
678
- } else if (averageBrightness > upperBrightnessBound + hysteresis && exposureValue.value > minExposure) {
679
- // Decrease exposure smoothly when too bright
680
- const step = calculateExposureStep(averageBrightness, targetBrightness);
681
- exposureValue.value = Math.max(minExposure, exposureValue.value - step);
1203
+ setIsFrameBlurry(false);
682
1204
  }
683
- // When within acceptable range (with hysteresis), don't adjust - prevents oscillation
684
1205
 
685
- const isBright = averageBrightness > lowerBrightnessBound;
686
- handleExposureValue(exposureValue.value);
687
- handleBrightness(isBright);
688
- return isBright;
689
- };
690
- const handleWorklet = frame => {
691
- 'worklet';
1206
+ // Only proceed if image quality is acceptable
1207
+ const hasAcceptableQuality = isOverallBright && isNotBlurry;
692
1208
 
693
- try {
694
- const isBright = handleExposureAndBrightness(frame);
695
- 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) {
696
1220
  return;
697
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
+ };
1263
+
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
+ }));
698
1279
 
699
- // Check for blur before processing - skip blurry frames
700
- // Use different thresholds: 25 for front (face detection), 30 for back (MRZ/text)
701
- // Higher thresholds with improved Laplacian algorithm using H+V gradients
702
- const isFront = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
703
- const blurThreshold = isFront ? 25 : 30;
704
- const blurry = checkBlurry(frame, blurThreshold);
705
- handleBlurStatus(blurry);
706
- if (blurry) {
707
- consecutiveBlurCount.value++;
708
- // Only trigger focus after 2 consecutive blurry frames (matching Flutter)
709
- if (consecutiveBlurCount.value >= 2) {
710
- triggerFocus();
711
- consecutiveBlurCount.value = 0;
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
+ });
712
1297
  }
713
- return;
714
- }
715
- // Reset blur count on sharp frame
716
- consecutiveBlurCount.value = 0;
717
-
718
- // Validate frame dimensions before processing
719
- if (!frame.width || !frame.height || frame.width <= 0 || frame.height <= 0) {
720
- console.warn('Invalid frame dimensions:', {
721
- width: frame.width,
722
- height: frame.height
723
- });
724
- return;
725
1298
  }
726
1299
 
727
- // Detect faces first with error handling
728
- let detectedFaces = [];
729
- try {
730
- if (faceDetectionEnabled) {
731
- detectedFaces = detectFaces(frame);
732
- // Reset error count on successful detection
733
- 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;
734
1314
  }
735
- } catch (faceError) {
736
- console.warn('Face detection failed:', faceError);
737
- faceDetectionErrorCount.value += 1;
738
-
739
- // Disable face detection temporarily after 5 consecutive errors
740
- if (faceDetectionErrorCount.value >= 5) {
741
- setFaceDetectionEnabled(false);
742
- // Re-enable after 10 seconds
743
- setTimeout(() => {
744
- setFaceDetectionEnabled(true);
745
- faceDetectionErrorCount.value = 0;
746
- }, 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);
747
1357
  }
748
- detectedFaces = []; // Continue without face detection
749
- }
750
1358
 
751
- // Create a copy of the frame for cropping to avoid buffer conflicts
752
- let image;
753
- try {
754
- image = crop(frame, {
755
- cropRegion: {
756
- top: 0,
757
- left: 0,
758
- width: 100,
759
- height: 100
760
- },
761
- includeImageBase64: true,
762
- saveAsFile: false
763
- });
764
- } catch (cropError) {
765
- console.warn('Crop operation failed:', cropError);
766
- 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);
767
1558
  }
768
1559
 
769
- // Text recognition with error handling
770
- // Note: CLAHE enhancement is applied to captured images, not live frames
771
- // ML Kit plugins work directly on Frame objects and don't support Mat input
772
- let scannedText;
773
- try {
774
- scannedText = scanText(frame);
775
- } catch (textError) {
776
- console.warn('Text recognition failed:', textError);
777
- scannedText = {
778
- blocks: [],
779
- resultText: ''
780
- };
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);
781
1572
  }
782
1573
 
783
- // Barcode scanning with error handling
784
- let barcodes = [];
785
- try {
786
- barcodes = scanCodes(frame, {
787
- barcodeTypes: ['code-128', 'code-39', 'code-93', 'ean-13', 'qr']
788
- });
789
- } catch (barcodeError) {
790
- console.warn('Barcode scanning failed:', barcodeError);
791
- 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;
792
1587
  }
793
- handleFaceAndText(scannedText.resultText ?? '', detectedFaces, frame.width, frame.height, barcodes.length ? barcodes[0] : undefined, image?.base64);
794
- } catch (error) {
795
- console.warn('Frame processing error:', error);
796
- }
797
- };
798
- const frameProcessor = useFrameProcessor(frame => {
799
- '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 = [];
800
1594
 
801
- if (!isCameraInitialized.value) {
802
- return;
803
- }
804
- if (Platform.OS === 'ios') {
805
- // iOS: Run at target 6 FPS using runAtTargetFps
806
- runAtTargetFps(6, () => {
807
- 'worklet';
808
-
809
- try {
810
- handleWorklet(frame);
811
- } catch (error) {
812
- console.warn('Frame processor error:', 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
+ }
813
1609
  }
814
- });
815
- } else {
816
- // Android: Run async without throttling
817
- try {
818
- runAsync(frame, () => {
819
- 'worklet';
820
-
821
- try {
822
- handleWorklet(frame);
823
- } catch (workletError) {
824
- console.warn('Worklet execution error:', 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
+ });
825
1621
  }
826
- });
827
- } catch (error) {
828
- console.warn('Frame processor error:', error);
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
+ }
1627
+ }
829
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);
830
1658
  }
831
- }, [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
+ }, []);
832
1666
  if (!permissionsRequested) {
833
- return /*#__PURE__*/_jsx(SafeAreaView, {
1667
+ return /*#__PURE__*/_jsxs(SafeAreaView, {
834
1668
  style: styles.permissionContainer,
835
- children: /*#__PURE__*/_jsx(ActivityIndicator, {
1669
+ children: [/*#__PURE__*/_jsx(StatusBar, {
1670
+ barStyle: "dark-content"
1671
+ }), /*#__PURE__*/_jsx(ActivityIndicator, {
836
1672
  size: "large",
837
1673
  color: theme.colors.primary
838
- })
1674
+ })]
839
1675
  });
840
1676
  }
841
- if (!cameraPermission.hasPermission) {
1677
+ if (!hasPermission) {
842
1678
  return /*#__PURE__*/_jsxs(SafeAreaView, {
843
1679
  style: styles.permissionContainer,
844
- children: [/*#__PURE__*/_jsx(Text, {
1680
+ children: [/*#__PURE__*/_jsx(StatusBar, {
1681
+ barStyle: "dark-content"
1682
+ }), /*#__PURE__*/_jsx(TextView, {
845
1683
  style: styles.permissionText,
846
1684
  children: t('general.noCameraPermissionGiven')
847
1685
  }), /*#__PURE__*/_jsx(StyledButton, {
@@ -853,34 +1691,13 @@ const IdentityDocumentCamera = ({
853
1691
  })]
854
1692
  });
855
1693
  }
856
- if (device == null) {
857
- return /*#__PURE__*/_jsx(SafeAreaView, {
858
- style: styles.permissionContainer,
859
- children: /*#__PURE__*/_jsx(TextView, {
860
- style: styles.permissionText,
861
- children: t('general.noCameraDetected')
862
- })
863
- });
864
- }
865
- const handleFocus = async event => {
866
- if (cameraRef.current && device.supportsFocus) {
867
- try {
868
- const {
869
- locationX,
870
- locationY
871
- } = event.nativeEvent;
872
- await cameraRef.current.focus({
873
- x: locationX,
874
- y: locationY
875
- });
876
- } catch (error) {
877
- // console.log('Error while focusing:', error);
878
- }
879
- }
880
- };
881
- return /*#__PURE__*/_jsx(View, {
1694
+ return /*#__PURE__*/_jsxs(View, {
882
1695
  style: StyleSheet.absoluteFill,
883
- 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, {
884
1701
  style: styles.guide,
885
1702
  children: [/*#__PURE__*/_jsx(LottieView, {
886
1703
  source: require('../../Shared/Animations/id-or-passport.json'),
@@ -913,22 +1730,360 @@ const IdentityDocumentCamera = ({
913
1730
  children: t('general.letsGo')
914
1731
  })]
915
1732
  }) : /*#__PURE__*/_jsxs(_Fragment, {
916
- children: [/*#__PURE__*/_jsx(Camera, {
1733
+ children: [/*#__PURE__*/_jsx(TrustchexCamera, {
917
1734
  ref: cameraRef,
918
- frameProcessor: frameProcessor,
919
1735
  style: StyleSheet.absoluteFill,
920
- device: device,
921
- format: format,
922
- isActive: isActive,
923
- photo: false,
924
- video: false,
925
- audio: false,
926
- torch: isTorchOn ? 'on' : 'off',
927
- fps: Math.max(format?.minFps ?? 0, 30),
928
- exposure: exposure,
929
- onInitialized: () => {
930
- isCameraInitialized.value = true;
931
- }
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
+ })
932
2087
  }), /*#__PURE__*/_jsxs(View, {
933
2088
  style: [styles.topZone, {
934
2089
  paddingTop: insets.top
@@ -946,85 +2101,197 @@ const IdentityDocumentCamera = ({
946
2101
  total: 3
947
2102
  })}` : ''
948
2103
  }), /*#__PURE__*/_jsx(TextView, {
949
- style: [styles.topZoneText, status === 'SCANNING' && styles.topZoneTextScanning, status === 'SCANNED' && styles.topZoneTextSuccess, status === 'INCORRECT' && styles.topZoneTextError, (isBrightnessLow || isFrameBlurry) && styles.topZoneTextWarning],
950
- 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') : ''
951
2118
  })]
952
2119
  }), /*#__PURE__*/_jsx(View, {
953
2120
  style: styles.leftZone
954
2121
  }), /*#__PURE__*/_jsx(View, {
955
2122
  style: styles.rightZone
956
- }), /*#__PURE__*/_jsxs(View, {
2123
+ }), /*#__PURE__*/_jsx(View, {
957
2124
  style: styles.bottomZone,
958
- children: [showDebugImages && currentFaceImage && /*#__PURE__*/_jsxs(View, {
959
- style: styles.imageContainer,
960
- children: [/*#__PURE__*/_jsx(Image, {
961
- source: {
962
- uri: `data:image/jpeg;base64,${currentFaceImage}`
963
- },
964
- style: styles.faceImage
965
- }), /*#__PURE__*/_jsx(TextView, {
966
- style: styles.imageContainerText,
967
- children: "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
+ })]
968
2290
  })]
969
- }), showDebugImages && currentSecondaryFaceImage && /*#__PURE__*/_jsxs(View, {
970
- style: styles.imageContainer,
971
- children: [/*#__PURE__*/_jsx(Image, {
972
- source: {
973
- uri: `data:image/jpeg;base64,${currentSecondaryFaceImage}`
974
- },
975
- style: styles.faceImage
976
- }), /*#__PURE__*/_jsx(TextView, {
977
- style: styles.imageContainerText,
978
- children: "Secondary Face"
979
- })]
980
- }), showDebugImages && _currentHologramMaskImage && /*#__PURE__*/_jsxs(View, {
981
- style: styles.imageContainer,
982
- children: [/*#__PURE__*/_jsx(Image, {
983
- source: {
984
- uri: `data:image/jpeg;base64,${_currentHologramMaskImage}`
985
- },
986
- style: styles.faceImage
987
- }), /*#__PURE__*/_jsx(TextView, {
988
- style: styles.imageContainerText,
989
- children: "Hologram Mask"
990
- })]
991
- }), showDebugImages && currentHologramImage && /*#__PURE__*/_jsxs(View, {
992
- style: styles.imageContainer,
993
- children: [/*#__PURE__*/_jsx(Image, {
994
- source: {
995
- uri: `data:image/jpeg;base64,${currentHologramImage}`
996
- },
997
- style: styles.faceImage
998
- }), /*#__PURE__*/_jsx(TextView, {
999
- style: styles.imageContainerText,
1000
- children: "Hologram"
1001
- })]
1002
- }), showDebugImages && /*#__PURE__*/_jsxs(View, {
1003
- style: styles.debugInfoContainer,
1004
- children: [/*#__PURE__*/_jsxs(TextView, {
1005
- style: styles.debugInfoText,
1006
- children: ["Step: ", nextStep]
1007
- }), /*#__PURE__*/_jsxs(TextView, {
1008
- style: styles.debugInfoText,
1009
- children: ["Status: ", status]
1010
- }), /*#__PURE__*/_jsx(TextView, {
1011
- style: styles.debugInfoText,
1012
- children: `Face: ${currentFaceImage ? '✓' : '✗'} ${!faceDetectionEnabled ? '(DISABLED)' : ''}`
1013
- }), /*#__PURE__*/_jsx(TextView, {
1014
- style: styles.debugInfoText,
1015
- children: `Hologram: ${currentHologramImage ? '✓' : '✗'} (${hologramDetectionCurrentRetryCount.value}/${HOLOGRAM_DETECTION_RETRY_COUNT})`
1016
- }), /*#__PURE__*/_jsx(TextView, {
1017
- style: styles.debugInfoText,
1018
- children: `2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'} (${secondaryFaceDetectionCurrentRetryCount.value}/${SECOND_FACE_DETECTION_RETRY_COUNT})`
1019
- })]
1020
- })]
2291
+ })
1021
2292
  }), /*#__PURE__*/_jsx(View, {
1022
2293
  style: [styles.scanArea, {
1023
- borderColor: status === 'SCANNED' || nextStep === 'COMPLETED' ? '#4CAF50' // Green - success
1024
- : status === 'INCORRECT' ? '#f44336' // Red - error
1025
- : status === 'SCANNING' ? '#2196F3' // Blue - processing
1026
- : isBrightnessLow || isFrameBlurry ? '#FFC107' // Yellow - warning
1027
- : 'white',
2294
+ borderColor: status === 'SCANNED' || nextStep === 'COMPLETED' ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
1028
2295
  borderWidth: status === 'SCANNING' ? 3 : 2
1029
2296
  }],
1030
2297
  children: nextStep === 'COMPLETED' || status === 'SCANNED' ? /*#__PURE__*/_jsx(LottieView, {
@@ -1048,12 +2315,100 @@ const IdentityDocumentCamera = ({
1048
2315
  loop: true,
1049
2316
  autoPlay: true
1050
2317
  }) : null
1051
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
1052
- onPress: handleFocus,
1053
- style: styles.focusArea,
1054
- 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
+ })]
1055
2410
  })]
1056
- })
2411
+ })]
1057
2412
  });
1058
2413
  };
1059
2414
  const styles = StyleSheet.create({
@@ -1086,14 +2441,6 @@ const styles = StyleSheet.create({
1086
2441
  alignItems: 'center',
1087
2442
  paddingHorizontal: 5
1088
2443
  },
1089
- focusArea: {
1090
- position: 'absolute',
1091
- top: 0,
1092
- left: 0,
1093
- width: '100%',
1094
- height: '100%',
1095
- zIndex: 2
1096
- },
1097
2444
  animation: {
1098
2445
  width: '100%',
1099
2446
  height: '100%',
@@ -1124,16 +2471,16 @@ const styles = StyleSheet.create({
1124
2471
  padding: 20
1125
2472
  },
1126
2473
  topZoneTextScanning: {
1127
- color: '#2196F3' // Blue when scanning
2474
+ color: '#2196F3'
1128
2475
  },
1129
2476
  topZoneTextSuccess: {
1130
- color: '#4CAF50' // Green for success
2477
+ color: '#4CAF50'
1131
2478
  },
1132
2479
  topZoneTextWarning: {
1133
- color: '#FFC107' // Yellow for warnings
2480
+ color: '#FFC107'
1134
2481
  },
1135
2482
  topZoneTextError: {
1136
- color: '#f44336' // Red for errors
2483
+ color: '#f44336'
1137
2484
  },
1138
2485
  leftZone: {
1139
2486
  position: 'absolute',
@@ -1159,12 +2506,24 @@ const styles = StyleSheet.create({
1159
2506
  bottom: 0,
1160
2507
  backgroundColor: '#00000099',
1161
2508
  padding: 20,
2509
+ display: 'flex',
2510
+ flexDirection: 'column',
2511
+ gap: 10,
2512
+ justifyContent: 'flex-start'
2513
+ },
2514
+ debugImagesRow: {
1162
2515
  display: 'flex',
1163
2516
  flexDirection: 'row',
1164
2517
  gap: 10,
1165
2518
  justifyContent: 'flex-start',
1166
2519
  flexWrap: 'wrap'
1167
2520
  },
2521
+ cardDetectionRow: {
2522
+ display: 'flex',
2523
+ flexDirection: 'row',
2524
+ justifyContent: 'center',
2525
+ marginTop: 5
2526
+ },
1168
2527
  imageContainer: {
1169
2528
  display: 'flex',
1170
2529
  flexDirection: 'column',
@@ -1184,6 +2543,18 @@ const styles = StyleSheet.create({
1184
2543
  borderWidth: 1,
1185
2544
  borderColor: 'white'
1186
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
+ },
1187
2558
  debugInfoContainer: {
1188
2559
  flex: 1,
1189
2560
  paddingLeft: 10