@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.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 (246) hide show
  1. package/README.md +151 -5
  2. package/android/build.gradle +35 -3
  3. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
  4. package/app.plugin.js +89 -8
  5. package/ios/HybridMyazaFaceDetector.swift +16 -3
  6. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  7. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  8. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  9. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  10. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  11. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  12. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  13. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  15. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  17. package/package.json +14 -2
  18. package/src/MyazaBiometricAuth.tsx +119 -0
  19. package/src/MyazaKYC.tsx +12 -0
  20. package/src/components/CountryField.tsx +8 -0
  21. package/src/components/CountryRegionPicker.tsx +184 -0
  22. package/src/components/DashedBorder.tsx +55 -0
  23. package/src/components/DialCodePicker.tsx +53 -67
  24. package/src/components/DialCodeRow.tsx +102 -0
  25. package/src/components/FramedMapPicker.tsx +122 -0
  26. package/src/components/GeoBadge.tsx +34 -0
  27. package/src/components/Icon.tsx +2 -176
  28. package/src/components/KycFlow.tsx +60 -32
  29. package/src/components/LineSkeleton.tsx +100 -0
  30. package/src/components/MapChrome.tsx +67 -0
  31. package/src/components/MapPinMarker.tsx +54 -0
  32. package/src/components/MapPinPicker.tsx +179 -0
  33. package/src/components/MilestoneTrack.tsx +155 -0
  34. package/src/components/MultiIdProgress.tsx +107 -0
  35. package/src/components/MyazaInput.tsx +39 -4
  36. package/src/components/MyazaSelect.tsx +2 -1
  37. package/src/components/OptionRow.tsx +22 -3
  38. package/src/components/OwnershipSlider.tsx +102 -0
  39. package/src/components/PhoneNumberInput.tsx +45 -4
  40. package/src/components/PoweredBy.tsx +5 -5
  41. package/src/components/PresenceBlocks.tsx +153 -0
  42. package/src/components/StaggerIn.tsx +63 -0
  43. package/src/components/StepView.tsx +43 -0
  44. package/src/components/StickyActions.tsx +56 -0
  45. package/src/components/dialCodeRows.ts +64 -0
  46. package/src/components/glass/FloatingSheet.tsx +73 -21
  47. package/src/components/glass/GlassSheet.tsx +14 -2
  48. package/src/components/icon-map.ts +176 -0
  49. package/src/components/stepHeaderMeta.tsx +45 -5
  50. package/src/config/addressCollection.ts +125 -0
  51. package/src/config/biometricOptions.ts +100 -0
  52. package/src/config/business.ts +26 -7
  53. package/src/config/businessDetailsValidity.ts +52 -0
  54. package/src/config/businessPrefill.ts +72 -0
  55. package/src/config/businessSteps.ts +20 -3
  56. package/src/config/consentStep.ts +19 -0
  57. package/src/config/keyPeople.ts +111 -7
  58. package/src/config/keyPeopleCorporate.ts +19 -0
  59. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  60. package/src/config/keyPeopleOwners.ts +26 -0
  61. package/src/config/keyPeoplePrefill.ts +138 -0
  62. package/src/config/keyPeopleSectionDefs.ts +65 -0
  63. package/src/config/keyPeopleSections.ts +167 -0
  64. package/src/config/proofOfAddress.ts +54 -11
  65. package/src/config/regions.ts +24 -0
  66. package/src/config/registrationHint.ts +1 -1
  67. package/src/config/stepOrder.ts +100 -6
  68. package/src/config/theme.ts +8 -2
  69. package/src/config/uploadLimits.ts +35 -0
  70. package/src/config/website.ts +36 -0
  71. package/src/config/workflowMerge.ts +16 -5
  72. package/src/emrtd/activeAuth.ts +100 -0
  73. package/src/emrtd/extras.ts +76 -0
  74. package/src/emrtd/files.ts +5 -0
  75. package/src/emrtd/index.ts +1 -0
  76. package/src/emrtd/read.ts +8 -1
  77. package/src/emrtd/session.ts +41 -0
  78. package/src/emrtd/stages.ts +11 -0
  79. package/src/index.ts +54 -1
  80. package/src/lib/address-current-location.ts +171 -0
  81. package/src/lib/address-field-modes.ts +119 -0
  82. package/src/lib/address-flow.ts +172 -0
  83. package/src/lib/address-helpers.ts +48 -0
  84. package/src/lib/address-line.ts +76 -0
  85. package/src/lib/address-pin-move.ts +97 -0
  86. package/src/lib/address-step-recovery.ts +63 -0
  87. package/src/lib/authed-image.ts +78 -0
  88. package/src/lib/biometric-auth.ts +52 -0
  89. package/src/lib/biometric-copy.ts +51 -0
  90. package/src/lib/captureRing.ts +83 -0
  91. package/src/lib/contact-recovery.ts +44 -0
  92. package/src/lib/country-adoption.ts +89 -0
  93. package/src/lib/inferred-country.ts +65 -0
  94. package/src/lib/livenessLayout.ts +49 -0
  95. package/src/lib/map-frame.ts +159 -0
  96. package/src/lib/map-tiles.ts +143 -0
  97. package/src/lib/multi-id.ts +210 -0
  98. package/src/lib/poa-country-gate.ts +27 -0
  99. package/src/lib/resubmit.ts +130 -0
  100. package/src/lib/result-copy.ts +116 -0
  101. package/src/lib/result-wait.ts +53 -0
  102. package/src/lib/review-map-surface.ts +26 -0
  103. package/src/lib/scope.ts +31 -0
  104. package/src/lib/screen-corners.ts +50 -0
  105. package/src/lib/selfie-upload-wait.ts +79 -0
  106. package/src/lib/step-log.ts +24 -3
  107. package/src/lib/street-view-fov.ts +42 -0
  108. package/src/lib/use-multi-id-plan.ts +29 -0
  109. package/src/lib/webview-available.ts +47 -0
  110. package/src/liveness/useLiveness.ts +6 -1
  111. package/src/liveness/useModelReady.ts +65 -0
  112. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  113. package/src/presence/background-math.ts +100 -0
  114. package/src/presence/background-store.ts +82 -0
  115. package/src/presence/background.ts +165 -0
  116. package/src/presence/foreground-service.ts +193 -0
  117. package/src/presence/fs.ts +51 -0
  118. package/src/presence/geofence.ts +41 -0
  119. package/src/presence/math.ts +44 -0
  120. package/src/presence/post.ts +40 -0
  121. package/src/presence/report.ts +87 -0
  122. package/src/presence/sampler.ts +110 -0
  123. package/src/presence/status.ts +92 -0
  124. package/src/presence/store.ts +94 -0
  125. package/src/presence/tier.ts +36 -0
  126. package/src/presence/watch-wait.ts +81 -0
  127. package/src/screens/AddressCountryControl.tsx +115 -0
  128. package/src/screens/ApplicantRoleStep.tsx +9 -5
  129. package/src/screens/BusinessCheckPanel.tsx +89 -0
  130. package/src/screens/BusinessDetailsFields.tsx +112 -0
  131. package/src/screens/BusinessDetailsStep.tsx +133 -145
  132. package/src/screens/BusinessDocumentSlot.tsx +5 -5
  133. package/src/screens/BusinessDocumentsStep.tsx +5 -56
  134. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  135. package/src/screens/BusinessPickedCard.tsx +86 -0
  136. package/src/screens/BusinessPickedSection.tsx +51 -0
  137. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  138. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  139. package/src/screens/BusinessSearch.tsx +199 -0
  140. package/src/screens/BusinessSearchResults.tsx +115 -0
  141. package/src/screens/CompanyInfoFields.tsx +91 -51
  142. package/src/screens/ConsentStep.tsx +37 -17
  143. package/src/screens/ContactActions.tsx +54 -0
  144. package/src/screens/ContactDestinationField.tsx +5 -0
  145. package/src/screens/ContactEntryPanel.tsx +75 -0
  146. package/src/screens/ContactVerificationStep.tsx +43 -42
  147. package/src/screens/CountrySelectStep.tsx +13 -147
  148. package/src/screens/IdTypeStep.tsx +12 -5
  149. package/src/screens/KeepLinksSheet.tsx +88 -0
  150. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  151. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  152. package/src/screens/KeyPeoplePending.tsx +154 -0
  153. package/src/screens/KeyPeopleSection.tsx +162 -0
  154. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  155. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  156. package/src/screens/KeyPersonCard.tsx +36 -4
  157. package/src/screens/KeyPersonForm.tsx +195 -36
  158. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  159. package/src/screens/KeyPersonOwners.tsx +109 -0
  160. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  161. package/src/screens/KeyPersonSheet.tsx +92 -108
  162. package/src/screens/LivenessAvatar.tsx +15 -3
  163. package/src/screens/LivenessStep.tsx +172 -52
  164. package/src/screens/NfcStep.tsx +14 -0
  165. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  166. package/src/screens/ProofOfAddressParts.tsx +151 -0
  167. package/src/screens/ProofOfAddressStep.tsx +58 -96
  168. package/src/screens/SubmittedBadge.tsx +25 -0
  169. package/src/screens/SubmittedError.tsx +64 -0
  170. package/src/screens/SubmittedResult.tsx +117 -0
  171. package/src/screens/SubmittedStep.tsx +82 -119
  172. package/src/screens/SubmittedSuccess.tsx +127 -0
  173. package/src/screens/SubmittedWaiting.tsx +45 -0
  174. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  175. package/src/screens/address/AddressIntroGate.tsx +148 -0
  176. package/src/screens/address/AddressMapStub.tsx +50 -0
  177. package/src/screens/address/AddressPinStep.tsx +178 -0
  178. package/src/screens/address/AddressReviewStep.tsx +162 -0
  179. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  180. package/src/screens/address/AddressSearchField.tsx +115 -0
  181. package/src/screens/address/AddressSearchStep.tsx +94 -0
  182. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  183. package/src/screens/address/DetailsSheet.tsx +175 -0
  184. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  185. package/src/screens/address/EntranceDropzone.tsx +178 -0
  186. package/src/screens/address/EntranceFraming.tsx +66 -0
  187. package/src/screens/address/EntrancePills.tsx +60 -0
  188. package/src/screens/address/FramedStreetView.tsx +155 -0
  189. package/src/screens/address/IntroDisclosures.tsx +210 -0
  190. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  191. package/src/screens/address/PinSummaryRow.tsx +105 -0
  192. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  193. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  194. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  195. package/src/screens/address/SearchResults.tsx +105 -0
  196. package/src/screens/address/SearchScreen.tsx +176 -0
  197. package/src/screens/address/SkipForNow.tsx +43 -0
  198. package/src/screens/address/StreetViewChrome.tsx +81 -0
  199. package/src/screens/address/detail-values.ts +22 -0
  200. package/src/screens/address/fix-source.ts +27 -0
  201. package/src/screens/address/index.ts +8 -0
  202. package/src/screens/address/meta.ts +44 -0
  203. package/src/screens/address/use-address-flow.ts +200 -0
  204. package/src/screens/address/use-label-pin.ts +80 -0
  205. package/src/screens/address/use-pin-actions.ts +192 -0
  206. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  207. package/src/screens/consent/model.ts +79 -10
  208. package/src/screens/liveness/CaptureRing.tsx +91 -0
  209. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  210. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  211. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  212. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  213. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  214. package/src/screens/liveness/index.ts +4 -0
  215. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  216. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  217. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  218. package/src/screens/useAddressPhotoAttach.ts +77 -0
  219. package/src/screens/useAwaitingPeople.ts +90 -0
  220. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  221. package/src/screens/usePoaAttach.ts +12 -15
  222. package/src/services/api-biometric.ts +37 -0
  223. package/src/services/api-types-biometric.ts +41 -0
  224. package/src/services/api-types.ts +251 -3
  225. package/src/services/api-verify-types.ts +90 -1
  226. package/src/services/api.ts +237 -3
  227. package/src/services/deviceMetadata.ts +1 -1
  228. package/src/services/errors.ts +10 -2
  229. package/src/services/location.ts +234 -0
  230. package/src/services/uploadErrors.ts +30 -0
  231. package/src/services/workflowGate.ts +8 -0
  232. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  233. package/src/store/address.ts +97 -0
  234. package/src/store/businessCheck.ts +113 -0
  235. package/src/store/derive.ts +44 -4
  236. package/src/store/kycStore.ts +352 -16
  237. package/src/store/serverConfig.ts +20 -0
  238. package/src/store/session.ts +227 -0
  239. package/src/store/state.ts +289 -2
  240. package/src/store/submit.ts +80 -13
  241. package/src/store/submitApplicant.ts +5 -11
  242. package/src/types/business.ts +23 -2
  243. package/src/types/config.ts +70 -2
  244. package/src/types/verification.ts +13 -1
  245. package/src/types/workflow.ts +112 -1
  246. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { MyazaAlert } from '../components/MyazaAlert';
6
+ import { ContactDestinationField } from './ContactDestinationField';
7
+ import { ContactChannelChoice } from './ContactChannelChoice';
8
+ import type { PhoneOtpChannel } from '../config/contact';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The first half of the contact step: where to send the code, and (when the
12
+ // workflow offers a choice) how. Composed as one widget so the step has a
13
+ // single child per phase. Split from ContactVerificationStep (200-line rule);
14
+ // mirrors Flutter's ContactEntryPanel.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export function ContactEntryPanel({
18
+ isEmail,
19
+ recovery,
20
+ email,
21
+ onEmailChange,
22
+ onPhoneChange,
23
+ defaultCountry,
24
+ geoCountry,
25
+ error,
26
+ disabled,
27
+ offeredChannels,
28
+ via,
29
+ onPickChannel,
30
+ }: {
31
+ isEmail: boolean;
32
+ /** Entered via submit recovery: the server refused this channel's proof. */
33
+ recovery: boolean;
34
+ email: string;
35
+ onEmailChange: (value: string) => void;
36
+ onPhoneChange: (value: { e164: string; isValid: boolean }) => void;
37
+ defaultCountry?: string;
38
+ geoCountry?: string | null;
39
+ error: string | null;
40
+ disabled: boolean;
41
+ offeredChannels: PhoneOtpChannel[];
42
+ via: PhoneOtpChannel;
43
+ onPickChannel: (channel: PhoneOtpChannel) => void;
44
+ }): React.ReactElement {
45
+ return (
46
+ <>
47
+ {recovery && (
48
+ <>
49
+ <MyazaAlert
50
+ variant="warning"
51
+ title="Please verify again"
52
+ message={`Your earlier confirmation has expired, so please verify ${isEmail ? 'your email' : 'your number'} once more. Everything else is saved, and we will submit again straight after.`}
53
+ />
54
+ <View style={{ height: spacing.md }} />
55
+ </>
56
+ )}
57
+ <ContactDestinationField
58
+ isEmail={isEmail}
59
+ email={email}
60
+ onEmailChange={onEmailChange}
61
+ onPhoneChange={onPhoneChange}
62
+ defaultCountry={defaultCountry}
63
+ geoCountry={geoCountry}
64
+ error={error}
65
+ disabled={disabled}
66
+ />
67
+ <ContactChannelChoice
68
+ offered={offeredChannels}
69
+ picked={via}
70
+ disabled={disabled}
71
+ onPick={onPickChannel}
72
+ />
73
+ </>
74
+ );
75
+ }
@@ -1,14 +1,12 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { View } from 'react-native';
3
3
 
4
4
  import { spacing } from '../config/theme';
5
5
  import { useEffectiveCountry, useKyc, useKycConfig, useKycStore } from '../components/runtime';
6
- import { MyazaButton } from '../components/MyazaButton';
7
- import { ContactDestinationField } from './ContactDestinationField';
6
+ import { ContactActions } from './ContactActions';
7
+ import { ContactEntryPanel } from './ContactEntryPanel';
8
8
  import { ContactVerifiedPanel } from './ContactVerifiedPanel';
9
9
  import { ContactCodeStep } from './ContactCodeStep';
10
- import { ContactChannelChoice } from './ContactChannelChoice';
11
- import { ContactFooterNote } from './ContactFooterNote';
12
10
  import {
13
11
  contactCodeLength,
14
12
  contactIsRequired,
@@ -17,6 +15,7 @@ import {
17
15
  type PhoneOtpChannel,
18
16
  } from '../config/contact';
19
17
  import { attemptsRemaining, describeCheckError, describeSendError } from '../services/contactErrors';
18
+ import { stepAfterContactVerified } from '../lib/contact-recovery';
20
19
 
21
20
  // ---------------------------------------------------------------------------
22
21
  // Contact-verification OTP step — email or phone, one component, two mounts.
@@ -37,6 +36,7 @@ export function ContactVerificationStep({
37
36
  const country = useEffectiveCountry();
38
37
  const store = useKycStore();
39
38
  const contact = useKyc((s) => s.contact);
39
+ const serverConfig = useKyc((s) => s.serverConfig);
40
40
 
41
41
  const isEmail = channel === 'email';
42
42
  const stepConfig = isEmail ? config.emailVerification : config.phoneVerification;
@@ -54,10 +54,9 @@ export function ContactVerificationStep({
54
54
  const [destination, setDestination] = useState(contact.emailAddress ?? '');
55
55
  // The phone field emits E.164 + validity from libphonenumber, so the step no
56
56
  // longer guesses either from raw text.
57
- const [phone, setPhone] = useState<{ e164: string; isValid: boolean }>({
58
- e164: contact.phoneNumber ?? '',
59
- isValid: false,
60
- });
57
+ const [phone, setPhone] = useState<{ e164: string; isValid: boolean }>(
58
+ { e164: contact.phoneNumber ?? '', isValid: false },
59
+ );
61
60
  const [challengeId, setChallengeId] = useState<string | null>(null);
62
61
  const [expiresAt, setExpiresAt] = useState<string | null>(null);
63
62
  const [code, setCode] = useState('');
@@ -69,7 +68,20 @@ export function ContactVerificationStep({
69
68
  const value = isEmail ? destination.trim() : phone.e164;
70
69
  const canSend = isEmail ? isValidContactEmail(value) : phone.isValid;
71
70
 
72
- const advance = useCallback(() => store.getState().nextStep(), [store]);
71
+ // Entered via submit recovery? (The server refused this channel's proof at
72
+ // submit — the token had expired or was already claimed.) Captured at mount:
73
+ // verifying clears the flag, and Continue must still route back to
74
+ // 'submitted' afterwards (which auto-submits with the fresh proof).
75
+ const recovery = useRef((contact.expired ?? []).includes(channel)).current;
76
+ const advance = useCallback(() => {
77
+ const target = stepAfterContactVerified({
78
+ recovery,
79
+ expired: store.getState().contact.expired ?? [],
80
+ channel,
81
+ });
82
+ if (target) store.getState().goToStep(target);
83
+ else store.getState().nextStep();
84
+ }, [store, channel, recovery]);
73
85
 
74
86
  // The sheet header is rendered by the shell and cannot see this step's state,
75
87
  // so publish what it needs to caption: the channel in play now, and the
@@ -158,42 +170,31 @@ export function ContactVerificationStep({
158
170
  onResend={(switchTo) => void send(switchTo)}
159
171
  />
160
172
  ) : (
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
- </>
173
+ <ContactEntryPanel
174
+ isEmail={isEmail}
175
+ recovery={recovery}
176
+ email={destination}
177
+ onEmailChange={setDestination}
178
+ onPhoneChange={setPhone}
179
+ defaultCountry={config.phoneVerification?.defaultCountry ?? country ?? serverConfig.geoCountry ?? undefined}
180
+ geoCountry={serverConfig.geoCountry}
181
+ error={error}
182
+ disabled={busy}
183
+ offeredChannels={offeredChannels}
184
+ via={via}
185
+ onPickChannel={setVia}
186
+ />
178
187
  )}
179
188
 
180
- <View style={{ height: spacing.md }} />
181
- <MyazaButton
182
- label={challengeId ? 'Verify code' : 'Send code'}
183
- loading={busy}
189
+ <ContactActions
190
+ hasChallenge={challengeId != null}
191
+ busy={busy}
184
192
  disabled={challengeId ? code.length !== codeLength : !canSend}
185
- onPress={() => (challengeId ? void check(code) : void send())}
193
+ required={required}
194
+ isEmail={isEmail}
195
+ onPrimary={() => (challengeId ? void check(code) : void send())}
196
+ onSkip={advance}
186
197
  />
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
198
  </View>
198
199
  );
199
200
  }
@@ -1,13 +1,9 @@
1
- import React, { useMemo, useState } from 'react';
2
- import { Pressable, ScrollView, View } from 'react-native';
1
+ import React, { useMemo } from 'react';
2
+ import { View } from 'react-native';
3
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';
4
+ import { useKyc, useKycConfig, useKycStore } from '../components/runtime';
5
+ import { CountryRegionPicker, CountryRow } from '../components/CountryRegionPicker';
6
+ import { regionCountryName } from '../config/regions';
11
7
  import { countrySelectOptions } from '../store/derive';
12
8
 
13
9
  // ---------------------------------------------------------------------------
@@ -54,7 +50,14 @@ export function CountrySelectStep(): React.ReactElement {
54
50
  };
55
51
 
56
52
  if (options.length > COUNTRY_SEARCH_THRESHOLD) {
57
- return <CountryRegionPicker countries={options} selected={selected} onPick={pick} />;
53
+ return (
54
+ <CountryRegionPicker
55
+ countries={options}
56
+ selected={selected}
57
+ geoCountry={serverConfig.geoCountry}
58
+ onPick={pick}
59
+ />
60
+ );
58
61
  }
59
62
 
60
63
  return (
@@ -71,140 +74,3 @@ export function CountrySelectStep(): React.ReactElement {
71
74
  </View>
72
75
  );
73
76
  }
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
- }
@@ -3,6 +3,7 @@ import { Pressable, View } from 'react-native';
3
3
 
4
4
  import { radius, spacing } from '../config/theme';
5
5
  import { ID_TYPES, resolveIdTypeDefinition } from '../config/idTypes';
6
+ import { useMultiIdPlan } from '../lib/use-multi-id-plan';
6
7
  import type { IdType, IdTypeDefinition } from '../types/config';
7
8
  import {
8
9
  useEffectiveCountry,
@@ -54,6 +55,10 @@ export function IdTypeStep(): React.ReactElement {
54
55
  const store = useKycStore();
55
56
  const serverConfig = useKyc((s) => s.serverConfig);
56
57
  const selectedIdType = useKyc((s) => s.selectedIdType);
58
+ // Multi-ID: this picker is for ONE check of several. It may only offer picks
59
+ // that leave every later check something to offer — the server validates the
60
+ // same rule, so an unsafe pick would be a submission it rejects.
61
+ const plan = useMultiIdPlan();
57
62
 
58
63
  // Select AND advance in one tap. Both go through the store so `nextStep`
59
64
  // routes off the value just written — a document ID to document-capture, a
@@ -104,13 +109,15 @@ export function IdTypeStep(): React.ReactElement {
104
109
  // what makes the disabled step actually disappear from the live flow rather
105
110
  // than still offering passports and licences that then have nowhere to be
106
111
  // captured. Mirrors the web SDK's IdTypeStep.
107
- const visible = useMemo<IdTypeDefinition[]>(
108
- () =>
112
+ const visible = useMemo<IdTypeDefinition[]>(() => {
113
+ const base =
109
114
  config.enableDocumentCapture === false
110
115
  ? available.filter((t) => !t.requiresDocumentCapture)
111
- : available,
112
- [available, config.enableDocumentCapture],
113
- );
116
+ : available;
117
+ // Multi-ID: only the picks that are still free AND leave every later check
118
+ // an option. An ID already used earlier in the run is simply not offered.
119
+ return plan ? base.filter((t) => plan.safeOptions.includes(t.key)) : base;
120
+ }, [available, config.enableDocumentCapture, plan]);
114
121
 
115
122
  if (serverConfig.status === 'loading') {
116
123
  return (
@@ -0,0 +1,88 @@
1
+ import React, { useState } from 'react';
2
+ import { Linking, View } from 'react-native';
3
+ import * as Clipboard from 'expo-clipboard';
4
+
5
+ import { spacing } from '../config/theme';
6
+ import { FloatingSheet } from '../components/glass/FloatingSheet';
7
+ import { Icon } from '../components/Icon';
8
+ import { MyazaAlert } from '../components/MyazaAlert';
9
+ import { MyazaButton } from '../components/MyazaButton';
10
+ import { MyazaText } from '../components/Typography';
11
+ import { useTheme } from '../components/runtime';
12
+
13
+ // Shown when the applicant taps Done while key people still owe a check.
14
+ //
15
+ // The invite links live on THIS success screen, and on mobile the screen dies
16
+ // with the app: once closed, the applicant has no way back to the links unless
17
+ // the org re-sends them. The session's own hosted web page is the way back —
18
+ // the same rehydrated success screen, alive for as long as the invites are —
19
+ // so the one moment to hand it over is the moment they leave.
20
+
21
+ export function KeepLinksSheet({
22
+ open,
23
+ url,
24
+ onClose,
25
+ onDone,
26
+ }: {
27
+ open: boolean;
28
+ url: string;
29
+ /** Dismiss the sheet only (they are staying on the success screen). */
30
+ onClose: () => void;
31
+ /** Close the sheet AND the flow (they are done here). */
32
+ onDone: () => void;
33
+ }): React.ReactElement {
34
+ const { colors } = useTheme();
35
+ const [copied, setCopied] = useState(false);
36
+
37
+ return (
38
+ <FloatingSheet visible={open} onClose={onClose}>
39
+ <View style={{ padding: spacing.md, paddingTop: spacing.xs, alignItems: 'center' }}>
40
+ <View
41
+ style={{
42
+ width: 52,
43
+ height: 52,
44
+ borderRadius: 26,
45
+ backgroundColor: colors.primary50,
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ }}
49
+ >
50
+ <Icon name="globe" size={26} color={colors.primary} />
51
+ </View>
52
+
53
+ <View style={{ height: spacing.md }} />
54
+ <MyazaText variant="heading3" style={{ textAlign: 'center' }}>
55
+ Keep a way back to these links
56
+ </MyazaText>
57
+ <View style={{ height: spacing.md }} />
58
+ <View style={{ width: '100%' }}>
59
+ <MyazaAlert
60
+ variant="warning"
61
+ title="This screen will not be shown again"
62
+ message="Your key people's links live on a web page made for you. Open it in your browser or copy the address somewhere safe, then check who has finished and share the links from there at any time."
63
+ />
64
+ </View>
65
+
66
+ <View style={{ height: spacing.lg }} />
67
+ <View style={{ width: '100%', gap: spacing.sm }}>
68
+ <MyazaButton
69
+ label="Open in my browser"
70
+ leadingIcon="globe"
71
+ onPress={() => void Linking.openURL(url).catch(() => undefined)}
72
+ />
73
+ <MyazaButton
74
+ label={copied ? 'Link copied' : 'Copy the page link'}
75
+ variant="outline"
76
+ leadingIcon={copied ? 'check' : 'copy'}
77
+ onPress={() => {
78
+ void Clipboard.setStringAsync(url)
79
+ .then(() => setCopied(true))
80
+ .catch(() => undefined);
81
+ }}
82
+ />
83
+ <MyazaButton label="I'm done here" variant="ghost" onPress={onDone} />
84
+ </View>
85
+ </View>
86
+ </FloatingSheet>
87
+ );
88
+ }