@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,133 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { Icon } from '../components/Icon';
8
+ import { WhatsAppIcon } from '../components/WhatsAppIcon';
9
+ import { CHANNEL_LABELS, type PhoneOtpChannel } from '../config/contact';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // How the user wants their one-time code delivered.
13
+ //
14
+ // The org decides which channels are ON OFFER; the person receiving the code
15
+ // decides between them, because only they know whether they have WhatsApp
16
+ // installed or whether SMS is landing for them today. With a single offered
17
+ // channel there is no choice to make and this renders nothing.
18
+ //
19
+ // Card shape, icon and selected treatment mirror the web SDK's picker so the
20
+ // step reads the same on every platform.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const HINTS: Record<PhoneOtpChannel, string> = {
24
+ sms: 'Text message',
25
+ whatsapp: 'Needs WhatsApp',
26
+ };
27
+
28
+ /** WhatsApp needs its own brand mark; SMS reads fine as a generic glyph. */
29
+ function ChannelGlyph({
30
+ channel,
31
+ color,
32
+ }: {
33
+ channel: PhoneOtpChannel;
34
+ color: string;
35
+ }): React.ReactElement {
36
+ return channel === 'whatsapp' ? (
37
+ <WhatsAppIcon size={16} color={color} />
38
+ ) : (
39
+ <Icon name="message-square" size={16} color={color} />
40
+ );
41
+ }
42
+
43
+ export function ContactChannelChoice({
44
+ offered,
45
+ picked,
46
+ disabled,
47
+ onPick,
48
+ }: {
49
+ offered: PhoneOtpChannel[];
50
+ picked: PhoneOtpChannel;
51
+ disabled?: boolean;
52
+ onPick: (channel: PhoneOtpChannel) => void;
53
+ }): React.ReactElement | null {
54
+ const { colors } = useTheme();
55
+ if (offered.length < 2) return null;
56
+
57
+ return (
58
+ <View>
59
+ <View style={{ height: spacing.md }} />
60
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
61
+ How should we send it?
62
+ </MyazaText>
63
+ <View style={{ height: spacing.xs }} />
64
+ <View style={{ flexDirection: 'row', gap: spacing.sm }}>
65
+ {offered.map((channel) => (
66
+ <ChannelTile
67
+ key={channel}
68
+ channel={channel}
69
+ selected={picked === channel}
70
+ disabled={disabled}
71
+ onPress={() => onPick(channel)}
72
+ />
73
+ ))}
74
+ </View>
75
+ </View>
76
+ );
77
+ }
78
+
79
+ function ChannelTile({
80
+ channel,
81
+ selected,
82
+ disabled,
83
+ onPress,
84
+ }: {
85
+ channel: PhoneOtpChannel;
86
+ selected: boolean;
87
+ disabled?: boolean;
88
+ onPress: () => void;
89
+ }): React.ReactElement {
90
+ const { colors } = useTheme();
91
+
92
+ return (
93
+ <Pressable
94
+ onPress={disabled ? undefined : onPress}
95
+ accessibilityRole="radio"
96
+ accessibilityState={{ checked: selected, disabled }}
97
+ accessibilityLabel={CHANNEL_LABELS[channel]}
98
+ style={{
99
+ flex: 1,
100
+ flexDirection: 'row',
101
+ alignItems: 'center',
102
+ gap: spacing.sm,
103
+ paddingHorizontal: spacing.sm,
104
+ paddingVertical: spacing.sm + 2,
105
+ borderRadius: radius.md,
106
+ borderWidth: 1.5,
107
+ borderColor: selected ? colors.primary : colors.border,
108
+ backgroundColor: selected ? `${colors.primary}0D` : 'transparent',
109
+ }}
110
+ >
111
+ <View
112
+ style={{
113
+ width: 34,
114
+ height: 34,
115
+ borderRadius: radius.sm,
116
+ alignItems: 'center',
117
+ justifyContent: 'center',
118
+ backgroundColor: selected ? `${colors.primary}1A` : colors.backgroundSecondary,
119
+ }}
120
+ >
121
+ <ChannelGlyph channel={channel} color={selected ? colors.primary : colors.textMuted} />
122
+ </View>
123
+ <View style={{ flex: 1, minWidth: 0 }}>
124
+ <MyazaText variant="bodyMedium" style={{ fontWeight: "600" }}>
125
+ {CHANNEL_LABELS[channel]}
126
+ </MyazaText>
127
+ <MyazaText variant="bodySmall" color={colors.textMuted} numberOfLines={1}>
128
+ {HINTS[channel]}
129
+ </MyazaText>
130
+ </View>
131
+ </Pressable>
132
+ );
133
+ }
@@ -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
+ }