@myazahq/kyc-sdk-react-native 2.4.0 → 2.5.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 (117) hide show
  1. package/android/build.gradle +35 -3
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
  3. package/ios/HybridMyazaFaceDetector.swift +8 -0
  4. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  5. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  6. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  7. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  8. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  9. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  10. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  11. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  12. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  13. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  14. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  15. package/package.json +1 -1
  16. package/src/MyazaKYC.tsx +12 -0
  17. package/src/components/DashedBorder.tsx +55 -0
  18. package/src/components/DialCodePicker.tsx +14 -7
  19. package/src/components/Icon.tsx +9 -0
  20. package/src/components/MultiIdProgress.tsx +107 -0
  21. package/src/components/MyazaInput.tsx +23 -4
  22. package/src/components/MyazaSelect.tsx +2 -1
  23. package/src/components/OwnershipSlider.tsx +102 -0
  24. package/src/components/PhoneNumberInput.tsx +37 -4
  25. package/src/components/StaggerIn.tsx +63 -0
  26. package/src/components/StepView.tsx +26 -0
  27. package/src/components/glass/FloatingSheet.tsx +73 -21
  28. package/src/components/glass/GlassSheet.tsx +14 -2
  29. package/src/components/stepHeaderMeta.tsx +3 -1
  30. package/src/config/business.ts +6 -6
  31. package/src/config/businessDetailsValidity.ts +52 -0
  32. package/src/config/businessPrefill.ts +72 -0
  33. package/src/config/businessSteps.ts +14 -3
  34. package/src/config/keyPeople.ts +111 -7
  35. package/src/config/keyPeopleCorporate.ts +19 -0
  36. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  37. package/src/config/keyPeopleOwners.ts +26 -0
  38. package/src/config/keyPeoplePrefill.ts +138 -0
  39. package/src/config/keyPeopleSectionDefs.ts +65 -0
  40. package/src/config/keyPeopleSections.ts +167 -0
  41. package/src/config/registrationHint.ts +1 -1
  42. package/src/config/stepOrder.ts +24 -4
  43. package/src/config/theme.ts +8 -2
  44. package/src/config/website.ts +36 -0
  45. package/src/config/workflowMerge.ts +5 -0
  46. package/src/emrtd/activeAuth.ts +100 -0
  47. package/src/emrtd/extras.ts +76 -0
  48. package/src/emrtd/files.ts +5 -0
  49. package/src/emrtd/index.ts +1 -0
  50. package/src/emrtd/read.ts +8 -1
  51. package/src/emrtd/session.ts +41 -0
  52. package/src/emrtd/stages.ts +11 -0
  53. package/src/lib/contact-recovery.ts +44 -0
  54. package/src/lib/multi-id.ts +210 -0
  55. package/src/lib/resubmit.ts +130 -0
  56. package/src/lib/screen-corners.ts +50 -0
  57. package/src/lib/step-log.ts +24 -3
  58. package/src/lib/use-multi-id-plan.ts +29 -0
  59. package/src/liveness/useModelReady.ts +65 -0
  60. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  61. package/src/screens/ApplicantRoleStep.tsx +9 -5
  62. package/src/screens/BusinessCheckPanel.tsx +89 -0
  63. package/src/screens/BusinessDetailsFields.tsx +109 -0
  64. package/src/screens/BusinessDetailsStep.tsx +131 -145
  65. package/src/screens/BusinessDocumentSlot.tsx +2 -3
  66. package/src/screens/BusinessDocumentsStep.tsx +1 -1
  67. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  68. package/src/screens/BusinessPickedCard.tsx +86 -0
  69. package/src/screens/BusinessPickedSection.tsx +51 -0
  70. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  71. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  72. package/src/screens/BusinessSearch.tsx +199 -0
  73. package/src/screens/BusinessSearchResults.tsx +115 -0
  74. package/src/screens/CompanyInfoFields.tsx +86 -51
  75. package/src/screens/ConsentStep.tsx +31 -0
  76. package/src/screens/ContactDestinationField.tsx +1 -0
  77. package/src/screens/ContactVerificationStep.tsx +34 -3
  78. package/src/screens/IdTypeStep.tsx +12 -5
  79. package/src/screens/KeepLinksSheet.tsx +88 -0
  80. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  81. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  82. package/src/screens/KeyPeoplePending.tsx +154 -0
  83. package/src/screens/KeyPeopleSection.tsx +162 -0
  84. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  85. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  86. package/src/screens/KeyPersonCard.tsx +36 -4
  87. package/src/screens/KeyPersonForm.tsx +195 -36
  88. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  89. package/src/screens/KeyPersonOwners.tsx +109 -0
  90. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  91. package/src/screens/KeyPersonSheet.tsx +92 -108
  92. package/src/screens/LivenessStep.tsx +18 -0
  93. package/src/screens/NfcStep.tsx +14 -0
  94. package/src/screens/ProofOfAddressStep.tsx +6 -3
  95. package/src/screens/SubmittedStep.tsx +66 -10
  96. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  97. package/src/screens/useAwaitingPeople.ts +90 -0
  98. package/src/services/api-types.ts +158 -2
  99. package/src/services/api-verify-types.ts +53 -1
  100. package/src/services/api.ts +104 -0
  101. package/src/services/deviceMetadata.ts +1 -1
  102. package/src/services/errors.ts +10 -2
  103. package/src/services/workflowGate.ts +1 -0
  104. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  105. package/src/store/businessCheck.ts +113 -0
  106. package/src/store/derive.ts +15 -0
  107. package/src/store/kycStore.ts +241 -14
  108. package/src/store/serverConfig.ts +2 -0
  109. package/src/store/session.ts +208 -0
  110. package/src/store/state.ts +155 -2
  111. package/src/store/submit.ts +63 -13
  112. package/src/store/submitApplicant.ts +5 -11
  113. package/src/types/business.ts +23 -2
  114. package/src/types/config.ts +22 -1
  115. package/src/types/verification.ts +1 -1
  116. package/src/types/workflow.ts +15 -0
  117. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -109,8 +109,40 @@ dependencies {
109
109
  // CameraX core — for the ImageProxy / ExperimentalGetImage APIs.
110
110
  compileOnly 'androidx.camera:camera-core:1.3.4'
111
111
 
112
- // Google ML Kit face detection — Android only. Same version as the Flutter SDK.
113
- implementation 'com.google.mlkit:face-detection:16.1.6'
114
- // On-device text recognition, for reading the MRZ off a passport or ID card.
112
+ // ── ML Kit: models fetched on demand by default ──────────────────────────
113
+ //
114
+ // Two variants of the same API exist. The `com.google.mlkit:*` artifacts embed
115
+ // the models in the APK; the `com.google.android.gms:play-services-mlkit-*`
116
+ // ones fetch them through Google Play Services on first use.
117
+ //
118
+ // Measured on a real integrator's release APK, the bundled pair cost
119
+ // **18.5 MB per device** (arm64: ocr_pipeline 10.55 MB + face_detector 7.95 MB)
120
+ // plus the .tflite files in assets/, which ship to EVERY device because assets
121
+ // are not split by ABI. Unbundled, that is ~0.4 MB of shims.
122
+ //
123
+ // The trade-off is real and is why the flag exists: the unbundled variants need
124
+ // Google Play Services, so they do not work on Huawei or AOSP builds without
125
+ // GMS. An org shipping to those devices sets `myazaKycBundledMlKit = true` in
126
+ // its root build.gradle ext and gets today's fully-offline behaviour back.
127
+ //
128
+ // Availability is NOT inferred per frame — `isModelReady()` is checked once
129
+ // before the camera opens (see HybridMyazaFaceDetector.kt), because a missing
130
+ // model would otherwise be indistinguishable from "no face in frame".
131
+ if (safeExtGet('myazaKycBundledMlKit', false).toString().toBoolean()) {
132
+ implementation 'com.google.mlkit:face-detection:16.1.6'
133
+ } else {
134
+ implementation 'com.google.android.gms:play-services-mlkit-face-detection:17.1.0'
135
+ }
136
+
137
+ // Text recognition is STILL BUNDLED, deliberately, and it is the larger of the
138
+ // two (10.55 MB vs 7.95 MB on arm64) — so this is the bigger half of the win,
139
+ // deferred rather than abandoned.
140
+ //
141
+ // Unbundling it needs the same isModelReady()/prepareModel() contract the face
142
+ // detector just gained. Without that, a missing OCR model is indistinguishable
143
+ // from "no text in frame": document auto-capture would simply never fire and
144
+ // MRZ scanning would never produce a key for the chip read, with nothing shown
145
+ // to the user. Shipping the dependency swap ahead of the availability handling
146
+ // would trade 10 MB for a silent failure on every fresh install.
115
147
  implementation 'com.google.mlkit:text-recognition:16.0.1'
116
148
  }
@@ -9,6 +9,10 @@ import com.google.mlkit.vision.face.Face
9
9
  import com.google.mlkit.vision.face.FaceDetection
10
10
  import com.google.mlkit.vision.face.FaceDetector
11
11
  import com.google.mlkit.vision.face.FaceDetectorOptions
12
+ import com.google.android.gms.common.moduleinstall.ModuleInstall
13
+ import com.google.android.gms.common.moduleinstall.ModuleInstallRequest
14
+ import com.margelo.nitro.NitroModules
15
+ import com.margelo.nitro.core.Promise
12
16
  import java.util.concurrent.CountDownLatch
13
17
  import java.util.concurrent.TimeUnit
14
18
 
@@ -39,6 +43,67 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
39
43
  .build(),
40
44
  )
41
45
 
46
+ // ── Model availability ───────────────────────────────────────────────────
47
+ //
48
+ // The default build fetches ML Kit's models through Play Services instead of
49
+ // bundling them (android/build.gradle), which is what keeps ~18.5 MB per
50
+ // device out of the APK. The cost is a window where detection cannot run:
51
+ // first launch before the download completes, or a device with no GMS at all.
52
+ //
53
+ // `detectFace` CANNOT report that. Its only channel is FaceResult, where a
54
+ // missing model and an empty frame are both `faceCount: 0` — so a user on a
55
+ // fresh install would watch "position your face" forever with nothing to
56
+ // explain it. Hence a separate, explicit contract, checked before the camera
57
+ // opens rather than inferred per frame.
58
+ //
59
+ // Cached rather than queried live: `areModulesAvailable` is asynchronous and
60
+ // `isModelReady` is a synchronous Nitro call, so blocking the worklet thread
61
+ // to answer it would be worse than the problem. The flag starts false and is
62
+ // set by prepareModel(), which the SDK primes at flow start.
63
+ @Volatile private var modelReady = false
64
+
65
+ override fun isModelReady(): Boolean = modelReady
66
+
67
+ override fun prepareModel(): Promise<Boolean> = Promise.async {
68
+ if (modelReady) return@async true
69
+
70
+ val ctx = NitroModules.applicationContext
71
+ ?: return@async false // No context — cannot ask Play Services anything.
72
+
73
+ val latch = CountDownLatch(1)
74
+ var ok = false
75
+ try {
76
+ val client = ModuleInstall.getClient(ctx)
77
+ // areModulesAvailable() answers "is it already here"; deferredInstall()
78
+ // asks Play Services to fetch it in the background if not. Requesting the
79
+ // install unconditionally is simpler AND correct — it is a no-op when the
80
+ // module is already present, and starting it early is the entire point.
81
+ client.areModulesAvailable(detector)
82
+ .addOnSuccessListener { response ->
83
+ ok = response.areModulesAvailable()
84
+ if (!ok) {
85
+ client.deferredInstall(
86
+ ModuleInstallRequest.newBuilder().addApi(detector).build(),
87
+ )
88
+ }
89
+ latch.countDown()
90
+ }
91
+ .addOnFailureListener {
92
+ // Thrown on devices without Google Play Services at all (Huawei, bare
93
+ // AOSP). Not an error to retry — it will never succeed on this device.
94
+ // Such orgs should build with `myazaKycBundledMlKit = true`.
95
+ ok = false
96
+ latch.countDown()
97
+ }
98
+ latch.await(MODEL_CHECK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
99
+ } catch (_: Throwable) {
100
+ ok = false
101
+ }
102
+
103
+ modelReady = ok
104
+ ok
105
+ }
106
+
42
107
  @ExperimentalGetImage
43
108
  override fun detectFace(frame: HybridFrameSpec): FaceResult {
44
109
  // Same access pattern as iOS (cast to the public NativeFrame): VisionCamera's
@@ -218,4 +283,14 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
218
283
  faceG = -1.0,
219
284
  faceB = -1.0,
220
285
  )
286
+
287
+ private companion object {
288
+ /**
289
+ * Bound on the availability query. It is a local Play Services call, not the
290
+ * model download — the download runs in the background afterwards. Generous
291
+ * enough for a cold Play Services process, short enough that a wedged one
292
+ * cannot stall flow start.
293
+ */
294
+ const val MODEL_CHECK_TIMEOUT_MS = 3_000L
295
+ }
221
296
  }
@@ -28,6 +28,14 @@ import NitroModules
28
28
  // ---------------------------------------------------------------------------
29
29
 
30
30
  final class HybridMyazaFaceDetector: HybridMyazaFaceDetectorSpec {
31
+ // Apple Vision ships with the OS — there is no model to fetch, no Play
32
+ // Services equivalent, and no window in which detection is unavailable. These
33
+ // exist only because Android fetches ML Kit's models on demand; iOS answers
34
+ // "ready" unconditionally so the shared TS flow needs no platform branch.
35
+ func isModelReady() throws -> Bool { true }
36
+
37
+ func prepareModel() throws -> Promise<Bool> { Promise.resolved(withResult: true) }
38
+
31
39
  func detectFace(frame: any HybridFrameSpec) throws -> FaceResult {
32
40
  guard
33
41
  let nativeFrame = frame as? NativeFrame,
@@ -14,6 +14,8 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
14
14
 
15
15
  #include "FaceResult.hpp"
16
16
  #include "JFaceResult.hpp"
17
+ #include <NitroModules/Promise.hpp>
18
+ #include <NitroModules/JPromise.hpp>
17
19
  #include <memory>
18
20
  #include <VisionCamera/HybridFrameSpec.hpp>
19
21
  #include <VisionCamera/JHybridFrameSpec.hpp>
@@ -56,5 +58,26 @@ namespace margelo::nitro::myazakyc {
56
58
  auto __result = method(_javaPart, std::dynamic_pointer_cast<margelo::nitro::camera::JHybridFrameSpec>(frame)->getJavaPart());
57
59
  return __result->toCpp();
58
60
  }
61
+ bool JHybridMyazaFaceDetectorSpec::isModelReady() {
62
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("isModelReady");
63
+ auto __result = method(_javaPart);
64
+ return static_cast<bool>(__result);
65
+ }
66
+ std::shared_ptr<Promise<bool>> JHybridMyazaFaceDetectorSpec::prepareModel() {
67
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("prepareModel");
68
+ auto __result = method(_javaPart);
69
+ return [&]() {
70
+ auto __promise = Promise<bool>::create();
71
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
72
+ auto __result = jni::static_ref_cast<jni::JBoolean>(__boxedResult);
73
+ __promise->resolve(static_cast<bool>(__result->value()));
74
+ });
75
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
76
+ jni::JniException __jniError(__throwable);
77
+ __promise->reject(std::make_exception_ptr(__jniError));
78
+ });
79
+ return __promise;
80
+ }();
81
+ }
59
82
 
60
83
  } // namespace margelo::nitro::myazakyc
@@ -55,6 +55,8 @@ namespace margelo::nitro::myazakyc {
55
55
  public:
56
56
  // Methods
57
57
  FaceResult detectFace(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override;
58
+ bool isModelReady() override;
59
+ std::shared_ptr<Promise<bool>> prepareModel() override;
58
60
 
59
61
  private:
60
62
  jni::global_ref<JHybridMyazaFaceDetectorSpec::JavaPart> _javaPart;
@@ -11,6 +11,7 @@ import androidx.annotation.Keep
11
11
  import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
13
  import com.margelo.nitro.camera.HybridFrameSpec
14
+ import com.margelo.nitro.core.Promise
14
15
  import com.margelo.nitro.core.HybridObject
15
16
 
16
17
  /**
@@ -32,6 +33,14 @@ abstract class HybridMyazaFaceDetectorSpec: HybridObject() {
32
33
  @DoNotStrip
33
34
  @Keep
34
35
  abstract fun detectFace(frame: com.margelo.nitro.camera.HybridFrameSpec): FaceResult
36
+
37
+ @DoNotStrip
38
+ @Keep
39
+ abstract fun isModelReady(): Boolean
40
+
41
+ @DoNotStrip
42
+ @Keep
43
+ abstract fun prepareModel(): Promise<Boolean>
35
44
 
36
45
  // Default implementation of `HybridObject.toString()`
37
46
  override fun toString(): String {
@@ -84,6 +84,14 @@ namespace margelo::nitro::myazakyc::bridge::swift {
84
84
  return margelo::nitro::camera::bridge::swift::get_std__shared_ptr_HybridFrameSpec_(cppType);
85
85
  }
86
86
 
87
+ // pragma MARK: std::function<void(bool /* result */)>
88
+ Func_void_bool create_Func_void_bool(void* NON_NULL swiftClosureWrapper) noexcept {
89
+ auto swiftClosure = KycSdkReactNative::Func_void_bool::fromUnsafe(swiftClosureWrapper);
90
+ return [swiftClosure = std::move(swiftClosure)](bool result) mutable -> void {
91
+ swiftClosure.call(result);
92
+ };
93
+ }
94
+
87
95
  // pragma MARK: std::shared_ptr<HybridMyazaFaceDetectorSpec>
88
96
  std::shared_ptr<HybridMyazaFaceDetectorSpec> create_std__shared_ptr_HybridMyazaFaceDetectorSpec_(void* NON_NULL swiftUnsafePointer) noexcept {
89
97
  KycSdkReactNative::HybridMyazaFaceDetectorSpec_cxx swiftPart = KycSdkReactNative::HybridMyazaFaceDetectorSpec_cxx::fromUnsafe(swiftUnsafePointer);
@@ -303,6 +303,40 @@ namespace margelo::nitro::myazakyc::bridge::swift {
303
303
  using std__weak_ptr_margelo__nitro__camera__HybridFrameSpec_ = std::weak_ptr<margelo::nitro::camera::HybridFrameSpec>;
304
304
  inline std__weak_ptr_margelo__nitro__camera__HybridFrameSpec_ weakify_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& strong) noexcept { return strong; }
305
305
 
306
+ // pragma MARK: std::shared_ptr<Promise<bool>>
307
+ /**
308
+ * Specialized version of `std::shared_ptr<Promise<bool>>`.
309
+ */
310
+ using std__shared_ptr_Promise_bool__ = std::shared_ptr<Promise<bool>>;
311
+ inline std::shared_ptr<Promise<bool>> create_std__shared_ptr_Promise_bool__() noexcept {
312
+ return Promise<bool>::create();
313
+ }
314
+ inline PromiseHolder<bool> wrap_std__shared_ptr_Promise_bool__(std::shared_ptr<Promise<bool>> promise) noexcept {
315
+ return PromiseHolder<bool>(std::move(promise));
316
+ }
317
+
318
+ // pragma MARK: std::function<void(bool /* result */)>
319
+ /**
320
+ * Specialized version of `std::function<void(bool)>`.
321
+ */
322
+ using Func_void_bool = std::function<void(bool /* result */)>;
323
+ /**
324
+ * Wrapper class for a `std::function<void(bool / * result * /)>`, this can be used from Swift.
325
+ */
326
+ class Func_void_bool_Wrapper final {
327
+ public:
328
+ explicit Func_void_bool_Wrapper(std::function<void(bool /* result */)>&& func): _function(std::make_unique<std::function<void(bool /* result */)>>(std::move(func))) {}
329
+ inline void call(bool result) const noexcept {
330
+ _function->operator()(result);
331
+ }
332
+ private:
333
+ std::unique_ptr<std::function<void(bool /* result */)>> _function;
334
+ } SWIFT_NONCOPYABLE;
335
+ Func_void_bool create_Func_void_bool(void* NON_NULL swiftClosureWrapper) noexcept;
336
+ inline Func_void_bool_Wrapper wrap_Func_void_bool(Func_void_bool value) noexcept {
337
+ return Func_void_bool_Wrapper(std::move(value));
338
+ }
339
+
306
340
  // pragma MARK: std::shared_ptr<HybridMyazaFaceDetectorSpec>
307
341
  /**
308
342
  * Specialized version of `std::shared_ptr<HybridMyazaFaceDetectorSpec>`.
@@ -324,6 +358,15 @@ namespace margelo::nitro::myazakyc::bridge::swift {
324
358
  return Result<FaceResult>::withError(error);
325
359
  }
326
360
 
361
+ // pragma MARK: Result<std::shared_ptr<Promise<bool>>>
362
+ using Result_std__shared_ptr_Promise_bool___ = Result<std::shared_ptr<Promise<bool>>>;
363
+ inline Result_std__shared_ptr_Promise_bool___ create_Result_std__shared_ptr_Promise_bool___(const std::shared_ptr<Promise<bool>>& value) noexcept {
364
+ return Result<std::shared_ptr<Promise<bool>>>::withValue(value);
365
+ }
366
+ inline Result_std__shared_ptr_Promise_bool___ create_Result_std__shared_ptr_Promise_bool___(const std::exception_ptr& error) noexcept {
367
+ return Result<std::shared_ptr<Promise<bool>>>::withError(error);
368
+ }
369
+
327
370
  // pragma MARK: std::shared_ptr<HybridMyazaRectDetectorSpec>
328
371
  /**
329
372
  * Specialized version of `std::shared_ptr<HybridMyazaRectDetectorSpec>`.
@@ -20,6 +20,7 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
20
20
  #include "FaceResult.hpp"
21
21
  #include <memory>
22
22
  #include <VisionCamera/HybridFrameSpec.hpp>
23
+ #include <NitroModules/Promise.hpp>
23
24
 
24
25
  #include "KycSdkReactNative-Swift-Cxx-Umbrella.hpp"
25
26
 
@@ -79,6 +80,22 @@ namespace margelo::nitro::myazakyc {
79
80
  auto __value = std::move(__result.value());
80
81
  return __value;
81
82
  }
83
+ inline bool isModelReady() override {
84
+ auto __result = _swiftPart.isModelReady();
85
+ if (__result.hasError()) [[unlikely]] {
86
+ std::rethrow_exception(__result.error());
87
+ }
88
+ auto __value = std::move(__result.value());
89
+ return __value;
90
+ }
91
+ inline std::shared_ptr<Promise<bool>> prepareModel() override {
92
+ auto __result = _swiftPart.prepareModel();
93
+ if (__result.hasError()) [[unlikely]] {
94
+ std::rethrow_exception(__result.error());
95
+ }
96
+ auto __value = std::move(__result.value());
97
+ return __value;
98
+ }
82
99
 
83
100
  private:
84
101
  KycSdkReactNative::HybridMyazaFaceDetectorSpec_cxx _swiftPart;
@@ -0,0 +1,46 @@
1
+ ///
2
+ /// Func_void_bool.swift
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ import NitroModules
9
+
10
+ /**
11
+ * Wraps a Swift `(_ value: Bool) -> Void` as a class.
12
+ * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
13
+ */
14
+ public final class Func_void_bool {
15
+ public typealias bridge = margelo.nitro.myazakyc.bridge.swift
16
+
17
+ private let closure: (_ value: Bool) -> Void
18
+
19
+ public init(_ closure: @escaping (_ value: Bool) -> Void) {
20
+ self.closure = closure
21
+ }
22
+
23
+ @inline(__always)
24
+ public func call(value: Bool) -> Void {
25
+ self.closure(value)
26
+ }
27
+
28
+ /**
29
+ * Casts this instance to a retained unsafe raw pointer.
30
+ * This acquires one additional strong reference on the object!
31
+ */
32
+ @inline(__always)
33
+ public func toUnsafe() -> UnsafeMutableRawPointer {
34
+ return Unmanaged.passRetained(self).toOpaque()
35
+ }
36
+
37
+ /**
38
+ * Casts an unsafe pointer to a `Func_void_bool`.
39
+ * The pointer has to be a retained opaque `Unmanaged<Func_void_bool>`.
40
+ * This removes one strong reference from the object!
41
+ */
42
+ @inline(__always)
43
+ public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_bool {
44
+ return Unmanaged<Func_void_bool>.fromOpaque(pointer).takeRetainedValue()
45
+ }
46
+ }
@@ -15,6 +15,8 @@ public protocol HybridMyazaFaceDetectorSpec_protocol: HybridObject {
15
15
 
16
16
  // Methods
17
17
  func detectFace(frame: (any HybridFrameSpec)) throws -> FaceResult
18
+ func isModelReady() throws -> Bool
19
+ func prepareModel() throws -> Promise<Bool>
18
20
  }
19
21
 
20
22
  public extension HybridMyazaFaceDetectorSpec_protocol {
@@ -140,4 +140,35 @@ open class HybridMyazaFaceDetectorSpec_cxx {
140
140
  return bridge.create_Result_FaceResult_(__exceptionPtr)
141
141
  }
142
142
  }
143
+
144
+ @inline(__always)
145
+ public final func isModelReady() -> bridge.Result_bool_ {
146
+ do {
147
+ let __result = try self.__implementation.isModelReady()
148
+ let __resultCpp = __result
149
+ return bridge.create_Result_bool_(__resultCpp)
150
+ } catch (let __error) {
151
+ let __exceptionPtr = __error.toCpp()
152
+ return bridge.create_Result_bool_(__exceptionPtr)
153
+ }
154
+ }
155
+
156
+ @inline(__always)
157
+ public final func prepareModel() -> bridge.Result_std__shared_ptr_Promise_bool___ {
158
+ do {
159
+ let __result = try self.__implementation.prepareModel()
160
+ let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in
161
+ let __promise = bridge.create_std__shared_ptr_Promise_bool__()
162
+ let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise)
163
+ __result
164
+ .then({ __result in __promiseHolder.resolve(__result) })
165
+ .catch({ __error in __promiseHolder.reject(__error.toCpp()) })
166
+ return __promise
167
+ }()
168
+ return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp)
169
+ } catch (let __error) {
170
+ let __exceptionPtr = __error.toCpp()
171
+ return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr)
172
+ }
173
+ }
143
174
  }
@@ -15,6 +15,8 @@ namespace margelo::nitro::myazakyc {
15
15
  // load custom methods/properties
16
16
  registerHybrids(this, [](Prototype& prototype) {
17
17
  prototype.registerHybridMethod("detectFace", &HybridMyazaFaceDetectorSpec::detectFace);
18
+ prototype.registerHybridMethod("isModelReady", &HybridMyazaFaceDetectorSpec::isModelReady);
19
+ prototype.registerHybridMethod("prepareModel", &HybridMyazaFaceDetectorSpec::prepareModel);
18
20
  });
19
21
  }
20
22
 
@@ -21,6 +21,7 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
21
21
  #include "FaceResult.hpp"
22
22
  #include <memory>
23
23
  #include <VisionCamera/HybridFrameSpec.hpp>
24
+ #include <NitroModules/Promise.hpp>
24
25
 
25
26
  namespace margelo::nitro::myazakyc {
26
27
 
@@ -54,6 +55,8 @@ namespace margelo::nitro::myazakyc {
54
55
  public:
55
56
  // Methods
56
57
  virtual FaceResult detectFace(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
58
+ virtual bool isModelReady() = 0;
59
+ virtual std::shared_ptr<Promise<bool>> prepareModel() = 0;
57
60
 
58
61
  protected:
59
62
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myazahq/kyc-sdk-react-native",
3
- "version": "2.4.0",
3
+ "version": "2.5.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": {
package/src/MyazaKYC.tsx CHANGED
@@ -16,6 +16,7 @@ import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
16
16
  import { WorkflowGate } from './components/WorkflowGate';
17
17
  import { useWorkflowMount } from './components/useWorkflowMount';
18
18
  import { safeReportError } from './services/errors';
19
+ import { primeFaceModel } from './liveness/visionCameraFaceDetector';
19
20
  import { KycFlow, type BackResult } from './components/KycFlow';
20
21
  import { MyazaButton } from './components/MyazaButton';
21
22
 
@@ -98,6 +99,17 @@ function MyazaKYCTrigger({
98
99
  const settled = state.status !== 'resolving';
99
100
  const open = wantOpen && settled;
100
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]);
112
+
101
113
  // Reported only once the user has actually tried to start. Prefetching must
102
114
  // not fire a consumer's error handler for a flow they never opened.
103
115
  const reportedRef = useRef(false);
@@ -0,0 +1,55 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import Svg, { Rect } from 'react-native-svg';
4
+
5
+ // ─── A dashed card border the platform cannot restyle ─────────────────────────
6
+ //
7
+ // Native `borderStyle: 'dashed'` is drawn by the OS, and iOS draws it with
8
+ // long dashes and wide gaps that nothing can tune — beside the web SDK's
9
+ // tight CSS pattern the same card looked hand-drawn. This draws the same
10
+ // rectangle with SVG, where the dash pattern is ours to set; the defaults
11
+ // match the web look (and the Flutter SDK's DashedRoundedBorder). Overlay it
12
+ // inside a relatively-positioned card and drop the native border props.
13
+
14
+ export function DashedBorder({
15
+ color,
16
+ radius,
17
+ strokeWidth = 2,
18
+ dash = 4,
19
+ gap = 3,
20
+ }: {
21
+ color: string;
22
+ radius: number;
23
+ strokeWidth?: number;
24
+ dash?: number;
25
+ gap?: number;
26
+ }): React.ReactElement {
27
+ const [size, setSize] = useState<{ w: number; h: number } | null>(null);
28
+ return (
29
+ <View
30
+ pointerEvents="none"
31
+ style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
32
+ onLayout={(e) =>
33
+ setSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })
34
+ }
35
+ >
36
+ {size && size.w > strokeWidth && size.h > strokeWidth ? (
37
+ <Svg width={size.w} height={size.h}>
38
+ {/* Inset by half the stroke so the line sits fully inside the card,
39
+ where a native border would have been. */}
40
+ <Rect
41
+ x={strokeWidth / 2}
42
+ y={strokeWidth / 2}
43
+ width={size.w - strokeWidth}
44
+ height={size.h - strokeWidth}
45
+ rx={radius}
46
+ fill="none"
47
+ stroke={color}
48
+ strokeWidth={strokeWidth}
49
+ strokeDasharray={[dash, gap]}
50
+ />
51
+ </Svg>
52
+ ) : null}
53
+ </View>
54
+ );
55
+ }
@@ -106,12 +106,16 @@ export function DialCodePicker({
106
106
  bottomOffset={keyboard}
107
107
  closeLabel="Close country picker"
108
108
  >
109
- <View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.sm }}>
109
+ {/* Clear air between the header row (handle + close) and the search
110
+ field — flush against the close button they read as one control. */}
111
+ <View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.md, paddingBottom: spacing.sm }}>
112
+ {/* Deliberately NOT autofocused: springing the keyboard the instant
113
+ the sheet opens hides half the list before the person has even
114
+ seen it — search is one tap away for whoever wants it. */}
110
115
  <MyazaInput
111
116
  value={query}
112
117
  onChangeText={setQuery}
113
118
  placeholder={searchPlaceholder}
114
- autoFocus
115
119
  prefix={<Icon name="search" size={18} color={colors.textSecondary} />}
116
120
  />
117
121
  </View>
@@ -159,19 +163,22 @@ function CountryRow({
159
163
  flexDirection: 'row',
160
164
  alignItems: 'center',
161
165
  paddingHorizontal: spacing.md,
162
- paddingVertical: spacing.sm,
166
+ // Roomier rows: a country list is a long scan, and cramped rows make
167
+ // every tap a precision job. Matches the web dropdown's row height.
168
+ paddingVertical: 12,
163
169
  backgroundColor: isSelected ? colors.primary50 : 'transparent',
164
170
  }}
165
171
  >
166
- <CountryFlag country={option.code} size={24} />
172
+ <CountryFlag country={option.code} size={28} />
167
173
  <View style={{ width: spacing.md }} />
168
174
  {/* No line clamp: "Bosnia & Herzegovina" and the like must read in full,
169
- which is what Flutter's Expanded(Text) gives. */}
170
- <MyazaText variant="bodyMedium" style={{ flex: 1 }}>
175
+ which is what Flutter's Expanded(Text) gives. Full body size (16),
176
+ the web dropdown's reading size — 14 read small against the flags. */}
177
+ <MyazaText variant="body" style={{ flex: 1 }}>
171
178
  {option.name}
172
179
  </MyazaText>
173
180
  {option.dialCode != null ? (
174
- <MyazaText variant="label" color={colors.textSecondary}>
181
+ <MyazaText variant="bodyMedium" color={colors.textSecondary}>
175
182
  {option.dialCode}
176
183
  </MyazaText>
177
184
  ) : null}
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import Ionicons from '@expo/vector-icons/Ionicons';
3
3
  import {
4
+ Info,
4
5
  BadgeCheck,
5
6
  BookUser,
6
7
  Building2,
@@ -31,6 +32,7 @@ import {
31
32
  Moon,
32
33
  MoveLeft,
33
34
  Nfc,
35
+ Filter,
34
36
  Pencil,
35
37
  Smartphone,
36
38
  RefreshCw,
@@ -43,6 +45,7 @@ import {
43
45
  Sun,
44
46
  Timer,
45
47
  Upload,
48
+ Plus,
46
49
  UserRound,
47
50
  UserRoundPlus,
48
51
  Link2,
@@ -83,10 +86,13 @@ export type IconName =
83
86
  | 'building-2'
84
87
  | 'user'
85
88
  | 'user-plus'
89
+ | 'plus'
90
+ | 'info'
86
91
  | 'users'
87
92
  | 'scan-line'
88
93
  | 'scan-face'
89
94
  | 'nfc'
95
+ | 'filter'
90
96
  | 'pencil'
91
97
  | 'search'
92
98
  | 'zap'
@@ -144,7 +150,9 @@ const ICONS: Record<IconName, LucideIcon> = {
144
150
  'badge-check': BadgeCheck,
145
151
  'building-2': Building2,
146
152
  user: UserRound,
153
+ info: Info,
147
154
  'user-plus': UserRoundPlus,
155
+ plus: Plus,
148
156
  link: Link2,
149
157
  copy: Copy,
150
158
  share: Share2,
@@ -152,6 +160,7 @@ const ICONS: Record<IconName, LucideIcon> = {
152
160
  'scan-line': ScanLine,
153
161
  'scan-face': ScanFace,
154
162
  nfc: Nfc,
163
+ filter: Filter,
155
164
  pencil: Pencil,
156
165
  search: Search,
157
166
  zap: Zap,