vesant-sdk 2.0.0-dev.76771da → 2.0.0-dev.9578c07

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.
Files changed (45) hide show
  1. package/dist/{client-CIEa7xYG.d.mts → client-BwkGrRW9.d.mts} +1 -1
  2. package/dist/{client-BJ87_Vv5.d.ts → client-CqSx0lAG.d.ts} +3 -3
  3. package/dist/{client-IAOGCBfm.d.mts → client-Dq0NMaXT.d.mts} +3 -3
  4. package/dist/{client-Bvp-f05-.d.ts → client-DvobLmsc.d.ts} +1 -1
  5. package/dist/compliance/index.d.mts +4 -4
  6. package/dist/compliance/index.d.ts +4 -4
  7. package/dist/decisions/index.d.mts +1 -1
  8. package/dist/decisions/index.d.ts +1 -1
  9. package/dist/geolocation/index.d.mts +3 -3
  10. package/dist/geolocation/index.d.ts +3 -3
  11. package/dist/index.d.mts +7 -7
  12. package/dist/index.d.ts +7 -7
  13. package/dist/index.js +54 -10
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +54 -10
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/kyc/core.d.mts +3 -3
  18. package/dist/kyc/core.d.ts +3 -3
  19. package/dist/kyc/core.js +54 -10
  20. package/dist/kyc/core.js.map +1 -1
  21. package/dist/kyc/core.mjs +54 -10
  22. package/dist/kyc/core.mjs.map +1 -1
  23. package/dist/kyc/index.d.mts +253 -21
  24. package/dist/kyc/index.d.ts +253 -21
  25. package/dist/kyc/index.js +54 -10
  26. package/dist/kyc/index.js.map +1 -1
  27. package/dist/kyc/index.mjs +54 -10
  28. package/dist/kyc/index.mjs.map +1 -1
  29. package/dist/react.d.mts +41 -6
  30. package/dist/react.d.ts +41 -6
  31. package/dist/react.js +603 -272
  32. package/dist/react.js.map +1 -1
  33. package/dist/react.mjs +603 -272
  34. package/dist/react.mjs.map +1 -1
  35. package/dist/risk-profile/index.d.mts +3 -3
  36. package/dist/risk-profile/index.d.ts +3 -3
  37. package/dist/scores/index.d.mts +1 -1
  38. package/dist/scores/index.d.ts +1 -1
  39. package/dist/{types-C4Zx0d_u.d.mts → types-BOFaMQxI.d.mts} +1 -1
  40. package/dist/{types-QUCWam16.d.mts → types-CBQRNL-l.d.mts} +10 -3
  41. package/dist/{types-QUCWam16.d.ts → types-CBQRNL-l.d.ts} +10 -3
  42. package/dist/{types-2utj53GK.d.ts → types-UGyDl1fd.d.ts} +1 -1
  43. package/dist/webhooks/index.d.mts +12 -4
  44. package/dist/webhooks/index.d.ts +12 -4
  45. package/package.json +1 -1
package/dist/react.js CHANGED
@@ -891,277 +891,599 @@ var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id
891
891
  var Upload = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNyAxN0gxNy4wMU0xNS42IDE0SDE4QzE4LjkzMTkgMTQgMTkuMzk3OCAxNCAxOS43NjU0IDE0LjE1MjJDMjAuMjU1NCAxNC4zNTUyIDIwLjY0NDggMTQuNzQ0NiAyMC44NDc4IDE1LjIzNDZDMjEgMTUuNjAyMiAyMSAxNi4wNjgxIDIxIDE3QzIxIDE3LjkzMTkgMjEgMTguMzk3OCAyMC44NDc4IDE4Ljc2NTRDMjAuNjQ0OCAxOS4yNTU0IDIwLjI1NTQgMTkuNjQ0OCAxOS43NjU0IDE5Ljg0NzhDMTkuMzk3OCAyMCAxOC45MzE5IDIwIDE4IDIwSDZDNS4wNjgxMiAyMCA0LjYwMjE4IDIwIDQuMjM0NjMgMTkuODQ3OEMzLjc0NDU4IDE5LjY0NDggMy4zNTUyMyAxOS4yNTU0IDMuMTUyMjQgMTguNzY1NEMzIDE4LjM5NzggMyAxNy45MzE5IDMgMTdDMyAxNi4wNjgxIDMgMTUuNjAyMiAzLjE1MjI0IDE1LjIzNDZDMy4zNTUyMyAxNC43NDQ2IDMuNzQ0NTggMTQuMzU1MiA0LjIzNDYzIDE0LjE1MjJDNC42MDIxOCAxNCA1LjA2ODEyIDE0IDYgMTRIOC40TTEyIDE1VjRNMTIgNEwxNSA3TTEyIDRMOSA3IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+DQo8L3N2Zz4=";
892
892
 
893
893
  // src/kyc/FaceCaptureModal.tsx
894
- var fileToBase64 = (file) => {
895
- return new Promise((resolve, reject) => {
896
- const reader = new FileReader();
897
- reader.readAsDataURL(file);
898
- reader.onload = () => resolve(reader.result);
899
- reader.onerror = (error) => reject(error);
900
- });
901
- };
894
+ var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
895
+ var LIVENESS_MESSAGES = [
896
+ "Align your face in the oval",
897
+ "Face too far. Please, move closer to the camera",
898
+ "Looking good! Stay still...",
899
+ "Processing liveness..."
900
+ ];
901
+ function headerSubtitle(stageKind) {
902
+ switch (stageKind) {
903
+ case "qr":
904
+ return "Use your phone to continue";
905
+ case "accepted":
906
+ return "All set";
907
+ case "declined":
908
+ return "Verification didn't match \u2014 try once more";
909
+ case "max_attempts":
910
+ return "We couldn't verify you";
911
+ default:
912
+ return "Please capture a clear photo of your face";
913
+ }
914
+ }
915
+ function defaultRenderQR(payload) {
916
+ const url = `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(payload)}`;
917
+ return /* @__PURE__ */ React__default.default.createElement("img", { src: url, alt: "QR code", width: 220, height: 220 });
918
+ }
902
919
  function FaceCaptureModal({
903
- onCapture,
904
- onCancel
920
+ client,
921
+ session,
922
+ onComplete,
923
+ defaultDevice,
924
+ renderQR
905
925
  }) {
906
- const inputRef = React.useRef(null);
907
- const [isProcessing, setIsProcessing] = React.useState(false);
908
- const [isHoveringPrimary, setIsHoveringPrimary] = React.useState(false);
909
- const [isHoveringCancel, setIsHoveringCancel] = React.useState(false);
910
- const [isHoveringClose, setIsHoveringClose] = React.useState(false);
911
- const isMobile = /Mobi|Android/i.test(navigator.userAgent);
912
- const handleFileChange = async (e) => {
913
- const file = e.target.files?.[0];
914
- if (!file) return;
915
- setIsProcessing(true);
916
- try {
917
- const base64 = await fileToBase64(file);
918
- setTimeout(() => {
919
- onCapture(base64);
920
- }, 300);
921
- } catch (error) {
922
- console.error("[Vesant SDK] Error processing image:", error instanceof Error ? error.message : "Unknown error");
923
- setIsProcessing(false);
926
+ const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
927
+ const initialStage = (() => {
928
+ const choice = defaultDevice ?? (isMobile ? "this" : "ask");
929
+ if (choice === "this") return { kind: "capture" };
930
+ if (choice === "mobile") return { kind: "qr", mobileConnected: false };
931
+ return { kind: "choose" };
932
+ })();
933
+ const [stage, setStage] = React.useState(initialStage);
934
+ const [attempts, setAttempts] = React.useState(session.attempts);
935
+ const maxAttempts = session.max_attempts || 1;
936
+ const [qrDeclinedReason, setQrDeclinedReason] = React.useState(null);
937
+ const [captureMode, setCaptureMode] = React.useState("idle");
938
+ const [capturedPreview, setCapturedPreview] = React.useState(null);
939
+ const [capturedBase64, setCapturedBase64] = React.useState(null);
940
+ const [livenessMessage, setLivenessMessage] = React.useState(LIVENESS_MESSAGES[0]);
941
+ const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
942
+ const cancelledRef = React.useRef(false);
943
+ const submittingRef = React.useRef(false);
944
+ React.useEffect(() => {
945
+ if (stage.kind !== "qr") return;
946
+ let stopped = false;
947
+ const intervalMs = 2e3;
948
+ const deadline = Date.now() + 15 * 60 * 1e3;
949
+ let mobileSeen = stage.mobileConnected;
950
+ const tick = async () => {
951
+ if (stopped || cancelledRef.current) return;
952
+ try {
953
+ if (mobileSeen) {
954
+ const result = await client.getReuseKycSessionStatus(session.reference);
955
+ if (result.status === "accepted") {
956
+ setStage({ kind: "accepted", result });
957
+ stopped = true;
958
+ return;
959
+ }
960
+ if (result.status === "declined") {
961
+ setAttempts(maxAttempts - (result.data?.retries_remaining ?? 0));
962
+ if (result.data?.retry_limit_exceeded) {
963
+ setStage({ kind: "max_attempts", result });
964
+ stopped = true;
965
+ return;
966
+ }
967
+ if (result.declined_reason) {
968
+ setQrDeclinedReason(result.declined_reason);
969
+ }
970
+ }
971
+ } else {
972
+ const handoff = await client.getHandoffSession(session.token);
973
+ if (handoff.mobile_connected) {
974
+ mobileSeen = true;
975
+ setStage({ kind: "qr", mobileConnected: true });
976
+ }
977
+ }
978
+ } catch {
979
+ }
980
+ if (Date.now() < deadline) {
981
+ setTimeout(tick, intervalMs);
982
+ }
983
+ };
984
+ tick();
985
+ return () => {
986
+ stopped = true;
987
+ };
988
+ }, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
989
+ const applyResult = (result) => {
990
+ if (result.status === "accepted") {
991
+ setStage({ kind: "accepted", result });
992
+ return true;
924
993
  }
994
+ if (result.status === "declined") {
995
+ const nextAttempts = maxAttempts - (result.data?.retries_remaining ?? 0);
996
+ setAttempts(nextAttempts);
997
+ setCapturedPreview(null);
998
+ setCapturedBase64(null);
999
+ if (result.data?.retry_limit_exceeded) {
1000
+ setStage({ kind: "max_attempts", result });
1001
+ } else {
1002
+ setStage({ kind: "declined", result });
1003
+ }
1004
+ return true;
1005
+ }
1006
+ return false;
925
1007
  };
926
- const overlayStyle = {
927
- position: "fixed",
928
- inset: 0,
929
- background: "rgba(0, 0, 0, 0.6)",
930
- backdropFilter: "blur(4px)",
931
- display: "flex",
932
- alignItems: "center",
933
- justifyContent: "center",
934
- zIndex: 999999,
935
- padding: "16px",
936
- animation: "fadeIn 0.2s ease-out"
937
- };
938
- const modalStyle = {
939
- background: "#ffffff",
940
- borderRadius: "16px",
941
- boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
942
- width: "100%",
943
- maxWidth: "448px",
944
- animation: "zoomIn 0.2s ease-out"
945
- };
946
- const headerStyle = {
947
- position: "relative",
948
- padding: "24px 24px 16px",
949
- borderBottom: "1px solid #f3f4f6"
950
- };
951
- const closeButtonStyle = {
952
- position: "absolute",
953
- top: "16px",
954
- right: "16px",
955
- padding: "8px",
956
- background: isHoveringClose ? "#f3f4f6" : "transparent",
957
- border: "none",
958
- borderRadius: "9999px",
959
- cursor: "pointer",
960
- transition: "background-color 0.2s",
961
- display: "flex",
962
- alignItems: "center",
963
- justifyContent: "center"
964
- };
965
- const titleStyle = {
966
- fontSize: "24px",
967
- fontWeight: 600,
968
- color: "#111827",
969
- margin: 0
970
- };
971
- const subtitleStyle = {
972
- fontSize: "14px",
973
- color: "#6b7280",
974
- marginTop: "4px"
975
- };
976
- const contentStyle = {
977
- padding: "32px 24px"
978
- };
979
- const visualGuideContainerStyle = {
980
- marginBottom: "32px",
981
- display: "flex",
982
- justifyContent: "center"
983
- };
984
- const visualGuideStyle = {
985
- position: "relative"
986
- };
987
- const circleStyle = {
988
- width: "128px",
989
- height: "128px",
990
- borderRadius: "50%",
991
- background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
992
- display: "flex",
993
- alignItems: "center",
994
- justifyContent: "center"
995
- };
996
- const badgeStyle = {
997
- position: "absolute",
998
- top: "-4px",
999
- right: "-4px",
1000
- width: "32px",
1001
- height: "32px",
1002
- background: "#ffffff",
1003
- borderRadius: "50%",
1004
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
1005
- display: "flex",
1006
- alignItems: "center",
1007
- justifyContent: "center"
1008
- };
1009
- const instructionsBoxStyle = {
1010
- background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1011
- borderRadius: "12px",
1012
- padding: "16px",
1013
- marginBottom: "24px"
1014
- };
1015
- const instructionsTitleStyle = {
1016
- fontSize: "14px",
1017
- fontWeight: 500,
1018
- color: "#111827",
1019
- marginBottom: "8px"
1020
- };
1021
- const instructionsListStyle = {
1022
- fontSize: "12px",
1023
- color: "#6b7280",
1024
- listStyle: "none",
1025
- padding: 0,
1026
- margin: 0
1027
- };
1028
- const instructionItemStyle = {
1029
- display: "flex",
1030
- alignItems: "flex-start",
1031
- marginBottom: "6px"
1032
- };
1033
- const bulletStyle = {
1034
- color: "#00bc7d",
1035
- marginRight: "8px",
1036
- flexShrink: 0
1037
- };
1038
- const buttonsContainerStyle = {
1039
- display: "flex",
1040
- flexDirection: "column",
1041
- gap: "12px"
1008
+ const runSubmit = async (proof) => {
1009
+ if (submittingRef.current) return;
1010
+ submittingRef.current = true;
1011
+ setStage({ kind: "submitting" });
1012
+ let settled = false;
1013
+ const ref = session.reference;
1014
+ try {
1015
+ const postPromise = (async () => {
1016
+ try {
1017
+ const data = await client.submitReuseKycSession({
1018
+ token: session.token,
1019
+ reference: ref,
1020
+ proof
1021
+ });
1022
+ return { ok: true, data };
1023
+ } catch (err2) {
1024
+ return { ok: false, error: err2 };
1025
+ }
1026
+ })();
1027
+ const pollPromise = (async () => {
1028
+ await new Promise((r) => setTimeout(r, 12e3));
1029
+ const deadline = Date.now() + 12e4;
1030
+ while (Date.now() < deadline) {
1031
+ if (settled || cancelledRef.current) return null;
1032
+ try {
1033
+ const r = await client.getReuseKycSessionStatus(ref);
1034
+ if (r.status === "accepted" || r.status === "declined") return r;
1035
+ } catch {
1036
+ }
1037
+ if (settled || cancelledRef.current) return null;
1038
+ await new Promise((r) => setTimeout(r, 3e3));
1039
+ }
1040
+ return null;
1041
+ })();
1042
+ const winner = await Promise.race([
1043
+ postPromise.then(
1044
+ (r) => r.ok ? { from: "post", data: r.data } : { from: "post_err" }
1045
+ ),
1046
+ pollPromise.then(
1047
+ (data) => data ? { from: "poll", data } : { from: "poll_done" }
1048
+ )
1049
+ ]);
1050
+ settled = true;
1051
+ if (winner.from === "post" && applyResult(winner.data)) return;
1052
+ if (winner.from === "poll" && applyResult(winner.data)) return;
1053
+ const [postResult, pollResult] = await Promise.all([postPromise, pollPromise]);
1054
+ if (postResult.ok && applyResult(postResult.data)) return;
1055
+ if (pollResult && applyResult(pollResult)) return;
1056
+ const err = postResult.ok ? null : postResult.error;
1057
+ setStage({
1058
+ kind: "error",
1059
+ message: err instanceof Error ? err.message : "Submission failed. Please try again."
1060
+ });
1061
+ } finally {
1062
+ settled = true;
1063
+ submittingRef.current = false;
1064
+ }
1042
1065
  };
1043
- const primaryButtonStyle = {
1044
- width: "100%",
1045
- background: isProcessing ? "#d1d5db" : isHoveringPrimary ? "#00a86d" : "#00bc7d",
1046
- color: "#ffffff",
1047
- fontWeight: 500,
1048
- padding: "14px 24px",
1049
- borderRadius: "12px",
1050
- border: "none",
1051
- cursor: isProcessing ? "not-allowed" : "pointer",
1052
- transition: "all 0.2s",
1053
- display: "flex",
1054
- alignItems: "center",
1055
- justifyContent: "center",
1056
- gap: "12px",
1057
- boxShadow: isProcessing ? "none" : isHoveringPrimary ? "0 20px 25px -5px rgba(0, 188, 125, 0.3)" : "0 10px 15px -3px rgba(0, 188, 125, 0.2)",
1058
- fontSize: "16px"
1066
+ const handleCameraCapture = (dataUrl) => {
1067
+ const raw = dataUrl.split(",")[1] ?? "";
1068
+ setCapturedPreview(dataUrl);
1069
+ setCapturedBase64(raw);
1070
+ setCaptureMode("preview");
1059
1071
  };
1060
- const cancelButtonStyle = {
1061
- width: "100%",
1062
- background: isProcessing ? "#f3f4f6" : isHoveringCancel ? "#f9fafb" : "#ffffff",
1063
- color: "#374151",
1064
- fontWeight: 500,
1065
- padding: "14px 24px",
1066
- borderRadius: "12px",
1067
- border: "2px solid #e5e7eb",
1068
- cursor: isProcessing ? "not-allowed" : "pointer",
1069
- transition: "all 0.2s",
1070
- fontSize: "16px"
1072
+ const handleCameraCancel = () => setCaptureMode("idle");
1073
+ const handleRetake = () => {
1074
+ setCapturedPreview(null);
1075
+ setCapturedBase64(null);
1076
+ setCaptureMode("live");
1071
1077
  };
1072
- const footerStyle = {
1073
- padding: "0 24px 24px"
1078
+ const handleConfirmSubmit = () => {
1079
+ if (!capturedBase64) return;
1080
+ void runSubmit(capturedBase64);
1074
1081
  };
1075
- const footerTextStyle = {
1076
- fontSize: "12px",
1077
- textAlign: "center",
1078
- color: "#9ca3af",
1079
- margin: 0
1082
+ const handleOpenCamera = () => {
1083
+ if (submittingRef.current) return;
1084
+ setCaptureMode("live");
1080
1085
  };
1081
- const spinnerStyle = {
1082
- width: "20px",
1083
- height: "20px",
1084
- border: "2px solid rgba(255, 255, 255, 0.3)",
1085
- borderTopColor: "#ffffff",
1086
- borderRadius: "50%",
1087
- animation: "spin 0.6s linear infinite"
1086
+ const handleRetakeAfterDecline = () => {
1087
+ setCapturedPreview(null);
1088
+ setCapturedBase64(null);
1089
+ setCaptureMode("live");
1090
+ setStage({ kind: "capture" });
1088
1091
  };
1089
- const hiddenInputStyle = {
1090
- display: "none"
1092
+ React.useEffect(() => {
1093
+ if (captureMode !== "live") return;
1094
+ setLivenessMessage(LIVENESS_MESSAGES[0]);
1095
+ let i = 0;
1096
+ const interval = setInterval(() => {
1097
+ i = (i + 1) % LIVENESS_MESSAGES.length;
1098
+ setLivenessMessage(LIVENESS_MESSAGES[i]);
1099
+ }, 3e3);
1100
+ return () => clearInterval(interval);
1101
+ }, [captureMode]);
1102
+ const close = (result) => {
1103
+ cancelledRef.current = true;
1104
+ onComplete(result);
1091
1105
  };
1092
- const styleTag = /* @__PURE__ */ React__default.default.createElement("style", null, `
1093
- @keyframes fadeIn {
1094
- from {
1095
- opacity: 0;
1096
- }
1097
- to {
1098
- opacity: 1;
1099
- }
1100
- }
1101
-
1102
- @keyframes zoomIn {
1103
- from {
1104
- opacity: 0;
1105
- transform: scale(0.95);
1106
- }
1107
- to {
1108
- opacity: 1;
1109
- transform: scale(1);
1110
- }
1111
- }
1112
-
1113
- @keyframes spin {
1114
- from {
1115
- transform: rotate(0deg);
1116
- }
1117
- to {
1118
- transform: rotate(360deg);
1119
- }
1120
- }
1121
- `);
1122
- return /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, styleTag, /* @__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(
1106
+ 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(
1123
1107
  "button",
1124
1108
  {
1125
- onClick: onCancel,
1126
- onMouseEnter: () => setIsHoveringClose(true),
1127
- onMouseLeave: () => setIsHoveringClose(false),
1128
- style: closeButtonStyle,
1129
- "aria-label": "Close"
1109
+ style: primaryButtonStyle,
1110
+ onClick: () => setStage({ kind: "capture" })
1111
+ },
1112
+ /* @__PURE__ */ React__default.default.createElement("img", { src: isMobile ? Camera : Upload, alt: "", width: 20, height: 20 }),
1113
+ /* @__PURE__ */ React__default.default.createElement("span", null, "Continue on this device")
1114
+ ), /* @__PURE__ */ React__default.default.createElement(
1115
+ "button",
1116
+ {
1117
+ style: secondaryButtonStyle,
1118
+ onClick: () => setStage({ kind: "qr", mobileConnected: false })
1130
1119
  },
1131
- /* @__PURE__ */ React__default.default.createElement("img", { src: Close, alt: "Close Icon", width: 16, height: 16 })
1132
- ), /* @__PURE__ */ React__default.default.createElement("h2", { style: titleStyle }, "Face Verification"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Please capture or upload a clear photo of your face")), /* @__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: "Camera Icon", width: 48, height: 48 })), /* @__PURE__ */ React__default.default.createElement("div", { style: badgeStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "Check Icon", width: 16, height: 16 })))), /* @__PURE__ */ React__default.default.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React__default.default.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React__default.default.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React__default.default.createElement(
1133
- "input",
1120
+ "Continue on mobile (scan QR)"
1121
+ ), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1122
+ const renderQRStage = (mobileConnected) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: qrBoxStyle }, (renderQR ?? defaultRenderQR)(qrPayload)), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, mobileConnected ? "Phone connected. Complete the capture on your mobile device\u2026" : "Scan this QR with your phone camera, then complete the face capture there."), mobileConnected && /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "12px auto" } }), qrDeclinedReason && /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Last attempt was declined."), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, qrDeclinedReason), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...alertTextStyle, marginTop: 4 } }, "Try again on your phone. Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts, ".")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement(
1123
+ "button",
1134
1124
  {
1135
- ref: inputRef,
1136
- type: "file",
1137
- accept: "image/*",
1138
- capture: isMobile ? "user" : void 0,
1139
- style: hiddenInputStyle,
1140
- onChange: handleFileChange,
1141
- disabled: isProcessing
1125
+ style: secondaryButtonStyle,
1126
+ onClick: () => setStage({ kind: "capture" })
1127
+ },
1128
+ "Use this device instead"
1129
+ ), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1130
+ const renderCapture = (declinedReason) => {
1131
+ if (captureMode === "live") {
1132
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement(
1133
+ LiveCamera,
1134
+ {
1135
+ onCapture: handleCameraCapture,
1136
+ onCancel: handleCameraCancel,
1137
+ message: livenessMessage
1138
+ }
1139
+ ), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1140
+ }
1141
+ if (captureMode === "preview" && capturedPreview) {
1142
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, declinedReason && /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Looks good? Submit this selfie or retake it."), /* @__PURE__ */ React__default.default.createElement("div", { style: previewBoxStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: capturedPreview, alt: "Captured selfie preview", style: previewImgStyle })), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleConfirmSubmit }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Submit")), /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: handleRetake }, "Retake"), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1143
+ }
1144
+ return /* @__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("div", { style: badgeStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React__default.default.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React__default.default.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React__default.default.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, declinedReason ? "Try again" : "Open Camera")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1145
+ };
1146
+ 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."));
1147
+ 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")));
1148
+ const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Verification declined."), /* @__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));
1149
+ 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."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1150
+ const renderError = (message) => /* @__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 }, "Something went wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__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) }, "Cancel")));
1151
+ const body = (() => {
1152
+ switch (stage.kind) {
1153
+ case "choose":
1154
+ return renderChoose();
1155
+ case "qr":
1156
+ return renderQRStage(stage.mobileConnected);
1157
+ case "capture":
1158
+ return renderCapture(stage.declinedReason);
1159
+ case "submitting":
1160
+ return renderSubmitting();
1161
+ case "accepted":
1162
+ return renderAccepted(stage.result);
1163
+ case "declined":
1164
+ return renderDeclined(stage.result);
1165
+ case "max_attempts":
1166
+ return renderMaxAttempts(stage.result);
1167
+ case "error":
1168
+ return renderError(stage.message);
1142
1169
  }
1143
- ), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement(
1170
+ })();
1171
+ 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(
1144
1172
  "button",
1145
1173
  {
1146
- onClick: () => inputRef.current?.click(),
1147
- onMouseEnter: () => setIsHoveringPrimary(true),
1148
- onMouseLeave: () => setIsHoveringPrimary(false),
1149
- disabled: isProcessing,
1150
- style: primaryButtonStyle
1174
+ onClick: () => close(null),
1175
+ style: closeButtonStyle,
1176
+ "aria-label": "Close"
1151
1177
  },
1152
- isProcessing ? /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, /* @__PURE__ */ React__default.default.createElement("div", { style: spinnerStyle }), /* @__PURE__ */ React__default.default.createElement("span", null, "Processing...")) : /* @__PURE__ */ React__default.default.createElement(React__default.default.Fragment, null, isMobile ? /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "Camera Icon", width: 20, height: 20 }) : /* @__PURE__ */ React__default.default.createElement("img", { src: Upload, alt: "Upload Icon", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, isMobile ? "Capture Photo" : "Upload Photo"))
1153
- ), /* @__PURE__ */ React__default.default.createElement(
1178
+ /* @__PURE__ */ React__default.default.createElement("img", { src: Close, alt: "", width: 16, height: 16 })
1179
+ ), /* @__PURE__ */ React__default.default.createElement("h2", { style: titleStyle }, "Face Verification"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, headerSubtitle(stage.kind))), body, /* @__PURE__ */ React__default.default.createElement("div", { style: footerStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: footerTextStyle }, "Your photo is securely processed and used only for verification.")))));
1180
+ }
1181
+ var keyframes = `
1182
+ @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
1183
+ @keyframes zoomIn { from { transform: scale(.96); opacity: 0 } to { transform: scale(1); opacity: 1 } }
1184
+ @keyframes spin { to { transform: rotate(360deg) } }
1185
+ `;
1186
+ var overlayStyle = {
1187
+ position: "fixed",
1188
+ inset: 0,
1189
+ background: "rgba(0,0,0,.6)",
1190
+ backdropFilter: "blur(4px)",
1191
+ display: "flex",
1192
+ alignItems: "center",
1193
+ justifyContent: "center",
1194
+ zIndex: 999999,
1195
+ padding: 16,
1196
+ animation: "fadeIn .2s ease-out"
1197
+ };
1198
+ var modalStyle = {
1199
+ background: "#fff",
1200
+ borderRadius: 16,
1201
+ boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
1202
+ width: "100%",
1203
+ maxWidth: 448,
1204
+ animation: "zoomIn .2s ease-out"
1205
+ };
1206
+ var headerStyle = {
1207
+ position: "relative",
1208
+ padding: "24px 24px 16px",
1209
+ borderBottom: "1px solid #f3f4f6"
1210
+ };
1211
+ var closeButtonStyle = {
1212
+ position: "absolute",
1213
+ top: 16,
1214
+ right: 16,
1215
+ padding: 8,
1216
+ background: "transparent",
1217
+ border: "none",
1218
+ borderRadius: 8,
1219
+ cursor: "pointer"
1220
+ };
1221
+ var titleStyle = { margin: "0 0 4px", fontSize: 18, fontWeight: 600, color: "#111827" };
1222
+ var subtitleStyle = { margin: 0, fontSize: 14, color: "#6b7280" };
1223
+ var contentStyle = {
1224
+ padding: 24,
1225
+ display: "flex",
1226
+ flexDirection: "column",
1227
+ gap: 16
1228
+ };
1229
+ var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
1230
+ var visualGuideStyle = { position: "relative" };
1231
+ var circleStyle = {
1232
+ width: 96,
1233
+ height: 96,
1234
+ borderRadius: "50%",
1235
+ background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
1236
+ display: "flex",
1237
+ alignItems: "center",
1238
+ justifyContent: "center"
1239
+ };
1240
+ var badgeStyle = {
1241
+ position: "absolute",
1242
+ bottom: -4,
1243
+ right: -4,
1244
+ width: 28,
1245
+ height: 28,
1246
+ borderRadius: "50%",
1247
+ background: "#ffffff",
1248
+ border: "2px solid #00bc7d",
1249
+ display: "flex",
1250
+ alignItems: "center",
1251
+ justifyContent: "center"
1252
+ };
1253
+ var successCircleStyle = {
1254
+ ...circleStyle,
1255
+ background: "linear-gradient(135deg, rgba(0, 188, 125, 0.25) 0%, rgba(0, 188, 125, 0.1) 100%)",
1256
+ margin: "0 auto"
1257
+ };
1258
+ var errorCircleStyle = {
1259
+ width: 64,
1260
+ height: 64,
1261
+ borderRadius: "50%",
1262
+ background: "#fee2e2",
1263
+ color: "#b91c1c",
1264
+ fontSize: 32,
1265
+ fontWeight: 700,
1266
+ display: "flex",
1267
+ alignItems: "center",
1268
+ justifyContent: "center",
1269
+ margin: "0 auto"
1270
+ };
1271
+ var qrBoxStyle = {
1272
+ display: "flex",
1273
+ justifyContent: "center",
1274
+ padding: 12,
1275
+ background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1276
+ borderRadius: 12
1277
+ };
1278
+ var alertBoxStyle = {
1279
+ background: "#fef3c7",
1280
+ border: "1px solid #fde68a",
1281
+ color: "#92400e",
1282
+ borderRadius: 8,
1283
+ padding: 12,
1284
+ fontSize: 14
1285
+ };
1286
+ var alertTextStyle = { margin: "4px 0 0", fontSize: 13, color: "#92400e" };
1287
+ var instructionsBoxStyle = {
1288
+ background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1289
+ borderRadius: 8,
1290
+ padding: 16
1291
+ };
1292
+ var instructionsTitleStyle = { margin: "0 0 8px", fontSize: 14, fontWeight: 600, color: "#374151" };
1293
+ var instructionsListStyle = { margin: 0, padding: 0, listStyle: "none" };
1294
+ var instructionItemStyle = { display: "flex", gap: 8, fontSize: 13, color: "#4b5563", lineHeight: 1.5 };
1295
+ var bulletStyle = { color: "#00bc7d" };
1296
+ var previewBoxStyle = {
1297
+ borderRadius: 12,
1298
+ overflow: "hidden",
1299
+ border: "2px solid rgba(0, 188, 125, 0.4)",
1300
+ background: "#0f172a",
1301
+ display: "flex",
1302
+ justifyContent: "center"
1303
+ };
1304
+ var previewImgStyle = {
1305
+ width: "100%",
1306
+ maxHeight: 320,
1307
+ objectFit: "contain",
1308
+ background: "#0f172a"
1309
+ };
1310
+ var buttonsContainerStyle = {
1311
+ display: "flex",
1312
+ flexDirection: "column",
1313
+ gap: 8
1314
+ };
1315
+ var primaryButtonStyle = {
1316
+ display: "flex",
1317
+ alignItems: "center",
1318
+ justifyContent: "center",
1319
+ gap: 8,
1320
+ padding: "12px 16px",
1321
+ background: "#00bc7d",
1322
+ color: "#ffffff",
1323
+ border: "none",
1324
+ borderRadius: 8,
1325
+ fontSize: 14,
1326
+ fontWeight: 600,
1327
+ cursor: "pointer"
1328
+ };
1329
+ var secondaryButtonStyle = {
1330
+ ...primaryButtonStyle,
1331
+ background: "#ffffff",
1332
+ color: "#374151",
1333
+ border: "2px solid #e5e7eb"
1334
+ };
1335
+ var cancelButtonStyle = {
1336
+ ...primaryButtonStyle,
1337
+ background: "transparent",
1338
+ color: "#6b7280",
1339
+ border: "none"
1340
+ };
1341
+ var spinnerStyle = {
1342
+ width: 16,
1343
+ height: 16,
1344
+ border: "2px solid rgba(0, 188, 125, 0.25)",
1345
+ borderTopColor: "#00bc7d",
1346
+ borderRadius: "50%",
1347
+ animation: "spin .8s linear infinite"
1348
+ };
1349
+ var attemptsTextStyle = {
1350
+ margin: "8px 0 0",
1351
+ fontSize: 12,
1352
+ color: "#9ca3af",
1353
+ textAlign: "center"
1354
+ };
1355
+ var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1356
+ var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1357
+ function LiveCamera({ onCapture, onCancel, message }) {
1358
+ const videoRef = React.useRef(null);
1359
+ const streamRef = React.useRef(null);
1360
+ const [ready, setReady] = React.useState(false);
1361
+ const [error, setError] = React.useState(null);
1362
+ React.useEffect(() => {
1363
+ let cancelled = false;
1364
+ async function init() {
1365
+ try {
1366
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
1367
+ throw new Error("Camera is not available in this browser");
1368
+ }
1369
+ const stream = await navigator.mediaDevices.getUserMedia({
1370
+ video: {
1371
+ facingMode: "user",
1372
+ width: { ideal: 1280 },
1373
+ height: { ideal: 1280 }
1374
+ },
1375
+ audio: false
1376
+ });
1377
+ if (cancelled) {
1378
+ stream.getTracks().forEach((t) => t.stop());
1379
+ return;
1380
+ }
1381
+ streamRef.current = stream;
1382
+ const video = videoRef.current;
1383
+ if (video) {
1384
+ video.srcObject = stream;
1385
+ video.onloadedmetadata = () => {
1386
+ video.play().catch(() => {
1387
+ });
1388
+ setReady(true);
1389
+ };
1390
+ }
1391
+ } catch (err) {
1392
+ setError(err instanceof Error ? err.message : "Unable to access camera");
1393
+ }
1394
+ }
1395
+ init();
1396
+ return () => {
1397
+ cancelled = true;
1398
+ streamRef.current?.getTracks().forEach((t) => t.stop());
1399
+ streamRef.current = null;
1400
+ };
1401
+ }, []);
1402
+ const capture = () => {
1403
+ const video = videoRef.current;
1404
+ if (!video || !video.videoWidth) return;
1405
+ const size = Math.min(video.videoWidth, video.videoHeight);
1406
+ const sx = (video.videoWidth - size) / 2;
1407
+ const sy = (video.videoHeight - size) / 2;
1408
+ const canvas = document.createElement("canvas");
1409
+ canvas.width = size;
1410
+ canvas.height = size;
1411
+ const ctx = canvas.getContext("2d");
1412
+ if (!ctx) return;
1413
+ ctx.translate(size, 0);
1414
+ ctx.scale(-1, 1);
1415
+ ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1416
+ streamRef.current?.getTracks().forEach((t) => t.stop());
1417
+ streamRef.current = null;
1418
+ onCapture(canvas.toDataURL("image/jpeg", 0.9));
1419
+ };
1420
+ if (error) {
1421
+ 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")));
1422
+ }
1423
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React__default.default.createElement("div", { style: videoFrameStyle }, /* @__PURE__ */ React__default.default.createElement(
1424
+ "video",
1425
+ {
1426
+ ref: videoRef,
1427
+ playsInline: true,
1428
+ muted: true,
1429
+ autoPlay: true,
1430
+ style: videoStyle
1431
+ }
1432
+ ), /* @__PURE__ */ React__default.default.createElement("div", { style: faceOvalOverlayStyle, "aria-hidden": "true" }), !ready && /* @__PURE__ */ React__default.default.createElement("div", { style: videoLoadingStyle }, "Starting camera\u2026")), message && /* @__PURE__ */ React__default.default.createElement("p", { style: livenessMessageStyle }, message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement(
1154
1433
  "button",
1155
1434
  {
1156
- onClick: onCancel,
1157
- onMouseEnter: () => setIsHoveringCancel(true),
1158
- onMouseLeave: () => setIsHoveringCancel(false),
1159
- disabled: isProcessing,
1160
- style: cancelButtonStyle
1435
+ style: ready ? primaryButtonStyle : { ...primaryButtonStyle, opacity: 0.5, cursor: "not-allowed" },
1436
+ onClick: capture,
1437
+ disabled: !ready
1161
1438
  },
1162
- "Cancel"
1163
- ))), /* @__PURE__ */ React__default.default.createElement("div", { style: footerStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: footerTextStyle }, "Your photo will be securely processed and used only for verification purposes")))));
1439
+ /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }),
1440
+ /* @__PURE__ */ React__default.default.createElement("span", null, "Capture")
1441
+ ), /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Cancel")));
1164
1442
  }
1443
+ var videoFrameStyle = {
1444
+ position: "relative",
1445
+ width: "100%",
1446
+ aspectRatio: "1 / 1",
1447
+ borderRadius: 12,
1448
+ overflow: "hidden",
1449
+ background: "#0f172a"
1450
+ };
1451
+ var videoStyle = {
1452
+ width: "100%",
1453
+ height: "100%",
1454
+ objectFit: "cover",
1455
+ transform: "scaleX(-1)"
1456
+ // mirror so the preview matches the user's POV
1457
+ };
1458
+ var faceOvalOverlayStyle = {
1459
+ position: "absolute",
1460
+ inset: 0,
1461
+ pointerEvents: "none",
1462
+ background: "radial-gradient(ellipse 38% 50% at center, transparent 60%, rgba(15, 23, 42, 0.55) 62%)",
1463
+ border: "2px solid rgba(0, 188, 125, 0.6)",
1464
+ borderRadius: 12
1465
+ };
1466
+ var videoLoadingStyle = {
1467
+ position: "absolute",
1468
+ inset: 0,
1469
+ display: "flex",
1470
+ alignItems: "center",
1471
+ justifyContent: "center",
1472
+ color: "#e5e7eb",
1473
+ fontSize: 14,
1474
+ fontWeight: 500
1475
+ };
1476
+ var livenessMessageStyle = {
1477
+ margin: 0,
1478
+ textAlign: "center",
1479
+ fontSize: 13,
1480
+ fontWeight: 500,
1481
+ color: "#374151",
1482
+ background: "rgba(0, 188, 125, 0.08)",
1483
+ border: "1px solid rgba(0, 188, 125, 0.25)",
1484
+ borderRadius: 8,
1485
+ padding: "8px 12px"
1486
+ };
1165
1487
  var FaceCaptureModal_default = FaceCaptureModal;
1166
1488
 
1167
1489
  // src/kyc/hooks.ts
@@ -1445,7 +1767,7 @@ function useKycPreferences(client, autoFetch = true) {
1445
1767
  refresh: fetchPreferences
1446
1768
  };
1447
1769
  }
1448
- function fileToBase642(file) {
1770
+ function fileToBase64(file) {
1449
1771
  return new Promise((resolve, reject) => {
1450
1772
  const reader = new FileReader();
1451
1773
  reader.onload = () => {
@@ -1502,43 +1824,52 @@ function getRiskColor(risk) {
1502
1824
  return colorMap[risk] || "#6b7280";
1503
1825
  }
1504
1826
  function useReuseKYCSubmission(client$1) {
1505
- const verifyFace = (reference, token) => {
1506
- return new Promise((resolve, reject) => {
1827
+ const startSession = React.useCallback(
1828
+ (request) => client$1.createReuseKycSession(request),
1829
+ [client$1]
1830
+ );
1831
+ const verifyFace = React.useCallback(
1832
+ (session, opts = {}) => new Promise((resolve) => {
1507
1833
  const container = document.createElement("div");
1508
1834
  document.body.appendChild(container);
1509
1835
  const root = client.createRoot(container);
1510
1836
  const cleanup = () => {
1511
1837
  root.unmount();
1512
- document.body.removeChild(container);
1838
+ container.remove();
1513
1839
  };
1514
1840
  const modal = React.createElement(FaceCaptureModal_default, {
1515
- onCapture: async (base64) => {
1516
- try {
1517
- const resp = await client$1.submitReuseKycSession({
1518
- proof: base64,
1519
- reference,
1520
- token
1521
- });
1522
- resolve(resp);
1523
- } catch (error) {
1524
- reject(error instanceof Error ? error : new Error("Face verification failed"));
1525
- } finally {
1526
- cleanup();
1527
- }
1528
- },
1529
- onCancel: () => {
1841
+ client: client$1,
1842
+ session,
1843
+ defaultDevice: opts.defaultDevice,
1844
+ renderQR: opts.renderQR,
1845
+ onComplete: (result) => {
1530
1846
  cleanup();
1531
- resolve(null);
1847
+ resolve(result);
1532
1848
  }
1533
1849
  });
1534
1850
  root.render(modal);
1535
- });
1536
- };
1537
- return { verifyFace };
1851
+ }),
1852
+ [client$1]
1853
+ );
1854
+ const runFlow = React.useCallback(
1855
+ async (request, opts = {}) => {
1856
+ const session = await startSession(request);
1857
+ if (!session.is_required) {
1858
+ return { kind: "not_required", session };
1859
+ }
1860
+ const result = await verifyFace(session, opts);
1861
+ if (result === null) {
1862
+ return { kind: "cancelled", session };
1863
+ }
1864
+ return { kind: "completed", session, result };
1865
+ },
1866
+ [startSession, verifyFace]
1867
+ );
1868
+ return { startSession, verifyFace, runFlow };
1538
1869
  }
1539
1870
 
1540
1871
  exports.createDeviceFingerprint = createDeviceFingerprint;
1541
- exports.fileToBase64 = fileToBase642;
1872
+ exports.fileToBase64 = fileToBase64;
1542
1873
  exports.formatKycStatus = formatKycStatus;
1543
1874
  exports.getBrowserInfo = getBrowserInfo;
1544
1875
  exports.getRiskColor = getRiskColor;