@myazahq/kyc-sdk-react-native 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -5
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DialCodePicker.tsx +47 -68
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -185
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MyazaInput.tsx +17 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/PhoneNumberInput.tsx +8 -0
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +42 -4
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +20 -1
- package/src/config/businessSteps.ts +8 -2
- package/src/config/consentStep.ts +19 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/stepOrder.ts +79 -5
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/workflowMerge.ts +11 -5
- package/src/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/BusinessDetailsFields.tsx +3 -0
- package/src/screens/BusinessDetailsStep.tsx +2 -0
- package/src/screens/BusinessDocumentSlot.tsx +3 -2
- package/src/screens/BusinessDocumentsStep.tsx +4 -55
- package/src/screens/CompanyInfoFields.tsx +6 -1
- package/src/screens/ConsentStep.tsx +6 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +4 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +26 -56
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +154 -52
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -99
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +71 -164
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +93 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +133 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +112 -3
- package/src/store/serverConfig.ts +18 -0
- package/src/store/session.ts +23 -4
- package/src/store/state.ts +134 -0
- package/src/store/submit.ts +18 -1
- package/src/types/config.ts +48 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// One-shot geolocation for the address-collection step.
|
|
3
|
+
//
|
|
4
|
+
// Best-effort BY CONTRACT, mirroring the web SDK's address-helpers: a denied
|
|
5
|
+
// permission, a device with location off, or a slow fix costs the `attested`
|
|
6
|
+
// tier (or the recentre convenience) — never the flow. The attest fix resolves
|
|
7
|
+
// to null / {} on any failure; the PIN's fix says WHY it failed, so the copy
|
|
8
|
+
// can send the person to the right remedy (see lib/address-current-location).
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
import * as Location from 'expo-location';
|
|
12
|
+
|
|
13
|
+
const FIX_TIMEOUT_MS = 8_000;
|
|
14
|
+
|
|
15
|
+
/** Accuracy at which a fix is good enough to stop waiting for the GPS. */
|
|
16
|
+
const PRECISE_ENOUGH_M = 25;
|
|
17
|
+
const PRECISE_WINDOW_MS = 8_000;
|
|
18
|
+
/**
|
|
19
|
+
* Once ANY fix exists, wait only this much longer for a better one. An indoor
|
|
20
|
+
* or wifi-derived fix never reaches 25m, and sitting out the whole window for
|
|
21
|
+
* an accuracy that is not coming reads as "it keeps loading".
|
|
22
|
+
*/
|
|
23
|
+
const FIRST_FIX_GRACE_MS = 3_000;
|
|
24
|
+
|
|
25
|
+
export interface DeviceFix {
|
|
26
|
+
lat: number;
|
|
27
|
+
lng: number;
|
|
28
|
+
accuracy: number | null;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
/** Android reports a mock-location provider; iOS has no equivalent (null). */
|
|
31
|
+
mocked: boolean | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Why a fix could not be taken. A refused permission, a phone that cannot
|
|
36
|
+
* place itself (location switched off, no provider), and a fix that took
|
|
37
|
+
* longer than the window are three different problems with three different
|
|
38
|
+
* remedies. Mirrors the web SDK's LocationFailure and Flutter's enum.
|
|
39
|
+
*/
|
|
40
|
+
export type LocationFailure = 'denied' | 'unavailable' | 'timeout' | 'unsupported';
|
|
41
|
+
|
|
42
|
+
export type PreciseFixOutcome = { fix: DeviceFix } | { failure: LocationFailure };
|
|
43
|
+
|
|
44
|
+
function toFix(pos: Location.LocationObject): DeviceFix {
|
|
45
|
+
return {
|
|
46
|
+
lat: pos.coords.latitude,
|
|
47
|
+
lng: pos.coords.longitude,
|
|
48
|
+
accuracy: typeof pos.coords.accuracy === 'number' ? pos.coords.accuracy : null,
|
|
49
|
+
timestamp: pos.timestamp || Date.now(),
|
|
50
|
+
mocked: typeof pos.mocked === 'boolean' ? pos.mocked : null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
55
|
+
return new Promise<T>((resolve, reject) => {
|
|
56
|
+
const timer = setTimeout(() => reject(new Error('location timeout')), ms);
|
|
57
|
+
promise.then(
|
|
58
|
+
(v) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
resolve(v);
|
|
61
|
+
},
|
|
62
|
+
(e) => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
reject(e);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The most recent fix ANY read produced, kept so the confirm-time attest read
|
|
71
|
+
// can fall back on it (see deviceFixFields).
|
|
72
|
+
let lastGoodFix: DeviceFix | null = null;
|
|
73
|
+
function remember(fix: DeviceFix): DeviceFix {
|
|
74
|
+
lastGoodFix = fix;
|
|
75
|
+
return fix;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A fix older than this no longer says where the phone is NOW. */
|
|
79
|
+
export const LAST_KNOWN_MAX_AGE_MS = 10 * 60_000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One fix for a presence report, or null. Asks for foreground permission on
|
|
83
|
+
* first use.
|
|
84
|
+
*
|
|
85
|
+
* Resolves the way the pin step's precise read learned to, in three rungs:
|
|
86
|
+
* a fresh read at BALANCED accuracy (the fence is 250m or wider, so a 100m
|
|
87
|
+
* fix is plenty; Expo's iOS read waits for a fix that MEETS the requested
|
|
88
|
+
* accuracy, and a High request indoors never delivered one inside the
|
|
89
|
+
* window, so every RN report on the iPhone came back `no_fix` while
|
|
90
|
+
* Flutter's Geolocator, which returns the first update, reported fine,
|
|
91
|
+
* 2026-09-08), then the platform's last known position, then the last fix
|
|
92
|
+
* this module itself took (the pin step's, minutes earlier), each accepted
|
|
93
|
+
* only within LAST_KNOWN_MAX_AGE_MS. Flutter's reporter carries the same
|
|
94
|
+
* last-known fallback.
|
|
95
|
+
*/
|
|
96
|
+
export async function currentPosition(): Promise<DeviceFix | null> {
|
|
97
|
+
try {
|
|
98
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
99
|
+
if (status !== 'granted') return null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const fresh = await withTimeout(
|
|
104
|
+
Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }),
|
|
105
|
+
FIX_TIMEOUT_MS,
|
|
106
|
+
).then(toFix, () => null);
|
|
107
|
+
if (fresh) return remember(fresh);
|
|
108
|
+
const known = await Location.getLastKnownPositionAsync({ maxAge: LAST_KNOWN_MAX_AGE_MS }).then(
|
|
109
|
+
(pos) => (pos ? toFix(pos) : null),
|
|
110
|
+
() => null,
|
|
111
|
+
);
|
|
112
|
+
if (known) return remember(known);
|
|
113
|
+
if (lastGoodFix && Date.now() - lastGoodFix.timestamp <= LAST_KNOWN_MAX_AGE_MS) return lastGoodFix;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A PRECISE fix: watch the position for up to ~8s, keep the most accurate
|
|
119
|
+
* reading, and resolve early once it is within 25m.
|
|
120
|
+
*
|
|
121
|
+
* A single `getCurrentPositionAsync` routinely answers with the first coarse
|
|
122
|
+
* wifi/cell reading — hundreds of metres out, before the GPS has warmed up —
|
|
123
|
+
* which is exactly the pin landing on the wrong compound. A WATCH also never
|
|
124
|
+
* hands back a cached fix, which is the other half of what a pin needs.
|
|
125
|
+
*
|
|
126
|
+
* Never throws: every failure is CLASSIFIED, and every caller falls back to
|
|
127
|
+
* placing the pin by hand.
|
|
128
|
+
*/
|
|
129
|
+
export async function precisePositionOutcome(): Promise<PreciseFixOutcome> {
|
|
130
|
+
let granted = false;
|
|
131
|
+
try {
|
|
132
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
133
|
+
granted = status === 'granted';
|
|
134
|
+
} catch {
|
|
135
|
+
// No location module behind the call: nothing on this device can answer.
|
|
136
|
+
return { failure: 'unsupported' };
|
|
137
|
+
}
|
|
138
|
+
if (!granted) return { failure: 'denied' };
|
|
139
|
+
try {
|
|
140
|
+
// Permission granted with the toggle OFF is the failure people hit most,
|
|
141
|
+
// and it is the one "allow location access" sends them the wrong way on.
|
|
142
|
+
if (!(await Location.hasServicesEnabledAsync())) return { failure: 'unavailable' };
|
|
143
|
+
} catch {
|
|
144
|
+
// An older module without the probe: let the watch decide.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new Promise<PreciseFixOutcome>((resolve) => {
|
|
148
|
+
let best: Location.LocationObject | null = null;
|
|
149
|
+
let settled = false;
|
|
150
|
+
let watchFailed = false;
|
|
151
|
+
let sub: Location.LocationSubscription | null = null;
|
|
152
|
+
let grace: ReturnType<typeof setTimeout> | null = null;
|
|
153
|
+
|
|
154
|
+
const finish = (): void => {
|
|
155
|
+
if (settled) return;
|
|
156
|
+
settled = true;
|
|
157
|
+
clearTimeout(windowTimer);
|
|
158
|
+
if (grace) clearTimeout(grace);
|
|
159
|
+
sub?.remove();
|
|
160
|
+
if (best) resolve({ fix: remember(toFix(best)) });
|
|
161
|
+
else resolve({ failure: watchFailed ? 'unavailable' : 'timeout' });
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const windowTimer = setTimeout(finish, PRECISE_WINDOW_MS);
|
|
165
|
+
|
|
166
|
+
Location.watchPositionAsync(
|
|
167
|
+
{ accuracy: Location.Accuracy.BestForNavigation, distanceInterval: 0 },
|
|
168
|
+
(pos) => {
|
|
169
|
+
const acc = pos.coords.accuracy ?? Infinity;
|
|
170
|
+
if (!best || acc < (best.coords.accuracy ?? Infinity)) best = pos;
|
|
171
|
+
if (acc <= PRECISE_ENOUGH_M) {
|
|
172
|
+
finish();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!grace) grace = setTimeout(finish, FIRST_FIX_GRACE_MS);
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
.then((subscription) => {
|
|
179
|
+
// The window may already have elapsed while the watch was starting —
|
|
180
|
+
// hold no subscription nobody will ever remove.
|
|
181
|
+
if (settled) subscription.remove();
|
|
182
|
+
else sub = subscription;
|
|
183
|
+
})
|
|
184
|
+
.catch(() => {
|
|
185
|
+
watchFailed = true;
|
|
186
|
+
finish();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The attest-presence device fix, as the fields the verify body carries.
|
|
193
|
+
* Empty when no fix could be taken — the submission simply goes without the
|
|
194
|
+
* `attested` tier.
|
|
195
|
+
*/
|
|
196
|
+
/** How old a fix from earlier in the SAME address flow may be and still stand
|
|
197
|
+
* in for the confirm-time read. Placing a pin takes a minute or two; a fix
|
|
198
|
+
* from that window still says the device was here, and the server judges it
|
|
199
|
+
* by its own `capturedAt` anyway. */
|
|
200
|
+
export const RECENT_FIX_MAX_AGE_MS = 3 * 60_000;
|
|
201
|
+
|
|
202
|
+
/** The reading the attest step should send: a fresh one when the read
|
|
203
|
+
* answered, else the recent one the flow already took, else nothing. Pure,
|
|
204
|
+
* so the rule is testable without a GPS. */
|
|
205
|
+
export function pickDeviceFix(
|
|
206
|
+
fresh: DeviceFix | null,
|
|
207
|
+
recent: DeviceFix | null,
|
|
208
|
+
now: number = Date.now(),
|
|
209
|
+
): DeviceFix | null {
|
|
210
|
+
if (fresh) return fresh;
|
|
211
|
+
if (recent && now - recent.timestamp <= RECENT_FIX_MAX_AGE_MS && recent.mocked !== true) return recent;
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function deviceFixFields(): Promise<{
|
|
216
|
+
deviceLat?: number;
|
|
217
|
+
deviceLng?: number;
|
|
218
|
+
deviceAccuracy?: number;
|
|
219
|
+
capturedAt?: string;
|
|
220
|
+
}> {
|
|
221
|
+
// A single getCurrentPositionAsync at confirm routinely times out on iOS
|
|
222
|
+
// while the GPS is still settling, and the submission then went out with
|
|
223
|
+
// no fix at all even though "Use my location" had just placed the pin on
|
|
224
|
+
// one (iPhone 16 Pro Max, 2026-09-07: every address run read "No device
|
|
225
|
+
// fix taken"). The fix the flow already holds is the fallback.
|
|
226
|
+
const fix = pickDeviceFix(await currentPosition(), lastGoodFix);
|
|
227
|
+
if (!fix) return {};
|
|
228
|
+
return {
|
|
229
|
+
deviceLat: fix.lat,
|
|
230
|
+
deviceLng: fix.lng,
|
|
231
|
+
...(fix.accuracy != null ? { deviceAccuracy: fix.accuracy } : {}),
|
|
232
|
+
capturedAt: new Date(fix.timestamp).toISOString(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { KYCApiError } from './api';
|
|
2
|
+
import { UPLOAD_HINT } from '../config/uploadLimits';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Why an upload failed, in words the person can act on.
|
|
6
|
+
//
|
|
7
|
+
// Every upload used to fail as "we could not upload that document", which tells
|
|
8
|
+
// the applicant nothing about whether to pick a smaller file, a different
|
|
9
|
+
// format, or simply try again on better signal. The server already says which
|
|
10
|
+
// it is; this reads its answer and hands back the sentence that matches.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** The refusal, as something the applicant can do something about. */
|
|
14
|
+
export function uploadFailureMessage(error: unknown): string {
|
|
15
|
+
const status = error instanceof KYCApiError ? error.statusCode : null;
|
|
16
|
+
const raw = error instanceof Error ? error.message.toLowerCase() : '';
|
|
17
|
+
|
|
18
|
+
if (status === 413 || raw.includes('too large')) {
|
|
19
|
+
return `That file is too large. ${UPLOAD_HINT}`;
|
|
20
|
+
}
|
|
21
|
+
if (raw.includes('mimetype') || raw.includes('mime type') || raw.includes('unsupported')) {
|
|
22
|
+
return `That file type is not supported. ${UPLOAD_HINT}`;
|
|
23
|
+
}
|
|
24
|
+
// A 4xx we have no better words for still says the file is the problem, so
|
|
25
|
+
// the person retries with a DIFFERENT one rather than the same one twice.
|
|
26
|
+
if (status !== null && status >= 400 && status < 500) {
|
|
27
|
+
return `We could not read that file. ${UPLOAD_HINT}`;
|
|
28
|
+
}
|
|
29
|
+
return 'We could not upload that document. Please check your connection and try again.';
|
|
30
|
+
}
|
|
@@ -103,6 +103,13 @@ export async function resolveWorkflow(
|
|
|
103
103
|
idTypes: res.idTypes,
|
|
104
104
|
branding: res.branding,
|
|
105
105
|
geoCountry: res.geoCountry,
|
|
106
|
+
// The resolution route does not serve these yet, so a workflow mount
|
|
107
|
+
// reads undefined and the address flow offers no search screen. That
|
|
108
|
+
// degrades correctly, and the passthrough means it starts working the
|
|
109
|
+
// day the server carries them.
|
|
110
|
+
addressSearch: res.addressSearch,
|
|
111
|
+
addressSearchMode: res.addressSearchMode,
|
|
112
|
+
mapsFrameUrl: res.mapsFrameUrl ?? null,
|
|
106
113
|
environment: res.environment,
|
|
107
114
|
fatal: false,
|
|
108
115
|
},
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AddressState } from './state';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Restoring a saved address pin.
|
|
5
|
+
//
|
|
6
|
+
// The whole address object is written into session progress verbatim, so the
|
|
7
|
+
// snapshot was written by WHATEVER build saved it: coerce every field back to
|
|
8
|
+
// its declared type and degrade to restoring less, never to breaking the flow.
|
|
9
|
+
//
|
|
10
|
+
// Deliberately does NOT restore the device fix (deviceLat / deviceLng /
|
|
11
|
+
// deviceAccuracy / capturedAt). That is the attest-presence reading, and it is
|
|
12
|
+
// taken fresh at confirm — a stale one would claim the applicant stood at the
|
|
13
|
+
// address on a day they did not.
|
|
14
|
+
//
|
|
15
|
+
// Split from session.ts per the 200-line rule. A MIRROR of the web SDK's
|
|
16
|
+
// RESTORE_PROGRESS address branch and the Flutter port.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
function str(v: unknown): string | null {
|
|
20
|
+
return typeof v === 'string' ? v : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The `parts` breakdown, when the snapshot carries an object for it. */
|
|
24
|
+
function restoreParts(v: unknown): Pick<AddressState, 'parts'> {
|
|
25
|
+
if (!v || typeof v !== 'object') return {};
|
|
26
|
+
const p = v as Record<string, unknown>;
|
|
27
|
+
return {
|
|
28
|
+
parts: {
|
|
29
|
+
street: str(p['street']),
|
|
30
|
+
area: str(p['area']),
|
|
31
|
+
city: str(p['city']),
|
|
32
|
+
state: str(p['state']),
|
|
33
|
+
postcode: str(p['postcode']),
|
|
34
|
+
country: str(p['country']),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The picked-label anchor, only when it is a real coordinate pair. */
|
|
40
|
+
function restorePickedAt(v: unknown): Pick<AddressState, 'pickedAt'> {
|
|
41
|
+
if (!v || typeof v !== 'object') return {};
|
|
42
|
+
const p = v as Record<string, unknown>;
|
|
43
|
+
return typeof p['lat'] === 'number' && typeof p['lng'] === 'number'
|
|
44
|
+
? { pickedAt: { lat: p['lat'], lng: p['lng'] } }
|
|
45
|
+
: {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The Street View frame, only when every field of it survived. A partial
|
|
49
|
+
* frame cannot be re-rendered, and half of one is not a capture. */
|
|
50
|
+
function restoreStreetView(v: unknown): Pick<AddressState, 'streetView'> {
|
|
51
|
+
if (!v || typeof v !== 'object') return {};
|
|
52
|
+
const sv = v as Record<string, unknown>;
|
|
53
|
+
return typeof sv['panoId'] === 'string' &&
|
|
54
|
+
typeof sv['heading'] === 'number' &&
|
|
55
|
+
typeof sv['pitch'] === 'number' &&
|
|
56
|
+
typeof sv['fov'] === 'number'
|
|
57
|
+
? {
|
|
58
|
+
streetView: {
|
|
59
|
+
panoId: sv['panoId'],
|
|
60
|
+
heading: sv['heading'],
|
|
61
|
+
pitch: sv['pitch'],
|
|
62
|
+
fov: sv['fov'],
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
: {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Rebuild the address from a progress snapshot. The caller has already checked
|
|
70
|
+
* that `lat` and `lng` are numbers — without those there is no pin, and there
|
|
71
|
+
* is nothing to restore.
|
|
72
|
+
*/
|
|
73
|
+
export function restoreAddress(a: Record<string, unknown>): AddressState {
|
|
74
|
+
return {
|
|
75
|
+
lat: a['lat'] as number,
|
|
76
|
+
lng: a['lng'] as number,
|
|
77
|
+
accuracy: typeof a['accuracy'] === 'number' ? a['accuracy'] : null,
|
|
78
|
+
directions: typeof a['directions'] === 'string' ? a['directions'] : '',
|
|
79
|
+
propertyName: typeof a['propertyName'] === 'string' ? a['propertyName'] : '',
|
|
80
|
+
propertyNumber: typeof a['propertyNumber'] === 'string' ? a['propertyNumber'] : '',
|
|
81
|
+
// The resolved line and its breakdown survive a restart too. Dropping them
|
|
82
|
+
// is why a resumed session showed raw coordinates where the applicant had
|
|
83
|
+
// picked an address.
|
|
84
|
+
...(typeof a['label'] === 'string' ? { label: a['label'] } : {}),
|
|
85
|
+
...(typeof a['street'] === 'string' ? { street: a['street'] } : {}),
|
|
86
|
+
// The edit-details claims survive a restart like every other typed field.
|
|
87
|
+
...(typeof a['unit'] === 'string' ? { unit: a['unit'] } : {}),
|
|
88
|
+
...(typeof a['neighbourhood'] === 'string' ? { neighbourhood: a['neighbourhood'] } : {}),
|
|
89
|
+
...(typeof a['city'] === 'string' ? { city: a['city'] } : {}),
|
|
90
|
+
...(typeof a['state'] === 'string' ? { state: a['state'] } : {}),
|
|
91
|
+
...(typeof a['postcode'] === 'string' ? { postcode: a['postcode'] } : {}),
|
|
92
|
+
...(a['labelKept'] === true ? { labelKept: true } : {}),
|
|
93
|
+
...restorePickedAt(a['pickedAt']),
|
|
94
|
+
...restoreParts(a['parts']),
|
|
95
|
+
...restoreStreetView(a['streetView']),
|
|
96
|
+
};
|
|
97
|
+
}
|
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,9 +54,11 @@ 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';
|
|
@@ -99,11 +101,14 @@ export function createKycStore(
|
|
|
99
101
|
sessionUrl: null,
|
|
100
102
|
businessCheck: { ...EMPTY_BUSINESS_CHECK },
|
|
101
103
|
selectedCountry: null,
|
|
104
|
+
countryAutoPicked: false,
|
|
102
105
|
selectedIdType: null,
|
|
103
106
|
idNumber: null,
|
|
104
107
|
multiIdSlotIndex: 0,
|
|
105
108
|
multiIdSlots: [],
|
|
106
109
|
multiIdRestored: null,
|
|
110
|
+
selfiePreviewUri: null,
|
|
111
|
+
selfieUpload: IDLE_SELFIE_UPLOAD,
|
|
107
112
|
mediaIds: {},
|
|
108
113
|
submissionResult: null,
|
|
109
114
|
serverConfig: serverConfig ?? INITIAL_SERVER_CONFIG,
|
|
@@ -124,6 +129,11 @@ export function createKycStore(
|
|
|
124
129
|
chipData: null,
|
|
125
130
|
poaDocumentType: null,
|
|
126
131
|
poaFileName: null,
|
|
132
|
+
address: null,
|
|
133
|
+
addressPhotoPreview: null,
|
|
134
|
+
addressIntroSeen: false,
|
|
135
|
+
addressEntranceFraming: false,
|
|
136
|
+
addressSandboxOutcome: null,
|
|
127
137
|
isLoading: false,
|
|
128
138
|
error: null,
|
|
129
139
|
|
|
@@ -137,16 +147,27 @@ export function createKycStore(
|
|
|
137
147
|
const branding = res.branding
|
|
138
148
|
? { ...res.branding, logo: normalizeDevAssetUrl(res.branding.logo, baseUrl) }
|
|
139
149
|
: res.branding;
|
|
150
|
+
// The facts that just landed can add a step AHEAD of the one the flow
|
|
151
|
+
// opened on (the address search step, on a consent-less address
|
|
152
|
+
// flow). Someone still standing on the placeholder's opening step,
|
|
153
|
+
// having done nothing, is moved to the real one; anyone who has
|
|
154
|
+
// moved is left alone. Mirrors Flutter's _loadServerConfig.
|
|
155
|
+
const before = openingStep(get());
|
|
140
156
|
set({
|
|
141
157
|
serverConfig: {
|
|
142
158
|
status: 'ready',
|
|
143
159
|
idTypes: res.idTypes,
|
|
144
160
|
branding,
|
|
145
161
|
geoCountry: res.geoCountry,
|
|
162
|
+
addressSearch: res.addressSearch,
|
|
163
|
+
addressSearchMode: res.addressSearchMode,
|
|
164
|
+
mapsFrameUrl: res.mapsFrameUrl ?? null,
|
|
146
165
|
environment: res.environment,
|
|
147
166
|
fatal: false,
|
|
148
167
|
},
|
|
149
168
|
});
|
|
169
|
+
const after = openingStep(get());
|
|
170
|
+
if (get().currentStep === before && after !== before) set({ currentStep: after });
|
|
150
171
|
} catch (err) {
|
|
151
172
|
const described = describeConfigError(err);
|
|
152
173
|
set((s) => ({
|
|
@@ -163,6 +184,7 @@ export function createKycStore(
|
|
|
163
184
|
const same = s.selectedCountry === country;
|
|
164
185
|
return {
|
|
165
186
|
selectedCountry: country,
|
|
187
|
+
countryAutoPicked: false,
|
|
166
188
|
selectedIdType: same ? s.selectedIdType : null,
|
|
167
189
|
idNumber: same ? s.idNumber : null,
|
|
168
190
|
// A multi-ID run's committed slots belong to the country they were
|
|
@@ -175,6 +197,11 @@ export function createKycStore(
|
|
|
175
197
|
});
|
|
176
198
|
},
|
|
177
199
|
|
|
200
|
+
setCountryAuto(country) {
|
|
201
|
+
get().setCountry(country);
|
|
202
|
+
if (!get().countryAutoPicked) set({ countryAutoPicked: true });
|
|
203
|
+
},
|
|
204
|
+
|
|
178
205
|
commitMultiIdSlot(nextStep, previews) {
|
|
179
206
|
set((s) => {
|
|
180
207
|
if (!s.selectedIdType) return {};
|
|
@@ -257,6 +284,22 @@ export function createKycStore(
|
|
|
257
284
|
set((s) => ({ mediaIds: { ...s.mediaIds, [key]: mediaId } }));
|
|
258
285
|
},
|
|
259
286
|
|
|
287
|
+
setSelfiePreview(uri) {
|
|
288
|
+
set({ selfiePreviewUri: uri });
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
clearSelfie() {
|
|
292
|
+
set((s) => ({
|
|
293
|
+
selfiePreviewUri: null,
|
|
294
|
+
selfieUpload: IDLE_SELFIE_UPLOAD,
|
|
295
|
+
mediaIds: { ...s.mediaIds, selfie: undefined, livenessVideo: undefined },
|
|
296
|
+
}));
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
setSelfieUpload(upload) {
|
|
300
|
+
set({ selfieUpload: upload });
|
|
301
|
+
},
|
|
302
|
+
|
|
260
303
|
setDocumentMediaId(mediaId, side) {
|
|
261
304
|
set((s) => ({
|
|
262
305
|
mediaIds: {
|
|
@@ -423,6 +466,41 @@ export function createKycStore(
|
|
|
423
466
|
});
|
|
424
467
|
},
|
|
425
468
|
|
|
469
|
+
setAddress(address) {
|
|
470
|
+
set({ address });
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
setAddressSandboxOutcome(addressSandboxOutcome) {
|
|
474
|
+
set({ addressSandboxOutcome });
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
setAddressPhoto(mediaId) {
|
|
478
|
+
set((s) => {
|
|
479
|
+
if (mediaId) return { mediaIds: { ...s.mediaIds, addressPhoto: mediaId } };
|
|
480
|
+
const { addressPhoto: _dropped, ...rest } = s.mediaIds;
|
|
481
|
+
return { mediaIds: rest };
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
setAddressPhotoPreview(uri) {
|
|
486
|
+
set({ addressPhotoPreview: uri });
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
markAddressIntroSeen() {
|
|
490
|
+
set({ addressIntroSeen: true });
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
setAddressEntranceFraming(framing) {
|
|
494
|
+
if (get().addressEntranceFraming !== framing) set({ addressEntranceFraming: framing });
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
clearAddress() {
|
|
498
|
+
set((s) => {
|
|
499
|
+
const { addressPhoto: _dropped, ...rest } = s.mediaIds;
|
|
500
|
+
return { address: null, mediaIds: rest, addressPhotoPreview: null };
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
|
|
426
504
|
setDocumentCapturePhase(phase) {
|
|
427
505
|
if (get().documentCapturePhase !== phase) set({ documentCapturePhase: phase });
|
|
428
506
|
},
|
|
@@ -569,15 +647,27 @@ export function createKycStore(
|
|
|
569
647
|
// already sitting on 'consent' (the subscribe below only fires on
|
|
570
648
|
// change, and recordStep dedupes if it fires too).
|
|
571
649
|
resetStepLog();
|
|
572
|
-
|
|
650
|
+
// The cached GPS fix and the once-per-verification auto-locate are
|
|
651
|
+
// scoped to ONE verification: an app session spans many opens of the
|
|
652
|
+
// SDK, and a fix taken an hour ago would drop the pin at wherever the
|
|
653
|
+
// phone was then.
|
|
654
|
+
resetCurrentFix();
|
|
655
|
+
resetAutoLocate();
|
|
656
|
+
// The opening step never depends on the fields reset below (only later
|
|
657
|
+
// steps do), so it is safe to read before the write.
|
|
658
|
+
const opening = openingStep(get());
|
|
659
|
+
recordStep(opening, multiIdSlotOf(get()), selectedIdTypeOf(get()));
|
|
573
660
|
set({
|
|
574
|
-
currentStep:
|
|
661
|
+
currentStep: opening,
|
|
575
662
|
selectedCountry: null,
|
|
663
|
+
countryAutoPicked: false,
|
|
576
664
|
selectedIdType: null,
|
|
577
665
|
idNumber: null,
|
|
578
666
|
multiIdSlotIndex: 0,
|
|
579
667
|
multiIdSlots: [],
|
|
580
668
|
multiIdRestored: null,
|
|
669
|
+
selfiePreviewUri: null,
|
|
670
|
+
selfieUpload: IDLE_SELFIE_UPLOAD,
|
|
581
671
|
mediaIds: {},
|
|
582
672
|
submissionResult: null,
|
|
583
673
|
documentScanPhase: 'front',
|
|
@@ -599,6 +689,11 @@ export function createKycStore(
|
|
|
599
689
|
chipData: null,
|
|
600
690
|
poaDocumentType: null,
|
|
601
691
|
poaFileName: null,
|
|
692
|
+
address: null,
|
|
693
|
+
addressPhotoPreview: null,
|
|
694
|
+
addressIntroSeen: false,
|
|
695
|
+
addressEntranceFraming: false,
|
|
696
|
+
addressSandboxOutcome: null,
|
|
602
697
|
isLoading: false,
|
|
603
698
|
error: null,
|
|
604
699
|
});
|
|
@@ -622,6 +717,20 @@ export function createKycStore(
|
|
|
622
717
|
// subscription above only fires on CHANGE, and a new store already sits on
|
|
623
718
|
// 'consent').
|
|
624
719
|
resetStepLog();
|
|
720
|
+
// The module-level GPS cache and the auto-locate latch are scoped to ONE
|
|
721
|
+
// verification, and this — not reset() — is where a verification begins.
|
|
722
|
+
// Clearing them only in reset() left both alive across modal launches, so a
|
|
723
|
+
// second verification in the same app session dropped its pin wherever the
|
|
724
|
+
// phone had been on the first (possibly hours and a city away) and skipped
|
|
725
|
+
// the automatic locate entirely, since the latch was already spent.
|
|
726
|
+
resetCurrentFix();
|
|
727
|
+
resetAutoLocate();
|
|
728
|
+
// A workflow that switched the consent screen off opens on its first real
|
|
729
|
+
// step; the literal 'consent' above is only the shape before the config is
|
|
730
|
+
// consulted. Set BEFORE the journey is recorded, so the log opens where the
|
|
731
|
+
// person does.
|
|
732
|
+
const opening = openingStep(store.getState());
|
|
733
|
+
if (opening !== store.getState().currentStep) store.setState({ currentStep: opening });
|
|
625
734
|
recordStep(
|
|
626
735
|
store.getState().currentStep,
|
|
627
736
|
multiIdSlotOf(store.getState()),
|