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.
@@ -289,7 +289,10 @@ var KycApiService = class {
289
289
  if (!response.ok) {
290
290
  const errorData = await response.json().catch(() => ({}));
291
291
  const message = errorData?.message || `Face upload failed with status ${response.status}`;
292
- throw new Error(message);
292
+ const error = new Error(message);
293
+ error.statusCode = response.status;
294
+ error.errorData = errorData;
295
+ throw error;
293
296
  }
294
297
  const data = await response.json();
295
298
  return data;
@@ -951,6 +954,8 @@ var FaceMeshService = class {
951
954
  }
952
955
  this.processLiveness(faceOnCanvas, w, h);
953
956
  } else {
957
+ this.livenessStateRef.current.currentYaw = null;
958
+ this.livenessStateRef.current.currentAbsYaw = null;
954
959
  const vid = this.videoRef.current;
955
960
  if (vid) {
956
961
  const vidW = Math.max(1, vid?.videoWidth || displayW);
@@ -994,6 +999,8 @@ var FaceMeshService = class {
994
999
  const midX = (leftEyeOuter.x + rightEyeOuter.x) / 2;
995
1000
  const yaw = (nT.x - midX) / Math.max(1e-6, faceWidth);
996
1001
  const absYaw = Math.abs(yaw);
1002
+ this.livenessStateRef.current.currentYaw = yaw;
1003
+ this.livenessStateRef.current.currentAbsYaw = absYaw;
997
1004
  const xs = faceOnCanvas.map((p) => p.x), ys = faceOnCanvas.map((p) => p.y);
998
1005
  const minX = Math.min(...xs) * w, maxX = Math.max(...xs) * w;
999
1006
  const minY = Math.min(...ys) * h, maxY = Math.max(...ys) * h;
@@ -1020,11 +1027,13 @@ var FaceMeshService = class {
1020
1027
  } else if (absYaw < centerThreshold) {
1021
1028
  state.centerHold += 1;
1022
1029
  if (state.centerHold >= holdFramesCenter) {
1023
- const newStage = "LEFT";
1024
- state.stage = newStage;
1025
- state.centerHold = 0;
1026
- if (this.callbacks.onLivenessUpdate) {
1027
- this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
1030
+ if (!state.livenessCompleted) {
1031
+ const newStage = "LEFT";
1032
+ state.stage = newStage;
1033
+ state.centerHold = 0;
1034
+ if (this.callbacks.onLivenessUpdate) {
1035
+ this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
1036
+ }
1028
1037
  }
1029
1038
  }
1030
1039
  } else {
@@ -1063,16 +1072,12 @@ var FaceMeshService = class {
1063
1072
  state.rightHold += 1;
1064
1073
  if (state.rightHold >= holdFramesTurn) {
1065
1074
  state.rightHold = 0;
1066
- if (!state.snapTriggered) {
1067
- state.snapTriggered = true;
1068
- const newStage = "DONE";
1069
- state.stage = newStage;
1070
- if (this.callbacks.onLivenessUpdate) {
1071
- this.callbacks.onLivenessUpdate(newStage, "Capturing...");
1072
- }
1073
- if (this.callbacks.onCaptureTrigger) {
1074
- this.callbacks.onCaptureTrigger();
1075
- }
1075
+ state.livenessCompleted = true;
1076
+ const newStage = "DONE";
1077
+ state.stage = newStage;
1078
+ state.centerHold = 0;
1079
+ if (this.callbacks.onLivenessUpdate) {
1080
+ this.callbacks.onLivenessUpdate(newStage, "Great! Now look straight at the camera");
1076
1081
  }
1077
1082
  }
1078
1083
  } else {
@@ -1081,6 +1086,28 @@ var FaceMeshService = class {
1081
1086
  this.callbacks.onLivenessUpdate(state.stage, yaw < -0.08 ? "You're facing left. Turn RIGHT" : "Turn a bit more RIGHT");
1082
1087
  }
1083
1088
  }
1089
+ } else if (state.stage === "DONE") {
1090
+ if (absYaw < centerThreshold && insideGuide) {
1091
+ state.centerHold += 1;
1092
+ if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
1093
+ state.snapTriggered = true;
1094
+ if (this.callbacks.onLivenessUpdate) {
1095
+ this.callbacks.onLivenessUpdate(state.stage, "Capturing...");
1096
+ }
1097
+ if (this.callbacks.onCaptureTrigger) {
1098
+ this.callbacks.onCaptureTrigger();
1099
+ }
1100
+ }
1101
+ } else {
1102
+ state.centerHold = 0;
1103
+ if (this.callbacks.onLivenessUpdate) {
1104
+ if (!insideGuide) {
1105
+ this.callbacks.onLivenessUpdate(state.stage, "Center your face inside the circle");
1106
+ } else {
1107
+ this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
1108
+ }
1109
+ }
1110
+ }
1084
1111
  }
1085
1112
  }
1086
1113
  }
@@ -1216,7 +1243,10 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1216
1243
  snapTriggered: false,
1217
1244
  lastResultsAt: 0,
1218
1245
  stage: "CENTER",
1219
- livenessReady: false
1246
+ livenessReady: false,
1247
+ currentYaw: null,
1248
+ currentAbsYaw: null,
1249
+ livenessCompleted: false
1220
1250
  });
1221
1251
  React.useEffect(() => {
1222
1252
  livenessStateRef.current.centerHold = refs.centerHold.current;
@@ -1237,6 +1267,22 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1237
1267
  }, []);
1238
1268
  const handleFaceCapture = React.useCallback(async () => {
1239
1269
  if (!videoRef.current) return;
1270
+ const centerThreshold = 0.05;
1271
+ const currentAbsYaw = livenessStateRef.current.currentAbsYaw;
1272
+ if (currentAbsYaw === null || currentAbsYaw === void 0) {
1273
+ setState((prev) => ({
1274
+ ...prev,
1275
+ livenessInstruction: "Please position your face in front of the camera"
1276
+ }));
1277
+ return;
1278
+ }
1279
+ if (currentAbsYaw >= centerThreshold) {
1280
+ setState((prev) => ({
1281
+ ...prev,
1282
+ livenessInstruction: "Please look straight at the camera before capturing"
1283
+ }));
1284
+ return;
1285
+ }
1240
1286
  setState((prev) => ({ ...prev, loading: true }));
1241
1287
  try {
1242
1288
  const video = videoRef.current;
@@ -1390,18 +1436,82 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1390
1436
  handleFaceCapture
1391
1437
  };
1392
1438
  }
1439
+ function Toast({ message, type = "info", duration = 5e3, onClose }) {
1440
+ const [isVisible, setIsVisible] = React.useState(true);
1441
+ React.useEffect(() => {
1442
+ const timer = setTimeout(() => {
1443
+ setIsVisible(false);
1444
+ setTimeout(() => {
1445
+ if (onClose) onClose();
1446
+ }, 300);
1447
+ }, duration);
1448
+ return () => clearTimeout(timer);
1449
+ }, [duration, onClose]);
1450
+ const bgColor = {
1451
+ success: "bg-green-600",
1452
+ error: "bg-red-600",
1453
+ info: "bg-blue-600",
1454
+ warning: "bg-yellow-600"
1455
+ }[type];
1456
+ return /* @__PURE__ */ jsxRuntime.jsx(
1457
+ "div",
1458
+ {
1459
+ className: `fixed top-4 right-4 z-[10000] transition-all duration-300 ${isVisible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2"}`,
1460
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1461
+ "div",
1462
+ {
1463
+ className: `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px] max-w-[500px]`,
1464
+ children: [
1465
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm font-medium", children: message }) }),
1466
+ /* @__PURE__ */ jsxRuntime.jsx(
1467
+ "button",
1468
+ {
1469
+ onClick: () => {
1470
+ setIsVisible(false);
1471
+ setTimeout(() => {
1472
+ if (onClose) onClose();
1473
+ }, 300);
1474
+ },
1475
+ className: "text-white hover:text-gray-200 transition-colors",
1476
+ "aria-label": "Close",
1477
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1478
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1479
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1480
+ ] })
1481
+ }
1482
+ )
1483
+ ]
1484
+ }
1485
+ )
1486
+ }
1487
+ );
1488
+ }
1393
1489
  function FaceScanModal({ onComplete }) {
1394
1490
  const faceCanvasRef = React.useRef(null);
1395
1491
  const navigate = reactRouterDom.useNavigate();
1396
1492
  const { apiService } = useKycContext();
1397
1493
  const [sessionError, setSessionError] = React.useState(null);
1494
+ const [toast, setToast] = React.useState(null);
1398
1495
  const { videoRef, cameraReady, stopCamera } = useCamera();
1399
1496
  const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, {
1400
1497
  onFaceUpload: async (blob) => {
1401
1498
  if (!apiService) {
1402
1499
  throw new Error("API service not initialized");
1403
1500
  }
1404
- await apiService.uploadFaceScan(blob);
1501
+ try {
1502
+ await apiService.uploadFaceScan(blob);
1503
+ } catch (error) {
1504
+ const errorMessage = error?.message || "";
1505
+ const errorData = error?.errorData || {};
1506
+ if (errorMessage.includes("Face already registered") || errorMessage.includes("already registered") || errorData?.message?.includes("Face already registered") || error?.statusCode === 500 && errorMessage.includes("Face")) {
1507
+ setToast({
1508
+ message: "Face already registered",
1509
+ type: "warning"
1510
+ });
1511
+ return;
1512
+ }
1513
+ throw error;
1514
+ }
1405
1515
  },
1406
1516
  onFaceCaptureComplete: (imageData) => {
1407
1517
  if (onComplete) {
@@ -1496,61 +1606,72 @@ function FaceScanModal({ onComplete }) {
1496
1606
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[#9ca3af] text-sm", children: "Redirecting to QR code page..." })
1497
1607
  ] }) }) });
1498
1608
  }
1499
- 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: [
1500
- /* @__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" }) }),
1501
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4", children: [
1502
- !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." }),
1503
- 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..." }),
1504
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full aspect-square rounded-full overflow-hidden bg-black", children: [
1609
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1610
+ toast && /* @__PURE__ */ jsxRuntime.jsx(
1611
+ Toast,
1612
+ {
1613
+ message: toast.message,
1614
+ type: toast.type,
1615
+ onClose: () => setToast(null),
1616
+ duration: 6e3
1617
+ }
1618
+ ),
1619
+ /* @__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: [
1620
+ /* @__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" }) }),
1621
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4", children: [
1622
+ !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." }),
1623
+ 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..." }),
1624
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full aspect-square rounded-full overflow-hidden bg-black", children: [
1625
+ /* @__PURE__ */ jsxRuntime.jsx(
1626
+ "video",
1627
+ {
1628
+ ref: videoRef,
1629
+ playsInline: true,
1630
+ muted: true,
1631
+ className: "w-full h-full block bg-black object-cover -scale-x-100 origin-center"
1632
+ }
1633
+ ),
1634
+ /* @__PURE__ */ jsxRuntime.jsx(
1635
+ "canvas",
1636
+ {
1637
+ ref: faceCanvasRef,
1638
+ className: "absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
1639
+ }
1640
+ ),
1641
+ /* @__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" }) })
1642
+ ] }),
1643
+ !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: [
1644
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-bold mb-2.5 text-[22px] text-white", children: "Liveness Check" }),
1645
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2.5 text-base", children: state.livenessInstruction }),
1646
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2.5 text-lg", children: [
1647
+ /* @__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" }),
1648
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "2. Turn your face right" }),
1649
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1650
+ ] })
1651
+ ] }),
1505
1652
  /* @__PURE__ */ jsxRuntime.jsx(
1506
- "video",
1653
+ "button",
1507
1654
  {
1508
- ref: videoRef,
1509
- playsInline: true,
1510
- muted: true,
1511
- className: "w-full h-full block bg-black object-cover -scale-x-100 origin-center"
1655
+ type: "button",
1656
+ disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1657
+ onClick: handleFaceCapture,
1658
+ 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"}`,
1659
+ children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1512
1660
  }
1513
1661
  ),
1514
1662
  /* @__PURE__ */ jsxRuntime.jsx(
1515
- "canvas",
1663
+ "button",
1516
1664
  {
1517
- ref: faceCanvasRef,
1518
- className: "absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
1665
+ type: "button",
1666
+ onClick: handleRetry,
1667
+ disabled: state.loading,
1668
+ 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]"}`,
1669
+ children: "Restart"
1519
1670
  }
1520
- ),
1521
- /* @__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" }) })
1522
- ] }),
1523
- !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: [
1524
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-bold mb-2.5 text-[22px] text-white", children: "Liveness Check" }),
1525
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2.5 text-base", children: state.livenessInstruction }),
1526
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2.5 text-lg", children: [
1527
- /* @__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" }),
1528
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "2. Turn your face right" }),
1529
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1530
- ] })
1531
- ] }),
1532
- /* @__PURE__ */ jsxRuntime.jsx(
1533
- "button",
1534
- {
1535
- type: "button",
1536
- disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1537
- onClick: handleFaceCapture,
1538
- 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"}`,
1539
- children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1540
- }
1541
- ),
1542
- /* @__PURE__ */ jsxRuntime.jsx(
1543
- "button",
1544
- {
1545
- type: "button",
1546
- onClick: handleRetry,
1547
- disabled: state.loading,
1548
- 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]"}`,
1549
- children: "Restart"
1550
- }
1551
- )
1552
- ] })
1553
- ] }) });
1671
+ )
1672
+ ] })
1673
+ ] }) })
1674
+ ] });
1554
1675
  }
1555
1676
  var FaceScanModal_default = FaceScanModal;
1556
1677
  function MobileRouteContent({ onClose, onComplete }) {