vesant-sdk 1.7.2-dev.617dd44 → 1.7.2

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,77 @@ 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
+ var JOURNEY_FRAME_HEIGHT = "clamp(500px, 64vh, 680px)";
1007
+ var JOURNEY_CONFIRM_TIMEOUT_MS = 45e3;
1008
+ function journeyOutcomeFromMessage(data) {
1009
+ const status = data?.verification_status;
1010
+ if (typeof status !== "string") return null;
1011
+ switch (status) {
1012
+ case "verification.accepted":
1013
+ return "accepted";
1014
+ case "verification.declined":
1015
+ return "declined";
1016
+ case "verification.cancelled":
1017
+ case "request.timeout":
1018
+ case "request.invalid":
1019
+ case "request.unauthorized":
1020
+ case "request.deleted":
1021
+ return "cancelled";
1022
+ default:
1023
+ return null;
1024
+ }
1025
+ }
1026
+ function journeyPollIntervalMs(confirming) {
1027
+ return confirming ? 1e3 : 2500;
1028
+ }
1029
+ function pendingJourneyResult(reference) {
1030
+ return {
1031
+ event: "reuse_kyc",
1032
+ reference,
1033
+ resource_id: "",
1034
+ status: "pending",
1035
+ data: {
1036
+ retries_remaining: 0,
1037
+ retry_limit_exceeded: false,
1038
+ trigger_alert: false,
1039
+ enforce_logout: false,
1040
+ freeze_account: false,
1041
+ block: false,
1042
+ freeze_duration_minutes: 0
1043
+ }
1044
+ };
1045
+ }
1046
+ function tryStartOperation(guard) {
1047
+ if (guard.current) return false;
1048
+ guard.current = true;
1049
+ return true;
1050
+ }
1051
+ function releaseOperation(guard) {
1052
+ guard.current = false;
1053
+ }
1054
+ function retryTarget(hasHostedJourney) {
1055
+ return hasHostedJourney ? "journey" : "capture";
1056
+ }
1057
+ var CAPTURE_MODAL_MAX_WIDTH = 448;
1058
+ function modalMaxWidth(hasHostedJourney) {
1059
+ return hasHostedJourney ? CAPTURE_MODAL_MAX_WIDTH : CAPTURE_MODAL_MAX_WIDTH;
1060
+ }
1061
+ var JOURNEY_CONTENT_PADDING = 12;
1062
+ function resultForModalClose(stageKind, result, unconfirmed) {
1063
+ const terminal = stageKind === "accepted" || stageKind === "declined" || stageKind === "max_attempts";
1064
+ if (terminal && result !== void 0) return result;
1065
+ const awaitingConfirmation = stageKind === "confirming" || stageKind === "unconfirmed";
1066
+ if (awaitingConfirmation && unconfirmed !== void 0) return unconfirmed;
1067
+ return null;
1068
+ }
1069
+
1004
1070
  // src/kyc/FaceCaptureModal.tsx
1005
1071
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
1006
- var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
1072
+ var ACCEPTED_AUTO_CLOSE_MS = 1200;
1007
1073
  var LIVENESS_MESSAGES = [
1008
- "Position your face inside the circle",
1074
+ "Position your face inside the oval",
1009
1075
  "Make sure your face is well lit",
1010
1076
  "Remove glasses or hats if possible",
1011
1077
  "Look straight at the camera, then tap Capture"
@@ -1014,6 +1080,12 @@ function headerSubtitle(stageKind) {
1014
1080
  switch (stageKind) {
1015
1081
  case "qr":
1016
1082
  return "Use your phone to continue";
1083
+ case "journey":
1084
+ return "Follow the steps to verify your identity";
1085
+ case "confirming":
1086
+ return "Confirming your result";
1087
+ case "unconfirmed":
1088
+ return "Still confirming";
1017
1089
  case "accepted":
1018
1090
  return "All set";
1019
1091
  case "declined":
@@ -1039,7 +1111,9 @@ function FaceCaptureModal({
1039
1111
  }) {
1040
1112
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
1041
1113
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
1114
+ const journeyMode = Boolean(session.verification_url);
1042
1115
  const initialStage = (() => {
1116
+ if (journeyMode) return { kind: "journey", url: session.verification_url ?? "" };
1043
1117
  if (initialChoice === "this") return { kind: "capture" };
1044
1118
  if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
1045
1119
  return { kind: "choose" };
@@ -1057,19 +1131,20 @@ function FaceCaptureModal({
1057
1131
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
1058
1132
  const cancelledRef = useRef(false);
1059
1133
  const submittingRef = useRef(false);
1134
+ const journeyRetryingRef = useRef(false);
1060
1135
  useEffect(() => {
1061
1136
  const timer = setTimeout(() => {
1062
1137
  setStage(
1063
- (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
1138
+ (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" || prev.kind === "journey" ? { kind: "error", message: "Session expired" } : prev
1064
1139
  );
1065
- }, SESSION_EXPIRY_MS);
1140
+ }, REUSE_KYC_SESSION_EXPIRY_MS);
1066
1141
  return () => clearTimeout(timer);
1067
1142
  }, [session.token]);
1068
1143
  useEffect(() => {
1069
1144
  if (stage.kind !== "qr") return;
1070
1145
  let stopped = false;
1071
1146
  const intervalMs = 2e3;
1072
- const deadline = Date.now() + SESSION_EXPIRY_MS;
1147
+ const deadline = Date.now() + REUSE_KYC_SESSION_EXPIRY_MS;
1073
1148
  let mobileSeen = stage.mobileConnected;
1074
1149
  const tick = async () => {
1075
1150
  if (stopped || cancelledRef.current) return;
@@ -1110,6 +1185,82 @@ function FaceCaptureModal({
1110
1185
  stopped = true;
1111
1186
  };
1112
1187
  }, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
1188
+ const journeyUrl = stage.kind === "journey" || stage.kind === "confirming" ? stage.url : "";
1189
+ useEffect(() => {
1190
+ if (stage.kind !== "journey" && stage.kind !== "confirming" || !journeyUrl) return;
1191
+ const intervalMs = journeyPollIntervalMs(stage.kind === "confirming");
1192
+ let stopped = false;
1193
+ const checkOnce = async () => {
1194
+ try {
1195
+ const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
1196
+ if (applyResult(result)) {
1197
+ stopped = true;
1198
+ return true;
1199
+ }
1200
+ } catch {
1201
+ }
1202
+ return false;
1203
+ };
1204
+ const tick = async () => {
1205
+ if (stopped || cancelledRef.current) return;
1206
+ await checkOnce();
1207
+ if (!stopped && !cancelledRef.current) {
1208
+ setTimeout(tick, intervalMs);
1209
+ }
1210
+ };
1211
+ void tick();
1212
+ let frameOrigin = "";
1213
+ try {
1214
+ frameOrigin = new URL(journeyUrl).origin;
1215
+ } catch {
1216
+ frameOrigin = "";
1217
+ }
1218
+ const onMessage = (e) => {
1219
+ if (!frameOrigin || e.origin !== frameOrigin) return;
1220
+ const outcome = journeyOutcomeFromMessage(e.data);
1221
+ if (!outcome) return;
1222
+ void checkOnce();
1223
+ setStage((prev) => {
1224
+ if (prev.kind !== "journey") return prev;
1225
+ if (outcome === "cancelled") {
1226
+ return { kind: "error", message: "The verification was closed before it finished. Please try again." };
1227
+ }
1228
+ return { kind: "confirming", outcome, url: prev.url };
1229
+ });
1230
+ };
1231
+ window.addEventListener("message", onMessage);
1232
+ return () => {
1233
+ stopped = true;
1234
+ window.removeEventListener("message", onMessage);
1235
+ };
1236
+ }, [stage.kind, journeyUrl]);
1237
+ useEffect(() => {
1238
+ if (stage.kind !== "confirming") return;
1239
+ const outcome = stage.outcome;
1240
+ const timer = setTimeout(() => {
1241
+ setStage((prev) => prev.kind === "confirming" ? { kind: "unconfirmed", outcome } : prev);
1242
+ }, JOURNEY_CONFIRM_TIMEOUT_MS);
1243
+ return () => clearTimeout(timer);
1244
+ }, [stage.kind, stage.kind === "confirming" ? stage.outcome : ""]);
1245
+ const handleJourneyRetry = async () => {
1246
+ if (!tryStartOperation(journeyRetryingRef)) return;
1247
+ setStage({ kind: "journey", url: "" });
1248
+ try {
1249
+ const refreshed = await client.retryEventBasedFaceVerificationJourney(session.token);
1250
+ if (refreshed.verification_url) {
1251
+ setStage({ kind: "journey", url: refreshed.verification_url });
1252
+ return;
1253
+ }
1254
+ setStage({ kind: "error", message: "Unable to start a new attempt. Please try again." });
1255
+ } catch (err) {
1256
+ setStage({
1257
+ kind: "error",
1258
+ message: err instanceof Error ? err.message : "Unable to start a new attempt. Please try again."
1259
+ });
1260
+ } finally {
1261
+ releaseOperation(journeyRetryingRef);
1262
+ }
1263
+ };
1113
1264
  const applyResult = (result) => {
1114
1265
  if (result.status === "accepted") {
1115
1266
  setStage({ kind: "accepted", result });
@@ -1237,12 +1388,23 @@ function FaceCaptureModal({
1237
1388
  }, 3e3);
1238
1389
  return () => clearInterval(interval);
1239
1390
  }, [captureMode]);
1240
- const close = (result) => {
1391
+ const close = useCallback((result) => {
1241
1392
  cancelledRef.current = true;
1242
1393
  if (result === null && onCancel) {
1243
1394
  onCancel();
1244
1395
  }
1245
1396
  onComplete(result);
1397
+ }, [onCancel, onComplete]);
1398
+ useEffect(() => {
1399
+ if (stage.kind !== "accepted") return;
1400
+ const result = stage.result;
1401
+ const timer = setTimeout(() => close(result), ACCEPTED_AUTO_CLOSE_MS);
1402
+ return () => clearTimeout(timer);
1403
+ }, [close, stage]);
1404
+ const handleHeaderClose = () => {
1405
+ const result = "result" in stage ? stage.result : void 0;
1406
+ const unconfirmed = stage.kind === "confirming" || stage.kind === "unconfirmed" ? pendingJourneyResult(session.reference) : void 0;
1407
+ close(resultForModalClose(stage.kind, result, unconfirmed));
1246
1408
  };
1247
1409
  const renderChoose = () => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React.createElement("div", { style: circleStyle }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })))), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "How would you like to verify?"), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement(
1248
1410
  "button",
@@ -1301,12 +1463,37 @@ function FaceCaptureModal({
1301
1463
  };
1302
1464
  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
1465
  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));
1466
+ 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
1467
  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
1468
  const renderError = (message) => {
1307
1469
  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")));
1470
+ 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(
1471
+ "button",
1472
+ {
1473
+ style: primaryButtonStyle,
1474
+ onClick: () => retryTarget(journeyMode) === "journey" ? void handleJourneyRetry() : setStage({ kind: "capture" })
1475
+ },
1476
+ "Try Again"
1477
+ ), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1309
1478
  };
1479
+ const renderConfirming = () => /* @__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("h3", { style: titleStyle }, "Confirming your result\u2026"), /* @__PURE__ */ React.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
1480
+ const renderUnconfirmed = (outcome) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Still confirming"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, outcome === "accepted" ? "Your verification was submitted, but we haven't received the final result yet. You can close this window and we'll keep checking in the background." : "Your last attempt wasn't accepted, but we haven't received the details yet. You can try again or close this window."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, outcome === "declined" && /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, "Try Again"), /* @__PURE__ */ React.createElement(
1481
+ "button",
1482
+ {
1483
+ style: outcome === "accepted" ? primaryButtonStyle : cancelButtonStyle,
1484
+ onClick: () => close(pendingJourneyResult(session.reference))
1485
+ },
1486
+ "Close"
1487
+ )));
1488
+ const renderJourney = (url) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, padding: JOURNEY_CONTENT_PADDING } }, url ? /* @__PURE__ */ React.createElement(
1489
+ "iframe",
1490
+ {
1491
+ src: url,
1492
+ title: "Identity verification",
1493
+ allow: "camera; microphone",
1494
+ style: journeyFrameStyle
1495
+ }
1496
+ ) : /* @__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
1497
  const body = (() => {
1311
1498
  switch (stage.kind) {
1312
1499
  case "choose":
@@ -1315,6 +1502,12 @@ function FaceCaptureModal({
1315
1502
  return renderQRStage(stage.mobileConnected);
1316
1503
  case "capture":
1317
1504
  return renderCapture(stage.declinedReason);
1505
+ case "journey":
1506
+ return renderJourney(stage.url);
1507
+ case "confirming":
1508
+ return renderConfirming();
1509
+ case "unconfirmed":
1510
+ return renderUnconfirmed(stage.outcome);
1318
1511
  case "submitting":
1319
1512
  return renderSubmitting();
1320
1513
  case "accepted":
@@ -1327,10 +1520,10 @@ function FaceCaptureModal({
1327
1520
  return renderError(stage.message);
1328
1521
  }
1329
1522
  })();
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(
1523
+ 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
1524
  "button",
1332
1525
  {
1333
- onClick: () => close(null),
1526
+ onClick: handleHeaderClose,
1334
1527
  style: closeButtonStyle,
1335
1528
  "aria-label": "Close"
1336
1529
  },
@@ -1360,7 +1553,8 @@ var modalStyle = {
1360
1553
  borderRadius: 16,
1361
1554
  boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
1362
1555
  width: "100%",
1363
- maxWidth: 448,
1556
+ maxHeight: "calc(100vh - 32px)",
1557
+ overflowY: "auto",
1364
1558
  animation: "zoomIn .2s ease-out"
1365
1559
  };
1366
1560
  var headerStyle = {
@@ -1386,6 +1580,13 @@ var contentStyle = {
1386
1580
  flexDirection: "column",
1387
1581
  gap: 16
1388
1582
  };
1583
+ var journeyFrameStyle = {
1584
+ width: "100%",
1585
+ height: JOURNEY_FRAME_HEIGHT,
1586
+ border: 0,
1587
+ borderRadius: 12,
1588
+ background: "#f9fafb"
1589
+ };
1389
1590
  var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
1390
1591
  var visualGuideStyle = { position: "relative" };
1391
1592
  var circleStyle = {
@@ -1652,9 +1853,10 @@ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1652
1853
  if (error) {
1653
1854
  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
1855
  }
1655
- const detectionActive = ready && !faceDetectionFailed;
1656
- const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1657
- const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1856
+ const detectionAvailable = !faceDetectionFailed;
1857
+ const detectionActive = ready && detectionAvailable;
1858
+ const liveMessage = detectionAvailable ? FACE_MESSAGES[faceStatus] : message;
1859
+ const liveFaceColor = detectionAvailable ? FACE_COLORS[faceStatus] : void 0;
1658
1860
  const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1659
1861
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1660
1862
  const dynamicCircleStyle = {
@@ -1770,20 +1972,22 @@ var faceOverlayContainerStyle = {
1770
1972
  };
1771
1973
  var faceRingWrapStyle = {
1772
1974
  position: "relative",
1773
- width: "72%",
1774
- aspectRatio: "1 / 1"
1975
+ height: "64%",
1976
+ aspectRatio: "3 / 4"
1775
1977
  };
1776
1978
  var faceDashedRingStyle = {
1777
1979
  position: "absolute",
1778
1980
  inset: -6,
1779
- borderRadius: "50%",
1780
- border: "6px dashed rgba(255, 255, 255, 0.4)",
1781
- animation: "spin 10s linear infinite"
1981
+ borderRadius: 9999,
1982
+ border: "6px dashed rgba(255, 255, 255, 0.4)"
1782
1983
  };
1783
1984
  var faceCircleStyle = {
1784
1985
  position: "absolute",
1785
1986
  inset: 0,
1786
- borderRadius: "50%",
1987
+ // Tailwind's `rounded-full` resolves to a very large fixed radius. On a
1988
+ // 3:4 box that creates the normal KYC capsule/head guide with straighter
1989
+ // sides; 50% would instead produce a mathematically perfect ellipse.
1990
+ borderRadius: 9999,
1787
1991
  border: "4px solid #00bc7d",
1788
1992
  boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
1789
1993
  };
@@ -2126,6 +2330,7 @@ function isValidFileSize(file, maxSizeBytes = 10 * 1024 * 1024) {
2126
2330
  }
2127
2331
  function formatKycStatus(status) {
2128
2332
  const statusMap = {
2333
+ awaiting_kyc: "Awaiting KYC",
2129
2334
  pending: "Pending Review",
2130
2335
  accepted: "Approved",
2131
2336
  declined: "Rejected",
@@ -2136,6 +2341,8 @@ function formatKycStatus(status) {
2136
2341
  }
2137
2342
  function getStatusColor(status) {
2138
2343
  const colorMap = {
2344
+ awaiting_kyc: "#94a3b8",
2345
+ // slate — not started
2139
2346
  pending: "#f59e0b",
2140
2347
  // amber
2141
2348
  accepted: "#10b981",
@@ -2162,6 +2369,13 @@ function getRiskColor(risk) {
2162
2369
  };
2163
2370
  return colorMap[risk] || "#6b7280";
2164
2371
  }
2372
+ function isMobileDevice(userAgent) {
2373
+ const ua = userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
2374
+ return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
2375
+ }
2376
+ function buildEventBasedFaceVerificationQrPayload(token) {
2377
+ return `vesant://reuse-kyc?token=${encodeURIComponent(token)}`;
2378
+ }
2165
2379
  function useEventBasedFaceVerificationSubmission(client) {
2166
2380
  const startSession = useCallback(
2167
2381
  (request) => client.createEventBasedFaceVerificationSession(request),
@@ -2209,6 +2423,6 @@ function useEventBasedFaceVerificationSubmission(client) {
2209
2423
  return { startSession, verifyFace, runFlow };
2210
2424
  }
2211
2425
 
2212
- export { createDeviceFingerprint, fileToBase64, formatKycStatus, getBrowserInfo, getRiskColor, getStatusColor, isValidFileSize, isValidFileType, useCipherText, useCustomerProfile, useEventBasedFaceVerificationSubmission, useGeolocation, useKycAlerts, useKycOverview, useKycPreferences, useKycRequests, useKycSubmission, useLocationCapture, useLocationRequests, useLoginVerification, useRegistration, useTransactionVerification };
2426
+ export { FaceCaptureModal_default as FaceCaptureModal, buildEventBasedFaceVerificationQrPayload, createDeviceFingerprint, fileToBase64, formatKycStatus, getBrowserInfo, getRiskColor, getStatusColor, isMobileDevice, isValidFileSize, isValidFileType, useCipherText, useCustomerProfile, useEventBasedFaceVerificationSubmission, useGeolocation, useKycAlerts, useKycOverview, useKycPreferences, useKycRequests, useKycSubmission, useLocationCapture, useLocationRequests, useLoginVerification, useRegistration, useTransactionVerification };
2213
2427
  //# sourceMappingURL=react.mjs.map
2214
2428
  //# sourceMappingURL=react.mjs.map