vesant-sdk 1.6.6 → 1.7.0-dev.117915b
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 +14 -4
- package/dist/client-B0qhE2kr.d.mts +436 -0
- package/dist/{client-ePzhQKp9.d.mts → client-BolQlL5e.d.mts} +1 -1
- package/dist/{client-ePzhQKp9.d.ts → client-BolQlL5e.d.ts} +1 -1
- package/dist/{client-BlCxjbY2.d.mts → client-DF7hlMEz.d.ts} +18 -3
- package/dist/{client-C_A7QLcB.d.ts → client-DrjgZoH_.d.mts} +18 -3
- package/dist/client-DtH2RLuy.d.ts +436 -0
- package/dist/compliance/index.d.mts +25 -429
- package/dist/compliance/index.d.ts +25 -429
- package/dist/compliance/index.js +186 -105
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/index.mjs +186 -106
- package/dist/compliance/index.mjs.map +1 -1
- package/dist/decisions/index.d.mts +2 -2
- package/dist/decisions/index.d.ts +2 -2
- package/dist/decisions/index.js +1 -1
- package/dist/decisions/index.js.map +1 -1
- package/dist/decisions/index.mjs +1 -1
- package/dist/decisions/index.mjs.map +1 -1
- package/dist/geolocation/index.d.mts +4 -4
- package/dist/geolocation/index.d.ts +4 -4
- package/dist/geolocation/index.js +7 -24
- package/dist/geolocation/index.js.map +1 -1
- package/dist/geolocation/index.mjs +7 -24
- package/dist/geolocation/index.mjs.map +1 -1
- package/dist/index.d.mts +12 -70
- package/dist/index.d.ts +12 -70
- package/dist/index.js +305 -299
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +304 -298
- package/dist/index.mjs.map +1 -1
- package/dist/kyc/core.d.mts +4 -4
- package/dist/kyc/core.d.ts +4 -4
- package/dist/kyc/core.js +86 -27
- package/dist/kyc/core.js.map +1 -1
- package/dist/kyc/core.mjs +86 -28
- package/dist/kyc/core.mjs.map +1 -1
- package/dist/kyc/index.d.mts +309 -50
- package/dist/kyc/index.d.ts +309 -50
- package/dist/kyc/index.js +86 -27
- package/dist/kyc/index.js.map +1 -1
- package/dist/kyc/index.mjs +86 -28
- package/dist/kyc/index.mjs.map +1 -1
- package/dist/react.d.mts +48 -9
- package/dist/react.d.ts +48 -9
- package/dist/react.js +930 -276
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +929 -275
- package/dist/react.mjs.map +1 -1
- package/dist/risk-profile/index.d.mts +4 -4
- package/dist/risk-profile/index.d.ts +4 -4
- package/dist/risk-profile/index.js +1 -1
- package/dist/risk-profile/index.js.map +1 -1
- package/dist/risk-profile/index.mjs +1 -1
- package/dist/risk-profile/index.mjs.map +1 -1
- package/dist/scores/index.d.mts +2 -2
- package/dist/scores/index.d.ts +2 -2
- package/dist/scores/index.js +1 -1
- package/dist/scores/index.js.map +1 -1
- package/dist/scores/index.mjs +1 -1
- package/dist/scores/index.mjs.map +1 -1
- package/dist/tax/index.d.mts +23 -41
- package/dist/tax/index.d.ts +23 -41
- package/dist/tax/index.js +5 -37
- package/dist/tax/index.js.map +1 -1
- package/dist/tax/index.mjs +5 -37
- package/dist/tax/index.mjs.map +1 -1
- package/dist/{types-1RzYeSal.d.mts → types-BOFaMQxI.d.mts} +2 -2
- package/dist/{types-B4Ezqo7V.d.mts → types-CBQRNL-l.d.mts} +14 -1
- package/dist/{types-B4Ezqo7V.d.ts → types-CBQRNL-l.d.ts} +14 -1
- package/dist/{types-X5Md_dD_.d.ts → types-UGyDl1fd.d.ts} +2 -2
- package/dist/webhooks/index.d.mts +189 -2
- package/dist/webhooks/index.d.ts +189 -2
- package/dist/webhooks/index.js +49 -7
- package/dist/webhooks/index.js.map +1 -1
- package/dist/webhooks/index.mjs +49 -7
- package/dist/webhooks/index.mjs.map +1 -1
- package/package.json +16 -13
- package/dist/fraud/index.d.mts +0 -80
- package/dist/fraud/index.d.ts +0 -80
- package/dist/fraud/index.js +0 -606
- package/dist/fraud/index.js.map +0 -1
- package/dist/fraud/index.mjs +0 -604
- package/dist/fraud/index.mjs.map +0 -1
- package/dist/index-B04H4xfJ.d.mts +0 -320
- package/dist/index-CItMPmLL.d.ts +0 -320
package/dist/react.mjs
CHANGED
|
@@ -4,7 +4,19 @@ 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
|
+
var SDK_VERSION = "1.7.0";
|
|
8
|
+
|
|
9
|
+
// src/core/errors.ts
|
|
10
|
+
var VesantError = class _VesantError extends Error {
|
|
11
|
+
constructor(message, code, statusCode, details) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
this.details = details;
|
|
16
|
+
this.name = "VesantError";
|
|
17
|
+
Object.setPrototypeOf(this, _VesantError.prototype);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
8
20
|
|
|
9
21
|
// src/shared/browser-utils.ts
|
|
10
22
|
function generateUUID() {
|
|
@@ -194,7 +206,7 @@ function encodePayload(payload) {
|
|
|
194
206
|
} else if (typeof Buffer !== "undefined") {
|
|
195
207
|
return Buffer.from(json, "utf-8").toString("base64");
|
|
196
208
|
}
|
|
197
|
-
throw new
|
|
209
|
+
throw new VesantError("No base64 encoding method available", "BASE64_UNAVAILABLE");
|
|
198
210
|
}
|
|
199
211
|
async function generateCipherText(options, config) {
|
|
200
212
|
const warnings = [];
|
|
@@ -219,8 +231,9 @@ async function generateCipherText(options, config) {
|
|
|
219
231
|
if (location) {
|
|
220
232
|
locationData = location;
|
|
221
233
|
} else if (gpsRequiredByConfig) {
|
|
222
|
-
throw new
|
|
223
|
-
`GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied
|
|
234
|
+
throw new VesantError(
|
|
235
|
+
`GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
|
|
236
|
+
"GPS_REQUIRED"
|
|
224
237
|
);
|
|
225
238
|
} else {
|
|
226
239
|
warnings.push("GPS location not available or permission denied");
|
|
@@ -833,7 +846,7 @@ function useCustomerProfile(client, customerId, options = {}) {
|
|
|
833
846
|
profileRef.current = profile;
|
|
834
847
|
const updateProfile = useCallback(
|
|
835
848
|
async (updates) => {
|
|
836
|
-
if (!profileRef.current) throw new
|
|
849
|
+
if (!profileRef.current) throw new VesantError("Profile not loaded", "PROFILE_NOT_LOADED");
|
|
837
850
|
setLoading(true);
|
|
838
851
|
setError(null);
|
|
839
852
|
try {
|
|
@@ -869,280 +882,910 @@ function useCustomerProfile(client, customerId, options = {}) {
|
|
|
869
882
|
var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjAvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMtU1ZHLTIwMDEwOTA0L0RURC9zdmcxMC5kdGQiPg0KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4NCjxzdmcgdmVyc2lvbj0iMS4wIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgDQoJIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDY0IDY0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA2NCA2NCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBmaWxsPSIjMjMxRjIwIiBkPSJNNjAsMTBINDkuNjU2bC02LjgyOC02LjgyOEM0Mi4wNzgsMi40MjIsNDEuMDYyLDIsNDAsMkgyNGMtMS4wNjIsMC0yLjA3OCwwLjQyMi0yLjgyOCwxLjE3MkwxNC4zNDQsMTBINA0KCQljLTIuMjExLDAtNCwxLjc4OS00LDR2NDRjMCwyLjIxMSwxLjc4OSw0LDQsNGg1NmMyLjIxMSwwLDQtMS43ODksNC00VjE0QzY0LDExLjc4OSw2Mi4yMTEsMTAsNjAsMTB6IE0zMiw1MA0KCQljLTguODM2LDAtMTYtNy4xNjQtMTYtMTZzNy4xNjQtMTYsMTYtMTZzMTYsNy4xNjQsMTYsMTZTNDAuODM2LDUwLDMyLDUweiIvPg0KCTxjaXJjbGUgZmlsbD0iIzIzMUYyMCIgY3g9IjMyIiBjeT0iMzQiIHI9IjgiLz4NCjwvZz4NCjwvc3ZnPg==";
|
|
870
883
|
var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
|
|
871
884
|
var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
|
|
872
|
-
var Upload = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNyAxN0gxNy4wMU0xNS42IDE0SDE4QzE4LjkzMTkgMTQgMTkuMzk3OCAxNCAxOS43NjU0IDE0LjE1MjJDMjAuMjU1NCAxNC4zNTUyIDIwLjY0NDggMTQuNzQ0NiAyMC44NDc4IDE1LjIzNDZDMjEgMTUuNjAyMiAyMSAxNi4wNjgxIDIxIDE3QzIxIDE3LjkzMTkgMjEgMTguMzk3OCAyMC44NDc4IDE4Ljc2NTRDMjAuNjQ0OCAxOS4yNTU0IDIwLjI1NTQgMTkuNjQ0OCAxOS43NjU0IDE5Ljg0NzhDMTkuMzk3OCAyMCAxOC45MzE5IDIwIDE4IDIwSDZDNS4wNjgxMiAyMCA0LjYwMjE4IDIwIDQuMjM0NjMgMTkuODQ3OEMzLjc0NDU4IDE5LjY0NDggMy4zNTUyMyAxOS4yNTU0IDMuMTUyMjQgMTguNzY1NEMzIDE4LjM5NzggMyAxNy45MzE5IDMgMTdDMyAxNi4wNjgxIDMgMTUuNjAyMiAzLjE1MjI0IDE1LjIzNDZDMy4zNTUyMyAxNC43NDQ2IDMuNzQ0NTggMTQuMzU1MiA0LjIzNDYzIDE0LjE1MjJDNC42MDIxOCAxNCA1LjA2ODEyIDE0IDYgMTRIOC40TTEyIDE1VjRNMTIgNEwxNSA3TTEyIDRMOSA3IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+DQo8L3N2Zz4=";
|
|
873
885
|
|
|
874
|
-
// src/kyc/
|
|
875
|
-
var
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
886
|
+
// src/kyc/faceDetection.ts
|
|
887
|
+
var FACE_MESSAGES = {
|
|
888
|
+
idle: "Initializing camera...",
|
|
889
|
+
loading: "Loading face detection...",
|
|
890
|
+
scanning: "Position your face in the circle",
|
|
891
|
+
too_far: "Face too far. Please move closer to the camera",
|
|
892
|
+
too_close: "Too close. Please move back a little",
|
|
893
|
+
off_center: "Center your face in the circle",
|
|
894
|
+
ok: "Looking great! Tap the button to capture"
|
|
895
|
+
};
|
|
896
|
+
var FACE_COLORS = {
|
|
897
|
+
idle: "#9ca3af",
|
|
898
|
+
loading: "#3b82f6",
|
|
899
|
+
scanning: "#3b82f6",
|
|
900
|
+
too_far: "#ef4444",
|
|
901
|
+
too_close: "#f59e0b",
|
|
902
|
+
off_center: "#f59e0b",
|
|
903
|
+
ok: "#10b981"
|
|
882
904
|
};
|
|
905
|
+
var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
|
|
906
|
+
function mapDetectionsToFaceStatus(detections) {
|
|
907
|
+
if (detections.length === 0) return "scanning";
|
|
908
|
+
const bb = detections[0].boundingBox;
|
|
909
|
+
if (bb.width < 0.18) return "too_far";
|
|
910
|
+
if (bb.width > 0.72) return "too_close";
|
|
911
|
+
if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
|
|
912
|
+
return "off_center";
|
|
913
|
+
}
|
|
914
|
+
return "ok";
|
|
915
|
+
}
|
|
916
|
+
function isCaptureDisabled(ready, detectionActive, faceStatus) {
|
|
917
|
+
return !ready || detectionActive && faceStatus !== "ok";
|
|
918
|
+
}
|
|
919
|
+
function resolveMediaPipeBase(base) {
|
|
920
|
+
if (!base) return MEDIAPIPE_BASE;
|
|
921
|
+
return base.endsWith("/") ? base.slice(0, -1) : base;
|
|
922
|
+
}
|
|
923
|
+
var LIVENESS_SAMPLE_LIMIT = 20;
|
|
924
|
+
function buildLivenessArtifact(samples) {
|
|
925
|
+
if (samples.length === 0) return void 0;
|
|
926
|
+
return JSON.stringify({
|
|
927
|
+
version: 1,
|
|
928
|
+
source: "mediapipe-face-detection",
|
|
929
|
+
samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
|
|
933
|
+
function loadMediaPipeFaceDetection(assetBasePath) {
|
|
934
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
935
|
+
return Promise.reject(new Error("MediaPipe requires a browser environment"));
|
|
936
|
+
}
|
|
937
|
+
const w = window;
|
|
938
|
+
if (w.FaceDetection) {
|
|
939
|
+
return Promise.resolve(w.FaceDetection);
|
|
940
|
+
}
|
|
941
|
+
const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
|
|
942
|
+
const existing = mediapipeLoaderPromises.get(src);
|
|
943
|
+
if (existing) return existing;
|
|
944
|
+
const loader = new Promise((resolve, reject) => {
|
|
945
|
+
const script = document.createElement("script");
|
|
946
|
+
script.src = src;
|
|
947
|
+
script.async = true;
|
|
948
|
+
script.crossOrigin = "anonymous";
|
|
949
|
+
script.onload = () => {
|
|
950
|
+
if (w.FaceDetection) {
|
|
951
|
+
resolve(w.FaceDetection);
|
|
952
|
+
} else {
|
|
953
|
+
mediapipeLoaderPromises.delete(src);
|
|
954
|
+
reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
script.onerror = () => {
|
|
958
|
+
mediapipeLoaderPromises.delete(src);
|
|
959
|
+
reject(new Error("Failed to load MediaPipe face_detection script"));
|
|
960
|
+
};
|
|
961
|
+
document.head.appendChild(script);
|
|
962
|
+
});
|
|
963
|
+
mediapipeLoaderPromises.set(src, loader);
|
|
964
|
+
return loader;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/kyc/FaceCaptureModal.tsx
|
|
968
|
+
var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
969
|
+
var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
|
|
970
|
+
var LIVENESS_MESSAGES = [
|
|
971
|
+
"Position your face inside the circle",
|
|
972
|
+
"Make sure your face is well lit",
|
|
973
|
+
"Remove glasses or hats if possible",
|
|
974
|
+
"Look straight at the camera, then tap Capture"
|
|
975
|
+
];
|
|
976
|
+
function headerSubtitle(stageKind) {
|
|
977
|
+
switch (stageKind) {
|
|
978
|
+
case "qr":
|
|
979
|
+
return "Use your phone to continue";
|
|
980
|
+
case "accepted":
|
|
981
|
+
return "All set";
|
|
982
|
+
case "declined":
|
|
983
|
+
return "Verification Declined";
|
|
984
|
+
case "max_attempts":
|
|
985
|
+
return "We couldn't verify your identity";
|
|
986
|
+
default:
|
|
987
|
+
return "Please capture a clear photo of your face";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function defaultRenderQR(payload) {
|
|
991
|
+
const url = `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(payload)}`;
|
|
992
|
+
return /* @__PURE__ */ React.createElement("img", { src: url, alt: "QR code", width: 220, height: 220 });
|
|
993
|
+
}
|
|
883
994
|
function FaceCaptureModal({
|
|
884
|
-
|
|
885
|
-
|
|
995
|
+
client,
|
|
996
|
+
session,
|
|
997
|
+
onComplete,
|
|
998
|
+
onCancel,
|
|
999
|
+
defaultDevice,
|
|
1000
|
+
renderQR,
|
|
1001
|
+
mediapipeAssetBasePath
|
|
886
1002
|
}) {
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1003
|
+
const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
|
|
1004
|
+
const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
|
|
1005
|
+
const initialStage = (() => {
|
|
1006
|
+
if (initialChoice === "this") return { kind: "capture" };
|
|
1007
|
+
if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
|
|
1008
|
+
return { kind: "choose" };
|
|
1009
|
+
})();
|
|
1010
|
+
const hasMethodChoice = initialChoice === "ask";
|
|
1011
|
+
const [stage, setStage] = useState(initialStage);
|
|
1012
|
+
const [attempts, setAttempts] = useState(session.attempts);
|
|
1013
|
+
const maxAttempts = session.max_attempts || 1;
|
|
1014
|
+
const [qrDeclinedReason, setQrDeclinedReason] = useState(null);
|
|
1015
|
+
const [captureMode, setCaptureMode] = useState("idle");
|
|
1016
|
+
const [capturedPreview, setCapturedPreview] = useState(null);
|
|
1017
|
+
const [capturedBase64, setCapturedBase64] = useState(null);
|
|
1018
|
+
const [capturedArtifact, setCapturedArtifact] = useState(null);
|
|
1019
|
+
const [livenessMessage, setLivenessMessage] = useState(LIVENESS_MESSAGES[0]);
|
|
1020
|
+
const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
|
|
1021
|
+
const cancelledRef = useRef(false);
|
|
1022
|
+
const submittingRef = useRef(false);
|
|
1023
|
+
useEffect(() => {
|
|
1024
|
+
const timer = setTimeout(() => {
|
|
1025
|
+
setStage(
|
|
1026
|
+
(prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
|
|
1027
|
+
);
|
|
1028
|
+
}, SESSION_EXPIRY_MS);
|
|
1029
|
+
return () => clearTimeout(timer);
|
|
1030
|
+
}, [session.token]);
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (stage.kind !== "qr") return;
|
|
1033
|
+
let stopped = false;
|
|
1034
|
+
const intervalMs = 2e3;
|
|
1035
|
+
const deadline = Date.now() + SESSION_EXPIRY_MS;
|
|
1036
|
+
let mobileSeen = stage.mobileConnected;
|
|
1037
|
+
const tick = async () => {
|
|
1038
|
+
if (stopped || cancelledRef.current) return;
|
|
1039
|
+
try {
|
|
1040
|
+
if (mobileSeen) {
|
|
1041
|
+
const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
|
|
1042
|
+
if (result.status === "accepted") {
|
|
1043
|
+
setStage({ kind: "accepted", result });
|
|
1044
|
+
stopped = true;
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (result.status === "declined") {
|
|
1048
|
+
setAttempts(maxAttempts - (result.data?.retries_remaining ?? 0));
|
|
1049
|
+
if (result.data?.retry_limit_exceeded) {
|
|
1050
|
+
setStage({ kind: "max_attempts", result });
|
|
1051
|
+
stopped = true;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (result.declined_reason) {
|
|
1055
|
+
setQrDeclinedReason(result.declined_reason);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} else {
|
|
1059
|
+
const handoff = await client.getHandoffSession(session.token);
|
|
1060
|
+
if (handoff.mobile_connected) {
|
|
1061
|
+
mobileSeen = true;
|
|
1062
|
+
setStage({ kind: "qr", mobileConnected: true });
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
if (Date.now() < deadline) {
|
|
1068
|
+
setTimeout(tick, intervalMs);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
tick();
|
|
1072
|
+
return () => {
|
|
1073
|
+
stopped = true;
|
|
1074
|
+
};
|
|
1075
|
+
}, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
|
|
1076
|
+
const applyResult = (result) => {
|
|
1077
|
+
if (result.status === "accepted") {
|
|
1078
|
+
setStage({ kind: "accepted", result });
|
|
1079
|
+
return true;
|
|
905
1080
|
}
|
|
1081
|
+
if (result.status === "declined") {
|
|
1082
|
+
const nextAttempts = maxAttempts - (result.data?.retries_remaining ?? 0);
|
|
1083
|
+
setAttempts(nextAttempts);
|
|
1084
|
+
setCapturedPreview(null);
|
|
1085
|
+
setCapturedBase64(null);
|
|
1086
|
+
if (result.data?.retry_limit_exceeded) {
|
|
1087
|
+
setStage({ kind: "max_attempts", result });
|
|
1088
|
+
} else {
|
|
1089
|
+
setStage({ kind: "declined", result });
|
|
1090
|
+
}
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
return false;
|
|
906
1094
|
};
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
};
|
|
977
|
-
const badgeStyle = {
|
|
978
|
-
position: "absolute",
|
|
979
|
-
top: "-4px",
|
|
980
|
-
right: "-4px",
|
|
981
|
-
width: "32px",
|
|
982
|
-
height: "32px",
|
|
983
|
-
background: "#ffffff",
|
|
984
|
-
borderRadius: "50%",
|
|
985
|
-
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
|
986
|
-
display: "flex",
|
|
987
|
-
alignItems: "center",
|
|
988
|
-
justifyContent: "center"
|
|
989
|
-
};
|
|
990
|
-
const instructionsBoxStyle = {
|
|
991
|
-
background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
|
|
992
|
-
borderRadius: "12px",
|
|
993
|
-
padding: "16px",
|
|
994
|
-
marginBottom: "24px"
|
|
995
|
-
};
|
|
996
|
-
const instructionsTitleStyle = {
|
|
997
|
-
fontSize: "14px",
|
|
998
|
-
fontWeight: 500,
|
|
999
|
-
color: "#111827",
|
|
1000
|
-
marginBottom: "8px"
|
|
1001
|
-
};
|
|
1002
|
-
const instructionsListStyle = {
|
|
1003
|
-
fontSize: "12px",
|
|
1004
|
-
color: "#6b7280",
|
|
1005
|
-
listStyle: "none",
|
|
1006
|
-
padding: 0,
|
|
1007
|
-
margin: 0
|
|
1008
|
-
};
|
|
1009
|
-
const instructionItemStyle = {
|
|
1010
|
-
display: "flex",
|
|
1011
|
-
alignItems: "flex-start",
|
|
1012
|
-
marginBottom: "6px"
|
|
1095
|
+
const runSubmit = async (proof) => {
|
|
1096
|
+
if (submittingRef.current) return;
|
|
1097
|
+
submittingRef.current = true;
|
|
1098
|
+
setStage({ kind: "submitting" });
|
|
1099
|
+
let settled = false;
|
|
1100
|
+
const ref = session.reference;
|
|
1101
|
+
const baselineAttempts = attempts;
|
|
1102
|
+
const isFreshPollResult = (r) => {
|
|
1103
|
+
if (r.status === "accepted") return true;
|
|
1104
|
+
if (r.status === "declined") {
|
|
1105
|
+
const derivedAttempts = maxAttempts - (r.data?.retries_remaining ?? 0);
|
|
1106
|
+
return derivedAttempts > baselineAttempts;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
1109
|
+
};
|
|
1110
|
+
try {
|
|
1111
|
+
const postPromise = (async () => {
|
|
1112
|
+
try {
|
|
1113
|
+
const data = await client.submitEventBasedFaceVerificationSession({
|
|
1114
|
+
token: session.token,
|
|
1115
|
+
reference: ref,
|
|
1116
|
+
proof,
|
|
1117
|
+
// undefined when detection wasn't running for this capture —
|
|
1118
|
+
// JSON.stringify drops it, so the field is absent on the wire.
|
|
1119
|
+
liveness_artifact: capturedArtifact ?? void 0
|
|
1120
|
+
});
|
|
1121
|
+
return { ok: true, data };
|
|
1122
|
+
} catch (err2) {
|
|
1123
|
+
return { ok: false, error: err2 };
|
|
1124
|
+
}
|
|
1125
|
+
})();
|
|
1126
|
+
const pollPromise = (async () => {
|
|
1127
|
+
await new Promise((r) => setTimeout(r, 12e3));
|
|
1128
|
+
const deadline = Date.now() + 12e4;
|
|
1129
|
+
while (Date.now() < deadline) {
|
|
1130
|
+
if (settled || cancelledRef.current) return null;
|
|
1131
|
+
try {
|
|
1132
|
+
const r = await client.getEventBasedFaceVerificationSessionStatus(session.token);
|
|
1133
|
+
if (isFreshPollResult(r)) return r;
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
if (settled || cancelledRef.current) return null;
|
|
1137
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
})();
|
|
1141
|
+
const winner = await Promise.race([
|
|
1142
|
+
postPromise.then(
|
|
1143
|
+
(r) => r.ok ? { from: "post", data: r.data } : { from: "post_err" }
|
|
1144
|
+
),
|
|
1145
|
+
pollPromise.then(
|
|
1146
|
+
(data) => data ? { from: "poll", data } : { from: "poll_done" }
|
|
1147
|
+
)
|
|
1148
|
+
]);
|
|
1149
|
+
settled = true;
|
|
1150
|
+
if (winner.from === "post" && applyResult(winner.data)) return;
|
|
1151
|
+
if (winner.from === "poll" && applyResult(winner.data)) return;
|
|
1152
|
+
const [postResult, pollResult] = await Promise.all([postPromise, pollPromise]);
|
|
1153
|
+
if (postResult.ok && applyResult(postResult.data)) return;
|
|
1154
|
+
if (pollResult && applyResult(pollResult)) return;
|
|
1155
|
+
const err = postResult.ok ? null : postResult.error;
|
|
1156
|
+
setStage({
|
|
1157
|
+
kind: "error",
|
|
1158
|
+
message: err instanceof Error ? err.message : "Submission failed. Please try again."
|
|
1159
|
+
});
|
|
1160
|
+
} finally {
|
|
1161
|
+
settled = true;
|
|
1162
|
+
submittingRef.current = false;
|
|
1163
|
+
}
|
|
1013
1164
|
};
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1165
|
+
const handleCameraCapture = (dataUrl, livenessArtifact) => {
|
|
1166
|
+
const raw = dataUrl.split(",")[1] ?? "";
|
|
1167
|
+
setCapturedPreview(dataUrl);
|
|
1168
|
+
setCapturedBase64(raw);
|
|
1169
|
+
setCapturedArtifact(livenessArtifact ?? null);
|
|
1170
|
+
setCaptureMode("preview");
|
|
1018
1171
|
};
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1172
|
+
const handleCameraCancel = () => setCaptureMode("idle");
|
|
1173
|
+
const handleRetake = () => {
|
|
1174
|
+
setCapturedPreview(null);
|
|
1175
|
+
setCapturedBase64(null);
|
|
1176
|
+
setCapturedArtifact(null);
|
|
1177
|
+
setCaptureMode("live");
|
|
1023
1178
|
};
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
color: "#ffffff",
|
|
1028
|
-
fontWeight: 500,
|
|
1029
|
-
padding: "14px 24px",
|
|
1030
|
-
borderRadius: "12px",
|
|
1031
|
-
border: "none",
|
|
1032
|
-
cursor: isProcessing ? "not-allowed" : "pointer",
|
|
1033
|
-
transition: "all 0.2s",
|
|
1034
|
-
display: "flex",
|
|
1035
|
-
alignItems: "center",
|
|
1036
|
-
justifyContent: "center",
|
|
1037
|
-
gap: "12px",
|
|
1038
|
-
boxShadow: isProcessing ? "none" : isHoveringPrimary ? "0 20px 25px -5px rgba(0, 188, 125, 0.3)" : "0 10px 15px -3px rgba(0, 188, 125, 0.2)",
|
|
1039
|
-
fontSize: "16px"
|
|
1179
|
+
const handleConfirmSubmit = () => {
|
|
1180
|
+
if (!capturedBase64) return;
|
|
1181
|
+
void runSubmit(capturedBase64);
|
|
1040
1182
|
};
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
color: "#374151",
|
|
1045
|
-
fontWeight: 500,
|
|
1046
|
-
padding: "14px 24px",
|
|
1047
|
-
borderRadius: "12px",
|
|
1048
|
-
border: "2px solid #e5e7eb",
|
|
1049
|
-
cursor: isProcessing ? "not-allowed" : "pointer",
|
|
1050
|
-
transition: "all 0.2s",
|
|
1051
|
-
fontSize: "16px"
|
|
1183
|
+
const handleOpenCamera = () => {
|
|
1184
|
+
if (submittingRef.current) return;
|
|
1185
|
+
setCaptureMode("live");
|
|
1052
1186
|
};
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1187
|
+
const handleRetakeAfterDecline = () => {
|
|
1188
|
+
setCapturedPreview(null);
|
|
1189
|
+
setCapturedBase64(null);
|
|
1190
|
+
setCaptureMode("live");
|
|
1191
|
+
setStage({ kind: "capture" });
|
|
1055
1192
|
};
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1193
|
+
useEffect(() => {
|
|
1194
|
+
if (captureMode !== "live") return;
|
|
1195
|
+
setLivenessMessage(LIVENESS_MESSAGES[0]);
|
|
1196
|
+
let i = 0;
|
|
1197
|
+
const interval = setInterval(() => {
|
|
1198
|
+
i = (i + 1) % LIVENESS_MESSAGES.length;
|
|
1199
|
+
setLivenessMessage(LIVENESS_MESSAGES[i]);
|
|
1200
|
+
}, 3e3);
|
|
1201
|
+
return () => clearInterval(interval);
|
|
1202
|
+
}, [captureMode]);
|
|
1203
|
+
const close = (result) => {
|
|
1204
|
+
cancelledRef.current = true;
|
|
1205
|
+
if (result === null && onCancel) {
|
|
1206
|
+
onCancel();
|
|
1207
|
+
}
|
|
1208
|
+
onComplete(result);
|
|
1061
1209
|
};
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1210
|
+
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
|
+
"button",
|
|
1212
|
+
{
|
|
1213
|
+
style: primaryButtonStyle,
|
|
1214
|
+
onClick: () => setStage({ kind: "capture" })
|
|
1215
|
+
},
|
|
1216
|
+
/* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }),
|
|
1217
|
+
/* @__PURE__ */ React.createElement("span", null, "Continue on this device")
|
|
1218
|
+
), /* @__PURE__ */ React.createElement(
|
|
1219
|
+
"button",
|
|
1220
|
+
{
|
|
1221
|
+
style: secondaryButtonStyle,
|
|
1222
|
+
onClick: () => setStage({ kind: "qr", mobileConnected: false })
|
|
1223
|
+
},
|
|
1224
|
+
"Continue on mobile (scan QR)"
|
|
1225
|
+
), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
|
|
1226
|
+
const renderQRStage = (mobileConnected) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: qrBoxStyle }, (renderQR ?? defaultRenderQR)(qrPayload)), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, mobileConnected ? "Phone connected. Complete the capture on your mobile device\u2026" : "Scan this QR with your phone camera, then complete the face capture there."), mobileConnected && /* @__PURE__ */ React.createElement("div", { style: { ...spinnerStyle, margin: "12px auto" } }), qrDeclinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Last attempt was declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, qrDeclinedReason), /* @__PURE__ */ React.createElement("p", { style: { ...alertTextStyle, marginTop: 4 } }, "Try again on your phone. Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts, ".")), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement(
|
|
1227
|
+
"button",
|
|
1228
|
+
{
|
|
1229
|
+
style: secondaryButtonStyle,
|
|
1230
|
+
onClick: () => setStage({ kind: "capture" })
|
|
1231
|
+
},
|
|
1232
|
+
"Use this device instead"
|
|
1233
|
+
), hasMethodChoice && /* @__PURE__ */ React.createElement(
|
|
1234
|
+
"button",
|
|
1235
|
+
{
|
|
1236
|
+
style: secondaryButtonStyle,
|
|
1237
|
+
onClick: () => setStage({ kind: "choose" })
|
|
1238
|
+
},
|
|
1239
|
+
"Change verification method"
|
|
1240
|
+
), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
|
|
1241
|
+
const renderCapture = (declinedReason) => {
|
|
1242
|
+
if (captureMode === "live") {
|
|
1243
|
+
return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement(
|
|
1244
|
+
LiveCamera,
|
|
1245
|
+
{
|
|
1246
|
+
onCapture: handleCameraCapture,
|
|
1247
|
+
onCancel: handleCameraCancel,
|
|
1248
|
+
message: livenessMessage,
|
|
1249
|
+
assetBasePath: mediapipeAssetBasePath
|
|
1250
|
+
}
|
|
1251
|
+
), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
|
|
1252
|
+
}
|
|
1253
|
+
if (captureMode === "preview" && capturedPreview) {
|
|
1254
|
+
return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Looks good? Submit this selfie or retake it."), /* @__PURE__ */ React.createElement("div", { style: previewBoxStyle }, /* @__PURE__ */ React.createElement("img", { src: capturedPreview, alt: "Captured selfie preview", style: previewImgStyle })), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleConfirmSubmit }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, "Submit")), /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: handleRetake }, "Retake"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
|
|
1255
|
+
}
|
|
1256
|
+
return /* @__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("div", { style: badgeStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, declinedReason ? "Try Again" : "Open Camera")), hasMethodChoice && /* @__PURE__ */ React.createElement(
|
|
1257
|
+
"button",
|
|
1258
|
+
{
|
|
1259
|
+
style: secondaryButtonStyle,
|
|
1260
|
+
onClick: () => setStage({ kind: "choose" })
|
|
1261
|
+
},
|
|
1262
|
+
"Change verification method"
|
|
1263
|
+
), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
|
|
1069
1264
|
};
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1265
|
+
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
|
+
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));
|
|
1268
|
+
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
|
+
const renderError = (message) => {
|
|
1270
|
+
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("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try Again"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
|
|
1072
1272
|
};
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
@keyframes spin {
|
|
1095
|
-
from {
|
|
1096
|
-
transform: rotate(0deg);
|
|
1097
|
-
}
|
|
1098
|
-
to {
|
|
1099
|
-
transform: rotate(360deg);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
`);
|
|
1103
|
-
return /* @__PURE__ */ React.createElement(React.Fragment, null, styleTag, /* @__PURE__ */ React.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React.createElement("div", { style: modalStyle }, /* @__PURE__ */ React.createElement("div", { style: headerStyle }, /* @__PURE__ */ React.createElement(
|
|
1273
|
+
const body = (() => {
|
|
1274
|
+
switch (stage.kind) {
|
|
1275
|
+
case "choose":
|
|
1276
|
+
return renderChoose();
|
|
1277
|
+
case "qr":
|
|
1278
|
+
return renderQRStage(stage.mobileConnected);
|
|
1279
|
+
case "capture":
|
|
1280
|
+
return renderCapture(stage.declinedReason);
|
|
1281
|
+
case "submitting":
|
|
1282
|
+
return renderSubmitting();
|
|
1283
|
+
case "accepted":
|
|
1284
|
+
return renderAccepted(stage.result);
|
|
1285
|
+
case "declined":
|
|
1286
|
+
return renderDeclined(stage.result);
|
|
1287
|
+
case "max_attempts":
|
|
1288
|
+
return renderMaxAttempts(stage.result);
|
|
1289
|
+
case "error":
|
|
1290
|
+
return renderError(stage.message);
|
|
1291
|
+
}
|
|
1292
|
+
})();
|
|
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(
|
|
1104
1294
|
"button",
|
|
1105
1295
|
{
|
|
1106
|
-
onClick:
|
|
1107
|
-
onMouseEnter: () => setIsHoveringClose(true),
|
|
1108
|
-
onMouseLeave: () => setIsHoveringClose(false),
|
|
1296
|
+
onClick: () => close(null),
|
|
1109
1297
|
style: closeButtonStyle,
|
|
1110
1298
|
"aria-label": "Close"
|
|
1111
1299
|
},
|
|
1112
|
-
/* @__PURE__ */ React.createElement("img", { src: Close, alt: "
|
|
1113
|
-
), /* @__PURE__ */ React.createElement("h2", { style: titleStyle }, "Face Verification"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle },
|
|
1114
|
-
|
|
1300
|
+
/* @__PURE__ */ React.createElement("img", { src: Close, alt: "", width: 16, height: 16 })
|
|
1301
|
+
), /* @__PURE__ */ React.createElement("h2", { style: titleStyle }, "Face Verification"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, headerSubtitle(stage.kind))), body, /* @__PURE__ */ React.createElement("div", { style: footerStyle }, /* @__PURE__ */ React.createElement("p", { style: footerTextStyle }, "Your photo is securely processed and used only for verification.")))));
|
|
1302
|
+
}
|
|
1303
|
+
var keyframes = `
|
|
1304
|
+
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
|
1305
|
+
@keyframes zoomIn { from { transform: scale(.96); opacity: 0 } to { transform: scale(1); opacity: 1 } }
|
|
1306
|
+
@keyframes spin { to { transform: rotate(360deg) } }
|
|
1307
|
+
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }
|
|
1308
|
+
`;
|
|
1309
|
+
var overlayStyle = {
|
|
1310
|
+
position: "fixed",
|
|
1311
|
+
inset: 0,
|
|
1312
|
+
background: "rgba(0,0,0,.6)",
|
|
1313
|
+
backdropFilter: "blur(4px)",
|
|
1314
|
+
display: "flex",
|
|
1315
|
+
alignItems: "center",
|
|
1316
|
+
justifyContent: "center",
|
|
1317
|
+
zIndex: 999999,
|
|
1318
|
+
padding: 16,
|
|
1319
|
+
animation: "fadeIn .2s ease-out"
|
|
1320
|
+
};
|
|
1321
|
+
var modalStyle = {
|
|
1322
|
+
background: "#fff",
|
|
1323
|
+
borderRadius: 16,
|
|
1324
|
+
boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
|
|
1325
|
+
width: "100%",
|
|
1326
|
+
maxWidth: 448,
|
|
1327
|
+
animation: "zoomIn .2s ease-out"
|
|
1328
|
+
};
|
|
1329
|
+
var headerStyle = {
|
|
1330
|
+
position: "relative",
|
|
1331
|
+
padding: "24px 24px 16px",
|
|
1332
|
+
borderBottom: "1px solid #f3f4f6"
|
|
1333
|
+
};
|
|
1334
|
+
var closeButtonStyle = {
|
|
1335
|
+
position: "absolute",
|
|
1336
|
+
top: 16,
|
|
1337
|
+
right: 16,
|
|
1338
|
+
padding: 8,
|
|
1339
|
+
background: "transparent",
|
|
1340
|
+
border: "none",
|
|
1341
|
+
borderRadius: 8,
|
|
1342
|
+
cursor: "pointer"
|
|
1343
|
+
};
|
|
1344
|
+
var titleStyle = { margin: "0 0 4px", fontSize: 18, fontWeight: 600, color: "#111827" };
|
|
1345
|
+
var subtitleStyle = { margin: 0, fontSize: 14, color: "#6b7280" };
|
|
1346
|
+
var contentStyle = {
|
|
1347
|
+
padding: 24,
|
|
1348
|
+
display: "flex",
|
|
1349
|
+
flexDirection: "column",
|
|
1350
|
+
gap: 16
|
|
1351
|
+
};
|
|
1352
|
+
var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
|
|
1353
|
+
var visualGuideStyle = { position: "relative" };
|
|
1354
|
+
var circleStyle = {
|
|
1355
|
+
width: 96,
|
|
1356
|
+
height: 96,
|
|
1357
|
+
borderRadius: "50%",
|
|
1358
|
+
background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
|
|
1359
|
+
display: "flex",
|
|
1360
|
+
alignItems: "center",
|
|
1361
|
+
justifyContent: "center"
|
|
1362
|
+
};
|
|
1363
|
+
var badgeStyle = {
|
|
1364
|
+
position: "absolute",
|
|
1365
|
+
bottom: -4,
|
|
1366
|
+
right: -4,
|
|
1367
|
+
width: 28,
|
|
1368
|
+
height: 28,
|
|
1369
|
+
borderRadius: "50%",
|
|
1370
|
+
background: "#ffffff",
|
|
1371
|
+
border: "2px solid #00bc7d",
|
|
1372
|
+
display: "flex",
|
|
1373
|
+
alignItems: "center",
|
|
1374
|
+
justifyContent: "center"
|
|
1375
|
+
};
|
|
1376
|
+
var successCircleStyle = {
|
|
1377
|
+
...circleStyle,
|
|
1378
|
+
background: "linear-gradient(135deg, rgba(0, 188, 125, 0.25) 0%, rgba(0, 188, 125, 0.1) 100%)",
|
|
1379
|
+
margin: "0 auto"
|
|
1380
|
+
};
|
|
1381
|
+
var errorCircleStyle = {
|
|
1382
|
+
width: 64,
|
|
1383
|
+
height: 64,
|
|
1384
|
+
borderRadius: "50%",
|
|
1385
|
+
background: "#fee2e2",
|
|
1386
|
+
color: "#b91c1c",
|
|
1387
|
+
fontSize: 32,
|
|
1388
|
+
fontWeight: 700,
|
|
1389
|
+
display: "flex",
|
|
1390
|
+
alignItems: "center",
|
|
1391
|
+
justifyContent: "center",
|
|
1392
|
+
margin: "0 auto"
|
|
1393
|
+
};
|
|
1394
|
+
var qrBoxStyle = {
|
|
1395
|
+
display: "flex",
|
|
1396
|
+
justifyContent: "center",
|
|
1397
|
+
padding: 12,
|
|
1398
|
+
background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
|
|
1399
|
+
borderRadius: 12
|
|
1400
|
+
};
|
|
1401
|
+
var alertBoxStyle = {
|
|
1402
|
+
background: "#fef3c7",
|
|
1403
|
+
border: "1px solid #fde68a",
|
|
1404
|
+
color: "#92400e",
|
|
1405
|
+
borderRadius: 8,
|
|
1406
|
+
padding: 12,
|
|
1407
|
+
fontSize: 14
|
|
1408
|
+
};
|
|
1409
|
+
var alertTextStyle = { margin: "4px 0 0", fontSize: 13, color: "#92400e" };
|
|
1410
|
+
var instructionsBoxStyle = {
|
|
1411
|
+
background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
|
|
1412
|
+
borderRadius: 8,
|
|
1413
|
+
padding: 16
|
|
1414
|
+
};
|
|
1415
|
+
var instructionsTitleStyle = { margin: "0 0 8px", fontSize: 14, fontWeight: 600, color: "#374151" };
|
|
1416
|
+
var instructionsListStyle = { margin: 0, padding: 0, listStyle: "none" };
|
|
1417
|
+
var instructionItemStyle = { display: "flex", gap: 8, fontSize: 13, color: "#4b5563", lineHeight: 1.5 };
|
|
1418
|
+
var bulletStyle = { color: "#00bc7d" };
|
|
1419
|
+
var previewBoxStyle = {
|
|
1420
|
+
borderRadius: 12,
|
|
1421
|
+
overflow: "hidden",
|
|
1422
|
+
border: "2px solid rgba(0, 188, 125, 0.4)",
|
|
1423
|
+
background: "#0f172a",
|
|
1424
|
+
display: "flex",
|
|
1425
|
+
justifyContent: "center"
|
|
1426
|
+
};
|
|
1427
|
+
var previewImgStyle = {
|
|
1428
|
+
width: "100%",
|
|
1429
|
+
maxHeight: 320,
|
|
1430
|
+
objectFit: "contain",
|
|
1431
|
+
background: "#0f172a"
|
|
1432
|
+
};
|
|
1433
|
+
var buttonsContainerStyle = {
|
|
1434
|
+
display: "flex",
|
|
1435
|
+
flexDirection: "column",
|
|
1436
|
+
gap: 8
|
|
1437
|
+
};
|
|
1438
|
+
var primaryButtonStyle = {
|
|
1439
|
+
display: "flex",
|
|
1440
|
+
alignItems: "center",
|
|
1441
|
+
justifyContent: "center",
|
|
1442
|
+
gap: 8,
|
|
1443
|
+
padding: "12px 16px",
|
|
1444
|
+
background: "#00bc7d",
|
|
1445
|
+
color: "#ffffff",
|
|
1446
|
+
border: "none",
|
|
1447
|
+
borderRadius: 8,
|
|
1448
|
+
fontSize: 14,
|
|
1449
|
+
fontWeight: 600,
|
|
1450
|
+
cursor: "pointer"
|
|
1451
|
+
};
|
|
1452
|
+
var secondaryButtonStyle = {
|
|
1453
|
+
...primaryButtonStyle,
|
|
1454
|
+
background: "#ffffff",
|
|
1455
|
+
color: "#374151",
|
|
1456
|
+
border: "2px solid #e5e7eb"
|
|
1457
|
+
};
|
|
1458
|
+
var cancelButtonStyle = {
|
|
1459
|
+
...primaryButtonStyle,
|
|
1460
|
+
background: "transparent",
|
|
1461
|
+
color: "#6b7280",
|
|
1462
|
+
border: "none"
|
|
1463
|
+
};
|
|
1464
|
+
var spinnerStyle = {
|
|
1465
|
+
width: 16,
|
|
1466
|
+
height: 16,
|
|
1467
|
+
border: "2px solid rgba(0, 188, 125, 0.25)",
|
|
1468
|
+
borderTopColor: "#00bc7d",
|
|
1469
|
+
borderRadius: "50%",
|
|
1470
|
+
animation: "spin .8s linear infinite"
|
|
1471
|
+
};
|
|
1472
|
+
var attemptsTextStyle = {
|
|
1473
|
+
margin: "8px 0 0",
|
|
1474
|
+
fontSize: 12,
|
|
1475
|
+
color: "#9ca3af",
|
|
1476
|
+
textAlign: "center"
|
|
1477
|
+
};
|
|
1478
|
+
var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
|
|
1479
|
+
var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
|
|
1480
|
+
function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
|
|
1481
|
+
const videoRef = useRef(null);
|
|
1482
|
+
const streamRef = useRef(null);
|
|
1483
|
+
const samplesRef = useRef([]);
|
|
1484
|
+
const [ready, setReady] = useState(false);
|
|
1485
|
+
const [error, setError] = useState(null);
|
|
1486
|
+
const [faceStatus, setFaceStatus] = useState("idle");
|
|
1487
|
+
const [faceDetectionFailed, setFaceDetectionFailed] = useState(false);
|
|
1488
|
+
useEffect(() => {
|
|
1489
|
+
let cancelled = false;
|
|
1490
|
+
async function init() {
|
|
1491
|
+
try {
|
|
1492
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
|
|
1493
|
+
throw new Error("Camera is not available in this browser");
|
|
1494
|
+
}
|
|
1495
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1496
|
+
video: {
|
|
1497
|
+
facingMode: "user",
|
|
1498
|
+
width: { ideal: 1280 },
|
|
1499
|
+
height: { ideal: 1280 }
|
|
1500
|
+
},
|
|
1501
|
+
audio: false
|
|
1502
|
+
});
|
|
1503
|
+
if (cancelled) {
|
|
1504
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
streamRef.current = stream;
|
|
1508
|
+
const video = videoRef.current;
|
|
1509
|
+
if (video) {
|
|
1510
|
+
video.srcObject = stream;
|
|
1511
|
+
video.onloadedmetadata = () => {
|
|
1512
|
+
video.play().catch(() => {
|
|
1513
|
+
});
|
|
1514
|
+
setReady(true);
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
setError(err instanceof Error ? err.message : "Unable to access camera");
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
init();
|
|
1522
|
+
return () => {
|
|
1523
|
+
cancelled = true;
|
|
1524
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1525
|
+
streamRef.current = null;
|
|
1526
|
+
};
|
|
1527
|
+
}, []);
|
|
1528
|
+
useEffect(() => {
|
|
1529
|
+
if (!ready) {
|
|
1530
|
+
setFaceStatus("idle");
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
let cancelled = false;
|
|
1534
|
+
let rafId = null;
|
|
1535
|
+
let detector = null;
|
|
1536
|
+
setFaceStatus("loading");
|
|
1537
|
+
setFaceDetectionFailed(false);
|
|
1538
|
+
samplesRef.current = [];
|
|
1539
|
+
(async () => {
|
|
1540
|
+
try {
|
|
1541
|
+
const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
|
|
1542
|
+
if (cancelled) return;
|
|
1543
|
+
const assetBase = resolveMediaPipeBase(assetBasePath);
|
|
1544
|
+
const d = new FaceDetection({
|
|
1545
|
+
locateFile: (f) => `${assetBase}/${f}`
|
|
1546
|
+
});
|
|
1547
|
+
d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
|
|
1548
|
+
d.onResults((results) => {
|
|
1549
|
+
if (cancelled) return;
|
|
1550
|
+
const detections = results.detections ?? [];
|
|
1551
|
+
const status = mapDetectionsToFaceStatus(detections);
|
|
1552
|
+
samplesRef.current.push({
|
|
1553
|
+
t: Date.now(),
|
|
1554
|
+
status,
|
|
1555
|
+
...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
|
|
1556
|
+
});
|
|
1557
|
+
if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
|
|
1558
|
+
samplesRef.current.shift();
|
|
1559
|
+
}
|
|
1560
|
+
setFaceStatus(status);
|
|
1561
|
+
});
|
|
1562
|
+
await d.initialize();
|
|
1563
|
+
if (cancelled) return;
|
|
1564
|
+
detector = d;
|
|
1565
|
+
setFaceStatus("scanning");
|
|
1566
|
+
const tick = async () => {
|
|
1567
|
+
if (cancelled) return;
|
|
1568
|
+
const v = videoRef.current;
|
|
1569
|
+
if (v?.readyState === 4 && detector) {
|
|
1570
|
+
try {
|
|
1571
|
+
await detector.send({ image: v });
|
|
1572
|
+
} catch {
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (!cancelled) {
|
|
1576
|
+
rafId = requestAnimationFrame(tick);
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
tick();
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
console.error("Face detection failed to initialize:", err);
|
|
1582
|
+
if (!cancelled) {
|
|
1583
|
+
setFaceDetectionFailed(true);
|
|
1584
|
+
setFaceStatus("scanning");
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
})();
|
|
1588
|
+
return () => {
|
|
1589
|
+
cancelled = true;
|
|
1590
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
1591
|
+
try {
|
|
1592
|
+
detector?.close?.();
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}, [ready, assetBasePath]);
|
|
1597
|
+
const capture = () => {
|
|
1598
|
+
const video = videoRef.current;
|
|
1599
|
+
if (!video || !video.videoWidth) return;
|
|
1600
|
+
const size = Math.min(video.videoWidth, video.videoHeight);
|
|
1601
|
+
const sx = (video.videoWidth - size) / 2;
|
|
1602
|
+
const sy = (video.videoHeight - size) / 2;
|
|
1603
|
+
const canvas = document.createElement("canvas");
|
|
1604
|
+
canvas.width = size;
|
|
1605
|
+
canvas.height = size;
|
|
1606
|
+
const ctx = canvas.getContext("2d");
|
|
1607
|
+
if (!ctx) return;
|
|
1608
|
+
ctx.translate(size, 0);
|
|
1609
|
+
ctx.scale(-1, 1);
|
|
1610
|
+
ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
|
|
1611
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1612
|
+
streamRef.current = null;
|
|
1613
|
+
onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
|
|
1614
|
+
};
|
|
1615
|
+
if (error) {
|
|
1616
|
+
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
|
+
}
|
|
1618
|
+
const detectionActive = ready && !faceDetectionFailed;
|
|
1619
|
+
const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
|
|
1620
|
+
const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
|
|
1621
|
+
const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
|
|
1622
|
+
const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
|
|
1623
|
+
const dynamicCircleStyle = {
|
|
1624
|
+
...faceCircleStyle,
|
|
1625
|
+
...liveFaceColor ? { borderColor: liveFaceColor } : null,
|
|
1626
|
+
...ringPulse ? { animation: ringPulse } : null
|
|
1627
|
+
};
|
|
1628
|
+
return /* @__PURE__ */ React.createElement("div", { style: videoFrameStyle }, /* @__PURE__ */ React.createElement(
|
|
1629
|
+
"video",
|
|
1115
1630
|
{
|
|
1116
|
-
ref:
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
style:
|
|
1121
|
-
onChange: handleFileChange,
|
|
1122
|
-
disabled: isProcessing
|
|
1631
|
+
ref: videoRef,
|
|
1632
|
+
playsInline: true,
|
|
1633
|
+
muted: true,
|
|
1634
|
+
autoPlay: true,
|
|
1635
|
+
style: videoStyle
|
|
1123
1636
|
}
|
|
1124
|
-
), /* @__PURE__ */ React.createElement("div", { style:
|
|
1637
|
+
), /* @__PURE__ */ React.createElement("div", { style: faceOverlayContainerStyle, "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("div", { style: faceRingWrapStyle }, /* @__PURE__ */ React.createElement("div", { style: faceDashedRingStyle }), /* @__PURE__ */ React.createElement("div", { style: dynamicCircleStyle }))), liveMessage && /* @__PURE__ */ React.createElement("div", { style: liveMessagePillContainerStyle }, /* @__PURE__ */ React.createElement("div", { style: liveMessagePillStyle }, /* @__PURE__ */ React.createElement("span", { style: liveMessagePillTextStyle }, liveMessage))), /* @__PURE__ */ React.createElement(
|
|
1125
1638
|
"button",
|
|
1126
1639
|
{
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
style: primaryButtonStyle
|
|
1640
|
+
type: "button",
|
|
1641
|
+
onClick: onCancel,
|
|
1642
|
+
"aria-label": "Close camera",
|
|
1643
|
+
style: overlayCloseButtonStyle
|
|
1132
1644
|
},
|
|
1133
|
-
|
|
1134
|
-
), /* @__PURE__ */ React.createElement(
|
|
1645
|
+
/* @__PURE__ */ React.createElement("img", { src: Close, alt: "", width: 16, height: 16 })
|
|
1646
|
+
), !ready && /* @__PURE__ */ React.createElement("div", { style: videoLoadingStyle }, "Starting camera\u2026"), /* @__PURE__ */ React.createElement("div", { style: bottomGradientStyle }, /* @__PURE__ */ React.createElement(
|
|
1135
1647
|
"button",
|
|
1136
1648
|
{
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
style:
|
|
1649
|
+
type: "button",
|
|
1650
|
+
onClick: capture,
|
|
1651
|
+
disabled: captureDisabled,
|
|
1652
|
+
"aria-label": "Capture",
|
|
1653
|
+
style: captureDisabled ? { ...shutterButtonStyle, opacity: 0.5, cursor: "not-allowed" } : shutterButtonStyle
|
|
1142
1654
|
},
|
|
1143
|
-
"
|
|
1144
|
-
)))
|
|
1655
|
+
/* @__PURE__ */ React.createElement("span", { style: shutterInnerStyle })
|
|
1656
|
+
)));
|
|
1145
1657
|
}
|
|
1658
|
+
var videoFrameStyle = {
|
|
1659
|
+
position: "relative",
|
|
1660
|
+
width: "100%",
|
|
1661
|
+
aspectRatio: "3 / 4",
|
|
1662
|
+
borderRadius: 24,
|
|
1663
|
+
overflow: "hidden",
|
|
1664
|
+
background: "#000000",
|
|
1665
|
+
border: "4px solid rgba(255, 255, 255, 0.08)",
|
|
1666
|
+
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)"
|
|
1667
|
+
};
|
|
1668
|
+
var overlayCloseButtonStyle = {
|
|
1669
|
+
position: "absolute",
|
|
1670
|
+
top: 12,
|
|
1671
|
+
right: 12,
|
|
1672
|
+
width: 32,
|
|
1673
|
+
height: 32,
|
|
1674
|
+
display: "inline-flex",
|
|
1675
|
+
alignItems: "center",
|
|
1676
|
+
justifyContent: "center",
|
|
1677
|
+
padding: 0,
|
|
1678
|
+
border: "none",
|
|
1679
|
+
borderRadius: "50%",
|
|
1680
|
+
background: "rgba(0, 0, 0, 0.4)",
|
|
1681
|
+
backdropFilter: "blur(6px)",
|
|
1682
|
+
color: "#ffffff",
|
|
1683
|
+
cursor: "pointer",
|
|
1684
|
+
zIndex: 10
|
|
1685
|
+
};
|
|
1686
|
+
var bottomGradientStyle = {
|
|
1687
|
+
position: "absolute",
|
|
1688
|
+
bottom: 0,
|
|
1689
|
+
left: 0,
|
|
1690
|
+
right: 0,
|
|
1691
|
+
padding: "16px",
|
|
1692
|
+
background: "linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%)",
|
|
1693
|
+
display: "flex",
|
|
1694
|
+
alignItems: "center",
|
|
1695
|
+
justifyContent: "center",
|
|
1696
|
+
zIndex: 10
|
|
1697
|
+
};
|
|
1698
|
+
var shutterButtonStyle = {
|
|
1699
|
+
width: 64,
|
|
1700
|
+
height: 64,
|
|
1701
|
+
borderRadius: "50%",
|
|
1702
|
+
background: "rgba(255, 255, 255, 0.15)",
|
|
1703
|
+
backdropFilter: "blur(6px)",
|
|
1704
|
+
border: "2px solid rgba(255, 255, 255, 0.7)",
|
|
1705
|
+
display: "inline-flex",
|
|
1706
|
+
alignItems: "center",
|
|
1707
|
+
justifyContent: "center",
|
|
1708
|
+
cursor: "pointer",
|
|
1709
|
+
padding: 0,
|
|
1710
|
+
transition: "transform 120ms ease-out, background 120ms"
|
|
1711
|
+
};
|
|
1712
|
+
var shutterInnerStyle = {
|
|
1713
|
+
display: "block",
|
|
1714
|
+
width: 44,
|
|
1715
|
+
height: 44,
|
|
1716
|
+
borderRadius: "50%",
|
|
1717
|
+
background: "rgba(255, 255, 255, 0.85)"
|
|
1718
|
+
};
|
|
1719
|
+
var videoStyle = {
|
|
1720
|
+
width: "100%",
|
|
1721
|
+
height: "100%",
|
|
1722
|
+
objectFit: "cover",
|
|
1723
|
+
transform: "scaleX(-1)"
|
|
1724
|
+
// mirror so the preview matches the user's POV
|
|
1725
|
+
};
|
|
1726
|
+
var faceOverlayContainerStyle = {
|
|
1727
|
+
position: "absolute",
|
|
1728
|
+
inset: 0,
|
|
1729
|
+
pointerEvents: "none",
|
|
1730
|
+
display: "flex",
|
|
1731
|
+
alignItems: "center",
|
|
1732
|
+
justifyContent: "center"
|
|
1733
|
+
};
|
|
1734
|
+
var faceRingWrapStyle = {
|
|
1735
|
+
position: "relative",
|
|
1736
|
+
width: "72%",
|
|
1737
|
+
aspectRatio: "1 / 1"
|
|
1738
|
+
};
|
|
1739
|
+
var faceDashedRingStyle = {
|
|
1740
|
+
position: "absolute",
|
|
1741
|
+
inset: -6,
|
|
1742
|
+
borderRadius: "50%",
|
|
1743
|
+
border: "6px dashed rgba(255, 255, 255, 0.4)",
|
|
1744
|
+
animation: "spin 10s linear infinite"
|
|
1745
|
+
};
|
|
1746
|
+
var faceCircleStyle = {
|
|
1747
|
+
position: "absolute",
|
|
1748
|
+
inset: 0,
|
|
1749
|
+
borderRadius: "50%",
|
|
1750
|
+
border: "4px solid #00bc7d",
|
|
1751
|
+
boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
|
|
1752
|
+
};
|
|
1753
|
+
var videoLoadingStyle = {
|
|
1754
|
+
position: "absolute",
|
|
1755
|
+
inset: 0,
|
|
1756
|
+
display: "flex",
|
|
1757
|
+
alignItems: "center",
|
|
1758
|
+
justifyContent: "center",
|
|
1759
|
+
color: "#e5e7eb",
|
|
1760
|
+
fontSize: 14,
|
|
1761
|
+
fontWeight: 500,
|
|
1762
|
+
background: "rgba(0, 0, 0, 0.4)",
|
|
1763
|
+
zIndex: 5
|
|
1764
|
+
};
|
|
1765
|
+
var liveMessagePillContainerStyle = {
|
|
1766
|
+
position: "absolute",
|
|
1767
|
+
top: 12,
|
|
1768
|
+
left: "50%",
|
|
1769
|
+
transform: "translateX(-50%)",
|
|
1770
|
+
maxWidth: "84%",
|
|
1771
|
+
pointerEvents: "none"
|
|
1772
|
+
};
|
|
1773
|
+
var liveMessagePillStyle = {
|
|
1774
|
+
background: "rgba(0, 0, 0, 0.7)",
|
|
1775
|
+
backdropFilter: "blur(6px)",
|
|
1776
|
+
border: "1px solid rgba(255, 255, 255, 0.2)",
|
|
1777
|
+
borderRadius: 999,
|
|
1778
|
+
padding: "6px 12px",
|
|
1779
|
+
textAlign: "center",
|
|
1780
|
+
boxShadow: "0 10px 25px -5px rgba(0,0,0,.3)"
|
|
1781
|
+
};
|
|
1782
|
+
var liveMessagePillTextStyle = {
|
|
1783
|
+
margin: 0,
|
|
1784
|
+
fontSize: 12,
|
|
1785
|
+
fontWeight: 500,
|
|
1786
|
+
color: "#ffffff",
|
|
1787
|
+
animation: "pulse 2s ease-in-out infinite"
|
|
1788
|
+
};
|
|
1146
1789
|
var FaceCaptureModal_default = FaceCaptureModal;
|
|
1147
1790
|
|
|
1148
1791
|
// src/kyc/hooks.ts
|
|
@@ -1426,7 +2069,7 @@ function useKycPreferences(client, autoFetch = true) {
|
|
|
1426
2069
|
refresh: fetchPreferences
|
|
1427
2070
|
};
|
|
1428
2071
|
}
|
|
1429
|
-
function
|
|
2072
|
+
function fileToBase64(file) {
|
|
1430
2073
|
return new Promise((resolve, reject) => {
|
|
1431
2074
|
const reader = new FileReader();
|
|
1432
2075
|
reader.onload = () => {
|
|
@@ -1482,42 +2125,53 @@ function getRiskColor(risk) {
|
|
|
1482
2125
|
};
|
|
1483
2126
|
return colorMap[risk] || "#6b7280";
|
|
1484
2127
|
}
|
|
1485
|
-
function
|
|
1486
|
-
const
|
|
1487
|
-
|
|
2128
|
+
function useEventBasedFaceVerificationSubmission(client) {
|
|
2129
|
+
const startSession = useCallback(
|
|
2130
|
+
(request) => client.createEventBasedFaceVerificationSession(request),
|
|
2131
|
+
[client]
|
|
2132
|
+
);
|
|
2133
|
+
const verifyFace = useCallback(
|
|
2134
|
+
(session, opts = {}) => new Promise((resolve) => {
|
|
1488
2135
|
const container = document.createElement("div");
|
|
1489
2136
|
document.body.appendChild(container);
|
|
1490
2137
|
const root = createRoot(container);
|
|
1491
2138
|
const cleanup = () => {
|
|
1492
2139
|
root.unmount();
|
|
1493
|
-
|
|
2140
|
+
container.remove();
|
|
1494
2141
|
};
|
|
1495
2142
|
const modal = createElement(FaceCaptureModal_default, {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
resolve(resp);
|
|
1504
|
-
} catch (error) {
|
|
1505
|
-
reject(error instanceof Error ? error : new Error("Face verification failed"));
|
|
1506
|
-
} finally {
|
|
1507
|
-
cleanup();
|
|
1508
|
-
}
|
|
1509
|
-
},
|
|
1510
|
-
onCancel: () => {
|
|
2143
|
+
client,
|
|
2144
|
+
session,
|
|
2145
|
+
defaultDevice: opts.defaultDevice,
|
|
2146
|
+
renderQR: opts.renderQR,
|
|
2147
|
+
onCancel: opts.onCancel,
|
|
2148
|
+
mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
|
|
2149
|
+
onComplete: (result) => {
|
|
1511
2150
|
cleanup();
|
|
1512
|
-
resolve(
|
|
2151
|
+
resolve(result);
|
|
1513
2152
|
}
|
|
1514
2153
|
});
|
|
1515
2154
|
root.render(modal);
|
|
1516
|
-
})
|
|
1517
|
-
|
|
1518
|
-
|
|
2155
|
+
}),
|
|
2156
|
+
[client]
|
|
2157
|
+
);
|
|
2158
|
+
const runFlow = useCallback(
|
|
2159
|
+
async (request, opts = {}) => {
|
|
2160
|
+
const session = await startSession(request);
|
|
2161
|
+
if (!session.is_required) {
|
|
2162
|
+
return { kind: "not_required", session };
|
|
2163
|
+
}
|
|
2164
|
+
const result = await verifyFace(session, opts);
|
|
2165
|
+
if (result === null) {
|
|
2166
|
+
return { kind: "cancelled", session };
|
|
2167
|
+
}
|
|
2168
|
+
return { kind: "completed", session, result };
|
|
2169
|
+
},
|
|
2170
|
+
[startSession, verifyFace]
|
|
2171
|
+
);
|
|
2172
|
+
return { startSession, verifyFace, runFlow };
|
|
1519
2173
|
}
|
|
1520
2174
|
|
|
1521
|
-
export { createDeviceFingerprint,
|
|
2175
|
+
export { createDeviceFingerprint, fileToBase64, formatKycStatus, getBrowserInfo, getRiskColor, getStatusColor, isValidFileSize, isValidFileType, useCipherText, useCustomerProfile, useEventBasedFaceVerificationSubmission, useGeolocation, useKycAlerts, useKycOverview, useKycPreferences, useKycRequests, useKycSubmission, useLocationCapture, useLocationRequests, useLoginVerification, useRegistration, useTransactionVerification };
|
|
1522
2176
|
//# sourceMappingURL=react.mjs.map
|
|
1523
2177
|
//# sourceMappingURL=react.mjs.map
|