vesant-sdk 1.7.1-dev.8d808aa → 1.7.1-dev.a461ee2

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/dist/react.mjs CHANGED
@@ -924,19 +924,19 @@ var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id
924
924
  var FACE_MESSAGES = {
925
925
  idle: "Initializing camera...",
926
926
  loading: "Loading face detection...",
927
- scanning: "Position your face in the circle",
927
+ scanning: "Position your face in the oval",
928
928
  too_far: "Face too far. Please move closer to the camera",
929
929
  too_close: "Too close. Please move back a little",
930
- off_center: "Center your face in the circle",
930
+ off_center: "Center your face in the oval",
931
931
  ok: "Looking great! Tap the button to capture"
932
932
  };
933
933
  var FACE_COLORS = {
934
934
  idle: "#9ca3af",
935
- loading: "#3b82f6",
936
- scanning: "#3b82f6",
935
+ loading: "#ef4444",
936
+ scanning: "#ef4444",
937
937
  too_far: "#ef4444",
938
- too_close: "#f59e0b",
939
- off_center: "#f59e0b",
938
+ too_close: "#ef4444",
939
+ off_center: "#ef4444",
940
940
  ok: "#10b981"
941
941
  };
942
942
  var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
@@ -1001,11 +1001,27 @@ function loadMediaPipeFaceDetection(assetBasePath) {
1001
1001
  return loader;
1002
1002
  }
1003
1003
 
1004
+ // src/kyc/hostedJourney.ts
1005
+ var REUSE_KYC_SESSION_EXPIRY_MS = 10 * 60 * 1e3;
1006
+ function tryStartOperation(guard) {
1007
+ if (guard.current) return false;
1008
+ guard.current = true;
1009
+ return true;
1010
+ }
1011
+ function releaseOperation(guard) {
1012
+ guard.current = false;
1013
+ }
1014
+ function retryTarget(hasHostedJourney) {
1015
+ return hasHostedJourney ? "journey" : "capture";
1016
+ }
1017
+ function modalMaxWidth(hasHostedJourney) {
1018
+ return hasHostedJourney ? 680 : 448;
1019
+ }
1020
+
1004
1021
  // src/kyc/FaceCaptureModal.tsx
1005
1022
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
1006
- var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
1007
1023
  var LIVENESS_MESSAGES = [
1008
- "Position your face inside the circle",
1024
+ "Position your face inside the oval",
1009
1025
  "Make sure your face is well lit",
1010
1026
  "Remove glasses or hats if possible",
1011
1027
  "Look straight at the camera, then tap Capture"
@@ -1014,6 +1030,8 @@ function headerSubtitle(stageKind) {
1014
1030
  switch (stageKind) {
1015
1031
  case "qr":
1016
1032
  return "Use your phone to continue";
1033
+ case "journey":
1034
+ return "Follow the steps to verify your identity";
1017
1035
  case "accepted":
1018
1036
  return "All set";
1019
1037
  case "declined":
@@ -1039,7 +1057,9 @@ function FaceCaptureModal({
1039
1057
  }) {
1040
1058
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
1041
1059
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
1060
+ const journeyMode = Boolean(session.verification_url);
1042
1061
  const initialStage = (() => {
1062
+ if (journeyMode) return { kind: "journey", url: session.verification_url ?? "" };
1043
1063
  if (initialChoice === "this") return { kind: "capture" };
1044
1064
  if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
1045
1065
  return { kind: "choose" };
@@ -1057,19 +1077,20 @@ function FaceCaptureModal({
1057
1077
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
1058
1078
  const cancelledRef = useRef(false);
1059
1079
  const submittingRef = useRef(false);
1080
+ const journeyRetryingRef = useRef(false);
1060
1081
  useEffect(() => {
1061
1082
  const timer = setTimeout(() => {
1062
1083
  setStage(
1063
- (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
1084
+ (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" || prev.kind === "journey" ? { kind: "error", message: "Session expired" } : prev
1064
1085
  );
1065
- }, SESSION_EXPIRY_MS);
1086
+ }, REUSE_KYC_SESSION_EXPIRY_MS);
1066
1087
  return () => clearTimeout(timer);
1067
1088
  }, [session.token]);
1068
1089
  useEffect(() => {
1069
1090
  if (stage.kind !== "qr") return;
1070
1091
  let stopped = false;
1071
1092
  const intervalMs = 2e3;
1072
- const deadline = Date.now() + SESSION_EXPIRY_MS;
1093
+ const deadline = Date.now() + REUSE_KYC_SESSION_EXPIRY_MS;
1073
1094
  let mobileSeen = stage.mobileConnected;
1074
1095
  const tick = async () => {
1075
1096
  if (stopped || cancelledRef.current) return;
@@ -1110,6 +1131,64 @@ function FaceCaptureModal({
1110
1131
  stopped = true;
1111
1132
  };
1112
1133
  }, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
1134
+ useEffect(() => {
1135
+ if (stage.kind !== "journey" || !stage.url) return;
1136
+ let stopped = false;
1137
+ const checkOnce = async () => {
1138
+ try {
1139
+ const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
1140
+ if (applyResult(result)) {
1141
+ stopped = true;
1142
+ return true;
1143
+ }
1144
+ } catch {
1145
+ }
1146
+ return false;
1147
+ };
1148
+ const tick = async () => {
1149
+ if (stopped || cancelledRef.current) return;
1150
+ await checkOnce();
1151
+ if (!stopped && !cancelledRef.current) {
1152
+ setTimeout(tick, 2500);
1153
+ }
1154
+ };
1155
+ void tick();
1156
+ let frameOrigin = "";
1157
+ try {
1158
+ frameOrigin = new URL(stage.url).origin;
1159
+ } catch {
1160
+ frameOrigin = "";
1161
+ }
1162
+ const onMessage = (e) => {
1163
+ if (!frameOrigin || e.origin !== frameOrigin) return;
1164
+ const status = e.data?.verification_status;
1165
+ if (status) void checkOnce();
1166
+ };
1167
+ window.addEventListener("message", onMessage);
1168
+ return () => {
1169
+ stopped = true;
1170
+ window.removeEventListener("message", onMessage);
1171
+ };
1172
+ }, [stage.kind, stage.kind === "journey" ? stage.url : ""]);
1173
+ const handleJourneyRetry = async () => {
1174
+ if (!tryStartOperation(journeyRetryingRef)) return;
1175
+ setStage({ kind: "journey", url: "" });
1176
+ try {
1177
+ const refreshed = await client.retryEventBasedFaceVerificationJourney(session.token);
1178
+ if (refreshed.verification_url) {
1179
+ setStage({ kind: "journey", url: refreshed.verification_url });
1180
+ return;
1181
+ }
1182
+ setStage({ kind: "error", message: "Unable to start a new attempt. Please try again." });
1183
+ } catch (err) {
1184
+ setStage({
1185
+ kind: "error",
1186
+ message: err instanceof Error ? err.message : "Unable to start a new attempt. Please try again."
1187
+ });
1188
+ } finally {
1189
+ releaseOperation(journeyRetryingRef);
1190
+ }
1191
+ };
1113
1192
  const applyResult = (result) => {
1114
1193
  if (result.status === "accepted") {
1115
1194
  setStage({ kind: "accepted", result });
@@ -1301,12 +1380,28 @@ function FaceCaptureModal({
1301
1380
  };
1302
1381
  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."));
1303
1382
  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")));
1304
- 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));
1383
+ 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 }, journeyMode ? /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, /* @__PURE__ */ React.createElement("span", null, "Try Again")) : /* @__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));
1305
1384
  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")));
1306
1385
  const renderError = (message) => {
1307
1386
  const sessionExpired = /session\s+expired/i.test(message);
1308
- 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")));
1387
+ 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(
1388
+ "button",
1389
+ {
1390
+ style: primaryButtonStyle,
1391
+ onClick: () => retryTarget(journeyMode) === "journey" ? void handleJourneyRetry() : setStage({ kind: "capture" })
1392
+ },
1393
+ "Try Again"
1394
+ ), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1309
1395
  };
1396
+ const renderJourney = (url) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, url ? /* @__PURE__ */ React.createElement(
1397
+ "iframe",
1398
+ {
1399
+ src: url,
1400
+ title: "Identity verification",
1401
+ allow: "camera; microphone",
1402
+ style: journeyFrameStyle
1403
+ }
1404
+ ) : /* @__PURE__ */ React.createElement("div", { style: { padding: 24, textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Preparing your next attempt\u2026")), /* @__PURE__ */ React.createElement("p", { style: { ...attemptsTextStyle, textAlign: "center" } }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1310
1405
  const body = (() => {
1311
1406
  switch (stage.kind) {
1312
1407
  case "choose":
@@ -1315,6 +1410,8 @@ function FaceCaptureModal({
1315
1410
  return renderQRStage(stage.mobileConnected);
1316
1411
  case "capture":
1317
1412
  return renderCapture(stage.declinedReason);
1413
+ case "journey":
1414
+ return renderJourney(stage.url);
1318
1415
  case "submitting":
1319
1416
  return renderSubmitting();
1320
1417
  case "accepted":
@@ -1327,7 +1424,7 @@ function FaceCaptureModal({
1327
1424
  return renderError(stage.message);
1328
1425
  }
1329
1426
  })();
1330
- 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(
1427
+ 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, maxWidth: modalMaxWidth(journeyMode) } }, /* @__PURE__ */ React.createElement("div", { style: headerStyle }, /* @__PURE__ */ React.createElement(
1331
1428
  "button",
1332
1429
  {
1333
1430
  onClick: () => close(null),
@@ -1360,7 +1457,8 @@ var modalStyle = {
1360
1457
  borderRadius: 16,
1361
1458
  boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
1362
1459
  width: "100%",
1363
- maxWidth: 448,
1460
+ maxHeight: "calc(100vh - 32px)",
1461
+ overflowY: "auto",
1364
1462
  animation: "zoomIn .2s ease-out"
1365
1463
  };
1366
1464
  var headerStyle = {
@@ -1386,6 +1484,13 @@ var contentStyle = {
1386
1484
  flexDirection: "column",
1387
1485
  gap: 16
1388
1486
  };
1487
+ var journeyFrameStyle = {
1488
+ width: "100%",
1489
+ height: "min(520px, 60vh)",
1490
+ border: 0,
1491
+ borderRadius: 12,
1492
+ background: "#f9fafb"
1493
+ };
1389
1494
  var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
1390
1495
  var visualGuideStyle = { position: "relative" };
1391
1496
  var circleStyle = {
@@ -1652,9 +1757,10 @@ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1652
1757
  if (error) {
1653
1758
  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")));
1654
1759
  }
1655
- const detectionActive = ready && !faceDetectionFailed;
1656
- const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1657
- const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1760
+ const detectionAvailable = !faceDetectionFailed;
1761
+ const detectionActive = ready && detectionAvailable;
1762
+ const liveMessage = detectionAvailable ? FACE_MESSAGES[faceStatus] : message;
1763
+ const liveFaceColor = detectionAvailable ? FACE_COLORS[faceStatus] : void 0;
1658
1764
  const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1659
1765
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1660
1766
  const dynamicCircleStyle = {
@@ -1770,20 +1876,22 @@ var faceOverlayContainerStyle = {
1770
1876
  };
1771
1877
  var faceRingWrapStyle = {
1772
1878
  position: "relative",
1773
- width: "72%",
1774
- aspectRatio: "1 / 1"
1879
+ height: "64%",
1880
+ aspectRatio: "3 / 4"
1775
1881
  };
1776
1882
  var faceDashedRingStyle = {
1777
1883
  position: "absolute",
1778
1884
  inset: -6,
1779
- borderRadius: "50%",
1780
- border: "6px dashed rgba(255, 255, 255, 0.4)",
1781
- animation: "spin 10s linear infinite"
1885
+ borderRadius: 9999,
1886
+ border: "6px dashed rgba(255, 255, 255, 0.4)"
1782
1887
  };
1783
1888
  var faceCircleStyle = {
1784
1889
  position: "absolute",
1785
1890
  inset: 0,
1786
- borderRadius: "50%",
1891
+ // Tailwind's `rounded-full` resolves to a very large fixed radius. On a
1892
+ // 3:4 box that creates the normal KYC capsule/head guide with straighter
1893
+ // sides; 50% would instead produce a mathematically perfect ellipse.
1894
+ borderRadius: 9999,
1787
1895
  border: "4px solid #00bc7d",
1788
1896
  boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
1789
1897
  };