@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.
- package/README.md +151 -5
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DialCodePicker.tsx +47 -68
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -185
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MyazaInput.tsx +17 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/PhoneNumberInput.tsx +8 -0
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +42 -4
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +20 -1
- package/src/config/businessSteps.ts +8 -2
- package/src/config/consentStep.ts +19 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/stepOrder.ts +79 -5
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/workflowMerge.ts +11 -5
- package/src/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/BusinessDetailsFields.tsx +3 -0
- package/src/screens/BusinessDetailsStep.tsx +2 -0
- package/src/screens/BusinessDocumentSlot.tsx +3 -2
- package/src/screens/BusinessDocumentsStep.tsx +4 -55
- package/src/screens/CompanyInfoFields.tsx +6 -1
- package/src/screens/ConsentStep.tsx +6 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +4 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +26 -56
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +154 -52
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -99
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +71 -164
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +93 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +133 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +112 -3
- package/src/store/serverConfig.ts +18 -0
- package/src/store/session.ts +23 -4
- package/src/store/state.ts +134 -0
- package/src/store/submit.ts +18 -1
- package/src/types/config.ts +48 -1
- package/src/types/verification.ts +12 -0
- 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;
|
package/src/store/session.ts
CHANGED
|
@@ -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
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
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;
|
package/src/store/state.ts
CHANGED
|
@@ -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;
|
package/src/store/submit.ts
CHANGED
|
@@ -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
|
-
|
|
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>),
|
package/src/types/config.ts
CHANGED
|
@@ -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';
|
package/src/types/workflow.ts
CHANGED
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
// ── Proof of Address ────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
export type PoaDocumentType =
|
|
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) ───────────────────────────────────────────────────
|