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.js
CHANGED
|
@@ -10,7 +10,7 @@ var React__default = /*#__PURE__*/_interopDefault(React);
|
|
|
10
10
|
// src/geolocation/hooks.ts
|
|
11
11
|
|
|
12
12
|
// src/core/version.ts
|
|
13
|
-
var SDK_VERSION = "1.7.
|
|
13
|
+
var SDK_VERSION = "1.7.1";
|
|
14
14
|
|
|
15
15
|
// src/core/errors.ts
|
|
16
16
|
var VesantError = class _VesantError extends Error {
|
|
@@ -93,27 +93,63 @@ function getBrowserInfo() {
|
|
|
93
93
|
return { browser, browser_version: browserVersion, os, os_version: osVersion };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// src/geolocation/
|
|
97
|
-
var
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
// src/geolocation/integrity.ts
|
|
97
|
+
var isNil = (v) => v === null || v === void 0;
|
|
98
|
+
function assessGpsIntegrity(input, opts = {}) {
|
|
99
|
+
const flags = [];
|
|
100
|
+
const now = opts.now ?? (() => Date.now());
|
|
101
|
+
const maxAgeMs = opts.maxAgeMs ?? 6e4;
|
|
102
|
+
const minAccuracy = opts.minAccuracyMeters ?? 1;
|
|
103
|
+
const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
|
|
104
|
+
if (isNil(accuracy)) {
|
|
105
|
+
flags.push("accuracy_missing");
|
|
106
|
+
} else if (accuracy === 0) {
|
|
107
|
+
flags.push("accuracy_zero");
|
|
108
|
+
} else if (accuracy < minAccuracy) {
|
|
109
|
+
flags.push("accuracy_implausibly_small");
|
|
110
|
+
}
|
|
111
|
+
const hasMotion = !isNil(heading) || !isNil(speed);
|
|
112
|
+
if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
|
|
113
|
+
flags.push("motion_without_accuracy");
|
|
114
|
+
}
|
|
115
|
+
if (!isNil(altitude) && isNil(altitudeAccuracy)) {
|
|
116
|
+
flags.push("altitude_without_accuracy");
|
|
117
|
+
}
|
|
118
|
+
if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
|
|
119
|
+
flags.push("stale_timestamp");
|
|
113
120
|
}
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
|
|
122
|
+
flags.push(`permission_${opts.permissionState}`);
|
|
123
|
+
}
|
|
124
|
+
return { suspicious: flags.length > 0, flags };
|
|
125
|
+
}
|
|
126
|
+
function gpsIntegrityInputFromPosition(position) {
|
|
127
|
+
const c = position.coords;
|
|
128
|
+
return {
|
|
129
|
+
latitude: c.latitude,
|
|
130
|
+
longitude: c.longitude,
|
|
131
|
+
accuracy: c.accuracy,
|
|
132
|
+
altitude: c.altitude,
|
|
133
|
+
altitudeAccuracy: c.altitudeAccuracy,
|
|
134
|
+
heading: c.heading,
|
|
135
|
+
speed: c.speed,
|
|
136
|
+
timestamp: position.timestamp
|
|
137
|
+
};
|
|
116
138
|
}
|
|
139
|
+
async function probeGeolocationPermission() {
|
|
140
|
+
try {
|
|
141
|
+
if (typeof navigator === "undefined" || !navigator.permissions?.query) {
|
|
142
|
+
return "unavailable";
|
|
143
|
+
}
|
|
144
|
+
const status = await navigator.permissions.query({ name: "geolocation" });
|
|
145
|
+
return status.state;
|
|
146
|
+
} catch {
|
|
147
|
+
return "unavailable";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/geolocation/ciphertext.ts
|
|
152
|
+
var CIPHER_TEXT_EXPIRY_MINUTES = 5;
|
|
117
153
|
function getWebGLInfo() {
|
|
118
154
|
if (typeof document === "undefined") return null;
|
|
119
155
|
try {
|
|
@@ -145,6 +181,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
145
181
|
if (typeof navigator === "undefined" || !navigator.geolocation) {
|
|
146
182
|
return null;
|
|
147
183
|
}
|
|
184
|
+
const permissionState = await probeGeolocationPermission();
|
|
148
185
|
return new Promise((resolve) => {
|
|
149
186
|
navigator.geolocation.getCurrentPosition(
|
|
150
187
|
(position) => {
|
|
@@ -161,6 +198,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
161
198
|
resolve(null);
|
|
162
199
|
return;
|
|
163
200
|
}
|
|
201
|
+
const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
|
|
164
202
|
resolve({
|
|
165
203
|
latitude,
|
|
166
204
|
longitude,
|
|
@@ -169,7 +207,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
169
207
|
altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
|
|
170
208
|
heading: position.coords.heading ?? void 0,
|
|
171
209
|
speed: position.coords.speed ?? void 0,
|
|
172
|
-
timestamp: position.timestamp
|
|
210
|
+
timestamp: position.timestamp,
|
|
211
|
+
integrity_flags: flags.length ? flags : void 0
|
|
173
212
|
});
|
|
174
213
|
},
|
|
175
214
|
() => {
|
|
@@ -236,6 +275,9 @@ async function generateCipherText(options, config) {
|
|
|
236
275
|
);
|
|
237
276
|
if (location) {
|
|
238
277
|
locationData = location;
|
|
278
|
+
if (location.integrity_flags?.length) {
|
|
279
|
+
warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
|
|
280
|
+
}
|
|
239
281
|
} else if (gpsRequiredByConfig) {
|
|
240
282
|
throw new VesantError(
|
|
241
283
|
`GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
|
|
@@ -265,15 +307,7 @@ async function generateCipherText(options, config) {
|
|
|
265
307
|
};
|
|
266
308
|
const encoded = encodePayload(payload);
|
|
267
309
|
const timestamp = now.getTime().toString(36);
|
|
268
|
-
|
|
269
|
-
const hmacKey = options.signingKey || options.apiKey;
|
|
270
|
-
if (hmacKey) {
|
|
271
|
-
const message = `02.${timestamp}.${encoded}`;
|
|
272
|
-
const hmac = await computeHMAC(hmacKey, message);
|
|
273
|
-
cipherText = `${message}.${hmac}`;
|
|
274
|
-
} else {
|
|
275
|
-
cipherText = `01.${timestamp}.${encoded}`;
|
|
276
|
-
}
|
|
310
|
+
const cipherText = `01.${timestamp}.${encoded}`;
|
|
277
311
|
return {
|
|
278
312
|
cipherText,
|
|
279
313
|
locationCaptured: !!locationData,
|
|
@@ -338,8 +372,6 @@ function useGeolocation(client, options = {}) {
|
|
|
338
372
|
if (optionsRef.current.autoVerify) {
|
|
339
373
|
const detectAndVerify = async () => {
|
|
340
374
|
try {
|
|
341
|
-
const ipResponse = await fetch("https://api.ipify.org?format=json");
|
|
342
|
-
const { ip } = await ipResponse.json();
|
|
343
375
|
let deviceFingerprint;
|
|
344
376
|
if (optionsRef.current.includeDeviceFingerprint && typeof window !== "undefined") {
|
|
345
377
|
const deviceId = generateDeviceId();
|
|
@@ -353,7 +385,6 @@ function useGeolocation(client, options = {}) {
|
|
|
353
385
|
};
|
|
354
386
|
}
|
|
355
387
|
await verifyIP({
|
|
356
|
-
ip_address: ip,
|
|
357
388
|
user_id: optionsRef.current.userId || "",
|
|
358
389
|
event_type: optionsRef.current.eventType || "page_view",
|
|
359
390
|
device_fingerprint: deviceFingerprint
|
|
@@ -398,14 +429,11 @@ function useCipherText(client) {
|
|
|
398
429
|
const [configLoading, setConfigLoading] = React.useState(!!client);
|
|
399
430
|
const [configError, setConfigError] = React.useState(null);
|
|
400
431
|
const configRef = React.useRef(void 0);
|
|
401
|
-
const configPromiseRef = React.useRef(null);
|
|
402
432
|
React.useEffect(() => {
|
|
403
433
|
if (!client) return;
|
|
404
434
|
let cancelled = false;
|
|
405
435
|
setConfigLoading(true);
|
|
406
|
-
|
|
407
|
-
configPromiseRef.current = promise;
|
|
408
|
-
promise.then((cfg) => {
|
|
436
|
+
client.getGPSConfig().then((cfg) => {
|
|
409
437
|
if (!cancelled) {
|
|
410
438
|
configRef.current = cfg;
|
|
411
439
|
setConfigLoading(false);
|
|
@@ -420,13 +448,21 @@ function useCipherText(client) {
|
|
|
420
448
|
cancelled = true;
|
|
421
449
|
};
|
|
422
450
|
}, [client]);
|
|
451
|
+
const refreshConfig = React.useCallback(async () => {
|
|
452
|
+
if (!client) return;
|
|
453
|
+
try {
|
|
454
|
+
configRef.current = await client.refreshGPSConfig();
|
|
455
|
+
setConfigError(null);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
setConfigError(error instanceof Error ? error : new Error("Failed to refresh GPS config"));
|
|
458
|
+
}
|
|
459
|
+
}, [client]);
|
|
423
460
|
const generate = React.useCallback(
|
|
424
461
|
async (options) => {
|
|
425
462
|
try {
|
|
426
|
-
if (
|
|
463
|
+
if (client) {
|
|
427
464
|
try {
|
|
428
|
-
|
|
429
|
-
configRef.current = cfg;
|
|
465
|
+
configRef.current = await client.getGPSConfig();
|
|
430
466
|
} catch {
|
|
431
467
|
}
|
|
432
468
|
}
|
|
@@ -436,9 +472,9 @@ function useCipherText(client) {
|
|
|
436
472
|
return null;
|
|
437
473
|
}
|
|
438
474
|
},
|
|
439
|
-
[]
|
|
475
|
+
[client]
|
|
440
476
|
);
|
|
441
|
-
return { generate, configLoading, configError };
|
|
477
|
+
return { generate, refreshConfig, configLoading, configError };
|
|
442
478
|
}
|
|
443
479
|
function useLocationRequests(client, options = {}) {
|
|
444
480
|
const [requests, setRequests] = React.useState([]);
|
|
@@ -667,6 +703,12 @@ function useLocationCapture(client, options) {
|
|
|
667
703
|
if (accuracy !== null && accuracy !== void 0 && accuracy > 0) {
|
|
668
704
|
capture.accuracy = accuracy;
|
|
669
705
|
}
|
|
706
|
+
const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), {
|
|
707
|
+
permissionState: await probeGeolocationPermission()
|
|
708
|
+
});
|
|
709
|
+
if (flags.length) {
|
|
710
|
+
capture.integrity_flags = flags;
|
|
711
|
+
}
|
|
670
712
|
}
|
|
671
713
|
}
|
|
672
714
|
if (gpsError) {
|
|
@@ -893,19 +935,19 @@ var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id
|
|
|
893
935
|
var FACE_MESSAGES = {
|
|
894
936
|
idle: "Initializing camera...",
|
|
895
937
|
loading: "Loading face detection...",
|
|
896
|
-
scanning: "Position your face in the
|
|
938
|
+
scanning: "Position your face in the oval",
|
|
897
939
|
too_far: "Face too far. Please move closer to the camera",
|
|
898
940
|
too_close: "Too close. Please move back a little",
|
|
899
|
-
off_center: "Center your face in the
|
|
941
|
+
off_center: "Center your face in the oval",
|
|
900
942
|
ok: "Looking great! Tap the button to capture"
|
|
901
943
|
};
|
|
902
944
|
var FACE_COLORS = {
|
|
903
945
|
idle: "#9ca3af",
|
|
904
|
-
loading: "#
|
|
905
|
-
scanning: "#
|
|
946
|
+
loading: "#ef4444",
|
|
947
|
+
scanning: "#ef4444",
|
|
906
948
|
too_far: "#ef4444",
|
|
907
|
-
too_close: "#
|
|
908
|
-
off_center: "#
|
|
949
|
+
too_close: "#ef4444",
|
|
950
|
+
off_center: "#ef4444",
|
|
909
951
|
ok: "#10b981"
|
|
910
952
|
};
|
|
911
953
|
var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
|
|
@@ -970,11 +1012,77 @@ function loadMediaPipeFaceDetection(assetBasePath) {
|
|
|
970
1012
|
return loader;
|
|
971
1013
|
}
|
|
972
1014
|
|
|
1015
|
+
// src/kyc/hostedJourney.ts
|
|
1016
|
+
var REUSE_KYC_SESSION_EXPIRY_MS = 10 * 60 * 1e3;
|
|
1017
|
+
var JOURNEY_FRAME_HEIGHT = "clamp(500px, 64vh, 680px)";
|
|
1018
|
+
var JOURNEY_CONFIRM_TIMEOUT_MS = 45e3;
|
|
1019
|
+
function journeyOutcomeFromMessage(data) {
|
|
1020
|
+
const status = data?.verification_status;
|
|
1021
|
+
if (typeof status !== "string") return null;
|
|
1022
|
+
switch (status) {
|
|
1023
|
+
case "verification.accepted":
|
|
1024
|
+
return "accepted";
|
|
1025
|
+
case "verification.declined":
|
|
1026
|
+
return "declined";
|
|
1027
|
+
case "verification.cancelled":
|
|
1028
|
+
case "request.timeout":
|
|
1029
|
+
case "request.invalid":
|
|
1030
|
+
case "request.unauthorized":
|
|
1031
|
+
case "request.deleted":
|
|
1032
|
+
return "cancelled";
|
|
1033
|
+
default:
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function journeyPollIntervalMs(confirming) {
|
|
1038
|
+
return confirming ? 1e3 : 2500;
|
|
1039
|
+
}
|
|
1040
|
+
function pendingJourneyResult(reference) {
|
|
1041
|
+
return {
|
|
1042
|
+
event: "reuse_kyc",
|
|
1043
|
+
reference,
|
|
1044
|
+
resource_id: "",
|
|
1045
|
+
status: "pending",
|
|
1046
|
+
data: {
|
|
1047
|
+
retries_remaining: 0,
|
|
1048
|
+
retry_limit_exceeded: false,
|
|
1049
|
+
trigger_alert: false,
|
|
1050
|
+
enforce_logout: false,
|
|
1051
|
+
freeze_account: false,
|
|
1052
|
+
block: false,
|
|
1053
|
+
freeze_duration_minutes: 0
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function tryStartOperation(guard) {
|
|
1058
|
+
if (guard.current) return false;
|
|
1059
|
+
guard.current = true;
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
function releaseOperation(guard) {
|
|
1063
|
+
guard.current = false;
|
|
1064
|
+
}
|
|
1065
|
+
function retryTarget(hasHostedJourney) {
|
|
1066
|
+
return hasHostedJourney ? "journey" : "capture";
|
|
1067
|
+
}
|
|
1068
|
+
var CAPTURE_MODAL_MAX_WIDTH = 448;
|
|
1069
|
+
function modalMaxWidth(hasHostedJourney) {
|
|
1070
|
+
return hasHostedJourney ? CAPTURE_MODAL_MAX_WIDTH : CAPTURE_MODAL_MAX_WIDTH;
|
|
1071
|
+
}
|
|
1072
|
+
var JOURNEY_CONTENT_PADDING = 12;
|
|
1073
|
+
function resultForModalClose(stageKind, result, unconfirmed) {
|
|
1074
|
+
const terminal = stageKind === "accepted" || stageKind === "declined" || stageKind === "max_attempts";
|
|
1075
|
+
if (terminal && result !== void 0) return result;
|
|
1076
|
+
const awaitingConfirmation = stageKind === "confirming" || stageKind === "unconfirmed";
|
|
1077
|
+
if (awaitingConfirmation && unconfirmed !== void 0) return unconfirmed;
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
973
1081
|
// src/kyc/FaceCaptureModal.tsx
|
|
974
1082
|
var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
975
|
-
var
|
|
1083
|
+
var ACCEPTED_AUTO_CLOSE_MS = 1200;
|
|
976
1084
|
var LIVENESS_MESSAGES = [
|
|
977
|
-
"Position your face inside the
|
|
1085
|
+
"Position your face inside the oval",
|
|
978
1086
|
"Make sure your face is well lit",
|
|
979
1087
|
"Remove glasses or hats if possible",
|
|
980
1088
|
"Look straight at the camera, then tap Capture"
|
|
@@ -983,6 +1091,12 @@ function headerSubtitle(stageKind) {
|
|
|
983
1091
|
switch (stageKind) {
|
|
984
1092
|
case "qr":
|
|
985
1093
|
return "Use your phone to continue";
|
|
1094
|
+
case "journey":
|
|
1095
|
+
return "Follow the steps to verify your identity";
|
|
1096
|
+
case "confirming":
|
|
1097
|
+
return "Confirming your result";
|
|
1098
|
+
case "unconfirmed":
|
|
1099
|
+
return "Still confirming";
|
|
986
1100
|
case "accepted":
|
|
987
1101
|
return "All set";
|
|
988
1102
|
case "declined":
|
|
@@ -1008,7 +1122,9 @@ function FaceCaptureModal({
|
|
|
1008
1122
|
}) {
|
|
1009
1123
|
const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
|
|
1010
1124
|
const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
|
|
1125
|
+
const journeyMode = Boolean(session.verification_url);
|
|
1011
1126
|
const initialStage = (() => {
|
|
1127
|
+
if (journeyMode) return { kind: "journey", url: session.verification_url ?? "" };
|
|
1012
1128
|
if (initialChoice === "this") return { kind: "capture" };
|
|
1013
1129
|
if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
|
|
1014
1130
|
return { kind: "choose" };
|
|
@@ -1026,19 +1142,20 @@ function FaceCaptureModal({
|
|
|
1026
1142
|
const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
|
|
1027
1143
|
const cancelledRef = React.useRef(false);
|
|
1028
1144
|
const submittingRef = React.useRef(false);
|
|
1145
|
+
const journeyRetryingRef = React.useRef(false);
|
|
1029
1146
|
React.useEffect(() => {
|
|
1030
1147
|
const timer = setTimeout(() => {
|
|
1031
1148
|
setStage(
|
|
1032
|
-
(prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
|
|
1149
|
+
(prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" || prev.kind === "journey" ? { kind: "error", message: "Session expired" } : prev
|
|
1033
1150
|
);
|
|
1034
|
-
},
|
|
1151
|
+
}, REUSE_KYC_SESSION_EXPIRY_MS);
|
|
1035
1152
|
return () => clearTimeout(timer);
|
|
1036
1153
|
}, [session.token]);
|
|
1037
1154
|
React.useEffect(() => {
|
|
1038
1155
|
if (stage.kind !== "qr") return;
|
|
1039
1156
|
let stopped = false;
|
|
1040
1157
|
const intervalMs = 2e3;
|
|
1041
|
-
const deadline = Date.now() +
|
|
1158
|
+
const deadline = Date.now() + REUSE_KYC_SESSION_EXPIRY_MS;
|
|
1042
1159
|
let mobileSeen = stage.mobileConnected;
|
|
1043
1160
|
const tick = async () => {
|
|
1044
1161
|
if (stopped || cancelledRef.current) return;
|
|
@@ -1079,6 +1196,82 @@ function FaceCaptureModal({
|
|
|
1079
1196
|
stopped = true;
|
|
1080
1197
|
};
|
|
1081
1198
|
}, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
|
|
1199
|
+
const journeyUrl = stage.kind === "journey" || stage.kind === "confirming" ? stage.url : "";
|
|
1200
|
+
React.useEffect(() => {
|
|
1201
|
+
if (stage.kind !== "journey" && stage.kind !== "confirming" || !journeyUrl) return;
|
|
1202
|
+
const intervalMs = journeyPollIntervalMs(stage.kind === "confirming");
|
|
1203
|
+
let stopped = false;
|
|
1204
|
+
const checkOnce = async () => {
|
|
1205
|
+
try {
|
|
1206
|
+
const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
|
|
1207
|
+
if (applyResult(result)) {
|
|
1208
|
+
stopped = true;
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
return false;
|
|
1214
|
+
};
|
|
1215
|
+
const tick = async () => {
|
|
1216
|
+
if (stopped || cancelledRef.current) return;
|
|
1217
|
+
await checkOnce();
|
|
1218
|
+
if (!stopped && !cancelledRef.current) {
|
|
1219
|
+
setTimeout(tick, intervalMs);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
void tick();
|
|
1223
|
+
let frameOrigin = "";
|
|
1224
|
+
try {
|
|
1225
|
+
frameOrigin = new URL(journeyUrl).origin;
|
|
1226
|
+
} catch {
|
|
1227
|
+
frameOrigin = "";
|
|
1228
|
+
}
|
|
1229
|
+
const onMessage = (e) => {
|
|
1230
|
+
if (!frameOrigin || e.origin !== frameOrigin) return;
|
|
1231
|
+
const outcome = journeyOutcomeFromMessage(e.data);
|
|
1232
|
+
if (!outcome) return;
|
|
1233
|
+
void checkOnce();
|
|
1234
|
+
setStage((prev) => {
|
|
1235
|
+
if (prev.kind !== "journey") return prev;
|
|
1236
|
+
if (outcome === "cancelled") {
|
|
1237
|
+
return { kind: "error", message: "The verification was closed before it finished. Please try again." };
|
|
1238
|
+
}
|
|
1239
|
+
return { kind: "confirming", outcome, url: prev.url };
|
|
1240
|
+
});
|
|
1241
|
+
};
|
|
1242
|
+
window.addEventListener("message", onMessage);
|
|
1243
|
+
return () => {
|
|
1244
|
+
stopped = true;
|
|
1245
|
+
window.removeEventListener("message", onMessage);
|
|
1246
|
+
};
|
|
1247
|
+
}, [stage.kind, journeyUrl]);
|
|
1248
|
+
React.useEffect(() => {
|
|
1249
|
+
if (stage.kind !== "confirming") return;
|
|
1250
|
+
const outcome = stage.outcome;
|
|
1251
|
+
const timer = setTimeout(() => {
|
|
1252
|
+
setStage((prev) => prev.kind === "confirming" ? { kind: "unconfirmed", outcome } : prev);
|
|
1253
|
+
}, JOURNEY_CONFIRM_TIMEOUT_MS);
|
|
1254
|
+
return () => clearTimeout(timer);
|
|
1255
|
+
}, [stage.kind, stage.kind === "confirming" ? stage.outcome : ""]);
|
|
1256
|
+
const handleJourneyRetry = async () => {
|
|
1257
|
+
if (!tryStartOperation(journeyRetryingRef)) return;
|
|
1258
|
+
setStage({ kind: "journey", url: "" });
|
|
1259
|
+
try {
|
|
1260
|
+
const refreshed = await client.retryEventBasedFaceVerificationJourney(session.token);
|
|
1261
|
+
if (refreshed.verification_url) {
|
|
1262
|
+
setStage({ kind: "journey", url: refreshed.verification_url });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
setStage({ kind: "error", message: "Unable to start a new attempt. Please try again." });
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
setStage({
|
|
1268
|
+
kind: "error",
|
|
1269
|
+
message: err instanceof Error ? err.message : "Unable to start a new attempt. Please try again."
|
|
1270
|
+
});
|
|
1271
|
+
} finally {
|
|
1272
|
+
releaseOperation(journeyRetryingRef);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1082
1275
|
const applyResult = (result) => {
|
|
1083
1276
|
if (result.status === "accepted") {
|
|
1084
1277
|
setStage({ kind: "accepted", result });
|
|
@@ -1206,12 +1399,23 @@ function FaceCaptureModal({
|
|
|
1206
1399
|
}, 3e3);
|
|
1207
1400
|
return () => clearInterval(interval);
|
|
1208
1401
|
}, [captureMode]);
|
|
1209
|
-
const close = (result) => {
|
|
1402
|
+
const close = React.useCallback((result) => {
|
|
1210
1403
|
cancelledRef.current = true;
|
|
1211
1404
|
if (result === null && onCancel) {
|
|
1212
1405
|
onCancel();
|
|
1213
1406
|
}
|
|
1214
1407
|
onComplete(result);
|
|
1408
|
+
}, [onCancel, onComplete]);
|
|
1409
|
+
React.useEffect(() => {
|
|
1410
|
+
if (stage.kind !== "accepted") return;
|
|
1411
|
+
const result = stage.result;
|
|
1412
|
+
const timer = setTimeout(() => close(result), ACCEPTED_AUTO_CLOSE_MS);
|
|
1413
|
+
return () => clearTimeout(timer);
|
|
1414
|
+
}, [close, stage]);
|
|
1415
|
+
const handleHeaderClose = () => {
|
|
1416
|
+
const result = "result" in stage ? stage.result : void 0;
|
|
1417
|
+
const unconfirmed = stage.kind === "confirming" || stage.kind === "unconfirmed" ? pendingJourneyResult(session.reference) : void 0;
|
|
1418
|
+
close(resultForModalClose(stage.kind, result, unconfirmed));
|
|
1215
1419
|
};
|
|
1216
1420
|
const renderChoose = () => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: circleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })))), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "How would you like to verify?"), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement(
|
|
1217
1421
|
"button",
|
|
@@ -1270,12 +1474,37 @@ function FaceCaptureModal({
|
|
|
1270
1474
|
};
|
|
1271
1475
|
const renderSubmitting = () => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Verifying your photo\u2026"), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
|
|
1272
1476
|
const renderAccepted = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: successCircleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Verified"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Your identity has been confirmed. You can continue."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Continue")));
|
|
1273
|
-
const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Retake")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
|
|
1477
|
+
const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, journeyMode ? /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, /* @__PURE__ */ React__default.default.createElement("span", null, "Try Again")) : /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Retake")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
|
|
1274
1478
|
const renderMaxAttempts = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React__default.default.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__default.default.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), result.data?.block && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "This action has been blocked for security reasons."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
|
|
1275
1479
|
const renderError = (message) => {
|
|
1276
1480
|
const sessionExpired = /session\s+expired/i.test(message);
|
|
1277
|
-
return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement(
|
|
1481
|
+
return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement(
|
|
1482
|
+
"button",
|
|
1483
|
+
{
|
|
1484
|
+
style: primaryButtonStyle,
|
|
1485
|
+
onClick: () => retryTarget(journeyMode) === "journey" ? void handleJourneyRetry() : setStage({ kind: "capture" })
|
|
1486
|
+
},
|
|
1487
|
+
"Try Again"
|
|
1488
|
+
), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
|
|
1278
1489
|
};
|
|
1490
|
+
const renderConfirming = () => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Confirming your result\u2026"), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
|
|
1491
|
+
const renderUnconfirmed = (outcome) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Still confirming"), /* @__PURE__ */ React__default.default.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__default.default.createElement("div", { style: buttonsContainerStyle }, outcome === "declined" && /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, "Try Again"), /* @__PURE__ */ React__default.default.createElement(
|
|
1492
|
+
"button",
|
|
1493
|
+
{
|
|
1494
|
+
style: outcome === "accepted" ? primaryButtonStyle : cancelButtonStyle,
|
|
1495
|
+
onClick: () => close(pendingJourneyResult(session.reference))
|
|
1496
|
+
},
|
|
1497
|
+
"Close"
|
|
1498
|
+
)));
|
|
1499
|
+
const renderJourney = (url) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, padding: JOURNEY_CONTENT_PADDING } }, url ? /* @__PURE__ */ React__default.default.createElement(
|
|
1500
|
+
"iframe",
|
|
1501
|
+
{
|
|
1502
|
+
src: url,
|
|
1503
|
+
title: "Identity verification",
|
|
1504
|
+
allow: "camera; microphone",
|
|
1505
|
+
style: journeyFrameStyle
|
|
1506
|
+
}
|
|
1507
|
+
) : /* @__PURE__ */ React__default.default.createElement("div", { style: { padding: 24, textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Preparing your next attempt\u2026")), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, textAlign: "center" } }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
|
|
1279
1508
|
const body = (() => {
|
|
1280
1509
|
switch (stage.kind) {
|
|
1281
1510
|
case "choose":
|
|
@@ -1284,6 +1513,12 @@ function FaceCaptureModal({
|
|
|
1284
1513
|
return renderQRStage(stage.mobileConnected);
|
|
1285
1514
|
case "capture":
|
|
1286
1515
|
return renderCapture(stage.declinedReason);
|
|
1516
|
+
case "journey":
|
|
1517
|
+
return renderJourney(stage.url);
|
|
1518
|
+
case "confirming":
|
|
1519
|
+
return renderConfirming();
|
|
1520
|
+
case "unconfirmed":
|
|
1521
|
+
return renderUnconfirmed(stage.outcome);
|
|
1287
1522
|
case "submitting":
|
|
1288
1523
|
return renderSubmitting();
|
|
1289
1524
|
case "accepted":
|
|
@@ -1296,10 +1531,10 @@ function FaceCaptureModal({
|
|
|
1296
1531
|
return renderError(stage.message);
|
|
1297
1532
|
}
|
|
1298
1533
|
})();
|
|
1299
|
-
return /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, /* @__PURE__ */ React__default.default.createElement("style", null, keyframes), /* @__PURE__ */ React__default.default.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: modalStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: headerStyle }, /* @__PURE__ */ React__default.default.createElement(
|
|
1534
|
+
return /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, /* @__PURE__ */ React__default.default.createElement("style", null, keyframes), /* @__PURE__ */ React__default.default.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...modalStyle, maxWidth: modalMaxWidth(journeyMode) } }, /* @__PURE__ */ React__default.default.createElement("div", { style: headerStyle }, /* @__PURE__ */ React__default.default.createElement(
|
|
1300
1535
|
"button",
|
|
1301
1536
|
{
|
|
1302
|
-
onClick:
|
|
1537
|
+
onClick: handleHeaderClose,
|
|
1303
1538
|
style: closeButtonStyle,
|
|
1304
1539
|
"aria-label": "Close"
|
|
1305
1540
|
},
|
|
@@ -1329,7 +1564,8 @@ var modalStyle = {
|
|
|
1329
1564
|
borderRadius: 16,
|
|
1330
1565
|
boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
|
|
1331
1566
|
width: "100%",
|
|
1332
|
-
|
|
1567
|
+
maxHeight: "calc(100vh - 32px)",
|
|
1568
|
+
overflowY: "auto",
|
|
1333
1569
|
animation: "zoomIn .2s ease-out"
|
|
1334
1570
|
};
|
|
1335
1571
|
var headerStyle = {
|
|
@@ -1355,6 +1591,13 @@ var contentStyle = {
|
|
|
1355
1591
|
flexDirection: "column",
|
|
1356
1592
|
gap: 16
|
|
1357
1593
|
};
|
|
1594
|
+
var journeyFrameStyle = {
|
|
1595
|
+
width: "100%",
|
|
1596
|
+
height: JOURNEY_FRAME_HEIGHT,
|
|
1597
|
+
border: 0,
|
|
1598
|
+
borderRadius: 12,
|
|
1599
|
+
background: "#f9fafb"
|
|
1600
|
+
};
|
|
1358
1601
|
var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
|
|
1359
1602
|
var visualGuideStyle = { position: "relative" };
|
|
1360
1603
|
var circleStyle = {
|
|
@@ -1621,9 +1864,10 @@ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
|
|
|
1621
1864
|
if (error) {
|
|
1622
1865
|
return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
|
|
1623
1866
|
}
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1626
|
-
const
|
|
1867
|
+
const detectionAvailable = !faceDetectionFailed;
|
|
1868
|
+
const detectionActive = ready && detectionAvailable;
|
|
1869
|
+
const liveMessage = detectionAvailable ? FACE_MESSAGES[faceStatus] : message;
|
|
1870
|
+
const liveFaceColor = detectionAvailable ? FACE_COLORS[faceStatus] : void 0;
|
|
1627
1871
|
const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
|
|
1628
1872
|
const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
|
|
1629
1873
|
const dynamicCircleStyle = {
|
|
@@ -1739,20 +1983,22 @@ var faceOverlayContainerStyle = {
|
|
|
1739
1983
|
};
|
|
1740
1984
|
var faceRingWrapStyle = {
|
|
1741
1985
|
position: "relative",
|
|
1742
|
-
|
|
1743
|
-
aspectRatio: "
|
|
1986
|
+
height: "64%",
|
|
1987
|
+
aspectRatio: "3 / 4"
|
|
1744
1988
|
};
|
|
1745
1989
|
var faceDashedRingStyle = {
|
|
1746
1990
|
position: "absolute",
|
|
1747
1991
|
inset: -6,
|
|
1748
|
-
borderRadius:
|
|
1749
|
-
border: "6px dashed rgba(255, 255, 255, 0.4)"
|
|
1750
|
-
animation: "spin 10s linear infinite"
|
|
1992
|
+
borderRadius: 9999,
|
|
1993
|
+
border: "6px dashed rgba(255, 255, 255, 0.4)"
|
|
1751
1994
|
};
|
|
1752
1995
|
var faceCircleStyle = {
|
|
1753
1996
|
position: "absolute",
|
|
1754
1997
|
inset: 0,
|
|
1755
|
-
|
|
1998
|
+
// Tailwind's `rounded-full` resolves to a very large fixed radius. On a
|
|
1999
|
+
// 3:4 box that creates the normal KYC capsule/head guide with straighter
|
|
2000
|
+
// sides; 50% would instead produce a mathematically perfect ellipse.
|
|
2001
|
+
borderRadius: 9999,
|
|
1756
2002
|
border: "4px solid #00bc7d",
|
|
1757
2003
|
boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
|
|
1758
2004
|
};
|
|
@@ -2095,6 +2341,7 @@ function isValidFileSize(file, maxSizeBytes = 10 * 1024 * 1024) {
|
|
|
2095
2341
|
}
|
|
2096
2342
|
function formatKycStatus(status) {
|
|
2097
2343
|
const statusMap = {
|
|
2344
|
+
awaiting_kyc: "Awaiting KYC",
|
|
2098
2345
|
pending: "Pending Review",
|
|
2099
2346
|
accepted: "Approved",
|
|
2100
2347
|
declined: "Rejected",
|
|
@@ -2105,6 +2352,8 @@ function formatKycStatus(status) {
|
|
|
2105
2352
|
}
|
|
2106
2353
|
function getStatusColor(status) {
|
|
2107
2354
|
const colorMap = {
|
|
2355
|
+
awaiting_kyc: "#94a3b8",
|
|
2356
|
+
// slate — not started
|
|
2108
2357
|
pending: "#f59e0b",
|
|
2109
2358
|
// amber
|
|
2110
2359
|
accepted: "#10b981",
|