@myazahq/kyc-sdk-react-native 2.0.2 → 2.2.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 (283) hide show
  1. package/README.md +152 -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 +70 -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 +162 -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 +322 -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 +207 -0
  198. package/src/screens/CompanyInfoFields.tsx +83 -0
  199. package/src/screens/ConsentStep.tsx +68 -68
  200. package/src/screens/ContactChannelChoice.tsx +133 -0
  201. package/src/screens/ContactCodeStep.tsx +130 -0
  202. package/src/screens/ContactDestinationField.tsx +70 -0
  203. package/src/screens/ContactFooterNote.tsx +34 -0
  204. package/src/screens/ContactVerificationStep.tsx +199 -0
  205. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  206. package/src/screens/CountrySelectStep.tsx +210 -0
  207. package/src/screens/DocumentCaptureStep.tsx +156 -132
  208. package/src/screens/IdInputStep.tsx +13 -5
  209. package/src/screens/IdTypeStep.tsx +74 -33
  210. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  211. package/src/screens/KeyPersonCard.tsx +119 -0
  212. package/src/screens/KeyPersonForm.tsx +148 -0
  213. package/src/screens/KeyPersonSheet.tsx +184 -0
  214. package/src/screens/LivenessStep.tsx +155 -247
  215. package/src/screens/MrzScanView.tsx +173 -0
  216. package/src/screens/NfcStep.tsx +290 -0
  217. package/src/screens/ProofOfAddressStep.tsx +238 -0
  218. package/src/screens/QuestionnaireField.tsx +219 -0
  219. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  220. package/src/screens/QuestionnaireStep.tsx +76 -0
  221. package/src/screens/SubmittedStep.tsx +5 -0
  222. package/src/screens/consent/model.ts +109 -0
  223. package/src/screens/document/CameraPhase.tsx +150 -0
  224. package/src/screens/document/RequiredPill.tsx +60 -0
  225. package/src/screens/liveness/LightingBanner.tsx +50 -0
  226. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  227. package/src/screens/liveness/ProgressDots.tsx +67 -0
  228. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  229. package/src/screens/liveness/constants.ts +31 -0
  230. package/src/screens/liveness/index.ts +8 -0
  231. package/src/screens/liveness/resolveGuidance.ts +27 -0
  232. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  233. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  234. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  235. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  236. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  237. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  238. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  239. package/src/screens/nfc/availability.ts +73 -0
  240. package/src/screens/nfc/readErrorMessage.ts +93 -0
  241. package/src/screens/nfc/useChipPortrait.ts +58 -0
  242. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  243. package/src/screens/useFlashHole.ts +105 -0
  244. package/src/screens/useFlashSequence.ts +96 -0
  245. package/src/screens/usePoaAttach.ts +121 -0
  246. package/src/services/api-types.ts +164 -0
  247. package/src/services/api-verify-types.ts +99 -0
  248. package/src/services/api.ts +72 -102
  249. package/src/services/cardCrop.ts +25 -10
  250. package/src/services/contactErrors.ts +43 -0
  251. package/src/services/deviceMetadata.ts +1 -1
  252. package/src/services/documentPicker.ts +20 -0
  253. package/src/services/fingerprint-sources.ts +129 -0
  254. package/src/services/fingerprint.ts +101 -0
  255. package/src/services/haptics.ts +38 -0
  256. package/src/services/mediaCompress.ts +7 -2
  257. package/src/services/resolveUrl.ts +2 -2
  258. package/src/services/workflowGate.ts +138 -0
  259. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  260. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  261. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  262. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  263. package/src/store/derive.ts +225 -0
  264. package/src/store/kycStore.ts +250 -149
  265. package/src/store/state.ts +253 -0
  266. package/src/store/submit.ts +101 -0
  267. package/src/store/submitApplicant.ts +81 -0
  268. package/src/types/appearance.ts +113 -0
  269. package/src/types/business.ts +131 -0
  270. package/src/types/config.ts +150 -124
  271. package/src/types/id-types.ts +57 -0
  272. package/src/types/verification.ts +5 -0
  273. package/src/types/workflow.ts +141 -0
  274. package/src/utils/amountFormat.ts +73 -0
  275. package/src/utils/tokens.ts +4 -2
  276. package/src/__tests__/cardCrop.test.ts +0 -39
  277. package/src/__tests__/deviceMetadata.test.ts +0 -34
  278. package/src/__tests__/errors.test.ts +0 -34
  279. package/src/__tests__/flow.test.ts +0 -61
  280. package/src/__tests__/gestureDetector.test.ts +0 -37
  281. package/src/__tests__/liveness.test.ts +0 -112
  282. package/src/__tests__/resolveUrl.test.ts +0 -64
  283. package/src/__tests__/validators.test.ts +0 -38
@@ -0,0 +1,207 @@
1
+ import React, { useState } 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 { KeyPersonCard } from './KeyPersonCard';
9
+ import { KeyPersonSheet } from './KeyPersonSheet';
10
+ import { keyPeopleMinEntries } from '../config/businessSteps';
11
+ import {
12
+ emptyKeyPerson,
13
+ invalidKeyPersonRows,
14
+ isKeyPersonRowBlank,
15
+ isKeyPersonRowValid,
16
+ MAX_KEY_PEOPLE_ROWS,
17
+ type KeyPersonEntry,
18
+ } from '../config/keyPeople';
19
+ import type { KeyPersonRole } from '../types/business';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Applicant-declared directors, owners and signatories.
23
+ //
24
+ // The registry lookup discovers people too, and the server reconciles the two:
25
+ // someone the registry names who the applicant did NOT list is flagged
26
+ // `undisclosed`, which is a risk signal in its own right. So this screen is not
27
+ // merely data entry — what the applicant chooses to omit is evidence.
28
+ //
29
+ // The list stays a clean stack of compact summary cards; the FORM lives in the
30
+ // add/edit sheet (KeyPersonSheet). Tapping a card edits it; "Add a person"
31
+ // opens a fresh sheet. Removal is inside the edit sheet, behind a deliberate
32
+ // tap — never one stray touch on the list.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export const businessKeyPeopleMeta = {
36
+ title: 'Directors & owners',
37
+ description:
38
+ "List the company's directors and owners of 25% or more. Each will receive a link to verify their identity.",
39
+ };
40
+
41
+ type SheetState = { mode: 'add' } | { mode: 'edit'; index: number } | null;
42
+
43
+ export function BusinessKeyPeopleStep(): React.ReactElement {
44
+ const config = useKycConfig();
45
+ const store = useKycStore();
46
+ const { colors } = useTheme();
47
+ const rows = useKyc((s) => s.businessApplication.keyPeople);
48
+ const registryCountry = useKyc((s) => s.business.country);
49
+ const [sheet, setSheet] = useState<SheetState>(null);
50
+
51
+ const minEntries = keyPeopleMinEntries(config.business);
52
+ const validCount = rows.filter(isKeyPersonRowValid).length;
53
+ const invalidRows = invalidKeyPersonRows(rows);
54
+ const uboThreshold = config.business?.keyPeople?.ownershipThreshold ?? 25;
55
+
56
+ // Combined ownership above 100% is factually impossible — catch the typo
57
+ // here rather than shipping it into the registry cross-check as a doomed
58
+ // mismatch. Under 100% is fine (not every owner has to be listed).
59
+ const pctOf = (row: KeyPersonEntry): number => {
60
+ const n = Number(row.ownershipPct);
61
+ return row.ownershipPct.trim() !== '' && Number.isFinite(n) ? n : 0;
62
+ };
63
+ const totalPct = rows.reduce((sum, row) => sum + pctOf(row), 0);
64
+ const overAllocated = totalPct > 100;
65
+ const fmtPct = (n: number): string => (Number.isInteger(n) ? String(n) : n.toFixed(1));
66
+
67
+ const canContinue = validCount >= minEntries && invalidRows.length === 0 && !overAllocated;
68
+
69
+ // New people default to the business's registry country (picked on the
70
+ // details step) — most directors are local, and a foreign one just switches
71
+ // theirs. Mirrors the web and Flutter SDKs.
72
+ const defaultCountry = registryCountry ?? config.business?.country ?? '';
73
+
74
+ const commit = (next: KeyPersonEntry[]): void => store.getState().setKeyPeople(next);
75
+ const handleSave = (entry: KeyPersonEntry): void => {
76
+ if (sheet?.mode === 'edit') {
77
+ commit(rows.map((row, i) => (i === sheet.index ? entry : row)));
78
+ } else {
79
+ commit([...rows, entry]);
80
+ }
81
+ setSheet(null);
82
+ };
83
+ const handleRemove = (): void => {
84
+ if (sheet?.mode === 'edit') commit(rows.filter((_, i) => i !== sheet.index));
85
+ setSheet(null);
86
+ };
87
+
88
+ // Indexed access may be undefined under strict indexing — resolved once here
89
+ // so the sheet only ever mounts with a real entry.
90
+ const editEntry = sheet?.mode === 'edit' ? rows[sheet.index] : undefined;
91
+
92
+ const handleContinue = (): void => {
93
+ if (!canContinue) return;
94
+ // Half-typed rows are dropped rather than submitted: an entry the user
95
+ // abandoned is not a person they disclosed.
96
+ commit(rows.filter(isKeyPersonRowValid));
97
+ store.getState().nextStep();
98
+ };
99
+
100
+ // The dashed hint boxes and their copy mirror the web SDK's step 1:1.
101
+ const hint =
102
+ rows.length === 0 && minEntries === 0
103
+ ? "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."
104
+ : minEntries > 0 && validCount < minEntries
105
+ ? `List at least ${minEntries} ${minEntries === 1 ? 'person' : 'people'} to continue${
106
+ validCount > 0 ? ` (${validCount} of ${minEntries} added)` : ''
107
+ }.`
108
+ : null;
109
+
110
+ return (
111
+ <View>
112
+ {hint ? (
113
+ <View
114
+ style={{
115
+ borderWidth: 1,
116
+ borderStyle: 'dashed',
117
+ borderColor: colors.border,
118
+ borderRadius: radius.sm,
119
+ backgroundColor: colors.backgroundSecondary,
120
+ padding: spacing.md,
121
+ marginBottom: spacing.md,
122
+ }}
123
+ >
124
+ <MyazaText variant="bodySmall" color={colors.textSecondary}>
125
+ {hint}
126
+ </MyazaText>
127
+ </View>
128
+ ) : null}
129
+
130
+ {rows.map((row, index) =>
131
+ isKeyPersonRowBlank(row) ? null : (
132
+ <KeyPersonCard key={index} entry={row} onPress={() => setSheet({ mode: 'edit', index })} />
133
+ ),
134
+ )}
135
+ {/* The cards and the add affordance are different things — give the
136
+ boundary some air (cards already carry a small bottom margin). */}
137
+ {rows.some((row) => !isKeyPersonRowBlank(row)) ? (
138
+ <View style={{ height: spacing.sm }} />
139
+ ) : null}
140
+
141
+ {rows.length < MAX_KEY_PEOPLE_ROWS ? (
142
+ <MyazaButton
143
+ label="Add a person"
144
+ variant="outline"
145
+ leadingIcon="user-plus"
146
+ onPress={() => setSheet({ mode: 'add' })}
147
+ />
148
+ ) : (
149
+ <MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
150
+ {`You can list up to ${MAX_KEY_PEOPLE_ROWS} people here.`}
151
+ </MyazaText>
152
+ )}
153
+ {/* Breathing room between the add-person affordance and the summary bar —
154
+ matches the Flutter screen's SizedBox after its outline button. */}
155
+ <View style={{ height: spacing.md }} />
156
+
157
+ {/* Total-ownership summary at the DECISION point — the disabled Continue
158
+ button always explains itself, wherever the offending card is. */}
159
+ {totalPct > 0 ? (
160
+ <View
161
+ style={{
162
+ flexDirection: 'row',
163
+ justifyContent: 'space-between',
164
+ alignItems: 'center',
165
+ backgroundColor: overAllocated ? colors.errorBg : colors.backgroundSecondary,
166
+ borderRadius: radius.sm,
167
+ paddingHorizontal: spacing.md,
168
+ paddingVertical: spacing.sm + 4,
169
+ }}
170
+ >
171
+ <MyazaText variant="bodySmall" color={overAllocated ? colors.error : colors.textMuted}>
172
+ Total ownership listed
173
+ </MyazaText>
174
+ <MyazaText
175
+ variant="bodySmall"
176
+ color={overAllocated ? colors.error : colors.textDark}
177
+ style={{ fontWeight: '700' }}
178
+ >
179
+ {`${fmtPct(totalPct)}%`}
180
+ </MyazaText>
181
+ </View>
182
+ ) : null}
183
+ {overAllocated ? (
184
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
185
+ {`Together the percentages can't exceed 100% — reduce them by ${fmtPct(totalPct - 100)}%.`}
186
+ </MyazaText>
187
+ ) : null}
188
+
189
+ <View style={{ height: spacing.md }} />
190
+ <MyazaButton label="Continue" onPress={handleContinue} disabled={!canContinue} />
191
+
192
+ {sheet && (sheet.mode === 'add' || editEntry) ? (
193
+ <KeyPersonSheet
194
+ mode={sheet.mode}
195
+ initial={editEntry ?? { ...emptyKeyPerson(), country: defaultCountry }}
196
+ uboThreshold={uboThreshold}
197
+ otherPctTotal={editEntry ? totalPct - pctOf(editEntry) : totalPct}
198
+ onSave={handleSave}
199
+ onRemove={sheet.mode === 'edit' ? handleRemove : undefined}
200
+ onClose={() => setSheet(null)}
201
+ />
202
+ ) : null}
203
+ </View>
204
+ );
205
+ }
206
+
207
+ export type { KeyPersonRole };
@@ -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
  );
@@ -0,0 +1,133 @@
1
+ import React 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 { Icon } from '../components/Icon';
8
+ import { WhatsAppIcon } from '../components/WhatsAppIcon';
9
+ import { CHANNEL_LABELS, type PhoneOtpChannel } from '../config/contact';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // How the user wants their one-time code delivered.
13
+ //
14
+ // The org decides which channels are ON OFFER; the person receiving the code
15
+ // decides between them, because only they know whether they have WhatsApp
16
+ // installed or whether SMS is landing for them today. With a single offered
17
+ // channel there is no choice to make and this renders nothing.
18
+ //
19
+ // Card shape, icon and selected treatment mirror the web SDK's picker so the
20
+ // step reads the same on every platform.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const HINTS: Record<PhoneOtpChannel, string> = {
24
+ sms: 'Text message',
25
+ whatsapp: 'Needs WhatsApp',
26
+ };
27
+
28
+ /** WhatsApp needs its own brand mark; SMS reads fine as a generic glyph. */
29
+ function ChannelGlyph({
30
+ channel,
31
+ color,
32
+ }: {
33
+ channel: PhoneOtpChannel;
34
+ color: string;
35
+ }): React.ReactElement {
36
+ return channel === 'whatsapp' ? (
37
+ <WhatsAppIcon size={16} color={color} />
38
+ ) : (
39
+ <Icon name="message-square" size={16} color={color} />
40
+ );
41
+ }
42
+
43
+ export function ContactChannelChoice({
44
+ offered,
45
+ picked,
46
+ disabled,
47
+ onPick,
48
+ }: {
49
+ offered: PhoneOtpChannel[];
50
+ picked: PhoneOtpChannel;
51
+ disabled?: boolean;
52
+ onPick: (channel: PhoneOtpChannel) => void;
53
+ }): React.ReactElement | null {
54
+ const { colors } = useTheme();
55
+ if (offered.length < 2) return null;
56
+
57
+ return (
58
+ <View>
59
+ <View style={{ height: spacing.md }} />
60
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
61
+ How should we send it?
62
+ </MyazaText>
63
+ <View style={{ height: spacing.xs }} />
64
+ <View style={{ flexDirection: 'row', gap: spacing.sm }}>
65
+ {offered.map((channel) => (
66
+ <ChannelTile
67
+ key={channel}
68
+ channel={channel}
69
+ selected={picked === channel}
70
+ disabled={disabled}
71
+ onPress={() => onPick(channel)}
72
+ />
73
+ ))}
74
+ </View>
75
+ </View>
76
+ );
77
+ }
78
+
79
+ function ChannelTile({
80
+ channel,
81
+ selected,
82
+ disabled,
83
+ onPress,
84
+ }: {
85
+ channel: PhoneOtpChannel;
86
+ selected: boolean;
87
+ disabled?: boolean;
88
+ onPress: () => void;
89
+ }): React.ReactElement {
90
+ const { colors } = useTheme();
91
+
92
+ return (
93
+ <Pressable
94
+ onPress={disabled ? undefined : onPress}
95
+ accessibilityRole="radio"
96
+ accessibilityState={{ checked: selected, disabled }}
97
+ accessibilityLabel={CHANNEL_LABELS[channel]}
98
+ style={{
99
+ flex: 1,
100
+ flexDirection: 'row',
101
+ alignItems: 'center',
102
+ gap: spacing.sm,
103
+ paddingHorizontal: spacing.sm,
104
+ paddingVertical: spacing.sm + 2,
105
+ borderRadius: radius.md,
106
+ borderWidth: 1.5,
107
+ borderColor: selected ? colors.primary : colors.border,
108
+ backgroundColor: selected ? `${colors.primary}0D` : 'transparent',
109
+ }}
110
+ >
111
+ <View
112
+ style={{
113
+ width: 34,
114
+ height: 34,
115
+ borderRadius: radius.sm,
116
+ alignItems: 'center',
117
+ justifyContent: 'center',
118
+ backgroundColor: selected ? `${colors.primary}1A` : colors.backgroundSecondary,
119
+ }}
120
+ >
121
+ <ChannelGlyph channel={channel} color={selected ? colors.primary : colors.textMuted} />
122
+ </View>
123
+ <View style={{ flex: 1, minWidth: 0 }}>
124
+ <MyazaText variant="bodyMedium" style={{ fontWeight: "600" }}>
125
+ {CHANNEL_LABELS[channel]}
126
+ </MyazaText>
127
+ <MyazaText variant="bodySmall" color={colors.textMuted} numberOfLines={1}>
128
+ {HINTS[channel]}
129
+ </MyazaText>
130
+ </View>
131
+ </Pressable>
132
+ );
133
+ }