astra-sdk-web 1.1.6 → 1.1.8

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.
@@ -84,7 +84,10 @@ var KycApiService = class {
84
84
  if (!response.ok) {
85
85
  const errorData = await response.json().catch(() => ({}));
86
86
  const message = errorData?.message || `Face upload failed with status ${response.status}`;
87
- throw new Error(message);
87
+ const error = new Error(message);
88
+ error.statusCode = response.status;
89
+ error.errorData = errorData;
90
+ throw error;
88
91
  }
89
92
  const data = await response.json();
90
93
  return data;
@@ -746,6 +749,8 @@ var FaceMeshService = class {
746
749
  }
747
750
  this.processLiveness(faceOnCanvas, w, h);
748
751
  } else {
752
+ this.livenessStateRef.current.currentYaw = null;
753
+ this.livenessStateRef.current.currentAbsYaw = null;
749
754
  const vid = this.videoRef.current;
750
755
  if (vid) {
751
756
  const vidW = Math.max(1, vid?.videoWidth || displayW);
@@ -789,6 +794,8 @@ var FaceMeshService = class {
789
794
  const midX = (leftEyeOuter.x + rightEyeOuter.x) / 2;
790
795
  const yaw = (nT.x - midX) / Math.max(1e-6, faceWidth);
791
796
  const absYaw = Math.abs(yaw);
797
+ this.livenessStateRef.current.currentYaw = yaw;
798
+ this.livenessStateRef.current.currentAbsYaw = absYaw;
792
799
  const xs = faceOnCanvas.map((p) => p.x), ys = faceOnCanvas.map((p) => p.y);
793
800
  const minX = Math.min(...xs) * w, maxX = Math.max(...xs) * w;
794
801
  const minY = Math.min(...ys) * h, maxY = Math.max(...ys) * h;
@@ -815,11 +822,13 @@ var FaceMeshService = class {
815
822
  } else if (absYaw < centerThreshold) {
816
823
  state.centerHold += 1;
817
824
  if (state.centerHold >= holdFramesCenter) {
818
- const newStage = "LEFT";
819
- state.stage = newStage;
820
- state.centerHold = 0;
821
- if (this.callbacks.onLivenessUpdate) {
822
- this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
825
+ if (!state.livenessCompleted) {
826
+ const newStage = "LEFT";
827
+ state.stage = newStage;
828
+ state.centerHold = 0;
829
+ if (this.callbacks.onLivenessUpdate) {
830
+ this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
831
+ }
823
832
  }
824
833
  }
825
834
  } else {
@@ -858,16 +867,12 @@ var FaceMeshService = class {
858
867
  state.rightHold += 1;
859
868
  if (state.rightHold >= holdFramesTurn) {
860
869
  state.rightHold = 0;
861
- if (!state.snapTriggered) {
862
- state.snapTriggered = true;
863
- const newStage = "DONE";
864
- state.stage = newStage;
865
- if (this.callbacks.onLivenessUpdate) {
866
- this.callbacks.onLivenessUpdate(newStage, "Capturing...");
867
- }
868
- if (this.callbacks.onCaptureTrigger) {
869
- this.callbacks.onCaptureTrigger();
870
- }
870
+ state.livenessCompleted = true;
871
+ const newStage = "DONE";
872
+ state.stage = newStage;
873
+ state.centerHold = 0;
874
+ if (this.callbacks.onLivenessUpdate) {
875
+ this.callbacks.onLivenessUpdate(newStage, "Great! Now look straight at the camera");
871
876
  }
872
877
  }
873
878
  } else {
@@ -876,6 +881,28 @@ var FaceMeshService = class {
876
881
  this.callbacks.onLivenessUpdate(state.stage, yaw < -0.08 ? "You're facing left. Turn RIGHT" : "Turn a bit more RIGHT");
877
882
  }
878
883
  }
884
+ } else if (state.stage === "DONE") {
885
+ if (absYaw < centerThreshold && insideGuide) {
886
+ state.centerHold += 1;
887
+ if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
888
+ state.snapTriggered = true;
889
+ if (this.callbacks.onLivenessUpdate) {
890
+ this.callbacks.onLivenessUpdate(state.stage, "Capturing...");
891
+ }
892
+ if (this.callbacks.onCaptureTrigger) {
893
+ this.callbacks.onCaptureTrigger();
894
+ }
895
+ }
896
+ } else {
897
+ state.centerHold = 0;
898
+ if (this.callbacks.onLivenessUpdate) {
899
+ if (!insideGuide) {
900
+ this.callbacks.onLivenessUpdate(state.stage, "Center your face inside the circle");
901
+ } else {
902
+ this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
903
+ }
904
+ }
905
+ }
879
906
  }
880
907
  }
881
908
  }
@@ -1011,7 +1038,10 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1011
1038
  snapTriggered: false,
1012
1039
  lastResultsAt: 0,
1013
1040
  stage: "CENTER",
1014
- livenessReady: false
1041
+ livenessReady: false,
1042
+ currentYaw: null,
1043
+ currentAbsYaw: null,
1044
+ livenessCompleted: false
1015
1045
  });
1016
1046
  React.useEffect(() => {
1017
1047
  livenessStateRef.current.centerHold = refs.centerHold.current;
@@ -1032,6 +1062,22 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1032
1062
  }, []);
1033
1063
  const handleFaceCapture = React.useCallback(async () => {
1034
1064
  if (!videoRef.current) return;
1065
+ const centerThreshold = 0.05;
1066
+ const currentAbsYaw = livenessStateRef.current.currentAbsYaw;
1067
+ if (currentAbsYaw === null || currentAbsYaw === void 0) {
1068
+ setState((prev) => ({
1069
+ ...prev,
1070
+ livenessInstruction: "Please position your face in front of the camera"
1071
+ }));
1072
+ return;
1073
+ }
1074
+ if (currentAbsYaw >= centerThreshold) {
1075
+ setState((prev) => ({
1076
+ ...prev,
1077
+ livenessInstruction: "Please look straight at the camera before capturing"
1078
+ }));
1079
+ return;
1080
+ }
1035
1081
  setState((prev) => ({ ...prev, loading: true }));
1036
1082
  try {
1037
1083
  const video = videoRef.current;
@@ -1185,18 +1231,82 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1185
1231
  handleFaceCapture
1186
1232
  };
1187
1233
  }
1234
+ function Toast({ message, type = "info", duration = 5e3, onClose }) {
1235
+ const [isVisible, setIsVisible] = React.useState(true);
1236
+ React.useEffect(() => {
1237
+ const timer = setTimeout(() => {
1238
+ setIsVisible(false);
1239
+ setTimeout(() => {
1240
+ if (onClose) onClose();
1241
+ }, 300);
1242
+ }, duration);
1243
+ return () => clearTimeout(timer);
1244
+ }, [duration, onClose]);
1245
+ const bgColor = {
1246
+ success: "bg-green-600",
1247
+ error: "bg-red-600",
1248
+ info: "bg-blue-600",
1249
+ warning: "bg-yellow-600"
1250
+ }[type];
1251
+ return /* @__PURE__ */ jsxRuntime.jsx(
1252
+ "div",
1253
+ {
1254
+ className: `fixed top-4 right-4 z-[10000] transition-all duration-300 ${isVisible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2"}`,
1255
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1256
+ "div",
1257
+ {
1258
+ className: `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px] max-w-[500px]`,
1259
+ children: [
1260
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm font-medium", children: message }) }),
1261
+ /* @__PURE__ */ jsxRuntime.jsx(
1262
+ "button",
1263
+ {
1264
+ onClick: () => {
1265
+ setIsVisible(false);
1266
+ setTimeout(() => {
1267
+ if (onClose) onClose();
1268
+ }, 300);
1269
+ },
1270
+ className: "text-white hover:text-gray-200 transition-colors",
1271
+ "aria-label": "Close",
1272
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1273
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1274
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1275
+ ] })
1276
+ }
1277
+ )
1278
+ ]
1279
+ }
1280
+ )
1281
+ }
1282
+ );
1283
+ }
1188
1284
  function FaceScanModal({ onComplete }) {
1189
1285
  const faceCanvasRef = React.useRef(null);
1190
1286
  const navigate = reactRouterDom.useNavigate();
1191
1287
  const { apiService } = useKycContext();
1192
1288
  const [sessionError, setSessionError] = React.useState(null);
1289
+ const [toast, setToast] = React.useState(null);
1193
1290
  const { videoRef, cameraReady, stopCamera } = useCamera();
1194
1291
  const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, {
1195
1292
  onFaceUpload: async (blob) => {
1196
1293
  if (!apiService) {
1197
1294
  throw new Error("API service not initialized");
1198
1295
  }
1199
- await apiService.uploadFaceScan(blob);
1296
+ try {
1297
+ await apiService.uploadFaceScan(blob);
1298
+ } catch (error) {
1299
+ const errorMessage = error?.message || "";
1300
+ const errorData = error?.errorData || {};
1301
+ if (errorMessage.includes("Face already registered") || errorMessage.includes("already registered") || errorData?.message?.includes("Face already registered") || error?.statusCode === 500 && errorMessage.includes("Face")) {
1302
+ setToast({
1303
+ message: "Face already registered",
1304
+ type: "warning"
1305
+ });
1306
+ return;
1307
+ }
1308
+ throw error;
1309
+ }
1200
1310
  },
1201
1311
  onFaceCaptureComplete: (imageData) => {
1202
1312
  if (onComplete) {
@@ -1291,61 +1401,72 @@ function FaceScanModal({ onComplete }) {
1291
1401
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[#9ca3af] text-sm", children: "Redirecting to QR code page..." })
1292
1402
  ] }) }) });
1293
1403
  }
1294
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl mt-48", children: [
1295
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-white text-center", children: "Capture Face" }) }),
1296
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4", children: [
1297
- !state.modelLoading && state.modelLoaded && !state.livenessFailed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-[#0a2315] text-[#34d399] py-3.5 px-4 rounded-xl text-sm border border-[#155e3b] text-left", children: "Face detection model loaded." }),
1298
- state.modelLoading && !state.livenessFailed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-[#1f2937] text-[#e5e7eb] py-3.5 px-4 rounded-xl text-sm border border-[#374151] text-left", children: "Loading face detection model..." }),
1299
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full aspect-square rounded-full overflow-hidden bg-black", children: [
1404
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1405
+ toast && /* @__PURE__ */ jsxRuntime.jsx(
1406
+ Toast,
1407
+ {
1408
+ message: toast.message,
1409
+ type: toast.type,
1410
+ onClose: () => setToast(null),
1411
+ duration: 6e3
1412
+ }
1413
+ ),
1414
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl mt-48", children: [
1415
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-white text-center", children: "Capture Face" }) }),
1416
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4", children: [
1417
+ !state.modelLoading && state.modelLoaded && !state.livenessFailed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-[#0a2315] text-[#34d399] py-3.5 px-4 rounded-xl text-sm border border-[#155e3b] text-left", children: "Face detection model loaded." }),
1418
+ state.modelLoading && !state.livenessFailed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-[#1f2937] text-[#e5e7eb] py-3.5 px-4 rounded-xl text-sm border border-[#374151] text-left", children: "Loading face detection model..." }),
1419
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full aspect-square rounded-full overflow-hidden bg-black", children: [
1420
+ /* @__PURE__ */ jsxRuntime.jsx(
1421
+ "video",
1422
+ {
1423
+ ref: videoRef,
1424
+ playsInline: true,
1425
+ muted: true,
1426
+ className: "w-full h-full block bg-black object-cover -scale-x-100 origin-center"
1427
+ }
1428
+ ),
1429
+ /* @__PURE__ */ jsxRuntime.jsx(
1430
+ "canvas",
1431
+ {
1432
+ ref: faceCanvasRef,
1433
+ className: "absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
1434
+ }
1435
+ ),
1436
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 pointer-events-none z-[3]", children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "50", cy: "50", r: "44", fill: "none", stroke: "#22c55e", strokeWidth: "2", strokeDasharray: "1 3" }) })
1437
+ ] }),
1438
+ !state.livenessFailed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-gradient-to-b from-[rgba(17,24,39,0.9)] to-[rgba(17,24,39,0.6)] text-[#e5e7eb] p-4 rounded-2xl text-base border border-[#30363d]", children: [
1439
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-bold mb-2.5 text-[22px] text-white", children: "Liveness Check" }),
1440
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2.5 text-base", children: state.livenessInstruction }),
1441
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2.5 text-lg", children: [
1442
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "CENTER" || state.livenessStage === "LEFT" || state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "1. Look Straight" }),
1443
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "2. Turn your face right" }),
1444
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1445
+ ] })
1446
+ ] }),
1300
1447
  /* @__PURE__ */ jsxRuntime.jsx(
1301
- "video",
1448
+ "button",
1302
1449
  {
1303
- ref: videoRef,
1304
- playsInline: true,
1305
- muted: true,
1306
- className: "w-full h-full block bg-black object-cover -scale-x-100 origin-center"
1450
+ type: "button",
1451
+ disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1452
+ onClick: handleFaceCapture,
1453
+ className: `py-3.5 px-4 rounded-xl text-base font-bold border-none transition-colors ${state.cameraReady && !state.loading && (state.livenessFailed || state.livenessStage === "DONE") ? "bg-[#22c55e] text-[#0b0f17] cursor-pointer hover:bg-[#16a34a]" : "bg-[#374151] text-[#e5e7eb] cursor-not-allowed"}`,
1454
+ children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1307
1455
  }
1308
1456
  ),
1309
1457
  /* @__PURE__ */ jsxRuntime.jsx(
1310
- "canvas",
1458
+ "button",
1311
1459
  {
1312
- ref: faceCanvasRef,
1313
- className: "absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
1460
+ type: "button",
1461
+ onClick: handleRetry,
1462
+ disabled: state.loading,
1463
+ className: `py-3 px-4 rounded-[10px] text-[15px] font-semibold border-none w-full transition-colors ${state.loading ? "bg-[#374151] text-[#e5e7eb] cursor-not-allowed opacity-50" : "bg-[#374151] text-[#e5e7eb] cursor-pointer hover:bg-[#4b5563]"}`,
1464
+ children: "Restart"
1314
1465
  }
1315
- ),
1316
- /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 pointer-events-none z-[3]", children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "50", cy: "50", r: "44", fill: "none", stroke: "#22c55e", strokeWidth: "2", strokeDasharray: "1 3" }) })
1317
- ] }),
1318
- !state.livenessFailed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-gradient-to-b from-[rgba(17,24,39,0.9)] to-[rgba(17,24,39,0.6)] text-[#e5e7eb] p-4 rounded-2xl text-base border border-[#30363d]", children: [
1319
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-bold mb-2.5 text-[22px] text-white", children: "Liveness Check" }),
1320
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2.5 text-base", children: state.livenessInstruction }),
1321
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2.5 text-lg", children: [
1322
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "CENTER" || state.livenessStage === "LEFT" || state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "1. Look Straight" }),
1323
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "2. Turn your face right" }),
1324
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1325
- ] })
1326
- ] }),
1327
- /* @__PURE__ */ jsxRuntime.jsx(
1328
- "button",
1329
- {
1330
- type: "button",
1331
- disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1332
- onClick: handleFaceCapture,
1333
- className: `py-3.5 px-4 rounded-xl text-base font-bold border-none transition-colors ${state.cameraReady && !state.loading && (state.livenessFailed || state.livenessStage === "DONE") ? "bg-[#22c55e] text-[#0b0f17] cursor-pointer hover:bg-[#16a34a]" : "bg-[#374151] text-[#e5e7eb] cursor-not-allowed"}`,
1334
- children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1335
- }
1336
- ),
1337
- /* @__PURE__ */ jsxRuntime.jsx(
1338
- "button",
1339
- {
1340
- type: "button",
1341
- onClick: handleRetry,
1342
- disabled: state.loading,
1343
- className: `py-3 px-4 rounded-[10px] text-[15px] font-semibold border-none w-full transition-colors ${state.loading ? "bg-[#374151] text-[#e5e7eb] cursor-not-allowed opacity-50" : "bg-[#374151] text-[#e5e7eb] cursor-pointer hover:bg-[#4b5563]"}`,
1344
- children: "Restart"
1345
- }
1346
- )
1347
- ] })
1348
- ] }) });
1466
+ )
1467
+ ] })
1468
+ ] }) })
1469
+ ] });
1349
1470
  }
1350
1471
  var FaceScanModal_default = FaceScanModal;
1351
1472
  function MobileRouteContent({ onClose, onComplete }) {