@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
@@ -22,13 +22,45 @@ import { INITIAL_SERVER_CONFIG, describeConfigError, type ServerConfigState } fr
22
22
  import {
23
23
  EMPTY_BUSINESS,
24
24
  EMPTY_BUSINESS_APPLICATION,
25
+ EMPTY_BUSINESS_CHECK,
25
26
  type KYCSubmissionResult,
26
27
  type KycState,
27
28
  type KycStore,
28
29
  } from './state';
29
- import { nextStepAfter, nfcDecision, previousStepBefore } from './derive';
30
+ import { nextStepAfter, nfcDecision, openingStep, previousStepBefore } from './derive';
31
+
32
+ /** The steps that make up ONE ID's evidence. Leaving this set is what ends a
33
+ * multi-ID check — the leg has several exits depending on the ID. */
34
+ const ID_EVIDENCE_STEPS = new Set<KYCStep>(['id-input', 'document-capture', 'nfc']);
35
+
36
+ /** The 1-based check the applicant is on, or undefined outside a multi-ID run.
37
+ * Only emitted once a slot has been COMMITTED — on an ordinary run the field
38
+ * would be a constant 1 on every entry, which is noise. */
39
+ function multiIdSlotOf(s: KycState): number | undefined {
40
+ return s.multiIdSlots.length > 0 ? s.multiIdSlots.length + 1 : undefined;
41
+ }
42
+
43
+ /** The ID selected at this moment — see StepLogEntry.idType. */
44
+ function selectedIdTypeOf(s: KycState): string | undefined {
45
+ return s.selectedIdType ?? undefined;
46
+ }
47
+
48
+ /** The active multi-ID plan for the store's current state, or null. */
49
+ function multiIdPlanFor(s: KycState): ReturnType<typeof multiIdPlan> {
50
+ return multiIdPlan(
51
+ { ...s.config, country: s.selectedCountry ?? s.config.country },
52
+ { multiIdSlotIndex: s.multiIdSlotIndex, multiIdSlots: s.multiIdSlots },
53
+ s.serverConfig.status === 'ready' ? s.serverConfig.idTypes : [],
54
+ );
55
+ }
30
56
  import { recordStep, resetStepLog } from '../lib/step-log';
57
+ import { resetAutoLocate, resetCurrentFix } from '../lib/address-current-location';
58
+ import { multiIdPlan } from '../lib/multi-id';
59
+ import { resolveIdTypeDefinition } from '../config/idTypes';
31
60
  import { buildVerifyRequest } from './submit';
61
+ import { IDLE_SELFIE_UPLOAD } from '../lib/selfie-upload-wait';
62
+ import { resetBusinessCheck, runBusinessCheck } from './businessCheck';
63
+ import { startAttemptSession, watchSessionProgress } from './session';
32
64
  import { applicantMediaCaptured, buildApplicantVerifyRequest } from './submitApplicant';
33
65
 
34
66
  export * from './state';
@@ -65,9 +97,18 @@ export function createKycStore(
65
97
  api,
66
98
 
67
99
  currentStep: 'consent',
100
+ sessionId: null,
101
+ sessionUrl: null,
102
+ businessCheck: { ...EMPTY_BUSINESS_CHECK },
68
103
  selectedCountry: null,
104
+ countryAutoPicked: false,
69
105
  selectedIdType: null,
70
106
  idNumber: null,
107
+ multiIdSlotIndex: 0,
108
+ multiIdSlots: [],
109
+ multiIdRestored: null,
110
+ selfiePreviewUri: null,
111
+ selfieUpload: IDLE_SELFIE_UPLOAD,
71
112
  mediaIds: {},
72
113
  submissionResult: null,
73
114
  serverConfig: serverConfig ?? INITIAL_SERVER_CONFIG,
@@ -88,6 +129,11 @@ export function createKycStore(
88
129
  chipData: null,
89
130
  poaDocumentType: null,
90
131
  poaFileName: null,
132
+ address: null,
133
+ addressPhotoPreview: null,
134
+ addressIntroSeen: false,
135
+ addressEntranceFraming: false,
136
+ addressSandboxOutcome: null,
91
137
  isLoading: false,
92
138
  error: null,
93
139
 
@@ -101,15 +147,27 @@ export function createKycStore(
101
147
  const branding = res.branding
102
148
  ? { ...res.branding, logo: normalizeDevAssetUrl(res.branding.logo, baseUrl) }
103
149
  : res.branding;
150
+ // The facts that just landed can add a step AHEAD of the one the flow
151
+ // opened on (the address search step, on a consent-less address
152
+ // flow). Someone still standing on the placeholder's opening step,
153
+ // having done nothing, is moved to the real one; anyone who has
154
+ // moved is left alone. Mirrors Flutter's _loadServerConfig.
155
+ const before = openingStep(get());
104
156
  set({
105
157
  serverConfig: {
106
158
  status: 'ready',
107
159
  idTypes: res.idTypes,
108
160
  branding,
161
+ geoCountry: res.geoCountry,
162
+ addressSearch: res.addressSearch,
163
+ addressSearchMode: res.addressSearchMode,
164
+ mapsFrameUrl: res.mapsFrameUrl ?? null,
109
165
  environment: res.environment,
110
166
  fatal: false,
111
167
  },
112
168
  });
169
+ const after = openingStep(get());
170
+ if (get().currentStep === before && after !== before) set({ currentStep: after });
113
171
  } catch (err) {
114
172
  const described = describeConfigError(err);
115
173
  set((s) => ({
@@ -122,11 +180,96 @@ export function createKycStore(
122
180
  // Changing country invalidates the ID choice: the same key can mean a
123
181
  // different document (or none) elsewhere, so it is cleared rather than
124
182
  // silently carried across.
125
- set((s) => ({
126
- selectedCountry: country,
127
- selectedIdType: s.selectedCountry === country ? s.selectedIdType : null,
128
- idNumber: s.selectedCountry === country ? s.idNumber : null,
129
- }));
183
+ set((s) => {
184
+ const same = s.selectedCountry === country;
185
+ return {
186
+ selectedCountry: country,
187
+ countryAutoPicked: false,
188
+ selectedIdType: same ? s.selectedIdType : null,
189
+ idNumber: same ? s.idNumber : null,
190
+ // A multi-ID run's committed slots belong to the country they were
191
+ // picked in; carrying them into a new one would offer an ID no
192
+ // register there can verify.
193
+ multiIdSlotIndex: same ? s.multiIdSlotIndex : 0,
194
+ multiIdSlots: same ? s.multiIdSlots : [],
195
+ multiIdRestored: same ? s.multiIdRestored : null,
196
+ };
197
+ });
198
+ },
199
+
200
+ setCountryAuto(country) {
201
+ get().setCountry(country);
202
+ if (!get().countryAutoPicked) set({ countryAutoPicked: true });
203
+ },
204
+
205
+ commitMultiIdSlot(nextStep, previews) {
206
+ set((s) => {
207
+ if (!s.selectedIdType) return {};
208
+ const slot = {
209
+ idType: s.selectedIdType,
210
+ ...(s.idNumber ? { idNumber: s.idNumber } : {}),
211
+ ...(s.mediaIds.documentFront ? { documentFront: s.mediaIds.documentFront } : {}),
212
+ ...(s.mediaIds.documentBack ? { documentBack: s.mediaIds.documentBack } : {}),
213
+ ...(s.mediaIds.documentFrontVideo
214
+ ? { documentFrontVideo: s.mediaIds.documentFrontVideo }
215
+ : {}),
216
+ ...(s.mediaIds.documentBackVideo
217
+ ? { documentBackVideo: s.mediaIds.documentBackVideo }
218
+ : {}),
219
+ ...(s.chipData ? { chipData: s.chipData } : {}),
220
+ ...(previews?.front ? { documentFrontImage: previews.front } : {}),
221
+ ...(previews?.back ? { documentBackImage: previews.back } : {}),
222
+ };
223
+ // The working evidence is cleared for the next check; the SELFIE and
224
+ // its video are run-level and deliberately untouched.
225
+ return {
226
+ multiIdSlots: [...s.multiIdSlots, slot],
227
+ multiIdSlotIndex: s.multiIdSlotIndex + 1,
228
+ selectedIdType: null,
229
+ idNumber: null,
230
+ multiIdRestored: null,
231
+ mediaIds: {
232
+ ...s.mediaIds,
233
+ documentFront: undefined,
234
+ documentBack: undefined,
235
+ documentFrontVideo: undefined,
236
+ documentBackVideo: undefined,
237
+ },
238
+ // The chip belongs to the check just committed; the next one reads
239
+ // its own document (or none).
240
+ chipData: null,
241
+ documentScanPhase: 'front' as const,
242
+ currentStep: nextStep,
243
+ };
244
+ });
245
+ },
246
+
247
+ uncommitMultiIdSlot(step) {
248
+ set((s) => {
249
+ const last = s.multiIdSlots[s.multiIdSlots.length - 1];
250
+ if (!last) return {};
251
+ // Restore what that check captured — changing an earlier ID must not
252
+ // mean re-photographing a document that is still perfectly good.
253
+ return {
254
+ multiIdSlots: s.multiIdSlots.slice(0, -1),
255
+ multiIdSlotIndex: Math.max(s.multiIdSlots.length - 1, 0),
256
+ selectedIdType: last.idType,
257
+ idNumber: last.idNumber ?? null,
258
+ multiIdRestored: {
259
+ ...(last.documentFrontImage ? { front: last.documentFrontImage } : {}),
260
+ ...(last.documentBackImage ? { back: last.documentBackImage } : {}),
261
+ },
262
+ mediaIds: {
263
+ ...s.mediaIds,
264
+ documentFront: last.documentFront,
265
+ documentBack: last.documentBack,
266
+ documentFrontVideo: last.documentFrontVideo,
267
+ documentBackVideo: last.documentBackVideo,
268
+ },
269
+ chipData: last.chipData ?? null,
270
+ currentStep: step,
271
+ };
272
+ });
130
273
  },
131
274
 
132
275
  setIdType(idType) {
@@ -141,6 +284,22 @@ export function createKycStore(
141
284
  set((s) => ({ mediaIds: { ...s.mediaIds, [key]: mediaId } }));
142
285
  },
143
286
 
287
+ setSelfiePreview(uri) {
288
+ set({ selfiePreviewUri: uri });
289
+ },
290
+
291
+ clearSelfie() {
292
+ set((s) => ({
293
+ selfiePreviewUri: null,
294
+ selfieUpload: IDLE_SELFIE_UPLOAD,
295
+ mediaIds: { ...s.mediaIds, selfie: undefined, livenessVideo: undefined },
296
+ }));
297
+ },
298
+
299
+ setSelfieUpload(upload) {
300
+ set({ selfieUpload: upload });
301
+ },
302
+
144
303
  setDocumentMediaId(mediaId, side) {
145
304
  set((s) => ({
146
305
  mediaIds: {
@@ -163,11 +322,29 @@ export function createKycStore(
163
322
  },
164
323
 
165
324
  setContactVerified(channel, destination, token) {
325
+ set((s) => {
326
+ // A fresh proof clears its channel's "server refused this" flag.
327
+ const expired = (s.contact.expired ?? []).filter((c) => c !== channel);
328
+ return {
329
+ contact:
330
+ channel === 'email'
331
+ ? { ...s.contact, emailAddress: destination, emailToken: token, expired }
332
+ : { ...s.contact, phoneNumber: destination, phoneToken: token, expired },
333
+ };
334
+ });
335
+ },
336
+
337
+ clearContactProofs(channels) {
338
+ // The server refused these proofs at submit (single-use tokens expire
339
+ // ~30 min after the OTP check, and session restore can resurrect a
340
+ // dead one). Drop the tokens, keep the destinations, flag the steps.
166
341
  set((s) => ({
167
- contact:
168
- channel === 'email'
169
- ? { ...s.contact, emailAddress: destination, emailToken: token }
170
- : { ...s.contact, phoneNumber: destination, phoneToken: token },
342
+ contact: {
343
+ ...s.contact,
344
+ ...(channels.includes('email') ? { emailToken: undefined } : {}),
345
+ ...(channels.includes('phone') ? { phoneToken: undefined } : {}),
346
+ expired: channels,
347
+ },
171
348
  }));
172
349
  },
173
350
 
@@ -180,14 +357,54 @@ export function createKycStore(
180
357
  }));
181
358
  },
182
359
 
360
+ setSessionId(sessionId, sessionUrl) {
361
+ set({ sessionId, ...(sessionUrl !== undefined ? { sessionUrl } : {}) });
362
+ },
363
+
364
+ async checkBusiness() {
365
+ return runBusinessCheck(get, set);
366
+ },
367
+
183
368
  setBusinessField(key, value) {
184
- set((s) => ({ business: { ...s.business, [key]: value } }));
369
+ // Any change to WHICH company this is about invalidates the answer we
370
+ // hold, so the check never describes one business while the field names
371
+ // another. Everything the previous register told us about the old
372
+ // company goes with it — only what the REGISTER wrote: an applicant who
373
+ // typed their own address meant it. Clearing it also unblocks the next
374
+ // lookup, whose prefill only ever writes into an empty field, so
375
+ // leftovers were not merely stale, they were suppressing the real
376
+ // answer. Mirrors the web SDK's setDetails.
377
+ const identityChanged =
378
+ key === 'registrationNumber' || key === 'country' || key === 'product';
379
+ set((s) => {
380
+ const business = { ...s.business, [key]: value };
381
+ if (identityChanged) {
382
+ for (const prefilledKey of s.businessCheck.prefilled) {
383
+ if (prefilledKey !== key) (business as Record<string, unknown>)[prefilledKey] = '';
384
+ }
385
+ }
386
+ return { business };
387
+ });
388
+ if (identityChanged) resetBusinessCheck(set);
389
+ },
390
+
391
+ applyBusinessPrefill(patch, prefilled) {
392
+ set((s) => ({
393
+ business: { ...s.business, ...patch },
394
+ businessCheck: { ...s.businessCheck, prefilled },
395
+ }));
185
396
  },
186
397
 
187
398
  setKeyPeople(rows) {
188
399
  set((s) => ({ businessApplication: { ...s.businessApplication, keyPeople: rows } }));
189
400
  },
190
401
 
402
+ setUboUnidentifiable(checked) {
403
+ set((s) => ({
404
+ businessApplication: { ...s.businessApplication, uboUnidentifiable: checked },
405
+ }));
406
+ },
407
+
191
408
  setBusinessDocument(doc) {
192
409
  set((s) => ({
193
410
  businessApplication: {
@@ -249,6 +466,41 @@ export function createKycStore(
249
466
  });
250
467
  },
251
468
 
469
+ setAddress(address) {
470
+ set({ address });
471
+ },
472
+
473
+ setAddressSandboxOutcome(addressSandboxOutcome) {
474
+ set({ addressSandboxOutcome });
475
+ },
476
+
477
+ setAddressPhoto(mediaId) {
478
+ set((s) => {
479
+ if (mediaId) return { mediaIds: { ...s.mediaIds, addressPhoto: mediaId } };
480
+ const { addressPhoto: _dropped, ...rest } = s.mediaIds;
481
+ return { mediaIds: rest };
482
+ });
483
+ },
484
+
485
+ setAddressPhotoPreview(uri) {
486
+ set({ addressPhotoPreview: uri });
487
+ },
488
+
489
+ markAddressIntroSeen() {
490
+ set({ addressIntroSeen: true });
491
+ },
492
+
493
+ setAddressEntranceFraming(framing) {
494
+ if (get().addressEntranceFraming !== framing) set({ addressEntranceFraming: framing });
495
+ },
496
+
497
+ clearAddress() {
498
+ set((s) => {
499
+ const { addressPhoto: _dropped, ...rest } = s.mediaIds;
500
+ return { address: null, mediaIds: rest, addressPhotoPreview: null };
501
+ });
502
+ },
503
+
252
504
  setDocumentCapturePhase(phase) {
253
505
  if (get().documentCapturePhase !== phase) set({ documentCapturePhase: phase });
254
506
  },
@@ -276,6 +528,26 @@ export function createKycStore(
276
528
 
277
529
  nextStep() {
278
530
  const next = nextStepAfter(get().currentStep, get());
531
+
532
+ // Multi-ID: the run walks the capture leg once PER ID. Intercepted here
533
+ // rather than in each screen because it is one rule — "the applicant
534
+ // finished this check" — and the leg has several exits (a number-only
535
+ // ID leaves from id-input, a document ID from document-capture or the
536
+ // chip read after it).
537
+ const plan = multiIdPlanFor(get());
538
+ if (plan && ID_EVIDENCE_STEPS.has(get().currentStep) && !ID_EVIDENCE_STEPS.has(next)) {
539
+ const previews = get().multiIdRestored ?? undefined;
540
+ if (!plan.last) {
541
+ // Another ID to go: commit this one and hand the picker back.
542
+ get().commitMultiIdSlot('id-type', previews);
543
+ emitStepChange('id-type');
544
+ return;
545
+ }
546
+ // The final check: commit it, then carry on to the shared selfie.
547
+ get().commitMultiIdSlot(next, previews);
548
+ emitStepChange(next);
549
+ return;
550
+ }
279
551
  // Leaving document capture is the moment the chip step either appears or
280
552
  // silently does not. Four independent gates can remove it and a missing
281
553
  // step looks the same however it went missing, so say which one.
@@ -294,6 +566,22 @@ export function createKycStore(
294
566
  },
295
567
 
296
568
  previousStep() {
569
+ // Multi-ID: stepping back from the picker means re-doing the PREVIOUS
570
+ // check, not leaving the flow. The slot is uncommitted so its ID number
571
+ // and captures come back — changing an earlier ID must not mean
572
+ // re-photographing a document that is still perfectly good.
573
+ const slots = get().multiIdSlots;
574
+ if (get().currentStep === 'id-type' && slots.length > 0) {
575
+ const last = slots[slots.length - 1]!;
576
+ const def = resolveIdTypeDefinition(
577
+ get().selectedCountry ?? get().config.country ?? '',
578
+ last.idType,
579
+ );
580
+ const step: KYCStep = def?.requiresDocumentCapture === false ? 'id-input' : 'document-capture';
581
+ get().uncommitMultiIdSlot(step);
582
+ emitStepChange(step);
583
+ return;
584
+ }
297
585
  const prev = previousStepBefore(get().currentStep, get());
298
586
  if (prev !== get().currentStep) {
299
587
  set({ currentStep: prev, immersiveCapture: false, navDirection: 'back' });
@@ -322,7 +610,7 @@ export function createKycStore(
322
610
  const res = await withRetry(() => api.verify(request), { onRetry });
323
611
  const result: KYCSubmissionResult = {
324
612
  verificationId: res.verificationId,
325
- status: 'pending',
613
+ status: 'processing',
326
614
  };
327
615
  set({
328
616
  submissionResult: result,
@@ -359,18 +647,36 @@ export function createKycStore(
359
647
  // already sitting on 'consent' (the subscribe below only fires on
360
648
  // change, and recordStep dedupes if it fires too).
361
649
  resetStepLog();
362
- recordStep('consent');
650
+ // The cached GPS fix and the once-per-verification auto-locate are
651
+ // scoped to ONE verification: an app session spans many opens of the
652
+ // SDK, and a fix taken an hour ago would drop the pin at wherever the
653
+ // phone was then.
654
+ resetCurrentFix();
655
+ resetAutoLocate();
656
+ // The opening step never depends on the fields reset below (only later
657
+ // steps do), so it is safe to read before the write.
658
+ const opening = openingStep(get());
659
+ recordStep(opening, multiIdSlotOf(get()), selectedIdTypeOf(get()));
363
660
  set({
364
- currentStep: 'consent',
661
+ currentStep: opening,
365
662
  selectedCountry: null,
663
+ countryAutoPicked: false,
366
664
  selectedIdType: null,
367
665
  idNumber: null,
666
+ multiIdSlotIndex: 0,
667
+ multiIdSlots: [],
668
+ multiIdRestored: null,
669
+ selfiePreviewUri: null,
670
+ selfieUpload: IDLE_SELFIE_UPLOAD,
368
671
  mediaIds: {},
369
672
  submissionResult: null,
370
673
  documentScanPhase: 'front',
371
674
  documentCapturePhase: 'front',
372
675
  immersiveCapture: false,
373
676
  flashPaint: null,
677
+ sessionId: null,
678
+ sessionUrl: null,
679
+ businessCheck: { ...EMPTY_BUSINESS_CHECK },
374
680
  questionnaireAnswers: {},
375
681
  contact: {},
376
682
  contactChallenge: null,
@@ -383,6 +689,11 @@ export function createKycStore(
383
689
  chipData: null,
384
690
  poaDocumentType: null,
385
691
  poaFileName: null,
692
+ address: null,
693
+ addressPhotoPreview: null,
694
+ addressIntroSeen: false,
695
+ addressEntranceFraming: false,
696
+ addressSandboxOutcome: null,
386
697
  isLoading: false,
387
698
  error: null,
388
699
  });
@@ -395,7 +706,9 @@ export function createKycStore(
395
706
  // collapses consecutive duplicates). Rides the submission as
396
707
  // metadata.device.stepLog for the dashboard timeline.
397
708
  store.subscribe((s, prev) => {
398
- if (s.currentStep !== prev.currentStep) recordStep(s.currentStep);
709
+ if (s.currentStep !== prev.currentStep) {
710
+ recordStep(s.currentStep, multiIdSlotOf(s), selectedIdTypeOf(s));
711
+ }
399
712
  });
400
713
 
401
714
  // A fresh store IS a fresh session: the runtime provider creates one per
@@ -404,7 +717,30 @@ export function createKycStore(
404
717
  // subscription above only fires on CHANGE, and a new store already sits on
405
718
  // 'consent').
406
719
  resetStepLog();
407
- recordStep(store.getState().currentStep);
720
+ // The module-level GPS cache and the auto-locate latch are scoped to ONE
721
+ // verification, and this — not reset() — is where a verification begins.
722
+ // Clearing them only in reset() left both alive across modal launches, so a
723
+ // second verification in the same app session dropped its pin wherever the
724
+ // phone had been on the first (possibly hours and a city away) and skipped
725
+ // the automatic locate entirely, since the latch was already spent.
726
+ resetCurrentFix();
727
+ resetAutoLocate();
728
+ // A workflow that switched the consent screen off opens on its first real
729
+ // step; the literal 'consent' above is only the shape before the config is
730
+ // consulted. Set BEFORE the journey is recorded, so the log opens where the
731
+ // person does.
732
+ const opening = openingStep(store.getState());
733
+ if (opening !== store.getState().currentStep) store.setState({ currentStep: opening });
734
+ recordStep(
735
+ store.getState().currentStep,
736
+ multiIdSlotOf(store.getState()),
737
+ selectedIdTypeOf(store.getState()),
738
+ );
739
+
740
+ // The attempt session: minted at launch (a fresh store IS a fresh attempt),
741
+ // with progress written as the user moves. Both best-effort by contract.
742
+ startAttemptSession(store);
743
+ watchSessionProgress(store);
408
744
 
409
745
  return store;
410
746
  }
@@ -19,6 +19,26 @@ export interface ServerConfigState {
19
19
  status: ServerConfigStatus;
20
20
  idTypes: SdkConfigIdType[];
21
21
  branding?: SdkConfigBranding;
22
+ /** The visitor's country from their IP — a default, never evidence. */
23
+ geoCountry?: string | null;
24
+ /**
25
+ * Whether the platform has a forward-search backend, and which one. Absent
26
+ * means the address flow simply has no search screen: the applicant places
27
+ * the pin by hand, which is the fallback every address failure degrades to.
28
+ *
29
+ * The Google KEY itself never rides here (it is the hosted page's alone);
30
+ * the framed picker reaches this SDK as a URL instead — see mapsFrameUrl.
31
+ * Street View entrance framing still does not exist on mobile.
32
+ */
33
+ addressSearch?: boolean;
34
+ addressSearchMode?: 'autocomplete' | 'basic';
35
+ /**
36
+ * The framed Google-map picker page for a WebView (`react-native-webview`,
37
+ * an optional peer): our hosted /embed/map plus a signed APP grant. Null or
38
+ * absent ⇒ the built-in OSM picker, which is also the fallback when the
39
+ * page never reports ready.
40
+ */
41
+ mapsFrameUrl?: string | null;
22
42
  environment?: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
23
43
  /** HTTP status of a failed config fetch (if any). */
24
44
  statusCode?: number;