@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,130 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { ContactCodeEntry } from '../components/ContactCodeEntry';
8
+ import { ExpiryCountdown } from '../components/ExpiryCountdown';
9
+ import { CHANNEL_LABELS, type PhoneOtpChannel } from '../config/contact';
10
+ import type { OtpInputStyle } from '../types/workflow';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // The second half of a contact check: the code has been sent, now enter it.
14
+ //
15
+ // Split from ContactVerificationStep (200-line rule). Auto-submits on the last
16
+ // digit — the user has already typed the whole code by then, and making them
17
+ // press a button as well is a step for nothing.
18
+ //
19
+ // Layout mirrors the web SDK: the code field, then a single row carrying the
20
+ // live expiry on the left and Resend on the right, then the switch-channel
21
+ // escape hatch. A code that never arrives is usually a channel problem rather
22
+ // than a typo, so resending down the same channel is rarely the fix.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export function ContactCodeStep({
26
+ codeLength,
27
+ inputStyle,
28
+ code,
29
+ busy,
30
+ error,
31
+ attemptsLeft,
32
+ expiresAt,
33
+ otherChannel,
34
+ onChange,
35
+ onCheck,
36
+ onResend,
37
+ }: {
38
+ codeLength: number;
39
+ inputStyle: OtpInputStyle;
40
+ code: string;
41
+ busy: boolean;
42
+ error: string | null;
43
+ attemptsLeft?: number;
44
+ /** ISO expiry from the send response; null falls back to static copy. */
45
+ expiresAt: string | null;
46
+ /** The channel NOT in use, when the workflow offers a second one. */
47
+ otherChannel: PhoneOtpChannel | null;
48
+ onChange: (code: string) => void;
49
+ onCheck: (code: string) => void;
50
+ /** Resends; with a channel, switches to it first. */
51
+ onResend: (switchTo?: PhoneOtpChannel) => void;
52
+ }): React.ReactElement {
53
+ const { colors } = useTheme();
54
+ // No "enter the code we sent to …" line here: the sheet header carries it
55
+ // (see contactMeta), matching the web SDK. Two copies drifted apart.
56
+ return (
57
+ <View>
58
+ <ContactCodeEntry
59
+ code={code}
60
+ onChange={onChange}
61
+ codeLength={codeLength}
62
+ style={inputStyle}
63
+ disabled={busy}
64
+ onComplete={onCheck}
65
+ />
66
+
67
+ <View style={{ height: spacing.sm }} />
68
+ <View
69
+ style={{
70
+ flexDirection: 'row',
71
+ alignItems: 'center',
72
+ justifyContent: 'space-between',
73
+ }}
74
+ >
75
+ {expiresAt ? (
76
+ <ExpiryCountdown expiresAt={expiresAt} />
77
+ ) : (
78
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
79
+ The code expires in 5 minutes.
80
+ </MyazaText>
81
+ )}
82
+ <Pressable
83
+ onPress={() => onResend()}
84
+ disabled={busy}
85
+ accessibilityRole="button"
86
+ hitSlop={8}
87
+ >
88
+ <MyazaText
89
+ variant="bodySmall"
90
+ color={busy ? colors.textMuted : colors.primary}
91
+ style={{ fontWeight: '600' }}
92
+ >
93
+ Resend code
94
+ </MyazaText>
95
+ </Pressable>
96
+ </View>
97
+
98
+ {otherChannel ? (
99
+ <>
100
+ <View style={{ height: spacing.sm }} />
101
+ <Pressable
102
+ onPress={() => onResend(otherChannel)}
103
+ disabled={busy}
104
+ accessibilityRole="button"
105
+ hitSlop={8}
106
+ >
107
+ <MyazaText
108
+ variant="bodySmall"
109
+ color={colors.textMuted}
110
+ style={{ textAlign: 'center' }}
111
+ >
112
+ {`Didn't get it? Send by ${CHANNEL_LABELS[otherChannel]} instead`}
113
+ </MyazaText>
114
+ </Pressable>
115
+ </>
116
+ ) : null}
117
+
118
+ {error ? (
119
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
120
+ {/* The remaining count is what stops someone burning the whole
121
+ budget guessing — without it there is no signal the code is
122
+ about to die. */}
123
+ {attemptsLeft !== undefined
124
+ ? `${error} ${attemptsLeft} ${attemptsLeft === 1 ? 'attempt' : 'attempts'} left.`
125
+ : error}
126
+ </MyazaText>
127
+ ) : null}
128
+ </View>
129
+ );
130
+ }
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { MyazaInput } from '../components/MyazaInput';
8
+ import { PhoneNumberInput } from '../components/PhoneNumberInput';
9
+
10
+ /**
11
+ * "Where should we send it": an email field, or the dial-code phone input that
12
+ * formats nationally and emits E.164. Mirrors the web SDK's component of the
13
+ * same name and Flutter's ContactDestinationField.
14
+ */
15
+ export function ContactDestinationField({
16
+ isEmail,
17
+ email,
18
+ onEmailChange,
19
+ onPhoneChange,
20
+ defaultCountry,
21
+ error,
22
+ disabled,
23
+ }: {
24
+ isEmail: boolean;
25
+ email: string;
26
+ onEmailChange: (value: string) => void;
27
+ onPhoneChange: (value: { e164: string; isValid: boolean }) => void;
28
+ defaultCountry?: string;
29
+ error: string | null;
30
+ disabled?: boolean;
31
+ }): React.ReactElement {
32
+ if (isEmail) {
33
+ return (
34
+ <MyazaInput
35
+ label="Email address"
36
+ value={email}
37
+ onChangeText={onEmailChange}
38
+ placeholder="you@example.com"
39
+ keyboardType="email-address"
40
+ autoCapitalize="none"
41
+ error={error}
42
+ editable={!disabled}
43
+ autoFocus
44
+ />
45
+ );
46
+ }
47
+
48
+ return (
49
+ <View>
50
+ <MyazaText variant="label" style={{ marginBottom: spacing.xs }}>
51
+ Phone number
52
+ </MyazaText>
53
+ <PhoneNumberInput
54
+ defaultCountry={defaultCountry}
55
+ disabled={disabled}
56
+ onChange={onPhoneChange}
57
+ />
58
+ {error ? <ErrorLine text={error} /> : null}
59
+ </View>
60
+ );
61
+ }
62
+
63
+ function ErrorLine({ text }: { text: string }): React.ReactElement {
64
+ const { colors } = useTheme();
65
+ return (
66
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
67
+ {text}
68
+ </MyazaText>
69
+ );
70
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { Icon } from '../components/Icon';
8
+
9
+ /**
10
+ * The reassurance line under the contact step's actions, matching the web SDK.
11
+ *
12
+ * Small, but it answers the question the step provokes: an email step is asked
13
+ * "what will you do with my address", a phone step "will this cost me money".
14
+ */
15
+ export function ContactFooterNote({ isEmail }: { isEmail: boolean }): React.ReactElement {
16
+ const { colors } = useTheme();
17
+ return (
18
+ <View
19
+ style={{
20
+ flexDirection: 'row',
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ gap: spacing.xs,
24
+ }}
25
+ >
26
+ <Icon name={isEmail ? 'mail' : 'smartphone'} size={12} color={colors.textMuted} />
27
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
28
+ {isEmail
29
+ ? 'We only use this to verify your identity.'
30
+ : 'Standard message rates may apply.'}
31
+ </MyazaText>
32
+ </View>
33
+ );
34
+ }
@@ -0,0 +1,199 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { useEffectiveCountry, useKyc, useKycConfig, useKycStore } from '../components/runtime';
6
+ import { MyazaButton } from '../components/MyazaButton';
7
+ import { ContactDestinationField } from './ContactDestinationField';
8
+ import { ContactVerifiedPanel } from './ContactVerifiedPanel';
9
+ import { ContactCodeStep } from './ContactCodeStep';
10
+ import { ContactChannelChoice } from './ContactChannelChoice';
11
+ import { ContactFooterNote } from './ContactFooterNote';
12
+ import {
13
+ contactCodeLength,
14
+ contactIsRequired,
15
+ isValidContactEmail,
16
+ offeredPhoneChannels,
17
+ type PhoneOtpChannel,
18
+ } from '../config/contact';
19
+ import { attemptsRemaining, describeCheckError, describeSendError } from '../services/contactErrors';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Contact-verification OTP step — email or phone, one component, two mounts.
23
+ //
24
+ // enter destination → send → code entry → verified → continue
25
+ //
26
+ // The proof token is single-use and rides the /verify submission. The server
27
+ // enforces whether it was required; the `required: false` skip here is UX, and
28
+ // a skipped optional check simply submits without the proof.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export function ContactVerificationStep({
32
+ channel,
33
+ }: {
34
+ channel: 'email' | 'phone';
35
+ }): React.ReactElement {
36
+ const config = useKycConfig();
37
+ const country = useEffectiveCountry();
38
+ const store = useKycStore();
39
+ const contact = useKyc((s) => s.contact);
40
+
41
+ const isEmail = channel === 'email';
42
+ const stepConfig = isEmail ? config.emailVerification : config.phoneVerification;
43
+ const required = contactIsRequired(stepConfig);
44
+ const codeLength = contactCodeLength(stepConfig);
45
+ const inputStyle = stepConfig?.inputStyle ?? 'segmented';
46
+ // The org decides which channels are on offer; the user picks between them.
47
+ const offeredChannels: PhoneOtpChannel[] = isEmail
48
+ ? []
49
+ : offeredPhoneChannels(config.phoneVerification?.channels);
50
+ const [via, setVia] = useState<PhoneOtpChannel>(offeredChannels[0] ?? 'sms');
51
+
52
+ const alreadyVerified = isEmail ? contact.emailToken != null : contact.phoneToken != null;
53
+
54
+ const [destination, setDestination] = useState(contact.emailAddress ?? '');
55
+ // The phone field emits E.164 + validity from libphonenumber, so the step no
56
+ // longer guesses either from raw text.
57
+ const [phone, setPhone] = useState<{ e164: string; isValid: boolean }>({
58
+ e164: contact.phoneNumber ?? '',
59
+ isValid: false,
60
+ });
61
+ const [challengeId, setChallengeId] = useState<string | null>(null);
62
+ const [expiresAt, setExpiresAt] = useState<string | null>(null);
63
+ const [code, setCode] = useState('');
64
+ const [busy, setBusy] = useState(false);
65
+ const [error, setError] = useState<string | null>(null);
66
+ const [attemptsLeft, setAttemptsLeft] = useState<number | undefined>(undefined);
67
+ const otherChannel = offeredChannels.find((c) => c !== via) ?? null;
68
+
69
+ const value = isEmail ? destination.trim() : phone.e164;
70
+ const canSend = isEmail ? isValidContactEmail(value) : phone.isValid;
71
+
72
+ const advance = useCallback(() => store.getState().nextStep(), [store]);
73
+
74
+ // The sheet header is rendered by the shell and cannot see this step's state,
75
+ // so publish what it needs to caption: the channel in play now, and the
76
+ // destination once a code is actually out. Re-published when the user picks a
77
+ // different delivery channel so the header's "by WhatsApp" follows the choice.
78
+ useEffect(() => {
79
+ store.getState().setContactChallenge({
80
+ channel,
81
+ ...(isEmail ? {} : { via }),
82
+ ...(challengeId ? { destination: value } : {}),
83
+ });
84
+ }, [store, channel, isEmail, via, challengeId, value]);
85
+
86
+ /** `switchTo` re-sends over the other channel when the first one didn't land. */
87
+ const send = async (switchTo?: PhoneOtpChannel): Promise<void> => {
88
+ const sendVia = switchTo ?? via;
89
+ if (switchTo) setVia(switchTo);
90
+ setBusy(true);
91
+ setError(null);
92
+ setAttemptsLeft(undefined);
93
+ try {
94
+ const res = await store.getState().api.contactSend({
95
+ channel,
96
+ destination: value,
97
+ codeLength,
98
+ ...(stepConfig?.maxAttempts != null ? { maxAttempts: stepConfig.maxAttempts } : {}),
99
+ ...(!isEmail && country ? { country } : {}),
100
+ ...(isEmail ? {} : { via: sendVia }),
101
+ });
102
+ // Remember what was typed BEFORE the code lands: a user who backs out
103
+ // here and returns should not have to retype it.
104
+ store.getState().setContactDestination(channel, value);
105
+ setChallengeId(res.challengeId);
106
+ setExpiresAt(res.expiresAt ?? null);
107
+ setCode('');
108
+ } catch (err) {
109
+ setError(describeSendError(err));
110
+ } finally {
111
+ setBusy(false);
112
+ }
113
+ };
114
+
115
+ const check = async (submitted: string): Promise<void> => {
116
+ if (!challengeId || submitted.length !== codeLength) return;
117
+ setBusy(true);
118
+ setError(null);
119
+ try {
120
+ const res = await store.getState().api.contactCheck({ challengeId, code: submitted });
121
+ store.getState().setContactVerified(channel, value, res.token);
122
+ advance();
123
+ } catch (err) {
124
+ setError(describeCheckError(err));
125
+ setAttemptsLeft(attemptsRemaining(err));
126
+ setCode('');
127
+ } finally {
128
+ setBusy(false);
129
+ }
130
+ };
131
+
132
+ // Returning to a step already passed — nothing left to do here.
133
+ if (alreadyVerified) {
134
+ return (
135
+ <ContactVerifiedPanel
136
+ isEmail={isEmail}
137
+ destination={isEmail ? contact.emailAddress : contact.phoneNumber}
138
+ onContinue={advance}
139
+ />
140
+ );
141
+ }
142
+
143
+ // One frame for both halves, like web: the panel swaps, the actions stay put.
144
+ return (
145
+ <View>
146
+ {challengeId ? (
147
+ <ContactCodeStep
148
+ codeLength={codeLength}
149
+ inputStyle={inputStyle}
150
+ code={code}
151
+ busy={busy}
152
+ error={error}
153
+ attemptsLeft={attemptsLeft}
154
+ expiresAt={expiresAt}
155
+ otherChannel={otherChannel}
156
+ onChange={setCode}
157
+ onCheck={(full) => void check(full)}
158
+ onResend={(switchTo) => void send(switchTo)}
159
+ />
160
+ ) : (
161
+ <>
162
+ <ContactDestinationField
163
+ isEmail={isEmail}
164
+ email={destination}
165
+ onEmailChange={setDestination}
166
+ onPhoneChange={setPhone}
167
+ defaultCountry={config.phoneVerification?.defaultCountry ?? country ?? undefined}
168
+ error={error}
169
+ disabled={busy}
170
+ />
171
+ <ContactChannelChoice
172
+ offered={offeredChannels}
173
+ picked={via}
174
+ disabled={busy}
175
+ onPick={setVia}
176
+ />
177
+ </>
178
+ )}
179
+
180
+ <View style={{ height: spacing.md }} />
181
+ <MyazaButton
182
+ label={challengeId ? 'Verify code' : 'Send code'}
183
+ loading={busy}
184
+ disabled={challengeId ? code.length !== codeLength : !canSend}
185
+ onPress={() => (challengeId ? void check(code) : void send())}
186
+ />
187
+
188
+ {!required ? (
189
+ <>
190
+ <View style={{ height: spacing.sm }} />
191
+ <MyazaButton label="Skip for now" variant="ghost" disabled={busy} onPress={advance} />
192
+ </>
193
+ ) : null}
194
+
195
+ <View style={{ height: spacing.md }} />
196
+ <ContactFooterNote isEmail={isEmail} />
197
+ </View>
198
+ );
199
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { MyazaButton } from '../components/MyazaButton';
6
+ import { VerifiedNotice } from '../components/VerifiedNotice';
7
+
8
+ /**
9
+ * Shown when the user returns to a contact step they already passed: the
10
+ * confirmation card plus Continue. Mirrors the web SDK's verified branch and
11
+ * Flutter's ContactVerifiedView.
12
+ */
13
+ export function ContactVerifiedPanel({
14
+ isEmail,
15
+ destination,
16
+ onContinue,
17
+ }: {
18
+ isEmail: boolean;
19
+ /** The verified address/number, when known — shown verbatim, like web. */
20
+ destination?: string;
21
+ onContinue: () => void;
22
+ }): React.ReactElement {
23
+ return (
24
+ <View>
25
+ <VerifiedNotice
26
+ label={
27
+ destination
28
+ ? `${destination} is verified.`
29
+ : `Your ${isEmail ? 'email' : 'phone number'} is verified.`
30
+ }
31
+ />
32
+ <View style={{ height: spacing.md }} />
33
+ <MyazaButton label="Continue" onPress={onContinue} />
34
+ </View>
35
+ );
36
+ }
@@ -0,0 +1,210 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Pressable, ScrollView, 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 { MyazaInput } from '../components/MyazaInput';
8
+ import { CountryFlag } from '../components/CountryFlag';
9
+ import { Icon } from '../components/Icon';
10
+ import { groupCountriesByRegion, regionCountryName } from '../config/regions';
11
+ import { countrySelectOptions } from '../store/derive';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Country selection for multi-region flows (`countries` has more than one).
15
+ //
16
+ // The pick sets the session's EFFECTIVE country, which every later step reads —
17
+ // the ID types on offer, the validators, the endpoints and chip capability all
18
+ // follow from it.
19
+ //
20
+ // Above a handful of countries a flat list stops being a chooser and becomes a
21
+ // scroll, so past SEARCH_THRESHOLD this switches to a searchable, region-grouped
22
+ // picker. Global Documents can offer ~200 countries.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Above this many offered countries the flat list becomes a searchable,
26
+ * region-grouped picker. Exported because the sheet has to know too: the
27
+ * picker pins its search box and therefore needs a non-scrolling body. */
28
+ export const COUNTRY_SEARCH_THRESHOLD = 5;
29
+
30
+ export const countrySelectMeta = {
31
+ title: 'Where was your ID issued?',
32
+ description: 'Choose the country that issued your identity document.',
33
+ };
34
+
35
+ export function CountrySelectStep(): React.ReactElement {
36
+ const config = useKycConfig();
37
+ const store = useKycStore();
38
+ // Only an actual PICK counts as selected. `useEffectiveCountry` falls back to
39
+ // the workflow's primary country, which on this screen would render a
40
+ // highlighted row for a choice the user has not made yet.
41
+ const selected = useKyc((s) => s.selectedCountry);
42
+
43
+ // Multi-region flows carry `countries`; the KYB applicant leg does not, so
44
+ // it offers the org's GRANTED countries instead — see countrySelectOptions.
45
+ const serverConfig = useKyc((s) => s.serverConfig);
46
+ const options = useMemo(
47
+ () => countrySelectOptions({ config, serverConfig }),
48
+ [config, serverConfig],
49
+ );
50
+
51
+ const pick = (country: string): void => {
52
+ store.getState().setCountry(country);
53
+ store.getState().nextStep();
54
+ };
55
+
56
+ if (options.length > COUNTRY_SEARCH_THRESHOLD) {
57
+ return <CountryRegionPicker countries={options} selected={selected} onPick={pick} />;
58
+ }
59
+
60
+ return (
61
+ <View>
62
+ {options.map((country) => (
63
+ <CountryRow
64
+ key={country}
65
+ code={country}
66
+ name={regionCountryName(country)}
67
+ selected={country === selected}
68
+ onPress={() => pick(country)}
69
+ />
70
+ ))}
71
+ </View>
72
+ );
73
+ }
74
+
75
+ function CountryRegionPicker({
76
+ countries,
77
+ selected,
78
+ onPick,
79
+ }: {
80
+ countries: string[];
81
+ selected: string | null;
82
+ onPick: (country: string) => void;
83
+ }): React.ReactElement {
84
+ const { colors } = useTheme();
85
+ const [query, setQuery] = useState('');
86
+
87
+ const groups = useMemo(() => {
88
+ const needle = query.trim().toLowerCase();
89
+ const filtered = needle
90
+ ? countries.filter(
91
+ (code) =>
92
+ code.toLowerCase().includes(needle) ||
93
+ regionCountryName(code).toLowerCase().includes(needle),
94
+ )
95
+ : countries;
96
+ return groupCountriesByRegion(filtered);
97
+ }, [countries, query]);
98
+
99
+ const empty = groups.length === 0;
100
+
101
+ return (
102
+ // The search box stays put and the list owns its own scroll, so a
103
+ // ~240-country flow does not scroll the search field off the top. That
104
+ // relies on the SHEET rendering this step with `fillsViewport` — inside a
105
+ // scroll view the nested list below would take an unbounded height, outgrow
106
+ // the viewport, and drag the search box off the top with it.
107
+ <View style={{ flex: 1 }}>
108
+ <MyazaInput
109
+ value={query}
110
+ onChangeText={setQuery}
111
+ placeholder="Search countries…"
112
+ autoCapitalize="none"
113
+ prefix={<Icon name="search" size={18} color={colors.textSecondary} />}
114
+ />
115
+ <View style={{ height: spacing.md }} />
116
+
117
+ {empty ? (
118
+ <MyazaText variant="bodyMedium" style={{ textAlign: 'center', marginTop: spacing.lg }}>
119
+ No countries match your search.
120
+ </MyazaText>
121
+ ) : (
122
+ // `nestedScrollEnabled` stays: Android refuses to scroll a nested
123
+ // same-direction list without it, and this is still nested on the
124
+ // flat-list path and inside any future scrolling parent.
125
+ <ScrollView
126
+ nestedScrollEnabled
127
+ keyboardShouldPersistTaps="handled"
128
+ showsVerticalScrollIndicator={false}
129
+ >
130
+ {groups.map((group) => (
131
+ <View key={group.region}>
132
+ {/* Uppercase, tracked-out section label — Flutter's region
133
+ header. Lowercase text at the default weight read as another
134
+ list row rather than a divider. */}
135
+ <MyazaText
136
+ variant="bodySmall"
137
+ color={colors.textSecondary}
138
+ style={{
139
+ fontWeight: '700',
140
+ letterSpacing: 0.6,
141
+ paddingHorizontal: spacing.xs,
142
+ paddingVertical: spacing.sm,
143
+ }}
144
+ >
145
+ {group.region.toUpperCase()}
146
+ </MyazaText>
147
+ {group.countries.map((entry) => (
148
+ <CountryRow
149
+ key={entry.code}
150
+ code={entry.code}
151
+ name={entry.name}
152
+ selected={entry.code === selected}
153
+ onPress={() => onPick(entry.code)}
154
+ />
155
+ ))}
156
+ </View>
157
+ ))}
158
+ </ScrollView>
159
+ )}
160
+ </View>
161
+ );
162
+ }
163
+
164
+ function CountryRow({
165
+ code,
166
+ name,
167
+ selected,
168
+ onPress,
169
+ }: {
170
+ code: string;
171
+ name: string;
172
+ selected: boolean;
173
+ onPress: () => void;
174
+ }): React.ReactElement {
175
+ const { colors } = useTheme();
176
+ return (
177
+ <Pressable
178
+ onPress={onPress}
179
+ accessibilityRole="radio"
180
+ accessibilityState={{ checked: selected }}
181
+ accessibilityLabel={name}
182
+ // Matches the Flutter SDK's CountryOptionTile exactly — the two had
183
+ // drifted (24px flag / 12px padding here vs 32 / 16 there), which is why
184
+ // the same screen looked like a different component on each platform. The
185
+ // spacing and radius scales are identical across the SDKs, so there is
186
+ // nothing platform-specific to reconcile: `md` means 16 on both.
187
+ style={{
188
+ flexDirection: 'row',
189
+ alignItems: 'center',
190
+ padding: spacing.md,
191
+ borderRadius: radius.md,
192
+ borderWidth: selected ? 1.5 : 1,
193
+ borderColor: selected ? colors.primary : colors.border,
194
+ backgroundColor: selected ? colors.primary50 : colors.background,
195
+ marginBottom: spacing.sm,
196
+ }}
197
+ >
198
+ <CountryFlag country={code} size={32} />
199
+ <View style={{ width: 12 }} />
200
+ <MyazaText variant="label" style={{ flex: 1, fontWeight: '500' }}>
201
+ {name}
202
+ </MyazaText>
203
+ <Icon
204
+ name="chevron-right"
205
+ size={18}
206
+ color={selected ? colors.primary : colors.textSecondary}
207
+ />
208
+ </Pressable>
209
+ );
210
+ }