@myazahq/kyc-sdk-react-native 2.3.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 (129) 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 -2
  16. package/src/MyazaKYC.tsx +12 -0
  17. package/src/components/BrandBar.tsx +137 -0
  18. package/src/components/DashedBorder.tsx +55 -0
  19. package/src/components/DialCodePicker.tsx +25 -24
  20. package/src/components/DocumentReviewSide.tsx +34 -14
  21. package/src/components/Icon.tsx +14 -0
  22. package/src/components/KycSheet.tsx +39 -140
  23. package/src/components/MediaSourceSheet.tsx +7 -37
  24. package/src/components/MultiIdProgress.tsx +107 -0
  25. package/src/components/MyazaDateField.tsx +6 -20
  26. package/src/components/MyazaInput.tsx +23 -4
  27. package/src/components/MyazaSelect.tsx +17 -40
  28. package/src/components/OwnershipSlider.tsx +102 -0
  29. package/src/components/PhoneNumberInput.tsx +37 -4
  30. package/src/components/SandboxBanner.tsx +92 -0
  31. package/src/components/StaggerIn.tsx +63 -0
  32. package/src/components/StepHeader.tsx +15 -2
  33. package/src/components/StepView.tsx +26 -0
  34. package/src/components/glass/ChromeGlass.tsx +65 -0
  35. package/src/components/glass/FloatingSheet.tsx +242 -0
  36. package/src/components/glass/GlassSheet.tsx +50 -0
  37. package/src/components/glass/GlassSurface.tsx +31 -3
  38. package/src/components/stepHeaderMeta.tsx +3 -1
  39. package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
  40. package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
  41. package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
  42. package/src/config/business.ts +6 -6
  43. package/src/config/businessDetailsValidity.ts +52 -0
  44. package/src/config/businessPrefill.ts +72 -0
  45. package/src/config/businessSteps.ts +14 -3
  46. package/src/config/keyPeople.ts +111 -7
  47. package/src/config/keyPeopleCorporate.ts +19 -0
  48. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  49. package/src/config/keyPeopleOwners.ts +26 -0
  50. package/src/config/keyPeoplePrefill.ts +138 -0
  51. package/src/config/keyPeopleSectionDefs.ts +65 -0
  52. package/src/config/keyPeopleSections.ts +167 -0
  53. package/src/config/registrationHint.ts +1 -1
  54. package/src/config/stepOrder.ts +24 -4
  55. package/src/config/theme.ts +8 -2
  56. package/src/config/website.ts +36 -0
  57. package/src/config/workflowMerge.ts +5 -0
  58. package/src/emrtd/activeAuth.ts +100 -0
  59. package/src/emrtd/extras.ts +76 -0
  60. package/src/emrtd/files.ts +5 -0
  61. package/src/emrtd/index.ts +1 -0
  62. package/src/emrtd/read.ts +8 -1
  63. package/src/emrtd/session.ts +41 -0
  64. package/src/emrtd/stages.ts +11 -0
  65. package/src/lib/contact-recovery.ts +44 -0
  66. package/src/lib/multi-id.ts +210 -0
  67. package/src/lib/resubmit.ts +130 -0
  68. package/src/lib/screen-corners.ts +50 -0
  69. package/src/lib/step-log.ts +24 -3
  70. package/src/lib/use-multi-id-plan.ts +29 -0
  71. package/src/liveness/useModelReady.ts +65 -0
  72. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  73. package/src/screens/ApplicantRoleStep.tsx +9 -5
  74. package/src/screens/BusinessCheckPanel.tsx +89 -0
  75. package/src/screens/BusinessDetailsFields.tsx +109 -0
  76. package/src/screens/BusinessDetailsStep.tsx +131 -145
  77. package/src/screens/BusinessDocumentSlot.tsx +2 -3
  78. package/src/screens/BusinessDocumentsStep.tsx +1 -1
  79. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  80. package/src/screens/BusinessPickedCard.tsx +86 -0
  81. package/src/screens/BusinessPickedSection.tsx +51 -0
  82. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  83. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  84. package/src/screens/BusinessSearch.tsx +199 -0
  85. package/src/screens/BusinessSearchResults.tsx +115 -0
  86. package/src/screens/CompanyInfoFields.tsx +86 -51
  87. package/src/screens/ConsentStep.tsx +31 -0
  88. package/src/screens/ContactDestinationField.tsx +1 -0
  89. package/src/screens/ContactVerificationStep.tsx +34 -3
  90. package/src/screens/IdTypeStep.tsx +12 -5
  91. package/src/screens/KeepLinksSheet.tsx +88 -0
  92. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  93. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  94. package/src/screens/KeyPeoplePending.tsx +154 -0
  95. package/src/screens/KeyPeopleSection.tsx +162 -0
  96. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  97. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  98. package/src/screens/KeyPersonCard.tsx +36 -4
  99. package/src/screens/KeyPersonForm.tsx +195 -36
  100. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  101. package/src/screens/KeyPersonOwners.tsx +109 -0
  102. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  103. package/src/screens/KeyPersonSheet.tsx +92 -108
  104. package/src/screens/LivenessStep.tsx +18 -0
  105. package/src/screens/NfcStep.tsx +14 -0
  106. package/src/screens/ProofOfAddressStep.tsx +6 -3
  107. package/src/screens/SubmittedStep.tsx +66 -10
  108. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  109. package/src/screens/useAwaitingPeople.ts +90 -0
  110. package/src/services/api-types.ts +158 -2
  111. package/src/services/api-verify-types.ts +53 -1
  112. package/src/services/api.ts +104 -0
  113. package/src/services/deviceMetadata.ts +1 -1
  114. package/src/services/errors.ts +10 -2
  115. package/src/services/workflowGate.ts +1 -0
  116. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  117. package/src/store/businessCheck.ts +113 -0
  118. package/src/store/derive.ts +15 -0
  119. package/src/store/kycStore.ts +241 -14
  120. package/src/store/serverConfig.ts +2 -0
  121. package/src/store/session.ts +208 -0
  122. package/src/store/state.ts +155 -2
  123. package/src/store/submit.ts +63 -13
  124. package/src/store/submitApplicant.ts +5 -11
  125. package/src/types/business.ts +23 -2
  126. package/src/types/config.ts +29 -3
  127. package/src/types/verification.ts +1 -1
  128. package/src/types/workflow.ts +15 -0
  129. 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.3.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": {
@@ -61,7 +61,6 @@
61
61
  "dependencies": {
62
62
  "@expo-google-fonts/karla": "^0.4.2",
63
63
  "@expo-google-fonts/space-grotesk": "^0.4.1",
64
- "@expo/ui": "~56.0.16",
65
64
  "@expo/vector-icons": "^14.0.4",
66
65
  "country-flag-icons": "^1.6.17",
67
66
  "expo-application": "~56.0.3",
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,137 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Image, View } from 'react-native';
3
+ import { SvgXml } from 'react-native-svg';
4
+
5
+ import { radius, spacing } from '../config/theme';
6
+ import { useTheme } from './runtime';
7
+ import { MyazaText } from './Typography';
8
+
9
+ /** Whether a logo URI points at an SVG (extension check, query-safe). */
10
+ export function isSvgUri(uri: string): boolean {
11
+ const path = uri.split('?')[0] ?? uri;
12
+ return path.toLowerCase().endsWith('.svg');
13
+ }
14
+
15
+ // Deliberately NOT on a Liquid Glass capsule, though it would balance the
16
+ // theme/close capsule opposite it. Glass at capsule scale is the material for
17
+ // controls — something that floats and responds to touch. The brand is a label,
18
+ // so a pill around it promises a tap that never does anything. The header block
19
+ // behind it is glass, which is the correct use: a surface, not a control.
20
+ //
21
+ // Persistent header brand — circular logo avatar + company name. A broken/missing
22
+ // logo image collapses the whole brand bar (mirrors the web SDK's `onError` and
23
+ // Flutter's `errorBuilder`), so the header never shows a broken-image box.
24
+ export function BrandBar({
25
+ logoUri,
26
+ companyName,
27
+ }: {
28
+ logoUri: string;
29
+ companyName: string;
30
+ }): React.ReactElement | null {
31
+ const { colors } = useTheme();
32
+ const [broken, setBroken] = useState(false);
33
+ const svg = isSvgUri(logoUri);
34
+ // SVG logos render through SvgXml with SELF-FETCHED markup — the exact
35
+ // pipeline the country flags use, which is proven on-device. SvgUri's own
36
+ // fetch/sizing was flaky here (the favicon rendered at intrinsic size
37
+ // instead of filling the chip).
38
+ const [xml, setXml] = useState<string | null>(null);
39
+ useEffect(() => {
40
+ if (!svg) return;
41
+ let cancelled = false;
42
+ fetch(logoUri)
43
+ .then((r) => (r.ok ? r.text() : Promise.reject(new Error(String(r.status)))))
44
+ .then((text) => {
45
+ if (!cancelled) setXml(text);
46
+ })
47
+ .catch(() => {
48
+ if (!cancelled) setBroken(true);
49
+ });
50
+ return () => {
51
+ cancelled = true;
52
+ };
53
+ }, [logoUri, svg]);
54
+ if (broken) return null;
55
+ return (
56
+ <>
57
+ {/* TWO layers, deliberately.
58
+
59
+ A shadow and `overflow: 'hidden'` CANNOT live on the same view on iOS:
60
+ overflow-hidden compiles to `masksToBounds`, which clips the shadow
61
+ away with everything else outside the bounds. Android draws `elevation`
62
+ outside the view, so it survived there — which is why the chip lost its
63
+ ring on iPhone only.
64
+
65
+ So the outer view owns the border and shadow and does NOT clip, and the
66
+ inner one clips the logo. Same split Flutter gets for free by putting
67
+ the border on the Container's decoration and the crop in a ClipOval.
68
+
69
+ The white plate is load-bearing, not decoration: brand logos are
70
+ routinely dark artwork on a transparent background (the import picks up
71
+ favicons), which would vanish against a dark header. */}
72
+ <View
73
+ style={{
74
+ width: 28,
75
+ height: 28,
76
+ borderRadius: radius.full,
77
+ backgroundColor: '#FFFFFF',
78
+ // Light ring + soft shadow — the same treatment as the Flutter SDK's
79
+ // brand chip (black 5% ring, black 6% blur-4 y-1 shadow) and the web
80
+ // SDK's ring-1 ring-black/5.
81
+ borderWidth: 1,
82
+ borderColor: 'rgba(0,0,0,0.05)',
83
+ shadowColor: '#000000',
84
+ shadowOpacity: 0.06,
85
+ shadowRadius: 4,
86
+ shadowOffset: { width: 0, height: 1 },
87
+ elevation: 1,
88
+ marginRight: spacing.sm,
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ }}
92
+ >
93
+ <View
94
+ style={{
95
+ // Fills the parent's CONTENT box (inside its 1px border) and is the
96
+ // only thing that clips, so the logo reaches the ring on every side.
97
+ alignSelf: 'stretch',
98
+ flex: 1,
99
+ borderRadius: radius.full,
100
+ overflow: 'hidden',
101
+ }}
102
+ >
103
+ {/* Fill + cover-crop, matching Flutter's SizedBox.expand + BoxFit.cover.
104
+
105
+ Sized in PERCENTAGES, not the literal 26 this used to hardcode: that
106
+ number was "28 minus the 1px border on each side", so it silently
107
+ went wrong the moment either value changed, and it left the logo a
108
+ pixel short of the ring. The clipping parent now owns the geometry.
109
+
110
+ SVG logos are common here because the brand import picks up site
111
+ favicons, and <Image> cannot decode SVG at all — hence the fetched
112
+ markup through SvgXml. `slice` is the SVG spelling of cover-crop. */}
113
+ {svg ? (
114
+ xml ? (
115
+ <SvgXml xml={xml} width="100%" height="100%" preserveAspectRatio="xMidYMid slice" />
116
+ ) : null
117
+ ) : (
118
+ <Image
119
+ source={{ uri: logoUri }}
120
+ style={{ width: '100%', height: '100%' }}
121
+ resizeMode="cover"
122
+ onError={() => setBroken(true)}
123
+ />
124
+ )}
125
+ </View>
126
+ </View>
127
+ <MyazaText
128
+ variant="heading3"
129
+ numberOfLines={1}
130
+ color={colors.textDark}
131
+ style={{ flexShrink: 1, fontSize: 14 }}
132
+ >
133
+ {companyName}
134
+ </MyazaText>
135
+ </>
136
+ );
137
+ }