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.js CHANGED
@@ -930,19 +930,19 @@ var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id
930
930
  var FACE_MESSAGES = {
931
931
  idle: "Initializing camera...",
932
932
  loading: "Loading face detection...",
933
- scanning: "Position your face in the circle",
933
+ scanning: "Position your face in the oval",
934
934
  too_far: "Face too far. Please move closer to the camera",
935
935
  too_close: "Too close. Please move back a little",
936
- off_center: "Center your face in the circle",
936
+ off_center: "Center your face in the oval",
937
937
  ok: "Looking great! Tap the button to capture"
938
938
  };
939
939
  var FACE_COLORS = {
940
940
  idle: "#9ca3af",
941
- loading: "#3b82f6",
942
- scanning: "#3b82f6",
941
+ loading: "#ef4444",
942
+ scanning: "#ef4444",
943
943
  too_far: "#ef4444",
944
- too_close: "#f59e0b",
945
- off_center: "#f59e0b",
944
+ too_close: "#ef4444",
945
+ off_center: "#ef4444",
946
946
  ok: "#10b981"
947
947
  };
948
948
  var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
@@ -1007,11 +1007,77 @@ function loadMediaPipeFaceDetection(assetBasePath) {
1007
1007
  return loader;
1008
1008
  }
1009
1009
 
1010
+ // src/kyc/hostedJourney.ts
1011
+ var REUSE_KYC_SESSION_EXPIRY_MS = 10 * 60 * 1e3;
1012
+ var JOURNEY_FRAME_HEIGHT = "clamp(500px, 64vh, 680px)";
1013
+ var JOURNEY_CONFIRM_TIMEOUT_MS = 45e3;
1014
+ function journeyOutcomeFromMessage(data) {
1015
+ const status = data?.verification_status;
1016
+ if (typeof status !== "string") return null;
1017
+ switch (status) {
1018
+ case "verification.accepted":
1019
+ return "accepted";
1020
+ case "verification.declined":
1021
+ return "declined";
1022
+ case "verification.cancelled":
1023
+ case "request.timeout":
1024
+ case "request.invalid":
1025
+ case "request.unauthorized":
1026
+ case "request.deleted":
1027
+ return "cancelled";
1028
+ default:
1029
+ return null;
1030
+ }
1031
+ }
1032
+ function journeyPollIntervalMs(confirming) {
1033
+ return confirming ? 1e3 : 2500;
1034
+ }
1035
+ function pendingJourneyResult(reference) {
1036
+ return {
1037
+ event: "reuse_kyc",
1038
+ reference,
1039
+ resource_id: "",
1040
+ status: "pending",
1041
+ data: {
1042
+ retries_remaining: 0,
1043
+ retry_limit_exceeded: false,
1044
+ trigger_alert: false,
1045
+ enforce_logout: false,
1046
+ freeze_account: false,
1047
+ block: false,
1048
+ freeze_duration_minutes: 0
1049
+ }
1050
+ };
1051
+ }
1052
+ function tryStartOperation(guard) {
1053
+ if (guard.current) return false;
1054
+ guard.current = true;
1055
+ return true;
1056
+ }
1057
+ function releaseOperation(guard) {
1058
+ guard.current = false;
1059
+ }
1060
+ function retryTarget(hasHostedJourney) {
1061
+ return hasHostedJourney ? "journey" : "capture";
1062
+ }
1063
+ var CAPTURE_MODAL_MAX_WIDTH = 448;
1064
+ function modalMaxWidth(hasHostedJourney) {
1065
+ return hasHostedJourney ? CAPTURE_MODAL_MAX_WIDTH : CAPTURE_MODAL_MAX_WIDTH;
1066
+ }
1067
+ var JOURNEY_CONTENT_PADDING = 12;
1068
+ function resultForModalClose(stageKind, result, unconfirmed) {
1069
+ const terminal = stageKind === "accepted" || stageKind === "declined" || stageKind === "max_attempts";
1070
+ if (terminal && result !== void 0) return result;
1071
+ const awaitingConfirmation = stageKind === "confirming" || stageKind === "unconfirmed";
1072
+ if (awaitingConfirmation && unconfirmed !== void 0) return unconfirmed;
1073
+ return null;
1074
+ }
1075
+
1010
1076
  // src/kyc/FaceCaptureModal.tsx
1011
1077
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
1012
- var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
1078
+ var ACCEPTED_AUTO_CLOSE_MS = 1200;
1013
1079
  var LIVENESS_MESSAGES = [
1014
- "Position your face inside the circle",
1080
+ "Position your face inside the oval",
1015
1081
  "Make sure your face is well lit",
1016
1082
  "Remove glasses or hats if possible",
1017
1083
  "Look straight at the camera, then tap Capture"
@@ -1020,6 +1086,12 @@ function headerSubtitle(stageKind) {
1020
1086
  switch (stageKind) {
1021
1087
  case "qr":
1022
1088
  return "Use your phone to continue";
1089
+ case "journey":
1090
+ return "Follow the steps to verify your identity";
1091
+ case "confirming":
1092
+ return "Confirming your result";
1093
+ case "unconfirmed":
1094
+ return "Still confirming";
1023
1095
  case "accepted":
1024
1096
  return "All set";
1025
1097
  case "declined":
@@ -1045,7 +1117,9 @@ function FaceCaptureModal({
1045
1117
  }) {
1046
1118
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
1047
1119
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
1120
+ const journeyMode = Boolean(session.verification_url);
1048
1121
  const initialStage = (() => {
1122
+ if (journeyMode) return { kind: "journey", url: session.verification_url ?? "" };
1049
1123
  if (initialChoice === "this") return { kind: "capture" };
1050
1124
  if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
1051
1125
  return { kind: "choose" };
@@ -1063,19 +1137,20 @@ function FaceCaptureModal({
1063
1137
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
1064
1138
  const cancelledRef = React.useRef(false);
1065
1139
  const submittingRef = React.useRef(false);
1140
+ const journeyRetryingRef = React.useRef(false);
1066
1141
  React.useEffect(() => {
1067
1142
  const timer = setTimeout(() => {
1068
1143
  setStage(
1069
- (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
1144
+ (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" || prev.kind === "journey" ? { kind: "error", message: "Session expired" } : prev
1070
1145
  );
1071
- }, SESSION_EXPIRY_MS);
1146
+ }, REUSE_KYC_SESSION_EXPIRY_MS);
1072
1147
  return () => clearTimeout(timer);
1073
1148
  }, [session.token]);
1074
1149
  React.useEffect(() => {
1075
1150
  if (stage.kind !== "qr") return;
1076
1151
  let stopped = false;
1077
1152
  const intervalMs = 2e3;
1078
- const deadline = Date.now() + SESSION_EXPIRY_MS;
1153
+ const deadline = Date.now() + REUSE_KYC_SESSION_EXPIRY_MS;
1079
1154
  let mobileSeen = stage.mobileConnected;
1080
1155
  const tick = async () => {
1081
1156
  if (stopped || cancelledRef.current) return;
@@ -1116,6 +1191,82 @@ function FaceCaptureModal({
1116
1191
  stopped = true;
1117
1192
  };
1118
1193
  }, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
1194
+ const journeyUrl = stage.kind === "journey" || stage.kind === "confirming" ? stage.url : "";
1195
+ React.useEffect(() => {
1196
+ if (stage.kind !== "journey" && stage.kind !== "confirming" || !journeyUrl) return;
1197
+ const intervalMs = journeyPollIntervalMs(stage.kind === "confirming");
1198
+ let stopped = false;
1199
+ const checkOnce = async () => {
1200
+ try {
1201
+ const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
1202
+ if (applyResult(result)) {
1203
+ stopped = true;
1204
+ return true;
1205
+ }
1206
+ } catch {
1207
+ }
1208
+ return false;
1209
+ };
1210
+ const tick = async () => {
1211
+ if (stopped || cancelledRef.current) return;
1212
+ await checkOnce();
1213
+ if (!stopped && !cancelledRef.current) {
1214
+ setTimeout(tick, intervalMs);
1215
+ }
1216
+ };
1217
+ void tick();
1218
+ let frameOrigin = "";
1219
+ try {
1220
+ frameOrigin = new URL(journeyUrl).origin;
1221
+ } catch {
1222
+ frameOrigin = "";
1223
+ }
1224
+ const onMessage = (e) => {
1225
+ if (!frameOrigin || e.origin !== frameOrigin) return;
1226
+ const outcome = journeyOutcomeFromMessage(e.data);
1227
+ if (!outcome) return;
1228
+ void checkOnce();
1229
+ setStage((prev) => {
1230
+ if (prev.kind !== "journey") return prev;
1231
+ if (outcome === "cancelled") {
1232
+ return { kind: "error", message: "The verification was closed before it finished. Please try again." };
1233
+ }
1234
+ return { kind: "confirming", outcome, url: prev.url };
1235
+ });
1236
+ };
1237
+ window.addEventListener("message", onMessage);
1238
+ return () => {
1239
+ stopped = true;
1240
+ window.removeEventListener("message", onMessage);
1241
+ };
1242
+ }, [stage.kind, journeyUrl]);
1243
+ React.useEffect(() => {
1244
+ if (stage.kind !== "confirming") return;
1245
+ const outcome = stage.outcome;
1246
+ const timer = setTimeout(() => {
1247
+ setStage((prev) => prev.kind === "confirming" ? { kind: "unconfirmed", outcome } : prev);
1248
+ }, JOURNEY_CONFIRM_TIMEOUT_MS);
1249
+ return () => clearTimeout(timer);
1250
+ }, [stage.kind, stage.kind === "confirming" ? stage.outcome : ""]);
1251
+ const handleJourneyRetry = async () => {
1252
+ if (!tryStartOperation(journeyRetryingRef)) return;
1253
+ setStage({ kind: "journey", url: "" });
1254
+ try {
1255
+ const refreshed = await client.retryEventBasedFaceVerificationJourney(session.token);
1256
+ if (refreshed.verification_url) {
1257
+ setStage({ kind: "journey", url: refreshed.verification_url });
1258
+ return;
1259
+ }
1260
+ setStage({ kind: "error", message: "Unable to start a new attempt. Please try again." });
1261
+ } catch (err) {
1262
+ setStage({
1263
+ kind: "error",
1264
+ message: err instanceof Error ? err.message : "Unable to start a new attempt. Please try again."
1265
+ });
1266
+ } finally {
1267
+ releaseOperation(journeyRetryingRef);
1268
+ }
1269
+ };
1119
1270
  const applyResult = (result) => {
1120
1271
  if (result.status === "accepted") {
1121
1272
  setStage({ kind: "accepted", result });
@@ -1243,12 +1394,23 @@ function FaceCaptureModal({
1243
1394
  }, 3e3);
1244
1395
  return () => clearInterval(interval);
1245
1396
  }, [captureMode]);
1246
- const close = (result) => {
1397
+ const close = React.useCallback((result) => {
1247
1398
  cancelledRef.current = true;
1248
1399
  if (result === null && onCancel) {
1249
1400
  onCancel();
1250
1401
  }
1251
1402
  onComplete(result);
1403
+ }, [onCancel, onComplete]);
1404
+ React.useEffect(() => {
1405
+ if (stage.kind !== "accepted") return;
1406
+ const result = stage.result;
1407
+ const timer = setTimeout(() => close(result), ACCEPTED_AUTO_CLOSE_MS);
1408
+ return () => clearTimeout(timer);
1409
+ }, [close, stage]);
1410
+ const handleHeaderClose = () => {
1411
+ const result = "result" in stage ? stage.result : void 0;
1412
+ const unconfirmed = stage.kind === "confirming" || stage.kind === "unconfirmed" ? pendingJourneyResult(session.reference) : void 0;
1413
+ close(resultForModalClose(stage.kind, result, unconfirmed));
1252
1414
  };
1253
1415
  const renderChoose = () => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: circleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })))), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "How would you like to verify?"), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement(
1254
1416
  "button",
@@ -1307,12 +1469,37 @@ function FaceCaptureModal({
1307
1469
  };
1308
1470
  const renderSubmitting = () => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Verifying your photo\u2026"), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
1309
1471
  const renderAccepted = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: successCircleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Verified"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Your identity has been confirmed. You can continue."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Continue")));
1310
- const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Retake")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1472
+ const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, journeyMode ? /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, /* @__PURE__ */ React__default.default.createElement("span", null, "Try Again")) : /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Retake")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1311
1473
  const renderMaxAttempts = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React__default.default.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__default.default.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), result.data?.block && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "This action has been blocked for security reasons."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1312
1474
  const renderError = (message) => {
1313
1475
  const sessionExpired = /session\s+expired/i.test(message);
1314
- return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try Again"), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1476
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement(
1477
+ "button",
1478
+ {
1479
+ style: primaryButtonStyle,
1480
+ onClick: () => retryTarget(journeyMode) === "journey" ? void handleJourneyRetry() : setStage({ kind: "capture" })
1481
+ },
1482
+ "Try Again"
1483
+ ), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1315
1484
  };
1485
+ const renderConfirming = () => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Confirming your result\u2026"), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
1486
+ const renderUnconfirmed = (outcome) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Still confirming"), /* @__PURE__ */ React__default.default.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__default.default.createElement("div", { style: buttonsContainerStyle }, outcome === "declined" && /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => void handleJourneyRetry() }, "Try Again"), /* @__PURE__ */ React__default.default.createElement(
1487
+ "button",
1488
+ {
1489
+ style: outcome === "accepted" ? primaryButtonStyle : cancelButtonStyle,
1490
+ onClick: () => close(pendingJourneyResult(session.reference))
1491
+ },
1492
+ "Close"
1493
+ )));
1494
+ const renderJourney = (url) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, padding: JOURNEY_CONTENT_PADDING } }, url ? /* @__PURE__ */ React__default.default.createElement(
1495
+ "iframe",
1496
+ {
1497
+ src: url,
1498
+ title: "Identity verification",
1499
+ allow: "camera; microphone",
1500
+ style: journeyFrameStyle
1501
+ }
1502
+ ) : /* @__PURE__ */ React__default.default.createElement("div", { style: { padding: 24, textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Preparing your next attempt\u2026")), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, textAlign: "center" } }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1316
1503
  const body = (() => {
1317
1504
  switch (stage.kind) {
1318
1505
  case "choose":
@@ -1321,6 +1508,12 @@ function FaceCaptureModal({
1321
1508
  return renderQRStage(stage.mobileConnected);
1322
1509
  case "capture":
1323
1510
  return renderCapture(stage.declinedReason);
1511
+ case "journey":
1512
+ return renderJourney(stage.url);
1513
+ case "confirming":
1514
+ return renderConfirming();
1515
+ case "unconfirmed":
1516
+ return renderUnconfirmed(stage.outcome);
1324
1517
  case "submitting":
1325
1518
  return renderSubmitting();
1326
1519
  case "accepted":
@@ -1333,10 +1526,10 @@ function FaceCaptureModal({
1333
1526
  return renderError(stage.message);
1334
1527
  }
1335
1528
  })();
1336
- return /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, /* @__PURE__ */ React__default.default.createElement("style", null, keyframes), /* @__PURE__ */ React__default.default.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: modalStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: headerStyle }, /* @__PURE__ */ React__default.default.createElement(
1529
+ return /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, /* @__PURE__ */ React__default.default.createElement("style", null, keyframes), /* @__PURE__ */ React__default.default.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...modalStyle, maxWidth: modalMaxWidth(journeyMode) } }, /* @__PURE__ */ React__default.default.createElement("div", { style: headerStyle }, /* @__PURE__ */ React__default.default.createElement(
1337
1530
  "button",
1338
1531
  {
1339
- onClick: () => close(null),
1532
+ onClick: handleHeaderClose,
1340
1533
  style: closeButtonStyle,
1341
1534
  "aria-label": "Close"
1342
1535
  },
@@ -1366,7 +1559,8 @@ var modalStyle = {
1366
1559
  borderRadius: 16,
1367
1560
  boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
1368
1561
  width: "100%",
1369
- maxWidth: 448,
1562
+ maxHeight: "calc(100vh - 32px)",
1563
+ overflowY: "auto",
1370
1564
  animation: "zoomIn .2s ease-out"
1371
1565
  };
1372
1566
  var headerStyle = {
@@ -1392,6 +1586,13 @@ var contentStyle = {
1392
1586
  flexDirection: "column",
1393
1587
  gap: 16
1394
1588
  };
1589
+ var journeyFrameStyle = {
1590
+ width: "100%",
1591
+ height: JOURNEY_FRAME_HEIGHT,
1592
+ border: 0,
1593
+ borderRadius: 12,
1594
+ background: "#f9fafb"
1595
+ };
1395
1596
  var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
1396
1597
  var visualGuideStyle = { position: "relative" };
1397
1598
  var circleStyle = {
@@ -1658,9 +1859,10 @@ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1658
1859
  if (error) {
1659
1860
  return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
1660
1861
  }
1661
- const detectionActive = ready && !faceDetectionFailed;
1662
- const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1663
- const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1862
+ const detectionAvailable = !faceDetectionFailed;
1863
+ const detectionActive = ready && detectionAvailable;
1864
+ const liveMessage = detectionAvailable ? FACE_MESSAGES[faceStatus] : message;
1865
+ const liveFaceColor = detectionAvailable ? FACE_COLORS[faceStatus] : void 0;
1664
1866
  const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1665
1867
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1666
1868
  const dynamicCircleStyle = {
@@ -1776,20 +1978,22 @@ var faceOverlayContainerStyle = {
1776
1978
  };
1777
1979
  var faceRingWrapStyle = {
1778
1980
  position: "relative",
1779
- width: "72%",
1780
- aspectRatio: "1 / 1"
1981
+ height: "64%",
1982
+ aspectRatio: "3 / 4"
1781
1983
  };
1782
1984
  var faceDashedRingStyle = {
1783
1985
  position: "absolute",
1784
1986
  inset: -6,
1785
- borderRadius: "50%",
1786
- border: "6px dashed rgba(255, 255, 255, 0.4)",
1787
- animation: "spin 10s linear infinite"
1987
+ borderRadius: 9999,
1988
+ border: "6px dashed rgba(255, 255, 255, 0.4)"
1788
1989
  };
1789
1990
  var faceCircleStyle = {
1790
1991
  position: "absolute",
1791
1992
  inset: 0,
1792
- borderRadius: "50%",
1993
+ // Tailwind's `rounded-full` resolves to a very large fixed radius. On a
1994
+ // 3:4 box that creates the normal KYC capsule/head guide with straighter
1995
+ // sides; 50% would instead produce a mathematically perfect ellipse.
1996
+ borderRadius: 9999,
1793
1997
  border: "4px solid #00bc7d",
1794
1998
  boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
1795
1999
  };
@@ -2132,6 +2336,7 @@ function isValidFileSize(file, maxSizeBytes = 10 * 1024 * 1024) {
2132
2336
  }
2133
2337
  function formatKycStatus(status) {
2134
2338
  const statusMap = {
2339
+ awaiting_kyc: "Awaiting KYC",
2135
2340
  pending: "Pending Review",
2136
2341
  accepted: "Approved",
2137
2342
  declined: "Rejected",
@@ -2142,6 +2347,8 @@ function formatKycStatus(status) {
2142
2347
  }
2143
2348
  function getStatusColor(status) {
2144
2349
  const colorMap = {
2350
+ awaiting_kyc: "#94a3b8",
2351
+ // slate — not started
2145
2352
  pending: "#f59e0b",
2146
2353
  // amber
2147
2354
  accepted: "#10b981",
@@ -2168,6 +2375,13 @@ function getRiskColor(risk) {
2168
2375
  };
2169
2376
  return colorMap[risk] || "#6b7280";
2170
2377
  }
2378
+ function isMobileDevice(userAgent) {
2379
+ const ua = userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
2380
+ return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
2381
+ }
2382
+ function buildEventBasedFaceVerificationQrPayload(token) {
2383
+ return `vesant://reuse-kyc?token=${encodeURIComponent(token)}`;
2384
+ }
2171
2385
  function useEventBasedFaceVerificationSubmission(client$1) {
2172
2386
  const startSession = React.useCallback(
2173
2387
  (request) => client$1.createEventBasedFaceVerificationSession(request),
@@ -2215,12 +2429,15 @@ function useEventBasedFaceVerificationSubmission(client$1) {
2215
2429
  return { startSession, verifyFace, runFlow };
2216
2430
  }
2217
2431
 
2432
+ exports.FaceCaptureModal = FaceCaptureModal_default;
2433
+ exports.buildEventBasedFaceVerificationQrPayload = buildEventBasedFaceVerificationQrPayload;
2218
2434
  exports.createDeviceFingerprint = createDeviceFingerprint;
2219
2435
  exports.fileToBase64 = fileToBase64;
2220
2436
  exports.formatKycStatus = formatKycStatus;
2221
2437
  exports.getBrowserInfo = getBrowserInfo;
2222
2438
  exports.getRiskColor = getRiskColor;
2223
2439
  exports.getStatusColor = getStatusColor;
2440
+ exports.isMobileDevice = isMobileDevice;
2224
2441
  exports.isValidFileSize = isValidFileSize;
2225
2442
  exports.isValidFileType = isValidFileType;
2226
2443
  exports.useCipherText = useCipherText;