astra-sdk-web 1.1.8 → 1.1.10

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.
@@ -14,6 +14,12 @@ var React__default = /*#__PURE__*/_interopDefault(React);
14
14
  // src/components/KycFlow.tsx
15
15
 
16
16
  // src/services/kycApiService.ts
17
+ var COMPLETED_STEPS = {
18
+ INITIATED: "initiated",
19
+ FACE: "face_scan",
20
+ DOCS: "document_upload",
21
+ COMPLETED: "completed"
22
+ };
17
23
  var KycApiService = class {
18
24
  config;
19
25
  constructor(config) {
@@ -131,6 +137,36 @@ var KycApiService = class {
131
137
  throw new Error(`Document upload failed: ${message}`);
132
138
  }
133
139
  }
140
+ /**
141
+ * Retry session - resets the session to allow face registration again
142
+ */
143
+ async retrySession() {
144
+ const deviceType = this.config.deviceType || this.detectDeviceType();
145
+ try {
146
+ const response = await fetch(
147
+ `${this.config.apiBaseUrl}/api/v2/dashboard/merchant/onsite/session/${this.config.sessionId}/retry`,
148
+ {
149
+ method: "POST",
150
+ headers: {
151
+ "x-server-key": this.config.serverKey,
152
+ "device-type": deviceType,
153
+ "Content-Type": "application/json"
154
+ },
155
+ credentials: "include"
156
+ }
157
+ );
158
+ if (!response.ok) {
159
+ const errorData = await response.json().catch(() => ({}));
160
+ const message = errorData?.message || `Retry failed with status ${response.status}`;
161
+ throw new Error(message);
162
+ }
163
+ const data = await response.json();
164
+ return data;
165
+ } catch (error) {
166
+ const message = error?.message || "Retry failed";
167
+ throw new Error(`Retry failed: ${message}`);
168
+ }
169
+ }
134
170
  /**
135
171
  * Check if session is active, throw error if not
136
172
  */
@@ -405,6 +441,7 @@ function DocumentUploadModal({ onComplete }) {
405
441
  const navigate = reactRouterDom.useNavigate();
406
442
  const { apiService } = useKycContext();
407
443
  const [sessionError, setSessionError] = React.useState(null);
444
+ const [kycCompleted, setKycCompleted] = React.useState(false);
408
445
  const {
409
446
  state,
410
447
  setState,
@@ -420,6 +457,15 @@ function DocumentUploadModal({ onComplete }) {
420
457
  throw new Error("API service not initialized");
421
458
  }
422
459
  await apiService.uploadDocument(blob, docType);
460
+ try {
461
+ const statusResponse = await apiService.getSessionStatus();
462
+ const { completed_steps, status } = statusResponse.data;
463
+ if (status === "COMPLETED" || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) {
464
+ setKycCompleted(true);
465
+ }
466
+ } catch (error) {
467
+ console.error("Error checking completion status:", error);
468
+ }
423
469
  },
424
470
  onUpload: (file, docType) => {
425
471
  if (onComplete) {
@@ -436,7 +482,24 @@ function DocumentUploadModal({ onComplete }) {
436
482
  const checkSession = async () => {
437
483
  if (!apiService) return;
438
484
  try {
439
- await apiService.checkSessionActive();
485
+ const statusResponse = await apiService.getSessionStatus();
486
+ const { completed_steps, next_step, status } = statusResponse.data;
487
+ if (status === "COMPLETED" || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) {
488
+ setKycCompleted(true);
489
+ return;
490
+ }
491
+ if (status !== "ACTIVE") {
492
+ throw new Error("Session expired or inactive");
493
+ }
494
+ if (completed_steps.includes(COMPLETED_STEPS.DOCS)) {
495
+ if (completed_steps.includes(COMPLETED_STEPS.FACE) && completed_steps.includes(COMPLETED_STEPS.DOCS)) {
496
+ setKycCompleted(true);
497
+ return;
498
+ }
499
+ }
500
+ if (next_step === COMPLETED_STEPS.FACE && !completed_steps.includes(COMPLETED_STEPS.FACE)) {
501
+ console.warn("Face scan not completed, but in document upload modal");
502
+ }
440
503
  setSessionError(null);
441
504
  } catch (error) {
442
505
  const message = error.message || "Session expired or inactive";
@@ -448,6 +511,14 @@ function DocumentUploadModal({ onComplete }) {
448
511
  };
449
512
  checkSession();
450
513
  }, [apiService, navigate]);
514
+ if (kycCompleted) {
515
+ 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.jsx("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
516
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4 text-6xl", children: "\u2705" }),
517
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-green-500", children: "KYC Completed" }),
518
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[#e5e7eb] mb-4 text-lg", children: "All steps have been completed successfully." }),
519
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[#9ca3af] text-sm mb-6", children: "Please return to your desktop to continue." })
520
+ ] }) }) });
521
+ }
451
522
  if (sessionError) {
452
523
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 bg-black p-4 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
453
524
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-red-500", children: "Session Expired" }),
@@ -882,7 +953,8 @@ var FaceMeshService = class {
882
953
  }
883
954
  }
884
955
  } else if (state.stage === "DONE") {
885
- if (absYaw < centerThreshold && insideGuide) {
956
+ const reducedThreshold = 0.08;
957
+ if (absYaw < reducedThreshold) {
886
958
  state.centerHold += 1;
887
959
  if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
888
960
  state.snapTriggered = true;
@@ -896,11 +968,7 @@ var FaceMeshService = class {
896
968
  } else {
897
969
  state.centerHold = 0;
898
970
  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
- }
971
+ this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
904
972
  }
905
973
  }
906
974
  }
@@ -1062,7 +1130,7 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1062
1130
  }, []);
1063
1131
  const handleFaceCapture = React.useCallback(async () => {
1064
1132
  if (!videoRef.current) return;
1065
- const centerThreshold = 0.05;
1133
+ const reducedThreshold = 0.08;
1066
1134
  const currentAbsYaw = livenessStateRef.current.currentAbsYaw;
1067
1135
  if (currentAbsYaw === null || currentAbsYaw === void 0) {
1068
1136
  setState((prev) => ({
@@ -1071,7 +1139,7 @@ function useFaceScan(videoRef, canvasRef, callbacks) {
1071
1139
  }));
1072
1140
  return;
1073
1141
  }
1074
- if (currentAbsYaw >= centerThreshold) {
1142
+ if (currentAbsYaw >= reducedThreshold) {
1075
1143
  setState((prev) => ({
1076
1144
  ...prev,
1077
1145
  livenessInstruction: "Please look straight at the camera before capturing"
@@ -1287,6 +1355,9 @@ function FaceScanModal({ onComplete }) {
1287
1355
  const { apiService } = useKycContext();
1288
1356
  const [sessionError, setSessionError] = React.useState(null);
1289
1357
  const [toast, setToast] = React.useState(null);
1358
+ const [showRetryButton, setShowRetryButton] = React.useState(false);
1359
+ const [isRetrying, setIsRetrying] = React.useState(false);
1360
+ const [kycCompleted, setKycCompleted] = React.useState(false);
1290
1361
  const { videoRef, cameraReady, stopCamera } = useCamera();
1291
1362
  const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, {
1292
1363
  onFaceUpload: async (blob) => {
@@ -1299,10 +1370,12 @@ function FaceScanModal({ onComplete }) {
1299
1370
  const errorMessage = error?.message || "";
1300
1371
  const errorData = error?.errorData || {};
1301
1372
  if (errorMessage.includes("Face already registered") || errorMessage.includes("already registered") || errorData?.message?.includes("Face already registered") || error?.statusCode === 500 && errorMessage.includes("Face")) {
1373
+ setShowRetryButton(true);
1302
1374
  setToast({
1303
- message: "Face already registered",
1375
+ message: "Face already registered. Click Retry to register again.",
1304
1376
  type: "warning"
1305
1377
  });
1378
+ setState((prev) => ({ ...prev, loading: false }));
1306
1379
  return;
1307
1380
  }
1308
1381
  throw error;
@@ -1318,7 +1391,25 @@ function FaceScanModal({ onComplete }) {
1318
1391
  const checkSession = async () => {
1319
1392
  if (!apiService) return;
1320
1393
  try {
1321
- await apiService.checkSessionActive();
1394
+ const statusResponse = await apiService.getSessionStatus();
1395
+ const { completed_steps, next_step, status } = statusResponse.data;
1396
+ if (status !== "ACTIVE") {
1397
+ throw new Error("Session expired or inactive");
1398
+ }
1399
+ if (status === "COMPLETED" || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) {
1400
+ setKycCompleted(true);
1401
+ return;
1402
+ }
1403
+ if (completed_steps.includes(COMPLETED_STEPS.FACE)) {
1404
+ setState((prev) => ({ ...prev, showDocumentUpload: true }));
1405
+ return;
1406
+ }
1407
+ if (next_step !== COMPLETED_STEPS.FACE && next_step !== COMPLETED_STEPS.INITIATED) {
1408
+ if (next_step === COMPLETED_STEPS.DOCS) {
1409
+ setState((prev) => ({ ...prev, showDocumentUpload: true }));
1410
+ return;
1411
+ }
1412
+ }
1322
1413
  setSessionError(null);
1323
1414
  } catch (error) {
1324
1415
  const message = error.message || "Session expired or inactive";
@@ -1329,7 +1420,7 @@ function FaceScanModal({ onComplete }) {
1329
1420
  }
1330
1421
  };
1331
1422
  checkSession();
1332
- }, [apiService, navigate]);
1423
+ }, [apiService, navigate, setState]);
1333
1424
  React.useEffect(() => {
1334
1425
  setState((prev) => ({ ...prev, cameraReady }));
1335
1426
  }, [cameraReady, setState]);
@@ -1356,7 +1447,47 @@ function FaceScanModal({ onComplete }) {
1356
1447
  }
1357
1448
  }
1358
1449
  }, [cameraReady, apiService]);
1359
- const handleRetry = () => {
1450
+ const handleRetry = async () => {
1451
+ if (!apiService || isRetrying) return;
1452
+ setIsRetrying(true);
1453
+ setShowRetryButton(false);
1454
+ setToast(null);
1455
+ try {
1456
+ await apiService.retrySession();
1457
+ stopCamera();
1458
+ setState({
1459
+ cameraReady: false,
1460
+ livenessStage: "CENTER",
1461
+ livenessReady: false,
1462
+ livenessFailed: false,
1463
+ modelLoading: true,
1464
+ modelLoaded: false,
1465
+ livenessInstruction: "Look straight at the camera",
1466
+ loading: false,
1467
+ allStepsCompleted: false,
1468
+ capturedImage: null,
1469
+ showDocumentUpload: false
1470
+ });
1471
+ refs.centerHold.current = 0;
1472
+ refs.leftHold.current = 0;
1473
+ refs.rightHold.current = 0;
1474
+ refs.snapTriggered.current = false;
1475
+ refs.lastResultsAt.current = 0;
1476
+ refs.modelLoaded.current = false;
1477
+ refs.livenessFailed.current = false;
1478
+ setTimeout(() => {
1479
+ window.location.reload();
1480
+ }, 500);
1481
+ } catch (error) {
1482
+ setIsRetrying(false);
1483
+ setShowRetryButton(true);
1484
+ setToast({
1485
+ message: error?.message || "Retry failed. Please try again.",
1486
+ type: "error"
1487
+ });
1488
+ }
1489
+ };
1490
+ const handleRestart = () => {
1360
1491
  stopCamera();
1361
1492
  setState({
1362
1493
  cameraReady: false,
@@ -1382,6 +1513,14 @@ function FaceScanModal({ onComplete }) {
1382
1513
  window.location.reload();
1383
1514
  }, 100);
1384
1515
  };
1516
+ if (kycCompleted) {
1517
+ 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.jsx("div", { className: "max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
1518
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4 text-6xl", children: "\u2705" }),
1519
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 mb-4 text-[26px] font-bold text-green-500", children: "KYC Completed" }),
1520
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[#e5e7eb] mb-4 text-lg", children: "All steps have been completed successfully." }),
1521
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[#9ca3af] text-sm mb-6", children: "Please return to your desktop to continue." })
1522
+ ] }) }) });
1523
+ }
1385
1524
  if (state.showDocumentUpload) {
1386
1525
  return /* @__PURE__ */ jsxRuntime.jsx(
1387
1526
  DocumentUploadModal_default,
@@ -1444,13 +1583,23 @@ function FaceScanModal({ onComplete }) {
1444
1583
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: state.livenessStage === "DONE" ? "opacity-100" : "opacity-30", children: "3. Turn your face left" })
1445
1584
  ] })
1446
1585
  ] }),
1586
+ showRetryButton && /* @__PURE__ */ jsxRuntime.jsx(
1587
+ "button",
1588
+ {
1589
+ type: "button",
1590
+ onClick: handleRetry,
1591
+ disabled: isRetrying || state.loading,
1592
+ className: "py-3.5 px-4 rounded-xl text-base font-bold border-none transition-colors bg-[#f59e0b] text-[#0b0f17] cursor-pointer hover:bg-[#d97706] disabled:opacity-50 disabled:cursor-not-allowed",
1593
+ children: isRetrying ? "Retrying..." : "Retry Face Registration"
1594
+ }
1595
+ ),
1447
1596
  /* @__PURE__ */ jsxRuntime.jsx(
1448
1597
  "button",
1449
1598
  {
1450
1599
  type: "button",
1451
- disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE",
1600
+ disabled: !state.cameraReady || state.loading || !state.livenessFailed && state.livenessStage !== "DONE" || showRetryButton,
1452
1601
  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"}`,
1602
+ 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") && !showRetryButton ? "bg-[#22c55e] text-[#0b0f17] cursor-pointer hover:bg-[#16a34a]" : "bg-[#374151] text-[#e5e7eb] cursor-not-allowed"}`,
1454
1603
  children: state.loading ? "Capturing..." : state.livenessFailed || state.livenessStage === "DONE" ? "Capture & Continue" : "Complete steps to continue"
1455
1604
  }
1456
1605
  ),
@@ -1458,9 +1607,9 @@ function FaceScanModal({ onComplete }) {
1458
1607
  "button",
1459
1608
  {
1460
1609
  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]"}`,
1610
+ onClick: handleRestart,
1611
+ disabled: state.loading || isRetrying,
1612
+ className: `py-3 px-4 rounded-[10px] text-[15px] font-semibold border-none w-full transition-colors ${state.loading || isRetrying ? "bg-[#374151] text-[#e5e7eb] cursor-not-allowed opacity-50" : "bg-[#374151] text-[#e5e7eb] cursor-pointer hover:bg-[#4b5563]"}`,
1464
1613
  children: "Restart"
1465
1614
  }
1466
1615
  )