@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useKyc, useKycConfig, useKycStore } from '../../components/runtime';
|
|
4
|
+
import { addressFlowFor } from '../../config/addressCollection';
|
|
5
|
+
import { webViewAvailable } from '../../lib/webview-available';
|
|
6
|
+
import { isBusinessFlow } from '../../config/business';
|
|
7
|
+
import { addressFlowSteps, addressVendorsStubbed, isAddressStep } from '../../lib/address-flow';
|
|
8
|
+
import { addressExitStep, recoverAddressStep } from '../../lib/address-step-recovery';
|
|
9
|
+
import { defaultMapView } from '../../lib/map-tiles';
|
|
10
|
+
import { buildStepOrder, nextStepInOrder } from '../../config/stepOrder';
|
|
11
|
+
import { geoDefaultCountry } from '../../lib/country-adoption';
|
|
12
|
+
import { configScope } from '../../lib/scope';
|
|
13
|
+
import { stepOrderOptions } from '../../store/kycStore';
|
|
14
|
+
import { deviceFixFields } from '../../services/location';
|
|
15
|
+
import { savePresencePin } from '../../presence/store';
|
|
16
|
+
import { usePinActions } from './use-pin-actions';
|
|
17
|
+
import type { KYCStep } from '../../types/config';
|
|
18
|
+
import type { AddressState } from '../../store/state';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// The address flow's shared brain: every address step mounts this hook and gets
|
|
22
|
+
// the same derived flags, the same step list, and the same actions — which is
|
|
23
|
+
// what keeps four separate screens telling one story.
|
|
24
|
+
//
|
|
25
|
+
// Navigation rides the SDK's ONE ordered step list rather than a second copy:
|
|
26
|
+
// the flow's steps are already in `buildStepOrder`, so stepping forward, going
|
|
27
|
+
// back, and backing out of the flow entirely all fall out of the same list the
|
|
28
|
+
// progress bar reads. That is why nothing here reimplements goBack — the
|
|
29
|
+
// sheet's own back button already walks it — and why goNext is the store's
|
|
30
|
+
// nextStep. `nextAddressStep`/`prevAddressStep` stay in lib/address-flow as
|
|
31
|
+
// part of the pure mirror the three SDKs share.
|
|
32
|
+
//
|
|
33
|
+
// A MIRROR of the web SDK's steps/address/use-address-flow.ts.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export function useAddressFlow() {
|
|
37
|
+
const config = useKycConfig();
|
|
38
|
+
const store = useKycStore();
|
|
39
|
+
const address = useKyc((s) => s.address);
|
|
40
|
+
const serverSearch = useKyc((s) => s.serverConfig.addressSearch === true);
|
|
41
|
+
const selectedCountry = useKyc((s) => s.selectedCountry);
|
|
42
|
+
const businessCountry = useKyc((s) => s.business.country);
|
|
43
|
+
const [uploading, setUploading] = useState(false);
|
|
44
|
+
const [confirming, setConfirming] = useState(false);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
const cfg = config.addressCollection;
|
|
48
|
+
const isBusiness = isBusinessFlow(config);
|
|
49
|
+
const vendorEnvironment = useKyc((s) => s.serverConfig.environment ?? null);
|
|
50
|
+
const mapsFrameUrl = useKyc((s) => s.serverConfig.mapsFrameUrl ?? null);
|
|
51
|
+
const flow = addressFlowFor(
|
|
52
|
+
config,
|
|
53
|
+
serverSearch,
|
|
54
|
+
addressVendorsStubbed({ environment: vendorEnvironment }),
|
|
55
|
+
Boolean(mapsFrameUrl) && webViewAvailable(),
|
|
56
|
+
);
|
|
57
|
+
// KYB: the premises pin + directions ARE the capture, so the flow is that
|
|
58
|
+
// single step and its Continue commits.
|
|
59
|
+
//
|
|
60
|
+
// Memoised on the three flags it derives from, so `steps` — and the two
|
|
61
|
+
// navigation callbacks that close over it — keep a stable identity. A fresh
|
|
62
|
+
// array on every render would make `goNext` a new function each time, and
|
|
63
|
+
// the entrance step depends on that identity inside an effect.
|
|
64
|
+
const { searchAvailable, photoMode, streetViewOffered } = flow;
|
|
65
|
+
const steps: KYCStep[] = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
isBusiness
|
|
68
|
+
? ['address-collection']
|
|
69
|
+
: addressFlowSteps({ searchAvailable, photoMode, streetViewOffered }),
|
|
70
|
+
[isBusiness, searchAvailable, photoMode, streetViewOffered],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// A resumed attempt restores the step it was saved on, and the flow may have
|
|
74
|
+
// changed shape since: `searchAvailable` is a SERVER flag that lands after the
|
|
75
|
+
// restore, and a republish can drop the entrance photo. Left alone the
|
|
76
|
+
// applicant sits on a screen the order does not contain, where Continue has
|
|
77
|
+
// nothing to advance to and Back has no predecessor. Gated on the config
|
|
78
|
+
// having settled: until then the order is not the real one.
|
|
79
|
+
const configSettled = useKyc((s) => s.serverConfig.status !== 'loading');
|
|
80
|
+
const currentStep = useKyc((s) => s.currentStep);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!configSettled || !isAddressStep(currentStep)) return;
|
|
83
|
+
const options = stepOrderOptions(store.getState());
|
|
84
|
+
const inFlow = buildStepOrder(options).filter(isAddressStep);
|
|
85
|
+
const exit = addressExitStep(buildStepOrder({ ...options, hasAddressCollection: true }));
|
|
86
|
+
const recovered = recoverAddressStep(currentStep, inFlow, exit);
|
|
87
|
+
if (recovered) store.getState().goToStep(recovered);
|
|
88
|
+
}, [configSettled, currentStep, store]);
|
|
89
|
+
|
|
90
|
+
// The address scope declares the visitor's IP country by default, ONCE,
|
|
91
|
+
// while nothing is declared; a geocode or a picked address then corrects
|
|
92
|
+
// the guess (lib/country-adoption.ts). A full flow never defaults from it.
|
|
93
|
+
const geoCountry = useKyc((s) => s.serverConfig.geoCountry ?? null);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const s = store.getState();
|
|
96
|
+
const country = geoDefaultCountry({
|
|
97
|
+
geoCountry,
|
|
98
|
+
selectedCountry: s.selectedCountry,
|
|
99
|
+
scope: configScope(s.config),
|
|
100
|
+
accepted: s.config.proofOfAddress?.countries,
|
|
101
|
+
});
|
|
102
|
+
if (country) s.setCountryAuto(country);
|
|
103
|
+
}, [geoCountry, store]);
|
|
104
|
+
|
|
105
|
+
const pin = address ? { lat: address.lat, lng: address.lng } : null;
|
|
106
|
+
const country = isBusiness ? (businessCountry ?? undefined) : (selectedCountry ?? config.country);
|
|
107
|
+
const view = defaultMapView(country);
|
|
108
|
+
|
|
109
|
+
const pinActions = usePinActions(store, setError);
|
|
110
|
+
|
|
111
|
+
/** Leave the address flow forwards: after the review, a KYB commit, a skip. */
|
|
112
|
+
const exitForward = useCallback(() => {
|
|
113
|
+
const state = store.getState();
|
|
114
|
+
const last = steps[steps.length - 1]!;
|
|
115
|
+
store.getState().goToStep(nextStepInOrder(last, stepOrderOptions(state)));
|
|
116
|
+
}, [steps, store]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The step after the current one within the flow, else the flow's exit:
|
|
120
|
+
* exactly what the store's own `nextStep` computes, since the flow's steps
|
|
121
|
+
* are contiguous in the ordered list. Going through the store also keeps
|
|
122
|
+
* the navigation direction and the camera flag handled in one place.
|
|
123
|
+
*/
|
|
124
|
+
const goNext = useCallback(() => store.getState().nextStep(), [store]);
|
|
125
|
+
|
|
126
|
+
const pickPhoto = useCallback(
|
|
127
|
+
async (upload: () => Promise<string>, previewUri: string) => {
|
|
128
|
+
setError(null);
|
|
129
|
+
setUploading(true);
|
|
130
|
+
try {
|
|
131
|
+
const mediaId = await upload();
|
|
132
|
+
store.getState().setAddressPhoto(mediaId);
|
|
133
|
+
store.getState().setAddressPhotoPreview(previewUri);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
setError(
|
|
136
|
+
err instanceof Error && err.message
|
|
137
|
+
? err.message
|
|
138
|
+
: 'Upload failed. Please check your connection and try again.',
|
|
139
|
+
);
|
|
140
|
+
} finally {
|
|
141
|
+
setUploading(false);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[store],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const removePhoto = useCallback(() => {
|
|
148
|
+
store.getState().setAddressPhoto(null);
|
|
149
|
+
store.getState().setAddressPhotoPreview(null);
|
|
150
|
+
}, [store]);
|
|
151
|
+
|
|
152
|
+
/** Commit: the one-shot attest fix (best-effort), then leave the flow. */
|
|
153
|
+
const confirm = useCallback(async () => {
|
|
154
|
+
const current = store.getState().address;
|
|
155
|
+
if (!current) return;
|
|
156
|
+
setConfirming(true);
|
|
157
|
+
let committed: AddressState = current;
|
|
158
|
+
if (cfg?.attestPresence) {
|
|
159
|
+
// Best-effort by contract: a denied prompt or a slow read costs the
|
|
160
|
+
// `attested` tier, never the flow.
|
|
161
|
+
committed = { ...current, ...(await deviceFixFields()) };
|
|
162
|
+
store.getState().setAddress(committed);
|
|
163
|
+
}
|
|
164
|
+
// Presence verification keeps the confirmed pin ON-DEVICE so later
|
|
165
|
+
// foreground reports evaluate the fence locally. Keyed by the org's user
|
|
166
|
+
// reference: without one there is nothing to report against.
|
|
167
|
+
if (cfg?.presence?.enabled === true && config.userId) {
|
|
168
|
+
savePresencePin(
|
|
169
|
+
config.userId,
|
|
170
|
+
{ lat: committed.lat, lng: committed.lng },
|
|
171
|
+
{ alwaysOn: cfg.presence.alwaysOn === true },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
setConfirming(false);
|
|
175
|
+
exitForward();
|
|
176
|
+
}, [cfg?.attestPresence, cfg?.presence?.enabled, config.userId, exitForward, store]);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
cfg,
|
|
180
|
+
address,
|
|
181
|
+
isBusiness,
|
|
182
|
+
steps,
|
|
183
|
+
pin,
|
|
184
|
+
view,
|
|
185
|
+
country,
|
|
186
|
+
photoMode: flow.photoMode,
|
|
187
|
+
streetViewOffered: flow.streetViewOffered,
|
|
188
|
+
searchAvailable: flow.searchAvailable,
|
|
189
|
+
uploading,
|
|
190
|
+
confirming,
|
|
191
|
+
error,
|
|
192
|
+
setError,
|
|
193
|
+
pickPhoto,
|
|
194
|
+
removePhoto,
|
|
195
|
+
goNext,
|
|
196
|
+
exitForward,
|
|
197
|
+
confirm,
|
|
198
|
+
...pinActions,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { REVERSE_DEBOUNCE_MS, addressVendorsStubbed, SAMPLE_ADDRESS_LINE } from '../../lib/address-flow';
|
|
4
|
+
import type { KycStore } from '../../store/state';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Reverse geocoding: the pin's human-readable line. Split from
|
|
8
|
+
// use-pin-actions.ts (200-line rule).
|
|
9
|
+
//
|
|
10
|
+
// Debounced, because a drag settles several times, and DROPPED when the pin
|
|
11
|
+
// moved again meanwhile: the comparison is exact coordinate equality, so a
|
|
12
|
+
// late answer can never label somewhere the applicant has already left.
|
|
13
|
+
// Failures are swallowed; the line is a convenience. Display only, and the
|
|
14
|
+
// pin's own country is handed to `onGeocoded` so the declaration can follow
|
|
15
|
+
// the evidence (lib/country-adoption.ts). Mirrors Flutter's
|
|
16
|
+
// address_pin_label.dart.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export function useLabelPin(
|
|
20
|
+
store: KycStore,
|
|
21
|
+
onGeocoded: (country: string | null | undefined) => void,
|
|
22
|
+
): { labelPin: (lat: number, lng: number, delay?: number) => void; labelling: boolean } {
|
|
23
|
+
const reverseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
24
|
+
// The summary line shows nothing while a pin has no address, so it has to
|
|
25
|
+
// say WHY: an answer is coming, or none is.
|
|
26
|
+
const [labelling, setLabelling] = useState(false);
|
|
27
|
+
|
|
28
|
+
useEffect(
|
|
29
|
+
() => () => {
|
|
30
|
+
if (reverseTimer.current) clearTimeout(reverseTimer.current);
|
|
31
|
+
},
|
|
32
|
+
[],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const labelPin = useCallback(
|
|
36
|
+
(lat: number, lng: number, delay = REVERSE_DEBOUNCE_MS) => {
|
|
37
|
+
if (reverseTimer.current) clearTimeout(reverseTimer.current);
|
|
38
|
+
setLabelling(true);
|
|
39
|
+
// SANDBOX never reverse-geocodes: the sample line demonstrates the
|
|
40
|
+
// summary card with no network call (the web SDK's preview/sandbox rule).
|
|
41
|
+
if (addressVendorsStubbed({ environment: store.getState().serverConfig.environment })) {
|
|
42
|
+
reverseTimer.current = setTimeout(() => {
|
|
43
|
+
const cur = store.getState().address;
|
|
44
|
+
if (!cur || cur.lat !== lat || cur.lng !== lng || cur.label) return;
|
|
45
|
+
store.getState().setAddress({ ...cur, label: SAMPLE_ADDRESS_LINE });
|
|
46
|
+
setLabelling(false);
|
|
47
|
+
}, 0);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
reverseTimer.current = setTimeout(() => {
|
|
51
|
+
void store
|
|
52
|
+
.getState()
|
|
53
|
+
.api.addressReverse(lat, lng)
|
|
54
|
+
.then((r) => {
|
|
55
|
+
const cur = store.getState().address;
|
|
56
|
+
// Exact coordinate equality: anything else means the pin moved
|
|
57
|
+
// while the request was out, and this answer describes a spot the
|
|
58
|
+
// applicant has already left.
|
|
59
|
+
if (!r.line || !cur || cur.lat !== lat || cur.lng !== lng) return;
|
|
60
|
+
store.getState().setAddress({
|
|
61
|
+
...cur,
|
|
62
|
+
label: r.line,
|
|
63
|
+
...(r.parts ? { parts: r.parts } : {}),
|
|
64
|
+
// A resolved street retires the typed one (its input hides) —
|
|
65
|
+
// back to UNDEFINED, never '': the details sheet reads '' as
|
|
66
|
+
// deliberately cleared, which suppressed the resolved-street
|
|
67
|
+
// prefill it should be showing.
|
|
68
|
+
...(r.parts?.street ? { street: undefined } : {}),
|
|
69
|
+
});
|
|
70
|
+
onGeocoded(r.parts?.country);
|
|
71
|
+
})
|
|
72
|
+
.catch(() => undefined)
|
|
73
|
+
.finally(() => setLabelling(false));
|
|
74
|
+
}, delay);
|
|
75
|
+
},
|
|
76
|
+
[onGeocoded, store],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return { labelPin, labelling };
|
|
80
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { fixApplied, pinMove, withoutPickedLabel } from '../../lib/address-pin-move';
|
|
4
|
+
import { isAddressStep } from '../../lib/address-flow';
|
|
5
|
+
import {
|
|
6
|
+
currentFix as cachedFix,
|
|
7
|
+
currentFixFailure,
|
|
8
|
+
locating as fixInFlight,
|
|
9
|
+
locationFailureMessage,
|
|
10
|
+
prefetchCurrentFix,
|
|
11
|
+
type CurrentFix,
|
|
12
|
+
} from '../../lib/address-current-location';
|
|
13
|
+
import { adoptionDecision } from '../../lib/country-adoption';
|
|
14
|
+
import { configScope } from '../../lib/scope';
|
|
15
|
+
import { useLabelPin } from './use-label-pin';
|
|
16
|
+
import { fixSourceFor } from './fix-source';
|
|
17
|
+
import type { KycStore } from '../../store/state';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// The pin's mechanics — the shared current-location fix and every way the pin
|
|
21
|
+
// can move. Split from use-address-flow.ts (200-line rule); the reverse
|
|
22
|
+
// geocode that labels the pin lives in use-label-pin.ts, the fix source in
|
|
23
|
+
// fix-source.ts. A MIRROR of the web SDK's steps/address/use-pin-actions.ts.
|
|
24
|
+
//
|
|
25
|
+
// One rule runs through all of it: a HUMAN-CONFIRMED label is never silently
|
|
26
|
+
// discarded, and never silently kept against the applicant's wishes either.
|
|
27
|
+
// They decide, via the pin step's keep/update row.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface PinActions {
|
|
31
|
+
currentFix: CurrentFix | null;
|
|
32
|
+
locating: boolean;
|
|
33
|
+
startPrefetch: () => void;
|
|
34
|
+
applyCurrentFix: (onDone?: () => void, opts?: { silent?: boolean }) => Promise<void>;
|
|
35
|
+
setPin: (next: { lat: number; lng: number }, accuracy?: number | null) => void;
|
|
36
|
+
keepPickedLabel: () => void;
|
|
37
|
+
adoptPinAddress: () => void;
|
|
38
|
+
relabelPin: () => void;
|
|
39
|
+
locateToPin: () => Promise<void>;
|
|
40
|
+
/** The declared country follows the evidence: the rule, and why each of
|
|
41
|
+
* its guards exists, is lib/country-adoption.ts. */
|
|
42
|
+
adoptGeocodedCountry: (country: string | null | undefined, opts?: { explicit?: boolean }) => void;
|
|
43
|
+
/** A reverse geocode is out: the pin has no line YET, rather than none. */
|
|
44
|
+
labelling: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function usePinActions(
|
|
48
|
+
store: KycStore,
|
|
49
|
+
setError: (message: string | null) => void,
|
|
50
|
+
): PinActions {
|
|
51
|
+
const [fix, setFix] = useState<CurrentFix | null>(cachedFix());
|
|
52
|
+
const [fixPending, setFixPending] = useState(fixInFlight());
|
|
53
|
+
|
|
54
|
+
const fixSource = useMemo(() => fixSourceFor(store), [store]);
|
|
55
|
+
|
|
56
|
+
const adoptGeocodedCountry = useCallback(
|
|
57
|
+
(country: string | null | undefined, opts?: { explicit?: boolean }) => {
|
|
58
|
+
const st = store.getState();
|
|
59
|
+
const decision = adoptionDecision({
|
|
60
|
+
country,
|
|
61
|
+
selectedCountry: st.selectedCountry,
|
|
62
|
+
countryAutoPicked: st.countryAutoPicked,
|
|
63
|
+
scope: configScope(st.config),
|
|
64
|
+
accepted: st.config.proofOfAddress?.countries,
|
|
65
|
+
explicit: opts?.explicit,
|
|
66
|
+
});
|
|
67
|
+
if (!decision) return;
|
|
68
|
+
if (decision.auto) st.setCountryAuto(decision.country);
|
|
69
|
+
else st.setCountry(decision.country);
|
|
70
|
+
},
|
|
71
|
+
[store],
|
|
72
|
+
);
|
|
73
|
+
const { labelPin, labelling } = useLabelPin(store, adoptGeocodedCountry);
|
|
74
|
+
|
|
75
|
+
// The fix may have resolved on an earlier step (it is module-level): a
|
|
76
|
+
// guessed declaration is corrected on this mount too, not only when the
|
|
77
|
+
// prefetch happens to finish here.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
adoptGeocodedCountry(cachedFix()?.parts?.country);
|
|
80
|
+
}, [adoptGeocodedCountry]);
|
|
81
|
+
|
|
82
|
+
// ── The shared current-location fix ───────────────────────────────────────
|
|
83
|
+
const startPrefetch = useCallback(() => {
|
|
84
|
+
setFixPending(true);
|
|
85
|
+
void prefetchCurrentFix(fixSource).then((f) => {
|
|
86
|
+
setFix(f);
|
|
87
|
+
setFixPending(false);
|
|
88
|
+
adoptGeocodedCountry(f?.parts?.country);
|
|
89
|
+
});
|
|
90
|
+
}, [adoptGeocodedCountry, fixSource]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Land the pin ON the current fix — the bootstrap override, used while no
|
|
94
|
+
* address exists yet. It sets NO `pickedAt`, so the resulting label is
|
|
95
|
+
* treated as derived and re-derives freely on the next move.
|
|
96
|
+
*/
|
|
97
|
+
const applyCurrentFix = useCallback(
|
|
98
|
+
async (onDone?: () => void, opts?: { silent?: boolean }) => {
|
|
99
|
+
setError(null);
|
|
100
|
+
setFixPending(true);
|
|
101
|
+
// A TAP may retry a previously failed attempt (permission may have been
|
|
102
|
+
// granted since); the silent auto path never re-prompts.
|
|
103
|
+
const f = await prefetchCurrentFix(fixSource, { retry: !opts?.silent });
|
|
104
|
+
setFix(f);
|
|
105
|
+
setFixPending(false);
|
|
106
|
+
adoptGeocodedCountry(f?.parts?.country);
|
|
107
|
+
// The fix can take up to eight seconds, and "Skip for now" is a decision
|
|
108
|
+
// the applicant may make inside that window. A late fix writing an
|
|
109
|
+
// address they declined to give (and an onDone navigating them back onto
|
|
110
|
+
// a step they just left) undoes that decision, so a continuation whose
|
|
111
|
+
// step is no longer in the address flow drops everything on the floor.
|
|
112
|
+
if (!isAddressStep(store.getState().currentStep)) return;
|
|
113
|
+
const cur = store.getState().address;
|
|
114
|
+
// The silent apply resolves seconds after it started, and the person may
|
|
115
|
+
// have picked a searched address meanwhile. A late GPS fix must never
|
|
116
|
+
// overwrite a choice they made; an explicit tap still overrides.
|
|
117
|
+
if (opts?.silent && cur) {
|
|
118
|
+
onDone?.();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (f) {
|
|
122
|
+
store.getState().setAddress(fixApplied(cur, f));
|
|
123
|
+
} else if (!opts?.silent) {
|
|
124
|
+
// Which remedy the person needs depends on WHY the read failed.
|
|
125
|
+
setError(locationFailureMessage(currentFixFailure()));
|
|
126
|
+
}
|
|
127
|
+
onDone?.();
|
|
128
|
+
},
|
|
129
|
+
[adoptGeocodedCountry, fixSource, setError, store],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const setPin = useCallback(
|
|
133
|
+
(next: { lat: number; lng: number }, accuracy: number | null = null) => {
|
|
134
|
+
const move = pinMove(store.getState().address, next, accuracy);
|
|
135
|
+
if (move.kind === 'ignore') return;
|
|
136
|
+
store.getState().setAddress(move.address);
|
|
137
|
+
// A kept label needs no reverse geocode: it already names the property.
|
|
138
|
+
if (move.kind === 'rebuild') labelPin(next.lat, next.lng);
|
|
139
|
+
},
|
|
140
|
+
[labelPin, store],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const keepPickedLabel = useCallback(() => {
|
|
144
|
+
const cur = store.getState().address;
|
|
145
|
+
if (cur) store.getState().setAddress({ ...cur, labelKept: true });
|
|
146
|
+
}, [store]);
|
|
147
|
+
|
|
148
|
+
/** The applicant adopts the PIN's address: drop the pick, re-derive. */
|
|
149
|
+
const adoptPinAddress = useCallback(() => {
|
|
150
|
+
const cur = store.getState().address;
|
|
151
|
+
if (!cur) return;
|
|
152
|
+
store.getState().setAddress(withoutPickedLabel(cur));
|
|
153
|
+
labelPin(cur.lat, cur.lng, 0);
|
|
154
|
+
}, [labelPin, store]);
|
|
155
|
+
|
|
156
|
+
/** Reverse-geocode the current pin's line — for a session restored from a
|
|
157
|
+
* snapshot saved before labels existed, which would show raw coordinates. */
|
|
158
|
+
const relabelPin = useCallback(() => {
|
|
159
|
+
const cur = store.getState().address;
|
|
160
|
+
if (cur && !cur.label) labelPin(cur.lat, cur.lng, 0);
|
|
161
|
+
}, [labelPin, store]);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The on-map locate control: moves the PIN through `setPin`, so a picked
|
|
165
|
+
* address is protected by the same keep/update prompt as any other pin move
|
|
166
|
+
* and a mistaken tap destroys nothing.
|
|
167
|
+
*/
|
|
168
|
+
const locateToPin = useCallback(async () => {
|
|
169
|
+
setError(null);
|
|
170
|
+
setFixPending(true);
|
|
171
|
+
const f = await prefetchCurrentFix(fixSource, { retry: true });
|
|
172
|
+
setFix(f);
|
|
173
|
+
setFixPending(false);
|
|
174
|
+
adoptGeocodedCountry(f?.parts?.country);
|
|
175
|
+
if (f) setPin({ lat: f.lat, lng: f.lng }, f.accuracy);
|
|
176
|
+
else setError(locationFailureMessage(currentFixFailure()));
|
|
177
|
+
}, [adoptGeocodedCountry, fixSource, setError, setPin]);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
labelling,
|
|
181
|
+
currentFix: fix,
|
|
182
|
+
locating: fixPending,
|
|
183
|
+
startPrefetch,
|
|
184
|
+
applyCurrentFix,
|
|
185
|
+
setPin,
|
|
186
|
+
keepPickedLabel,
|
|
187
|
+
adoptPinAddress,
|
|
188
|
+
relabelPin,
|
|
189
|
+
locateToPin,
|
|
190
|
+
adoptGeocodedCountry,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { configScope, isFaceScope } from '../../lib/scope';
|
|
1
2
|
import type { MyazaKYCConfig } from '../../types/config';
|
|
2
3
|
import type { IconName } from '../../components/Icon';
|
|
3
4
|
import { fillTokens } from '../../utils/tokens';
|
|
4
5
|
import { hasEmailVerificationStep, hasPhoneVerificationStep } from '../../config/contact';
|
|
6
|
+
import { hasActiveQuestionnaire } from '../../config/questionnaire';
|
|
7
|
+
import { hasProofOfAddressStep } from '../../config/proofOfAddress';
|
|
8
|
+
import { hasAddressCollectionStep } from '../../config/addressCollection';
|
|
5
9
|
import {
|
|
6
10
|
hasApplicantVerification,
|
|
7
11
|
hasBusinessDocumentsStep,
|
|
@@ -45,15 +49,55 @@ const DEFAULT_DESCRIPTION =
|
|
|
45
49
|
const DEFAULT_BUSINESS_DESCRIPTION =
|
|
46
50
|
'We need to verify your business to comply with regulatory requirements. This process is quick and secure.';
|
|
47
51
|
|
|
52
|
+
const SCOPE_TITLES: Record<string, string> = {
|
|
53
|
+
address: 'Address Verification',
|
|
54
|
+
'biometric-authentication': 'Face Check',
|
|
55
|
+
'biometric-enrollment': 'Face Enrolment',
|
|
56
|
+
questionnaire: 'A Few Questions',
|
|
57
|
+
contact: 'Confirm Your Contact Details',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
|
61
|
+
address:
|
|
62
|
+
'We need to confirm your home address to comply with regulatory requirements. This process is quick and secure.',
|
|
63
|
+
'biometric-authentication':
|
|
64
|
+
'A quick face check confirms it is really you. This takes a few seconds and is secure.',
|
|
65
|
+
'biometric-enrollment':
|
|
66
|
+
'A quick selfie sets up face checks for next time, so you will not have to prove your identity again. This takes a few seconds and is secure.',
|
|
67
|
+
questionnaire:
|
|
68
|
+
'A few questions keep your account details up to date and help us comply with regulatory requirements.',
|
|
69
|
+
contact:
|
|
70
|
+
'We need to re-confirm the email address and phone number on your account. This takes a minute and is secure.',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const SCOPE_BULLETS: Record<string, ConsentProcessStep[]> = {
|
|
74
|
+
address: [
|
|
75
|
+
{ icon: 'map-pin-house', label: 'Pin your home address on a map' },
|
|
76
|
+
{ icon: 'badge-check', label: 'Confirm the details only you can know' },
|
|
77
|
+
],
|
|
78
|
+
'biometric-authentication': [
|
|
79
|
+
{ icon: 'scan-face', label: 'Take a quick selfie with liveness checks' },
|
|
80
|
+
{ icon: 'badge-check', label: 'We match it against your enrolled face' },
|
|
81
|
+
],
|
|
82
|
+
'biometric-enrollment': [
|
|
83
|
+
{ icon: 'scan-face', label: 'Take a quick selfie with liveness checks' },
|
|
84
|
+
{ icon: 'badge-check', label: 'It becomes your face check for next time' },
|
|
85
|
+
],
|
|
86
|
+
questionnaire: [{ icon: 'badge-check', label: 'Answer a few short questions' }],
|
|
87
|
+
contact: [{ icon: 'lock', label: 'Confirm your contact details with a one-time code' }],
|
|
88
|
+
};
|
|
89
|
+
|
|
48
90
|
export function buildConsentModel(config: MyazaKYCConfig): ConsentModel {
|
|
49
91
|
const isBusiness = config.subjectType === 'business';
|
|
92
|
+
const scope = isBusiness ? null : configScope(config);
|
|
93
|
+
const faceScope = isFaceScope(scope);
|
|
50
94
|
const firstName = config.userData?.firstName ?? '';
|
|
51
95
|
|
|
52
96
|
const defaultTitle = firstName
|
|
53
97
|
? `Welcome, ${firstName}`
|
|
54
98
|
: isBusiness
|
|
55
99
|
? 'Business Verification'
|
|
56
|
-
: 'Identity Verification';
|
|
100
|
+
: SCOPE_TITLES[scope ?? ''] ?? 'Identity Verification';
|
|
57
101
|
const title = config.consent?.title
|
|
58
102
|
? fillTokens(config.consent.title, config.userData)
|
|
59
103
|
: defaultTitle;
|
|
@@ -61,40 +105,65 @@ export function buildConsentModel(config: MyazaKYCConfig): ConsentModel {
|
|
|
61
105
|
? fillTokens(config.consent.description, config.userData)
|
|
62
106
|
: isBusiness
|
|
63
107
|
? DEFAULT_BUSINESS_DESCRIPTION
|
|
64
|
-
: DEFAULT_DESCRIPTION;
|
|
108
|
+
: SCOPE_DESCRIPTIONS[scope ?? ''] ?? DEFAULT_DESCRIPTION;
|
|
65
109
|
|
|
66
110
|
// A business flow captures a face only when the applicant verifies their own
|
|
67
111
|
// identity in-flow; an individual flow whenever the selfie step is on.
|
|
68
112
|
const capturesFace = isBusiness
|
|
69
113
|
? hasApplicantVerification(config.business)
|
|
70
|
-
: config.enableSelfie !== false;
|
|
71
|
-
const recordsVideo =
|
|
114
|
+
: faceScope || (!scope && config.enableSelfie !== false);
|
|
115
|
+
const recordsVideo =
|
|
116
|
+
capturesFace || (!isBusiness && !scope && config.enableDocumentCapture !== false);
|
|
72
117
|
|
|
73
118
|
const steps: ConsentProcessStep[] = isBusiness
|
|
74
119
|
? [
|
|
75
120
|
{ icon: 'building-2', label: 'Collect your business registration details' },
|
|
76
121
|
{ icon: 'badge-check', label: 'Verify your business against the official registry' },
|
|
77
122
|
]
|
|
78
|
-
:
|
|
123
|
+
: scope
|
|
124
|
+
? // COPY the catalogue entry: pushing below would otherwise mutate the
|
|
125
|
+
// shared constant, appending one more bullet per re-render.
|
|
126
|
+
[...(SCOPE_BULLETS[scope] ?? [])]
|
|
127
|
+
: [
|
|
79
128
|
{ icon: 'badge-check', label: 'Verify your government-issued ID' },
|
|
80
129
|
{ icon: 'user', label: 'Collect basic personal information' },
|
|
81
130
|
];
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
// On the CONTACT scope the catalogue bullet already says this — appending
|
|
132
|
+
// the generic line showed "confirm your contact details" twice the moment
|
|
133
|
+
// both channels were on.
|
|
134
|
+
const hasEmail = hasEmailVerificationStep(config.emailVerification);
|
|
135
|
+
const hasPhone = hasPhoneVerificationStep(config.phoneVerification);
|
|
136
|
+
if (scope !== 'contact' && (hasEmail || hasPhone)) {
|
|
137
|
+
const what = hasEmail && hasPhone ? 'email and phone number' : hasEmail ? 'email' : 'phone number';
|
|
138
|
+
steps.push({ icon: 'lock', label: `Confirm your ${what} with a one-time code` });
|
|
84
139
|
}
|
|
85
|
-
if (!isBusiness && config.enableDocumentCapture !== false) {
|
|
140
|
+
if (!isBusiness && !scope && config.enableDocumentCapture !== false) {
|
|
86
141
|
steps.push({ icon: 'scan-line', label: 'Capture a photo of your ID document' });
|
|
87
142
|
}
|
|
88
143
|
// Chip-capable IDs additionally read the document's NFC chip — listed when
|
|
89
144
|
// the flow enables NFC so the user knows to have the physical document to
|
|
90
145
|
// hand (a non-chip ID simply skips the step). Individual flows only; a KYB
|
|
91
146
|
// config can't carry `nfc`.
|
|
92
|
-
if (!isBusiness && config.nfc?.enabled) {
|
|
147
|
+
if (!isBusiness && !scope && config.nfc?.enabled) {
|
|
93
148
|
steps.push({ icon: 'nfc', label: 'Scan your document’s security chip (NFC)' });
|
|
94
149
|
}
|
|
95
|
-
if (!isBusiness && config.enableSelfie !== false) {
|
|
150
|
+
if (!isBusiness && !scope && config.enableSelfie !== false) {
|
|
96
151
|
steps.push({ icon: 'scan-face', label: 'Take a selfie for facial verification' });
|
|
97
152
|
}
|
|
153
|
+
// Post-capture features, in the order the flow runs them. Each is gated on
|
|
154
|
+
// the flow that actually asks for it, and skipped where a scope's own
|
|
155
|
+
// catalogue bullet already covers the same step.
|
|
156
|
+
if (!isBusiness && hasProofOfAddressStep(config.proofOfAddress)) {
|
|
157
|
+
steps.push({ icon: 'file-text', label: 'Upload a proof of address document' });
|
|
158
|
+
}
|
|
159
|
+
if (scope !== 'address' && hasAddressCollectionStep(config.addressCollection)) {
|
|
160
|
+
steps.push({ icon: 'map-pin-house', label: 'Pin your address on a map' });
|
|
161
|
+
}
|
|
162
|
+
// The step-order predicate, not a raw fields check: a questionnaire with
|
|
163
|
+
// questions but enabled: false never runs, so it must not be promised.
|
|
164
|
+
if (scope !== 'questionnaire' && hasActiveQuestionnaire(config.questionnaire)) {
|
|
165
|
+
steps.push({ icon: 'badge-check', label: 'Answer a few short questions' });
|
|
166
|
+
}
|
|
98
167
|
if (isBusiness && hasKeyPeopleCollection(config.business)) {
|
|
99
168
|
steps.push({ icon: 'users', label: "List the company's directors and owners" });
|
|
100
169
|
}
|