@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -5
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- 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 -176
- 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/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- 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/contact-recovery.ts +44 -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/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -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/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- 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/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- 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 +82 -119
- 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/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -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 +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { KycState, KycStore } from './state';
|
|
2
|
+
import { collectDeviceMetadata } from '../services/deviceMetadata';
|
|
3
|
+
import { restoreAddress } from './address';
|
|
4
|
+
import { getStepLog } from '../lib/step-log';
|
|
5
|
+
import { emptyKeyPerson, type KeyPersonEntry } from '../config/keyPeople';
|
|
6
|
+
import type { SessionStartResponse } from '../services/api-types';
|
|
7
|
+
import { persistentDeviceId } from '../services/fingerprint-sources';
|
|
8
|
+
import { openingStep } from './derive';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The attempt SESSION: minting at launch, and progress writes as the user moves.
|
|
12
|
+
//
|
|
13
|
+
// Ported from the web SDK (MyazaKYC.tsx beginSession + useSessionProgress),
|
|
14
|
+
// with the same contracts:
|
|
15
|
+
//
|
|
16
|
+
// Best-effort throughout. Sessions power resumability, the dashboard's live
|
|
17
|
+
// attempt view, session webhooks, and the registry check at selection —
|
|
18
|
+
// verifying is never conditional on one existing, so every failure here is
|
|
19
|
+
// swallowed.
|
|
20
|
+
//
|
|
21
|
+
// Untouched progress is never written. The presence of stored progress IS
|
|
22
|
+
// "they started"; a save-on-mount would make every opened flow look started,
|
|
23
|
+
// and the server tells an abandoned first screen from a worked-through form
|
|
24
|
+
// by exactly this absence.
|
|
25
|
+
//
|
|
26
|
+
// A remount RESTORES the user to where they were, like web: the resumed
|
|
27
|
+
// session's stored progress hydrates the store (restoreAttemptProgress),
|
|
28
|
+
// so their step, captures and typed data survive an app restart.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const SAVE_DEBOUNCE_MS = 800;
|
|
32
|
+
|
|
33
|
+
// Collapses concurrent mints into one request per launch (mirrors the web
|
|
34
|
+
// SDK's start-session-once). Dev-mode double-invocation fired /session/start
|
|
35
|
+
// twice ~600ms apart; without an externalUserId the server has nothing to
|
|
36
|
+
// resume by, so the second call minted a SECOND session — the flow adopted one
|
|
37
|
+
// and the orphan sat on the org's list forever as "Not started". Short window
|
|
38
|
+
// on purpose: it collapses a double-invoke, never caches sessions, and a
|
|
39
|
+
// failure clears immediately so a retry is a real retry.
|
|
40
|
+
const inflightStarts = new Map<string, Promise<SessionStartResponse>>();
|
|
41
|
+
|
|
42
|
+
/** Mint (or resume) the attempt session and remember its id. Fire-and-forget. */
|
|
43
|
+
export function startAttemptSession(store: KycStore): void {
|
|
44
|
+
const s = store.getState();
|
|
45
|
+
if (s.sessionId) return;
|
|
46
|
+
const externalUserId = s.config.userId ?? s.config.metadata?.['userId'];
|
|
47
|
+
const key = `${s.config.apiKey}|${s.config.workflowId ?? ''}|${externalUserId ?? ''}`;
|
|
48
|
+
let start = inflightStarts.get(key);
|
|
49
|
+
if (!start) {
|
|
50
|
+
// The device id is the anonymous-mount resume fallback: without a userId
|
|
51
|
+
// the server has nothing else to find the previous attempt by, and every
|
|
52
|
+
// app relaunch minted a fresh session. Hashed server-side before storage.
|
|
53
|
+
start = persistentDeviceId().then((deviceRef) =>
|
|
54
|
+
s.api.startSession({
|
|
55
|
+
externalUserId,
|
|
56
|
+
...(deviceRef ? { deviceRef } : {}),
|
|
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>,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
inflightStarts.set(key, start);
|
|
64
|
+
start.then(
|
|
65
|
+
() => setTimeout(() => inflightStarts.delete(key), 2000),
|
|
66
|
+
() => inflightStarts.delete(key),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
void start
|
|
70
|
+
.then((res) => {
|
|
71
|
+
store.getState().setSessionId(res.sessionId, res.url ?? null);
|
|
72
|
+
// Resuming: put the user back where they were, exactly as web does.
|
|
73
|
+
// Media references are pruned server-side of anything expired, so a
|
|
74
|
+
// restored capture slot is one whose bytes genuinely still exist.
|
|
75
|
+
if (res.progress) restoreAttemptProgress(store, res.progress);
|
|
76
|
+
})
|
|
77
|
+
.catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hydrate the store from a resumed session's stored progress — the RN mirror
|
|
82
|
+
* of the web reducer's RESTORE_PROGRESS. Only fields the snapshot carries are
|
|
83
|
+
* touched; everything else keeps its launch value, so a partial or old
|
|
84
|
+
* snapshot degrades to restoring less, never to breaking the flow.
|
|
85
|
+
*/
|
|
86
|
+
export function restoreAttemptProgress(
|
|
87
|
+
store: KycStore,
|
|
88
|
+
progress: NonNullable<SessionStartResponse['progress']>,
|
|
89
|
+
): void {
|
|
90
|
+
const s = store.getState();
|
|
91
|
+
const d = (progress.data ?? {}) as Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
const app = d['businessApplication'] as
|
|
94
|
+
| (Partial<KycState['businessApplication']> & { keyPeople?: Array<Record<string, unknown>> })
|
|
95
|
+
| undefined;
|
|
96
|
+
|
|
97
|
+
store.setState({
|
|
98
|
+
// The terminal step is a RESULT, not a position. A submission that FAILED
|
|
99
|
+
// still writes `submitted` as the last step reached, so resuming there
|
|
100
|
+
// submits again, fails the same way and writes it again: closing the app
|
|
101
|
+
// and reopening lands straight back on the error with no way forward.
|
|
102
|
+
// Starting the flow again also re-resolves the workflow, so an applicant
|
|
103
|
+
// caught mid-flight across a republish walks the CURRENT steps rather than
|
|
104
|
+
// submitting against rules they were never shown.
|
|
105
|
+
...(typeof progress.step === 'string' && progress.step !== 'submitted'
|
|
106
|
+
? { currentStep: progress.step as KycState['currentStep'] }
|
|
107
|
+
: {}),
|
|
108
|
+
...(progress.mediaIds ? { mediaIds: { ...s.mediaIds, ...progress.mediaIds } } : {}),
|
|
109
|
+
...(typeof d['selectedCountry'] === 'string'
|
|
110
|
+
? { selectedCountry: d['selectedCountry'] as string }
|
|
111
|
+
: {}),
|
|
112
|
+
...(typeof d['selectedIdType'] === 'string'
|
|
113
|
+
? { selectedIdType: d['selectedIdType'] as KycState['selectedIdType'] }
|
|
114
|
+
: {}),
|
|
115
|
+
...(typeof d['idNumber'] === 'string' ? { idNumber: d['idNumber'] as string } : {}),
|
|
116
|
+
...(d['business'] && typeof d['business'] === 'object'
|
|
117
|
+
? { business: { ...s.business, ...(d['business'] as object) } }
|
|
118
|
+
: {}),
|
|
119
|
+
...(app
|
|
120
|
+
? {
|
|
121
|
+
businessApplication: {
|
|
122
|
+
...s.businessApplication,
|
|
123
|
+
...app,
|
|
124
|
+
// Rows saved BEFORE the sectioned redesign predate `roles`, `title`
|
|
125
|
+
// and `owners`; a restored attempt must not hand the cards a shape
|
|
126
|
+
// they cannot read. Same normalization as web's RESTORE_PROGRESS.
|
|
127
|
+
...(app.keyPeople
|
|
128
|
+
? {
|
|
129
|
+
keyPeople: app.keyPeople.map((row) => ({
|
|
130
|
+
...emptyKeyPerson(),
|
|
131
|
+
...row,
|
|
132
|
+
roles:
|
|
133
|
+
Array.isArray(row['roles']) && (row['roles'] as unknown[]).length > 0
|
|
134
|
+
? (row['roles'] as KeyPersonEntry['roles'])
|
|
135
|
+
: [(row['role'] as KeyPersonEntry['role']) ?? 'director'],
|
|
136
|
+
title: typeof row['title'] === 'string' ? (row['title'] as string) : '',
|
|
137
|
+
owners: Array.isArray(row['owners'])
|
|
138
|
+
? (row['owners'] as KeyPersonEntry['owners'])
|
|
139
|
+
: [],
|
|
140
|
+
})),
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
143
|
+
} as KycState['businessApplication'],
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
...(d['contact'] && typeof d['contact'] === 'object'
|
|
147
|
+
? { contact: { ...s.contact, ...(d['contact'] as object) } }
|
|
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
|
+
})()),
|
|
156
|
+
...(d['questionnaireAnswers'] && typeof d['questionnaireAnswers'] === 'object'
|
|
157
|
+
? {
|
|
158
|
+
questionnaireAnswers: {
|
|
159
|
+
...s.questionnaireAnswers,
|
|
160
|
+
...(d['questionnaireAnswers'] as object),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The progress snapshot the server stores — mirrors the web SDK's shape, so
|
|
168
|
+
* the dashboard's attempt page reads both without branching. */
|
|
169
|
+
export function progressFromState(s: ReturnType<KycStore['getState']>): Record<string, unknown> {
|
|
170
|
+
const mediaIds = Object.fromEntries(
|
|
171
|
+
Object.entries(s.mediaIds ?? {}).filter(([, v]) => typeof v === 'string' && v),
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
step: s.currentStep,
|
|
175
|
+
stepLog: getStepLog(),
|
|
176
|
+
mediaIds,
|
|
177
|
+
data: {
|
|
178
|
+
selectedCountry: s.selectedCountry ?? undefined,
|
|
179
|
+
selectedIdType: s.selectedIdType ?? undefined,
|
|
180
|
+
idNumber: s.idNumber || undefined,
|
|
181
|
+
business: s.business,
|
|
182
|
+
businessApplication: s.businessApplication,
|
|
183
|
+
contact: s.contact,
|
|
184
|
+
questionnaireAnswers: s.questionnaireAnswers,
|
|
185
|
+
address: s.address ?? undefined,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
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;
|
|
198
|
+
return Object.keys((payload['mediaIds'] as object) ?? {}).length === 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Persist progress as the user advances. Debounced, deduped by snapshot, and
|
|
203
|
+
* a no-op until the session exists.
|
|
204
|
+
*/
|
|
205
|
+
export function watchSessionProgress(store: KycStore): () => void {
|
|
206
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
207
|
+
let lastSaved = '';
|
|
208
|
+
|
|
209
|
+
const unsubscribe = store.subscribe(() => {
|
|
210
|
+
if (timer) clearTimeout(timer);
|
|
211
|
+
timer = setTimeout(() => {
|
|
212
|
+
const s = store.getState();
|
|
213
|
+
if (!s.sessionId) return;
|
|
214
|
+
const payload = progressFromState(s);
|
|
215
|
+
if (isUntouchedProgress(payload, openingStep(s))) return;
|
|
216
|
+
const fingerprint = JSON.stringify(payload);
|
|
217
|
+
if (fingerprint === lastSaved) return;
|
|
218
|
+
lastSaved = fingerprint;
|
|
219
|
+
void s.api.saveProgress(s.sessionId, payload).catch(() => undefined);
|
|
220
|
+
}, SAVE_DEBOUNCE_MS);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return () => {
|
|
224
|
+
unsubscribe();
|
|
225
|
+
if (timer) clearTimeout(timer);
|
|
226
|
+
};
|
|
227
|
+
}
|
package/src/store/state.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { FlashHole } from '../components/flashHoleGeometry';
|
|
|
9
9
|
|
|
10
10
|
import type { StoreApi } from 'zustand/vanilla';
|
|
11
11
|
|
|
12
|
-
import type { KYCApi, KeyPersonInvite } from '../services/api';
|
|
12
|
+
import type { BusinessCompanyRecord, KYCApi, KeyPersonInvite, RegistryOfficer } from '../services/api';
|
|
13
13
|
import type { IdType, KYCStep, ResolvedKYCConfig } from '../types/config';
|
|
14
14
|
import type { PoaDocumentType } from '../types/workflow';
|
|
15
15
|
import type { ApplicantRole } from '../types/business';
|
|
@@ -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;
|
|
@@ -35,7 +38,7 @@ export interface KYCMediaIds {
|
|
|
35
38
|
|
|
36
39
|
export interface KYCSubmissionResult {
|
|
37
40
|
verificationId: string;
|
|
38
|
-
status: '
|
|
41
|
+
status: 'processing';
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export type DocumentScanPhase = 'front' | 'back' | 'complete';
|
|
@@ -48,12 +51,69 @@ export interface BusinessState {
|
|
|
48
51
|
product: string | null;
|
|
49
52
|
registrationNumber: string;
|
|
50
53
|
registrationName: string;
|
|
54
|
+
/** Dev/sandbox only: pins the canned outcome served instead of calling the
|
|
55
|
+
* register. Sent as metadata.sandboxOutcome; production ignores it. */
|
|
56
|
+
sandboxOutcome: string;
|
|
57
|
+
/** ISO 3166-2 registry region, for the four countries whose register is
|
|
58
|
+
* split by state or emirate. Empty for everywhere else. Rides the search
|
|
59
|
+
* and the selection-time check; the submission relies on the prepaid
|
|
60
|
+
* record, mirroring the web SDK. */
|
|
61
|
+
subdivisionCode: string;
|
|
51
62
|
/** Where key-people invite links are emailed. */
|
|
52
63
|
contactEmail: string;
|
|
53
64
|
address: string;
|
|
54
65
|
email: string;
|
|
55
66
|
phone: string;
|
|
56
67
|
website: string;
|
|
68
|
+
/** Registry facts the applicant states; submitted as their own answer. */
|
|
69
|
+
dateOfIncorporation: string;
|
|
70
|
+
taxId: string;
|
|
71
|
+
vatNumber: string;
|
|
72
|
+
companyType: string;
|
|
73
|
+
natureOfBusiness: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The registry check run when the applicant confirms their company.
|
|
78
|
+
*
|
|
79
|
+
* `skipped` and `limit_reached` are normal outcomes, not failures: the
|
|
80
|
+
* organisation could not be charged (or this application has spent its lookup
|
|
81
|
+
* budget), so the flow carries on and the check happens at submission instead.
|
|
82
|
+
* Mirrors the web SDK's BusinessCheckState — keep the two in lockstep.
|
|
83
|
+
*/
|
|
84
|
+
export interface BusinessCheckState {
|
|
85
|
+
status: 'idle' | 'checking' | 'found' | 'not_found' | 'skipped' | 'unavailable' | 'limit_reached';
|
|
86
|
+
/** What the register holds, when it answered. */
|
|
87
|
+
company: BusinessCompanyRecord | null;
|
|
88
|
+
/** The officers on file — what makes the key-people question a confirmation. */
|
|
89
|
+
officers: RegistryOfficer[];
|
|
90
|
+
/** Which company was checked (normalised), so a changed number re-runs it. */
|
|
91
|
+
checkedNumber: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Which form fields the REGISTER filled, as opposed to the applicant.
|
|
94
|
+
*
|
|
95
|
+
* Kept so that changing which company this is can clear exactly those and
|
|
96
|
+
* nothing else. Without it, switching company left the previous register's
|
|
97
|
+
* address and email sitting in the form under the new company's name — and
|
|
98
|
+
* because the prefill only writes into empty fields, those leftovers also
|
|
99
|
+
* blocked the new register's real values from ever landing.
|
|
100
|
+
*/
|
|
101
|
+
prefilled: (keyof BusinessState)[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const EMPTY_BUSINESS_CHECK: BusinessCheckState = {
|
|
105
|
+
status: 'idle',
|
|
106
|
+
company: null,
|
|
107
|
+
officers: [],
|
|
108
|
+
checkedNumber: null,
|
|
109
|
+
prefilled: [],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** What `checkBusiness` resolves with. Only a definitive "not on the register"
|
|
113
|
+
* stops the flow; the company record is handed back for the prefill. */
|
|
114
|
+
export interface BusinessCheckResult {
|
|
115
|
+
canContinue: boolean;
|
|
116
|
+
company: BusinessCompanyRecord | null;
|
|
57
117
|
}
|
|
58
118
|
|
|
59
119
|
/** One uploaded supporting document. */
|
|
@@ -83,6 +143,13 @@ export interface BusinessApplicationState {
|
|
|
83
143
|
* one screening, no duplicate invite.
|
|
84
144
|
*/
|
|
85
145
|
applicantKeyPersonIndex: number | null;
|
|
146
|
+
/**
|
|
147
|
+
* The applicant attests that no natural person qualifies as a UBO (public
|
|
148
|
+
* share structures, complex trusts, nominee arrangements) - the FATF
|
|
149
|
+
* fallback. An attestation the server records and the org can branch on,
|
|
150
|
+
* never a verdict; the registry lookup still says what it says.
|
|
151
|
+
*/
|
|
152
|
+
uboUnidentifiable: boolean;
|
|
86
153
|
}
|
|
87
154
|
|
|
88
155
|
export const EMPTY_BUSINESS_APPLICATION: BusinessApplicationState = {
|
|
@@ -91,6 +158,7 @@ export const EMPTY_BUSINESS_APPLICATION: BusinessApplicationState = {
|
|
|
91
158
|
applicantRole: null,
|
|
92
159
|
applicantName: '',
|
|
93
160
|
applicantKeyPersonIndex: null,
|
|
161
|
+
uboUnidentifiable: false,
|
|
94
162
|
};
|
|
95
163
|
|
|
96
164
|
export const EMPTY_BUSINESS: BusinessState = {
|
|
@@ -98,11 +166,18 @@ export const EMPTY_BUSINESS: BusinessState = {
|
|
|
98
166
|
product: null,
|
|
99
167
|
registrationNumber: '',
|
|
100
168
|
registrationName: '',
|
|
169
|
+
sandboxOutcome: '',
|
|
170
|
+
subdivisionCode: '',
|
|
101
171
|
contactEmail: '',
|
|
102
172
|
address: '',
|
|
103
173
|
email: '',
|
|
104
174
|
phone: '',
|
|
105
175
|
website: '',
|
|
176
|
+
dateOfIncorporation: '',
|
|
177
|
+
taxId: '',
|
|
178
|
+
vatNumber: '',
|
|
179
|
+
companyType: '',
|
|
180
|
+
natureOfBusiness: '',
|
|
106
181
|
};
|
|
107
182
|
|
|
108
183
|
export interface ContactState {
|
|
@@ -112,6 +187,80 @@ export interface ContactState {
|
|
|
112
187
|
emailToken?: string;
|
|
113
188
|
/** Single-use proof from a passed phone check. */
|
|
114
189
|
phoneToken?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Channels whose proof the SERVER refused at submit (422
|
|
192
|
+
* contact_verification_required). Proofs are single-use and expire ~30
|
|
193
|
+
* minutes after the OTP check, but they ride session progress and are
|
|
194
|
+
* restored on resume — so a resumed attempt can carry a dead proof while
|
|
195
|
+
* the step still shows "verified". This routes the person back to
|
|
196
|
+
* re-verify instead of a retry that resubmits the same dead token forever;
|
|
197
|
+
* setContactVerified clears its channel.
|
|
198
|
+
*/
|
|
199
|
+
expired?: Array<'email' | 'phone'>;
|
|
200
|
+
}
|
|
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;
|
|
115
264
|
}
|
|
116
265
|
|
|
117
266
|
/** Document-capture sub-phase — drives the sheet header title/description. */
|
|
@@ -120,18 +269,93 @@ export type DocumentCapturePhase = 'front' | 'front-preview' | 'back' | 'review'
|
|
|
120
269
|
/** The mediaIds keys settable via `setMediaId`. */
|
|
121
270
|
export type MediaIdKey = keyof KYCMediaIds;
|
|
122
271
|
|
|
272
|
+
/** One committed multi-ID check. `documentFront/Back` are mediaIds; the
|
|
273
|
+
* `*Image` fields are LOCAL previews and never reach the wire. */
|
|
274
|
+
export interface MultiIdSlot {
|
|
275
|
+
idType: IdType;
|
|
276
|
+
idNumber?: string;
|
|
277
|
+
documentFront?: string;
|
|
278
|
+
documentBack?: string;
|
|
279
|
+
/** Each check records its OWN document capture. The row's flat
|
|
280
|
+
* documentFrontVideo column holds one, so a multi-ID run keeps them per
|
|
281
|
+
* check or loses all but one. */
|
|
282
|
+
documentFrontVideo?: string;
|
|
283
|
+
documentBackVideo?: string;
|
|
284
|
+
/** This check's own chip read. The chip belongs to a PARTICULAR document, so
|
|
285
|
+
* sending it top-level attributed it to the primary check — which is how a
|
|
286
|
+
* passport's chip read was dropped for being submitted alongside a BVN. */
|
|
287
|
+
chipData?: EmrtdReadResult | null;
|
|
288
|
+
documentFrontImage?: string;
|
|
289
|
+
documentBackImage?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
123
292
|
export interface KycState {
|
|
124
293
|
config: ResolvedKYCConfig;
|
|
125
294
|
api: KYCApi;
|
|
126
295
|
|
|
127
296
|
currentStep: KYCStep;
|
|
297
|
+
/**
|
|
298
|
+
* The attempt SESSION this run is recorded under (`/session/start`). Null
|
|
299
|
+
* when minting failed or preview — verifying is never conditional on it. It
|
|
300
|
+
* rides the /verify body so the verification adopts the session's id, and it
|
|
301
|
+
* is what the registry check at selection anchors its charge on.
|
|
302
|
+
*/
|
|
303
|
+
sessionId: string | null;
|
|
304
|
+
/** The session's own hosted web page (see SessionStartResponse.url). */
|
|
305
|
+
sessionUrl: string | null;
|
|
306
|
+
/**
|
|
307
|
+
* What the register said about the company the applicant identified — the
|
|
308
|
+
* paid check run at SELECTION (`/business/select`), so the officer list is
|
|
309
|
+
* already here by the time the key-people step asks for it. `officers` is
|
|
310
|
+
* the prefill's input; `checkedNumber` stops a re-check of the same company.
|
|
311
|
+
*/
|
|
312
|
+
businessCheck: BusinessCheckState;
|
|
128
313
|
/**
|
|
129
314
|
* The country picked on the multi-region country-select step. Null on a
|
|
130
315
|
* single-country flow, where `config.country` is the answer.
|
|
131
316
|
*/
|
|
132
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;
|
|
133
325
|
selectedIdType: IdType | null;
|
|
134
326
|
idNumber: string | null;
|
|
327
|
+
/**
|
|
328
|
+
* Multi-ID: which check the applicant is on (0-based), and the ones already
|
|
329
|
+
* committed. A committed slot keeps its LOCAL preview images as well as its
|
|
330
|
+
* mediaIds, so stepping back into it restores what was captured rather than
|
|
331
|
+
* asking for a document that is still perfectly good.
|
|
332
|
+
*/
|
|
333
|
+
multiIdSlotIndex: number;
|
|
334
|
+
multiIdSlots: MultiIdSlot[];
|
|
335
|
+
/**
|
|
336
|
+
* Capture previews handed back when stepping BACK into a committed slot.
|
|
337
|
+
*
|
|
338
|
+
* RN keeps document previews in the capture screen's own state, so a restored
|
|
339
|
+
* slot has to hand them somewhere the remounting screen can read them. Null
|
|
340
|
+
* on the forward journey.
|
|
341
|
+
*/
|
|
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;
|
|
135
359
|
mediaIds: KYCMediaIds;
|
|
136
360
|
submissionResult: KYCSubmissionResult | null;
|
|
137
361
|
serverConfig: ServerConfigState;
|
|
@@ -203,6 +427,26 @@ export interface KycState {
|
|
|
203
427
|
poaDocumentType: PoaDocumentType | null;
|
|
204
428
|
/** Its file name, so the uploaded state can name what it has. */
|
|
205
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;
|
|
206
450
|
/**
|
|
207
451
|
* Contact-verification results. The proofs are single-use and ride the
|
|
208
452
|
* /verify submission; the addresses are kept so returning to the step (or a
|
|
@@ -217,15 +461,43 @@ export interface KycState {
|
|
|
217
461
|
// Actions
|
|
218
462
|
loadServerConfig: () => Promise<void>;
|
|
219
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;
|
|
220
467
|
setIdType: (idType: IdType) => void;
|
|
221
468
|
setIdNumber: (idNumber: string) => void;
|
|
469
|
+
/** Commit the current slot's evidence and move to the next check. The
|
|
470
|
+
* previews come from the capture screen, which owns them. */
|
|
471
|
+
commitMultiIdSlot: (nextStep: KYCStep, previews?: { front?: string; back?: string }) => void;
|
|
472
|
+
/** Step BACK into the previous slot, restoring what it captured. */
|
|
473
|
+
uncommitMultiIdSlot: (step: KYCStep) => void;
|
|
222
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;
|
|
223
481
|
setDocumentMediaId: (mediaId: string, side: 'front' | 'back') => void;
|
|
224
482
|
setQuestionnaireAnswer: (key: string, value: QuestionnaireAnswerValue | undefined) => void;
|
|
225
483
|
setContactVerified: (channel: 'email' | 'phone', destination: string, token: string) => void;
|
|
226
484
|
setContactDestination: (channel: 'email' | 'phone', destination: string) => void;
|
|
485
|
+
/** The server refused these channels' proofs at submit — drop the tokens and
|
|
486
|
+
* flag the channels so their steps re-verify then resubmit. */
|
|
487
|
+
clearContactProofs: (channels: Array<'email' | 'phone'>) => void;
|
|
227
488
|
setBusinessField: <K extends keyof BusinessState>(key: K, value: BusinessState[K]) => void;
|
|
489
|
+
/** Writes the register's answers into empty fields + records which ones it
|
|
490
|
+
* filled, in one set — so a company change can clear exactly those. */
|
|
491
|
+
applyBusinessPrefill: (
|
|
492
|
+
patch: Partial<BusinessState>,
|
|
493
|
+
prefilled: (keyof BusinessState)[],
|
|
494
|
+
) => void;
|
|
495
|
+
setSessionId: (sessionId: string, sessionUrl?: string | null) => void;
|
|
496
|
+
/** Run the paid registry check for the typed company. Never blocks the flow. */
|
|
497
|
+
checkBusiness: () => Promise<BusinessCheckResult>;
|
|
228
498
|
setKeyPeople: (rows: KeyPersonEntry[]) => void;
|
|
499
|
+
/** The UBO-exemption attestation (FATF fallback) — see BusinessApplicationState. */
|
|
500
|
+
setUboUnidentifiable: (checked: boolean) => void;
|
|
229
501
|
setBusinessDocument: (doc: BusinessDocumentUpload) => void;
|
|
230
502
|
removeBusinessDocument: (type: string) => void;
|
|
231
503
|
setApplicant: (role: ApplicantRole, name: string, keyPersonIndex?: number | null) => void;
|
|
@@ -234,6 +506,21 @@ export interface KycState {
|
|
|
234
506
|
setChipData: (data: EmrtdReadResult) => void;
|
|
235
507
|
setProofOfAddress: (mediaId: string, docType: PoaDocumentType, fileName: string) => void;
|
|
236
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;
|
|
237
524
|
setDocumentCapturePhase: (phase: DocumentCapturePhase) => void;
|
|
238
525
|
setContactChallenge: (challenge: ContactChallenge | null) => void;
|
|
239
526
|
setImmersiveCapture: (immersive: boolean) => void;
|