@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,238 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { ActivityIndicator, Image, Pressable, 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 { Icon } from '../components/Icon';
9
+ import { MediaSourceSheet } from '../components/MediaSourceSheet';
10
+ import { MyazaSelect } from '../components/MyazaSelect';
11
+ import { withRetry } from '../services/retry';
12
+ import { compressDocumentImage } from '../services/mediaCompress';
13
+ import { poaDocumentTypes, poaMaxAgeDays, poaTypeLabel } from '../config/proofOfAddress';
14
+ import { usePoaAttach, type PoaPick } from './usePoaAttach';
15
+ import type { PoaDocumentType } from '../types/workflow';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Proof of Address — pick the kind of document, then supply it.
19
+ //
20
+ // Mirrors the web SDK: a DASHED drop zone that NAMES the document being asked
21
+ // for ("Upload your utility bill") plus what's accepted, replaced after upload
22
+ // by a row showing the file, its kind, and an X to remove it. A generic "choose
23
+ // a file" hid which of the offered document kinds the user had to supply.
24
+ //
25
+ // The check itself is soft — the server reads the document and reports a
26
+ // verdict, but never fails the verification over it — so nothing here blocks
27
+ // beyond making sure a file was actually attached.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export function proofOfAddressMeta(maxAgeDays: number): { title: string; description: string } {
31
+ return {
32
+ title: 'Proof of address',
33
+ description: `Upload a document that shows your name and home address, issued within the last ${maxAgeDays} days.`,
34
+ };
35
+ }
36
+
37
+ export function ProofOfAddressStep(): React.ReactElement {
38
+ const config = useKycConfig();
39
+ const store = useKycStore();
40
+ const { colors } = useTheme();
41
+ const mediaId = useKyc((s) => s.mediaIds.proofOfAddress);
42
+ const fileName = useKyc((s) => s.poaFileName);
43
+ const storedType = useKyc((s) => s.poaDocumentType);
44
+
45
+ const types = poaDocumentTypes(config.proofOfAddress);
46
+ const [selectedType, setSelectedType] = useState<PoaDocumentType>(storedType ?? types[0]!);
47
+ const [busy, setBusy] = useState(false);
48
+ const [error, setError] = useState<string | null>(null);
49
+ // The picked file, kept for the thumbnail — the only way a user catches
50
+ // "wrong photo from the camera roll" before submitting.
51
+ const [preview, setPreview] = useState<{ uri: string; isPdf: boolean } | null>(null);
52
+
53
+ const typeLabel = poaTypeLabel(selectedType, config.proofOfAddress);
54
+
55
+ const upload = useCallback(
56
+ async ({ uri, mimeType, name }: PoaPick) => {
57
+ setBusy(true);
58
+ setError(null);
59
+ const isPdf = (mimeType ?? '').toLowerCase().startsWith('application/pdf');
60
+ setPreview({ uri, isPdf });
61
+ try {
62
+ // Photos are compressed like any other capture; a PDF is passed through
63
+ // untouched — re-encoding it would destroy the text the server reads.
64
+ const finalUri = isPdf ? uri : await compressDocumentImage(uri).catch(() => uri);
65
+ const id = await withRetry(() =>
66
+ store.getState().api.upload({ uri: finalUri, type: mimeType, name }, 'proof_of_address'),
67
+ );
68
+ store.getState().setProofOfAddress(id, selectedType, name);
69
+ } catch {
70
+ setPreview(null);
71
+ setError('We could not upload that document. Please try again.');
72
+ } finally {
73
+ setBusy(false);
74
+ }
75
+ },
76
+ [selectedType, store],
77
+ );
78
+
79
+ const { pick, sheetOpen, closeSheet, choosePhoto, takePhoto, chooseFile } = usePoaAttach(
80
+ upload,
81
+ setError,
82
+ );
83
+
84
+ const remove = useCallback(() => {
85
+ setPreview(null);
86
+ setError(null);
87
+ store.getState().clearProofOfAddress();
88
+ }, [store]);
89
+
90
+ const uploaded = Boolean(mediaId) && !busy;
91
+
92
+ return (
93
+ <View>
94
+ <MediaSourceSheet
95
+ open={sheetOpen}
96
+ onClose={closeSheet}
97
+ title="Upload your document"
98
+ options={[
99
+ {
100
+ icon: 'image',
101
+ label: 'Photo library',
102
+ caption: 'Pick a photo you already have',
103
+ onPress: () => void choosePhoto(),
104
+ },
105
+ {
106
+ icon: 'camera',
107
+ label: 'Take a photo',
108
+ caption: 'Photograph the document now',
109
+ onPress: () => void takePhoto(),
110
+ },
111
+ {
112
+ icon: 'file-text',
113
+ label: 'Choose a file',
114
+ caption: 'A PDF or image from your files',
115
+ onPress: () => void chooseFile(),
116
+ },
117
+ ]}
118
+ />
119
+ {types.length > 1 ? (
120
+ <>
121
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
122
+ Document type
123
+ </MyazaText>
124
+ <MyazaSelect<PoaDocumentType>
125
+ value={selectedType}
126
+ sheetTitle="Document type"
127
+ // Locked once a file is attached: switching the kind afterwards
128
+ // would mislabel the document already uploaded.
129
+ enabled={!uploaded && !busy}
130
+ options={types.map((type) => ({
131
+ value: type,
132
+ label: poaTypeLabel(type, config.proofOfAddress),
133
+ }))}
134
+ onChange={setSelectedType}
135
+ />
136
+ <View style={{ height: spacing.lg }} />
137
+ </>
138
+ ) : null}
139
+
140
+ {uploaded ? (
141
+ <View
142
+ style={{
143
+ flexDirection: 'row',
144
+ alignItems: 'center',
145
+ padding: spacing.md,
146
+ borderRadius: radius.md,
147
+ borderWidth: 1,
148
+ borderColor: colors.border,
149
+ backgroundColor: colors.backgroundSecondary,
150
+ }}
151
+ >
152
+ {preview && !preview.isPdf ? (
153
+ <Image
154
+ source={{ uri: preview.uri }}
155
+ style={{ width: 48, height: 48, borderRadius: radius.sm }}
156
+ resizeMode="cover"
157
+ />
158
+ ) : (
159
+ <View
160
+ style={{
161
+ width: 48,
162
+ height: 48,
163
+ borderRadius: radius.sm,
164
+ borderWidth: 1,
165
+ borderColor: colors.border,
166
+ alignItems: 'center',
167
+ justifyContent: 'center',
168
+ }}
169
+ >
170
+ <Icon name="file-text" size={22} color={colors.primary} />
171
+ </View>
172
+ )}
173
+ <View style={{ width: spacing.md, flexShrink: 0 }} />
174
+ <View style={{ flex: 1, minWidth: 0 }}>
175
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600' }} numberOfLines={1}>
176
+ {fileName ?? 'Document uploaded'}
177
+ </MyazaText>
178
+ <MyazaText variant="bodySmall" color={colors.textSecondary} numberOfLines={1}>
179
+ {typeLabel}
180
+ </MyazaText>
181
+ </View>
182
+ <Pressable
183
+ onPress={remove}
184
+ accessibilityRole="button"
185
+ accessibilityLabel="Remove document"
186
+ hitSlop={8}
187
+ >
188
+ <Icon name="x" size={18} color={colors.textSecondary} />
189
+ </Pressable>
190
+ </View>
191
+ ) : (
192
+ <Pressable
193
+ onPress={() => {
194
+ if (!busy) void pick();
195
+ }}
196
+ accessibilityRole="button"
197
+ accessibilityLabel={`Upload your ${typeLabel.toLowerCase()}`}
198
+ style={{
199
+ alignItems: 'center',
200
+ justifyContent: 'center',
201
+ paddingVertical: spacing.xl,
202
+ paddingHorizontal: spacing.lg,
203
+ borderRadius: radius.md,
204
+ borderWidth: 1.5,
205
+ borderStyle: 'dashed',
206
+ borderColor: colors.border,
207
+ }}
208
+ >
209
+ {busy ? (
210
+ <ActivityIndicator size="small" color={colors.primary} />
211
+ ) : (
212
+ <Icon name="upload" size={30} color={colors.textSecondary} />
213
+ )}
214
+ <View style={{ height: spacing.sm }} />
215
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
216
+ {busy ? 'Uploading…' : `Upload your ${typeLabel.toLowerCase()}`}
217
+ </MyazaText>
218
+ <MyazaText variant="bodySmall" color={colors.textSecondary}>
219
+ Photo or PDF, up to 20MB
220
+ </MyazaText>
221
+ </Pressable>
222
+ )}
223
+
224
+ {error ? (
225
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
226
+ {error}
227
+ </MyazaText>
228
+ ) : null}
229
+
230
+ <View style={{ height: spacing.lg }} />
231
+ <MyazaButton
232
+ label="Continue"
233
+ disabled={!mediaId || busy}
234
+ onPress={() => store.getState().nextStep()}
235
+ />
236
+ </View>
237
+ );
238
+ }
@@ -0,0 +1,219 @@
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 { MyazaDateField } from '../components/MyazaDateField';
8
+ import { MyazaInput } from '../components/MyazaInput';
9
+ import { MyazaSelect } from '../components/MyazaSelect';
10
+ import { OptionRow } from '../components/OptionRow';
11
+ import { MoneyField as QuestionnaireMoneyField } from './QuestionnaireMoneyField';
12
+ import { currencyKeyFor } from '../config/questionnaire';
13
+ import type { QuestionnaireAnswerValue, QuestionnaireField as FieldDef } from '../types/workflow';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // One questionnaire question.
17
+ //
18
+ // Mirrors the Flutter SDK field-for-field, because a questionnaire is authored
19
+ // once and rendered on every platform — a question that looks like a different
20
+ // control per platform reads as a different question:
21
+ //
22
+ // label the question, with a RED * when required (not "(optional)" on
23
+ // the ones that aren't — most questions are required, so marking
24
+ // the exception is quieter than marking the rule)
25
+ // select a select field that opens a sheet
26
+ // multiselect tappable cards with square checks (the choices are the point)
27
+ // boolean two centred Yes / No cards
28
+ // money amount + currency on ONE row
29
+ // text/number a plain input
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export function QuestionnaireFieldView({
33
+ field,
34
+ value,
35
+ currencyValue,
36
+ error,
37
+ onChange,
38
+ onCurrencyChange,
39
+ }: {
40
+ field: FieldDef;
41
+ value: QuestionnaireAnswerValue | undefined;
42
+ /** money only: the `<key>_currency` companion answer. */
43
+ currencyValue?: string;
44
+ error?: string;
45
+ onChange: (value: QuestionnaireAnswerValue | undefined) => void;
46
+ onCurrencyChange: (currency: string) => void;
47
+ }): React.ReactElement {
48
+ const { colors } = useTheme();
49
+
50
+ const isPlainInput = field.type === 'text' || field.type === 'number';
51
+
52
+ return (
53
+ <View style={{ marginBottom: spacing.lg }}>
54
+ <FieldLabel
55
+ text={field.label}
56
+ required={field.required === true}
57
+ helpText={
58
+ field.helpText ??
59
+ (field.type === 'multiselect' ? 'Choose all that apply.' : undefined)
60
+ }
61
+ />
62
+
63
+ {isPlainInput ? (
64
+ <MyazaInput
65
+ value={value === undefined ? '' : String(value)}
66
+ onChangeText={(text) => onChange(text === '' ? undefined : text)}
67
+ placeholder={field.placeholder}
68
+ error={error ?? null}
69
+ keyboardType={field.type === 'number' ? 'numeric' : 'default'}
70
+ autoCapitalize="none"
71
+ />
72
+ ) : null}
73
+
74
+ {field.type === 'date' ? (
75
+ // A real calendar, not a "type YYYY-MM-DD" text box — same picker
76
+ // contract as the Flutter SDK's _DateField / showMyazaDatePicker.
77
+ <MyazaDateField
78
+ value={typeof value === 'string' ? value : undefined}
79
+ onChange={(iso) => onChange(iso)}
80
+ placeholder={field.placeholder}
81
+ error={error ?? null}
82
+ />
83
+ ) : null}
84
+
85
+ {field.type === 'money' ? (
86
+ <QuestionnaireMoneyField
87
+ field={field}
88
+ value={value}
89
+ currencyValue={currencyValue}
90
+ error={error}
91
+ onChange={onChange}
92
+ onCurrencyChange={onCurrencyChange}
93
+ />
94
+ ) : null}
95
+
96
+ {field.type === 'boolean' ? (
97
+ <View style={{ flexDirection: 'row', gap: spacing.sm }}>
98
+ {[true, false].map((option) => (
99
+ <View key={String(option)} style={{ flex: 1 }}>
100
+ <BooleanChoice
101
+ label={option ? 'Yes' : 'No'}
102
+ selected={value === option}
103
+ // Re-tapping the chosen answer clears it — the only way to
104
+ // un-answer an optional yes/no.
105
+ onPress={() => onChange(value === option ? undefined : option)}
106
+ />
107
+ </View>
108
+ ))}
109
+ </View>
110
+ ) : null}
111
+
112
+ {field.type === 'select' ? (
113
+ <MyazaSelect<string>
114
+ value={typeof value === 'string' ? value : null}
115
+ hint={field.placeholder ?? 'Select an option'}
116
+ sheetTitle={field.label}
117
+ options={(field.options ?? []).map((option) => ({
118
+ value: option.value,
119
+ label: option.label,
120
+ }))}
121
+ onChange={(next) => onChange(next)}
122
+ />
123
+ ) : null}
124
+
125
+ {field.type === 'multiselect'
126
+ ? (field.options ?? []).map((option) => {
127
+ const selected = Array.isArray(value) && value.includes(option.value);
128
+ return (
129
+ <OptionRow
130
+ key={option.value}
131
+ label={option.label}
132
+ selected={selected}
133
+ multi
134
+ onPress={() => {
135
+ const current = Array.isArray(value) ? value : [];
136
+ const next = selected
137
+ ? current.filter((v) => v !== option.value)
138
+ : [...current, option.value];
139
+ onChange(next.length > 0 ? next : undefined);
140
+ }}
141
+ />
142
+ );
143
+ })
144
+ : null}
145
+
146
+ {/* Inputs draw their own error; the rest need one underneath. */}
147
+ {error && !isPlainInput && field.type !== 'money' ? (
148
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
149
+ {error}
150
+ </MyazaText>
151
+ ) : null}
152
+ </View>
153
+ );
154
+ }
155
+
156
+ /** A centred Yes / No card — no radio mark; the label IS the choice. */
157
+ function BooleanChoice({
158
+ label,
159
+ selected,
160
+ onPress,
161
+ }: {
162
+ label: string;
163
+ selected: boolean;
164
+ onPress: () => void;
165
+ }): React.ReactElement {
166
+ const { colors } = useTheme();
167
+ return (
168
+ <Pressable
169
+ onPress={onPress}
170
+ accessibilityRole="radio"
171
+ accessibilityState={{ checked: selected }}
172
+ accessibilityLabel={label}
173
+ style={{
174
+ height: 48,
175
+ alignItems: 'center',
176
+ justifyContent: 'center',
177
+ borderRadius: radius.md,
178
+ borderWidth: selected ? 1.5 : 1,
179
+ borderColor: selected ? colors.primary : colors.border,
180
+ backgroundColor: selected ? colors.primary50 : 'transparent',
181
+ }}
182
+ >
183
+ <MyazaText
184
+ variant="body"
185
+ color={selected ? colors.primary : undefined}
186
+ style={{ fontWeight: '600' }}
187
+ >
188
+ {label}
189
+ </MyazaText>
190
+ </Pressable>
191
+ );
192
+ }
193
+
194
+ function FieldLabel({
195
+ text,
196
+ required,
197
+ helpText,
198
+ }: {
199
+ text: string;
200
+ required: boolean;
201
+ helpText?: string;
202
+ }): React.ReactElement {
203
+ const { colors } = useTheme();
204
+ return (
205
+ <View style={{ marginBottom: spacing.xs }}>
206
+ <MyazaText variant="body" style={{ fontWeight: '600' }}>
207
+ {text}
208
+ {required ? <MyazaText color={colors.error}> *</MyazaText> : null}
209
+ </MyazaText>
210
+ {helpText ? (
211
+ <MyazaText variant="bodySmall" color={colors.textSecondary}>
212
+ {helpText}
213
+ </MyazaText>
214
+ ) : null}
215
+ </View>
216
+ );
217
+ }
218
+
219
+ export { currencyKeyFor };
@@ -0,0 +1,134 @@
1
+ import React, { useState } from 'react';
2
+ import { 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 { CountryFlag } from '../components/CountryFlag';
10
+ import { currencyFlagCountry, currencyName } from '../config/currencyFlags';
11
+ import { formatGroupedAmount, parseGroupedAmount } from '../utils/amountFormat';
12
+ import type { QuestionnaireAnswerValue, QuestionnaireField as FieldDef } from '../types/workflow';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // A money question: an amount plus the currency it is in.
16
+ //
17
+ // Two answer keys, not one — `<key>` and `<key>_currency`. Keeping them as
18
+ // separate scalars is what lets a decision graph compare the amount with `gt`
19
+ // and `lt`; a `{amount, currency}` object would not compare at all.
20
+ //
21
+ // Laid out as ONE row — amount, then the currency beside it — mirroring the
22
+ // Flutter SDK. Currencies stacked below the field read as a second question
23
+ // rather than a unit attached to the number above them.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Matches MyazaInput's single-line height so the two controls line up. */
27
+ const CONTROL_HEIGHT = 48;
28
+
29
+ /** Flag for a currency code — blank for supranational codes (XOF, XAU…). */
30
+ function CurrencyFlag({ code }: { code: string }): React.ReactElement | null {
31
+ const country = currencyFlagCountry(code);
32
+ if (!country) return null;
33
+ return <CountryFlag country={country} size={18} />;
34
+ }
35
+
36
+ export function MoneyField({
37
+ field,
38
+ value,
39
+ currencyValue,
40
+ error,
41
+ onChange,
42
+ onCurrencyChange,
43
+ }: {
44
+ field: FieldDef;
45
+ value: QuestionnaireAnswerValue | undefined;
46
+ currencyValue?: string;
47
+ error?: string;
48
+ onChange: (value: QuestionnaireAnswerValue | undefined) => void;
49
+ onCurrencyChange: (currency: string) => void;
50
+ }): React.ReactElement {
51
+ const { colors } = useTheme();
52
+ const currencies = field.currencies ?? [];
53
+ // The showing currency is the answer when the user never opens the picker,
54
+ // which is why the payload defaults to the definition's first.
55
+ const currency = currencyValue ?? currencies[0];
56
+
57
+ // The field shows a GROUPED string while the answer stays a number. Seeded
58
+ // once from any previously entered answer (returning to the step via back),
59
+ // then owned by the input — same as Flutter's controller.
60
+ const [display, setDisplay] = useState(() =>
61
+ value === undefined ? '' : formatGroupedAmount(String(value)),
62
+ );
63
+
64
+ const onAmountText = (text: string): void => {
65
+ const grouped = formatGroupedAmount(text);
66
+ setDisplay(grouped);
67
+ const amount = parseGroupedAmount(grouped);
68
+ onChange(amount === null ? undefined : amount);
69
+ };
70
+
71
+ return (
72
+ // Currency FIRST, then the amount — it reads as a unit prefix
73
+ // ("NGN 250,000"), the way a currency is actually written.
74
+ // TOP-aligned, not centered: the amount column grows taller when its
75
+ // validation error renders under it, and centering would drag the
76
+ // currency select down out of line with the input box. Both controls are
77
+ // CONTROL_HEIGHT tall, so top alignment keeps them level always.
78
+ <View style={{ flexDirection: 'row', alignItems: 'flex-start' }}>
79
+ {currencies.length > 1 ? (
80
+ <>
81
+ {/* Height is pinned so the select matches MyazaInput's single-line
82
+ box exactly; without it the select renders short and the two
83
+ controls sit at different heights. */}
84
+ <View style={{ height: CONTROL_HEIGHT }}>
85
+ <MyazaSelect<string>
86
+ value={currency}
87
+ sheetTitle="Currency"
88
+ height={CONTROL_HEIGHT}
89
+ compact
90
+ options={currencies.map((code) => ({
91
+ value: code,
92
+ label: code,
93
+ description: currencyName(code) ?? undefined,
94
+ leading: <CurrencyFlag code={code} />,
95
+ }))}
96
+ onChange={onCurrencyChange}
97
+ />
98
+ </View>
99
+ <View style={{ width: spacing.sm }} />
100
+ </>
101
+ ) : currency ? (
102
+ <>
103
+ <View
104
+ style={{
105
+ height: CONTROL_HEIGHT,
106
+ flexDirection: 'row',
107
+ alignItems: 'center',
108
+ paddingHorizontal: spacing.md,
109
+ borderRadius: radius.sm,
110
+ backgroundColor: colors.primary50,
111
+ }}
112
+ >
113
+ <CurrencyFlag code={currency} />
114
+ <View style={{ width: spacing.xs }} />
115
+ <MyazaText variant="body" style={{ fontWeight: '600' }}>
116
+ {currency}
117
+ </MyazaText>
118
+ </View>
119
+ <View style={{ width: spacing.sm }} />
120
+ </>
121
+ ) : null}
122
+
123
+ <View style={{ flex: 1 }}>
124
+ <MyazaInput
125
+ value={display}
126
+ onChangeText={onAmountText}
127
+ placeholder={field.placeholder ?? '0.00'}
128
+ error={error ?? null}
129
+ keyboardType="decimal-pad"
130
+ />
131
+ </View>
132
+ </View>
133
+ );
134
+ }
@@ -0,0 +1,76 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useKyc, useKycConfig, useKycStore } from '../components/runtime';
6
+ import { MyazaButton } from '../components/MyazaButton';
7
+ import { currencyKeyFor, validateQuestionnaire } from '../config/questionnaire';
8
+ import { QuestionnaireFieldView } from './QuestionnaireField';
9
+ import type { QuestionnaireAnswerValue } from '../types/workflow';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Extra-info questionnaire — compliance declarations asked after capture and
13
+ // just before submission.
14
+ //
15
+ // Questions come from the workflow; answers ride the /verify submission and are
16
+ // re-validated server-side against the PUBLISHED definition, so what happens
17
+ // here is purely about telling the user before they spend a round trip.
18
+ //
19
+ // The step's title/description live in the sheet header (KycFlow reads them),
20
+ // matching every other step.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** Header title/description for the questionnaire step. Used by KycFlow. */
24
+ export function questionnaireMeta(
25
+ title: string | undefined,
26
+ description: string | undefined,
27
+ ): { title: string; description: string } {
28
+ return {
29
+ title: title ?? 'A few more questions',
30
+ description:
31
+ description ??
32
+ 'This information is required for compliance and helps keep your account safe.',
33
+ };
34
+ }
35
+
36
+ export function QuestionnaireStep(): React.ReactElement {
37
+ const config = useKycConfig();
38
+ const store = useKycStore();
39
+ const answers = useKyc((s) => s.questionnaireAnswers);
40
+ const [errors, setErrors] = useState<Record<string, string>>({});
41
+
42
+ const fields = config.questionnaire?.fields ?? [];
43
+
44
+ const setAnswer = (key: string, value: QuestionnaireAnswerValue | undefined): void => {
45
+ store.getState().setQuestionnaireAnswer(key, value);
46
+ // Clear this field's error as soon as it is touched — leaving it up while
47
+ // the user is fixing it reads as though the fix didn't take.
48
+ setErrors((prev) => (prev[key] ? { ...prev, [key]: '' } : prev));
49
+ };
50
+
51
+ const handleContinue = (): void => {
52
+ const nextErrors = validateQuestionnaire(fields, answers);
53
+ setErrors(nextErrors);
54
+ if (Object.keys(nextErrors).length > 0) return;
55
+ store.getState().nextStep();
56
+ };
57
+
58
+ return (
59
+ <View>
60
+ {fields.map((field) => (
61
+ <QuestionnaireFieldView
62
+ key={field.key}
63
+ field={field}
64
+ value={answers[field.key]}
65
+ currencyValue={answers[currencyKeyFor(field)] as string | undefined}
66
+ error={errors[field.key] || undefined}
67
+ onChange={(value) => setAnswer(field.key, value)}
68
+ onCurrencyChange={(currency) => setAnswer(currencyKeyFor(field), currency)}
69
+ />
70
+ ))}
71
+
72
+ <View style={{ height: spacing.sm }} />
73
+ <MyazaButton label="Continue" onPress={handleContinue} />
74
+ </View>
75
+ );
76
+ }
@@ -11,6 +11,7 @@ import { MyazaAlert } from '../components/MyazaAlert';
11
11
  import { MyazaPulseLoader } from '../components/MyazaPulseLoader';
12
12
  import { Icon } from '../components/Icon';
13
13
  import { fillTokens } from '../utils/tokens';
14
+ import { KeyPeopleInviteLinks } from './KeyPeopleInviteLinks';
14
15
 
15
16
  // Terminal screen — 1:1 with the Flutter SDK's SubmittedScreen. Calls
16
17
  // submitAsync on mount; renders submitting / success / error views with the same
@@ -135,6 +136,9 @@ export function SubmittedStep({ onClose }: { onClose: () => void }): React.React
135
136
  const description = config.success?.description
136
137
  ? fillTokens(config.success.description, config.userData)
137
138
  : DEFAULT_SUCCESS_DESCRIPTION;
139
+ // KYB: per-person verification links for full-KYC key people — rendered so
140
+ // the applicant can send each one immediately.
141
+ const invites = store.getState().keyPeopleInvites; // set before phase flips to success
138
142
 
139
143
  return (
140
144
  <View style={{ flex: 1, justifyContent: 'space-between' }}>
@@ -151,6 +155,7 @@ export function SubmittedStep({ onClose }: { onClose: () => void }): React.React
151
155
  <MyazaText variant="bodyMedium" style={{ textAlign: 'center' }}>
152
156
  {description}
153
157
  </MyazaText>
158
+ <KeyPeopleInviteLinks invites={invites} />
154
159
  </View>
155
160
  <MyazaButton label="Done" onPress={onClose} />
156
161
  </View>