@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,149 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import {
5
+ getCountries,
6
+ getCountryCallingCode,
7
+ parsePhoneNumberFromString,
8
+ type CountryCode,
9
+ } from 'libphonenumber-js/min';
10
+
11
+ import { radius, sizing, spacing } from '../config/theme';
12
+ import { useTheme } from './runtime';
13
+ import { MyazaText } from './Typography';
14
+ import { MyazaInput } from './MyazaInput';
15
+ import { Icon } from './Icon';
16
+ import { CountryFlag } from './CountryFlag';
17
+ import { DialCodePicker, type DialCodeOption } from './DialCodePicker';
18
+ import { formatNationalNumber } from '../config/phone';
19
+ // Names come from the generated table, NOT Intl.DisplayNames: Hermes may ship
20
+ // without region data, and every row then reads as a bare ISO code.
21
+ import { COUNTRY_NAMES } from '../config/countryNames.g';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Phone field: a dial-code country picker beside a nationally-formatted number.
25
+ //
26
+ // The RN mirror of the web SDK's PhoneNumberInput and Flutter's
27
+ // phone_number_input.dart. Before this the step was a bare text field asking
28
+ // for "+2348012345678" — the user had to know their own country code and type
29
+ // it, and validity was a length guess.
30
+ //
31
+ // Formatting and validity both come from libphonenumber: "8031234567" shows as
32
+ // "803 123 4567" for NG, and a wrong-length number is rejected by the country's
33
+ // actual numbering plan rather than a 6–15 digit heuristic.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export function PhoneNumberInput({
37
+ defaultCountry,
38
+ disabled,
39
+ onChange,
40
+ }: {
41
+ /** Seeds the picker (ISO-2). Falls back to NG. */
42
+ defaultCountry?: string;
43
+ disabled?: boolean;
44
+ /** Fires on every edit with the E.164 value ('' until parseable) + validity. */
45
+ onChange: (value: { e164: string; isValid: boolean; country: string }) => void;
46
+ }): React.ReactElement {
47
+ const { colors } = useTheme();
48
+
49
+ const options = useMemo<DialCodeOption[]>(
50
+ () =>
51
+ getCountries()
52
+ // Only countries the shared table can name. libphonenumber lists five
53
+ // that it cannot (AC, CC, CX, IO, TA — dependencies with populations in
54
+ // the hundreds, reachable on their parent country's code anyway), and a
55
+ // row reading "AC" is worse than no row. Dropping them also makes this
56
+ // list exactly Flutter's, which is generated from the same table.
57
+ .filter((code) => COUNTRY_NAMES[code])
58
+ .map((code) => ({
59
+ code,
60
+ name: COUNTRY_NAMES[code] as string,
61
+ dialCode: `+${getCountryCallingCode(code)}`,
62
+ }))
63
+ .sort((a, b) => a.name.localeCompare(b.name)),
64
+ [],
65
+ );
66
+
67
+ const seed = (defaultCountry?.toUpperCase() ?? 'NG') as CountryCode;
68
+ const [country, setCountry] = useState<CountryCode>(
69
+ options.some((o) => o.code === seed) ? seed : ('NG' as CountryCode),
70
+ );
71
+ const [national, setNational] = useState('');
72
+ const [open, setOpen] = useState(false);
73
+
74
+ const selected = options.find((o) => o.code === country);
75
+
76
+ const emit = (nextCountry: CountryCode, nextNational: string): void => {
77
+ const parsed = parsePhoneNumberFromString(nextNational, nextCountry);
78
+ onChange({
79
+ e164: parsed?.number ?? '',
80
+ isValid: parsed?.isValid() ?? false,
81
+ country: nextCountry,
82
+ });
83
+ };
84
+
85
+ // The caret sits at the end: separators shift on nearly every keystroke, so
86
+ // tracking an interior caret fights the formatter more than it helps.
87
+ const onTyped = (raw: string): void => {
88
+ const formatted = formatNationalNumber(raw, country);
89
+ setNational(formatted);
90
+ emit(country, formatted);
91
+ };
92
+
93
+ const pickCountry = (code: string): void => {
94
+ const next = code as CountryCode;
95
+ setCountry(next);
96
+ setOpen(false);
97
+ // Re-group the digits already typed under the new country's plan.
98
+ const regrouped = formatNationalNumber(national, next);
99
+ setNational(regrouped);
100
+ emit(next, regrouped);
101
+ };
102
+
103
+ return (
104
+ <View>
105
+ <View style={{ flexDirection: 'row', gap: spacing.sm }}>
106
+ <Pressable
107
+ onPress={disabled ? undefined : () => setOpen(true)}
108
+ accessibilityRole="button"
109
+ accessibilityLabel={`Country code ${selected?.dialCode ?? ''}`}
110
+ style={{
111
+ flexDirection: 'row',
112
+ alignItems: 'center',
113
+ gap: spacing.xs,
114
+ paddingHorizontal: spacing.sm + 2,
115
+ height: sizing.inputHeight,
116
+ borderWidth: 1,
117
+ borderColor: colors.border,
118
+ borderRadius: radius.sm,
119
+ backgroundColor: colors.background,
120
+ opacity: disabled ? 0.6 : 1,
121
+ }}
122
+ >
123
+ <CountryFlag country={country} size={22} />
124
+ <MyazaText variant="body">{selected?.dialCode ?? ''}</MyazaText>
125
+ <Icon name="chevron-down" size={14} color={colors.textMuted} />
126
+ </Pressable>
127
+
128
+ <View style={{ flex: 1 }}>
129
+ <MyazaInput
130
+ value={national}
131
+ onChangeText={onTyped}
132
+ placeholder="803 123 4567"
133
+ keyboardType="phone-pad"
134
+ editable={!disabled}
135
+ autoFocus
136
+ />
137
+ </View>
138
+ </View>
139
+
140
+ <DialCodePicker
141
+ visible={open}
142
+ options={options}
143
+ selected={country}
144
+ onPick={pickCountry}
145
+ onClose={() => setOpen(false)}
146
+ />
147
+ </View>
148
+ );
149
+ }
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import { Linking, Pressable, View } from 'react-native';
3
+
4
+ import { PRODUCT_URL, brandMarkColor } from '../config/brand';
5
+ import { spacing } from '../config/theme';
6
+ import { MyazaWordmark } from './MyazaWordmark';
7
+ import { useTheme } from './runtime';
8
+ import { MyazaText } from './Typography';
9
+
10
+ /**
11
+ * Vendor attribution, pinned below the step body on every screen.
12
+ *
13
+ * "POWERED BY", deliberately, not "Secured by" — at this moment we are
14
+ * COLLECTING a passport and a live selfie, not protecting something, so what the
15
+ * user needs is provenance (a name to hold responsible) rather than an
16
+ * unfalsifiable security promise, which is the kind of reassurance a phishing
17
+ * screen writes.
18
+ *
19
+ * The lockup mirrors the dashboard's canonical treatment: [Myaza wordmark],
20
+ * hairline rule, TRUST in tracked uppercase. The divider is a 1px VIEW, not a
21
+ * "|" character — a typed pipe sits on the text baseline at whatever weight the
22
+ * font gives it and reads as a separator between two names. The rule is what
23
+ * makes it one brand: Myaza Trust.
24
+ *
25
+ * Kept on the camera screens too: a trust mark that disappears exactly where
26
+ * biometrics are captured would vanish where it matters most.
27
+ */
28
+ export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.ReactElement {
29
+ const { colors } = useTheme();
30
+ // ONE Myaza tone for the whole mark — label, wordmark lettering, rule and
31
+ // TRUST — picked against the background it will actually sit on rather than
32
+ // taken from the org's palette. `colors.background` already reflects an org
33
+ // override, so this holds for a custom background too, not just light/dark.
34
+ const markColor = brandMarkColor(colors.background);
35
+
36
+ return (
37
+ <View
38
+ style={{
39
+ paddingTop: spacing.md,
40
+ // Symmetric with the top. It was `lg`, which read as a heavy band on the
41
+ // one screen where vertical space is contested — the searchable country
42
+ // list, where the footer costs a visible row. The home-indicator inset
43
+ // rides on top, so notched devices still clear it comfortably.
44
+ paddingBottom: spacing.md + bottomInset,
45
+ alignItems: 'center',
46
+ }}
47
+ >
48
+ {/* openURL can reject (no browser / a locked-down device). The mark itself
49
+ is the point, so a failed open is swallowed rather than surfaced. */}
50
+ <Pressable
51
+ onPress={() => {
52
+ void Linking.openURL(PRODUCT_URL).catch(() => undefined);
53
+ }}
54
+ accessibilityRole="link"
55
+ accessibilityLabel="Powered by Myaza Trust"
56
+ hitSlop={8}
57
+ style={{ flexDirection: 'row', alignItems: 'center', gap: 10, opacity: 0.9 }}
58
+ >
59
+ {/* Small and muted on purpose — "Powered by" is connective tissue, not
60
+ the message. The BRAND carries the weight. */}
61
+ <MyazaText variant="body" color={markColor} style={{ flexShrink: 1, fontSize: 12 }}>
62
+ Powered by
63
+ </MyazaText>
64
+
65
+ {/* The lockup, spaced TIGHTER than the gap before it so it reads as one
66
+ mark rather than three evenly-spaced items. */}
67
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
68
+ <MyazaWordmark height={28} wordmark={markColor} />
69
+
70
+ <View style={{ width: 1, height: 24, backgroundColor: markColor, opacity: 0.35 }} />
71
+
72
+ {/* ~half the wordmark's height — the ratio the dashboard lockup uses. */}
73
+ {/* Pinned to the brand face rather than inheriting: this word is part
74
+ of the MARK, and the font an org sets in their workflow would
75
+ otherwise redraw someone else's logo in the customer's typeface.
76
+ Matches the web's BRAND_FONT_STACK on the same element. */}
77
+ <MyazaText
78
+ brandMark
79
+ variant="body"
80
+ color={markColor}
81
+ style={{ flexShrink: 1, fontSize: 14, fontWeight: '600', letterSpacing: 2 }}
82
+ >
83
+ TRUST
84
+ </MyazaText>
85
+ </View>
86
+ </Pressable>
87
+ </View>
88
+ );
89
+ }
@@ -0,0 +1,149 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing, ScrollView, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useTheme } from './runtime';
6
+ import { MyazaText } from './Typography';
7
+ import { MyazaButton } from './MyazaButton';
8
+ import { Icon } from './Icon';
9
+ import type { ReadyContent } from './readyPrimerContent';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // "Here's what happens next" — shown once before a capture step opens the
13
+ // camera.
14
+ //
15
+ // It exists because the camera used to appear unannounced: a user who does not
16
+ // know their ID or their face is about to be photographed fumbles the first
17
+ // attempt, and a retake costs more than a sentence of warning.
18
+ //
19
+ // It sits BEFORE the camera-permission primer: first what we are about to do,
20
+ // then the OS prompt. Two asks in a row, each with a reason, never a surprise.
21
+ //
22
+ // A port of the Flutter SDK's ReadyPrimer and the web SDK's — same single-column
23
+ // shape (one hero, at most three expectations, one primary action) and the same
24
+ // copy, so the flow reads identically whichever SDK an org embeds.
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export function ReadyPrimer({
28
+ content,
29
+ onReady,
30
+ buttonLabel = "I'm ready",
31
+ }: {
32
+ content: ReadyContent;
33
+ onReady: () => void;
34
+ buttonLabel?: string;
35
+ }): React.ReactElement {
36
+ const { colors } = useTheme();
37
+
38
+ return (
39
+ // TOP-ALIGNED, like the web and Flutter. The sheet body guarantees the
40
+ // child at least the viewport height, so centring parked the whole primer
41
+ // in the middle and left a dead band under the step indicator.
42
+ <ScrollView
43
+ contentContainerStyle={{ paddingVertical: spacing.md }}
44
+ showsVerticalScrollIndicator={false}
45
+ >
46
+ {/* Hero — matches the permission primer's icon treatment so the two
47
+ screens read as one sequence rather than two different designs. */}
48
+ <View
49
+ style={{
50
+ paddingHorizontal: 24,
51
+ paddingVertical: 36,
52
+ backgroundColor: `${colors.primary}0D`, // 5%
53
+ borderWidth: 1,
54
+ borderColor: `${colors.primary}26`, // 15%
55
+ borderRadius: radius.lg,
56
+ alignItems: 'center',
57
+ }}
58
+ >
59
+ <View style={{ width: 80, height: 80, alignItems: 'center', justifyContent: 'center' }}>
60
+ <PulseRing color={colors.primary} />
61
+ <View
62
+ style={{
63
+ width: 64,
64
+ height: 64,
65
+ borderRadius: 32,
66
+ backgroundColor: `${colors.primary}26`, // 15%
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ }}
70
+ >
71
+ <Icon name={content.icon} size={28} color={colors.primary} />
72
+ </View>
73
+ </View>
74
+
75
+ <View style={{ height: spacing.md }} />
76
+ <MyazaText variant="heading3" style={{ textAlign: 'center' }}>
77
+ {content.title}
78
+ </MyazaText>
79
+ <View style={{ height: 6 }} />
80
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
81
+ {content.body}
82
+ </MyazaText>
83
+ </View>
84
+
85
+ <View style={{ height: 20 }} />
86
+
87
+ {/* Expectations — the same row idiom as the consent screen's
88
+ "during this process we will" list. */}
89
+ {content.checklist.map((item) => (
90
+ <View key={item.label} style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
91
+ <View
92
+ style={{
93
+ width: 36,
94
+ height: 36,
95
+ borderRadius: radius.xs,
96
+ backgroundColor: `${colors.primary}1A`, // 10%
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ }}
100
+ >
101
+ <Icon name={item.icon} size={18} color={colors.primary} />
102
+ </View>
103
+ <View style={{ width: 12 }} />
104
+ <MyazaText variant="bodyMedium" style={{ flex: 1 }}>
105
+ {item.label}
106
+ </MyazaText>
107
+ </View>
108
+ ))}
109
+
110
+ <View style={{ height: 8 }} />
111
+ {/* One primary action. MyazaButton already clears the 44pt minimum. */}
112
+ <MyazaButton label={buttonLabel} onPress={onReady} />
113
+ </ScrollView>
114
+ );
115
+ }
116
+
117
+ /** The web's `animate-pulse-ring` — a ring that expands and fades behind the glyph. */
118
+ function PulseRing({ color }: { color: string }): React.ReactElement {
119
+ const t = useRef(new Animated.Value(0)).current;
120
+
121
+ useEffect(() => {
122
+ const loop = Animated.loop(
123
+ Animated.timing(t, {
124
+ toValue: 1,
125
+ duration: 2000,
126
+ easing: Easing.out(Easing.ease),
127
+ useNativeDriver: true,
128
+ }),
129
+ );
130
+ loop.start();
131
+ return () => loop.stop();
132
+ }, [t]);
133
+
134
+ return (
135
+ <Animated.View
136
+ pointerEvents="none"
137
+ style={{
138
+ position: 'absolute',
139
+ width: 64,
140
+ height: 64,
141
+ borderRadius: 32,
142
+ borderWidth: 2,
143
+ borderColor: color,
144
+ opacity: t.interpolate({ inputRange: [0, 1], outputRange: [0.45, 0] }),
145
+ transform: [{ scale: t.interpolate({ inputRange: [0, 1], outputRange: [1, 1.35] }) }],
146
+ }}
147
+ />
148
+ );
149
+ }
@@ -0,0 +1,91 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { useKycStore } from './runtime';
5
+ import type { KYCStep } from '../types/config';
6
+ import { ConsentStep } from '../screens/ConsentStep';
7
+ import { IdTypeStep } from '../screens/IdTypeStep';
8
+ import { IdInputStep } from '../screens/IdInputStep';
9
+ import { DocumentCaptureStep } from '../screens/DocumentCaptureStep';
10
+ import { LivenessStep } from '../screens/LivenessStep';
11
+ import { SubmittedStep } from '../screens/SubmittedStep';
12
+ import { QuestionnaireStep } from '../screens/QuestionnaireStep';
13
+ import { ContactVerificationStep } from '../screens/ContactVerificationStep';
14
+ import { ProofOfAddressStep } from '../screens/ProofOfAddressStep';
15
+ import { CountrySelectStep } from '../screens/CountrySelectStep';
16
+ import { BusinessDetailsStep } from '../screens/BusinessDetailsStep';
17
+ import { BusinessKeyPeopleStep } from '../screens/BusinessKeyPeopleStep';
18
+ import { BusinessDocumentsStep } from '../screens/BusinessDocumentsStep';
19
+ import { ApplicantRoleStep } from '../screens/ApplicantRoleStep';
20
+ import { NfcStep } from '../screens/NfcStep';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // The step router — which screen a step renders.
24
+ //
25
+ // Split from KycFlow.tsx (200-line rule) so adding a step touches the router
26
+ // and the step order, and nothing else.
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export function StepView({ step, onClose }: { step: KYCStep; onClose: () => void }): React.ReactElement {
30
+ switch (step) {
31
+ case 'consent':
32
+ return <ConsentStep />;
33
+ case 'id-type':
34
+ return <IdTypeStep />;
35
+ case 'id-input':
36
+ return <IdInputStep />;
37
+ case 'document-capture':
38
+ return <DocumentCaptureStep />;
39
+ case 'liveness':
40
+ return <LivenessStep />;
41
+ case 'questionnaire':
42
+ return <QuestionnaireStep />;
43
+ // Distinct keys are LOAD-BEARING: both mounts are the same component type
44
+ // at the same position, so without them React reuses the instance when the
45
+ // flow moves email → phone. The phone step would open holding the email
46
+ // step's challengeId, show its code panel, and verifying would file the
47
+ // EMAIL proof as the phone token — which the server then drops, blocking
48
+ // submission on a phone check the user appears to have passed.
49
+ case 'email-verification':
50
+ return <ContactVerificationStep key="contact-email" channel="email" />;
51
+ case 'phone-verification':
52
+ return <ContactVerificationStep key="contact-phone" channel="phone" />;
53
+ case 'proof-of-address':
54
+ return <ProofOfAddressStep />;
55
+ case 'country-select':
56
+ return <CountrySelectStep />;
57
+ case 'business-details':
58
+ return <BusinessDetailsStep />;
59
+ case 'business-key-people':
60
+ return <BusinessKeyPeopleStep />;
61
+ case 'business-documents':
62
+ return <BusinessDocumentsStep />;
63
+ case 'applicant-role':
64
+ return <ApplicantRoleStep />;
65
+ case 'nfc':
66
+ return <NfcStep />;
67
+ case 'submitted':
68
+ return <SubmittedStep onClose={onClose} />;
69
+ default:
70
+ return <UnimplementedStep step={step} />;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * A step that is in the order but has no screen on this platform yet.
76
+ *
77
+ * It advances rather than rendering something — the previous `default` fell
78
+ * back to the consent screen, which put the user in a loop they could not leave
79
+ * (consent's Continue leads back to the same step). Skipping degrades an
80
+ * unbuilt step to "it didn't run", which is the same thing a workflow that left
81
+ * it off would do, and is recoverable. Logged so it surfaces in development
82
+ * instead of only as a step that mysteriously never appears.
83
+ */
84
+ function UnimplementedStep({ step }: { step: KYCStep }): React.ReactElement {
85
+ const store = useKycStore();
86
+ useEffect(() => {
87
+ console.warn(`[myaza-kyc] No screen for step "${step}" — skipping.`);
88
+ store.getState().nextStep();
89
+ }, [step, store]);
90
+ return <View />;
91
+ }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { StyleSheet, Text, type TextProps, type TextStyle } from 'react-native';
3
3
 
4
4
  import { useTheme } from './runtime';
5
- import { fontFamilyFor } from './fonts';
5
+ import { fontFamilyFor, markFamilyName } from './fonts';
6
6
 
7
7
  // Branded text. `variant` picks size/weight; headings render in Space Grotesk and
8
8
  // body/labels in Karla — the SAME families the Flutter SDK uses (google_fonts) —
@@ -34,15 +34,24 @@ const HEADINGS = new Set<TextVariant>(['heading1', 'heading2', 'heading3']);
34
34
  export interface MyazaTextProps extends TextProps {
35
35
  variant?: TextVariant;
36
36
  color?: string;
37
+ /**
38
+ * Render in the Myaza brand face, ignoring the org's font override.
39
+ *
40
+ * For the wordmark ONLY (the "TRUST" in the footer lockup). Everything else
41
+ * in the flow is the customer's UI and should take the customer's typeface;
42
+ * a logo is not.
43
+ */
44
+ brandMark?: boolean;
37
45
  }
38
46
 
39
47
  export function MyazaText({
40
48
  variant = 'body',
41
49
  color,
42
50
  style,
51
+ brandMark = false,
43
52
  ...rest
44
53
  }: MyazaTextProps): React.ReactElement {
45
- const { colors, fontsLoaded } = useTheme();
54
+ const { colors, fontsLoaded, brandFonts } = useTheme();
46
55
  const defaultColor =
47
56
  variant === 'bodyMedium'
48
57
  ? colors.textSecondary
@@ -54,13 +63,19 @@ export function MyazaText({
54
63
  // right Space Grotesk / Karla weight (custom fonts ignore `fontWeight`, so the
55
64
  // weight must be baked into the family).
56
65
  const flat = (StyleSheet.flatten([VARIANTS[variant], style]) ?? {}) as TextStyle;
57
- const family = fontFamilyFor(HEADINGS.has(variant), flat.fontWeight, fontsLoaded);
66
+ const family = brandMark
67
+ ? markFamilyName(flat.fontWeight, fontsLoaded)
68
+ : fontFamilyFor(HEADINGS.has(variant), flat.fontWeight, fontsLoaded, brandFonts);
58
69
 
59
70
  return (
60
71
  <Text
61
72
  style={[
62
73
  flat,
63
74
  { color: color ?? defaultColor },
75
+ // EVERY resolved family — bundled or brand — encodes its weight in the
76
+ // family name, so fontWeight must always be cleared. Leaving it makes
77
+ // the platform synthesise a bolder face on top of an already-bold file:
78
+ // the glyphs render wider than they were measured and the text clips.
64
79
  family ? { fontFamily: family, fontWeight: undefined } : null,
65
80
  ]}
66
81
  {...rest}
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useTheme } from './runtime';
6
+ import { MyazaText } from './Typography';
7
+ import { Icon } from './Icon';
8
+
9
+ /**
10
+ * "This is already verified" confirmation, shown when a user returns to a
11
+ * contact step they have already passed.
12
+ *
13
+ * A primary-tinted card rather than a bare row, matching the web and Flutter
14
+ * SDKs: it is the one moment in the step that reports success, and it should
15
+ * read as a result rather than as another line of copy.
16
+ */
17
+ export function VerifiedNotice({ label }: { label: string }): React.ReactElement {
18
+ const { colors } = useTheme();
19
+ return (
20
+ <View
21
+ style={{
22
+ flexDirection: 'row',
23
+ alignItems: 'center',
24
+ gap: spacing.sm + 4,
25
+ padding: spacing.md,
26
+ borderRadius: radius.md,
27
+ backgroundColor: `${colors.primary}0D`,
28
+ borderWidth: 1,
29
+ borderColor: `${colors.primary}4D`,
30
+ }}
31
+ >
32
+ <Icon name="badge-check" size={20} color={colors.primary} />
33
+ <MyazaText variant="body" style={{ flex: 1, fontWeight: '600' }}>
34
+ {label}
35
+ </MyazaText>
36
+ </View>
37
+ );
38
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import Svg, { Path } from 'react-native-svg';
3
+
4
+ // The WhatsApp mark. Lucide dropped brand icons, and the nearest generic glyph
5
+ // (a speech bubble) does not read as WhatsApp — the whole point of offering the
6
+ // choice is that the user recognises the channel at a glance.
7
+ //
8
+ // Path data: Simple Icons (CC0), 24×24 viewBox. Byte-identical to the web and
9
+ // Flutter SDKs so the tile looks the same everywhere.
10
+ //
11
+ // Tinted by `color` rather than fixed brand green: it sits in a tile whose icon
12
+ // takes the selected/unselected treatment, and a fixed green would fight that
13
+ // (and the dark theme). Nominative use of the mark to label the channel.
14
+ const WHATSAPP_PATH =
15
+ 'M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z';
16
+
17
+ export function WhatsAppIcon({
18
+ size = 16,
19
+ color,
20
+ }: {
21
+ size?: number;
22
+ color: string;
23
+ }): React.ReactElement {
24
+ return (
25
+ <Svg width={size} height={size} viewBox="0 0 24 24">
26
+ <Path d={WHATSAPP_PATH} fill={color} />
27
+ </Svg>
28
+ );
29
+ }