@myazahq/kyc-sdk-react-native 2.5.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 (167) hide show
  1. package/README.md +151 -5
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
  3. package/app.plugin.js +89 -8
  4. package/ios/HybridMyazaFaceDetector.swift +8 -3
  5. package/package.json +14 -2
  6. package/src/MyazaBiometricAuth.tsx +119 -0
  7. package/src/components/CountryField.tsx +8 -0
  8. package/src/components/CountryRegionPicker.tsx +184 -0
  9. package/src/components/DialCodePicker.tsx +47 -68
  10. package/src/components/DialCodeRow.tsx +102 -0
  11. package/src/components/FramedMapPicker.tsx +122 -0
  12. package/src/components/GeoBadge.tsx +34 -0
  13. package/src/components/Icon.tsx +2 -185
  14. package/src/components/KycFlow.tsx +60 -32
  15. package/src/components/LineSkeleton.tsx +100 -0
  16. package/src/components/MapChrome.tsx +67 -0
  17. package/src/components/MapPinMarker.tsx +54 -0
  18. package/src/components/MapPinPicker.tsx +179 -0
  19. package/src/components/MilestoneTrack.tsx +155 -0
  20. package/src/components/MyazaInput.tsx +17 -1
  21. package/src/components/OptionRow.tsx +22 -3
  22. package/src/components/PhoneNumberInput.tsx +8 -0
  23. package/src/components/PoweredBy.tsx +5 -5
  24. package/src/components/PresenceBlocks.tsx +153 -0
  25. package/src/components/StepView.tsx +17 -0
  26. package/src/components/StickyActions.tsx +56 -0
  27. package/src/components/dialCodeRows.ts +64 -0
  28. package/src/components/icon-map.ts +176 -0
  29. package/src/components/stepHeaderMeta.tsx +42 -4
  30. package/src/config/addressCollection.ts +125 -0
  31. package/src/config/biometricOptions.ts +100 -0
  32. package/src/config/business.ts +20 -1
  33. package/src/config/businessSteps.ts +8 -2
  34. package/src/config/consentStep.ts +19 -0
  35. package/src/config/proofOfAddress.ts +54 -11
  36. package/src/config/regions.ts +24 -0
  37. package/src/config/stepOrder.ts +79 -5
  38. package/src/config/uploadLimits.ts +35 -0
  39. package/src/config/workflowMerge.ts +11 -5
  40. package/src/index.ts +54 -1
  41. package/src/lib/address-current-location.ts +171 -0
  42. package/src/lib/address-field-modes.ts +119 -0
  43. package/src/lib/address-flow.ts +172 -0
  44. package/src/lib/address-helpers.ts +48 -0
  45. package/src/lib/address-line.ts +76 -0
  46. package/src/lib/address-pin-move.ts +97 -0
  47. package/src/lib/address-step-recovery.ts +63 -0
  48. package/src/lib/authed-image.ts +78 -0
  49. package/src/lib/biometric-auth.ts +52 -0
  50. package/src/lib/biometric-copy.ts +51 -0
  51. package/src/lib/captureRing.ts +83 -0
  52. package/src/lib/country-adoption.ts +89 -0
  53. package/src/lib/inferred-country.ts +65 -0
  54. package/src/lib/livenessLayout.ts +49 -0
  55. package/src/lib/map-frame.ts +159 -0
  56. package/src/lib/map-tiles.ts +143 -0
  57. package/src/lib/poa-country-gate.ts +27 -0
  58. package/src/lib/result-copy.ts +116 -0
  59. package/src/lib/result-wait.ts +53 -0
  60. package/src/lib/review-map-surface.ts +26 -0
  61. package/src/lib/scope.ts +31 -0
  62. package/src/lib/selfie-upload-wait.ts +79 -0
  63. package/src/lib/street-view-fov.ts +42 -0
  64. package/src/lib/webview-available.ts +47 -0
  65. package/src/liveness/useLiveness.ts +6 -1
  66. package/src/presence/background-math.ts +100 -0
  67. package/src/presence/background-store.ts +82 -0
  68. package/src/presence/background.ts +165 -0
  69. package/src/presence/foreground-service.ts +193 -0
  70. package/src/presence/fs.ts +51 -0
  71. package/src/presence/geofence.ts +41 -0
  72. package/src/presence/math.ts +44 -0
  73. package/src/presence/post.ts +40 -0
  74. package/src/presence/report.ts +87 -0
  75. package/src/presence/sampler.ts +110 -0
  76. package/src/presence/status.ts +92 -0
  77. package/src/presence/store.ts +94 -0
  78. package/src/presence/tier.ts +36 -0
  79. package/src/presence/watch-wait.ts +81 -0
  80. package/src/screens/AddressCountryControl.tsx +115 -0
  81. package/src/screens/BusinessDetailsFields.tsx +3 -0
  82. package/src/screens/BusinessDetailsStep.tsx +2 -0
  83. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  84. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  85. package/src/screens/CompanyInfoFields.tsx +6 -1
  86. package/src/screens/ConsentStep.tsx +6 -17
  87. package/src/screens/ContactActions.tsx +54 -0
  88. package/src/screens/ContactDestinationField.tsx +4 -0
  89. package/src/screens/ContactEntryPanel.tsx +75 -0
  90. package/src/screens/ContactVerificationStep.tsx +26 -56
  91. package/src/screens/CountrySelectStep.tsx +13 -147
  92. package/src/screens/LivenessAvatar.tsx +15 -3
  93. package/src/screens/LivenessStep.tsx +154 -52
  94. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  95. package/src/screens/ProofOfAddressParts.tsx +151 -0
  96. package/src/screens/ProofOfAddressStep.tsx +58 -99
  97. package/src/screens/SubmittedBadge.tsx +25 -0
  98. package/src/screens/SubmittedError.tsx +64 -0
  99. package/src/screens/SubmittedResult.tsx +117 -0
  100. package/src/screens/SubmittedStep.tsx +71 -164
  101. package/src/screens/SubmittedSuccess.tsx +127 -0
  102. package/src/screens/SubmittedWaiting.tsx +45 -0
  103. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  104. package/src/screens/address/AddressIntroGate.tsx +148 -0
  105. package/src/screens/address/AddressMapStub.tsx +50 -0
  106. package/src/screens/address/AddressPinStep.tsx +178 -0
  107. package/src/screens/address/AddressReviewStep.tsx +162 -0
  108. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  109. package/src/screens/address/AddressSearchField.tsx +115 -0
  110. package/src/screens/address/AddressSearchStep.tsx +94 -0
  111. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  112. package/src/screens/address/DetailsSheet.tsx +175 -0
  113. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  114. package/src/screens/address/EntranceDropzone.tsx +178 -0
  115. package/src/screens/address/EntranceFraming.tsx +66 -0
  116. package/src/screens/address/EntrancePills.tsx +60 -0
  117. package/src/screens/address/FramedStreetView.tsx +155 -0
  118. package/src/screens/address/IntroDisclosures.tsx +210 -0
  119. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  120. package/src/screens/address/PinSummaryRow.tsx +105 -0
  121. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  122. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  123. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  124. package/src/screens/address/SearchResults.tsx +105 -0
  125. package/src/screens/address/SearchScreen.tsx +176 -0
  126. package/src/screens/address/SkipForNow.tsx +43 -0
  127. package/src/screens/address/StreetViewChrome.tsx +81 -0
  128. package/src/screens/address/detail-values.ts +22 -0
  129. package/src/screens/address/fix-source.ts +27 -0
  130. package/src/screens/address/index.ts +8 -0
  131. package/src/screens/address/meta.ts +44 -0
  132. package/src/screens/address/use-address-flow.ts +200 -0
  133. package/src/screens/address/use-label-pin.ts +80 -0
  134. package/src/screens/address/use-pin-actions.ts +192 -0
  135. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  136. package/src/screens/consent/model.ts +79 -10
  137. package/src/screens/liveness/CaptureRing.tsx +91 -0
  138. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  139. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  140. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  141. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  142. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  143. package/src/screens/liveness/index.ts +4 -0
  144. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  145. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  146. package/src/screens/useAddressPhotoAttach.ts +77 -0
  147. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  148. package/src/screens/usePoaAttach.ts +12 -15
  149. package/src/services/api-biometric.ts +37 -0
  150. package/src/services/api-types-biometric.ts +41 -0
  151. package/src/services/api-types.ts +93 -1
  152. package/src/services/api-verify-types.ts +37 -0
  153. package/src/services/api.ts +133 -3
  154. package/src/services/deviceMetadata.ts +1 -1
  155. package/src/services/location.ts +234 -0
  156. package/src/services/uploadErrors.ts +30 -0
  157. package/src/services/workflowGate.ts +7 -0
  158. package/src/store/address.ts +97 -0
  159. package/src/store/derive.ts +29 -4
  160. package/src/store/kycStore.ts +112 -3
  161. package/src/store/serverConfig.ts +18 -0
  162. package/src/store/session.ts +23 -4
  163. package/src/store/state.ts +134 -0
  164. package/src/store/submit.ts +18 -1
  165. package/src/types/config.ts +48 -1
  166. package/src/types/verification.ts +12 -0
  167. package/src/types/workflow.ts +97 -1
@@ -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>),
@@ -7,7 +7,7 @@ import type { ResubmitConfig } from '../lib/resubmit';
7
7
  // `types/config` remains the single import for consumers.
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
- import type { KYCSubmission, KYCError } from './verification';
10
+ import type { KYCSubmission, KYCError, KYCResult } from './verification';
11
11
  import type { IdType, IdTypeForCountry, SupportedCountry } from './id-types';
12
12
  import type {
13
13
  KYCAppearance,
@@ -25,6 +25,7 @@ import type {
25
25
  QuestionnaireConfig,
26
26
  WorkflowCountry,
27
27
  MultiIdConfig,
28
+ AddressCollectionConfig,
28
29
  } from './workflow';
29
30
 
30
31
  export type * from './workflow';
@@ -56,6 +57,13 @@ export type KYCStep =
56
57
  | 'applicant-role'
57
58
  | 'liveness'
58
59
  | 'proof-of-address'
60
+ // The address flow, in order: find it (search) → confirm it (the PIN step,
61
+ // which keeps the original 'address-collection' wire name so older session
62
+ // progress restores cleanly) → show it (entrance photo) → commit it.
63
+ | 'address-search'
64
+ | 'address-collection'
65
+ | 'address-entrance'
66
+ | 'address-review'
59
67
  | 'questionnaire'
60
68
  | 'submitted';
61
69
 
@@ -164,15 +172,42 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
164
172
  */
165
173
  keyPeopleLinkRecovery?: boolean;
166
174
 
175
+ /**
176
+ * Show the consent (welcome) screen as the flow's first step. Default true.
177
+ * `false` is for a host app that has already collected the person's
178
+ * consent: the flow then opens straight on its first real step (the contact
179
+ * codes, the country picker, the ID list, the business form, or a scoped
180
+ * flow's own check). Normally set by a workflow. It does not change what
181
+ * the organisation attests to the verification provider.
182
+ */
183
+ consentStep?: boolean;
184
+
167
185
  /** Colours in the flash sequence (2–5, default 4). Flash modes only. */
168
186
  flashSequenceLength?: number;
169
187
 
188
+ /**
189
+ * The biometric scopes' flow options (workflow-driven, or passed here on a
190
+ * prop-configured mount): `selfieReview` shows the captured selfie with
191
+ * Retake and Continue before submitting (off by default on both biometric
192
+ * scopes); `resultDelivery` says where a re-authentication's verdict lands,
193
+ * 'both' (the default: the SDK holds the person on one loading screen until
194
+ * the check settles, and the org's webhook receives it too), 'app' (the
195
+ * same wait, but the server sends no webhook for the check) or 'webhook'
196
+ * (fire-and-forget); `doneButton` (default true) hides the final screen's Done
197
+ * when the host app closes the flow itself from `onResult`. A flow key wins
198
+ * per field. See config/biometricOptions.
199
+ */
200
+ biometric?: import('../config/biometricOptions').BiometricFlowConfig;
201
+
170
202
  // ── Workflow-driven blocks ────────────────────────────────────────────────
171
203
  // Authored in the dashboard's workflow builder, not usually in consumer code.
172
204
  // A resolved flow always wins over a prop of the same name.
173
205
 
174
206
  /** What this flow verifies. Absent = 'individual' (classic KYC). */
175
207
  subjectType?: SubjectType;
208
+ /** Workflow scope — what this flow verifies (absent = full verification).
209
+ * Only ever set by a resolved workflow config / hosted session snapshot. */
210
+ scope?: import('../lib/scope').WorkflowScope;
176
211
 
177
212
  /** KYB registry configuration. Required when `subjectType` is 'business'. */
178
213
  business?: WorkflowBusinessConfig;
@@ -196,6 +231,10 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
196
231
  /** Proof-of-address document check, after capture. */
197
232
  proofOfAddress?: ProofOfAddressConfig;
198
233
 
234
+ /** Address Intelligence: a map-pin smart address (+ optional door photo and
235
+ * directions), corroborated server-side. KYC AND KYB (premises pin). */
236
+ addressCollection?: AddressCollectionConfig;
237
+
199
238
  /** Compliance declarations asked just before submission. */
200
239
  questionnaire?: QuestionnaireConfig;
201
240
 
@@ -279,6 +318,14 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
279
318
  * status: 'processing' — results arrive async via webhook.
280
319
  */
281
320
  onSubmit?: (submission: KYCSubmission) => void;
321
+ /**
322
+ * Fires once with the verdict when the flow WAITS for it in-app (a
323
+ * biometric re-authentication on `resultDelivery: 'both'`, the default, or 'app').
324
+ * Never fires on a fire-and-forget flow, and never on a wait that timed out:
325
+ * the webhook stays the record either way. Carries state and reason only,
326
+ * never result data.
327
+ */
328
+ onResult?: (result: KYCResult) => void;
282
329
  onClose?: () => void;
283
330
  /**
284
331
  * Fires for technical errors only. Receives a {@link KYCError} carrying a
@@ -2,6 +2,18 @@
2
2
  // Submission callback payload (returned to onSubmit)
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
+ /**
6
+ * The verdict a flow that waited for it hands to `onResult`: the same state
7
+ * vocabulary `GET /api/kyc/status/:id` serves, plus the reason pair. No result
8
+ * data (scores, biodata) ever rides here; that stays behind the secret key.
9
+ */
10
+ export interface KYCResult {
11
+ verificationId: string;
12
+ status: import('../services/api-types').SessionStatus;
13
+ reason: string | null;
14
+ reasonCode: string | null;
15
+ }
16
+
5
17
  export interface KYCSubmission {
6
18
  verificationId: string;
7
19
  status: 'processing';
@@ -13,7 +13,16 @@
13
13
 
14
14
  // ── Proof of Address ────────────────────────────────────────────────────────
15
15
 
16
- export type PoaDocumentType = 'utility_bill' | 'bank_statement' | 'tenancy_agreement' | 'other';
16
+ export type PoaDocumentType =
17
+ | 'utility_bill'
18
+ | 'bank_statement'
19
+ | 'tenancy_agreement'
20
+ | 'government_document'
21
+ | 'other';
22
+
23
+ /** Whether the applicant's name must appear on the document — judged by the
24
+ * server; read here only to word the step. */
25
+ export type PoaNameRule = 'required' | 'optional' | 'off';
17
26
 
18
27
  export interface ProofOfAddressConfig {
19
28
  /** Adds the Proof of Address step (after capture, before the questionnaire). */
@@ -24,6 +33,93 @@ export interface ProofOfAddressConfig {
24
33
  otherLabel?: string;
25
34
  /** Recency window the server checks the document date against (default 90). */
26
35
  maxAgeDays?: number;
36
+ /**
37
+ * The org's accepted countries (absent/empty = all). On the ADDRESS SCOPE
38
+ * this is exactly what the declared-country picker offers; on a full flow it
39
+ * gates the step client-side (the server stays soft).
40
+ */
41
+ countries?: string[];
42
+ /**
43
+ * Per-country document-kind overrides (ISO-2 → kinds). A present entry
44
+ * REPLACES `documentTypes` for that country.
45
+ */
46
+ countryDocuments?: Record<string, PoaDocumentType[]>;
47
+ /** The default name rule for every country and kind (absent = required). */
48
+ nameMatch?: PoaNameRule;
49
+ /** Per-country, per-kind exceptions to `nameMatch` (ISO-2 → kind → rule). */
50
+ countryNameMatch?: Record<string, Partial<Record<PoaDocumentType, PoaNameRule>>>;
51
+ }
52
+
53
+ // ── Address Intelligence (smart-address capture) ────────────────────────────
54
+
55
+ export interface AddressCollectionConfig {
56
+ /** Adds the address-collection step (after Proof of Address; on KYB flows it
57
+ * collects the business premises pin). */
58
+ enabled?: boolean;
59
+ /** Block Continue without a confirmed pin (the server 422s without one too). */
60
+ requirePin?: boolean;
61
+ /** Whether the door-photo input is offered/required (default optional). */
62
+ photo?: 'off' | 'optional' | 'required';
63
+ /** Whether the directions field is offered/required (default optional). */
64
+ directions?: 'off' | 'optional' | 'required';
65
+ /**
66
+ * The GROUP default for the typed details-sheet fields: `'off'` hides them
67
+ * all, `'optional'` (the default) offers them, `'required'` requires the
68
+ * house or flat NUMBER (the building name stays optional). Resolved per
69
+ * field by lib/address-field-modes.ts, the mirror of the server's rule; the
70
+ * pin step holds Continue and the review holds Confirm until every required
71
+ * field shows a value, because the server 422s a submission that arrives
72
+ * without one.
73
+ */
74
+ propertyFields?: 'off' | 'optional' | 'required';
75
+ /**
76
+ * Per-field overrides on the group default, keyed by the typed field
77
+ * (`propertyName`, `propertyNumber`, `street`, `unit`, `neighbourhood`,
78
+ * `city`, `state`, `postcode`). A `'required'` field the applicant leaves on
79
+ * its map prefill is submitted as displayed: they saw it and confirmed by
80
+ * continuing.
81
+ */
82
+ fields?: Partial<
83
+ Record<
84
+ 'propertyName' | 'propertyNumber' | 'street' | 'unit' | 'neighbourhood' | 'city' | 'state' | 'postcode',
85
+ 'off' | 'optional' | 'required'
86
+ >
87
+ >;
88
+ /**
89
+ * Street View entrance framing. Default 'optional' (on wherever coverage
90
+ * exists, with the photo as the fallback); 'required' is a client-UX gate
91
+ * that removes the Skip affordance while coverage exists — no-coverage
92
+ * still falls back to the photo, and the server never refuses over it.
93
+ *
94
+ * Offered on mobile through the hosted /embed/street-view page in a
95
+ * WebView on the app grant, the way the framed map is (needs the server's
96
+ * maps frame URL and the optional `react-native-webview` peer); without
97
+ * either the entrance step is photo-only. See addressFlowOptions.
98
+ */
99
+ streetView?: 'off' | 'optional' | 'required';
100
+ /**
101
+ * Take a one-shot device GPS fix when the pin is confirmed, so the server
102
+ * can judge "captured at the claimed address" (the `attested` tier). The fix
103
+ * is a CLAIM the server evaluates, never a verdict.
104
+ */
105
+ attestPresence?: boolean;
106
+ /**
107
+ * Phase 2: multi-day presence verification. When enabled, the SDK stores the
108
+ * confirmed pin ON-DEVICE so later `reportAddressPresence()` calls can
109
+ * evaluate the fence locally — coordinates never leave the phone after
110
+ * capture. `background` is reserved for the org-opt-in OS-geofence tier.
111
+ */
112
+ presence?: {
113
+ enabled?: boolean;
114
+ windowDays?: number;
115
+ minNights?: number;
116
+ minDays?: number;
117
+ dwellFloorMinutes?: number;
118
+ background?: boolean;
119
+ /** OkHi-style always-on monitoring: the server renews each
120
+ * resolved cycle, and the on-device pin never self-expires. */
121
+ alwaysOn?: boolean;
122
+ };
27
123
  }
28
124
 
29
125
  // ── NFC chip read (eMRTD) ───────────────────────────────────────────────────