@myazahq/kyc-sdk-react-native 2.0.2 → 2.2.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 (283) hide show
  1. package/README.md +152 -8
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/AndroidManifest.xml +14 -4
  4. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
  8. package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
  9. package/app.plugin.js +102 -13
  10. package/ios/HybridMyazaEmrtd.swift +447 -0
  11. package/ios/HybridMyazaFaceDetector.swift +104 -2
  12. package/ios/HybridMyazaRectDetector.swift +103 -0
  13. package/ios/HybridMyazaTextRecognizer.swift +111 -0
  14. package/nitrogen/generated/.gitattributes +1 -0
  15. package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
  16. package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
  17. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
  18. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
  19. package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
  20. package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
  21. package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
  22. package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
  23. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
  24. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
  25. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
  26. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
  27. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
  28. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
  29. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
  30. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
  31. package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
  42. package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
  44. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
  45. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
  46. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
  47. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
  48. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
  49. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
  50. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
  51. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
  52. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
  53. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
  54. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
  56. package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
  57. package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
  58. package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
  59. package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
  60. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  61. package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
  62. package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
  63. package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
  64. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  65. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  66. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
  67. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
  68. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
  69. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
  70. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
  71. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
  72. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
  73. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
  74. package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
  75. package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
  76. package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
  77. package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
  78. package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
  79. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
  80. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
  81. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
  82. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
  83. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
  84. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
  85. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
  86. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
  87. package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
  88. package/package.json +16 -4
  89. package/src/MyazaKYC.tsx +115 -22
  90. package/src/capture/documentIdentity.ts +153 -0
  91. package/src/capture/documentSignals.ts +177 -0
  92. package/src/capture/documentTextGate.ts +171 -0
  93. package/src/capture/hints.ts +57 -0
  94. package/src/capture/index.ts +24 -0
  95. package/src/capture/rectDetector.ts +84 -0
  96. package/src/capture/rectGate.ts +91 -0
  97. package/src/capture/useAutoCapture.ts +198 -0
  98. package/src/components/CameraViewfinder.tsx +226 -70
  99. package/src/components/ContactCodeEntry.tsx +139 -0
  100. package/src/components/CountryField.tsx +87 -0
  101. package/src/components/CountryFlag.tsx +15 -15
  102. package/src/components/DialCodePicker.tsx +186 -0
  103. package/src/components/DocumentCropper.tsx +1 -1
  104. package/src/components/DocumentReview.tsx +142 -0
  105. package/src/components/DocumentReviewSide.tsx +135 -0
  106. package/src/components/DocumentReviewZoom.tsx +119 -0
  107. package/src/components/ExpiryCountdown.tsx +52 -0
  108. package/src/components/FlashOverlay.tsx +156 -0
  109. package/src/components/Icon.tsx +70 -1
  110. package/src/components/KycFlow.tsx +66 -94
  111. package/src/components/KycSheet.tsx +148 -18
  112. package/src/components/MediaSourceSheet.tsx +163 -0
  113. package/src/components/MyazaButton.tsx +14 -2
  114. package/src/components/MyazaDateField.tsx +240 -0
  115. package/src/components/MyazaInput.tsx +73 -26
  116. package/src/components/MyazaSelect.tsx +283 -0
  117. package/src/components/MyazaWordmark.tsx +33 -0
  118. package/src/components/OptionRow.tsx +132 -0
  119. package/src/components/PhoneNumberInput.tsx +149 -0
  120. package/src/components/PoweredBy.tsx +89 -0
  121. package/src/components/ReadyPrimer.tsx +149 -0
  122. package/src/components/StepView.tsx +91 -0
  123. package/src/components/Typography.tsx +18 -3
  124. package/src/components/VerifiedNotice.tsx +38 -0
  125. package/src/components/WhatsAppIcon.tsx +29 -0
  126. package/src/components/WorkflowGate.tsx +128 -0
  127. package/src/components/brand-font.ts +113 -0
  128. package/src/components/flashHoleGeometry.ts +35 -0
  129. package/src/components/flow/FatalConfigError.tsx +56 -0
  130. package/src/components/fonts.ts +2 -19
  131. package/src/components/readyPrimerContent.ts +52 -0
  132. package/src/components/runtime.tsx +38 -43
  133. package/src/components/stepHeaderMeta.tsx +87 -0
  134. package/src/components/theme-provider.tsx +89 -0
  135. package/src/components/useWorkflowMount.ts +116 -0
  136. package/src/components/viewfinder/CardGuide.tsx +60 -0
  137. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  138. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  139. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  140. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  141. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  142. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  143. package/src/config/brand.ts +97 -0
  144. package/src/config/business.ts +179 -0
  145. package/src/config/businessSteps.ts +105 -0
  146. package/src/config/contact.ts +159 -0
  147. package/src/config/countryNames.g.ts +254 -0
  148. package/src/config/currencyFlags.ts +144 -0
  149. package/src/config/font-resolve.ts +78 -0
  150. package/src/config/idTypes.ts +96 -1
  151. package/src/config/keyPeople.ts +162 -0
  152. package/src/config/phone.ts +39 -0
  153. package/src/config/proofOfAddress.ts +75 -0
  154. package/src/config/questionnaire.ts +153 -0
  155. package/src/config/regions.ts +118 -0
  156. package/src/config/registrationHint.ts +87 -0
  157. package/src/config/stepOrder.ts +118 -0
  158. package/src/config/theme.ts +38 -2
  159. package/src/config/workflowMerge.ts +124 -0
  160. package/src/emrtd/bac.ts +128 -0
  161. package/src/emrtd/bytes.ts +127 -0
  162. package/src/emrtd/crypto.ts +163 -0
  163. package/src/emrtd/der.ts +192 -0
  164. package/src/emrtd/dg2.ts +111 -0
  165. package/src/emrtd/files.ts +215 -0
  166. package/src/emrtd/index.ts +30 -0
  167. package/src/emrtd/mrzKey.ts +54 -0
  168. package/src/emrtd/native.ts +79 -0
  169. package/src/emrtd/optionalRead.ts +42 -0
  170. package/src/emrtd/read.ts +224 -0
  171. package/src/emrtd/retry.ts +98 -0
  172. package/src/emrtd/secureMessaging.ts +178 -0
  173. package/src/emrtd/session.ts +200 -0
  174. package/src/emrtd/stages.ts +122 -0
  175. package/src/index.ts +110 -0
  176. package/src/lib/calendar.ts +71 -0
  177. package/src/lib/use-keyboard-inset.ts +34 -0
  178. package/src/liveness/challengeManager.ts +27 -1
  179. package/src/liveness/faceContinuity.ts +153 -0
  180. package/src/liveness/flashDetector.ts +145 -0
  181. package/src/liveness/flashReadyGate.ts +106 -0
  182. package/src/liveness/flashRunner.ts +118 -0
  183. package/src/liveness/integritySignals.ts +60 -0
  184. package/src/liveness/livenessState.ts +139 -0
  185. package/src/liveness/speech.ts +19 -1
  186. package/src/liveness/types.ts +48 -1
  187. package/src/liveness/useFlashPhase.ts +78 -0
  188. package/src/liveness/useLiveness.ts +260 -75
  189. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  190. package/src/mrz/extract.ts +101 -0
  191. package/src/mrz/parse.ts +178 -0
  192. package/src/mrz/textRecognizer.ts +149 -0
  193. package/src/screens/ApplicantRoleStep.tsx +322 -0
  194. package/src/screens/BusinessDetailsStep.tsx +212 -0
  195. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  196. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  197. package/src/screens/BusinessKeyPeopleStep.tsx +207 -0
  198. package/src/screens/CompanyInfoFields.tsx +83 -0
  199. package/src/screens/ConsentStep.tsx +68 -68
  200. package/src/screens/ContactChannelChoice.tsx +133 -0
  201. package/src/screens/ContactCodeStep.tsx +130 -0
  202. package/src/screens/ContactDestinationField.tsx +70 -0
  203. package/src/screens/ContactFooterNote.tsx +34 -0
  204. package/src/screens/ContactVerificationStep.tsx +199 -0
  205. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  206. package/src/screens/CountrySelectStep.tsx +210 -0
  207. package/src/screens/DocumentCaptureStep.tsx +156 -132
  208. package/src/screens/IdInputStep.tsx +13 -5
  209. package/src/screens/IdTypeStep.tsx +74 -33
  210. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  211. package/src/screens/KeyPersonCard.tsx +119 -0
  212. package/src/screens/KeyPersonForm.tsx +148 -0
  213. package/src/screens/KeyPersonSheet.tsx +184 -0
  214. package/src/screens/LivenessStep.tsx +155 -247
  215. package/src/screens/MrzScanView.tsx +173 -0
  216. package/src/screens/NfcStep.tsx +290 -0
  217. package/src/screens/ProofOfAddressStep.tsx +238 -0
  218. package/src/screens/QuestionnaireField.tsx +219 -0
  219. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  220. package/src/screens/QuestionnaireStep.tsx +76 -0
  221. package/src/screens/SubmittedStep.tsx +5 -0
  222. package/src/screens/consent/model.ts +109 -0
  223. package/src/screens/document/CameraPhase.tsx +150 -0
  224. package/src/screens/document/RequiredPill.tsx +60 -0
  225. package/src/screens/liveness/LightingBanner.tsx +50 -0
  226. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  227. package/src/screens/liveness/ProgressDots.tsx +67 -0
  228. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  229. package/src/screens/liveness/constants.ts +31 -0
  230. package/src/screens/liveness/index.ts +8 -0
  231. package/src/screens/liveness/resolveGuidance.ts +27 -0
  232. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  233. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  234. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  235. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  236. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  237. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  238. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  239. package/src/screens/nfc/availability.ts +73 -0
  240. package/src/screens/nfc/readErrorMessage.ts +93 -0
  241. package/src/screens/nfc/useChipPortrait.ts +58 -0
  242. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  243. package/src/screens/useFlashHole.ts +105 -0
  244. package/src/screens/useFlashSequence.ts +96 -0
  245. package/src/screens/usePoaAttach.ts +121 -0
  246. package/src/services/api-types.ts +164 -0
  247. package/src/services/api-verify-types.ts +99 -0
  248. package/src/services/api.ts +72 -102
  249. package/src/services/cardCrop.ts +25 -10
  250. package/src/services/contactErrors.ts +43 -0
  251. package/src/services/deviceMetadata.ts +1 -1
  252. package/src/services/documentPicker.ts +20 -0
  253. package/src/services/fingerprint-sources.ts +129 -0
  254. package/src/services/fingerprint.ts +101 -0
  255. package/src/services/haptics.ts +38 -0
  256. package/src/services/mediaCompress.ts +7 -2
  257. package/src/services/resolveUrl.ts +2 -2
  258. package/src/services/workflowGate.ts +138 -0
  259. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  260. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  261. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  262. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  263. package/src/store/derive.ts +225 -0
  264. package/src/store/kycStore.ts +250 -149
  265. package/src/store/state.ts +253 -0
  266. package/src/store/submit.ts +101 -0
  267. package/src/store/submitApplicant.ts +81 -0
  268. package/src/types/appearance.ts +113 -0
  269. package/src/types/business.ts +131 -0
  270. package/src/types/config.ts +150 -124
  271. package/src/types/id-types.ts +57 -0
  272. package/src/types/verification.ts +5 -0
  273. package/src/types/workflow.ts +141 -0
  274. package/src/utils/amountFormat.ts +73 -0
  275. package/src/utils/tokens.ts +4 -2
  276. package/src/__tests__/cardCrop.test.ts +0 -39
  277. package/src/__tests__/deviceMetadata.test.ts +0 -34
  278. package/src/__tests__/errors.test.ts +0 -34
  279. package/src/__tests__/flow.test.ts +0 -61
  280. package/src/__tests__/gestureDetector.test.ts +0 -37
  281. package/src/__tests__/liveness.test.ts +0 -112
  282. package/src/__tests__/resolveUrl.test.ts +0 -64
  283. package/src/__tests__/validators.test.ts +0 -38
@@ -45,6 +45,10 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
45
45
  // frame → CameraX ImageProxy → the underlying android.media.Image for ML Kit.
46
46
  val proxy: ImageProxy = (frame as? NativeFrame)?.image ?: return noFace(128.0)
47
47
  val brightness = averageLuma(proxy)
48
+ // Mean RGB of the face region, for flash (screen-reflection) liveness.
49
+ // Taken in this same pass because the planes are already mapped — a second
50
+ // native module would walk the whole frame again for numbers available now.
51
+ val faceRgb = averageFaceRgb(proxy)
48
52
  val mediaImage = proxy.image ?: return noFace(brightness)
49
53
 
50
54
  val rotation = proxy.imageInfo.rotationDegrees
@@ -55,7 +59,9 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
55
59
  detector.process(input)
56
60
  .addOnSuccessListener { faces ->
57
61
  val face = faces.maxByOrNull { it.boundingBox.width() * it.boundingBox.height() }
58
- result = face?.let { toResult(it, mediaImage.width, mediaImage.height, rotation, faces.size, brightness) }
62
+ result = face?.let {
63
+ toResult(it, mediaImage.width, mediaImage.height, rotation, faces.size, brightness, faceRgb)
64
+ }
59
65
  latch.countDown()
60
66
  }
61
67
  .addOnFailureListener { latch.countDown() }
@@ -71,11 +77,13 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
71
77
  rotation: Int,
72
78
  faceCount: Int,
73
79
  brightness: Double,
80
+ faceRgb: Triple<Double, Double, Double>,
74
81
  ): FaceResult {
75
82
  // After rotation the frame is upright; when rotated 90/270 the stored width/
76
83
  // height are swapped, so the smaller dimension is the upright "width".
77
84
  val frameWidth = if (rotation == 90 || rotation == 270) imageHeight else imageWidth
78
85
  val ratio = (face.boundingBox.width().toDouble() / frameWidth).coerceIn(0.0, 1.0)
86
+ val frameHeight = if (rotation == 90 || rotation == 270) imageWidth else imageHeight
79
87
  return FaceResult(
80
88
  headEulerAngleX = face.headEulerAngleX.toDouble(),
81
89
  headEulerAngleY = face.headEulerAngleY.toDouble(),
@@ -86,9 +94,82 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
86
94
  faceSizeRatio = ratio,
87
95
  faceCount = faceCount.toDouble(),
88
96
  brightness = brightness,
97
+ // Face centre, so the TS continuity guard can tell "the same face moved"
98
+ // from "a different face appeared" — size alone cannot, since two people
99
+ // at the same distance measure alike.
100
+ faceCenterX = (face.boundingBox.exactCenterX().toDouble() / frameWidth).coerceIn(0.0, 1.0),
101
+ faceCenterY = (face.boundingBox.exactCenterY().toDouble() / frameHeight).coerceIn(0.0, 1.0),
102
+ // ML Kit's per-face id, stable while it follows the SAME face. A change is
103
+ // PROOF of a different face — something Vision cannot report on iOS — so
104
+ // it strengthens the geometric guard rather than replacing it.
105
+ trackingId = (face.trackingId ?: -1).toDouble(),
106
+ faceR = faceRgb.first,
107
+ faceG = faceRgb.second,
108
+ faceB = faceRgb.third,
89
109
  )
90
110
  }
91
111
 
112
+ /**
113
+ * Mean RGB (0–255 each) of the centre crop, where the positioning gate has
114
+ * already put the face. Flash liveness measures how that patch of skin
115
+ * reflects each emitted colour.
116
+ *
117
+ * The crop matches the web and iOS SDKs (centre 40% × 50%) so all three — and
118
+ * the server's re-analysis of the recorded video — measure the same region.
119
+ * Sampled from the raw YUV planes rather than converting the frame: a full
120
+ * colour conversion per frame would cost far more than the ~1k samples taken
121
+ * here.
122
+ */
123
+ private fun averageFaceRgb(image: ImageProxy): Triple<Double, Double, Double> {
124
+ val yPlane = image.planes.getOrNull(0) ?: return Triple(-1.0, -1.0, -1.0)
125
+ val uPlane = image.planes.getOrNull(1) ?: return Triple(-1.0, -1.0, -1.0)
126
+ val vPlane = image.planes.getOrNull(2) ?: return Triple(-1.0, -1.0, -1.0)
127
+ val w = image.width
128
+ val h = image.height
129
+ if (w <= 0 || h <= 0) return Triple(-1.0, -1.0, -1.0)
130
+
131
+ val x0 = (w * 0.3).toInt()
132
+ val x1 = (w * 0.7).toInt()
133
+ val y0 = (h * 0.25).toInt()
134
+ val y1 = (h * 0.75).toInt()
135
+ val stepX = maxOf(1, (x1 - x0) / 32)
136
+ val stepY = maxOf(1, (y1 - y0) / 32)
137
+
138
+ val yBuf = yPlane.buffer
139
+ val uBuf = uPlane.buffer
140
+ val vBuf = vPlane.buffer
141
+ var rs = 0.0
142
+ var gs = 0.0
143
+ var bs = 0.0
144
+ var count = 0
145
+
146
+ var y = y0
147
+ while (y < y1) {
148
+ var x = x0
149
+ while (x < x1) {
150
+ val yIdx = y * yPlane.rowStride + x * yPlane.pixelStride
151
+ // Chroma is quarter-resolution on 4:2:0, hence the halved coordinates.
152
+ val cIdx = (y / 2) * uPlane.rowStride + (x / 2) * uPlane.pixelStride
153
+ val vIdx = (y / 2) * vPlane.rowStride + (x / 2) * vPlane.pixelStride
154
+ if (yIdx < yBuf.limit() && cIdx < uBuf.limit() && vIdx < vBuf.limit()) {
155
+ val luma = (yBuf.get(yIdx).toInt() and 0xFF).toDouble()
156
+ val cb = (uBuf.get(cIdx).toInt() and 0xFF) - 128.0
157
+ val cr = (vBuf.get(vIdx).toInt() and 0xFF) - 128.0
158
+ rs += luma + 1.402 * cr
159
+ gs += luma - 0.344136 * cb - 0.714136 * cr
160
+ bs += luma + 1.772 * cb
161
+ count++
162
+ }
163
+ x += stepX
164
+ }
165
+ y += stepY
166
+ }
167
+
168
+ if (count == 0) return Triple(-1.0, -1.0, -1.0)
169
+ fun clamp(v: Double) = (v / count).coerceIn(0.0, 255.0)
170
+ return Triple(clamp(rs), clamp(gs), clamp(bs))
171
+ }
172
+
92
173
  /** Mean luma (0–255) sampled on a coarse ~64×48 grid from the Y plane. */
93
174
  private fun averageLuma(image: ImageProxy): Double {
94
175
  val plane = image.planes.getOrNull(0) ?: return 128.0
@@ -130,5 +211,11 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
130
211
  faceSizeRatio = 0.0,
131
212
  faceCount = 0.0,
132
213
  brightness = brightness,
214
+ faceCenterX = -1.0,
215
+ faceCenterY = -1.0,
216
+ trackingId = -1.0,
217
+ faceR = -1.0,
218
+ faceG = -1.0,
219
+ faceB = -1.0,
133
220
  )
134
221
  }
@@ -0,0 +1,34 @@
1
+ package com.margelo.nitro.myazakyc
2
+
3
+ import com.margelo.nitro.camera.HybridFrameSpec
4
+
5
+ /**
6
+ * Android half of document edge detection — deliberately a NO-OP.
7
+ *
8
+ * ML Kit has no frame-by-frame rectangle detector, and there is no honest way to
9
+ * fake one: a hand-rolled contour finder over camera frames would be slower than
10
+ * the text pass it sits beside and far less reliable, and a detector that
11
+ * reports a box it isn't sure about is worse than none, because auto-capture
12
+ * would shoot on it.
13
+ *
14
+ * So Android's auto-capture rides TEXT recognition alone. That is not a
15
+ * degraded path — the text gate answers framing (from the region the recognised
16
+ * text occupies) and identity (from the printed words) by itself on both
17
+ * platforms; iOS simply gets geometry as an extra corroborating signal.
18
+ *
19
+ * Reporting `found = false` is what makes the shared TypeScript gate fall back
20
+ * cleanly, with no platform branch at the call site.
21
+ */
22
+ class HybridMyazaRectDetector : HybridMyazaRectDetectorSpec() {
23
+
24
+ override fun detectRect(frame: HybridFrameSpec): DetectedRect =
25
+ DetectedRect(
26
+ found = false,
27
+ x = 0.0,
28
+ y = 0.0,
29
+ width = 0.0,
30
+ height = 0.0,
31
+ aspect = 0.0,
32
+ confidence = 0.0,
33
+ )
34
+ }
@@ -0,0 +1,118 @@
1
+ package com.margelo.nitro.myazakyc
2
+
3
+ import androidx.camera.core.ExperimentalGetImage
4
+ import androidx.camera.core.ImageProxy
5
+ import com.margelo.nitro.camera.HybridFrameSpec
6
+ import com.margelo.nitro.camera.public.NativeFrame
7
+ import android.net.Uri
8
+ import com.google.mlkit.vision.common.InputImage
9
+ import com.margelo.nitro.NitroModules
10
+ import com.margelo.nitro.core.Promise
11
+ import com.google.mlkit.vision.text.TextRecognition
12
+ import com.google.mlkit.vision.text.TextRecognizer
13
+ import com.google.mlkit.vision.text.latin.TextRecognizerOptions
14
+ import java.util.concurrent.CountDownLatch
15
+ import java.util.concurrent.TimeUnit
16
+
17
+ /**
18
+ * Android half of on-device text recognition, implementing the nitrogen-generated
19
+ * `HybridMyazaTextRecognizerSpec` from src/specs/MyazaTextRecognizer.nitro.ts.
20
+ * The iOS half is HybridMyazaTextRecognizer.swift (Apple Vision).
21
+ *
22
+ * Used for reading the MRZ off a passport or ID card. Returns LINES only —
23
+ * parsing lives in TypeScript, where it is testable against the ICAO specimen
24
+ * without a device, and where the check digits do the actual deciding.
25
+ *
26
+ * Like the face detector, the Nitro worklet calls this synchronously, so ML
27
+ * Kit's async recogniser is awaited on the worklet thread with a short bound.
28
+ * Text recognition is heavier than face detection, hence the longer timeout —
29
+ * but it still returns empty rather than stalling the camera pipeline.
30
+ */
31
+ class HybridMyazaTextRecognizer : HybridMyazaTextRecognizerSpec() {
32
+ private val recognizer: TextRecognizer =
33
+ TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
34
+
35
+ @ExperimentalGetImage
36
+ override fun recognizeText(frame: HybridFrameSpec, bottomFraction: Double): TextResult {
37
+ val proxy: ImageProxy = (frame as? NativeFrame)?.image ?: return TextResult(emptyArray())
38
+ val mediaImage = proxy.image ?: return TextResult(emptyArray())
39
+
40
+ val rotation = proxy.imageInfo.rotationDegrees
41
+ val input = InputImage.fromMediaImage(mediaImage, rotation)
42
+
43
+ // The upright frame height: when rotated 90/270 the stored dimensions swap.
44
+ val uprightHeight = if (rotation == 90 || rotation == 270) mediaImage.width else mediaImage.height
45
+ val fraction = bottomFraction.coerceIn(0.05, 1.0)
46
+ // ML Kit has no region-of-interest, so the crop is applied to the RESULT:
47
+ // anything whose box sits above the band is page text, not MRZ. That still
48
+ // buys the important half of the benefit — the printed fields stop
49
+ // competing with the MRZ for the extractor's attention.
50
+ val cutoffY = uprightHeight * (1.0 - fraction)
51
+
52
+ var lines: Array<String> = emptyArray()
53
+ val latch = CountDownLatch(1)
54
+ recognizer.process(input)
55
+ .addOnSuccessListener { text ->
56
+ lines = text.textBlocks
57
+ .flatMap { it.lines }
58
+ .filter { line ->
59
+ val box = line.boundingBox
60
+ // A line with no box cannot be placed, so it is kept rather than
61
+ // dropped — the check digits will reject it if it is noise.
62
+ box == null || fraction >= 1.0 || box.centerY() >= cutoffY
63
+ }
64
+ // Top to bottom: the MRZ's rows must arrive adjacent and in order for
65
+ // the extractor's grouping to work.
66
+ .sortedBy { it.boundingBox?.centerY() ?: 0 }
67
+ .map { it.text }
68
+ .toTypedArray()
69
+ latch.countDown()
70
+ }
71
+ .addOnFailureListener { latch.countDown() }
72
+
73
+ // 2s, not the 400ms this shipped with. ML Kit runs full-frame latin
74
+ // recognition here (it has no ROI), which routinely takes longer than
75
+ // 400ms on mid-tier hardware — so the await expired and returned EMPTY,
76
+ // and auto-capture on Android almost never saw a single line of text.
77
+ // The frame processor drops frames while busy, so a longer wait costs
78
+ // fps, not a queue; an empty result costs the whole feature.
79
+ latch.await(2000, TimeUnit.MILLISECONDS)
80
+ return TextResult(lines)
81
+ }
82
+
83
+ override fun recognizeTextInImage(uri: String, bottomFraction: Double): Promise<TextResult> {
84
+ return Promise.async {
85
+ val ctx = NitroModules.applicationContext
86
+ ?: return@async TextResult(emptyArray())
87
+ // A still is already upright — unlike the live frame, which arrives
88
+ // rotated by the camera pipeline and needs the rotation passed in.
89
+ val input = runCatching { InputImage.fromFilePath(ctx, Uri.parse(uri)) }
90
+ .getOrNull() ?: return@async TextResult(emptyArray())
91
+
92
+ val fraction = bottomFraction.coerceIn(0.05, 1.0)
93
+ val cutoffY = input.height * (1.0 - fraction)
94
+
95
+ val latch = CountDownLatch(1)
96
+ var lines: Array<String> = emptyArray()
97
+ recognizer.process(input)
98
+ .addOnSuccessListener { text ->
99
+ lines = text.textBlocks
100
+ .flatMap { it.lines }
101
+ .filter { line ->
102
+ val box = line.boundingBox
103
+ box == null || fraction >= 1.0 || box.centerY() >= cutoffY
104
+ }
105
+ .sortedBy { it.boundingBox?.centerY() ?: 0 }
106
+ .map { it.text }
107
+ .toTypedArray()
108
+ latch.countDown()
109
+ }
110
+ .addOnFailureListener { latch.countDown() }
111
+
112
+ // A still can be large, so this is more generous than the per-frame path
113
+ // — it runs once, off the camera thread, and the user is already waiting.
114
+ latch.await(4, TimeUnit.SECONDS)
115
+ TextResult(lines)
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,117 @@
1
+ package com.margelo.nitro.myazakyc
2
+
3
+ import android.graphics.Bitmap
4
+ import jj2000.j2k.codestream.HeaderInfo
5
+ import jj2000.j2k.codestream.reader.BitstreamReaderAgent
6
+ import jj2000.j2k.codestream.reader.HeaderDecoder
7
+ import jj2000.j2k.decoder.Decoder
8
+ import jj2000.j2k.fileformat.reader.FileFormatReader
9
+ import jj2000.j2k.image.DataBlkInt
10
+ import jj2000.j2k.image.ImgDataConverter
11
+ import jj2000.j2k.image.invcomptransf.InvCompTransf
12
+ import jj2000.j2k.util.ISRandomAccessIO
13
+ import jj2000.j2k.util.ParameterList
14
+ import jj2000.j2k.wavelet.synthesis.InverseWT
15
+ import java.io.ByteArrayInputStream
16
+
17
+ /**
18
+ * JPEG 2000 → Bitmap, for the DG2 chip portrait.
19
+ *
20
+ * Android's BitmapFactory has NO JPEG 2000 decoder — on iOS ImageIO registers
21
+ * `public.jpeg-2000`, which is why the portrait always showed there and never
22
+ * here (the Flutter SDK documents the same asymmetry in dg2_image.dart). Since
23
+ * no platform API will ever decode it, the decoder is bundled: JJ2000, the
24
+ * JPEG 2000 reference implementation (pure Java, `edu.ucar:jj2000`), driven
25
+ * through the same header→bitstream→entropy→dequant→inverse-DWT→inverse-CT
26
+ * chain JMRTD's Android decoder uses.
27
+ *
28
+ * Null on ANY failure, including exotic codestreams (component subsampling) —
29
+ * the portrait is a courtesy and the caller falls back to the generic mark,
30
+ * exactly as it does today.
31
+ */
32
+ internal object Jp2Decoder {
33
+
34
+ fun decode(bytes: ByteArray): Bitmap? =
35
+ try {
36
+ decodeUnsafe(bytes)
37
+ } catch (_: Throwable) {
38
+ null
39
+ }
40
+
41
+ private fun decodeUnsafe(bytes: ByteArray): Bitmap? {
42
+ val input = ISRandomAccessIO(ByteArrayInputStream(bytes), bytes.size, 1, bytes.size)
43
+
44
+ // The decoder's own defaults; JJ2000 refuses to run without a full list.
45
+ val defaults = ParameterList()
46
+ for (p in Decoder.getAllParameters()) if (p[3] != null) defaults[p[0]] = p[3]
47
+ val pl = ParameterList(defaults)
48
+
49
+ // DG2 stores either a bare codestream or the full JP2 container; the
50
+ // FileFormatReader tells them apart and finds the codestream box.
51
+ val ff = FileFormatReader(input)
52
+ ff.readFileFormat()
53
+ if (ff.JP2FFUsed) input.seek(ff.firstCodeStreamPos)
54
+
55
+ val hi = HeaderInfo()
56
+ val hd = HeaderDecoder(input, pl, hi)
57
+ val nComp = hd.numComps
58
+ if (nComp != 1 && nComp != 3) return null
59
+ val depth = IntArray(nComp) { hd.getOriginalBitDepth(it) }
60
+ val decSpec = hd.decoderSpecs
61
+
62
+ val reader = BitstreamReaderAgent.createInstance(input, hd, pl, decSpec, false, hi)
63
+ val entropy = hd.createEntropyDecoder(reader, pl)
64
+ val roi = hd.createROIDeScaler(entropy, pl, decSpec)
65
+ val dequant = hd.createDequantizer(roi, depth, decSpec)
66
+ val invWT = InverseWT.createInstance(dequant, decSpec)
67
+ invWT.setImgResLevel(reader.imgRes)
68
+ val converter = ImgDataConverter(invWT, 0)
69
+ val ict = InvCompTransf(converter, decSpec, depth, pl)
70
+
71
+ ict.setTile(0, 0)
72
+ val width = ict.imgWidth
73
+ val height = ict.imgHeight
74
+ if (width <= 0 || height <= 0 || width * height > 4_000_000) return null
75
+ // Subsampled components would need per-component upsampling; every real
76
+ // DG2 portrait observed is 4:4:4 or grayscale, so anything else bails to
77
+ // the caller's fallback rather than risking a garbled face.
78
+ for (c in 0 until nComp) {
79
+ if (ict.getCompImgWidth(c) != width || ict.getCompImgHeight(c) != height) return null
80
+ }
81
+
82
+ val channels = Array(nComp) { c ->
83
+ val blk = DataBlkInt(0, 0, width, height)
84
+ var out = ict.getInternCompData(blk, c) as DataBlkInt
85
+ // A progressive return means the data isn't final yet — ask again.
86
+ while (out.progressive) out = ict.getInternCompData(out, c) as DataBlkInt
87
+
88
+ val fixed = ict.getFixedPoint(c)
89
+ val shift = 1 shl (depth[c] - 1)
90
+ val max = (1 shl depth[c]) - 1
91
+ val plane = IntArray(width * height)
92
+ for (y in 0 until height) {
93
+ var src = out.offset + y * out.scanw
94
+ var dst = y * width
95
+ for (x in 0 until width) {
96
+ // Undo the fixed-point scaling, then the DC level shift, clamp to
97
+ // the component's range, and normalise to 8 bits.
98
+ var v = (out.data[src] shr fixed) + shift
99
+ if (v < 0) v = 0 else if (v > max) v = max
100
+ plane[dst] = if (max == 255) v else (v * 255) / max
101
+ src += 1
102
+ dst += 1
103
+ }
104
+ }
105
+ plane
106
+ }
107
+
108
+ val argb = IntArray(width * height)
109
+ val r = channels[0]
110
+ val g = if (nComp == 3) channels[1] else channels[0]
111
+ val b = if (nComp == 3) channels[2] else channels[0]
112
+ for (i in argb.indices) {
113
+ argb[i] = (0xFF shl 24) or (r[i] shl 16) or (g[i] shl 8) or b[i]
114
+ }
115
+ return Bitmap.createBitmap(argb, width, height, Bitmap.Config.ARGB_8888)
116
+ }
117
+ }
package/app.plugin.js CHANGED
@@ -1,28 +1,58 @@
1
- const { withInfoPlist, withAndroidManifest, createRunOncePlugin, AndroidConfig } = require('expo/config-plugins');
1
+ const {
2
+ withInfoPlist,
3
+ withEntitlementsPlist,
4
+ withAndroidManifest,
5
+ createRunOncePlugin,
6
+ AndroidConfig,
7
+ } = require('expo/config-plugins');
2
8
 
3
9
  const pkg = require('./package.json');
4
10
 
5
11
  // ---------------------------------------------------------------------------
6
12
  // Expo config plugin for @myazahq/kyc-sdk-react-native.
7
13
  //
8
- // The native face-detector frame-processor plugin (iOS Apple Vision / Android
9
- // ML Kit) is autolinked via expo-module.config.json (the podspec + the Android
10
- // Gradle project + MyazaFaceDetectorPackage). This config plugin only handles
11
- // the things that must be written into the HOST app's native config:
14
+ // The native modules (face detector, text recogniser, eMRTD reader) autolink
15
+ // via expo-module.config.json. This plugin only handles what must be written
16
+ // into the HOST app's native config:
12
17
  //
13
- // • iOS — NSCameraUsageDescription (document scan + selfie liveness).
18
+ // • iOS — NSCameraUsageDescription (document scan + selfie liveness), and,
19
+ // when NFC is enabled, NFCReaderUsageDescription plus the
20
+ // reader-session entitlements.
14
21
  // NOTE: voice guidance is TTS OUTPUT only, so NO microphone usage
15
22
  // string and no NSMicrophoneUsageDescription are added.
16
- // • Android — CAMERA + INTERNET permissions.
23
+ // • Android — CAMERA + INTERNET, plus NFC when enabled.
17
24
  //
18
- // Camera *preview/capture* itself comes from react-native-vision-camera; add its
19
- // own config plugin too (it injects the VisionCamera frame-processor build flag).
20
- // This plugin is intentionally minimal and idempotent (createRunOncePlugin).
25
+ // NFC is OPT-IN (`nfc: true`). The entitlement requires the App ID to carry the
26
+ // "NFC Tag Reading" capability in the Apple Developer portal; adding it
27
+ // unconditionally would break code-signing for every consumer who does not read
28
+ // chips and has not enabled it.
29
+ //
30
+ // Camera preview/capture itself comes from react-native-vision-camera; add its
31
+ // config plugin too. This one is intentionally minimal and idempotent.
21
32
  // ---------------------------------------------------------------------------
22
33
 
23
34
  const CAMERA_USAGE =
24
35
  'We use the camera to photograph your ID document and to capture a live selfie for identity verification.';
25
36
 
37
+ const NFC_USAGE =
38
+ 'We read the secure chip in your passport or ID card to confirm the document is genuine.';
39
+
40
+ /**
41
+ * The eMRTD application identifier.
42
+ *
43
+ * iOS will not open an ISO-7816 session at all unless the AID the app intends
44
+ * to select is declared — an undeclared one fails at connect time with an error
45
+ * that says nothing about the cause.
46
+ */
47
+ const EMRTD_AID = 'A0000002471001';
48
+
49
+ /** Adds a value to a plist array, leaving any existing entries alone. */
50
+ function addToList(container, key, value) {
51
+ const current = container[key];
52
+ const list = Array.isArray(current) ? current : [];
53
+ if (!list.includes(value)) container[key] = [...list, value];
54
+ }
55
+
26
56
  function withIosCameraUsage(config, customMessage) {
27
57
  return withInfoPlist(config, (cfg) => {
28
58
  if (!cfg.modResults.NSCameraUsageDescription) {
@@ -32,11 +62,62 @@ function withIosCameraUsage(config, customMessage) {
32
62
  });
33
63
  }
34
64
 
35
- function withAndroidCameraPermissions(config) {
65
+ const AID_KEY = 'com.apple.developer.nfc.readersession.iso7816.select-identifiers';
66
+ const FORMATS_KEY = 'com.apple.developer.nfc.readersession.formats';
67
+
68
+ /**
69
+ * Info.plist half of NFC. Exported so the split below can be tested without a
70
+ * device build — which is the only thing that caught it being wrong.
71
+ * @param {Record<string, unknown>} plist
72
+ */
73
+ function applyNfcInfoPlist(plist, customMessage) {
74
+ if (!plist.NFCReaderUsageDescription) {
75
+ plist.NFCReaderUsageDescription = customMessage || NFC_USAGE;
76
+ }
77
+ // The AID list is an Info.plist key, NOT an entitlement — Apple documents it
78
+ // there, and the App ID's "NFC Tag Reading" capability covers only the
79
+ // formats entitlement. Adding it to the entitlements file as well fails the
80
+ // build outright: Xcode finds no matching capability to put in the profile
81
+ // and refuses to sign ("not found and could not be included in profile").
82
+ addToList(plist, AID_KEY, EMRTD_AID);
83
+ return plist;
84
+ }
85
+
86
+ /**
87
+ * Entitlements half of NFC.
88
+ * @param {Record<string, unknown>} plist
89
+ */
90
+ function applyNfcEntitlements(plist) {
91
+ // 'TAG' is the raw ISO-7816 format a passport needs; 'NDEF' is a different
92
+ // capability entirely and does not cover it.
93
+ addToList(plist, FORMATS_KEY, 'TAG');
94
+ // Actively REMOVE the AID list from here. An earlier version of this plugin
95
+ // wrote it to the entitlements too, and prebuild MERGES into whatever
96
+ // entitlements file already exists — so without this, anyone who built once
97
+ // with that version keeps a key that makes signing fail forever, and
98
+ // re-running prebuild never clears it.
99
+ delete plist[AID_KEY];
100
+ return plist;
101
+ }
102
+
103
+ function withIosNfc(config, customMessage) {
104
+ config = withInfoPlist(config, (cfg) => {
105
+ applyNfcInfoPlist(cfg.modResults, customMessage);
106
+ return cfg;
107
+ });
108
+
109
+ return withEntitlementsPlist(config, (cfg) => {
110
+ applyNfcEntitlements(cfg.modResults);
111
+ return cfg;
112
+ });
113
+ }
114
+
115
+ function withAndroidPermissions(config, { nfc }) {
36
116
  return withAndroidManifest(config, (cfg) => {
37
117
  AndroidConfig.Permissions.ensurePermissions(cfg.modResults, [
38
118
  'android.permission.CAMERA',
39
119
  'android.permission.INTERNET',
120
+ ...(nfc ? ['android.permission.NFC'] : []),
40
121
  ]);
41
122
  return cfg;
42
123
  });
@@ -44,12 +125,20 @@ function withAndroidCameraPermissions(config) {
44
125
 
45
126
  /**
46
127
  * @param {object} config
47
- * @param {{ cameraPermission?: string }} [props]
128
+ * @param {{ cameraPermission?: string, nfc?: boolean, nfcPermission?: string }} [props]
48
129
  */
49
130
  function withMyazaKyc(config, props = {}) {
50
131
  config = withIosCameraUsage(config, props.cameraPermission);
51
- config = withAndroidCameraPermissions(config);
132
+ config = withAndroidPermissions(config, { nfc: props.nfc === true });
133
+ if (props.nfc === true) {
134
+ config = withIosNfc(config, props.nfcPermission);
135
+ }
52
136
  return config;
53
137
  }
54
138
 
55
139
  module.exports = createRunOncePlugin(withMyazaKyc, pkg.name, pkg.version);
140
+ // Named exports for the tests. The plugin itself stays the default export, so
141
+ // consumers' app.json entries are unaffected.
142
+ module.exports.applyNfcInfoPlist = applyNfcInfoPlist;
143
+ module.exports.applyNfcEntitlements = applyNfcEntitlements;
144
+ module.exports.EMRTD_AID = EMRTD_AID;