@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 (219) hide show
  1. package/README.md +277 -7
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +146 -10
  9. package/ios/HybridMyazaFaceDetector.swift +8 -3
  10. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  12. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  14. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  16. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  18. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  19. package/package.json +30 -9
  20. package/src/MyazaKYC.tsx +4 -11
  21. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  22. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  23. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  24. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  25. package/src/assets/fonts/OFL-Karla.txt +93 -0
  26. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  27. package/src/assets/fonts/README.md +12 -0
  28. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  30. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  31. package/src/capture/useAutoCapture.ts +8 -1
  32. package/src/components/CountryField.tsx +8 -0
  33. package/src/components/CountryRegionPicker.tsx +184 -0
  34. package/src/components/DialCodePicker.tsx +47 -68
  35. package/src/components/DialCodeRow.tsx +102 -0
  36. package/src/components/DocumentCropper.tsx +30 -24
  37. package/src/components/DocumentReview.tsx +11 -1
  38. package/src/components/DocumentReviewSide.tsx +6 -2
  39. package/src/components/DocumentReviewZoom.tsx +5 -1
  40. package/src/components/FramedMapPicker.tsx +122 -0
  41. package/src/components/GeoBadge.tsx +34 -0
  42. package/src/components/Icon.tsx +10 -192
  43. package/src/components/KycFlow.tsx +69 -34
  44. package/src/components/LineSkeleton.tsx +100 -0
  45. package/src/components/MapChrome.tsx +67 -0
  46. package/src/components/MapPinMarker.tsx +54 -0
  47. package/src/components/MapPinPicker.tsx +179 -0
  48. package/src/components/MilestoneTrack.tsx +155 -0
  49. package/src/components/MyazaButton.tsx +9 -0
  50. package/src/components/MyazaInput.tsx +17 -1
  51. package/src/components/OptionRow.tsx +22 -3
  52. package/src/components/PhoneNumberInput.tsx +8 -0
  53. package/src/components/PoweredBy.tsx +5 -5
  54. package/src/components/PresenceBlocks.tsx +153 -0
  55. package/src/components/RequiredDocumentPill.tsx +114 -0
  56. package/src/components/StepHeader.tsx +23 -3
  57. package/src/components/StepView.tsx +17 -0
  58. package/src/components/StickyActions.tsx +56 -0
  59. package/src/components/dialCodeRows.ts +64 -0
  60. package/src/components/documentReviewCopy.ts +39 -0
  61. package/src/components/fonts.ts +14 -17
  62. package/src/components/icon-map.ts +176 -0
  63. package/src/components/stepHeaderMeta.tsx +48 -5
  64. package/src/config/addressCollection.ts +125 -0
  65. package/src/config/biometricOptions.ts +100 -0
  66. package/src/config/business.ts +20 -1
  67. package/src/config/businessSteps.ts +8 -2
  68. package/src/config/consentStep.ts +19 -0
  69. package/src/config/documentCaptureMethods.ts +31 -0
  70. package/src/config/proofOfAddress.ts +54 -11
  71. package/src/config/regions.ts +24 -0
  72. package/src/config/stepOrder.ts +79 -5
  73. package/src/config/uploadLimits.ts +35 -0
  74. package/src/config/workflowMerge.ts +13 -5
  75. package/src/index.ts +49 -1
  76. package/src/lib/address-current-location.ts +171 -0
  77. package/src/lib/address-field-modes.ts +119 -0
  78. package/src/lib/address-flow.ts +172 -0
  79. package/src/lib/address-helpers.ts +48 -0
  80. package/src/lib/address-line.ts +76 -0
  81. package/src/lib/address-pin-move.ts +97 -0
  82. package/src/lib/address-step-recovery.ts +63 -0
  83. package/src/lib/authed-image.ts +78 -0
  84. package/src/lib/biometric-copy.ts +51 -0
  85. package/src/lib/captureRing.ts +83 -0
  86. package/src/lib/country-adoption.ts +89 -0
  87. package/src/lib/documentCaptureCheck.ts +136 -0
  88. package/src/lib/inferred-country.ts +65 -0
  89. package/src/lib/livenessLayout.ts +49 -0
  90. package/src/lib/map-frame.ts +159 -0
  91. package/src/lib/map-tiles.ts +143 -0
  92. package/src/lib/model-ready.ts +82 -0
  93. package/src/lib/poa-country-gate.ts +27 -0
  94. package/src/lib/prime-models.ts +50 -0
  95. package/src/lib/resubmit.ts +36 -7
  96. package/src/lib/result-copy.ts +116 -0
  97. package/src/lib/result-wait.ts +53 -0
  98. package/src/lib/review-map-surface.ts +26 -0
  99. package/src/lib/scope.ts +31 -0
  100. package/src/lib/selfie-sharpness.ts +211 -0
  101. package/src/lib/selfie-upload-wait.ts +79 -0
  102. package/src/lib/street-view-fov.ts +42 -0
  103. package/src/lib/webview-available.ts +47 -0
  104. package/src/liveness/avatarSource.ts +59 -0
  105. package/src/liveness/useLiveness.ts +6 -1
  106. package/src/liveness/useModelReady.ts +9 -55
  107. package/src/mrz/extract.ts +71 -4
  108. package/src/mrz/textRecognizer.ts +36 -0
  109. package/src/mrz/useTextModelReady.ts +21 -0
  110. package/src/presence/background-math.ts +100 -0
  111. package/src/presence/background-store.ts +82 -0
  112. package/src/presence/background.ts +165 -0
  113. package/src/presence/foreground-service.ts +193 -0
  114. package/src/presence/fs.ts +51 -0
  115. package/src/presence/geofence.ts +41 -0
  116. package/src/presence/math.ts +44 -0
  117. package/src/presence/post.ts +40 -0
  118. package/src/presence/report.ts +87 -0
  119. package/src/presence/sampler.ts +110 -0
  120. package/src/presence/status.ts +92 -0
  121. package/src/presence/store.ts +94 -0
  122. package/src/presence/tier.ts +36 -0
  123. package/src/presence/watch-wait.ts +81 -0
  124. package/src/screens/AddressCountryControl.tsx +115 -0
  125. package/src/screens/BusinessDetailsFields.tsx +3 -0
  126. package/src/screens/BusinessDetailsStep.tsx +2 -0
  127. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  128. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  129. package/src/screens/CompanyInfoFields.tsx +6 -1
  130. package/src/screens/ConsentStep.tsx +6 -17
  131. package/src/screens/ContactActions.tsx +54 -0
  132. package/src/screens/ContactDestinationField.tsx +4 -0
  133. package/src/screens/ContactEntryPanel.tsx +75 -0
  134. package/src/screens/ContactVerificationStep.tsx +26 -56
  135. package/src/screens/CountrySelectStep.tsx +13 -147
  136. package/src/screens/DocumentCaptureStep.tsx +177 -93
  137. package/src/screens/LivenessAvatar.tsx +33 -16
  138. package/src/screens/LivenessStep.tsx +188 -49
  139. package/src/screens/MrzScanView.tsx +33 -0
  140. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  141. package/src/screens/ProofOfAddressParts.tsx +151 -0
  142. package/src/screens/ProofOfAddressStep.tsx +58 -99
  143. package/src/screens/SubmittedBadge.tsx +25 -0
  144. package/src/screens/SubmittedError.tsx +64 -0
  145. package/src/screens/SubmittedResult.tsx +117 -0
  146. package/src/screens/SubmittedStep.tsx +71 -164
  147. package/src/screens/SubmittedSuccess.tsx +127 -0
  148. package/src/screens/SubmittedWaiting.tsx +45 -0
  149. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  150. package/src/screens/address/AddressIntroGate.tsx +148 -0
  151. package/src/screens/address/AddressMapStub.tsx +50 -0
  152. package/src/screens/address/AddressPinStep.tsx +178 -0
  153. package/src/screens/address/AddressReviewStep.tsx +162 -0
  154. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  155. package/src/screens/address/AddressSearchField.tsx +115 -0
  156. package/src/screens/address/AddressSearchStep.tsx +94 -0
  157. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  158. package/src/screens/address/DetailsSheet.tsx +175 -0
  159. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  160. package/src/screens/address/EntranceDropzone.tsx +178 -0
  161. package/src/screens/address/EntranceFraming.tsx +66 -0
  162. package/src/screens/address/EntrancePills.tsx +60 -0
  163. package/src/screens/address/FramedStreetView.tsx +155 -0
  164. package/src/screens/address/IntroDisclosures.tsx +210 -0
  165. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  166. package/src/screens/address/PinSummaryRow.tsx +105 -0
  167. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  168. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  169. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  170. package/src/screens/address/SearchResults.tsx +105 -0
  171. package/src/screens/address/SearchScreen.tsx +176 -0
  172. package/src/screens/address/SkipForNow.tsx +43 -0
  173. package/src/screens/address/StreetViewChrome.tsx +81 -0
  174. package/src/screens/address/detail-values.ts +22 -0
  175. package/src/screens/address/fix-source.ts +27 -0
  176. package/src/screens/address/index.ts +8 -0
  177. package/src/screens/address/meta.ts +44 -0
  178. package/src/screens/address/use-address-flow.ts +200 -0
  179. package/src/screens/address/use-label-pin.ts +80 -0
  180. package/src/screens/address/use-pin-actions.ts +192 -0
  181. package/src/screens/consent/model.ts +79 -10
  182. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  183. package/src/screens/document/UploadPhase.tsx +177 -0
  184. package/src/screens/document/useDocumentCamera.ts +109 -0
  185. package/src/screens/liveness/CaptureRing.tsx +91 -0
  186. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  187. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  188. package/src/screens/liveness/LivenessOutcome.tsx +8 -4
  189. package/src/screens/liveness/SelfiePreview.tsx +31 -7
  190. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  191. package/src/screens/liveness/index.ts +4 -0
  192. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  193. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  194. package/src/screens/useAddressPhotoAttach.ts +77 -0
  195. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  196. package/src/screens/usePoaAttach.ts +12 -15
  197. package/src/services/api-types.ts +119 -1
  198. package/src/services/api-verify-types.ts +37 -0
  199. package/src/services/api.ts +149 -3
  200. package/src/services/deviceMetadata.ts +1 -1
  201. package/src/services/location.ts +234 -0
  202. package/src/services/mediaCompress.ts +23 -5
  203. package/src/services/uploadErrors.ts +30 -0
  204. package/src/services/workflowGate.ts +7 -0
  205. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  206. package/src/store/address.ts +97 -0
  207. package/src/store/derive.ts +29 -4
  208. package/src/store/kycStore.ts +120 -5
  209. package/src/store/serverConfig.ts +18 -0
  210. package/src/store/session.ts +23 -4
  211. package/src/store/state.ts +134 -0
  212. package/src/store/submit.ts +18 -1
  213. package/src/types/config.ts +63 -1
  214. package/src/types/verification.ts +12 -0
  215. package/src/types/workflow.ts +97 -1
  216. package/src/assets/liveness/Blink.gif +0 -0
  217. package/src/assets/liveness/Nod.gif +0 -0
  218. package/src/assets/liveness/Smile.gif +0 -0
  219. package/src/assets/liveness/Turn.gif +0 -0
@@ -7,14 +7,19 @@
7
7
  // keeps a single answer to each of those questions.
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
+ import { configScope } from '../lib/scope';
10
11
  import { requiresDocumentCapture, supportsNfcChip } from '../config/idTypes';
11
12
  import {
13
+ buildStepOrder,
12
14
  getStepProgress,
13
15
  nextStepInOrder,
14
16
  previousStepInOrder,
15
17
  type StepOrderOptions,
16
18
  } from '../config/stepOrder';
17
19
  import { hasProofOfAddressStep } from '../config/proofOfAddress';
20
+ import { addressFlowFor, hasAddressCollectionStep } from '../config/addressCollection';
21
+ import { webViewAvailable } from '../lib/webview-available';
22
+ import { addressVendorsStubbed } from '../lib/address-flow';
18
23
  import { businessProductsForCountry, isBusinessFlow } from '../config/business';
19
24
  import {
20
25
  hasApplicantVerification,
@@ -22,6 +27,7 @@ import {
22
27
  hasKeyPeopleCollection,
23
28
  } from '../config/businessSteps';
24
29
  import { hasActiveQuestionnaire } from '../config/questionnaire';
30
+ import { hasConsentStep, openingStepOf } from '../config/consentStep';
25
31
  import { applicantSelfCountry, keyPeoplePayload } from '../config/keyPeople';
26
32
  import { featuresFor } from './serverConfig';
27
33
  import type { VerifyRequest } from '../services/api';
@@ -58,6 +64,7 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
58
64
  const idType = state.selectedIdType;
59
65
  return {
60
66
  isBusiness: isBusinessFlow(config),
67
+ scope: configScope(config),
61
68
  business: config.business,
62
69
  hasDocCapture: idType ? requiresDocumentCapture(idType) : true,
63
70
  hasNfc: nfcEnabled(state),
@@ -72,9 +79,20 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
72
79
  hasCountrySelect:
73
80
  countrySelectOptions(state).length > 1 &&
74
81
  applicantSelfCountry(state.businessApplication) === null,
82
+ hasConsent: hasConsentStep(config),
75
83
  hasEmailVerification: config.emailVerification?.enabled === true,
76
84
  hasPhoneVerification: config.phoneVerification?.enabled === true,
77
85
  hasPoa: hasProofOfAddressStep(config.proofOfAddress),
86
+ hasAddressCollection: hasAddressCollectionStep(config.addressCollection),
87
+ // An absent search flag means no search SCREEN, never an error: the
88
+ // applicant places the pin by hand, the fallback every address failure
89
+ // degrades to.
90
+ addressFlow: addressFlowFor(
91
+ config,
92
+ state.serverConfig.addressSearch === true,
93
+ addressVendorsStubbed({ environment: state.serverConfig.environment }),
94
+ Boolean(state.serverConfig.mapsFrameUrl) && webViewAvailable(),
95
+ ),
78
96
  hasQuestionnaire: hasActiveQuestionnaire(config.questionnaire),
79
97
  // Present only on a session a reviewer sent back. Narrows the order to the
80
98
  // steps they ticked, so somebody fixing one blurry photo is not walked
@@ -237,11 +255,18 @@ export function previousStepBefore(step: KYCStep, state: KycState): KYCStep {
237
255
  return previousStepInOrder(step, stepOrderOptions(state));
238
256
  }
239
257
 
258
+ /**
259
+ * The step this flow OPENS on: 'consent', or the first real step when the
260
+ * workflow switched the consent screen off (`consentStep: false`). The store
261
+ * starts and resets here, the progress watcher judges "untouched" against it,
262
+ * and the flow hides Back on it. Never depends on anything the applicant has
263
+ * yet to choose: only later steps do.
264
+ */
265
+ export function openingStep(state: KycState): KYCStep {
266
+ return openingStepOf(buildStepOrder(stepOrderOptions(state)));
267
+ }
268
+
240
269
  /** Percentage complete, for the sheet's progress indicator. */
241
270
  export function stepProgress(step: KYCStep, state: KycState): number {
242
271
  return getStepProgress(step, stepOrderOptions(state));
243
272
  }
244
-
245
- // ---------------------------------------------------------------------------
246
- // Store factory
247
- // ---------------------------------------------------------------------------
@@ -27,7 +27,7 @@ import {
27
27
  type KycState,
28
28
  type KycStore,
29
29
  } from './state';
30
- import { nextStepAfter, nfcDecision, previousStepBefore } from './derive';
30
+ import { nextStepAfter, nfcDecision, openingStep, previousStepBefore } from './derive';
31
31
 
32
32
  /** The steps that make up ONE ID's evidence. Leaving this set is what ends a
33
33
  * multi-ID check — the leg has several exits depending on the ID. */
@@ -54,12 +54,15 @@ function multiIdPlanFor(s: KycState): ReturnType<typeof multiIdPlan> {
54
54
  );
55
55
  }
56
56
  import { recordStep, resetStepLog } from '../lib/step-log';
57
+ import { resetAutoLocate, resetCurrentFix } from '../lib/address-current-location';
57
58
  import { multiIdPlan } from '../lib/multi-id';
58
59
  import { resolveIdTypeDefinition } from '../config/idTypes';
59
60
  import { buildVerifyRequest } from './submit';
61
+ import { IDLE_SELFIE_UPLOAD } from '../lib/selfie-upload-wait';
60
62
  import { resetBusinessCheck, runBusinessCheck } from './businessCheck';
61
63
  import { startAttemptSession, watchSessionProgress } from './session';
62
64
  import { applicantMediaCaptured, buildApplicantVerifyRequest } from './submitApplicant';
65
+ import { keptIdType } from '../lib/resubmit';
63
66
 
64
67
  export * from './state';
65
68
  export {
@@ -99,11 +102,17 @@ export function createKycStore(
99
102
  sessionUrl: null,
100
103
  businessCheck: { ...EMPTY_BUSINESS_CHECK },
101
104
  selectedCountry: null,
102
- selectedIdType: null,
105
+ countryAutoPicked: false,
106
+ // A send-back that did not ask for the ID keeps the original one: the
107
+ // ID steps are narrowed out, so the kept ID is selected here instead.
108
+ // A restored session only overwrites it with a saved pick.
109
+ selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
103
110
  idNumber: null,
104
111
  multiIdSlotIndex: 0,
105
112
  multiIdSlots: [],
106
113
  multiIdRestored: null,
114
+ selfiePreviewUri: null,
115
+ selfieUpload: IDLE_SELFIE_UPLOAD,
107
116
  mediaIds: {},
108
117
  submissionResult: null,
109
118
  serverConfig: serverConfig ?? INITIAL_SERVER_CONFIG,
@@ -124,6 +133,11 @@ export function createKycStore(
124
133
  chipData: null,
125
134
  poaDocumentType: null,
126
135
  poaFileName: null,
136
+ address: null,
137
+ addressPhotoPreview: null,
138
+ addressIntroSeen: false,
139
+ addressEntranceFraming: false,
140
+ addressSandboxOutcome: null,
127
141
  isLoading: false,
128
142
  error: null,
129
143
 
@@ -137,16 +151,27 @@ export function createKycStore(
137
151
  const branding = res.branding
138
152
  ? { ...res.branding, logo: normalizeDevAssetUrl(res.branding.logo, baseUrl) }
139
153
  : res.branding;
154
+ // The facts that just landed can add a step AHEAD of the one the flow
155
+ // opened on (the address search step, on a consent-less address
156
+ // flow). Someone still standing on the placeholder's opening step,
157
+ // having done nothing, is moved to the real one; anyone who has
158
+ // moved is left alone. Mirrors Flutter's _loadServerConfig.
159
+ const before = openingStep(get());
140
160
  set({
141
161
  serverConfig: {
142
162
  status: 'ready',
143
163
  idTypes: res.idTypes,
144
164
  branding,
145
165
  geoCountry: res.geoCountry,
166
+ addressSearch: res.addressSearch,
167
+ addressSearchMode: res.addressSearchMode,
168
+ mapsFrameUrl: res.mapsFrameUrl ?? null,
146
169
  environment: res.environment,
147
170
  fatal: false,
148
171
  },
149
172
  });
173
+ const after = openingStep(get());
174
+ if (get().currentStep === before && after !== before) set({ currentStep: after });
150
175
  } catch (err) {
151
176
  const described = describeConfigError(err);
152
177
  set((s) => ({
@@ -163,6 +188,7 @@ export function createKycStore(
163
188
  const same = s.selectedCountry === country;
164
189
  return {
165
190
  selectedCountry: country,
191
+ countryAutoPicked: false,
166
192
  selectedIdType: same ? s.selectedIdType : null,
167
193
  idNumber: same ? s.idNumber : null,
168
194
  // A multi-ID run's committed slots belong to the country they were
@@ -175,6 +201,11 @@ export function createKycStore(
175
201
  });
176
202
  },
177
203
 
204
+ setCountryAuto(country) {
205
+ get().setCountry(country);
206
+ if (!get().countryAutoPicked) set({ countryAutoPicked: true });
207
+ },
208
+
178
209
  commitMultiIdSlot(nextStep, previews) {
179
210
  set((s) => {
180
211
  if (!s.selectedIdType) return {};
@@ -257,6 +288,22 @@ export function createKycStore(
257
288
  set((s) => ({ mediaIds: { ...s.mediaIds, [key]: mediaId } }));
258
289
  },
259
290
 
291
+ setSelfiePreview(uri) {
292
+ set({ selfiePreviewUri: uri });
293
+ },
294
+
295
+ clearSelfie() {
296
+ set((s) => ({
297
+ selfiePreviewUri: null,
298
+ selfieUpload: IDLE_SELFIE_UPLOAD,
299
+ mediaIds: { ...s.mediaIds, selfie: undefined, livenessVideo: undefined },
300
+ }));
301
+ },
302
+
303
+ setSelfieUpload(upload) {
304
+ set({ selfieUpload: upload });
305
+ },
306
+
260
307
  setDocumentMediaId(mediaId, side) {
261
308
  set((s) => ({
262
309
  mediaIds: {
@@ -423,6 +470,41 @@ export function createKycStore(
423
470
  });
424
471
  },
425
472
 
473
+ setAddress(address) {
474
+ set({ address });
475
+ },
476
+
477
+ setAddressSandboxOutcome(addressSandboxOutcome) {
478
+ set({ addressSandboxOutcome });
479
+ },
480
+
481
+ setAddressPhoto(mediaId) {
482
+ set((s) => {
483
+ if (mediaId) return { mediaIds: { ...s.mediaIds, addressPhoto: mediaId } };
484
+ const { addressPhoto: _dropped, ...rest } = s.mediaIds;
485
+ return { mediaIds: rest };
486
+ });
487
+ },
488
+
489
+ setAddressPhotoPreview(uri) {
490
+ set({ addressPhotoPreview: uri });
491
+ },
492
+
493
+ markAddressIntroSeen() {
494
+ set({ addressIntroSeen: true });
495
+ },
496
+
497
+ setAddressEntranceFraming(framing) {
498
+ if (get().addressEntranceFraming !== framing) set({ addressEntranceFraming: framing });
499
+ },
500
+
501
+ clearAddress() {
502
+ set((s) => {
503
+ const { addressPhoto: _dropped, ...rest } = s.mediaIds;
504
+ return { address: null, mediaIds: rest, addressPhotoPreview: null };
505
+ });
506
+ },
507
+
426
508
  setDocumentCapturePhase(phase) {
427
509
  if (get().documentCapturePhase !== phase) set({ documentCapturePhase: phase });
428
510
  },
@@ -569,15 +651,29 @@ export function createKycStore(
569
651
  // already sitting on 'consent' (the subscribe below only fires on
570
652
  // change, and recordStep dedupes if it fires too).
571
653
  resetStepLog();
572
- recordStep('consent', multiIdSlotOf(get()), selectedIdTypeOf(get()));
654
+ // The cached GPS fix and the once-per-verification auto-locate are
655
+ // scoped to ONE verification: an app session spans many opens of the
656
+ // SDK, and a fix taken an hour ago would drop the pin at wherever the
657
+ // phone was then.
658
+ resetCurrentFix();
659
+ resetAutoLocate();
660
+ // The opening step never depends on the fields reset below (only later
661
+ // steps do), so it is safe to read before the write.
662
+ const opening = openingStep(get());
663
+ recordStep(opening, multiIdSlotOf(get()), selectedIdTypeOf(get()));
573
664
  set({
574
- currentStep: 'consent',
665
+ currentStep: opening,
575
666
  selectedCountry: null,
576
- selectedIdType: null,
667
+ countryAutoPicked: false,
668
+ // Same kept ID as at creation: the narrowed flow has no picker to
669
+ // choose it again.
670
+ selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
577
671
  idNumber: null,
578
672
  multiIdSlotIndex: 0,
579
673
  multiIdSlots: [],
580
674
  multiIdRestored: null,
675
+ selfiePreviewUri: null,
676
+ selfieUpload: IDLE_SELFIE_UPLOAD,
581
677
  mediaIds: {},
582
678
  submissionResult: null,
583
679
  documentScanPhase: 'front',
@@ -599,6 +695,11 @@ export function createKycStore(
599
695
  chipData: null,
600
696
  poaDocumentType: null,
601
697
  poaFileName: null,
698
+ address: null,
699
+ addressPhotoPreview: null,
700
+ addressIntroSeen: false,
701
+ addressEntranceFraming: false,
702
+ addressSandboxOutcome: null,
602
703
  isLoading: false,
603
704
  error: null,
604
705
  });
@@ -622,6 +723,20 @@ export function createKycStore(
622
723
  // subscription above only fires on CHANGE, and a new store already sits on
623
724
  // 'consent').
624
725
  resetStepLog();
726
+ // The module-level GPS cache and the auto-locate latch are scoped to ONE
727
+ // verification, and this — not reset() — is where a verification begins.
728
+ // Clearing them only in reset() left both alive across modal launches, so a
729
+ // second verification in the same app session dropped its pin wherever the
730
+ // phone had been on the first (possibly hours and a city away) and skipped
731
+ // the automatic locate entirely, since the latch was already spent.
732
+ resetCurrentFix();
733
+ resetAutoLocate();
734
+ // A workflow that switched the consent screen off opens on its first real
735
+ // step; the literal 'consent' above is only the shape before the config is
736
+ // consulted. Set BEFORE the journey is recorded, so the log opens where the
737
+ // person does.
738
+ const opening = openingStep(store.getState());
739
+ if (opening !== store.getState().currentStep) store.setState({ currentStep: opening });
625
740
  recordStep(
626
741
  store.getState().currentStep,
627
742
  multiIdSlotOf(store.getState()),
@@ -21,6 +21,24 @@ export interface ServerConfigState {
21
21
  branding?: SdkConfigBranding;
22
22
  /** The visitor's country from their IP — a default, never evidence. */
23
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;
24
42
  environment?: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
25
43
  /** HTTP status of a failed config fetch (if any). */
26
44
  statusCode?: number;
@@ -1,8 +1,11 @@
1
1
  import type { KycState, KycStore } from './state';
2
+ import { collectDeviceMetadata } from '../services/deviceMetadata';
3
+ import { restoreAddress } from './address';
2
4
  import { getStepLog } from '../lib/step-log';
3
5
  import { emptyKeyPerson, type KeyPersonEntry } from '../config/keyPeople';
4
6
  import type { SessionStartResponse } from '../services/api-types';
5
7
  import { persistentDeviceId } from '../services/fingerprint-sources';
8
+ import { openingStep } from './derive';
6
9
 
7
10
  // ---------------------------------------------------------------------------
8
11
  // The attempt SESSION: minting at launch, and progress writes as the user moves.
@@ -52,6 +55,9 @@ export function startAttemptSession(store: KycStore): void {
52
55
  externalUserId,
53
56
  ...(deviceRef ? { deviceRef } : {}),
54
57
  ...(s.config.workflowId ? { workflowId: s.config.workflowId } : {}),
58
+ // What this phone is, sent up front: the dashboard's in-progress row
59
+ // shows Device and Source from the moment the SDK loads.
60
+ device: collectDeviceMetadata() as unknown as Record<string, unknown>,
55
61
  }),
56
62
  );
57
63
  inflightStarts.set(key, start);
@@ -140,6 +146,13 @@ export function restoreAttemptProgress(
140
146
  ...(d['contact'] && typeof d['contact'] === 'object'
141
147
  ? { contact: { ...s.contact, ...(d['contact'] as object) } }
142
148
  : {}),
149
+ // The pin restores only when it is a real pin — a partial snapshot must
150
+ // degrade to restoring less, never to a shape the step cannot read.
151
+ ...((() => {
152
+ const a = d['address'] as Record<string, unknown> | undefined;
153
+ if (!a || typeof a['lat'] !== 'number' || typeof a['lng'] !== 'number') return {};
154
+ return { address: restoreAddress(a) };
155
+ })()),
143
156
  ...(d['questionnaireAnswers'] && typeof d['questionnaireAnswers'] === 'object'
144
157
  ? {
145
158
  questionnaireAnswers: {
@@ -169,13 +182,19 @@ export function progressFromState(s: ReturnType<KycStore['getState']>): Record<s
169
182
  businessApplication: s.businessApplication,
170
183
  contact: s.contact,
171
184
  questionnaireAnswers: s.questionnaireAnswers,
185
+ address: s.address ?? undefined,
172
186
  },
173
187
  };
174
188
  }
175
189
 
176
- /** Nothing here needs saving until they move off the opening screen. */
177
- export function isUntouchedProgress(payload: Record<string, unknown>): boolean {
178
- if (payload['step'] !== 'consent') return false;
190
+ /**
191
+ * Nothing here needs saving until they move off the opening screen. Judged
192
+ * against the flow's OPENING step ('consent' unless the workflow switched it
193
+ * off, then the first real step), or a consent-less flow would save on mount
194
+ * and every opened link would read "In progress" on the dashboard.
195
+ */
196
+ export function isUntouchedProgress(payload: Record<string, unknown>, opening: string = 'consent'): boolean {
197
+ if (payload['step'] !== opening) return false;
179
198
  return Object.keys((payload['mediaIds'] as object) ?? {}).length === 0;
180
199
  }
181
200
 
@@ -193,7 +212,7 @@ export function watchSessionProgress(store: KycStore): () => void {
193
212
  const s = store.getState();
194
213
  if (!s.sessionId) return;
195
214
  const payload = progressFromState(s);
196
- if (isUntouchedProgress(payload)) return;
215
+ if (isUntouchedProgress(payload, openingStep(s))) return;
197
216
  const fingerprint = JSON.stringify(payload);
198
217
  if (fingerprint === lastSaved) return;
199
218
  lastSaved = fingerprint;
@@ -21,10 +21,13 @@ import type { ServerConfigState } from './serverConfig';
21
21
  import type { CaptureIntegrity } from '../liveness/integritySignals';
22
22
  import type { MrzScan } from '../mrz/parse';
23
23
  import type { EmrtdReadResult } from '../emrtd';
24
+ import type { SelfieUploadState } from '../lib/selfie-upload-wait';
24
25
 
25
26
  export interface KYCMediaIds {
26
27
  /** Proof-of-address document (image or PDF). */
27
28
  proofOfAddress?: string;
29
+ /** Address Intelligence door photo (image only). */
30
+ addressPhoto?: string;
28
31
  documentFront?: string;
29
32
  documentBack?: string;
30
33
  selfie?: string;
@@ -196,6 +199,70 @@ export interface ContactState {
196
199
  expired?: Array<'email' | 'phone'>;
197
200
  }
198
201
 
202
+ /**
203
+ * The smart address the address-collection step gathers. The pin is the map
204
+ * centre the user settled on; the device* fields are the one-shot attest fix
205
+ * taken at Continue (a CLAIM the server evaluates, never a verdict).
206
+ */
207
+ export interface AddressState {
208
+ lat: number;
209
+ lng: number;
210
+ accuracy: number | null;
211
+ directions: string;
212
+ /** Building or estate name — back on the edit-details form (user decision
213
+ * 2026-08-31: everything editable, OkHi-style). */
214
+ propertyName: string;
215
+ propertyNumber: string;
216
+ /** A street the applicant TYPED. Prefilled from the map's answer in the
217
+ * edit-details sheet; stored only once the applicant edits it, so an
218
+ * untouched prefill is never submitted as their claim. */
219
+ street?: string;
220
+ /** The rest of the OkHi-style edit-details form: unit + area/region
221
+ * corrections. All applicant claims, stored only when typed; the server
222
+ * never feeds them into corroboration. undefined = never touched (the
223
+ * map's prefill shows), '' = cleared. */
224
+ unit?: string;
225
+ neighbourhood?: string;
226
+ city?: string;
227
+ state?: string;
228
+ postcode?: string;
229
+ /**
230
+ * The pin's human-readable line, from a search pick or a reverse geocode.
231
+ * Shown in the flow AND sent with the submission as the applicant-confirmed
232
+ * line: the server prefers it over its own weaker derivation.
233
+ */
234
+ label?: string;
235
+ /** Where the label was PICKED for. Presence means the label is
236
+ * human-confirmed, so it survives pin nudges instead of being re-derived on
237
+ * every drag. Absent = the label came from a reverse geocode. Display
238
+ * bookkeeping — never on the wire. */
239
+ pickedAt?: { lat: number; lng: number };
240
+ /** The applicant explicitly chose to KEEP the picked label after moving the
241
+ * pin. Reset when the pin crosses the credibility radius, so the question
242
+ * is asked again exactly once out there. Never on the wire. */
243
+ labelKept?: boolean;
244
+ /** The label broken down — what the details sheet shows as structured rows.
245
+ * Display only, like label's own anchor fields. */
246
+ parts?: {
247
+ street?: string | null;
248
+ area?: string | null;
249
+ city?: string | null;
250
+ state?: string | null;
251
+ postcode?: string | null;
252
+ /** The pin's own ISO-2, from the geocoder; never a sheet row. */
253
+ country?: string | null;
254
+ };
255
+ /** Street View entrance frame: coordinates only, so the server fetches the
256
+ * image with its own key. Captured here through the framed
257
+ * /embed/street-view page in a WebView (FramedStreetView), and also
258
+ * restored from a session begun on a hosted page. */
259
+ streetView?: { panoId: string; heading: number; pitch: number; fov: number };
260
+ deviceLat?: number;
261
+ deviceLng?: number;
262
+ deviceAccuracy?: number;
263
+ capturedAt?: string;
264
+ }
265
+
199
266
  /** Document-capture sub-phase — drives the sheet header title/description. */
200
267
  export type DocumentCapturePhase = 'front' | 'front-preview' | 'back' | 'review';
201
268
 
@@ -248,6 +315,13 @@ export interface KycState {
248
315
  * single-country flow, where `config.country` is the answer.
249
316
  */
250
317
  selectedCountry: string | null;
318
+ /**
319
+ * The declared country was GUESSED (the address scope's IP default, or a
320
+ * geocode adopted from the applicant's fix) rather than picked, so later
321
+ * evidence may correct it; an explicit pick clears it. Mirrors the web
322
+ * SDK's `countryAutoPicked`. See lib/country-adoption.ts.
323
+ */
324
+ countryAutoPicked: boolean;
251
325
  selectedIdType: IdType | null;
252
326
  idNumber: string | null;
253
327
  /**
@@ -266,6 +340,22 @@ export interface KycState {
266
340
  * on the forward journey.
267
341
  */
268
342
  multiIdRestored: { front?: string; back?: string } | null;
343
+ /**
344
+ * The captured selfie's local file path, kept CENTRALLY (the liveness
345
+ * screen's own state dies with it on unmount) so leaving the step and
346
+ * returning restores the review screen instead of re-running the whole
347
+ * gesture check. mediaIds.selfie beside it is the durable record; on a
348
+ * restored session the path may be gone while the mediaId survives.
349
+ */
350
+ selfiePreviewUri: string | null;
351
+ /**
352
+ * Where the selfie (and its liveness video) upload has got to. The liveness
353
+ * step kicks the upload off and, with the review hidden, hands over BEFORE
354
+ * it lands; the submitted step waits on this record rather than on the
355
+ * step that started it (lib/selfie-upload-wait.ts). mediaIds.selfie beside
356
+ * it is the durable proof; this is the in-flight state.
357
+ */
358
+ selfieUpload: SelfieUploadState;
269
359
  mediaIds: KYCMediaIds;
270
360
  submissionResult: KYCSubmissionResult | null;
271
361
  serverConfig: ServerConfigState;
@@ -337,6 +427,26 @@ export interface KycState {
337
427
  poaDocumentType: PoaDocumentType | null;
338
428
  /** Its file name, so the uploaded state can name what it has. */
339
429
  poaFileName: string | null;
430
+ /** The smart address, when the address-collection step gathered one. */
431
+ address: AddressState | null;
432
+ /** Local URI of the uploaded entrance photo, so the review step can show it.
433
+ * A display artefact — never serialised, never restored (a resumed session
434
+ * holds the mediaId but not the bytes). */
435
+ addressPhotoPreview: string | null;
436
+ /** The presence "how it works" primer was acknowledged this session. */
437
+ addressIntroSeen: boolean;
438
+ /** The entrance step is showing the Street View framer, so the sheet
439
+ * header says so. Transient: never serialised, never restored. */
440
+ addressEntranceFraming: boolean;
441
+ /** Dev/sandbox only: the pinned address RESULT outcome (the web SDK's
442
+ * Test-result tabs). Null = the server default. Rides
443
+ * metadata.sandboxOutcome at submit; production ignores it. */
444
+ addressSandboxOutcome:
445
+ | 'address_attested'
446
+ | 'address_corroborated'
447
+ | 'address_collected'
448
+ | 'address_mismatch'
449
+ | null;
340
450
  /**
341
451
  * Contact-verification results. The proofs are single-use and ride the
342
452
  * /verify submission; the addresses are kept so returning to the step (or a
@@ -351,6 +461,9 @@ export interface KycState {
351
461
  // Actions
352
462
  loadServerConfig: () => Promise<void>;
353
463
  setCountry: (country: string) => void;
464
+ /** Declare a GUESSED country: the same reset as `setCountry`, flagged so
465
+ * later evidence may correct it. Mirrors the web SDK's SET_COUNTRY_AUTO. */
466
+ setCountryAuto: (country: string) => void;
354
467
  setIdType: (idType: IdType) => void;
355
468
  setIdNumber: (idNumber: string) => void;
356
469
  /** Commit the current slot's evidence and move to the next check. The
@@ -359,6 +472,12 @@ export interface KycState {
359
472
  /** Step BACK into the previous slot, restoring what it captured. */
360
473
  uncommitMultiIdSlot: (step: KYCStep) => void;
361
474
  setMediaId: (key: MediaIdKey, mediaId: string) => void;
475
+ /** Record the captured selfie's local path (null on retake). */
476
+ setSelfiePreview: (uri: string | null) => void;
477
+ /** Retake: drop the selfie preview + its uploaded media ids in one set. */
478
+ clearSelfie: () => void;
479
+ /** The upload hook's progress report (see `selfieUpload`). */
480
+ setSelfieUpload: (upload: SelfieUploadState) => void;
362
481
  setDocumentMediaId: (mediaId: string, side: 'front' | 'back') => void;
363
482
  setQuestionnaireAnswer: (key: string, value: QuestionnaireAnswerValue | undefined) => void;
364
483
  setContactVerified: (channel: 'email' | 'phone', destination: string, token: string) => void;
@@ -387,6 +506,21 @@ export interface KycState {
387
506
  setChipData: (data: EmrtdReadResult) => void;
388
507
  setProofOfAddress: (mediaId: string, docType: PoaDocumentType, fileName: string) => void;
389
508
  clearProofOfAddress: () => void;
509
+ /** Set / update the smart address (null clears it). */
510
+ setAddress: (address: AddressState | null) => void;
511
+ setAddressSandboxOutcome: (outcome: KycState['addressSandboxOutcome']) => void;
512
+ /** Attach / remove the door photo (null removes it). */
513
+ setAddressPhoto: (mediaId: string | null) => void;
514
+ /** Remember the local URI of the picked entrance photo (null on remove). */
515
+ setAddressPhotoPreview: (uri: string | null) => void;
516
+ /** The applicant acknowledged the presence primer. */
517
+ markAddressIntroSeen: () => void;
518
+ /** The entrance step is (or stops) showing the Street View framer. */
519
+ setAddressEntranceFraming: (framing: boolean) => void;
520
+ /** Drop the pin, its uploaded photo and that photo's preview together — a
521
+ * half-cleared address would show an entrance for a building nobody
522
+ * pinned. */
523
+ clearAddress: () => void;
390
524
  setDocumentCapturePhase: (phase: DocumentCapturePhase) => void;
391
525
  setContactChallenge: (challenge: ContactChallenge | null) => void;
392
526
  setImmersiveCapture: (immersive: boolean) => void;
@@ -12,11 +12,13 @@
12
12
  // unconfigured contact proof must not claim a check that never ran.
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
+ import { configScope, SCOPE_ID_TYPES } from '../lib/scope';
15
16
  import { collectDeviceMetadata } from '../services/deviceMetadata';
16
17
  import { generateRequestId } from '../utils/uuid';
17
18
  import { hasActiveQuestionnaire, questionnairePayload } from '../config/questionnaire';
18
19
  import { multiIdWireSlots } from '../lib/multi-id';
19
20
  import { isBusinessFlow } from '../config/business';
21
+ import { addressPayload } from '../config/addressCollection';
20
22
  import { businessSubmission, effectiveCountry } from './derive';
21
23
  import type { ClientFingerprint } from '../services/fingerprint';
22
24
  import type { VerifyRequest } from '../services/api';
@@ -66,7 +68,13 @@ export function buildVerifyRequest(
66
68
 
67
69
  return {
68
70
  country: business ? business.country : effectiveCountry(state),
69
- idType: business ? business.product : (primary?.idType ?? state.selectedIdType ?? ''),
71
+ // Scoped flows carry the scope's transport marker instead of a picked ID —
72
+ // the server requires a published workflow of the matching scope for it.
73
+ idType: business
74
+ ? business.product
75
+ : configScope(state.config)
76
+ ? SCOPE_ID_TYPES[configScope(state.config)!]
77
+ : (primary?.idType ?? state.selectedIdType ?? ''),
70
78
  idNumber: business ? undefined : (primary?.idNumber ?? state.idNumber ?? undefined),
71
79
  ...(multiSlots
72
80
  ? {
@@ -91,6 +99,10 @@ export function buildVerifyRequest(
91
99
  : state.mediaIds,
92
100
  ...(state.config.workflowId ? { workflowId: state.config.workflowId } : {}),
93
101
  ...(state.poaDocumentType ? { proofOfAddressType: state.poaDocumentType } : {}),
102
+ // The smart address, when the step gathered one. Sent on individual AND
103
+ // business submissions (a KYB flow's pin is the business premises) — the
104
+ // server validates it against the workflow either way.
105
+ ...(state.address ? { address: addressPayload(state.address, state.config.addressCollection) } : {}),
94
106
  ...(state.contact.emailToken || state.contact.phoneToken
95
107
  ? {
96
108
  contact: {
@@ -136,6 +148,11 @@ export function buildVerifyRequest(
136
148
  ...(business && state.business.sandboxOutcome
137
149
  ? { sandboxOutcome: state.business.sandboxOutcome }
138
150
  : {}),
151
+ // The address flow's Test-result pick (dev/sandbox tabs on the review
152
+ // step) — same contract, same key, no clash: business flows never set it.
153
+ ...(!business && state.addressSandboxOutcome
154
+ ? { sandboxOutcome: state.addressSandboxOutcome }
155
+ : {}),
139
156
  requestId: generateRequestId(),
140
157
  device: {
141
158
  ...(collectDeviceMetadata() as unknown as Record<string, unknown>),