vesant-sdk 1.7.0 → 1.7.1-dev.0520b46
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/dist/{client-B0qhE2kr.d.mts → client-5q4whhGu.d.mts} +2 -2
- package/dist/{client-DF7hlMEz.d.ts → client-BaUekQT8.d.ts} +96 -52
- package/dist/{client-DtH2RLuy.d.ts → client-DUHC-68_.d.ts} +2 -2
- package/dist/{client-DrjgZoH_.d.mts → client-XDEhh9VN.d.mts} +96 -52
- package/dist/{client-BolQlL5e.d.mts → client-q9fN8Hhf.d.mts} +103 -1
- package/dist/{client-BolQlL5e.d.ts → client-q9fN8Hhf.d.ts} +103 -1
- package/dist/compliance/index.d.mts +3 -3
- package/dist/compliance/index.d.ts +3 -3
- package/dist/compliance/index.js +218 -90
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/index.mjs +218 -90
- package/dist/compliance/index.mjs.map +1 -1
- package/dist/decisions/index.d.mts +1 -1
- package/dist/decisions/index.d.ts +1 -1
- package/dist/decisions/index.js +69 -8
- package/dist/decisions/index.js.map +1 -1
- package/dist/decisions/index.mjs +69 -8
- package/dist/decisions/index.mjs.map +1 -1
- package/dist/geolocation/index.d.mts +68 -7
- package/dist/geolocation/index.d.ts +68 -7
- package/dist/geolocation/index.js +223 -90
- package/dist/geolocation/index.js.map +1 -1
- package/dist/geolocation/index.mjs +220 -91
- package/dist/geolocation/index.mjs.map +1 -1
- package/dist/index-C3zUKydT.d.mts +264 -0
- package/dist/index-DSDgS09k.d.ts +264 -0
- package/dist/index.d.mts +27 -10
- package/dist/index.d.ts +27 -10
- package/dist/index.js +418 -126
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +409 -127
- package/dist/index.mjs.map +1 -1
- package/dist/kyc/core.d.mts +2 -2
- package/dist/kyc/core.d.ts +2 -2
- package/dist/kyc/core.js +102 -21
- package/dist/kyc/core.js.map +1 -1
- package/dist/kyc/core.mjs +102 -22
- package/dist/kyc/core.mjs.map +1 -1
- package/dist/kyc/index.d.mts +154 -30
- package/dist/kyc/index.d.ts +154 -30
- package/dist/kyc/index.js +102 -21
- package/dist/kyc/index.js.map +1 -1
- package/dist/kyc/index.mjs +102 -22
- package/dist/kyc/index.mjs.map +1 -1
- package/dist/react.d.mts +14 -3
- package/dist/react.d.ts +14 -3
- package/dist/react.js +317 -68
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +317 -68
- package/dist/react.mjs.map +1 -1
- package/dist/risk-profile/index.d.mts +1 -1
- package/dist/risk-profile/index.d.ts +1 -1
- package/dist/risk-profile/index.js +69 -8
- package/dist/risk-profile/index.js.map +1 -1
- package/dist/risk-profile/index.mjs +69 -8
- package/dist/risk-profile/index.mjs.map +1 -1
- package/dist/scores/index.d.mts +1 -1
- package/dist/scores/index.d.ts +1 -1
- package/dist/scores/index.js +69 -8
- package/dist/scores/index.js.map +1 -1
- package/dist/scores/index.mjs +69 -8
- package/dist/scores/index.mjs.map +1 -1
- package/dist/tax/index.d.mts +2 -2
- package/dist/tax/index.d.ts +2 -2
- package/dist/tax/index.js +74 -10
- package/dist/tax/index.js.map +1 -1
- package/dist/tax/index.mjs +74 -10
- package/dist/tax/index.mjs.map +1 -1
- package/dist/webhooks/index.d.mts +2 -189
- package/dist/webhooks/index.d.ts +2 -189
- package/dist/webhooks/index.js +85 -21
- package/dist/webhooks/index.js.map +1 -1
- package/dist/webhooks/index.mjs +85 -22
- package/dist/webhooks/index.mjs.map +1 -1
- package/package.json +4 -1
package/dist/react.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client';
|
|
|
4
4
|
// src/geolocation/hooks.ts
|
|
5
5
|
|
|
6
6
|
// src/core/version.ts
|
|
7
|
-
var SDK_VERSION = "1.7.
|
|
7
|
+
var SDK_VERSION = "1.7.1";
|
|
8
8
|
|
|
9
9
|
// src/core/errors.ts
|
|
10
10
|
var VesantError = class _VesantError extends Error {
|
|
@@ -87,27 +87,63 @@ function getBrowserInfo() {
|
|
|
87
87
|
return { browser, browser_version: browserVersion, os, os_version: osVersion };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// src/geolocation/
|
|
91
|
-
var
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
90
|
+
// src/geolocation/integrity.ts
|
|
91
|
+
var isNil = (v) => v === null || v === void 0;
|
|
92
|
+
function assessGpsIntegrity(input, opts = {}) {
|
|
93
|
+
const flags = [];
|
|
94
|
+
const now = opts.now ?? (() => Date.now());
|
|
95
|
+
const maxAgeMs = opts.maxAgeMs ?? 6e4;
|
|
96
|
+
const minAccuracy = opts.minAccuracyMeters ?? 1;
|
|
97
|
+
const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
|
|
98
|
+
if (isNil(accuracy)) {
|
|
99
|
+
flags.push("accuracy_missing");
|
|
100
|
+
} else if (accuracy === 0) {
|
|
101
|
+
flags.push("accuracy_zero");
|
|
102
|
+
} else if (accuracy < minAccuracy) {
|
|
103
|
+
flags.push("accuracy_implausibly_small");
|
|
104
|
+
}
|
|
105
|
+
const hasMotion = !isNil(heading) || !isNil(speed);
|
|
106
|
+
if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
|
|
107
|
+
flags.push("motion_without_accuracy");
|
|
108
|
+
}
|
|
109
|
+
if (!isNil(altitude) && isNil(altitudeAccuracy)) {
|
|
110
|
+
flags.push("altitude_without_accuracy");
|
|
111
|
+
}
|
|
112
|
+
if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
|
|
113
|
+
flags.push("stale_timestamp");
|
|
107
114
|
}
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
|
|
116
|
+
flags.push(`permission_${opts.permissionState}`);
|
|
117
|
+
}
|
|
118
|
+
return { suspicious: flags.length > 0, flags };
|
|
119
|
+
}
|
|
120
|
+
function gpsIntegrityInputFromPosition(position) {
|
|
121
|
+
const c = position.coords;
|
|
122
|
+
return {
|
|
123
|
+
latitude: c.latitude,
|
|
124
|
+
longitude: c.longitude,
|
|
125
|
+
accuracy: c.accuracy,
|
|
126
|
+
altitude: c.altitude,
|
|
127
|
+
altitudeAccuracy: c.altitudeAccuracy,
|
|
128
|
+
heading: c.heading,
|
|
129
|
+
speed: c.speed,
|
|
130
|
+
timestamp: position.timestamp
|
|
131
|
+
};
|
|
110
132
|
}
|
|
133
|
+
async function probeGeolocationPermission() {
|
|
134
|
+
try {
|
|
135
|
+
if (typeof navigator === "undefined" || !navigator.permissions?.query) {
|
|
136
|
+
return "unavailable";
|
|
137
|
+
}
|
|
138
|
+
const status = await navigator.permissions.query({ name: "geolocation" });
|
|
139
|
+
return status.state;
|
|
140
|
+
} catch {
|
|
141
|
+
return "unavailable";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/geolocation/ciphertext.ts
|
|
146
|
+
var CIPHER_TEXT_EXPIRY_MINUTES = 5;
|
|
111
147
|
function getWebGLInfo() {
|
|
112
148
|
if (typeof document === "undefined") return null;
|
|
113
149
|
try {
|
|
@@ -139,6 +175,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
139
175
|
if (typeof navigator === "undefined" || !navigator.geolocation) {
|
|
140
176
|
return null;
|
|
141
177
|
}
|
|
178
|
+
const permissionState = await probeGeolocationPermission();
|
|
142
179
|
return new Promise((resolve) => {
|
|
143
180
|
navigator.geolocation.getCurrentPosition(
|
|
144
181
|
(position) => {
|
|
@@ -155,6 +192,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
155
192
|
resolve(null);
|
|
156
193
|
return;
|
|
157
194
|
}
|
|
195
|
+
const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
|
|
158
196
|
resolve({
|
|
159
197
|
latitude,
|
|
160
198
|
longitude,
|
|
@@ -163,7 +201,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
163
201
|
altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
|
|
164
202
|
heading: position.coords.heading ?? void 0,
|
|
165
203
|
speed: position.coords.speed ?? void 0,
|
|
166
|
-
timestamp: position.timestamp
|
|
204
|
+
timestamp: position.timestamp,
|
|
205
|
+
integrity_flags: flags.length ? flags : void 0
|
|
167
206
|
});
|
|
168
207
|
},
|
|
169
208
|
() => {
|
|
@@ -230,6 +269,9 @@ async function generateCipherText(options, config) {
|
|
|
230
269
|
);
|
|
231
270
|
if (location) {
|
|
232
271
|
locationData = location;
|
|
272
|
+
if (location.integrity_flags?.length) {
|
|
273
|
+
warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
|
|
274
|
+
}
|
|
233
275
|
} else if (gpsRequiredByConfig) {
|
|
234
276
|
throw new VesantError(
|
|
235
277
|
`GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
|
|
@@ -259,15 +301,7 @@ async function generateCipherText(options, config) {
|
|
|
259
301
|
};
|
|
260
302
|
const encoded = encodePayload(payload);
|
|
261
303
|
const timestamp = now.getTime().toString(36);
|
|
262
|
-
|
|
263
|
-
const hmacKey = options.signingKey || options.apiKey;
|
|
264
|
-
if (hmacKey) {
|
|
265
|
-
const message = `02.${timestamp}.${encoded}`;
|
|
266
|
-
const hmac = await computeHMAC(hmacKey, message);
|
|
267
|
-
cipherText = `${message}.${hmac}`;
|
|
268
|
-
} else {
|
|
269
|
-
cipherText = `01.${timestamp}.${encoded}`;
|
|
270
|
-
}
|
|
304
|
+
const cipherText = `01.${timestamp}.${encoded}`;
|
|
271
305
|
return {
|
|
272
306
|
cipherText,
|
|
273
307
|
locationCaptured: !!locationData,
|
|
@@ -332,8 +366,6 @@ function useGeolocation(client, options = {}) {
|
|
|
332
366
|
if (optionsRef.current.autoVerify) {
|
|
333
367
|
const detectAndVerify = async () => {
|
|
334
368
|
try {
|
|
335
|
-
const ipResponse = await fetch("https://api.ipify.org?format=json");
|
|
336
|
-
const { ip } = await ipResponse.json();
|
|
337
369
|
let deviceFingerprint;
|
|
338
370
|
if (optionsRef.current.includeDeviceFingerprint && typeof window !== "undefined") {
|
|
339
371
|
const deviceId = generateDeviceId();
|
|
@@ -347,7 +379,6 @@ function useGeolocation(client, options = {}) {
|
|
|
347
379
|
};
|
|
348
380
|
}
|
|
349
381
|
await verifyIP({
|
|
350
|
-
ip_address: ip,
|
|
351
382
|
user_id: optionsRef.current.userId || "",
|
|
352
383
|
event_type: optionsRef.current.eventType || "page_view",
|
|
353
384
|
device_fingerprint: deviceFingerprint
|
|
@@ -392,14 +423,11 @@ function useCipherText(client) {
|
|
|
392
423
|
const [configLoading, setConfigLoading] = useState(!!client);
|
|
393
424
|
const [configError, setConfigError] = useState(null);
|
|
394
425
|
const configRef = useRef(void 0);
|
|
395
|
-
const configPromiseRef = useRef(null);
|
|
396
426
|
useEffect(() => {
|
|
397
427
|
if (!client) return;
|
|
398
428
|
let cancelled = false;
|
|
399
429
|
setConfigLoading(true);
|
|
400
|
-
|
|
401
|
-
configPromiseRef.current = promise;
|
|
402
|
-
promise.then((cfg) => {
|
|
430
|
+
client.getGPSConfig().then((cfg) => {
|
|
403
431
|
if (!cancelled) {
|
|
404
432
|
configRef.current = cfg;
|
|
405
433
|
setConfigLoading(false);
|
|
@@ -414,13 +442,21 @@ function useCipherText(client) {
|
|
|
414
442
|
cancelled = true;
|
|
415
443
|
};
|
|
416
444
|
}, [client]);
|
|
445
|
+
const refreshConfig = useCallback(async () => {
|
|
446
|
+
if (!client) return;
|
|
447
|
+
try {
|
|
448
|
+
configRef.current = await client.refreshGPSConfig();
|
|
449
|
+
setConfigError(null);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
setConfigError(error instanceof Error ? error : new Error("Failed to refresh GPS config"));
|
|
452
|
+
}
|
|
453
|
+
}, [client]);
|
|
417
454
|
const generate = useCallback(
|
|
418
455
|
async (options) => {
|
|
419
456
|
try {
|
|
420
|
-
if (
|
|
457
|
+
if (client) {
|
|
421
458
|
try {
|
|
422
|
-
|
|
423
|
-
configRef.current = cfg;
|
|
459
|
+
configRef.current = await client.getGPSConfig();
|
|
424
460
|
} catch {
|
|
425
461
|
}
|
|
426
462
|
}
|
|
@@ -430,9 +466,9 @@ function useCipherText(client) {
|
|
|
430
466
|
return null;
|
|
431
467
|
}
|
|
432
468
|
},
|
|
433
|
-
[]
|
|
469
|
+
[client]
|
|
434
470
|
);
|
|
435
|
-
return { generate, configLoading, configError };
|
|
471
|
+
return { generate, refreshConfig, configLoading, configError };
|
|
436
472
|
}
|
|
437
473
|
function useLocationRequests(client, options = {}) {
|
|
438
474
|
const [requests, setRequests] = useState([]);
|
|
@@ -661,6 +697,12 @@ function useLocationCapture(client, options) {
|
|
|
661
697
|
if (accuracy !== null && accuracy !== void 0 && accuracy > 0) {
|
|
662
698
|
capture.accuracy = accuracy;
|
|
663
699
|
}
|
|
700
|
+
const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), {
|
|
701
|
+
permissionState: await probeGeolocationPermission()
|
|
702
|
+
});
|
|
703
|
+
if (flags.length) {
|
|
704
|
+
capture.integrity_flags = flags;
|
|
705
|
+
}
|
|
664
706
|
}
|
|
665
707
|
}
|
|
666
708
|
if (gpsError) {
|
|
@@ -887,19 +929,19 @@ var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id
|
|
|
887
929
|
var FACE_MESSAGES = {
|
|
888
930
|
idle: "Initializing camera...",
|
|
889
931
|
loading: "Loading face detection...",
|
|
890
|
-
scanning: "Position your face in the
|
|
932
|
+
scanning: "Position your face in the oval",
|
|
891
933
|
too_far: "Face too far. Please move closer to the camera",
|
|
892
934
|
too_close: "Too close. Please move back a little",
|
|
893
|
-
off_center: "Center your face in the
|
|
935
|
+
off_center: "Center your face in the oval",
|
|
894
936
|
ok: "Looking great! Tap the button to capture"
|
|
895
937
|
};
|
|
896
938
|
var FACE_COLORS = {
|
|
897
939
|
idle: "#9ca3af",
|
|
898
|
-
loading: "#
|
|
899
|
-
scanning: "#
|
|
940
|
+
loading: "#ef4444",
|
|
941
|
+
scanning: "#ef4444",
|
|
900
942
|
too_far: "#ef4444",
|
|
901
|
-
too_close: "#
|
|
902
|
-
off_center: "#
|
|
943
|
+
too_close: "#ef4444",
|
|
944
|
+
off_center: "#ef4444",
|
|
903
945
|
ok: "#10b981"
|
|
904
946
|
};
|
|
905
947
|
var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
|
|
@@ -964,11 +1006,77 @@ function loadMediaPipeFaceDetection(assetBasePath) {
|
|
|
964
1006
|
return loader;
|
|
965
1007
|
}
|
|
966
1008
|
|
|
1009
|
+
// src/kyc/hostedJourney.ts
|
|
1010
|
+
var REUSE_KYC_SESSION_EXPIRY_MS = 10 * 60 * 1e3;
|
|
1011
|
+
var JOURNEY_FRAME_HEIGHT = "clamp(500px, 64vh, 680px)";
|
|
1012
|
+
var JOURNEY_CONFIRM_TIMEOUT_MS = 45e3;
|
|
1013
|
+
function journeyOutcomeFromMessage(data) {
|
|
1014
|
+
const status = data?.verification_status;
|
|
1015
|
+
if (typeof status !== "string") return null;
|
|
1016
|
+
switch (status) {
|
|
1017
|
+
case "verification.accepted":
|
|
1018
|
+
return "accepted";
|
|
1019
|
+
case "verification.declined":
|
|
1020
|
+
return "declined";
|
|
1021
|
+
case "verification.cancelled":
|
|
1022
|
+
case "request.timeout":
|
|
1023
|
+
case "request.invalid":
|
|
1024
|
+
case "request.unauthorized":
|
|
1025
|
+
case "request.deleted":
|
|
1026
|
+
return "cancelled";
|
|
1027
|
+
default:
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function journeyPollIntervalMs(confirming) {
|
|
1032
|
+
return confirming ? 1e3 : 2500;
|
|
1033
|
+
}
|
|
1034
|
+
function pendingJourneyResult(reference) {
|
|
1035
|
+
return {
|
|
1036
|
+
event: "reuse_kyc",
|
|
1037
|
+
reference,
|
|
1038
|
+
resource_id: "",
|
|
1039
|
+
status: "pending",
|
|
1040
|
+
data: {
|
|
1041
|
+
retries_remaining: 0,
|
|
1042
|
+
retry_limit_exceeded: false,
|
|
1043
|
+
trigger_alert: false,
|
|
1044
|
+
enforce_logout: false,
|
|
1045
|
+
freeze_account: false,
|
|
1046
|
+
block: false,
|
|
1047
|
+
freeze_duration_minutes: 0
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function tryStartOperation(guard) {
|
|
1052
|
+
if (guard.current) return false;
|
|
1053
|
+
guard.current = true;
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
function releaseOperation(guard) {
|
|
1057
|
+
guard.current = false;
|
|
1058
|
+
}
|
|
1059
|
+
function retryTarget(hasHostedJourney) {
|
|
1060
|
+
return hasHostedJourney ? "journey" : "capture";
|
|
1061
|
+
}
|
|
1062
|
+
var CAPTURE_MODAL_MAX_WIDTH = 448;
|
|
1063
|
+
function modalMaxWidth(hasHostedJourney) {
|
|
1064
|
+
return hasHostedJourney ? CAPTURE_MODAL_MAX_WIDTH : CAPTURE_MODAL_MAX_WIDTH;
|
|
1065
|
+
}
|
|
1066
|
+
var JOURNEY_CONTENT_PADDING = 12;
|
|
1067
|
+
function resultForModalClose(stageKind, result, unconfirmed) {
|
|
1068
|
+
const terminal = stageKind === "accepted" || stageKind === "declined" || stageKind === "max_attempts";
|
|
1069
|
+
if (terminal && result !== void 0) return result;
|
|
1070
|
+
const awaitingConfirmation = stageKind === "confirming" || stageKind === "unconfirmed";
|
|
1071
|
+
if (awaitingConfirmation && unconfirmed !== void 0) return unconfirmed;
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
967
1075
|
// src/kyc/FaceCaptureModal.tsx
|
|
968
1076
|
var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
969
|
-
var
|
|
1077
|
+
var ACCEPTED_AUTO_CLOSE_MS = 1200;
|
|
970
1078
|
var LIVENESS_MESSAGES = [
|
|
971
|
-
"Position your face inside the
|
|
1079
|
+
"Position your face inside the oval",
|
|
972
1080
|
"Make sure your face is well lit",
|
|
973
1081
|
"Remove glasses or hats if possible",
|
|
974
1082
|
"Look straight at the camera, then tap Capture"
|
|
@@ -977,6 +1085,12 @@ function headerSubtitle(stageKind) {
|
|
|
977
1085
|
switch (stageKind) {
|
|
978
1086
|
case "qr":
|
|
979
1087
|
return "Use your phone to continue";
|
|
1088
|
+
case "journey":
|
|
1089
|
+
return "Follow the steps to verify your identity";
|
|
1090
|
+
case "confirming":
|
|
1091
|
+
return "Confirming your result";
|
|
1092
|
+
case "unconfirmed":
|
|
1093
|
+
return "Still confirming";
|
|
980
1094
|
case "accepted":
|
|
981
1095
|
return "All set";
|
|
982
1096
|
case "declined":
|
|
@@ -1002,7 +1116,9 @@ function FaceCaptureModal({
|
|
|
1002
1116
|
}) {
|
|
1003
1117
|
const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
|
|
1004
1118
|
const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
|
|
1119
|
+
const journeyMode = Boolean(session.verification_url);
|
|
1005
1120
|
const initialStage = (() => {
|
|
1121
|
+
if (journeyMode) return { kind: "journey", url: session.verification_url ?? "" };
|
|
1006
1122
|
if (initialChoice === "this") return { kind: "capture" };
|
|
1007
1123
|
if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
|
|
1008
1124
|
return { kind: "choose" };
|
|
@@ -1020,19 +1136,20 @@ function FaceCaptureModal({
|
|
|
1020
1136
|
const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
|
|
1021
1137
|
const cancelledRef = useRef(false);
|
|
1022
1138
|
const submittingRef = useRef(false);
|
|
1139
|
+
const journeyRetryingRef = useRef(false);
|
|
1023
1140
|
useEffect(() => {
|
|
1024
1141
|
const timer = setTimeout(() => {
|
|
1025
1142
|
setStage(
|
|
1026
|
-
(prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
|
|
1143
|
+
(prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" || prev.kind === "journey" ? { kind: "error", message: "Session expired" } : prev
|
|
1027
1144
|
);
|
|
1028
|
-
},
|
|
1145
|
+
}, REUSE_KYC_SESSION_EXPIRY_MS);
|
|
1029
1146
|
return () => clearTimeout(timer);
|
|
1030
1147
|
}, [session.token]);
|
|
1031
1148
|
useEffect(() => {
|
|
1032
1149
|
if (stage.kind !== "qr") return;
|
|
1033
1150
|
let stopped = false;
|
|
1034
1151
|
const intervalMs = 2e3;
|
|
1035
|
-
const deadline = Date.now() +
|
|
1152
|
+
const deadline = Date.now() + REUSE_KYC_SESSION_EXPIRY_MS;
|
|
1036
1153
|
let mobileSeen = stage.mobileConnected;
|
|
1037
1154
|
const tick = async () => {
|
|
1038
1155
|
if (stopped || cancelledRef.current) return;
|
|
@@ -1073,6 +1190,82 @@ function FaceCaptureModal({
|
|
|
1073
1190
|
stopped = true;
|
|
1074
1191
|
};
|
|
1075
1192
|
}, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
|
|
1193
|
+
const journeyUrl = stage.kind === "journey" || stage.kind === "confirming" ? stage.url : "";
|
|
1194
|
+
useEffect(() => {
|
|
1195
|
+
if (stage.kind !== "journey" && stage.kind !== "confirming" || !journeyUrl) return;
|
|
1196
|
+
const intervalMs = journeyPollIntervalMs(stage.kind === "confirming");
|
|
1197
|
+
let stopped = false;
|
|
1198
|
+
const checkOnce = async () => {
|
|
1199
|
+
try {
|
|
1200
|
+
const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
|
|
1201
|
+
if (applyResult(result)) {
|
|
1202
|
+
stopped = true;
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
return false;
|
|
1208
|
+
};
|
|
1209
|
+
const tick = async () => {
|
|
1210
|
+
if (stopped || cancelledRef.current) return;
|
|
1211
|
+
await checkOnce();
|
|
1212
|
+
if (!stopped && !cancelledRef.current) {
|
|
1213
|
+
setTimeout(tick, intervalMs);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
void tick();
|
|
1217
|
+
let frameOrigin = "";
|
|
1218
|
+
try {
|
|
1219
|
+
frameOrigin = new URL(journeyUrl).origin;
|
|
1220
|
+
} catch {
|
|
1221
|
+
frameOrigin = "";
|
|
1222
|
+
}
|
|
1223
|
+
const onMessage = (e) => {
|
|
1224
|
+
if (!frameOrigin || e.origin !== frameOrigin) return;
|
|
1225
|
+
const outcome = journeyOutcomeFromMessage(e.data);
|
|
1226
|
+
if (!outcome) return;
|
|
1227
|
+
void checkOnce();
|
|
1228
|
+
setStage((prev) => {
|
|
1229
|
+
if (prev.kind !== "journey") return prev;
|
|
1230
|
+
if (outcome === "cancelled") {
|
|
1231
|
+
return { kind: "error", message: "The verification was closed before it finished. Please try again." };
|
|
1232
|
+
}
|
|
1233
|
+
return { kind: "confirming", outcome, url: prev.url };
|
|
1234
|
+
});
|
|
1235
|
+
};
|
|
1236
|
+
window.addEventListener("message", onMessage);
|
|
1237
|
+
return () => {
|
|
1238
|
+
stopped = true;
|
|
1239
|
+
window.removeEventListener("message", onMessage);
|
|
1240
|
+
};
|
|
1241
|
+
}, [stage.kind, journeyUrl]);
|
|
1242
|
+
useEffect(() => {
|
|
1243
|
+
if (stage.kind !== "confirming") return;
|
|
1244
|
+
const outcome = stage.outcome;
|
|
1245
|
+
const timer = setTimeout(() => {
|
|
1246
|
+
setStage((prev) => prev.kind === "confirming" ? { kind: "unconfirmed", outcome } : prev);
|
|
1247
|
+
}, JOURNEY_CONFIRM_TIMEOUT_MS);
|
|
1248
|
+
return () => clearTimeout(timer);
|
|
1249
|
+
}, [stage.kind, stage.kind === "confirming" ? stage.outcome : ""]);
|
|
1250
|
+
const handleJourneyRetry = async () => {
|
|
1251
|
+
if (!tryStartOperation(journeyRetryingRef)) return;
|
|
1252
|
+
setStage({ kind: "journey", url: "" });
|
|
1253
|
+
try {
|
|
1254
|
+
const refreshed = await client.retryEventBasedFaceVerificationJourney(session.token);
|
|
1255
|
+
if (refreshed.verification_url) {
|
|
1256
|
+
setStage({ kind: "journey", url: refreshed.verification_url });
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
setStage({ kind: "error", message: "Unable to start a new attempt. Please try again." });
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
setStage({
|
|
1262
|
+
kind: "error",
|
|
1263
|
+
message: err instanceof Error ? err.message : "Unable to start a new attempt. Please try again."
|
|
1264
|
+
});
|
|
1265
|
+
} finally {
|
|
1266
|
+
releaseOperation(journeyRetryingRef);
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1076
1269
|
const applyResult = (result) => {
|
|
1077
1270
|
if (result.status === "accepted") {
|
|
1078
1271
|
setStage({ kind: "accepted", result });
|
|
@@ -1200,12 +1393,23 @@ function FaceCaptureModal({
|
|
|
1200
1393
|
}, 3e3);
|
|
1201
1394
|
return () => clearInterval(interval);
|
|
1202
1395
|
}, [captureMode]);
|
|
1203
|
-
const close = (result) => {
|
|
1396
|
+
const close = useCallback((result) => {
|
|
1204
1397
|
cancelledRef.current = true;
|
|
1205
1398
|
if (result === null && onCancel) {
|
|
1206
1399
|
onCancel();
|
|
1207
1400
|
}
|
|
1208
1401
|
onComplete(result);
|
|
1402
|
+
}, [onCancel, onComplete]);
|
|
1403
|
+
useEffect(() => {
|
|
1404
|
+
if (stage.kind !== "accepted") return;
|
|
1405
|
+
const result = stage.result;
|
|
1406
|
+
const timer = setTimeout(() => close(result), ACCEPTED_AUTO_CLOSE_MS);
|
|
1407
|
+
return () => clearTimeout(timer);
|
|
1408
|
+
}, [close, stage]);
|
|
1409
|
+
const handleHeaderClose = () => {
|
|
1410
|
+
const result = "result" in stage ? stage.result : void 0;
|
|
1411
|
+
const unconfirmed = stage.kind === "confirming" || stage.kind === "unconfirmed" ? pendingJourneyResult(session.reference) : void 0;
|
|
1412
|
+
close(resultForModalClose(stage.kind, result, unconfirmed));
|
|
1209
1413
|
};
|
|
1210
1414
|
const renderChoose = () => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React.createElement("div", { style: circleStyle }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })))), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "How would you like to verify?"), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement(
|
|
1211
1415
|
"button",
|
|
@@ -1264,12 +1468,37 @@ function FaceCaptureModal({
|
|
|
1264
1468
|
};
|
|
1265
1469
|
const renderSubmitting = () => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Verifying your photo\u2026"), /* @__PURE__ */ React.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
|
|
1266
1470
|
const renderAccepted = (result) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: successCircleStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Verified"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Your identity has been confirmed. You can continue."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Continue")));
|
|
1267
|
-
const renderDeclined = (result) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, "Retake")), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
|
|
1471
|
+
const renderDeclined = (result) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, journeyMode ? /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, /* @__PURE__ */ React.createElement("span", null, "Try Again")) : /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, "Retake")), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
|
|
1268
1472
|
const renderMaxAttempts = (result) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "For your security, your account has been temporarily restricted", result.data.freeze_duration_minutes ? ` for ${result.data.freeze_duration_minutes} minutes` : "", ". Please contact support if you need immediate help."), result.data?.enforce_logout && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), result.data?.block && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "This action has been blocked for security reasons."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
|
|
1269
1473
|
const renderError = (message) => {
|
|
1270
1474
|
const sessionExpired = /session\s+expired/i.test(message);
|
|
1271
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React.createElement(
|
|
1475
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React.createElement(
|
|
1476
|
+
"button",
|
|
1477
|
+
{
|
|
1478
|
+
style: primaryButtonStyle,
|
|
1479
|
+
onClick: () => retryTarget(journeyMode) === "journey" ? void handleJourneyRetry() : setStage({ kind: "capture" })
|
|
1480
|
+
},
|
|
1481
|
+
"Try Again"
|
|
1482
|
+
), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
|
|
1272
1483
|
};
|
|
1484
|
+
const renderConfirming = () => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Confirming your result\u2026"), /* @__PURE__ */ React.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
|
|
1485
|
+
const renderUnconfirmed = (outcome) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Still confirming"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, outcome === "accepted" ? "Your verification was submitted, but we haven't received the final result yet. You can close this window and we'll keep checking in the background." : "Your last attempt wasn't accepted, but we haven't received the details yet. You can try again or close this window."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, outcome === "declined" && /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, "Try Again"), /* @__PURE__ */ React.createElement(
|
|
1486
|
+
"button",
|
|
1487
|
+
{
|
|
1488
|
+
style: outcome === "accepted" ? primaryButtonStyle : cancelButtonStyle,
|
|
1489
|
+
onClick: () => close(pendingJourneyResult(session.reference))
|
|
1490
|
+
},
|
|
1491
|
+
"Close"
|
|
1492
|
+
)));
|
|
1493
|
+
const renderJourney = (url) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, padding: JOURNEY_CONTENT_PADDING } }, url ? /* @__PURE__ */ React.createElement(
|
|
1494
|
+
"iframe",
|
|
1495
|
+
{
|
|
1496
|
+
src: url,
|
|
1497
|
+
title: "Identity verification",
|
|
1498
|
+
allow: "camera; microphone",
|
|
1499
|
+
style: journeyFrameStyle
|
|
1500
|
+
}
|
|
1501
|
+
) : /* @__PURE__ */ React.createElement("div", { style: { padding: 24, textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Preparing your next attempt\u2026")), /* @__PURE__ */ React.createElement("p", { style: { ...attemptsTextStyle, textAlign: "center" } }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
|
|
1273
1502
|
const body = (() => {
|
|
1274
1503
|
switch (stage.kind) {
|
|
1275
1504
|
case "choose":
|
|
@@ -1278,6 +1507,12 @@ function FaceCaptureModal({
|
|
|
1278
1507
|
return renderQRStage(stage.mobileConnected);
|
|
1279
1508
|
case "capture":
|
|
1280
1509
|
return renderCapture(stage.declinedReason);
|
|
1510
|
+
case "journey":
|
|
1511
|
+
return renderJourney(stage.url);
|
|
1512
|
+
case "confirming":
|
|
1513
|
+
return renderConfirming();
|
|
1514
|
+
case "unconfirmed":
|
|
1515
|
+
return renderUnconfirmed(stage.outcome);
|
|
1281
1516
|
case "submitting":
|
|
1282
1517
|
return renderSubmitting();
|
|
1283
1518
|
case "accepted":
|
|
@@ -1290,10 +1525,10 @@ function FaceCaptureModal({
|
|
|
1290
1525
|
return renderError(stage.message);
|
|
1291
1526
|
}
|
|
1292
1527
|
})();
|
|
1293
|
-
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("style", null, keyframes), /* @__PURE__ */ React.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React.createElement("div", { style: modalStyle }, /* @__PURE__ */ React.createElement("div", { style: headerStyle }, /* @__PURE__ */ React.createElement(
|
|
1528
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("style", null, keyframes), /* @__PURE__ */ React.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React.createElement("div", { style: { ...modalStyle, maxWidth: modalMaxWidth(journeyMode) } }, /* @__PURE__ */ React.createElement("div", { style: headerStyle }, /* @__PURE__ */ React.createElement(
|
|
1294
1529
|
"button",
|
|
1295
1530
|
{
|
|
1296
|
-
onClick:
|
|
1531
|
+
onClick: handleHeaderClose,
|
|
1297
1532
|
style: closeButtonStyle,
|
|
1298
1533
|
"aria-label": "Close"
|
|
1299
1534
|
},
|
|
@@ -1323,7 +1558,8 @@ var modalStyle = {
|
|
|
1323
1558
|
borderRadius: 16,
|
|
1324
1559
|
boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
|
|
1325
1560
|
width: "100%",
|
|
1326
|
-
|
|
1561
|
+
maxHeight: "calc(100vh - 32px)",
|
|
1562
|
+
overflowY: "auto",
|
|
1327
1563
|
animation: "zoomIn .2s ease-out"
|
|
1328
1564
|
};
|
|
1329
1565
|
var headerStyle = {
|
|
@@ -1349,6 +1585,13 @@ var contentStyle = {
|
|
|
1349
1585
|
flexDirection: "column",
|
|
1350
1586
|
gap: 16
|
|
1351
1587
|
};
|
|
1588
|
+
var journeyFrameStyle = {
|
|
1589
|
+
width: "100%",
|
|
1590
|
+
height: JOURNEY_FRAME_HEIGHT,
|
|
1591
|
+
border: 0,
|
|
1592
|
+
borderRadius: 12,
|
|
1593
|
+
background: "#f9fafb"
|
|
1594
|
+
};
|
|
1352
1595
|
var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
|
|
1353
1596
|
var visualGuideStyle = { position: "relative" };
|
|
1354
1597
|
var circleStyle = {
|
|
@@ -1615,9 +1858,10 @@ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
|
|
|
1615
1858
|
if (error) {
|
|
1616
1859
|
return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
|
|
1617
1860
|
}
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
const
|
|
1861
|
+
const detectionAvailable = !faceDetectionFailed;
|
|
1862
|
+
const detectionActive = ready && detectionAvailable;
|
|
1863
|
+
const liveMessage = detectionAvailable ? FACE_MESSAGES[faceStatus] : message;
|
|
1864
|
+
const liveFaceColor = detectionAvailable ? FACE_COLORS[faceStatus] : void 0;
|
|
1621
1865
|
const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
|
|
1622
1866
|
const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
|
|
1623
1867
|
const dynamicCircleStyle = {
|
|
@@ -1733,20 +1977,22 @@ var faceOverlayContainerStyle = {
|
|
|
1733
1977
|
};
|
|
1734
1978
|
var faceRingWrapStyle = {
|
|
1735
1979
|
position: "relative",
|
|
1736
|
-
|
|
1737
|
-
aspectRatio: "
|
|
1980
|
+
height: "64%",
|
|
1981
|
+
aspectRatio: "3 / 4"
|
|
1738
1982
|
};
|
|
1739
1983
|
var faceDashedRingStyle = {
|
|
1740
1984
|
position: "absolute",
|
|
1741
1985
|
inset: -6,
|
|
1742
|
-
borderRadius:
|
|
1743
|
-
border: "6px dashed rgba(255, 255, 255, 0.4)"
|
|
1744
|
-
animation: "spin 10s linear infinite"
|
|
1986
|
+
borderRadius: 9999,
|
|
1987
|
+
border: "6px dashed rgba(255, 255, 255, 0.4)"
|
|
1745
1988
|
};
|
|
1746
1989
|
var faceCircleStyle = {
|
|
1747
1990
|
position: "absolute",
|
|
1748
1991
|
inset: 0,
|
|
1749
|
-
|
|
1992
|
+
// Tailwind's `rounded-full` resolves to a very large fixed radius. On a
|
|
1993
|
+
// 3:4 box that creates the normal KYC capsule/head guide with straighter
|
|
1994
|
+
// sides; 50% would instead produce a mathematically perfect ellipse.
|
|
1995
|
+
borderRadius: 9999,
|
|
1750
1996
|
border: "4px solid #00bc7d",
|
|
1751
1997
|
boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
|
|
1752
1998
|
};
|
|
@@ -2089,6 +2335,7 @@ function isValidFileSize(file, maxSizeBytes = 10 * 1024 * 1024) {
|
|
|
2089
2335
|
}
|
|
2090
2336
|
function formatKycStatus(status) {
|
|
2091
2337
|
const statusMap = {
|
|
2338
|
+
awaiting_kyc: "Awaiting KYC",
|
|
2092
2339
|
pending: "Pending Review",
|
|
2093
2340
|
accepted: "Approved",
|
|
2094
2341
|
declined: "Rejected",
|
|
@@ -2099,6 +2346,8 @@ function formatKycStatus(status) {
|
|
|
2099
2346
|
}
|
|
2100
2347
|
function getStatusColor(status) {
|
|
2101
2348
|
const colorMap = {
|
|
2349
|
+
awaiting_kyc: "#94a3b8",
|
|
2350
|
+
// slate — not started
|
|
2102
2351
|
pending: "#f59e0b",
|
|
2103
2352
|
// amber
|
|
2104
2353
|
accepted: "#10b981",
|