@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -7
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +146 -10
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +30 -9
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- 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/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +10 -192
- package/src/components/KycFlow.tsx +69 -34
- 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/MyazaButton.tsx +9 -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/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +48 -5
- 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/documentCaptureMethods.ts +31 -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 +13 -5
- package/src/index.ts +49 -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-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/documentCaptureCheck.ts +136 -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/model-ready.ts +82 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- 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-sharpness.ts +211 -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/avatarSource.ts +59 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -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/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/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +33 -16
- package/src/screens/LivenessStep.tsx +188 -49
- package/src/screens/MrzScanView.tsx +33 -0
- 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/consent/model.ts +79 -10
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- 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 +8 -4
- package/src/screens/liveness/SelfiePreview.tsx +31 -7
- 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-types.ts +119 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +149 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/mediaCompress.ts +23 -5
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +120 -5
- 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 +63 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
package/src/store/derive.ts
CHANGED
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
// keeps a single answer to each of those questions.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
|
+
import { configScope } from '../lib/scope';
|
|
10
11
|
import { requiresDocumentCapture, supportsNfcChip } from '../config/idTypes';
|
|
11
12
|
import {
|
|
13
|
+
buildStepOrder,
|
|
12
14
|
getStepProgress,
|
|
13
15
|
nextStepInOrder,
|
|
14
16
|
previousStepInOrder,
|
|
15
17
|
type StepOrderOptions,
|
|
16
18
|
} from '../config/stepOrder';
|
|
17
19
|
import { hasProofOfAddressStep } from '../config/proofOfAddress';
|
|
20
|
+
import { addressFlowFor, hasAddressCollectionStep } from '../config/addressCollection';
|
|
21
|
+
import { webViewAvailable } from '../lib/webview-available';
|
|
22
|
+
import { addressVendorsStubbed } from '../lib/address-flow';
|
|
18
23
|
import { businessProductsForCountry, isBusinessFlow } from '../config/business';
|
|
19
24
|
import {
|
|
20
25
|
hasApplicantVerification,
|
|
@@ -22,6 +27,7 @@ import {
|
|
|
22
27
|
hasKeyPeopleCollection,
|
|
23
28
|
} from '../config/businessSteps';
|
|
24
29
|
import { hasActiveQuestionnaire } from '../config/questionnaire';
|
|
30
|
+
import { hasConsentStep, openingStepOf } from '../config/consentStep';
|
|
25
31
|
import { applicantSelfCountry, keyPeoplePayload } from '../config/keyPeople';
|
|
26
32
|
import { featuresFor } from './serverConfig';
|
|
27
33
|
import type { VerifyRequest } from '../services/api';
|
|
@@ -58,6 +64,7 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
|
|
|
58
64
|
const idType = state.selectedIdType;
|
|
59
65
|
return {
|
|
60
66
|
isBusiness: isBusinessFlow(config),
|
|
67
|
+
scope: configScope(config),
|
|
61
68
|
business: config.business,
|
|
62
69
|
hasDocCapture: idType ? requiresDocumentCapture(idType) : true,
|
|
63
70
|
hasNfc: nfcEnabled(state),
|
|
@@ -72,9 +79,20 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
|
|
|
72
79
|
hasCountrySelect:
|
|
73
80
|
countrySelectOptions(state).length > 1 &&
|
|
74
81
|
applicantSelfCountry(state.businessApplication) === null,
|
|
82
|
+
hasConsent: hasConsentStep(config),
|
|
75
83
|
hasEmailVerification: config.emailVerification?.enabled === true,
|
|
76
84
|
hasPhoneVerification: config.phoneVerification?.enabled === true,
|
|
77
85
|
hasPoa: hasProofOfAddressStep(config.proofOfAddress),
|
|
86
|
+
hasAddressCollection: hasAddressCollectionStep(config.addressCollection),
|
|
87
|
+
// An absent search flag means no search SCREEN, never an error: the
|
|
88
|
+
// applicant places the pin by hand, the fallback every address failure
|
|
89
|
+
// degrades to.
|
|
90
|
+
addressFlow: addressFlowFor(
|
|
91
|
+
config,
|
|
92
|
+
state.serverConfig.addressSearch === true,
|
|
93
|
+
addressVendorsStubbed({ environment: state.serverConfig.environment }),
|
|
94
|
+
Boolean(state.serverConfig.mapsFrameUrl) && webViewAvailable(),
|
|
95
|
+
),
|
|
78
96
|
hasQuestionnaire: hasActiveQuestionnaire(config.questionnaire),
|
|
79
97
|
// Present only on a session a reviewer sent back. Narrows the order to the
|
|
80
98
|
// steps they ticked, so somebody fixing one blurry photo is not walked
|
|
@@ -237,11 +255,18 @@ export function previousStepBefore(step: KYCStep, state: KycState): KYCStep {
|
|
|
237
255
|
return previousStepInOrder(step, stepOrderOptions(state));
|
|
238
256
|
}
|
|
239
257
|
|
|
258
|
+
/**
|
|
259
|
+
* The step this flow OPENS on: 'consent', or the first real step when the
|
|
260
|
+
* workflow switched the consent screen off (`consentStep: false`). The store
|
|
261
|
+
* starts and resets here, the progress watcher judges "untouched" against it,
|
|
262
|
+
* and the flow hides Back on it. Never depends on anything the applicant has
|
|
263
|
+
* yet to choose: only later steps do.
|
|
264
|
+
*/
|
|
265
|
+
export function openingStep(state: KycState): KYCStep {
|
|
266
|
+
return openingStepOf(buildStepOrder(stepOrderOptions(state)));
|
|
267
|
+
}
|
|
268
|
+
|
|
240
269
|
/** Percentage complete, for the sheet's progress indicator. */
|
|
241
270
|
export function stepProgress(step: KYCStep, state: KycState): number {
|
|
242
271
|
return getStepProgress(step, stepOrderOptions(state));
|
|
243
272
|
}
|
|
244
|
-
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
// Store factory
|
|
247
|
-
// ---------------------------------------------------------------------------
|
package/src/store/kycStore.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type KycState,
|
|
28
28
|
type KycStore,
|
|
29
29
|
} from './state';
|
|
30
|
-
import { nextStepAfter, nfcDecision, previousStepBefore } from './derive';
|
|
30
|
+
import { nextStepAfter, nfcDecision, openingStep, previousStepBefore } from './derive';
|
|
31
31
|
|
|
32
32
|
/** The steps that make up ONE ID's evidence. Leaving this set is what ends a
|
|
33
33
|
* multi-ID check — the leg has several exits depending on the ID. */
|
|
@@ -54,12 +54,15 @@ function multiIdPlanFor(s: KycState): ReturnType<typeof multiIdPlan> {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
import { recordStep, resetStepLog } from '../lib/step-log';
|
|
57
|
+
import { resetAutoLocate, resetCurrentFix } from '../lib/address-current-location';
|
|
57
58
|
import { multiIdPlan } from '../lib/multi-id';
|
|
58
59
|
import { resolveIdTypeDefinition } from '../config/idTypes';
|
|
59
60
|
import { buildVerifyRequest } from './submit';
|
|
61
|
+
import { IDLE_SELFIE_UPLOAD } from '../lib/selfie-upload-wait';
|
|
60
62
|
import { resetBusinessCheck, runBusinessCheck } from './businessCheck';
|
|
61
63
|
import { startAttemptSession, watchSessionProgress } from './session';
|
|
62
64
|
import { applicantMediaCaptured, buildApplicantVerifyRequest } from './submitApplicant';
|
|
65
|
+
import { keptIdType } from '../lib/resubmit';
|
|
63
66
|
|
|
64
67
|
export * from './state';
|
|
65
68
|
export {
|
|
@@ -99,11 +102,17 @@ export function createKycStore(
|
|
|
99
102
|
sessionUrl: null,
|
|
100
103
|
businessCheck: { ...EMPTY_BUSINESS_CHECK },
|
|
101
104
|
selectedCountry: null,
|
|
102
|
-
|
|
105
|
+
countryAutoPicked: false,
|
|
106
|
+
// A send-back that did not ask for the ID keeps the original one: the
|
|
107
|
+
// ID steps are narrowed out, so the kept ID is selected here instead.
|
|
108
|
+
// A restored session only overwrites it with a saved pick.
|
|
109
|
+
selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
|
|
103
110
|
idNumber: null,
|
|
104
111
|
multiIdSlotIndex: 0,
|
|
105
112
|
multiIdSlots: [],
|
|
106
113
|
multiIdRestored: null,
|
|
114
|
+
selfiePreviewUri: null,
|
|
115
|
+
selfieUpload: IDLE_SELFIE_UPLOAD,
|
|
107
116
|
mediaIds: {},
|
|
108
117
|
submissionResult: null,
|
|
109
118
|
serverConfig: serverConfig ?? INITIAL_SERVER_CONFIG,
|
|
@@ -124,6 +133,11 @@ export function createKycStore(
|
|
|
124
133
|
chipData: null,
|
|
125
134
|
poaDocumentType: null,
|
|
126
135
|
poaFileName: null,
|
|
136
|
+
address: null,
|
|
137
|
+
addressPhotoPreview: null,
|
|
138
|
+
addressIntroSeen: false,
|
|
139
|
+
addressEntranceFraming: false,
|
|
140
|
+
addressSandboxOutcome: null,
|
|
127
141
|
isLoading: false,
|
|
128
142
|
error: null,
|
|
129
143
|
|
|
@@ -137,16 +151,27 @@ export function createKycStore(
|
|
|
137
151
|
const branding = res.branding
|
|
138
152
|
? { ...res.branding, logo: normalizeDevAssetUrl(res.branding.logo, baseUrl) }
|
|
139
153
|
: res.branding;
|
|
154
|
+
// The facts that just landed can add a step AHEAD of the one the flow
|
|
155
|
+
// opened on (the address search step, on a consent-less address
|
|
156
|
+
// flow). Someone still standing on the placeholder's opening step,
|
|
157
|
+
// having done nothing, is moved to the real one; anyone who has
|
|
158
|
+
// moved is left alone. Mirrors Flutter's _loadServerConfig.
|
|
159
|
+
const before = openingStep(get());
|
|
140
160
|
set({
|
|
141
161
|
serverConfig: {
|
|
142
162
|
status: 'ready',
|
|
143
163
|
idTypes: res.idTypes,
|
|
144
164
|
branding,
|
|
145
165
|
geoCountry: res.geoCountry,
|
|
166
|
+
addressSearch: res.addressSearch,
|
|
167
|
+
addressSearchMode: res.addressSearchMode,
|
|
168
|
+
mapsFrameUrl: res.mapsFrameUrl ?? null,
|
|
146
169
|
environment: res.environment,
|
|
147
170
|
fatal: false,
|
|
148
171
|
},
|
|
149
172
|
});
|
|
173
|
+
const after = openingStep(get());
|
|
174
|
+
if (get().currentStep === before && after !== before) set({ currentStep: after });
|
|
150
175
|
} catch (err) {
|
|
151
176
|
const described = describeConfigError(err);
|
|
152
177
|
set((s) => ({
|
|
@@ -163,6 +188,7 @@ export function createKycStore(
|
|
|
163
188
|
const same = s.selectedCountry === country;
|
|
164
189
|
return {
|
|
165
190
|
selectedCountry: country,
|
|
191
|
+
countryAutoPicked: false,
|
|
166
192
|
selectedIdType: same ? s.selectedIdType : null,
|
|
167
193
|
idNumber: same ? s.idNumber : null,
|
|
168
194
|
// A multi-ID run's committed slots belong to the country they were
|
|
@@ -175,6 +201,11 @@ export function createKycStore(
|
|
|
175
201
|
});
|
|
176
202
|
},
|
|
177
203
|
|
|
204
|
+
setCountryAuto(country) {
|
|
205
|
+
get().setCountry(country);
|
|
206
|
+
if (!get().countryAutoPicked) set({ countryAutoPicked: true });
|
|
207
|
+
},
|
|
208
|
+
|
|
178
209
|
commitMultiIdSlot(nextStep, previews) {
|
|
179
210
|
set((s) => {
|
|
180
211
|
if (!s.selectedIdType) return {};
|
|
@@ -257,6 +288,22 @@ export function createKycStore(
|
|
|
257
288
|
set((s) => ({ mediaIds: { ...s.mediaIds, [key]: mediaId } }));
|
|
258
289
|
},
|
|
259
290
|
|
|
291
|
+
setSelfiePreview(uri) {
|
|
292
|
+
set({ selfiePreviewUri: uri });
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
clearSelfie() {
|
|
296
|
+
set((s) => ({
|
|
297
|
+
selfiePreviewUri: null,
|
|
298
|
+
selfieUpload: IDLE_SELFIE_UPLOAD,
|
|
299
|
+
mediaIds: { ...s.mediaIds, selfie: undefined, livenessVideo: undefined },
|
|
300
|
+
}));
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
setSelfieUpload(upload) {
|
|
304
|
+
set({ selfieUpload: upload });
|
|
305
|
+
},
|
|
306
|
+
|
|
260
307
|
setDocumentMediaId(mediaId, side) {
|
|
261
308
|
set((s) => ({
|
|
262
309
|
mediaIds: {
|
|
@@ -423,6 +470,41 @@ export function createKycStore(
|
|
|
423
470
|
});
|
|
424
471
|
},
|
|
425
472
|
|
|
473
|
+
setAddress(address) {
|
|
474
|
+
set({ address });
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
setAddressSandboxOutcome(addressSandboxOutcome) {
|
|
478
|
+
set({ addressSandboxOutcome });
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
setAddressPhoto(mediaId) {
|
|
482
|
+
set((s) => {
|
|
483
|
+
if (mediaId) return { mediaIds: { ...s.mediaIds, addressPhoto: mediaId } };
|
|
484
|
+
const { addressPhoto: _dropped, ...rest } = s.mediaIds;
|
|
485
|
+
return { mediaIds: rest };
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
setAddressPhotoPreview(uri) {
|
|
490
|
+
set({ addressPhotoPreview: uri });
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
markAddressIntroSeen() {
|
|
494
|
+
set({ addressIntroSeen: true });
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
setAddressEntranceFraming(framing) {
|
|
498
|
+
if (get().addressEntranceFraming !== framing) set({ addressEntranceFraming: framing });
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
clearAddress() {
|
|
502
|
+
set((s) => {
|
|
503
|
+
const { addressPhoto: _dropped, ...rest } = s.mediaIds;
|
|
504
|
+
return { address: null, mediaIds: rest, addressPhotoPreview: null };
|
|
505
|
+
});
|
|
506
|
+
},
|
|
507
|
+
|
|
426
508
|
setDocumentCapturePhase(phase) {
|
|
427
509
|
if (get().documentCapturePhase !== phase) set({ documentCapturePhase: phase });
|
|
428
510
|
},
|
|
@@ -569,15 +651,29 @@ export function createKycStore(
|
|
|
569
651
|
// already sitting on 'consent' (the subscribe below only fires on
|
|
570
652
|
// change, and recordStep dedupes if it fires too).
|
|
571
653
|
resetStepLog();
|
|
572
|
-
|
|
654
|
+
// The cached GPS fix and the once-per-verification auto-locate are
|
|
655
|
+
// scoped to ONE verification: an app session spans many opens of the
|
|
656
|
+
// SDK, and a fix taken an hour ago would drop the pin at wherever the
|
|
657
|
+
// phone was then.
|
|
658
|
+
resetCurrentFix();
|
|
659
|
+
resetAutoLocate();
|
|
660
|
+
// The opening step never depends on the fields reset below (only later
|
|
661
|
+
// steps do), so it is safe to read before the write.
|
|
662
|
+
const opening = openingStep(get());
|
|
663
|
+
recordStep(opening, multiIdSlotOf(get()), selectedIdTypeOf(get()));
|
|
573
664
|
set({
|
|
574
|
-
currentStep:
|
|
665
|
+
currentStep: opening,
|
|
575
666
|
selectedCountry: null,
|
|
576
|
-
|
|
667
|
+
countryAutoPicked: false,
|
|
668
|
+
// Same kept ID as at creation: the narrowed flow has no picker to
|
|
669
|
+
// choose it again.
|
|
670
|
+
selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
|
|
577
671
|
idNumber: null,
|
|
578
672
|
multiIdSlotIndex: 0,
|
|
579
673
|
multiIdSlots: [],
|
|
580
674
|
multiIdRestored: null,
|
|
675
|
+
selfiePreviewUri: null,
|
|
676
|
+
selfieUpload: IDLE_SELFIE_UPLOAD,
|
|
581
677
|
mediaIds: {},
|
|
582
678
|
submissionResult: null,
|
|
583
679
|
documentScanPhase: 'front',
|
|
@@ -599,6 +695,11 @@ export function createKycStore(
|
|
|
599
695
|
chipData: null,
|
|
600
696
|
poaDocumentType: null,
|
|
601
697
|
poaFileName: null,
|
|
698
|
+
address: null,
|
|
699
|
+
addressPhotoPreview: null,
|
|
700
|
+
addressIntroSeen: false,
|
|
701
|
+
addressEntranceFraming: false,
|
|
702
|
+
addressSandboxOutcome: null,
|
|
602
703
|
isLoading: false,
|
|
603
704
|
error: null,
|
|
604
705
|
});
|
|
@@ -622,6 +723,20 @@ export function createKycStore(
|
|
|
622
723
|
// subscription above only fires on CHANGE, and a new store already sits on
|
|
623
724
|
// 'consent').
|
|
624
725
|
resetStepLog();
|
|
726
|
+
// The module-level GPS cache and the auto-locate latch are scoped to ONE
|
|
727
|
+
// verification, and this — not reset() — is where a verification begins.
|
|
728
|
+
// Clearing them only in reset() left both alive across modal launches, so a
|
|
729
|
+
// second verification in the same app session dropped its pin wherever the
|
|
730
|
+
// phone had been on the first (possibly hours and a city away) and skipped
|
|
731
|
+
// the automatic locate entirely, since the latch was already spent.
|
|
732
|
+
resetCurrentFix();
|
|
733
|
+
resetAutoLocate();
|
|
734
|
+
// A workflow that switched the consent screen off opens on its first real
|
|
735
|
+
// step; the literal 'consent' above is only the shape before the config is
|
|
736
|
+
// consulted. Set BEFORE the journey is recorded, so the log opens where the
|
|
737
|
+
// person does.
|
|
738
|
+
const opening = openingStep(store.getState());
|
|
739
|
+
if (opening !== store.getState().currentStep) store.setState({ currentStep: opening });
|
|
625
740
|
recordStep(
|
|
626
741
|
store.getState().currentStep,
|
|
627
742
|
multiIdSlotOf(store.getState()),
|
|
@@ -21,6 +21,24 @@ export interface ServerConfigState {
|
|
|
21
21
|
branding?: SdkConfigBranding;
|
|
22
22
|
/** The visitor's country from their IP — a default, never evidence. */
|
|
23
23
|
geoCountry?: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the platform has a forward-search backend, and which one. Absent
|
|
26
|
+
* means the address flow simply has no search screen: the applicant places
|
|
27
|
+
* the pin by hand, which is the fallback every address failure degrades to.
|
|
28
|
+
*
|
|
29
|
+
* The Google KEY itself never rides here (it is the hosted page's alone);
|
|
30
|
+
* the framed picker reaches this SDK as a URL instead — see mapsFrameUrl.
|
|
31
|
+
* Street View entrance framing still does not exist on mobile.
|
|
32
|
+
*/
|
|
33
|
+
addressSearch?: boolean;
|
|
34
|
+
addressSearchMode?: 'autocomplete' | 'basic';
|
|
35
|
+
/**
|
|
36
|
+
* The framed Google-map picker page for a WebView (`react-native-webview`,
|
|
37
|
+
* an optional peer): our hosted /embed/map plus a signed APP grant. Null or
|
|
38
|
+
* absent ⇒ the built-in OSM picker, which is also the fallback when the
|
|
39
|
+
* page never reports ready.
|
|
40
|
+
*/
|
|
41
|
+
mapsFrameUrl?: string | null;
|
|
24
42
|
environment?: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
|
|
25
43
|
/** HTTP status of a failed config fetch (if any). */
|
|
26
44
|
statusCode?: number;
|
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>),
|