@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 (219) hide show
  1. package/README.md +277 -7
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +146 -10
  9. package/ios/HybridMyazaFaceDetector.swift +8 -3
  10. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  12. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  14. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  16. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  18. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  19. package/package.json +30 -9
  20. package/src/MyazaKYC.tsx +4 -11
  21. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  22. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  23. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  24. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  25. package/src/assets/fonts/OFL-Karla.txt +93 -0
  26. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  27. package/src/assets/fonts/README.md +12 -0
  28. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  30. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  31. package/src/capture/useAutoCapture.ts +8 -1
  32. package/src/components/CountryField.tsx +8 -0
  33. package/src/components/CountryRegionPicker.tsx +184 -0
  34. package/src/components/DialCodePicker.tsx +47 -68
  35. package/src/components/DialCodeRow.tsx +102 -0
  36. package/src/components/DocumentCropper.tsx +30 -24
  37. package/src/components/DocumentReview.tsx +11 -1
  38. package/src/components/DocumentReviewSide.tsx +6 -2
  39. package/src/components/DocumentReviewZoom.tsx +5 -1
  40. package/src/components/FramedMapPicker.tsx +122 -0
  41. package/src/components/GeoBadge.tsx +34 -0
  42. package/src/components/Icon.tsx +10 -192
  43. package/src/components/KycFlow.tsx +69 -34
  44. package/src/components/LineSkeleton.tsx +100 -0
  45. package/src/components/MapChrome.tsx +67 -0
  46. package/src/components/MapPinMarker.tsx +54 -0
  47. package/src/components/MapPinPicker.tsx +179 -0
  48. package/src/components/MilestoneTrack.tsx +155 -0
  49. package/src/components/MyazaButton.tsx +9 -0
  50. package/src/components/MyazaInput.tsx +17 -1
  51. package/src/components/OptionRow.tsx +22 -3
  52. package/src/components/PhoneNumberInput.tsx +8 -0
  53. package/src/components/PoweredBy.tsx +5 -5
  54. package/src/components/PresenceBlocks.tsx +153 -0
  55. package/src/components/RequiredDocumentPill.tsx +114 -0
  56. package/src/components/StepHeader.tsx +23 -3
  57. package/src/components/StepView.tsx +17 -0
  58. package/src/components/StickyActions.tsx +56 -0
  59. package/src/components/dialCodeRows.ts +64 -0
  60. package/src/components/documentReviewCopy.ts +39 -0
  61. package/src/components/fonts.ts +14 -17
  62. package/src/components/icon-map.ts +176 -0
  63. package/src/components/stepHeaderMeta.tsx +48 -5
  64. package/src/config/addressCollection.ts +125 -0
  65. package/src/config/biometricOptions.ts +100 -0
  66. package/src/config/business.ts +20 -1
  67. package/src/config/businessSteps.ts +8 -2
  68. package/src/config/consentStep.ts +19 -0
  69. package/src/config/documentCaptureMethods.ts +31 -0
  70. package/src/config/proofOfAddress.ts +54 -11
  71. package/src/config/regions.ts +24 -0
  72. package/src/config/stepOrder.ts +79 -5
  73. package/src/config/uploadLimits.ts +35 -0
  74. package/src/config/workflowMerge.ts +13 -5
  75. package/src/index.ts +49 -1
  76. package/src/lib/address-current-location.ts +171 -0
  77. package/src/lib/address-field-modes.ts +119 -0
  78. package/src/lib/address-flow.ts +172 -0
  79. package/src/lib/address-helpers.ts +48 -0
  80. package/src/lib/address-line.ts +76 -0
  81. package/src/lib/address-pin-move.ts +97 -0
  82. package/src/lib/address-step-recovery.ts +63 -0
  83. package/src/lib/authed-image.ts +78 -0
  84. package/src/lib/biometric-copy.ts +51 -0
  85. package/src/lib/captureRing.ts +83 -0
  86. package/src/lib/country-adoption.ts +89 -0
  87. package/src/lib/documentCaptureCheck.ts +136 -0
  88. package/src/lib/inferred-country.ts +65 -0
  89. package/src/lib/livenessLayout.ts +49 -0
  90. package/src/lib/map-frame.ts +159 -0
  91. package/src/lib/map-tiles.ts +143 -0
  92. package/src/lib/model-ready.ts +82 -0
  93. package/src/lib/poa-country-gate.ts +27 -0
  94. package/src/lib/prime-models.ts +50 -0
  95. package/src/lib/resubmit.ts +36 -7
  96. package/src/lib/result-copy.ts +116 -0
  97. package/src/lib/result-wait.ts +53 -0
  98. package/src/lib/review-map-surface.ts +26 -0
  99. package/src/lib/scope.ts +31 -0
  100. package/src/lib/selfie-sharpness.ts +211 -0
  101. package/src/lib/selfie-upload-wait.ts +79 -0
  102. package/src/lib/street-view-fov.ts +42 -0
  103. package/src/lib/webview-available.ts +47 -0
  104. package/src/liveness/avatarSource.ts +59 -0
  105. package/src/liveness/useLiveness.ts +6 -1
  106. package/src/liveness/useModelReady.ts +9 -55
  107. package/src/mrz/extract.ts +71 -4
  108. package/src/mrz/textRecognizer.ts +36 -0
  109. package/src/mrz/useTextModelReady.ts +21 -0
  110. package/src/presence/background-math.ts +100 -0
  111. package/src/presence/background-store.ts +82 -0
  112. package/src/presence/background.ts +165 -0
  113. package/src/presence/foreground-service.ts +193 -0
  114. package/src/presence/fs.ts +51 -0
  115. package/src/presence/geofence.ts +41 -0
  116. package/src/presence/math.ts +44 -0
  117. package/src/presence/post.ts +40 -0
  118. package/src/presence/report.ts +87 -0
  119. package/src/presence/sampler.ts +110 -0
  120. package/src/presence/status.ts +92 -0
  121. package/src/presence/store.ts +94 -0
  122. package/src/presence/tier.ts +36 -0
  123. package/src/presence/watch-wait.ts +81 -0
  124. package/src/screens/AddressCountryControl.tsx +115 -0
  125. package/src/screens/BusinessDetailsFields.tsx +3 -0
  126. package/src/screens/BusinessDetailsStep.tsx +2 -0
  127. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  128. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  129. package/src/screens/CompanyInfoFields.tsx +6 -1
  130. package/src/screens/ConsentStep.tsx +6 -17
  131. package/src/screens/ContactActions.tsx +54 -0
  132. package/src/screens/ContactDestinationField.tsx +4 -0
  133. package/src/screens/ContactEntryPanel.tsx +75 -0
  134. package/src/screens/ContactVerificationStep.tsx +26 -56
  135. package/src/screens/CountrySelectStep.tsx +13 -147
  136. package/src/screens/DocumentCaptureStep.tsx +177 -93
  137. package/src/screens/LivenessAvatar.tsx +33 -16
  138. package/src/screens/LivenessStep.tsx +188 -49
  139. package/src/screens/MrzScanView.tsx +33 -0
  140. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  141. package/src/screens/ProofOfAddressParts.tsx +151 -0
  142. package/src/screens/ProofOfAddressStep.tsx +58 -99
  143. package/src/screens/SubmittedBadge.tsx +25 -0
  144. package/src/screens/SubmittedError.tsx +64 -0
  145. package/src/screens/SubmittedResult.tsx +117 -0
  146. package/src/screens/SubmittedStep.tsx +71 -164
  147. package/src/screens/SubmittedSuccess.tsx +127 -0
  148. package/src/screens/SubmittedWaiting.tsx +45 -0
  149. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  150. package/src/screens/address/AddressIntroGate.tsx +148 -0
  151. package/src/screens/address/AddressMapStub.tsx +50 -0
  152. package/src/screens/address/AddressPinStep.tsx +178 -0
  153. package/src/screens/address/AddressReviewStep.tsx +162 -0
  154. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  155. package/src/screens/address/AddressSearchField.tsx +115 -0
  156. package/src/screens/address/AddressSearchStep.tsx +94 -0
  157. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  158. package/src/screens/address/DetailsSheet.tsx +175 -0
  159. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  160. package/src/screens/address/EntranceDropzone.tsx +178 -0
  161. package/src/screens/address/EntranceFraming.tsx +66 -0
  162. package/src/screens/address/EntrancePills.tsx +60 -0
  163. package/src/screens/address/FramedStreetView.tsx +155 -0
  164. package/src/screens/address/IntroDisclosures.tsx +210 -0
  165. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  166. package/src/screens/address/PinSummaryRow.tsx +105 -0
  167. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  168. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  169. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  170. package/src/screens/address/SearchResults.tsx +105 -0
  171. package/src/screens/address/SearchScreen.tsx +176 -0
  172. package/src/screens/address/SkipForNow.tsx +43 -0
  173. package/src/screens/address/StreetViewChrome.tsx +81 -0
  174. package/src/screens/address/detail-values.ts +22 -0
  175. package/src/screens/address/fix-source.ts +27 -0
  176. package/src/screens/address/index.ts +8 -0
  177. package/src/screens/address/meta.ts +44 -0
  178. package/src/screens/address/use-address-flow.ts +200 -0
  179. package/src/screens/address/use-label-pin.ts +80 -0
  180. package/src/screens/address/use-pin-actions.ts +192 -0
  181. package/src/screens/consent/model.ts +79 -10
  182. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  183. package/src/screens/document/UploadPhase.tsx +177 -0
  184. package/src/screens/document/useDocumentCamera.ts +109 -0
  185. package/src/screens/liveness/CaptureRing.tsx +91 -0
  186. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  187. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  188. package/src/screens/liveness/LivenessOutcome.tsx +8 -4
  189. package/src/screens/liveness/SelfiePreview.tsx +31 -7
  190. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  191. package/src/screens/liveness/index.ts +4 -0
  192. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  193. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  194. package/src/screens/useAddressPhotoAttach.ts +77 -0
  195. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  196. package/src/screens/usePoaAttach.ts +12 -15
  197. package/src/services/api-types.ts +119 -1
  198. package/src/services/api-verify-types.ts +37 -0
  199. package/src/services/api.ts +149 -3
  200. package/src/services/deviceMetadata.ts +1 -1
  201. package/src/services/location.ts +234 -0
  202. package/src/services/mediaCompress.ts +23 -5
  203. package/src/services/uploadErrors.ts +30 -0
  204. package/src/services/workflowGate.ts +7 -0
  205. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  206. package/src/store/address.ts +97 -0
  207. package/src/store/derive.ts +29 -4
  208. package/src/store/kycStore.ts +120 -5
  209. package/src/store/serverConfig.ts +18 -0
  210. package/src/store/session.ts +23 -4
  211. package/src/store/state.ts +134 -0
  212. package/src/store/submit.ts +18 -1
  213. package/src/types/config.ts +63 -1
  214. package/src/types/verification.ts +12 -0
  215. package/src/types/workflow.ts +97 -1
  216. package/src/assets/liveness/Blink.gif +0 -0
  217. package/src/assets/liveness/Nod.gif +0 -0
  218. package/src/assets/liveness/Smile.gif +0 -0
  219. package/src/assets/liveness/Turn.gif +0 -0
@@ -0,0 +1,154 @@
1
+ package com.margelo.nitro.myazakyc
2
+
3
+ import co.myazahq.kyc.rn.BuildConfig
4
+ import com.google.android.gms.common.api.OptionalModuleApi
5
+ import com.google.android.gms.common.moduleinstall.InstallStatusListener
6
+ import com.google.android.gms.common.moduleinstall.ModuleInstall
7
+ import com.google.android.gms.common.moduleinstall.ModuleInstallClient
8
+ import com.google.android.gms.common.moduleinstall.ModuleInstallRequest
9
+ import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate
10
+ import com.margelo.nitro.NitroModules
11
+ import java.util.concurrent.CountDownLatch
12
+ import java.util.concurrent.TimeUnit
13
+
14
+ /**
15
+ * The readiness gate both ML Kit detectors share.
16
+ *
17
+ * The default build fetches ML Kit's models through Play Services instead of
18
+ * bundling them (android/build.gradle), which is what keeps ~18.5 MB per device
19
+ * out of the APK. The cost is a window where the detector cannot run: first
20
+ * launch before the download completes, or a device with no GMS at all.
21
+ *
22
+ * Neither detector can report that through its own result. A face detector
23
+ * answers in `FaceResult`, where a missing model and an empty frame are both
24
+ * `faceCount: 0`; a text recogniser answers in `TextResult`, where both are an
25
+ * empty `lines` array. So a user on a fresh install would watch "position your
26
+ * face" forever, or aim at a passport that never auto-captures, with nothing to
27
+ * explain it. Hence a separate, explicit contract, checked before the camera
28
+ * opens rather than inferred per frame.
29
+ *
30
+ * Two properties make it actually settle, and both were missing once:
31
+ * - The install is URGENT (`installModules`). `deferredInstall` leaves the
32
+ * timing to Play Services, which may wait for an idle, charging phone; right
33
+ * for a model somebody might want next week, wrong for one a person is
34
+ * waiting on now.
35
+ * - The answer is REFRESHED. `isModelReady` is a synchronous Nitro call and
36
+ * `areModulesAvailable` is asynchronous, so it returns the cached flag, but
37
+ * while the flag is false each call also starts a background re-check. The
38
+ * cache used to be set once at flow start and never again, so a download that
39
+ * finished a few seconds later was never noticed and the step gave up.
40
+ */
41
+ class MlKitModelReadiness(private val api: OptionalModuleApi) {
42
+ // A bundled build has the model in the APK, so there is nothing to wait for
43
+ // and nothing to ask. Asking anyway would be worse than useless: Play
44
+ // Services answers about its OWN optional modules, so it would report "not
45
+ // available" and strand a build that works perfectly offline — which is the
46
+ // exact scenario the bundled flag exists to serve.
47
+ @Volatile private var ready = BuildConfig.BUNDLED_ML_KIT
48
+ @Volatile private var checking = false
49
+ @Volatile private var installing = false
50
+
51
+ fun isReady(): Boolean {
52
+ if (!ready) refresh()
53
+ return ready
54
+ }
55
+
56
+ /**
57
+ * Ask Play Services for the model, returning true once it is usable, and
58
+ * request an urgent install when it is not.
59
+ *
60
+ * Safe to call repeatedly; returns immediately when already ready. Blocking
61
+ * — callers run it inside a Nitro `Promise.async`.
62
+ */
63
+ fun prepare(): Boolean {
64
+ if (ready) return true
65
+
66
+ val ctx = NitroModules.applicationContext
67
+ ?: return false // No context — cannot ask Play Services anything.
68
+
69
+ val latch = CountDownLatch(1)
70
+ try {
71
+ val client = ModuleInstall.getClient(ctx)
72
+ client.areModulesAvailable(api)
73
+ .addOnSuccessListener { response ->
74
+ if (response.areModulesAvailable()) ready = true else requestInstall(client)
75
+ latch.countDown()
76
+ }
77
+ .addOnFailureListener {
78
+ // Thrown on devices without Google Play Services at all (Huawei, bare
79
+ // AOSP). Not an error to retry — it will never succeed on this device.
80
+ // Such orgs should build with `myazaKycBundledMlKit = true`.
81
+ latch.countDown()
82
+ }
83
+ latch.await(MODEL_CHECK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
84
+ } catch (_: Throwable) {
85
+ // Leave `ready` as it was; the shared gate's wait decides when to stop.
86
+ }
87
+ return ready
88
+ }
89
+
90
+ /** Re-asks Play Services in the background; at most one check at a time. */
91
+ private fun refresh() {
92
+ if (checking) return
93
+ val ctx = NitroModules.applicationContext ?: return
94
+ checking = true
95
+ try {
96
+ ModuleInstall.getClient(ctx).areModulesAvailable(api)
97
+ .addOnSuccessListener { response -> if (response.areModulesAvailable()) ready = true }
98
+ .addOnCompleteListener { checking = false }
99
+ } catch (_: Throwable) {
100
+ checking = false
101
+ }
102
+ }
103
+
104
+ private fun requestInstall(client: ModuleInstallClient) {
105
+ if (installing) return
106
+ installing = true
107
+
108
+ val listener = object : InstallStatusListener {
109
+ override fun onInstallStatusUpdated(update: ModuleInstallStatusUpdate) {
110
+ when (update.installState) {
111
+ ModuleInstallStatusUpdate.InstallState.STATE_COMPLETED -> {
112
+ ready = true
113
+ installing = false
114
+ client.unregisterListener(this)
115
+ }
116
+ ModuleInstallStatusUpdate.InstallState.STATE_FAILED,
117
+ ModuleInstallStatusUpdate.InstallState.STATE_CANCELED -> {
118
+ // Cleared so the next prepare() can ask again.
119
+ installing = false
120
+ client.unregisterListener(this)
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ // `installModules` takes a ModuleInstallRequest; `deferredInstall` takes the
127
+ // OptionalModuleApi itself. Handing a request to the wrong one is how this
128
+ // file once stopped compiling.
129
+ val request = ModuleInstallRequest.Builder().addApi(api).setListener(listener).build()
130
+ client.installModules(request)
131
+ .addOnSuccessListener { response ->
132
+ // The listener never fires for a module that was already there.
133
+ if (response.areModulesAlreadyInstalled()) {
134
+ ready = true
135
+ installing = false
136
+ client.unregisterListener(listener)
137
+ }
138
+ }
139
+ .addOnFailureListener {
140
+ installing = false
141
+ client.unregisterListener(listener)
142
+ }
143
+ }
144
+
145
+ private companion object {
146
+ /**
147
+ * Bound on the availability query. It is a local Play Services call, not the
148
+ * model download — the download runs in the background afterwards. Generous
149
+ * enough for a cold Play Services process, short enough that a wedged one
150
+ * cannot stall flow start.
151
+ */
152
+ const val MODEL_CHECK_TIMEOUT_MS = 3_000L
153
+ }
154
+ }
package/app.plugin.js CHANGED
@@ -27,8 +27,12 @@ const pkg = require('./package.json');
27
27
  // unconditionally would break code-signing for every consumer who does not read
28
28
  // chips and has not enabled it.
29
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.
30
+ // Camera preview/capture itself comes from react-native-vision-camera, but v5
31
+ // ships NO config plugin (v4 did), so it must NOT be listed in the host app's
32
+ // `plugins`. Expo then loads the package's main entry as a plugin and prebuild
33
+ // dies on `Cannot find module '.../lib/VisionCamera'`. That is why the camera
34
+ // permission and usage string are declared HERE. This plugin is intentionally
35
+ // minimal and idempotent.
32
36
  // ---------------------------------------------------------------------------
33
37
 
34
38
  const CAMERA_USAGE =
@@ -37,6 +41,9 @@ const CAMERA_USAGE =
37
41
  const NFC_USAGE =
38
42
  'We read the secure chip in your passport or ID card to confirm the document is genuine.';
39
43
 
44
+ const LOCATION_USAGE =
45
+ 'We use your location to confirm you are at the address you pin during verification.';
46
+
40
47
  /**
41
48
  * The eMRTD application identifier.
42
49
  *
@@ -62,6 +69,23 @@ function withIosCameraUsage(config, customMessage) {
62
69
  });
63
70
  }
64
71
 
72
+ /**
73
+ * iOS location usage string, for the address-collection step's one-shot fix
74
+ * ("Use my current location" + the attest-presence claim). Written by DEFAULT
75
+ * because its ABSENCE is a crash: iOS kills an app that requests location
76
+ * without a usage string, and whether the step runs is decided by the org's
77
+ * WORKFLOW, not by anything the host app author can see at build time. An
78
+ * unused usage string costs nothing. `location: false` opts out.
79
+ */
80
+ function withIosLocationUsage(config, customMessage) {
81
+ return withInfoPlist(config, (cfg) => {
82
+ if (!cfg.modResults.NSLocationWhenInUseUsageDescription) {
83
+ cfg.modResults.NSLocationWhenInUseUsageDescription = customMessage || LOCATION_USAGE;
84
+ }
85
+ return cfg;
86
+ });
87
+ }
88
+
65
89
  const AID_KEY = 'com.apple.developer.nfc.readersession.iso7816.select-identifiers';
66
90
  const FORMATS_KEY = 'com.apple.developer.nfc.readersession.formats';
67
91
 
@@ -112,25 +136,135 @@ function withIosNfc(config, customMessage) {
112
136
  });
113
137
  }
114
138
 
115
- function withAndroidPermissions(config, { nfc }) {
139
+ /**
140
+ * The Android permission list for a given set of opt-ins. Pure and exported
141
+ * so the tests can pin what `location: 'always'` declares — a permission
142
+ * that changes review posture must never arrive by accident.
143
+ */
144
+ function androidPermissionsFor({ nfc, location, backgroundLocation }) {
145
+ return [
146
+ 'android.permission.CAMERA',
147
+ 'android.permission.INTERNET',
148
+ ...(nfc ? ['android.permission.NFC'] : []),
149
+ // Foreground location, for the address-collection step's one-shot fix.
150
+ ...(location
151
+ ? ['android.permission.ACCESS_COARSE_LOCATION', 'android.permission.ACCESS_FINE_LOCATION']
152
+ : []),
153
+ // The always-on presence tier (OkHi model, opt-in): OS geofencing needs
154
+ // the background grant, and Google Play reviews any app that declares
155
+ // it — which is why this is `location: 'always'` only, never a default.
156
+ // The foreground-service pair rides the same opt-in: FOREGROUND_SERVICE
157
+ // is a normal permission, and FOREGROUND_SERVICE_LOCATION (API 34+) is
158
+ // what lets expo-location's service declare the `location` type; Play
159
+ // reviews it under the same location declaration.
160
+ ...(backgroundLocation
161
+ ? [
162
+ 'android.permission.ACCESS_BACKGROUND_LOCATION',
163
+ 'android.permission.FOREGROUND_SERVICE',
164
+ 'android.permission.FOREGROUND_SERVICE_LOCATION',
165
+ ]
166
+ : []),
167
+ ];
168
+ }
169
+
170
+ function withAndroidPermissions(config, opts) {
171
+ return withAndroidManifest(config, (cfg) => {
172
+ AndroidConfig.Permissions.ensurePermissions(cfg.modResults, androidPermissionsFor(opts));
173
+ return cfg;
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Declares `android.hardware.nfc` as NOT required, on the host's manifest.
179
+ *
180
+ * This is not decoration, and it must never be separated from the NFC
181
+ * permission. Android INFERS `<uses-feature android:name="android.hardware.nfc"
182
+ * android:required="true">` from `android.permission.NFC` whenever the feature
183
+ * is not declared explicitly — and Play then hides the app from every device
184
+ * without an NFC radio. For a KYC SDK that is most of the catalogue in the
185
+ * markets we serve.
186
+ *
187
+ * So: the permission (androidPermissionsFor) and this declaration are written
188
+ * under the same `nfc: true` opt-in in withMyazaKyc, and nothing else writes
189
+ * either. plugin.test.ts pins that pairing with a source scan, because two
190
+ * separate functions are otherwise free to drift apart.
191
+ *
192
+ * Pure and exported so the pairing can be pinned by a test. The failure it
193
+ * guards has no runtime symptom in development — the app builds, installs and
194
+ * runs perfectly on the NFC-capable phone the developer is holding, and is
195
+ * simply absent from the store listing for everyone else.
196
+ *
197
+ * @param {Record<string, any>} manifest the `modResults.manifest` object
198
+ */
199
+ function ensureNfcFeatureOptional(manifest) {
200
+ const list = Array.isArray(manifest['uses-feature']) ? manifest['uses-feature'] : [];
201
+ const existing = list.find((f) => f && f.$ && f.$['android:name'] === 'android.hardware.nfc');
202
+ if (existing) {
203
+ // An explicit `required="true"` from the host or another library would
204
+ // reintroduce exactly the store-visibility problem above, so it is
205
+ // overwritten rather than respected.
206
+ existing.$['android:required'] = 'false';
207
+ } else {
208
+ list.push({
209
+ $: { 'android:name': 'android.hardware.nfc', 'android:required': 'false' },
210
+ });
211
+ }
212
+ manifest['uses-feature'] = list;
213
+ return manifest;
214
+ }
215
+
216
+ function withAndroidNfcFeature(config) {
116
217
  return withAndroidManifest(config, (cfg) => {
117
- AndroidConfig.Permissions.ensurePermissions(cfg.modResults, [
118
- 'android.permission.CAMERA',
119
- 'android.permission.INTERNET',
120
- ...(nfc ? ['android.permission.NFC'] : []),
121
- ]);
218
+ ensureNfcFeatureOptional(cfg.modResults.manifest);
219
+ return cfg;
220
+ });
221
+ }
222
+
223
+ /** iOS side of the always-on tier: the Always usage string plus the location
224
+ * background mode, without which a geofence wake is refused. */
225
+ function withIosBackgroundLocation(config, customMessage) {
226
+ return withInfoPlist(config, (cfg) => {
227
+ if (!cfg.modResults.NSLocationAlwaysAndWhenInUseUsageDescription) {
228
+ cfg.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
229
+ customMessage || LOCATION_USAGE;
230
+ }
231
+ const modes = Array.isArray(cfg.modResults.UIBackgroundModes)
232
+ ? cfg.modResults.UIBackgroundModes
233
+ : [];
234
+ if (!modes.includes('location')) modes.push('location');
235
+ cfg.modResults.UIBackgroundModes = modes;
122
236
  return cfg;
123
237
  });
124
238
  }
125
239
 
126
240
  /**
127
241
  * @param {object} config
128
- * @param {{ cameraPermission?: string, nfc?: boolean, nfcPermission?: string }} [props]
242
+ * @param {{ cameraPermission?: string, nfc?: boolean, nfcPermission?: string,
243
+ * location?: boolean | 'always', locationPermission?: string }} [props]
129
244
  */
130
245
  function withMyazaKyc(config, props = {}) {
246
+ const location = props.location !== false;
247
+ // `location: 'always'` opts into the background presence tier. Explicit on
248
+ // purpose: it changes the app's Play/App Store review posture, so it must
249
+ // never arrive as a side effect of a default.
250
+ const backgroundLocation = props.location === 'always';
131
251
  config = withIosCameraUsage(config, props.cameraPermission);
132
- config = withAndroidPermissions(config, { nfc: props.nfc === true });
252
+ config = withAndroidPermissions(config, {
253
+ nfc: props.nfc === true,
254
+ location,
255
+ backgroundLocation,
256
+ });
257
+ if (location) {
258
+ config = withIosLocationUsage(config, props.locationPermission);
259
+ }
260
+ if (backgroundLocation) {
261
+ config = withIosBackgroundLocation(config, props.locationPermission);
262
+ }
133
263
  if (props.nfc === true) {
264
+ // Android: the permission comes from androidPermissionsFor above; this adds
265
+ // the uses-feature that must accompany it. Never write one without the
266
+ // other — see ensureNfcFeatureOptional.
267
+ config = withAndroidNfcFeature(config);
134
268
  config = withIosNfc(config, props.nfcPermission);
135
269
  }
136
270
  return config;
@@ -141,4 +275,6 @@ module.exports = createRunOncePlugin(withMyazaKyc, pkg.name, pkg.version);
141
275
  // consumers' app.json entries are unaffected.
142
276
  module.exports.applyNfcInfoPlist = applyNfcInfoPlist;
143
277
  module.exports.applyNfcEntitlements = applyNfcEntitlements;
278
+ module.exports.androidPermissionsFor = androidPermissionsFor;
279
+ module.exports.ensureNfcFeatureOptional = ensureNfcFeatureOptional;
144
280
  module.exports.EMRTD_AID = EMRTD_AID;
@@ -303,8 +303,13 @@ final class HybridMyazaFaceDetector: HybridMyazaFaceDetectorSpec {
303
303
  return min(max(v, 0.0), 1.0)
304
304
  }
305
305
 
306
- /// Smile from outer-lip width relative to the face. Input band (0.40→0.50):
307
- /// a neutral mouth sits near 0, a clear smile lands well above the threshold.
306
+ /// Smile from outer-lip width relative to the face box. Input band
307
+ /// (0.36→0.46): the shared detector passes above 0.5, so the pass line sits
308
+ /// at a lip width of 0.41 of the box. It was 0.40→0.50 (pass at 0.45), which
309
+ /// on device needed a full grin — a modest, natural smile never crossed it.
310
+ /// ML Kit on Android returns a classifier probability instead, so this band
311
+ /// is the iOS half of one shared threshold: tune it here, not in
312
+ /// detectSmile, or Android loosens with it.
308
313
  private static func smileProbability(_ landmarks: VNFaceLandmarks2D?) -> Double {
309
314
  guard let pts = landmarks?.outerLips?.normalizedPoints, pts.count >= 4 else {
310
315
  return 0.0
@@ -315,7 +320,7 @@ final class HybridMyazaFaceDetector: HybridMyazaFaceDetectorSpec {
315
320
  minX = min(minX, Double(p.x)); maxX = max(maxX, Double(p.x))
316
321
  }
317
322
  let width = maxX - minX
318
- let score = (width - 0.40) / (0.50 - 0.40)
323
+ let score = (width - 0.36) / (0.46 - 0.36)
319
324
  return min(max(score, 0.0), 1.0)
320
325
  }
321
326
 
@@ -27,6 +27,14 @@ import NitroModules
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
29
  final class HybridMyazaTextRecognizer: HybridMyazaTextRecognizerSpec {
30
+ // Apple Vision ships with the OS — there is no model to fetch, no Play
31
+ // Services equivalent, and no window in which recognition is unavailable.
32
+ // These exist only because Android fetches ML Kit's models on demand; iOS
33
+ // answers "ready" unconditionally so the shared TS flow needs no platform
34
+ // branch.
35
+ func isModelReady() throws -> Bool { true }
36
+
37
+ func prepareModel() throws -> Promise<Bool> { Promise.resolved(withResult: true) }
30
38
 
31
39
  func recognizeText(frame: any HybridFrameSpec, bottomFraction: Double) throws -> TextResult {
32
40
  guard
@@ -76,5 +76,26 @@ namespace margelo::nitro::myazakyc {
76
76
  return __promise;
77
77
  }();
78
78
  }
79
+ bool JHybridMyazaTextRecognizerSpec::isModelReady() {
80
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("isModelReady");
81
+ auto __result = method(_javaPart);
82
+ return static_cast<bool>(__result);
83
+ }
84
+ std::shared_ptr<Promise<bool>> JHybridMyazaTextRecognizerSpec::prepareModel() {
85
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("prepareModel");
86
+ auto __result = method(_javaPart);
87
+ return [&]() {
88
+ auto __promise = Promise<bool>::create();
89
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
90
+ auto __result = jni::static_ref_cast<jni::JBoolean>(__boxedResult);
91
+ __promise->resolve(static_cast<bool>(__result->value()));
92
+ });
93
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
94
+ jni::JniException __jniError(__throwable);
95
+ __promise->reject(std::make_exception_ptr(__jniError));
96
+ });
97
+ return __promise;
98
+ }();
99
+ }
79
100
 
80
101
  } // namespace margelo::nitro::myazakyc
@@ -56,6 +56,8 @@ namespace margelo::nitro::myazakyc {
56
56
  // Methods
57
57
  TextResult recognizeText(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame, double bottomFraction) override;
58
58
  std::shared_ptr<Promise<TextResult>> recognizeTextInImage(const std::string& uri, double bottomFraction) override;
59
+ bool isModelReady() override;
60
+ std::shared_ptr<Promise<bool>> prepareModel() override;
59
61
 
60
62
  private:
61
63
  jni::global_ref<JHybridMyazaTextRecognizerSpec::JavaPart> _javaPart;
@@ -37,6 +37,14 @@ abstract class HybridMyazaTextRecognizerSpec: HybridObject() {
37
37
  @DoNotStrip
38
38
  @Keep
39
39
  abstract fun recognizeTextInImage(uri: String, bottomFraction: Double): Promise<TextResult>
40
+
41
+ @DoNotStrip
42
+ @Keep
43
+ abstract fun isModelReady(): Boolean
44
+
45
+ @DoNotStrip
46
+ @Keep
47
+ abstract fun prepareModel(): Promise<Boolean>
40
48
 
41
49
  // Default implementation of `HybridObject.toString()`
42
50
  override fun toString(): String {
@@ -90,6 +90,22 @@ namespace margelo::nitro::myazakyc {
90
90
  auto __value = std::move(__result.value());
91
91
  return __value;
92
92
  }
93
+ inline bool isModelReady() override {
94
+ auto __result = _swiftPart.isModelReady();
95
+ if (__result.hasError()) [[unlikely]] {
96
+ std::rethrow_exception(__result.error());
97
+ }
98
+ auto __value = std::move(__result.value());
99
+ return __value;
100
+ }
101
+ inline std::shared_ptr<Promise<bool>> prepareModel() override {
102
+ auto __result = _swiftPart.prepareModel();
103
+ if (__result.hasError()) [[unlikely]] {
104
+ std::rethrow_exception(__result.error());
105
+ }
106
+ auto __value = std::move(__result.value());
107
+ return __value;
108
+ }
93
109
 
94
110
  private:
95
111
  KycSdkReactNative::HybridMyazaTextRecognizerSpec_cxx _swiftPart;
@@ -16,6 +16,8 @@ public protocol HybridMyazaTextRecognizerSpec_protocol: HybridObject {
16
16
  // Methods
17
17
  func recognizeText(frame: (any HybridFrameSpec), bottomFraction: Double) throws -> TextResult
18
18
  func recognizeTextInImage(uri: String, bottomFraction: Double) throws -> Promise<TextResult>
19
+ func isModelReady() throws -> Bool
20
+ func prepareModel() throws -> Promise<Bool>
19
21
  }
20
22
 
21
23
  public extension HybridMyazaTextRecognizerSpec_protocol {
@@ -159,4 +159,35 @@ open class HybridMyazaTextRecognizerSpec_cxx {
159
159
  return bridge.create_Result_std__shared_ptr_Promise_TextResult___(__exceptionPtr)
160
160
  }
161
161
  }
162
+
163
+ @inline(__always)
164
+ public final func isModelReady() -> bridge.Result_bool_ {
165
+ do {
166
+ let __result = try self.__implementation.isModelReady()
167
+ let __resultCpp = __result
168
+ return bridge.create_Result_bool_(__resultCpp)
169
+ } catch (let __error) {
170
+ let __exceptionPtr = __error.toCpp()
171
+ return bridge.create_Result_bool_(__exceptionPtr)
172
+ }
173
+ }
174
+
175
+ @inline(__always)
176
+ public final func prepareModel() -> bridge.Result_std__shared_ptr_Promise_bool___ {
177
+ do {
178
+ let __result = try self.__implementation.prepareModel()
179
+ let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in
180
+ let __promise = bridge.create_std__shared_ptr_Promise_bool__()
181
+ let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise)
182
+ __result
183
+ .then({ __result in __promiseHolder.resolve(__result) })
184
+ .catch({ __error in __promiseHolder.reject(__error.toCpp()) })
185
+ return __promise
186
+ }()
187
+ return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp)
188
+ } catch (let __error) {
189
+ let __exceptionPtr = __error.toCpp()
190
+ return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr)
191
+ }
192
+ }
162
193
  }
@@ -16,6 +16,8 @@ namespace margelo::nitro::myazakyc {
16
16
  registerHybrids(this, [](Prototype& prototype) {
17
17
  prototype.registerHybridMethod("recognizeText", &HybridMyazaTextRecognizerSpec::recognizeText);
18
18
  prototype.registerHybridMethod("recognizeTextInImage", &HybridMyazaTextRecognizerSpec::recognizeTextInImage);
19
+ prototype.registerHybridMethod("isModelReady", &HybridMyazaTextRecognizerSpec::isModelReady);
20
+ prototype.registerHybridMethod("prepareModel", &HybridMyazaTextRecognizerSpec::prepareModel);
19
21
  });
20
22
  }
21
23
 
@@ -57,6 +57,8 @@ namespace margelo::nitro::myazakyc {
57
57
  // Methods
58
58
  virtual TextResult recognizeText(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame, double bottomFraction) = 0;
59
59
  virtual std::shared_ptr<Promise<TextResult>> recognizeTextInImage(const std::string& uri, double bottomFraction) = 0;
60
+ virtual bool isModelReady() = 0;
61
+ virtual std::shared_ptr<Promise<bool>> prepareModel() = 0;
60
62
 
61
63
  protected:
62
64
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myazahq/kyc-sdk-react-native",
3
- "version": "2.5.0",
3
+ "version": "3.0.0",
4
4
  "description": "Myaza KYC SDK for React Native (Expo) — ID verification, liveness detection, and document capture",
5
5
  "author": "Flitstack Technologies Inc.",
6
6
  "repository": {
@@ -49,6 +49,12 @@
49
49
  "license": "MIT",
50
50
  "peerDependencies": {
51
51
  "expo": ">=56",
52
+ "expo-application": ">=56",
53
+ "expo-device": ">=56",
54
+ "expo-document-picker": ">=56",
55
+ "expo-localization": ">=56",
56
+ "expo-task-manager": ">=14",
57
+ "react-native-webview": ">=13",
52
58
  "react": ">=19",
53
59
  "react-native": ">=0.83",
54
60
  "react-native-nitro-image": ">=0.15",
@@ -58,27 +64,42 @@
58
64
  "react-native-vision-camera-worklets": ">=5",
59
65
  "react-native-worklets": ">=0.8"
60
66
  },
67
+ "peerDependenciesMeta": {
68
+ "expo-application": {
69
+ "optional": true
70
+ },
71
+ "expo-device": {
72
+ "optional": true
73
+ },
74
+ "expo-document-picker": {
75
+ "optional": true
76
+ },
77
+ "expo-localization": {
78
+ "optional": true
79
+ },
80
+ "expo-task-manager": {
81
+ "optional": true
82
+ },
83
+ "react-native-webview": {
84
+ "optional": true
85
+ }
86
+ },
61
87
  "dependencies": {
62
- "@expo-google-fonts/karla": "^0.4.2",
63
- "@expo-google-fonts/space-grotesk": "^0.4.1",
64
- "@expo/vector-icons": "^14.0.4",
65
88
  "country-flag-icons": "^1.6.17",
66
- "expo-application": "~56.0.3",
67
89
  "libphonenumber-js": "^1.13.8",
68
90
  "expo-crypto": "~56.0.4",
69
91
  "expo-clipboard": "~56.0.0",
70
- "expo-device": "~56.0.4",
71
- "expo-document-picker": "~56.0.4",
72
92
  "expo-font": "~56.0.5",
73
93
  "expo-glass-effect": "~56.0.4",
74
94
  "expo-image-manipulator": "~56.0.17",
75
95
  "expo-image-picker": "~56.0.16",
76
- "expo-localization": "~56.0.6",
96
+ "expo-location": "~56.0.24",
77
97
  "expo-speech": "~56.0.3",
78
98
  "lucide-react-native": "^1.17.0",
79
99
  "react-native-compressor": "^1.19.0",
80
100
  "react-native-vision-camera": "^5.0.11",
81
- "zustand": "^5.0.2"
101
+ "zustand": "^5.0.2",
102
+ "expo-file-system": "~56.0.0"
82
103
  },
83
104
  "devDependencies": {
84
105
  "@types/jest": "^29.5.12",
package/src/MyazaKYC.tsx CHANGED
@@ -15,8 +15,8 @@ import type { KYCStep, MyazaKYCConfig, SupportedCountry } from './types/config';
15
15
  import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
16
16
  import { WorkflowGate } from './components/WorkflowGate';
17
17
  import { useWorkflowMount } from './components/useWorkflowMount';
18
+ import { usePrimeModels } from './lib/prime-models';
18
19
  import { safeReportError } from './services/errors';
19
- import { primeFaceModel } from './liveness/visionCameraFaceDetector';
20
20
  import { KycFlow, type BackResult } from './components/KycFlow';
21
21
  import { MyazaButton } from './components/MyazaButton';
22
22
 
@@ -99,16 +99,7 @@ function MyazaKYCTrigger({
99
99
  const settled = state.status !== 'resolving';
100
100
  const open = wantOpen && settled;
101
101
 
102
- // Start fetching the face model the moment the user opens the flow, so the
103
- // download overlaps consent and ID-type selection rather than stalling in
104
- // front of the camera. Mirrors the web SDK's primeFaceMesh() on mount; the
105
- // liveness step still gates on isFaceModelReady(), so this is best-effort.
106
- const primedRef = useRef(false);
107
- useEffect(() => {
108
- if (!wantOpen || primedRef.current) return;
109
- primedRef.current = true;
110
- primeFaceModel();
111
- }, [wantOpen]);
102
+ usePrimeModels(wantOpen, config.apiKey, config.devUrl);
112
103
 
113
104
  // Reported only once the user has actually tried to start. Prefetching must
114
105
  // not fire a consumer's error handler for a flow they never opened.
@@ -229,6 +220,8 @@ export function useMyazaKYC<C extends SupportedCountry = SupportedCountry>(
229
220
  configRef.current = config;
230
221
  const backRef = useRef<(() => BackResult) | null>(null);
231
222
 
223
+ usePrimeModels(wantOpen, config.apiKey, config.devUrl);
224
+
232
225
  // `refresh` is defined further down (it needs `wrappedConfig`), so it is
233
226
  // reached through a ref rather than reordering the component around it.
234
227
  const refreshRef = useRef<() => void>(() => undefined);