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.
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useState, useEffect, useRef, useContext, useCallback } from 'react';
2
- import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { useNavigate } from 'react-router-dom';
4
4
  import { FACEMESH_TESSELATION, FACEMESH_FACE_OVAL, FACEMESH_LEFT_EYE, FACEMESH_RIGHT_EYE, FACEMESH_LIPS, FaceMesh } from '@mediapipe/face_mesh';
5
5
  import { drawConnectors, drawLandmarks } from '@mediapipe/drawing_utils';
@@ -281,7 +281,10 @@ var KycApiService = class {
281
281
  if (!response.ok) {
282
282
  const errorData = await response.json().catch(() => ({}));
283
283
  const message = errorData?.message || `Face upload failed with status ${response.status}`;
284
- throw new Error(message);
284
+ const error = new Error(message);
285
+ error.statusCode = response.status;
286
+ error.errorData = errorData;
287
+ throw error;
285
288
  }
286
289
  const data = await response.json();
287
290
  return data;
@@ -943,6 +946,8 @@ var FaceMeshService = class {
943
946
  }
944
947
  this.processLiveness(faceOnCanvas, w, h);
945
948
  } else {
949
+ this.livenessStateRef.current.currentYaw = null;
950
+ this.livenessStateRef.current.currentAbsYaw = null;
946
951
  const vid = this.videoRef.current;
947
952
  if (vid) {
948
953
  const vidW = Math.max(1, vid?.videoWidth || displayW);
@@ -986,6 +991,8 @@ var FaceMeshService = class {
986
991
  const midX = (leftEyeOuter.x + rightEyeOuter.x) / 2;
987
992
  const yaw = (nT.x - midX) / Math.max(1e-6, faceWidth);
988
993
  const absYaw = Math.abs(yaw);
994
+ this.livenessStateRef.current.currentYaw = yaw;
995
+ this.livenessStateRef.current.currentAbsYaw = absYaw;
989
996
  const xs = faceOnCanvas.map((p) => p.x), ys = faceOnCanvas.map((p) => p.y);
990
997
  const minX = Math.min(...xs) * w, maxX = Math.max(...xs) * w;
991
998
  const minY = Math.min(...ys) * h, maxY = Math.max(...ys) * h;
@@ -1012,11 +1019,13 @@ var FaceMeshService = class {
1012
1019
  } else if (absYaw < centerThreshold) {
1013
1020
  state.centerHold += 1;
1014
1021
  if (state.centerHold >= holdFramesCenter) {
1015
- const newStage = "LEFT";
1016
- state.stage = newStage;
1017
- state.centerHold = 0;
1018
- if (this.callbacks.onLivenessUpdate) {
1019
- this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
1022
+ if (!state.livenessCompleted) {
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");
1028
+ }
1020
1029
  }
1021
1030
  }
1022
1031
  } else {
@@ -1055,16 +1064,12 @@ var FaceMeshService = class {
1055
1064
  state.rightHold += 1;
1056
1065
  if (state.rightHold >= holdFramesTurn) {
1057
1066
  state.rightHold = 0;
1058
- if (!state.snapTriggered) {
1059
- state.snapTriggered = true;
1060
- const newStage = "DONE";
1061
- state.stage = newStage;
1062
- if (this.callbacks.onLivenessUpdate) {
1063
- this.callbacks.onLivenessUpdate(newStage, "Capturing...");
1064
- }
1065
- if (this.callbacks.onCaptureTrigger) {
1066
- this.callbacks.onCaptureTrigger();
1067
- }
1067
+ state.livenessCompleted = true;
1068
+ const newStage = "DONE";
1069
+ state.stage = newStage;
1070
+ state.centerHold = 0;
1071
+ if (this.callbacks.onLivenessUpdate) {
1072
+ this.callbacks.onLivenessUpdate(newStage, "Great! Now look straight at the camera");
1068
1073
  }
1069
1074
  }
1070
1075
  } else {
@@ -1073,6 +1078,28 @@ var FaceMeshService = class {
1073
1078
  this.callbacks.onLivenessUpdate(state.stage, yaw < -0.08 ? "You're facing left. Turn RIGHT" : "Turn a bit more RIGHT");
1074
1079
  }
1075
1080
  }
1081
+ } else if (state.stage === "DONE") {
1082
+ if (absYaw < centerThreshold && insideGuide) {
1083
+ state.centerHold += 1;
1084
+ if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
1085
+ state.snapTriggered = true;
1086
+ if (this.callbacks.onLivenessUpdate) {
1087
+ this.callbacks.onLivenessUpdate(state.stage, "Capturing...");
1088
+ }
1089
+ if (this.callbacks.onCaptureTrigger) {
1090
+ this.callbacks.onCaptureTrigger();
1091
+ }
1092
+ }
1093
+ } else {
1094
+ state.centerHold = 0;
1095
+ if (this.callbacks.onLivenessUpdate) {
1096
+ if (!insideGuide) {
1097
+ this.callbacks.onLivenessUpdate(state.stage, "Center your face inside the circle");
1098
+ } else {
1099
+ this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
1100
+ }
1101
+ }
1102
+ }
1076
1103
  }
1077
1104
  }
1078
1105
  }
@@ -1208,7 +1235,10 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1208
1235
  snapTriggered: false,
1209
1236
  lastResultsAt: 0,
1210
1237
  stage: "CENTER",
1211
- livenessReady: false
1238
+ livenessReady: false,
1239
+ currentYaw: null,
1240
+ currentAbsYaw: null,
1241
+ livenessCompleted: false
1212
1242
  });
1213
1243
  useEffect(() => {
1214
1244
  livenessStateRef.current.centerHold = refs.centerHold.current;
@@ -1229,6 +1259,22 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1229
1259
  }, []);
1230
1260
  const handleFaceCapture = useCallback(async () => {
1231
1261
  if (!videoRef.current) return;
1262
+ const centerThreshold = 0.05;
1263
+ const currentAbsYaw = livenessStateRef.current.currentAbsYaw;
1264
+ if (currentAbsYaw === null || currentAbsYaw === void 0) {
1265
+ setState((prev) => ({
1266
+ ...prev,
1267
+ livenessInstruction: "Please position your face in front of the camera"
1268
+ }));
1269
+ return;
1270
+ }
1271
+ if (currentAbsYaw >= centerThreshold) {
1272
+ setState((prev) => ({
1273
+ ...prev,
1274
+ livenessInstruction: "Please look straight at the camera before capturing"
1275
+ }));
1276
+ return;
1277
+ }
1232
1278
  setState((prev) => ({ ...prev, loading: true }));
1233
1279
  try {
1234
1280
  const video = videoRef.current;
@@ -1382,18 +1428,82 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1382
1428
  handleFaceCapture
1383
1429
  };
1384
1430
  }
1431
+ function Toast({ message, type = "info", duration = 5e3, onClose }) {
1432
+ const [isVisible, setIsVisible] = useState(true);
1433
+ useEffect(() => {
1434
+ const timer = setTimeout(() => {
1435
+ setIsVisible(false);
1436
+ setTimeout(() => {
1437
+ if (onClose) onClose();
1438
+ }, 300);
1439
+ }, duration);
1440
+ return () => clearTimeout(timer);
1441
+ }, [duration, onClose]);
1442
+ const bgColor = {
1443
+ success: "bg-green-600",
1444
+ error: "bg-red-600",
1445
+ info: "bg-blue-600",
1446
+ warning: "bg-yellow-600"
1447
+ }[type];
1448
+ return /* @__PURE__ */ jsx(
1449
+ "div",
1450
+ {
1451
+ className: `fixed top-4 right-4 z-[10000] transition-all duration-300 ${isVisible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2"}`,
1452
+ children: /* @__PURE__ */ jsxs(
1453
+ "div",
1454
+ {
1455
+ className: `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px] max-w-[500px]`,
1456
+ children: [
1457
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx("p", { className: "m-0 text-sm font-medium", children: message }) }),
1458
+ /* @__PURE__ */ jsx(
1459
+ "button",
1460
+ {
1461
+ onClick: () => {
1462
+ setIsVisible(false);
1463
+ setTimeout(() => {
1464
+ if (onClose) onClose();
1465
+ }, 300);
1466
+ },
1467
+ className: "text-white hover:text-gray-200 transition-colors",
1468
+ "aria-label": "Close",
1469
+ children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1470
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1471
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1472
+ ] })
1473
+ }
1474
+ )
1475
+ ]
1476
+ }
1477
+ )
1478
+ }
1479
+ );
1480
+ }
1385
1481
  function FaceScanModal({ onComplete }) {
1386
1482
  const faceCanvasRef = useRef(null);
1387
1483
  const navigate = useNavigate();
1388
1484
  const { apiService } = useKycContext();
1389
1485
  const [sessionError, setSessionError] = useState(null);
1486
+ const [toast, setToast] = useState(null);
1390
1487
  const { videoRef, cameraReady, stopCamera } = useCamera();
1391
1488
  const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, {
1392
1489
  onFaceUpload: async (blob) => {
1393
1490
  if (!apiService) {
1394
1491
  throw new Error("API service not initialized");
1395
1492
  }
1396
- await apiService.uploadFaceScan(blob);
1493
+ try {
1494
+ await apiService.uploadFaceScan(blob);
1495
+ } catch (error) {
1496
+ const errorMessage = error?.message || "";
1497
+ const errorData = error?.errorData || {};
1498
+ if (errorMessage.includes("Face already registered") || errorMessage.includes("already registered") || errorData?.message?.includes("Face already registered") || error?.statusCode === 500 && errorMessage.includes("Face")) {
1499
+ setToast({
1500
+ message: "Face already registered",
1501
+ type: "warning"
1502
+ });
1503
+ return;
1504
+ }
1505
+ throw error;
1506
+ }
1397
1507
  },
1398
1508
  onFaceCaptureComplete: (imageData) => {
1399
1509
  if (onComplete) {
@@ -1488,61 +1598,72 @@ function FaceScanModal({ onComplete }) {
1488
1598
  /* @__PURE__ */ jsx("p", { className: "text-[#9ca3af] text-sm", children: "Redirecting to QR code page..." })
1489
1599
  ] }) }) });
1490
1600
  }
1491
- return /* @__PURE__ */ 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__ */ jsxs("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl mt-48", children: [
1492
- /* @__PURE__ */ jsx("div", { className: "relative mb-4", children: /* @__PURE__ */ jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-white text-center", children: "Capture Face" }) }),
1493
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
1494
- !state.modelLoading && state.modelLoaded && !state.livenessFailed && /* @__PURE__ */ 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." }),
1495
- state.modelLoading && !state.livenessFailed && /* @__PURE__ */ 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..." }),
1496
- /* @__PURE__ */ jsxs("div", { className: "relative w-full aspect-square rounded-full overflow-hidden bg-black", children: [
1601
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1602
+ toast && /* @__PURE__ */ jsx(
1603
+ Toast,
1604
+ {
1605
+ message: toast.message,
1606
+ type: toast.type,
1607
+ onClose: () => setToast(null),
1608
+ duration: 6e3
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ 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__ */ jsxs("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl mt-48", children: [
1612
+ /* @__PURE__ */ jsx("div", { className: "relative mb-4", children: /* @__PURE__ */ jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-white text-center", children: "Capture Face" }) }),
1613
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
1614
+ !state.modelLoading && state.modelLoaded && !state.livenessFailed && /* @__PURE__ */ 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." }),
1615
+ state.modelLoading && !state.livenessFailed && /* @__PURE__ */ 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..." }),
1616
+ /* @__PURE__ */ jsxs("div", { className: "relative w-full aspect-square rounded-full overflow-hidden bg-black", children: [
1617
+ /* @__PURE__ */ jsx(
1618
+ "video",
1619
+ {
1620
+ ref: videoRef,
1621
+ playsInline: true,
1622
+ muted: true,
1623
+ className: "w-full h-full block bg-black object-cover -scale-x-100 origin-center"
1624
+ }
1625
+ ),
1626
+ /* @__PURE__ */ jsx(
1627
+ "canvas",
1628
+ {
1629
+ ref: faceCanvasRef,
1630
+ className: "absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
1631
+ }
1632
+ ),
1633
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 pointer-events-none z-[3]", children: /* @__PURE__ */ jsx("circle", { cx: "50", cy: "50", r: "44", fill: "none", stroke: "#22c55e", strokeWidth: "2", strokeDasharray: "1 3" }) })
1634
+ ] }),
1635
+ !state.livenessFailed && /* @__PURE__ */ 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: [
1636
+ /* @__PURE__ */ jsx("div", { className: "font-bold mb-2.5 text-[22px] text-white", children: "Liveness Check" }),
1637
+ /* @__PURE__ */ jsx("div", { className: "mb-2.5 text-base", children: state.livenessInstruction }),
1638
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2.5 text-lg", children: [
1639
+ /* @__PURE__ */ jsx("div", { className: state.livenessStage === "CENTER" || state.livenessStage === "LEFT" || state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "1. Look Straight" }),
1640
+ /* @__PURE__ */ jsx("div", { className: state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "2. Turn your face right" }),
1641
+ /* @__PURE__ */ jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1642
+ ] })
1643
+ ] }),
1497
1644
  /* @__PURE__ */ jsx(
1498
- "video",
1645
+ "button",
1499
1646
  {
1500
- ref: videoRef,
1501
- playsInline: true,
1502
- muted: true,
1503
- className: "w-full h-full block bg-black object-cover -scale-x-100 origin-center"
1647
+ type: "button",
1648
+ disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1649
+ onClick: handleFaceCapture,
1650
+ 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"}`,
1651
+ children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1504
1652
  }
1505
1653
  ),
1506
1654
  /* @__PURE__ */ jsx(
1507
- "canvas",
1655
+ "button",
1508
1656
  {
1509
- ref: faceCanvasRef,
1510
- className: "absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
1657
+ type: "button",
1658
+ onClick: handleRetry,
1659
+ disabled: state.loading,
1660
+ 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]"}`,
1661
+ children: "Restart"
1511
1662
  }
1512
- ),
1513
- /* @__PURE__ */ jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 pointer-events-none z-[3]", children: /* @__PURE__ */ jsx("circle", { cx: "50", cy: "50", r: "44", fill: "none", stroke: "#22c55e", strokeWidth: "2", strokeDasharray: "1 3" }) })
1514
- ] }),
1515
- !state.livenessFailed && /* @__PURE__ */ 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: [
1516
- /* @__PURE__ */ jsx("div", { className: "font-bold mb-2.5 text-[22px] text-white", children: "Liveness Check" }),
1517
- /* @__PURE__ */ jsx("div", { className: "mb-2.5 text-base", children: state.livenessInstruction }),
1518
- /* @__PURE__ */ jsxs("div", { className: "grid gap-2.5 text-lg", children: [
1519
- /* @__PURE__ */ jsx("div", { className: state.livenessStage === "CENTER" || state.livenessStage === "LEFT" || state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "1. Look Straight" }),
1520
- /* @__PURE__ */ jsx("div", { className: state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40", children: "2. Turn your face right" }),
1521
- /* @__PURE__ */ jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1522
- ] })
1523
- ] }),
1524
- /* @__PURE__ */ jsx(
1525
- "button",
1526
- {
1527
- type: "button",
1528
- disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1529
- onClick: handleFaceCapture,
1530
- 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"}`,
1531
- children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1532
- }
1533
- ),
1534
- /* @__PURE__ */ jsx(
1535
- "button",
1536
- {
1537
- type: "button",
1538
- onClick: handleRetry,
1539
- disabled: state.loading,
1540
- 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]"}`,
1541
- children: "Restart"
1542
- }
1543
- )
1544
- ] })
1545
- ] }) });
1663
+ )
1664
+ ] })
1665
+ ] }) })
1666
+ ] });
1546
1667
  }
1547
1668
  var FaceScanModal_default = FaceScanModal;
1548
1669
  function MobileRouteContent({ onClose, onComplete }) {