@myazahq/kyc-sdk-react-native 2.0.2 → 2.1.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 (281) hide show
  1. package/README.md +111 -8
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/AndroidManifest.xml +14 -4
  4. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
  8. package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
  9. package/app.plugin.js +102 -13
  10. package/ios/HybridMyazaEmrtd.swift +447 -0
  11. package/ios/HybridMyazaFaceDetector.swift +104 -2
  12. package/ios/HybridMyazaRectDetector.swift +103 -0
  13. package/ios/HybridMyazaTextRecognizer.swift +111 -0
  14. package/nitrogen/generated/.gitattributes +1 -0
  15. package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
  16. package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
  17. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
  18. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
  19. package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
  20. package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
  21. package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
  22. package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
  23. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
  24. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
  25. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
  26. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
  27. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
  28. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
  29. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
  30. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
  31. package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
  42. package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
  44. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
  45. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
  46. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
  47. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
  48. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
  49. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
  50. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
  51. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
  52. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
  53. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
  54. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
  56. package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
  57. package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
  58. package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
  59. package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
  60. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  61. package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
  62. package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
  63. package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
  64. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  65. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  66. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
  67. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
  68. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
  69. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
  70. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
  71. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
  72. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
  73. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
  74. package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
  75. package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
  76. package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
  77. package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
  78. package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
  79. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
  80. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
  81. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
  82. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
  83. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
  84. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
  85. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
  86. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
  87. package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
  88. package/package.json +16 -4
  89. package/src/MyazaKYC.tsx +115 -22
  90. package/src/capture/documentIdentity.ts +153 -0
  91. package/src/capture/documentSignals.ts +177 -0
  92. package/src/capture/documentTextGate.ts +171 -0
  93. package/src/capture/hints.ts +57 -0
  94. package/src/capture/index.ts +24 -0
  95. package/src/capture/rectDetector.ts +84 -0
  96. package/src/capture/rectGate.ts +91 -0
  97. package/src/capture/useAutoCapture.ts +198 -0
  98. package/src/components/CameraViewfinder.tsx +226 -70
  99. package/src/components/ContactCodeEntry.tsx +139 -0
  100. package/src/components/CountryField.tsx +87 -0
  101. package/src/components/CountryFlag.tsx +15 -15
  102. package/src/components/DialCodePicker.tsx +186 -0
  103. package/src/components/DocumentCropper.tsx +1 -1
  104. package/src/components/DocumentReview.tsx +142 -0
  105. package/src/components/DocumentReviewSide.tsx +135 -0
  106. package/src/components/DocumentReviewZoom.tsx +119 -0
  107. package/src/components/ExpiryCountdown.tsx +52 -0
  108. package/src/components/FlashOverlay.tsx +156 -0
  109. package/src/components/Icon.tsx +67 -1
  110. package/src/components/KycFlow.tsx +66 -94
  111. package/src/components/KycSheet.tsx +148 -18
  112. package/src/components/MediaSourceSheet.tsx +163 -0
  113. package/src/components/MyazaButton.tsx +14 -2
  114. package/src/components/MyazaDateField.tsx +240 -0
  115. package/src/components/MyazaInput.tsx +73 -26
  116. package/src/components/MyazaSelect.tsx +283 -0
  117. package/src/components/MyazaWordmark.tsx +33 -0
  118. package/src/components/OptionRow.tsx +132 -0
  119. package/src/components/PhoneNumberInput.tsx +149 -0
  120. package/src/components/PoweredBy.tsx +89 -0
  121. package/src/components/ReadyPrimer.tsx +149 -0
  122. package/src/components/StepView.tsx +91 -0
  123. package/src/components/Typography.tsx +18 -3
  124. package/src/components/VerifiedNotice.tsx +38 -0
  125. package/src/components/WhatsAppIcon.tsx +29 -0
  126. package/src/components/WorkflowGate.tsx +128 -0
  127. package/src/components/brand-font.ts +113 -0
  128. package/src/components/flashHoleGeometry.ts +35 -0
  129. package/src/components/flow/FatalConfigError.tsx +56 -0
  130. package/src/components/fonts.ts +2 -19
  131. package/src/components/readyPrimerContent.ts +52 -0
  132. package/src/components/runtime.tsx +38 -43
  133. package/src/components/stepHeaderMeta.tsx +87 -0
  134. package/src/components/theme-provider.tsx +89 -0
  135. package/src/components/useWorkflowMount.ts +116 -0
  136. package/src/components/viewfinder/CardGuide.tsx +60 -0
  137. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  138. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  139. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  140. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  141. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  142. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  143. package/src/config/brand.ts +97 -0
  144. package/src/config/business.ts +179 -0
  145. package/src/config/businessSteps.ts +105 -0
  146. package/src/config/contact.ts +159 -0
  147. package/src/config/countryNames.g.ts +254 -0
  148. package/src/config/currencyFlags.ts +144 -0
  149. package/src/config/font-resolve.ts +78 -0
  150. package/src/config/idTypes.ts +96 -1
  151. package/src/config/keyPeople.ts +152 -0
  152. package/src/config/phone.ts +39 -0
  153. package/src/config/proofOfAddress.ts +75 -0
  154. package/src/config/questionnaire.ts +153 -0
  155. package/src/config/regions.ts +118 -0
  156. package/src/config/registrationHint.ts +87 -0
  157. package/src/config/stepOrder.ts +118 -0
  158. package/src/config/theme.ts +38 -2
  159. package/src/config/workflowMerge.ts +124 -0
  160. package/src/emrtd/bac.ts +128 -0
  161. package/src/emrtd/bytes.ts +127 -0
  162. package/src/emrtd/crypto.ts +163 -0
  163. package/src/emrtd/der.ts +192 -0
  164. package/src/emrtd/dg2.ts +111 -0
  165. package/src/emrtd/files.ts +215 -0
  166. package/src/emrtd/index.ts +30 -0
  167. package/src/emrtd/mrzKey.ts +54 -0
  168. package/src/emrtd/native.ts +79 -0
  169. package/src/emrtd/optionalRead.ts +42 -0
  170. package/src/emrtd/read.ts +224 -0
  171. package/src/emrtd/retry.ts +98 -0
  172. package/src/emrtd/secureMessaging.ts +178 -0
  173. package/src/emrtd/session.ts +200 -0
  174. package/src/emrtd/stages.ts +122 -0
  175. package/src/index.ts +110 -0
  176. package/src/lib/calendar.ts +71 -0
  177. package/src/lib/use-keyboard-inset.ts +34 -0
  178. package/src/liveness/challengeManager.ts +27 -1
  179. package/src/liveness/faceContinuity.ts +153 -0
  180. package/src/liveness/flashDetector.ts +145 -0
  181. package/src/liveness/flashReadyGate.ts +106 -0
  182. package/src/liveness/flashRunner.ts +118 -0
  183. package/src/liveness/integritySignals.ts +60 -0
  184. package/src/liveness/livenessState.ts +139 -0
  185. package/src/liveness/speech.ts +19 -1
  186. package/src/liveness/types.ts +48 -1
  187. package/src/liveness/useFlashPhase.ts +78 -0
  188. package/src/liveness/useLiveness.ts +260 -75
  189. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  190. package/src/mrz/extract.ts +101 -0
  191. package/src/mrz/parse.ts +178 -0
  192. package/src/mrz/textRecognizer.ts +149 -0
  193. package/src/screens/ApplicantRoleStep.tsx +331 -0
  194. package/src/screens/BusinessDetailsStep.tsx +212 -0
  195. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  196. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  197. package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
  198. package/src/screens/BusinessKeyPersonRow.tsx +170 -0
  199. package/src/screens/CompanyInfoFields.tsx +83 -0
  200. package/src/screens/ConsentStep.tsx +68 -68
  201. package/src/screens/ContactChannelChoice.tsx +133 -0
  202. package/src/screens/ContactCodeStep.tsx +130 -0
  203. package/src/screens/ContactDestinationField.tsx +70 -0
  204. package/src/screens/ContactFooterNote.tsx +34 -0
  205. package/src/screens/ContactVerificationStep.tsx +199 -0
  206. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  207. package/src/screens/CountrySelectStep.tsx +210 -0
  208. package/src/screens/DocumentCaptureStep.tsx +156 -132
  209. package/src/screens/IdInputStep.tsx +13 -5
  210. package/src/screens/IdTypeStep.tsx +74 -33
  211. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  212. package/src/screens/LivenessStep.tsx +155 -247
  213. package/src/screens/MrzScanView.tsx +173 -0
  214. package/src/screens/NfcStep.tsx +290 -0
  215. package/src/screens/ProofOfAddressStep.tsx +238 -0
  216. package/src/screens/QuestionnaireField.tsx +219 -0
  217. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  218. package/src/screens/QuestionnaireStep.tsx +76 -0
  219. package/src/screens/SubmittedStep.tsx +5 -0
  220. package/src/screens/consent/model.ts +109 -0
  221. package/src/screens/document/CameraPhase.tsx +150 -0
  222. package/src/screens/document/RequiredPill.tsx +60 -0
  223. package/src/screens/liveness/LightingBanner.tsx +50 -0
  224. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  225. package/src/screens/liveness/ProgressDots.tsx +67 -0
  226. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  227. package/src/screens/liveness/constants.ts +31 -0
  228. package/src/screens/liveness/index.ts +8 -0
  229. package/src/screens/liveness/resolveGuidance.ts +27 -0
  230. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  231. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  232. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  233. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  234. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  235. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  236. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  237. package/src/screens/nfc/availability.ts +73 -0
  238. package/src/screens/nfc/readErrorMessage.ts +93 -0
  239. package/src/screens/nfc/useChipPortrait.ts +58 -0
  240. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  241. package/src/screens/useFlashHole.ts +105 -0
  242. package/src/screens/useFlashSequence.ts +96 -0
  243. package/src/screens/usePoaAttach.ts +121 -0
  244. package/src/services/api-types.ts +164 -0
  245. package/src/services/api-verify-types.ts +99 -0
  246. package/src/services/api.ts +72 -102
  247. package/src/services/cardCrop.ts +25 -10
  248. package/src/services/contactErrors.ts +43 -0
  249. package/src/services/deviceMetadata.ts +1 -1
  250. package/src/services/documentPicker.ts +20 -0
  251. package/src/services/fingerprint-sources.ts +129 -0
  252. package/src/services/fingerprint.ts +101 -0
  253. package/src/services/haptics.ts +38 -0
  254. package/src/services/mediaCompress.ts +7 -2
  255. package/src/services/resolveUrl.ts +2 -2
  256. package/src/services/workflowGate.ts +138 -0
  257. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  258. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  259. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  260. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  261. package/src/store/derive.ts +225 -0
  262. package/src/store/kycStore.ts +250 -149
  263. package/src/store/state.ts +253 -0
  264. package/src/store/submit.ts +101 -0
  265. package/src/store/submitApplicant.ts +81 -0
  266. package/src/types/appearance.ts +113 -0
  267. package/src/types/business.ts +131 -0
  268. package/src/types/config.ts +150 -124
  269. package/src/types/id-types.ts +57 -0
  270. package/src/types/verification.ts +5 -0
  271. package/src/types/workflow.ts +141 -0
  272. package/src/utils/amountFormat.ts +73 -0
  273. package/src/utils/tokens.ts +4 -2
  274. package/src/__tests__/cardCrop.test.ts +0 -39
  275. package/src/__tests__/deviceMetadata.test.ts +0 -34
  276. package/src/__tests__/errors.test.ts +0 -34
  277. package/src/__tests__/flow.test.ts +0 -61
  278. package/src/__tests__/gestureDetector.test.ts +0 -37
  279. package/src/__tests__/liveness.test.ts +0 -112
  280. package/src/__tests__/resolveUrl.test.ts +0 -64
  281. package/src/__tests__/validators.test.ts +0 -38
@@ -0,0 +1,151 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { MyazaButton } from '../components/MyazaButton';
8
+ import { BusinessKeyPersonRow } from './BusinessKeyPersonRow';
9
+ import { keyPeopleMinEntries } from '../config/businessSteps';
10
+ import {
11
+ emptyKeyPerson,
12
+ invalidKeyPersonRows,
13
+ isKeyPersonRowValid,
14
+ MAX_KEY_PEOPLE_ROWS,
15
+ type KeyPersonEntry,
16
+ } from '../config/keyPeople';
17
+ import type { KeyPersonRole } from '../types/business';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Applicant-declared directors, owners and signatories.
21
+ //
22
+ // The registry lookup discovers people too, and the server reconciles the two:
23
+ // someone the registry names who the applicant did NOT list is flagged
24
+ // `undisclosed`, which is a risk signal in its own right. So this screen is not
25
+ // merely data entry — what the applicant chooses to omit is evidence.
26
+ //
27
+ // Layout, copy and per-row design mirror the web SDK's BusinessKeyPeopleStep
28
+ // (and Flutter's screen) 1:1 — the description lives in the step HEADER, the
29
+ // hints are dashed cards, and each person is a bordered card with selects.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export const businessKeyPeopleMeta = {
33
+ title: 'Directors & owners',
34
+ description:
35
+ "List the company's directors and owners of 25% or more. Each will receive a link to verify their identity.",
36
+ };
37
+
38
+ export function BusinessKeyPeopleStep(): React.ReactElement {
39
+ const config = useKycConfig();
40
+ const store = useKycStore();
41
+ const { colors } = useTheme();
42
+ const rows = useKyc((s) => s.businessApplication.keyPeople);
43
+ const registryCountry = useKyc((s) => s.business.country);
44
+
45
+ const minEntries = keyPeopleMinEntries(config.business);
46
+ const validCount = rows.filter(isKeyPersonRowValid).length;
47
+ const invalidRows = invalidKeyPersonRows(rows);
48
+
49
+ // Combined ownership above 100% is factually impossible — catch the typo
50
+ // here rather than shipping it into the registry cross-check as a doomed
51
+ // mismatch. Under 100% is fine (not every owner has to be listed).
52
+ const totalPct = rows.reduce((sum, row) => {
53
+ const n = Number(row.ownershipPct);
54
+ return row.ownershipPct.trim() !== '' && Number.isFinite(n) ? sum + n : sum;
55
+ }, 0);
56
+ const overAllocated = totalPct > 100;
57
+
58
+ const canContinue = validCount >= minEntries && invalidRows.length === 0 && !overAllocated;
59
+
60
+ const update = (index: number, patch: Partial<KeyPersonEntry>): void => {
61
+ store.getState().setKeyPeople(rows.map((row, i) => (i === index ? { ...row, ...patch } : row)));
62
+ };
63
+ // New rows default to the business's registry country (picked on the
64
+ // details step) — most directors are local, and a foreign one just switches
65
+ // theirs. Mirrors the web and Flutter SDKs.
66
+ const defaultCountry = registryCountry ?? config.business?.country ?? '';
67
+ const add = (): void =>
68
+ store.getState().setKeyPeople([...rows, { ...emptyKeyPerson(), country: defaultCountry }]);
69
+ const remove = (index: number): void =>
70
+ store.getState().setKeyPeople(rows.filter((_, i) => i !== index));
71
+
72
+ const handleContinue = (): void => {
73
+ if (!canContinue) return;
74
+ // Half-typed rows are dropped rather than submitted: an entry the user
75
+ // abandoned is not a person they disclosed.
76
+ store.getState().setKeyPeople(rows.filter(isKeyPersonRowValid));
77
+ store.getState().nextStep();
78
+ };
79
+
80
+ // The dashed hint boxes and their copy mirror the web SDK's step 1:1.
81
+ const hint =
82
+ rows.length === 0 && minEntries === 0
83
+ ? "You can skip this if you're unsure — we'll identify directors and owners from the official registry. Adding them here speeds up the review."
84
+ : minEntries > 0 && validCount < minEntries
85
+ ? `List at least ${minEntries} ${minEntries === 1 ? 'person' : 'people'} to continue${
86
+ validCount > 0 ? ` (${validCount} of ${minEntries} added)` : ''
87
+ }.`
88
+ : null;
89
+
90
+ return (
91
+ <View>
92
+ {hint ? (
93
+ <View
94
+ style={{
95
+ borderWidth: 1,
96
+ borderStyle: 'dashed',
97
+ borderColor: colors.border,
98
+ borderRadius: radius.sm,
99
+ backgroundColor: colors.backgroundSecondary,
100
+ padding: spacing.md,
101
+ }}
102
+ >
103
+ <MyazaText variant="bodySmall" color={colors.textSecondary}>
104
+ {hint}
105
+ </MyazaText>
106
+ </View>
107
+ ) : null}
108
+ {overAllocated ? (
109
+ <View
110
+ style={{
111
+ borderWidth: 1,
112
+ borderColor: `${colors.error}4D`,
113
+ borderRadius: radius.sm,
114
+ backgroundColor: colors.errorBg,
115
+ padding: spacing.md,
116
+ marginTop: hint ? spacing.sm : 0,
117
+ }}
118
+ >
119
+ <MyazaText variant="bodySmall" color={colors.error}>
120
+ {`The ownership percentages add up to ${totalPct}% — together they can't exceed 100%.`}
121
+ </MyazaText>
122
+ </View>
123
+ ) : null}
124
+
125
+ {rows.map((row, index) => (
126
+ <BusinessKeyPersonRow
127
+ key={index}
128
+ row={row}
129
+ index={index}
130
+ onChange={(patch) => update(index, patch)}
131
+ onRemove={() => remove(index)}
132
+ uboThreshold={config.business?.keyPeople?.ownershipThreshold}
133
+ />
134
+ ))}
135
+
136
+ <View style={{ height: spacing.md }} />
137
+ {rows.length < MAX_KEY_PEOPLE_ROWS ? (
138
+ <MyazaButton label="Add a person" variant="outline" leadingIcon="user-plus" onPress={add} />
139
+ ) : (
140
+ <MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
141
+ {`You can list up to ${MAX_KEY_PEOPLE_ROWS} people here.`}
142
+ </MyazaText>
143
+ )}
144
+
145
+ <View style={{ height: spacing.sm }} />
146
+ <MyazaButton label="Continue" onPress={handleContinue} disabled={!canContinue} />
147
+ </View>
148
+ );
149
+ }
150
+
151
+ export type { KeyPersonRole };
@@ -0,0 +1,170 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { MyazaInput } from '../components/MyazaInput';
8
+ import { MyazaSelect } from '../components/MyazaSelect';
9
+ import { CountryField } from '../components/CountryField';
10
+ import type { DialCodeOption } from '../components/DialCodePicker';
11
+ import { Icon } from '../components/Icon';
12
+ import { ALL_REGION_CODES, regionCountryName } from '../config/regions';
13
+ import { isValidContactEmail } from '../config/contact';
14
+ import {
15
+ KEY_PERSON_ROLES,
16
+ KEY_PERSON_ROLE_LABELS,
17
+ type KeyPersonEntry,
18
+ } from '../config/keyPeople';
19
+ import type { KeyPersonRole } from '../types/business';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // One editable director/owner row — mirrors the web SDK's BusinessKeyPersonRow
23
+ // and the Flutter row 1:1: bordered card, "PERSON N" header with remove, then
24
+ // full name → role (select) → ownership % → country (full ISO select — a key
25
+ // person's ID can be issued anywhere, not just the registry country) → email.
26
+ // Validation is LIVE per field, like the web: a broken value says so as it is
27
+ // typed, while blank optional fields stay quiet.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /** Whether the row's role is an ownership one (% is only meaningful then). */
31
+ function isOwnerRole(role: KeyPersonRole): boolean {
32
+ return role === 'beneficial_owner' || role === 'shareholder';
33
+ }
34
+
35
+ /** Every ISO-2 we can name, alphabetical — built once, shared by all rows. */
36
+ let countryOptions: DialCodeOption[] | null = null;
37
+ function allCountryOptions(): DialCodeOption[] {
38
+ countryOptions ??= [...ALL_REGION_CODES]
39
+ .map((code) => ({ code, name: regionCountryName(code) }))
40
+ .sort((a, b) => a.name.localeCompare(b.name));
41
+ return countryOptions;
42
+ }
43
+
44
+ export function BusinessKeyPersonRow({
45
+ row,
46
+ index,
47
+ onChange,
48
+ onRemove,
49
+ uboThreshold = 25,
50
+ }: {
51
+ row: KeyPersonEntry;
52
+ index: number;
53
+ onChange: (patch: Partial<KeyPersonEntry>) => void;
54
+ onRemove: () => void;
55
+ /** Ownership % at/above which the server treats a person as a beneficial
56
+ * owner (the workflow's `keyPeople.ownershipThreshold`, default 25). */
57
+ uboThreshold?: number;
58
+ }): React.ReactElement {
59
+ const { colors } = useTheme();
60
+ const options = useMemo(allCountryOptions, []);
61
+
62
+ const nameInvalid = row.name !== '' && row.name.trim().length < 2;
63
+ const emailInvalid = row.email.trim() !== '' && !isValidContactEmail(row.email.trim());
64
+ const pct = row.ownershipPct.trim();
65
+ const pctNum = Number(pct);
66
+ const pctInvalid = pct !== '' && (!Number.isFinite(pctNum) || pctNum < 0 || pctNum > 100);
67
+ // Surface the regulatory consequence as feedback: at/above the threshold the
68
+ // server escalates this person to a beneficial owner regardless of the role
69
+ // picked. Quiet when they already chose UBO — nothing new to say.
70
+ const uboHint =
71
+ !pctInvalid && pct !== '' && pctNum >= uboThreshold && row.role !== 'beneficial_owner';
72
+
73
+ return (
74
+ <View
75
+ style={{
76
+ borderWidth: 1,
77
+ borderColor: colors.border,
78
+ borderRadius: radius.sm,
79
+ padding: spacing.md,
80
+ marginTop: spacing.sm + 4,
81
+ }}
82
+ >
83
+ <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm }}>
84
+ <MyazaText
85
+ variant="bodySmall"
86
+ color={colors.textSecondary}
87
+ style={{ flex: 1, fontWeight: '600', letterSpacing: 0.6 }}
88
+ >
89
+ {`PERSON ${index + 1}`}
90
+ </MyazaText>
91
+ <Pressable
92
+ onPress={onRemove}
93
+ accessibilityRole="button"
94
+ accessibilityLabel={`Remove person ${index + 1}`}
95
+ hitSlop={8}
96
+ >
97
+ <Icon name="close" size={16} color={colors.textMuted} />
98
+ </Pressable>
99
+ </View>
100
+
101
+ <MyazaInput
102
+ label="Full name"
103
+ value={row.name}
104
+ onChangeText={(name) => onChange({ name })}
105
+ placeholder="e.g. Bola Owner"
106
+ // It's a person's name — start every word capitalized.
107
+ autoCapitalize="words"
108
+ error={nameInvalid ? 'Enter the person’s full name.' : null}
109
+ />
110
+
111
+ <View style={{ height: spacing.sm }} />
112
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
113
+ Role
114
+ </MyazaText>
115
+ <MyazaSelect
116
+ value={row.role}
117
+ sheetTitle="Role"
118
+ options={KEY_PERSON_ROLES.map((role) => ({
119
+ value: role,
120
+ label: KEY_PERSON_ROLE_LABELS[role],
121
+ }))}
122
+ onChange={(role) => onChange({ role })}
123
+ />
124
+
125
+ <View style={{ height: spacing.sm }} />
126
+ <MyazaInput
127
+ label="Ownership % (optional)"
128
+ value={row.ownershipPct}
129
+ onChangeText={(ownershipPct) => onChange({ ownershipPct })}
130
+ placeholder={isOwnerRole(row.role) ? 'e.g. 60' : '—'}
131
+ keyboardType="decimal-pad"
132
+ suffix={
133
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
134
+ %
135
+ </MyazaText>
136
+ }
137
+ error={pctInvalid ? 'Enter a value between 0 and 100.' : null}
138
+ helper={
139
+ uboHint
140
+ ? `At ${uboThreshold}% or more, this person counts as a beneficial owner.`
141
+ : undefined
142
+ }
143
+ />
144
+
145
+ <View style={{ height: spacing.sm }} />
146
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
147
+ Country <MyazaText variant="bodySmall" color={colors.textSecondary}>(where their ID was issued)</MyazaText>
148
+ </MyazaText>
149
+ {/* The SAME sheet as the phone field's dial-code picker (keyboard-aware,
150
+ autofocused search, results pinned above the keys) — minus the dial
151
+ codes. Two country pickers that feel different would read as a bug. */}
152
+ <CountryField
153
+ value={row.country || null}
154
+ options={options}
155
+ onChange={(country) => onChange({ country })}
156
+ />
157
+
158
+ <View style={{ height: spacing.sm }} />
159
+ <MyazaInput
160
+ label="Email (optional — used to send their verification link)"
161
+ value={row.email}
162
+ onChangeText={(email) => onChange({ email })}
163
+ placeholder="name@company.com"
164
+ keyboardType="email-address"
165
+ autoCapitalize="none"
166
+ error={emailInvalid ? 'Enter a valid email address.' : null}
167
+ />
168
+ </View>
169
+ );
170
+ }
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { MyazaInput } from '../components/MyazaInput';
8
+ import type { CompanyInfoField, CompanyInfoMode } from '../types/business';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The company profile a KYB workflow asks for.
12
+ //
13
+ // Split from BusinessDetailsStep (200-line rule). Each field's mode comes from
14
+ // the workflow and must match the server's resolution exactly — a field marked
15
+ // optional here but required there produces a 422 the user cannot act on.
16
+ // Section header, captions and placeholders mirror the web SDK's
17
+ // BusinessCompanyInfoFields 1:1.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const COMPANY_FIELD_LABELS: Record<CompanyInfoField, string> = {
21
+ address: 'Registered address',
22
+ email: 'Business email',
23
+ phone: 'Business phone',
24
+ website: 'Website',
25
+ };
26
+
27
+ const COMPANY_FIELD_PLACEHOLDERS: Record<CompanyInfoField, string> = {
28
+ address: 'e.g. 12 Marina Road, Lagos',
29
+ email: 'hello@company.com',
30
+ phone: '+234 800 000 0000',
31
+ website: 'company.com',
32
+ };
33
+
34
+ export function CompanyInfoFields({
35
+ fields,
36
+ modes,
37
+ values,
38
+ missing,
39
+ showErrors,
40
+ onChange,
41
+ }: {
42
+ fields: CompanyInfoField[];
43
+ modes: Record<CompanyInfoField, CompanyInfoMode>;
44
+ values: Record<CompanyInfoField, string>;
45
+ missing: CompanyInfoField[];
46
+ showErrors: boolean;
47
+ onChange: (field: CompanyInfoField, value: string) => void;
48
+ }): React.ReactElement | null {
49
+ const { colors } = useTheme();
50
+ if (fields.length === 0) return null;
51
+
52
+ return (
53
+ <>
54
+ <View style={{ height: spacing.md }} />
55
+ <MyazaText variant="bodyMedium" style={{ fontWeight: '600' }}>
56
+ Company information
57
+ </MyazaText>
58
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
59
+ We verify these details against the official registry record.
60
+ </MyazaText>
61
+ {fields.map((field) => (
62
+ <View key={field}>
63
+ <View style={{ height: spacing.sm }} />
64
+ <MyazaInput
65
+ label={`${COMPANY_FIELD_LABELS[field]}${modes[field] === 'required' ? ' *' : ' (optional)'}`}
66
+ value={values[field]}
67
+ onChangeText={(text) => onChange(field, text)}
68
+ placeholder={COMPANY_FIELD_PLACEHOLDERS[field]}
69
+ keyboardType={
70
+ field === 'email' ? 'email-address' : field === 'phone' ? 'phone-pad' : 'default'
71
+ }
72
+ autoCapitalize="none"
73
+ error={
74
+ showErrors && missing.includes(field)
75
+ ? `${COMPANY_FIELD_LABELS[field]} is required.`
76
+ : null
77
+ }
78
+ />
79
+ </View>
80
+ ))}
81
+ </>
82
+ );
83
+ }
@@ -1,52 +1,31 @@
1
- import React, { useState } from 'react';
2
- import { Pressable, View } from 'react-native';
1
+ import React from 'react';
2
+ import { Linking, Pressable, View } from 'react-native';
3
+ import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
3
4
 
4
5
  import { radius, spacing } from '../config/theme';
6
+ import { PRIVACY_URL, TERMS_URL } from '../config/brand';
5
7
  import { useKyc, useKycConfig, useTheme } from '../components/runtime';
6
8
  import { MyazaText } from '../components/Typography';
7
9
  import { MyazaButton } from '../components/MyazaButton';
8
- import { Icon, type IconName } from '../components/Icon';
9
- import { fillTokens } from '../utils/tokens';
10
+ import { Icon } from '../components/Icon';
11
+ import { buildConsentModel } from './consent/model';
10
12
 
11
- // Consent / welcome screen — 1:1 with the Flutter SDK's ConsentScreen:
12
- // shield hero, token-filled greeting, a "DURING THIS PROCESS WE WILL" card,
13
- // a consent checkbox (must agree), Continue (gated on agreement), lock footer.
14
- // The header title is intentionally empty for this step; the screen owns its hero.
15
-
16
- const DEFAULT_DESCRIPTION =
17
- 'We need to verify your identity to comply with regulatory requirements. This process is quick and secure.';
18
-
19
- interface ProcessStep {
20
- icon: IconName;
21
- label: string;
22
- }
13
+ // Consent / welcome screen — 1:1 with the web SDK's ConsentStep and the
14
+ // Flutter SDK's ConsentScreen: shield hero, token-filled greeting, a "DURING
15
+ // THIS PROCESS WE WILL" card, the derived legal notice, Continue, lock footer.
16
+ // Everything the screen SAYS — title, description, step list, the biometric
17
+ // sentence — is derived in consent/model.ts from what the flow actually does,
18
+ // so a KYB flow gets the business copy and never claims an ID/selfie step it
19
+ // doesn't run. The header title is intentionally empty for this step; the
20
+ // screen owns its hero.
23
21
 
24
22
  export function ConsentStep(): React.ReactElement {
25
23
  const { colors } = useTheme();
26
24
  const config = useKycConfig();
27
25
  const nextStep = useKyc((s) => s.nextStep);
28
- const [agreed, setAgreed] = useState(false);
29
-
30
- const firstName = config.userData?.firstName ?? '';
31
- const title = config.consent?.title
32
- ? fillTokens(config.consent.title, config.userData)
33
- : firstName
34
- ? `Welcome, ${firstName}`
35
- : 'Identity Verification';
36
- const description = config.consent?.description
37
- ? fillTokens(config.consent.description, config.userData)
38
- : DEFAULT_DESCRIPTION;
39
26
 
40
- const steps: ProcessStep[] = [
41
- { icon: 'badge-check', label: 'Verify your government-issued ID' },
42
- { icon: 'user', label: 'Collect basic personal information' },
43
- ...(config.enableDocumentCapture
44
- ? [{ icon: 'scan-line' as IconName, label: 'Capture a photo of your ID document' }]
45
- : []),
46
- ...(config.enableSelfie
47
- ? [{ icon: 'scan-face' as IconName, label: 'Take a selfie for facial verification' }]
48
- : []),
49
- ];
27
+ const { isBusiness, title, description, steps, capturesFace, recordsVideo } =
28
+ buildConsentModel(config);
50
29
 
51
30
  return (
52
31
  <View>
@@ -62,7 +41,6 @@ export function ConsentStep(): React.ReactElement {
62
41
  width: 56,
63
42
  height: 56,
64
43
  borderRadius: 28,
65
- backgroundColor: colors.primary,
66
44
  alignItems: 'center',
67
45
  justifyContent: 'center',
68
46
  shadowColor: colors.primary,
@@ -72,6 +50,22 @@ export function ConsentStep(): React.ReactElement {
72
50
  elevation: 6,
73
51
  }}
74
52
  >
53
+ {/* Top-left → bottom-right gradient badge, matching Flutter. A flat
54
+ fill was the last visual difference on this screen. The second
55
+ stop is the SAME primary at 70% opacity rather than a
56
+ pre-blended colour: composited over the sheet it produces
57
+ Flutter's `alphaBlend(primary@0.7, background)` exactly, and it
58
+ keeps working when the consumer overrides primaryColor or the
59
+ theme flips, which a hardcoded blend would not. */}
60
+ <Svg width={56} height={56} style={{ position: 'absolute' }}>
61
+ <Defs>
62
+ <LinearGradient id="myazaShieldHero" x1="0" y1="0" x2="1" y2="1">
63
+ <Stop offset="0" stopColor={colors.primary} stopOpacity={1} />
64
+ <Stop offset="1" stopColor={colors.primary} stopOpacity={0.7} />
65
+ </LinearGradient>
66
+ </Defs>
67
+ <Circle cx={28} cy={28} r={28} fill="url(#myazaShieldHero)" />
68
+ </Svg>
75
69
  <Icon name="shield" size={28} color={colors.onPrimary} />
76
70
  </View>
77
71
  </View>
@@ -118,46 +112,52 @@ export function ConsentStep(): React.ReactElement {
118
112
  <View style={{ height: spacing.lg }} />
119
113
 
120
114
  {/* Consent checkbox */}
121
- <Pressable
122
- onPress={() => setAgreed((v) => !v)}
123
- style={{
124
- flexDirection: 'row',
125
- alignItems: 'flex-start',
126
- padding: spacing.md,
127
- borderRadius: radius.sm,
128
- backgroundColor: agreed ? `${colors.primary}0F` : colors.backgroundSecondary,
129
- }}
130
- >
131
- <View
132
- style={{
133
- width: 22,
134
- height: 22,
135
- borderRadius: radius.xs - 2,
136
- backgroundColor: agreed ? colors.primary : 'transparent',
137
- borderWidth: 1.5,
138
- borderColor: agreed ? colors.primary : colors.gray400,
139
- alignItems: 'center',
140
- justifyContent: 'center',
141
- marginTop: 1,
142
- }}
115
+ {/* Consent is given by ACTING now, so the notice sits immediately above
116
+ the button it describes — adjacency is what makes it informed.
117
+ The biometric sentence is DERIVED: claiming facial recognition on a
118
+ flow with no selfie step would be false, and recording video without
119
+ saying so is the failure that actually matters. */}
120
+ <MyazaText variant="bodySmall" style={{ lineHeight: 18 }}>
121
+ By tapping Continue, you agree to the{' '}
122
+ <MyazaText
123
+ variant="bodySmall"
124
+ color={colors.textDark}
125
+ style={{ fontWeight: '600', textDecorationLine: 'underline' }}
126
+ onPress={() => void Linking.openURL(TERMS_URL).catch(() => undefined)}
143
127
  >
144
- {agreed ? <Icon name="check" size={14} color={colors.onPrimary} /> : null}
145
- </View>
146
- <View style={{ width: spacing.sm + 2 }} />
147
- <MyazaText variant="bodyMedium" color={colors.textDark} style={{ flex: 1, fontWeight: '600' }}>
148
- I consent to the collection and processing of my personal data for identity verification purposes.
128
+ End User Terms
129
+ </MyazaText>{' '}
130
+ and{' '}
131
+ <MyazaText
132
+ variant="bodySmall"
133
+ color={colors.textDark}
134
+ style={{ fontWeight: '600', textDecorationLine: 'underline' }}
135
+ onPress={() => void Linking.openURL(PRIVACY_URL).catch(() => undefined)}
136
+ >
137
+ Privacy Policy
149
138
  </MyazaText>
150
- </Pressable>
139
+ , and consent to your {isBusiness ? 'business and personal data' : 'personal data'} being
140
+ processed to verify your identity.
141
+ {capturesFace
142
+ ? ' This includes facial recognition and recording this session.'
143
+ : recordsVideo
144
+ ? ' This includes recording this session.'
145
+ : ''}
146
+ </MyazaText>
151
147
 
152
148
  <View style={{ height: spacing.lg }} />
153
- <MyazaButton label="Continue" onPress={agreed ? nextStep : undefined} disabled={!agreed} />
149
+ <MyazaButton label="Continue" onPress={nextStep} />
154
150
  <View style={{ height: spacing.sm }} />
155
151
 
156
152
  {/* Footer */}
157
153
  <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
158
154
  <Icon name="lock" size={13} color={colors.textMuted} />
159
155
  <View style={{ width: 6 }} />
160
- <MyazaText variant="bodySmall">Your data is encrypted and securely processed</MyazaText>
156
+ {/* flexShrink so a wide brand font wraps this instead of clipping it —
157
+ a Text in a row without it overflows rather than wrapping. */}
158
+ <MyazaText variant="bodySmall" style={{ flexShrink: 1 }}>
159
+ Your data is encrypted and securely processed
160
+ </MyazaText>
161
161
  </View>
162
162
  </View>
163
163
  );