@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.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 (246) hide show
  1. package/README.md +151 -5
  2. package/android/build.gradle +35 -3
  3. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
  4. package/app.plugin.js +89 -8
  5. package/ios/HybridMyazaFaceDetector.swift +16 -3
  6. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  7. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  8. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  9. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  10. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  11. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  12. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  13. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  15. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  17. package/package.json +14 -2
  18. package/src/MyazaBiometricAuth.tsx +119 -0
  19. package/src/MyazaKYC.tsx +12 -0
  20. package/src/components/CountryField.tsx +8 -0
  21. package/src/components/CountryRegionPicker.tsx +184 -0
  22. package/src/components/DashedBorder.tsx +55 -0
  23. package/src/components/DialCodePicker.tsx +53 -67
  24. package/src/components/DialCodeRow.tsx +102 -0
  25. package/src/components/FramedMapPicker.tsx +122 -0
  26. package/src/components/GeoBadge.tsx +34 -0
  27. package/src/components/Icon.tsx +2 -176
  28. package/src/components/KycFlow.tsx +60 -32
  29. package/src/components/LineSkeleton.tsx +100 -0
  30. package/src/components/MapChrome.tsx +67 -0
  31. package/src/components/MapPinMarker.tsx +54 -0
  32. package/src/components/MapPinPicker.tsx +179 -0
  33. package/src/components/MilestoneTrack.tsx +155 -0
  34. package/src/components/MultiIdProgress.tsx +107 -0
  35. package/src/components/MyazaInput.tsx +39 -4
  36. package/src/components/MyazaSelect.tsx +2 -1
  37. package/src/components/OptionRow.tsx +22 -3
  38. package/src/components/OwnershipSlider.tsx +102 -0
  39. package/src/components/PhoneNumberInput.tsx +45 -4
  40. package/src/components/PoweredBy.tsx +5 -5
  41. package/src/components/PresenceBlocks.tsx +153 -0
  42. package/src/components/StaggerIn.tsx +63 -0
  43. package/src/components/StepView.tsx +43 -0
  44. package/src/components/StickyActions.tsx +56 -0
  45. package/src/components/dialCodeRows.ts +64 -0
  46. package/src/components/glass/FloatingSheet.tsx +73 -21
  47. package/src/components/glass/GlassSheet.tsx +14 -2
  48. package/src/components/icon-map.ts +176 -0
  49. package/src/components/stepHeaderMeta.tsx +45 -5
  50. package/src/config/addressCollection.ts +125 -0
  51. package/src/config/biometricOptions.ts +100 -0
  52. package/src/config/business.ts +26 -7
  53. package/src/config/businessDetailsValidity.ts +52 -0
  54. package/src/config/businessPrefill.ts +72 -0
  55. package/src/config/businessSteps.ts +20 -3
  56. package/src/config/consentStep.ts +19 -0
  57. package/src/config/keyPeople.ts +111 -7
  58. package/src/config/keyPeopleCorporate.ts +19 -0
  59. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  60. package/src/config/keyPeopleOwners.ts +26 -0
  61. package/src/config/keyPeoplePrefill.ts +138 -0
  62. package/src/config/keyPeopleSectionDefs.ts +65 -0
  63. package/src/config/keyPeopleSections.ts +167 -0
  64. package/src/config/proofOfAddress.ts +54 -11
  65. package/src/config/regions.ts +24 -0
  66. package/src/config/registrationHint.ts +1 -1
  67. package/src/config/stepOrder.ts +100 -6
  68. package/src/config/theme.ts +8 -2
  69. package/src/config/uploadLimits.ts +35 -0
  70. package/src/config/website.ts +36 -0
  71. package/src/config/workflowMerge.ts +16 -5
  72. package/src/emrtd/activeAuth.ts +100 -0
  73. package/src/emrtd/extras.ts +76 -0
  74. package/src/emrtd/files.ts +5 -0
  75. package/src/emrtd/index.ts +1 -0
  76. package/src/emrtd/read.ts +8 -1
  77. package/src/emrtd/session.ts +41 -0
  78. package/src/emrtd/stages.ts +11 -0
  79. package/src/index.ts +54 -1
  80. package/src/lib/address-current-location.ts +171 -0
  81. package/src/lib/address-field-modes.ts +119 -0
  82. package/src/lib/address-flow.ts +172 -0
  83. package/src/lib/address-helpers.ts +48 -0
  84. package/src/lib/address-line.ts +76 -0
  85. package/src/lib/address-pin-move.ts +97 -0
  86. package/src/lib/address-step-recovery.ts +63 -0
  87. package/src/lib/authed-image.ts +78 -0
  88. package/src/lib/biometric-auth.ts +52 -0
  89. package/src/lib/biometric-copy.ts +51 -0
  90. package/src/lib/captureRing.ts +83 -0
  91. package/src/lib/contact-recovery.ts +44 -0
  92. package/src/lib/country-adoption.ts +89 -0
  93. package/src/lib/inferred-country.ts +65 -0
  94. package/src/lib/livenessLayout.ts +49 -0
  95. package/src/lib/map-frame.ts +159 -0
  96. package/src/lib/map-tiles.ts +143 -0
  97. package/src/lib/multi-id.ts +210 -0
  98. package/src/lib/poa-country-gate.ts +27 -0
  99. package/src/lib/resubmit.ts +130 -0
  100. package/src/lib/result-copy.ts +116 -0
  101. package/src/lib/result-wait.ts +53 -0
  102. package/src/lib/review-map-surface.ts +26 -0
  103. package/src/lib/scope.ts +31 -0
  104. package/src/lib/screen-corners.ts +50 -0
  105. package/src/lib/selfie-upload-wait.ts +79 -0
  106. package/src/lib/step-log.ts +24 -3
  107. package/src/lib/street-view-fov.ts +42 -0
  108. package/src/lib/use-multi-id-plan.ts +29 -0
  109. package/src/lib/webview-available.ts +47 -0
  110. package/src/liveness/useLiveness.ts +6 -1
  111. package/src/liveness/useModelReady.ts +65 -0
  112. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  113. package/src/presence/background-math.ts +100 -0
  114. package/src/presence/background-store.ts +82 -0
  115. package/src/presence/background.ts +165 -0
  116. package/src/presence/foreground-service.ts +193 -0
  117. package/src/presence/fs.ts +51 -0
  118. package/src/presence/geofence.ts +41 -0
  119. package/src/presence/math.ts +44 -0
  120. package/src/presence/post.ts +40 -0
  121. package/src/presence/report.ts +87 -0
  122. package/src/presence/sampler.ts +110 -0
  123. package/src/presence/status.ts +92 -0
  124. package/src/presence/store.ts +94 -0
  125. package/src/presence/tier.ts +36 -0
  126. package/src/presence/watch-wait.ts +81 -0
  127. package/src/screens/AddressCountryControl.tsx +115 -0
  128. package/src/screens/ApplicantRoleStep.tsx +9 -5
  129. package/src/screens/BusinessCheckPanel.tsx +89 -0
  130. package/src/screens/BusinessDetailsFields.tsx +112 -0
  131. package/src/screens/BusinessDetailsStep.tsx +133 -145
  132. package/src/screens/BusinessDocumentSlot.tsx +5 -5
  133. package/src/screens/BusinessDocumentsStep.tsx +5 -56
  134. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  135. package/src/screens/BusinessPickedCard.tsx +86 -0
  136. package/src/screens/BusinessPickedSection.tsx +51 -0
  137. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  138. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  139. package/src/screens/BusinessSearch.tsx +199 -0
  140. package/src/screens/BusinessSearchResults.tsx +115 -0
  141. package/src/screens/CompanyInfoFields.tsx +91 -51
  142. package/src/screens/ConsentStep.tsx +37 -17
  143. package/src/screens/ContactActions.tsx +54 -0
  144. package/src/screens/ContactDestinationField.tsx +5 -0
  145. package/src/screens/ContactEntryPanel.tsx +75 -0
  146. package/src/screens/ContactVerificationStep.tsx +43 -42
  147. package/src/screens/CountrySelectStep.tsx +13 -147
  148. package/src/screens/IdTypeStep.tsx +12 -5
  149. package/src/screens/KeepLinksSheet.tsx +88 -0
  150. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  151. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  152. package/src/screens/KeyPeoplePending.tsx +154 -0
  153. package/src/screens/KeyPeopleSection.tsx +162 -0
  154. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  155. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  156. package/src/screens/KeyPersonCard.tsx +36 -4
  157. package/src/screens/KeyPersonForm.tsx +195 -36
  158. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  159. package/src/screens/KeyPersonOwners.tsx +109 -0
  160. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  161. package/src/screens/KeyPersonSheet.tsx +92 -108
  162. package/src/screens/LivenessAvatar.tsx +15 -3
  163. package/src/screens/LivenessStep.tsx +172 -52
  164. package/src/screens/NfcStep.tsx +14 -0
  165. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  166. package/src/screens/ProofOfAddressParts.tsx +151 -0
  167. package/src/screens/ProofOfAddressStep.tsx +58 -96
  168. package/src/screens/SubmittedBadge.tsx +25 -0
  169. package/src/screens/SubmittedError.tsx +64 -0
  170. package/src/screens/SubmittedResult.tsx +117 -0
  171. package/src/screens/SubmittedStep.tsx +82 -119
  172. package/src/screens/SubmittedSuccess.tsx +127 -0
  173. package/src/screens/SubmittedWaiting.tsx +45 -0
  174. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  175. package/src/screens/address/AddressIntroGate.tsx +148 -0
  176. package/src/screens/address/AddressMapStub.tsx +50 -0
  177. package/src/screens/address/AddressPinStep.tsx +178 -0
  178. package/src/screens/address/AddressReviewStep.tsx +162 -0
  179. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  180. package/src/screens/address/AddressSearchField.tsx +115 -0
  181. package/src/screens/address/AddressSearchStep.tsx +94 -0
  182. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  183. package/src/screens/address/DetailsSheet.tsx +175 -0
  184. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  185. package/src/screens/address/EntranceDropzone.tsx +178 -0
  186. package/src/screens/address/EntranceFraming.tsx +66 -0
  187. package/src/screens/address/EntrancePills.tsx +60 -0
  188. package/src/screens/address/FramedStreetView.tsx +155 -0
  189. package/src/screens/address/IntroDisclosures.tsx +210 -0
  190. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  191. package/src/screens/address/PinSummaryRow.tsx +105 -0
  192. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  193. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  194. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  195. package/src/screens/address/SearchResults.tsx +105 -0
  196. package/src/screens/address/SearchScreen.tsx +176 -0
  197. package/src/screens/address/SkipForNow.tsx +43 -0
  198. package/src/screens/address/StreetViewChrome.tsx +81 -0
  199. package/src/screens/address/detail-values.ts +22 -0
  200. package/src/screens/address/fix-source.ts +27 -0
  201. package/src/screens/address/index.ts +8 -0
  202. package/src/screens/address/meta.ts +44 -0
  203. package/src/screens/address/use-address-flow.ts +200 -0
  204. package/src/screens/address/use-label-pin.ts +80 -0
  205. package/src/screens/address/use-pin-actions.ts +192 -0
  206. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  207. package/src/screens/consent/model.ts +79 -10
  208. package/src/screens/liveness/CaptureRing.tsx +91 -0
  209. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  210. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  211. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  212. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  213. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  214. package/src/screens/liveness/index.ts +4 -0
  215. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  216. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  217. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  218. package/src/screens/useAddressPhotoAttach.ts +77 -0
  219. package/src/screens/useAwaitingPeople.ts +90 -0
  220. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  221. package/src/screens/usePoaAttach.ts +12 -15
  222. package/src/services/api-biometric.ts +37 -0
  223. package/src/services/api-types-biometric.ts +41 -0
  224. package/src/services/api-types.ts +251 -3
  225. package/src/services/api-verify-types.ts +90 -1
  226. package/src/services/api.ts +237 -3
  227. package/src/services/deviceMetadata.ts +1 -1
  228. package/src/services/errors.ts +10 -2
  229. package/src/services/location.ts +234 -0
  230. package/src/services/uploadErrors.ts +30 -0
  231. package/src/services/workflowGate.ts +8 -0
  232. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  233. package/src/store/address.ts +97 -0
  234. package/src/store/businessCheck.ts +113 -0
  235. package/src/store/derive.ts +44 -4
  236. package/src/store/kycStore.ts +352 -16
  237. package/src/store/serverConfig.ts +20 -0
  238. package/src/store/session.ts +227 -0
  239. package/src/store/state.ts +289 -2
  240. package/src/store/submit.ts +80 -13
  241. package/src/store/submitApplicant.ts +5 -11
  242. package/src/types/business.ts +23 -2
  243. package/src/types/config.ts +70 -2
  244. package/src/types/verification.ts +13 -1
  245. package/src/types/workflow.ts +112 -1
  246. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -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.6.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,8 @@
49
49
  "license": "MIT",
50
50
  "peerDependencies": {
51
51
  "expo": ">=56",
52
+ "expo-task-manager": ">=14",
53
+ "react-native-webview": ">=13",
52
54
  "react": ">=19",
53
55
  "react-native": ">=0.83",
54
56
  "react-native-nitro-image": ">=0.15",
@@ -58,6 +60,14 @@
58
60
  "react-native-vision-camera-worklets": ">=5",
59
61
  "react-native-worklets": ">=0.8"
60
62
  },
63
+ "peerDependenciesMeta": {
64
+ "expo-task-manager": {
65
+ "optional": true
66
+ },
67
+ "react-native-webview": {
68
+ "optional": true
69
+ }
70
+ },
61
71
  "dependencies": {
62
72
  "@expo-google-fonts/karla": "^0.4.2",
63
73
  "@expo-google-fonts/space-grotesk": "^0.4.1",
@@ -74,11 +84,13 @@
74
84
  "expo-image-manipulator": "~56.0.17",
75
85
  "expo-image-picker": "~56.0.16",
76
86
  "expo-localization": "~56.0.6",
87
+ "expo-location": "~56.0.24",
77
88
  "expo-speech": "~56.0.3",
78
89
  "lucide-react-native": "^1.17.0",
79
90
  "react-native-compressor": "^1.19.0",
80
91
  "react-native-vision-camera": "^5.0.11",
81
- "zustand": "^5.0.2"
92
+ "zustand": "^5.0.2",
93
+ "expo-file-system": "~56.0.0"
82
94
  },
83
95
  "devDependencies": {
84
96
  "@types/jest": "^29.5.12",
@@ -0,0 +1,119 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Modal, Platform } from 'react-native';
3
+
4
+ import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
5
+ import { MyazaButton } from './components/MyazaButton';
6
+ import { BiometricAuthFlow } from './screens/biometric/BiometricAuthFlow';
7
+ import { primeFaceModel } from './liveness/visionCameraFaceDetector';
8
+ import { defaultReauthLabel } from './lib/biometric-auth';
9
+ import type { KYCAppearance, ResolvedKYCConfig, SupportedCountry } from './types/config';
10
+ import type { KYCError } from './types/verification';
11
+ import type { BiometricAuthResponse } from './services/api-types-biometric';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // MyazaBiometricAuth — returning-user face re-authentication ("prove it's
15
+ // still you"). Mirrors the web SDK's MyazaBiometricAuth: a verified user
16
+ // re-authenticates with a live selfie matched 1:1 against their enrollment
17
+ // reference (no gov-DB call, no re-KYC). Self-contained: its own trigger +
18
+ // modal, hosting the SDK's real liveness step inside a scoped runtime.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface MyazaBiometricAuthProps {
22
+ /** Publishable API key (`pk_…`). The environment is derived from its prefix. */
23
+ apiKey: string;
24
+ /** Dev-only base-URL override for `pk_dev_` keys. */
25
+ devUrl?: string;
26
+ /** The org's user reference (Entity.externalUserId) to re-authenticate. */
27
+ externalUserId: string;
28
+ /** Presence Intelligence method (default 'gestures'). */
29
+ livenessMode?: 'gestures' | 'flash' | 'both';
30
+ flashSequenceLength?: number;
31
+ appearance?: KYCAppearance;
32
+ disableClose?: boolean;
33
+ /** Open the modal on mount (no trigger button is rendered). */
34
+ defaultOpen?: boolean;
35
+ /** Custom trigger label (default "Verify it's you"). */
36
+ children?: string;
37
+ disabled?: boolean;
38
+ onOpen?: () => void;
39
+ /** The user re-authenticated. `token` is a single-use proof — verify it from
40
+ * your backend with a secret key at `/biometric/verify-proof`. */
41
+ onAuthenticated?: (result: { attemptId: string; confidence: number | null; token?: string }) => void;
42
+ /** The check ran but the user did not pass (no match / liveness failed). */
43
+ onFailed?: (result: { status: BiometricAuthResponse['status']; attemptId: string; confidence: number | null }) => void;
44
+ /** A technical error (network, not enrolled, insufficient credits, …). */
45
+ onError?: (error: KYCError) => void;
46
+ onClose?: () => void;
47
+ }
48
+
49
+ const MODAL_PRESENTATION = Platform.OS === 'ios' ? 'pageSheet' : 'fullScreen';
50
+
51
+ export function MyazaBiometricAuth(props: MyazaBiometricAuthProps): React.ReactElement {
52
+ const { children, disabled, defaultOpen, onOpen, onAuthenticated, onFailed, onError, onClose } = props;
53
+ const [open, setOpen] = useState(defaultOpen === true);
54
+
55
+ // The liveness step reads exactly the keys a KYC flow's config carries; the
56
+ // scope parks the step order on consent → liveness → submitted, and the
57
+ // flow only ever mounts the middle one. `country` is required by the
58
+ // resolved shape but nothing on this path reads it.
59
+ const config: ResolvedKYCConfig = {
60
+ apiKey: props.apiKey,
61
+ devUrl: props.devUrl,
62
+ country: 'NG' as SupportedCountry,
63
+ scope: 'biometric-authentication',
64
+ enableSelfie: true,
65
+ livenessMode: props.livenessMode ?? 'gestures',
66
+ flashSequenceLength: props.flashSequenceLength,
67
+ appearance: props.appearance,
68
+ disableClose: props.disableClose,
69
+ userId: props.externalUserId,
70
+ onError,
71
+ };
72
+
73
+ const openFlow = useCallback(() => {
74
+ void primeFaceModel();
75
+ setOpen(true);
76
+ onOpen?.();
77
+ }, [onOpen]);
78
+ const close = useCallback(() => {
79
+ setOpen(false);
80
+ onClose?.();
81
+ }, [onClose]);
82
+ const blockDismiss = props.disableClose === true;
83
+
84
+ return (
85
+ <MyazaThemeProvider appearance={props.appearance}>
86
+ {defaultOpen !== true && (
87
+ <MyazaButton
88
+ label={children ?? defaultReauthLabel(props.appearance?.companyName)}
89
+ disabled={disabled}
90
+ fullWidth={false}
91
+ onPress={openFlow}
92
+ />
93
+ )}
94
+ <Modal
95
+ visible={open}
96
+ animationType="slide"
97
+ presentationStyle={blockDismiss ? 'fullScreen' : MODAL_PRESENTATION}
98
+ statusBarTranslucent
99
+ navigationBarTranslucent
100
+ onRequestClose={blockDismiss ? () => undefined : close}
101
+ onDismiss={blockDismiss ? undefined : close}
102
+ >
103
+ {open ? (
104
+ // A fresh store per open: the runtime provider creates it once per
105
+ // mount, and the modal's children mount once per open.
106
+ <KycRuntimeProvider config={config}>
107
+ <BiometricAuthFlow
108
+ externalUserId={props.externalUserId}
109
+ onAuthenticated={(r) => onAuthenticated?.({ attemptId: r.attemptId, confidence: r.confidence, token: r.token })}
110
+ onFailed={(r) => onFailed?.({ status: r.status, attemptId: r.attemptId, confidence: r.confidence })}
111
+ onError={onError}
112
+ onClose={close}
113
+ />
114
+ </KycRuntimeProvider>
115
+ ) : null}
116
+ </Modal>
117
+ </MyazaThemeProvider>
118
+ );
119
+ }
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);
@@ -21,6 +21,8 @@ export function CountryField({
21
21
  value,
22
22
  options,
23
23
  onChange,
24
+ geoCountry,
25
+ grouped,
24
26
  placeholder = 'Select a country',
25
27
  searchPlaceholder = 'Search country',
26
28
  }: {
@@ -29,6 +31,10 @@ export function CountryField({
29
31
  /** The pickable countries — all ISO, or a workflow's registry subset. */
30
32
  options: DialCodeOption[];
31
33
  onChange: (code: string) => void;
34
+ /** The visitor's inferred country, pinned on top of the sheet as "Your location". */
35
+ geoCountry?: string | null;
36
+ /** Region headers between the rows, the country-select step's way. */
37
+ grouped?: boolean;
32
38
  placeholder?: string;
33
39
  searchPlaceholder?: string;
34
40
  }): React.ReactElement {
@@ -75,6 +81,8 @@ export function CountryField({
75
81
  visible={open}
76
82
  options={options}
77
83
  selected={value ?? ''}
84
+ pinned={geoCountry}
85
+ grouped={grouped}
78
86
  searchPlaceholder={searchPlaceholder}
79
87
  onPick={(code) => {
80
88
  setOpen(false);
@@ -0,0 +1,184 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Pressable, ScrollView, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { groupCountriesByRegion, pinGeoRow, regionCountryName } from '../config/regions';
6
+ import { useTheme } from './runtime';
7
+ import { MyazaText } from './Typography';
8
+ import { MyazaInput } from './MyazaInput';
9
+ import { CountryFlag } from './CountryFlag';
10
+ import { Icon } from './Icon';
11
+ import { GeoBadge } from './GeoBadge';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Searchable, region-grouped country picker for the country-select step. Used
15
+ // when a workflow offers many countries (Global Documents): a flat 200-country
16
+ // list is a scroll, not a chooser, so this adds a pinned search box and
17
+ // continent headers over a bounded scroll area. Mirrors the web SDK's
18
+ // CountryRegionPicker and Flutter's country_region_picker.dart.
19
+ //
20
+ // Split from CountrySelectStep (200-line rule); the step keeps the flat list
21
+ // and the threshold that decides between the two.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function CountryRegionPicker({
25
+ countries,
26
+ selected,
27
+ geoCountry,
28
+ onPick,
29
+ }: {
30
+ countries: string[];
31
+ selected: string | null;
32
+ /**
33
+ * The visitor's IP country. Lifted out of its continent to the very top and
34
+ * tagged, so the one country most likely to be theirs is the first thing
35
+ * they see rather than something to scroll for.
36
+ */
37
+ geoCountry?: string | null;
38
+ onPick: (country: string) => void;
39
+ }): React.ReactElement {
40
+ const { colors } = useTheme();
41
+ const [query, setQuery] = useState('');
42
+
43
+ const { pinned, groups } = useMemo(() => {
44
+ const needle = query.trim().toLowerCase();
45
+ const visible = needle
46
+ ? countries.filter(
47
+ (code) =>
48
+ code.toLowerCase().includes(needle) ||
49
+ regionCountryName(code).toLowerCase().includes(needle),
50
+ )
51
+ : countries;
52
+ const split = pinGeoRow(visible, geoCountry, (code) => code);
53
+ return { pinned: split.pinned, groups: groupCountriesByRegion(split.rest) };
54
+ }, [countries, query, geoCountry]);
55
+
56
+ const empty = groups.length === 0 && pinned === null;
57
+
58
+ return (
59
+ // The search box stays put and the list owns its own scroll, so a
60
+ // ~240-country flow does not scroll the search field off the top. That
61
+ // relies on the SHEET rendering this step with `fillsViewport` — inside a
62
+ // scroll view the nested list below would take an unbounded height, outgrow
63
+ // the viewport, and drag the search box off the top with it.
64
+ <View style={{ flex: 1 }}>
65
+ <MyazaInput
66
+ value={query}
67
+ onChangeText={setQuery}
68
+ placeholder="Search countries…"
69
+ autoCapitalize="none"
70
+ prefix={<Icon name="search" size={18} color={colors.textSecondary} />}
71
+ />
72
+ <View style={{ height: spacing.md }} />
73
+
74
+ {empty ? (
75
+ <MyazaText variant="bodyMedium" style={{ textAlign: 'center', marginTop: spacing.lg }}>
76
+ No countries match your search.
77
+ </MyazaText>
78
+ ) : (
79
+ // `nestedScrollEnabled` stays: Android refuses to scroll a nested
80
+ // same-direction list without it, and this is still nested on the
81
+ // flat-list path and inside any future scrolling parent.
82
+ <ScrollView
83
+ nestedScrollEnabled
84
+ keyboardShouldPersistTaps="handled"
85
+ showsVerticalScrollIndicator={false}
86
+ >
87
+ {pinned ? (
88
+ <CountryRow
89
+ code={pinned}
90
+ name={regionCountryName(pinned)}
91
+ selected={pinned === selected}
92
+ badge="Your location"
93
+ onPress={() => onPick(pinned)}
94
+ />
95
+ ) : null}
96
+ {groups.map((group) => (
97
+ <View key={group.region}>
98
+ {/* Uppercase, tracked-out section label — Flutter's region
99
+ header. Lowercase text at the default weight read as another
100
+ list row rather than a divider. */}
101
+ <MyazaText
102
+ variant="bodySmall"
103
+ color={colors.textSecondary}
104
+ style={{
105
+ fontWeight: '700',
106
+ letterSpacing: 0.6,
107
+ paddingHorizontal: spacing.xs,
108
+ paddingVertical: spacing.sm,
109
+ }}
110
+ >
111
+ {group.region.toUpperCase()}
112
+ </MyazaText>
113
+ {group.countries.map((entry) => (
114
+ <CountryRow
115
+ key={entry.code}
116
+ code={entry.code}
117
+ name={entry.name}
118
+ selected={entry.code === selected}
119
+ onPress={() => onPick(entry.code)}
120
+ />
121
+ ))}
122
+ </View>
123
+ ))}
124
+ </ScrollView>
125
+ )}
126
+ </View>
127
+ );
128
+ }
129
+
130
+ /**
131
+ * One selectable country row, shared by the flat list and the region picker
132
+ * so the two cannot drift apart. `badge` tags the pinned geo row; it is the
133
+ * same control in a second position, differing only by the tag.
134
+ */
135
+ export function CountryRow({
136
+ code,
137
+ name,
138
+ selected,
139
+ badge,
140
+ onPress,
141
+ }: {
142
+ code: string;
143
+ name: string;
144
+ selected: boolean;
145
+ badge?: string;
146
+ onPress: () => void;
147
+ }): React.ReactElement {
148
+ const { colors } = useTheme();
149
+ return (
150
+ <Pressable
151
+ onPress={onPress}
152
+ accessibilityRole="radio"
153
+ accessibilityState={{ checked: selected }}
154
+ accessibilityLabel={badge ? `${name}, ${badge.toLowerCase()}` : name}
155
+ // Matches the Flutter SDK's CountryOptionTile exactly — the two had
156
+ // drifted (24px flag / 12px padding here vs 32 / 16 there), which is why
157
+ // the same screen looked like a different component on each platform. The
158
+ // spacing and radius scales are identical across the SDKs, so there is
159
+ // nothing platform-specific to reconcile: `md` means 16 on both.
160
+ style={{
161
+ flexDirection: 'row',
162
+ alignItems: 'center',
163
+ padding: spacing.md,
164
+ borderRadius: radius.md,
165
+ borderWidth: selected ? 1.5 : 1,
166
+ borderColor: selected ? colors.primary : colors.border,
167
+ backgroundColor: selected ? colors.primary50 : colors.background,
168
+ marginBottom: spacing.sm,
169
+ }}
170
+ >
171
+ <CountryFlag country={code} size={32} />
172
+ <View style={{ width: 12 }} />
173
+ <MyazaText variant="label" style={{ flex: 1, fontWeight: '500' }}>
174
+ {name}
175
+ </MyazaText>
176
+ {badge ? <GeoBadge label={badge} /> : null}
177
+ <Icon
178
+ name="chevron-right"
179
+ size={18}
180
+ color={selected ? colors.primary : colors.textSecondary}
181
+ />
182
+ </Pressable>
183
+ );
184
+ }
@@ -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
+ }