@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,128 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { KYCError } from '../types/verification';
6
+ import type { MyazaKYCConfig } from '../types/config';
7
+ import { MyazaThemeProvider, useTheme } from './theme-provider';
8
+ import { MyazaPulseLoader } from './MyazaPulseLoader';
9
+ import { MyazaText } from './Typography';
10
+ import { MyazaButton } from './MyazaButton';
11
+ import { Icon } from './Icon';
12
+ import type { MountState, ResolvedMount } from './useWorkflowMount';
13
+
14
+ export type { ResolvedMount } from './useWorkflowMount';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // What a `workflowId` mount shows once it is inside the modal.
18
+ //
19
+ // Resolution itself now happens BEFORE the modal is presented (see
20
+ // `useWorkflowMount`), so in the normal case this renders `children` on the
21
+ // first frame and no barrier is ever seen. The loader survives for the one case
22
+ // the caller cannot pre-settle: a Retry after a failure, where the modal is
23
+ // already on screen.
24
+ //
25
+ // A failure is a dead end for the flow — it does NOT fall back to the props,
26
+ // because a workflow that cannot be loaded is not the same flow the props
27
+ // describe, and silently running a different one is worse than not running: the
28
+ // org configured those steps for a reason.
29
+ //
30
+ // Unlike the Flutter gate this one can be LEFT: the error state offers Retry
31
+ // and Close.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export function WorkflowGate({
35
+ config,
36
+ state,
37
+ onRetry,
38
+ onClose,
39
+ children,
40
+ }: {
41
+ config: MyazaKYCConfig;
42
+ state: MountState;
43
+ onRetry: () => void;
44
+ onClose: () => void;
45
+ children: (mount: ResolvedMount) => React.ReactElement;
46
+ }): React.ReactElement {
47
+ if (state.status === 'ready') return children(state.mount);
48
+
49
+ // These render before there is a resolved config, so they carry their own
50
+ // theme from the props' appearance — the flow's own appearance is precisely
51
+ // the thing being waited on.
52
+ return (
53
+ <MyazaThemeProvider appearance={config.appearance}>
54
+ {state.status === 'resolving' ? (
55
+ <GateLoading />
56
+ ) : (
57
+ <GateError error={state.error} onRetry={onRetry} onClose={onClose} />
58
+ )}
59
+ </MyazaThemeProvider>
60
+ );
61
+ }
62
+
63
+ function GateLoading(): React.ReactElement {
64
+ const { colors } = useTheme();
65
+ return (
66
+ <View
67
+ testID="kyc.workflow-gate.loading"
68
+ style={{
69
+ flex: 1,
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ backgroundColor: colors.background,
73
+ padding: spacing.xl,
74
+ }}
75
+ >
76
+ <MyazaPulseLoader size={64} />
77
+ <View style={{ height: spacing.md }} />
78
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
79
+ Preparing your verification…
80
+ </MyazaText>
81
+ </View>
82
+ );
83
+ }
84
+
85
+ function GateError({
86
+ error,
87
+ onRetry,
88
+ onClose,
89
+ }: {
90
+ error: KYCError;
91
+ onRetry: () => void;
92
+ onClose: () => void;
93
+ }): React.ReactElement {
94
+ const { colors } = useTheme();
95
+ // A bad key or an unpublished flow will fail identically on every attempt —
96
+ // offering Retry there would just invite the user to keep pressing it.
97
+ const retryable = error.code === 'network_error';
98
+ return (
99
+ <View
100
+ testID="kyc.workflow-gate.error"
101
+ style={{
102
+ flex: 1,
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ backgroundColor: colors.background,
106
+ padding: spacing.xl,
107
+ }}
108
+ >
109
+ <Icon name="alert" size={40} color={colors.error} />
110
+ <View style={{ height: spacing.md }} />
111
+ <MyazaText variant="heading3" style={{ textAlign: 'center' }}>
112
+ Verification unavailable
113
+ </MyazaText>
114
+ <View style={{ height: spacing.sm }} />
115
+ <MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
116
+ {error.message}
117
+ </MyazaText>
118
+ <View style={{ height: spacing.lg }} />
119
+ {retryable ? (
120
+ <>
121
+ <MyazaButton label="Try Again" onPress={onRetry} />
122
+ <View style={{ height: spacing.sm }} />
123
+ </>
124
+ ) : null}
125
+ <MyazaButton label="Close" variant="outline" onPress={onClose} />
126
+ </View>
127
+ );
128
+ }
@@ -0,0 +1,113 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as Font from 'expo-font';
3
+ import { BRAND_WEIGHTS, brandFamilyName } from '../config/font-resolve';
4
+
5
+ export { BRAND_WEIGHTS, brandFamilyName };
6
+
7
+ /**
8
+ * Load an org's brand font at runtime so a dashboard-picked family renders on
9
+ * mobile.
10
+ *
11
+ * WHY: React Native resolves `fontFamily` against families the app has
12
+ * REGISTERED — it never fetches. Setting `fontFamily: 'Poppins'` on a device
13
+ * where Poppins was never bundled silently falls back. The web SDK injects a
14
+ * stylesheet and Flutter has `google_fonts`; without this, RN was the one
15
+ * platform where a configured font quietly did nothing.
16
+ *
17
+ * ONE FILE PER WEIGHT, and that is the important part. Registering a single
18
+ * regular file and letting `fontWeight: '600'` ask the platform for a bolder
19
+ * face makes iOS SYNTHESISE the bold — it smears the glyphs wider AFTER the
20
+ * text has been measured, so the layout box is sized for the regular face and
21
+ * the rendered text overflows it. That is what truncated "Continue" to
22
+ * "Continu". Real weights are measured correctly, so nothing is clipped.
23
+ *
24
+ * This mirrors how the SDK's own Karla / Space Grotesk work: each weight is its
25
+ * own registered family and `fontWeight` is cleared at the call site.
26
+ *
27
+ * HOW: Google Fonts serves a different FORMAT per User-Agent — woff2 to
28
+ * browsers, TTF to anything it does not recognise. React Native's `fetch` sends
29
+ * no browser UA, so the request below returns TTF, the only format expo-font can
30
+ * load. Adding a browser-like UA here would return woff2 and break loading.
31
+ */
32
+
33
+ /** One in-flight/settled attempt per family; the fetch is not free. */
34
+ const attempted = new Map<string, Promise<boolean>>();
35
+
36
+ async function loadGoogleFont(family: string): Promise<boolean> {
37
+ try {
38
+ const url =
39
+ `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replace(/%20/g, '+')}` +
40
+ `:wght@${BRAND_WEIGHTS.join(';')}&display=swap`;
41
+ const css = await fetch(url).then((r) => (r.ok ? r.text() : ''));
42
+ if (!css) return false;
43
+
44
+ // Each @font-face carries its own weight + url. Pair them in document order
45
+ // rather than assuming the response lists weights in the order requested.
46
+ const faces = css.split('@font-face');
47
+ const assets: Record<string, string> = {};
48
+ for (const face of faces) {
49
+ const weight = Number(/font-weight:\s*(\d+)/.exec(face)?.[1] ?? 0);
50
+ const src = /url\((https:\/\/[^)]+)\)/.exec(face)?.[1];
51
+ if (!weight || !src) continue;
52
+ if (!(BRAND_WEIGHTS as readonly number[]).includes(weight)) continue;
53
+ const name = brandFamilyName(family, weight);
54
+ if (!Font.isLoaded(name)) assets[name] = src;
55
+ }
56
+ // A family with no 400 face is not usable as body text.
57
+ if (!Font.isLoaded(family) && !assets[family]) return false;
58
+ if (Object.keys(assets).length > 0) await Font.loadAsync(assets);
59
+ return true;
60
+ } catch {
61
+ // A brand font is decoration on an identity flow — an offline device or a
62
+ // blocked CDN must never affect a verification.
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function ensure(family: string): Promise<boolean> {
68
+ const existing = attempted.get(family);
69
+ if (existing) return existing;
70
+ const p = loadGoogleFont(family);
71
+ attempted.set(family, p);
72
+ return p;
73
+ }
74
+
75
+ /**
76
+ * Returns the families SAFE TO APPLY — i.e. actually registered right now.
77
+ *
78
+ * A name is only returned once its font exists. Applying it eagerly is what
79
+ * produced the silent fallback this fixes: RN needs the family to exist at the
80
+ * moment the style is set, so the order must be load → apply → re-render.
81
+ */
82
+ export function useBrandFonts(
83
+ body?: string,
84
+ heading?: string,
85
+ ): { body?: string; heading?: string } {
86
+ const [ready, setReady] = useState<Record<string, boolean>>({});
87
+
88
+ useEffect(() => {
89
+ let cancelled = false;
90
+ const wanted = [body, heading].filter((f): f is string => Boolean(f && f.trim()));
91
+ if (wanted.length === 0) return;
92
+
93
+ for (const family of wanted) {
94
+ // Already bundled by the HOST APP (a self-hosted brand font) — usable
95
+ // immediately, no fetch.
96
+ if (Font.isLoaded(family)) {
97
+ setReady((r) => (r[family] ? r : { ...r, [family]: true }));
98
+ continue;
99
+ }
100
+ void ensure(family).then((ok) => {
101
+ if (!cancelled && ok) setReady((r) => ({ ...r, [family]: true }));
102
+ });
103
+ }
104
+ return () => {
105
+ cancelled = true;
106
+ };
107
+ }, [body, heading]);
108
+
109
+ return {
110
+ body: body && ready[body] ? body : undefined,
111
+ heading: heading && ready[heading] ? heading : undefined,
112
+ };
113
+ }
@@ -0,0 +1,35 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The shape of the flash's cut-out.
3
+ //
4
+ // Kept free of native imports so the geometry can be tested without a renderer.
5
+ // The failure it guards against is purely visual — a wrong-shaped hole still
6
+ // passes the liveness check, so nothing else would ever raise it.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /** The preview's position on screen, in window coordinates. */
10
+ export interface FlashHole {
11
+ x: number;
12
+ y: number;
13
+ size: number;
14
+ }
15
+
16
+ /**
17
+ * The screen rectangle with the preview circle subtracted, as ONE path.
18
+ *
19
+ * Two subpaths, rendered with `fillRule="evenodd"` so the second is a hole
20
+ * rather than a second filled shape. The surround was originally four
21
+ * rectangles, which can only describe a rectangular gap — against a circular
22
+ * preview that left the corners painted over the user's face, which is the part
23
+ * they need to see to stay framed.
24
+ */
25
+ export function holePath(width: number, height: number, hole: FlashHole): string {
26
+ const r = hole.size / 2;
27
+ const cx = hole.x + r;
28
+ const cy = hole.y + r;
29
+ // Two half-arcs rather than one: a single 360° arc has identical start and
30
+ // end points and renders as nothing.
31
+ return (
32
+ `M0 0 H${width} V${height} H0 Z ` +
33
+ `M${cx - r} ${cy} A${r} ${r} 0 1 0 ${cx + r} ${cy} A${r} ${r} 0 1 0 ${cx - r} ${cy} Z`
34
+ );
35
+ }
@@ -0,0 +1,56 @@
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 { MyazaButton } from '../MyazaButton';
8
+ import { Icon } from '../Icon';
9
+ import { KycSheet } from '../KycSheet';
10
+
11
+ /**
12
+ * The flow cannot start — a bad API key, or a feature the org cannot use.
13
+ *
14
+ * Terminal by design: there is nothing the user can do about it and no step to
15
+ * fall back to, so the only action offered is Close. The consumer has already
16
+ * been told through `onError`; this is what the person holding the phone sees.
17
+ */
18
+ export function FatalConfigError({
19
+ message,
20
+ onClose,
21
+ }: {
22
+ message?: string | null;
23
+ onClose: () => void;
24
+ }): React.ReactElement {
25
+ const { colors } = useTheme();
26
+ return (
27
+ <KycSheet title="" onClose={onClose} hideBrand>
28
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
29
+ <View
30
+ style={{
31
+ width: 80,
32
+ height: 80,
33
+ borderRadius: radius.full,
34
+ backgroundColor: `${colors.error}1A`,
35
+ alignItems: 'center',
36
+ justifyContent: 'center',
37
+ }}
38
+ >
39
+ <Icon name="alert" size={40} color={colors.error} />
40
+ </View>
41
+ <View style={{ height: spacing.lg }} />
42
+ <MyazaText variant="heading2" style={{ textAlign: 'center' }}>
43
+ Unable to start verification
44
+ </MyazaText>
45
+ <View style={{ height: spacing.xs }} />
46
+ <MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ textAlign: 'center' }}>
47
+ {message ?? 'Please check your API key and try again.'}
48
+ </MyazaText>
49
+ <View style={{ height: spacing.lg }} />
50
+ <View style={{ width: '100%' }}>
51
+ <MyazaButton label="Close" onPress={onClose} />
52
+ </View>
53
+ </View>
54
+ </KycSheet>
55
+ );
56
+ }
@@ -1,7 +1,8 @@
1
1
  import { useFonts } from 'expo-font';
2
2
  import { SpaceGrotesk_500Medium, SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk';
3
3
  import { Karla_400Regular, Karla_500Medium, Karla_600SemiBold, Karla_700Bold } from '@expo-google-fonts/karla';
4
- import type { TextStyle } from 'react-native';
4
+ export { fontFamilyFor, markFamilyName, brandFamilyName, BRAND_WEIGHTS } from '../config/font-resolve';
5
+
5
6
 
6
7
  // Typography fonts — the SAME families the Flutter SDK uses via google_fonts:
7
8
  // Space Grotesk for headings, Karla for body. Loaded at runtime through
@@ -29,21 +30,3 @@ export function useMyazaFonts(): boolean {
29
30
  * Returns `undefined` until fonts are loaded so text falls back to the system
30
31
  * font + `fontWeight` without triggering an "unrecognized font" warning.
31
32
  */
32
- export function fontFamilyFor(
33
- isHeading: boolean,
34
- weight: TextStyle['fontWeight'],
35
- loaded: boolean,
36
- ): string | undefined {
37
- if (!loaded) return undefined;
38
- const w = String(weight ?? '400');
39
- const bold = w === '700' || w === '800' || w === '900' || w === 'bold';
40
- if (isHeading) {
41
- if (bold) return 'SpaceGrotesk_700Bold';
42
- if (w === '500') return 'SpaceGrotesk_500Medium';
43
- return 'SpaceGrotesk_600SemiBold'; // 600 default for headings
44
- }
45
- if (bold) return 'Karla_700Bold';
46
- if (w === '600') return 'Karla_600SemiBold';
47
- if (w === '500') return 'Karla_500Medium';
48
- return 'Karla_400Regular';
49
- }
@@ -0,0 +1,52 @@
1
+ import type { IconName } from './Icon';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // "Here's what happens next" copy.
5
+ //
6
+ // A port of the Flutter SDK's ready_primer_content.dart and the web SDK's
7
+ // ready-primer-content.ts. Kept as data in ONE place per platform so the
8
+ // document and liveness screens cannot drift from each other, and so the three
9
+ // SDKs can be diffed string-for-string.
10
+ //
11
+ // Any change here must be mirrored in the other two — someone who starts on the
12
+ // hosted web flow and finishes in a native app should read the same words.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface ReadyChecklistItem {
16
+ icon: IconName;
17
+ label: string;
18
+ }
19
+
20
+ export interface ReadyContent {
21
+ icon: IconName;
22
+ title: string;
23
+ body: string;
24
+ /** What to expect. Three at most; past that nobody reads it. */
25
+ checklist: ReadyChecklistItem[];
26
+ }
27
+
28
+ export const READY_DOCUMENT: ReadyContent = {
29
+ icon: 'scan-line',
30
+ title: "You're about to scan your ID",
31
+ body:
32
+ "We'll photograph your document and read it automatically. " +
33
+ 'Nothing is shared until you submit.',
34
+ checklist: [
35
+ { icon: 'id-card', label: 'Have your physical document with you' },
36
+ { icon: 'sun', label: 'Find even lighting, avoid glare' },
37
+ { icon: 'timer', label: 'Takes about a minute' },
38
+ ],
39
+ };
40
+
41
+ export const READY_LIVENESS: ReadyContent = {
42
+ icon: 'scan-face',
43
+ title: "Let's confirm you're really here",
44
+ body:
45
+ "You'll follow a few short prompts on screen. This proves a real person " +
46
+ 'is present, not a photo or a recording.',
47
+ checklist: [
48
+ { icon: 'user', label: 'Put your face in the circle' },
49
+ { icon: 'sun', label: 'Find even lighting, remove sunglasses' },
50
+ { icon: 'timer', label: 'Takes about 10 seconds' },
51
+ ],
52
+ };
@@ -1,29 +1,25 @@
1
- import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
1
+ import React, { createContext, useContext, useMemo, useRef } from 'react';
2
2
  import { useStore } from 'zustand';
3
3
 
4
- import { createKycStore, type KycState, type KycStore } from '../store/kycStore';
5
- import { resolveColors, type MyazaColorScheme, type ThemeMode } from '../config/theme';
6
- import type { MyazaKYCConfig } from '../types/config';
7
- import { useMyazaFonts } from './fonts';
4
+ import { createKycStore, effectiveCountry, type KycState, type KycStore } from '../store/kycStore';
5
+ import type { ResolvedKYCConfig, SupportedCountry } from '../types/config';
6
+ import type { ServerConfigState } from '../store/serverConfig';
7
+ import { MyazaThemeProvider } from './theme-provider';
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
- // Runtime context — owns the per-instance zustand store + theme mode. Mirrors
11
- // the Flutter SDK's ProviderScope + theme provider wiring.
10
+ // Runtime context — owns the per-instance zustand store. Mirrors the Flutter
11
+ // SDK's ProviderScope wiring.
12
+ //
13
+ // Theming moved to its own provider (theme-provider.tsx) because the two have
14
+ // different lifetimes: the trigger button and the workflow-resolution barrier
15
+ // need colours BEFORE there is a resolved config to build a store from. This
16
+ // one composes it, so a resolved workflow's appearance takes over from the
17
+ // props' appearance inside the flow.
12
18
  // ---------------------------------------------------------------------------
13
19
 
14
- interface ThemeValue {
15
- mode: ThemeMode;
16
- colors: MyazaColorScheme;
17
- setMode: (mode: ThemeMode) => void;
18
- toggle: () => void;
19
- /** True once Space Grotesk + Karla are loaded (system font until then). */
20
- fontsLoaded: boolean;
21
- }
22
-
23
20
  interface RuntimeValue {
24
21
  store: KycStore;
25
- config: MyazaKYCConfig;
26
- theme: ThemeValue;
22
+ config: ResolvedKYCConfig;
27
23
  }
28
24
 
29
25
  const RuntimeContext = createContext<RuntimeValue | null>(null);
@@ -38,38 +34,28 @@ function useRuntime(): RuntimeValue {
38
34
 
39
35
  export function KycRuntimeProvider({
40
36
  config,
37
+ serverConfig,
41
38
  children,
42
39
  }: {
43
- config: MyazaKYCConfig;
40
+ config: ResolvedKYCConfig;
41
+ /** Preloaded from a resolved workflow — skips the flow's own `/config` call. */
42
+ serverConfig?: ServerConfigState;
44
43
  children: React.ReactNode;
45
44
  }): React.ReactElement {
46
45
  // The store is created once per provider instance (per modal launch).
47
46
  const storeRef = useRef<KycStore | null>(null);
48
47
  if (storeRef.current === null) {
49
- storeRef.current = createKycStore(config);
48
+ storeRef.current = createKycStore(config, serverConfig);
50
49
  }
51
50
  const store = storeRef.current;
52
51
 
53
- const [mode, setMode] = useState<ThemeMode>(config.appearance?.theme ?? 'light');
54
- const colors = useMemo(() => resolveColors(mode, config.appearance), [mode, config.appearance]);
55
- const fontsLoaded = useMyazaFonts();
52
+ const value = useMemo<RuntimeValue>(() => ({ store, config }), [store, config]);
56
53
 
57
- const value = useMemo<RuntimeValue>(
58
- () => ({
59
- store,
60
- config,
61
- theme: {
62
- mode,
63
- colors,
64
- setMode,
65
- toggle: () => setMode((m) => (m === 'dark' ? 'light' : 'dark')),
66
- fontsLoaded,
67
- },
68
- }),
69
- [store, config, mode, colors, fontsLoaded],
54
+ return (
55
+ <RuntimeContext.Provider value={value}>
56
+ <MyazaThemeProvider appearance={config.appearance}>{children}</MyazaThemeProvider>
57
+ </RuntimeContext.Provider>
70
58
  );
71
-
72
- return <RuntimeContext.Provider value={value}>{children}</RuntimeContext.Provider>;
73
59
  }
74
60
 
75
61
  /** Selects from the KYC store (re-renders only on selected-slice changes). */
@@ -82,12 +68,21 @@ export function useKycStore(): KycStore {
82
68
  return useRuntime().store;
83
69
  }
84
70
 
71
+ /**
72
+ * The country this session is verifying against — the one picked on a
73
+ * multi-region flow, else the config's.
74
+ *
75
+ * Screens must use THIS, never `config.country`: on a multi-region flow the
76
+ * config carries only the primary, so reading it directly offers one country's
77
+ * IDs and validates against another's.
78
+ */
79
+ export function useEffectiveCountry(): SupportedCountry {
80
+ return useKyc(effectiveCountry);
81
+ }
82
+
85
83
  /** The resolved SDK config (props + callbacks) for this launch. */
86
- export function useKycConfig(): MyazaKYCConfig {
84
+ export function useKycConfig(): ResolvedKYCConfig {
87
85
  return useRuntime().config;
88
86
  }
89
87
 
90
- /** Theme colors + light/dark mode controls. */
91
- export function useTheme(): ThemeValue {
92
- return useRuntime().theme;
93
- }
88
+ export { MyazaThemeProvider, useTheme, type ThemeValue } from './theme-provider';
@@ -0,0 +1,87 @@
1
+ import { ID_TYPES } from '../config/idTypes';
2
+ import { poaMaxAgeDays } from '../config/proofOfAddress';
3
+ import { documentCaptureMeta } from '../screens/DocumentCaptureStep';
4
+ import { questionnaireMeta } from '../screens/QuestionnaireStep';
5
+ import { contactCodeLength, contactMeta, type ContactChallenge } from '../config/contact';
6
+
7
+ import { proofOfAddressMeta } from '../screens/ProofOfAddressStep';
8
+ import { countrySelectMeta } from '../screens/CountrySelectStep';
9
+ import { businessDetailsMeta } from '../screens/BusinessDetailsStep';
10
+ import { businessKeyPeopleMeta } from '../screens/BusinessKeyPeopleStep';
11
+ import { businessDocumentsMeta } from '../screens/BusinessDocumentsStep';
12
+ import { applicantRoleMeta } from '../screens/ApplicantRoleStep';
13
+ import { nfcMeta } from '../screens/NfcStep';
14
+ import type { DocumentCapturePhase } from '../store/state';
15
+ import type { KYCStep, ResolvedKYCConfig, SupportedCountry } from '../types/config';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // The sheet header's title and description, per step.
19
+ //
20
+ // Split from KycFlow.tsx (200-line rule). Each step owns its own copy — the
21
+ // screens export their `…Meta` — and this only chooses between them, so adding
22
+ // a step means adding its meta beside the screen, not editing a shell.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function labelFor(country: SupportedCountry, idType: string | null): string {
26
+ if (!idType) return 'Document';
27
+ return Object.values(ID_TYPES).flat().find((t) => t.key === idType)?.label ?? 'Document';
28
+ }
29
+
30
+ export interface StepHeaderContext {
31
+ config: ResolvedKYCConfig;
32
+ country: SupportedCountry;
33
+ selectedIdType: string | null;
34
+ documentCapturePhase: DocumentCapturePhase;
35
+ /** Outstanding contact code — lets the header describe the OTP in flight. */
36
+ contactChallenge: ContactChallenge | null;
37
+ }
38
+
39
+ export function stepHeaderMeta(
40
+ currentStep: KYCStep,
41
+ { config, country, selectedIdType, documentCapturePhase, contactChallenge }: StepHeaderContext,
42
+ ): { title: string; description: string | null } {
43
+ const label = labelFor(country, selectedIdType);
44
+ switch (currentStep) {
45
+ case 'consent':
46
+ return { title: '', description: null as string | null };
47
+ case 'id-type':
48
+ return { title: 'Select ID Type', description: "Choose the type of identification document you'd like to use." };
49
+ case 'id-input':
50
+ return { title: 'Enter Your Details', description: `Provide your ${label} for verification.` };
51
+ case 'document-capture':
52
+ // Phase-aware title/description live in the header (synced from the
53
+ // capture screen via `documentCapturePhase`) — mirrors Flutter.
54
+ return documentCaptureMeta(documentCapturePhase, label);
55
+ case 'liveness':
56
+ return { title: 'Face Verification', description: 'Follow the on-screen instructions' };
57
+ case 'questionnaire':
58
+ return questionnaireMeta(config.questionnaire?.title, config.questionnaire?.description);
59
+ case 'email-verification':
60
+ return contactMeta('email', {
61
+ codeLength: contactCodeLength(config.emailVerification),
62
+ challenge: contactChallenge,
63
+ });
64
+ case 'phone-verification':
65
+ return contactMeta('phone', {
66
+ codeLength: contactCodeLength(config.phoneVerification),
67
+ challenge: contactChallenge,
68
+ });
69
+ case 'proof-of-address':
70
+ return proofOfAddressMeta(poaMaxAgeDays(config.proofOfAddress));
71
+ case 'country-select':
72
+ return countrySelectMeta;
73
+ case 'business-details':
74
+ return businessDetailsMeta;
75
+ case 'business-key-people':
76
+ return businessKeyPeopleMeta;
77
+ case 'business-documents':
78
+ return businessDocumentsMeta;
79
+ case 'applicant-role':
80
+ return applicantRoleMeta;
81
+ case 'nfc':
82
+ return nfcMeta;
83
+ case 'submitted':
84
+ default:
85
+ return { title: '', description: null };
86
+ }
87
+ }