vesant-sdk 1.6.6 → 1.7.0-dev.e0ee6d5

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 (86) hide show
  1. package/README.md +14 -4
  2. package/dist/{client-ePzhQKp9.d.mts → client-BolQlL5e.d.mts} +1 -1
  3. package/dist/{client-ePzhQKp9.d.ts → client-BolQlL5e.d.ts} +1 -1
  4. package/dist/client-C3DCmGe9.d.ts +436 -0
  5. package/dist/{client-C_A7QLcB.d.ts → client-DMIRx7Tu.d.mts} +5 -3
  6. package/dist/{client-BlCxjbY2.d.mts → client-DoMSYMMR.d.ts} +5 -3
  7. package/dist/client-ZNdnpWe7.d.mts +436 -0
  8. package/dist/compliance/index.d.mts +25 -429
  9. package/dist/compliance/index.d.ts +25 -429
  10. package/dist/compliance/index.js +187 -103
  11. package/dist/compliance/index.js.map +1 -1
  12. package/dist/compliance/index.mjs +187 -104
  13. package/dist/compliance/index.mjs.map +1 -1
  14. package/dist/decisions/index.d.mts +2 -2
  15. package/dist/decisions/index.d.ts +2 -2
  16. package/dist/decisions/index.js +1 -1
  17. package/dist/decisions/index.js.map +1 -1
  18. package/dist/decisions/index.mjs +1 -1
  19. package/dist/decisions/index.mjs.map +1 -1
  20. package/dist/geolocation/index.d.mts +4 -4
  21. package/dist/geolocation/index.d.ts +4 -4
  22. package/dist/geolocation/index.js +7 -24
  23. package/dist/geolocation/index.js.map +1 -1
  24. package/dist/geolocation/index.mjs +7 -24
  25. package/dist/geolocation/index.mjs.map +1 -1
  26. package/dist/index.d.mts +12 -70
  27. package/dist/index.d.ts +12 -70
  28. package/dist/index.js +294 -292
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +293 -291
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/kyc/core.d.mts +4 -4
  33. package/dist/kyc/core.d.ts +4 -4
  34. package/dist/kyc/core.js +78 -23
  35. package/dist/kyc/core.js.map +1 -1
  36. package/dist/kyc/core.mjs +78 -24
  37. package/dist/kyc/core.mjs.map +1 -1
  38. package/dist/kyc/index.d.mts +269 -45
  39. package/dist/kyc/index.d.ts +269 -45
  40. package/dist/kyc/index.js +78 -23
  41. package/dist/kyc/index.js.map +1 -1
  42. package/dist/kyc/index.mjs +78 -24
  43. package/dist/kyc/index.mjs.map +1 -1
  44. package/dist/react.d.mts +42 -7
  45. package/dist/react.d.ts +42 -7
  46. package/dist/react.js +663 -277
  47. package/dist/react.js.map +1 -1
  48. package/dist/react.mjs +663 -277
  49. package/dist/react.mjs.map +1 -1
  50. package/dist/risk-profile/index.d.mts +4 -4
  51. package/dist/risk-profile/index.d.ts +4 -4
  52. package/dist/risk-profile/index.js +1 -1
  53. package/dist/risk-profile/index.js.map +1 -1
  54. package/dist/risk-profile/index.mjs +1 -1
  55. package/dist/risk-profile/index.mjs.map +1 -1
  56. package/dist/scores/index.d.mts +2 -2
  57. package/dist/scores/index.d.ts +2 -2
  58. package/dist/scores/index.js +1 -1
  59. package/dist/scores/index.js.map +1 -1
  60. package/dist/scores/index.mjs +1 -1
  61. package/dist/scores/index.mjs.map +1 -1
  62. package/dist/tax/index.d.mts +6 -41
  63. package/dist/tax/index.d.ts +6 -41
  64. package/dist/tax/index.js +1 -36
  65. package/dist/tax/index.js.map +1 -1
  66. package/dist/tax/index.mjs +1 -36
  67. package/dist/tax/index.mjs.map +1 -1
  68. package/dist/{types-1RzYeSal.d.mts → types-BOFaMQxI.d.mts} +2 -2
  69. package/dist/{types-B4Ezqo7V.d.mts → types-CBQRNL-l.d.mts} +14 -1
  70. package/dist/{types-B4Ezqo7V.d.ts → types-CBQRNL-l.d.ts} +14 -1
  71. package/dist/{types-X5Md_dD_.d.ts → types-UGyDl1fd.d.ts} +2 -2
  72. package/dist/webhooks/index.d.mts +189 -2
  73. package/dist/webhooks/index.d.ts +189 -2
  74. package/dist/webhooks/index.js +49 -7
  75. package/dist/webhooks/index.js.map +1 -1
  76. package/dist/webhooks/index.mjs +49 -7
  77. package/dist/webhooks/index.mjs.map +1 -1
  78. package/package.json +16 -13
  79. package/dist/fraud/index.d.mts +0 -80
  80. package/dist/fraud/index.d.ts +0 -80
  81. package/dist/fraud/index.js +0 -606
  82. package/dist/fraud/index.js.map +0 -1
  83. package/dist/fraud/index.mjs +0 -604
  84. package/dist/fraud/index.mjs.map +0 -1
  85. package/dist/index-B04H4xfJ.d.mts +0 -320
  86. package/dist/index-CItMPmLL.d.ts +0 -320
package/dist/react.mjs CHANGED
@@ -4,7 +4,19 @@ import { createRoot } from 'react-dom/client';
4
4
  // src/geolocation/hooks.ts
5
5
 
6
6
  // src/core/version.ts
7
- var SDK_VERSION = "1.6.6";
7
+ var SDK_VERSION = "1.7.0";
8
+
9
+ // src/core/errors.ts
10
+ var VesantError = class _VesantError extends Error {
11
+ constructor(message, code, statusCode, details) {
12
+ super(message);
13
+ this.code = code;
14
+ this.statusCode = statusCode;
15
+ this.details = details;
16
+ this.name = "VesantError";
17
+ Object.setPrototypeOf(this, _VesantError.prototype);
18
+ }
19
+ };
8
20
 
9
21
  // src/shared/browser-utils.ts
10
22
  function generateUUID() {
@@ -194,7 +206,7 @@ function encodePayload(payload) {
194
206
  } else if (typeof Buffer !== "undefined") {
195
207
  return Buffer.from(json, "utf-8").toString("base64");
196
208
  }
197
- throw new Error("No base64 encoding method available");
209
+ throw new VesantError("No base64 encoding method available", "BASE64_UNAVAILABLE");
198
210
  }
199
211
  async function generateCipherText(options, config) {
200
212
  const warnings = [];
@@ -219,8 +231,9 @@ async function generateCipherText(options, config) {
219
231
  if (location) {
220
232
  locationData = location;
221
233
  } else if (gpsRequiredByConfig) {
222
- throw new Error(
223
- `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`
234
+ throw new VesantError(
235
+ `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
236
+ "GPS_REQUIRED"
224
237
  );
225
238
  } else {
226
239
  warnings.push("GPS location not available or permission denied");
@@ -833,7 +846,7 @@ function useCustomerProfile(client, customerId, options = {}) {
833
846
  profileRef.current = profile;
834
847
  const updateProfile = useCallback(
835
848
  async (updates) => {
836
- if (!profileRef.current) throw new Error("Profile not loaded");
849
+ if (!profileRef.current) throw new VesantError("Profile not loaded", "PROFILE_NOT_LOADED");
837
850
  setLoading(true);
838
851
  setError(null);
839
852
  try {
@@ -872,277 +885,641 @@ var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id
872
885
  var Upload = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNyAxN0gxNy4wMU0xNS42IDE0SDE4QzE4LjkzMTkgMTQgMTkuMzk3OCAxNCAxOS43NjU0IDE0LjE1MjJDMjAuMjU1NCAxNC4zNTUyIDIwLjY0NDggMTQuNzQ0NiAyMC44NDc4IDE1LjIzNDZDMjEgMTUuNjAyMiAyMSAxNi4wNjgxIDIxIDE3QzIxIDE3LjkzMTkgMjEgMTguMzk3OCAyMC44NDc4IDE4Ljc2NTRDMjAuNjQ0OCAxOS4yNTU0IDIwLjI1NTQgMTkuNjQ0OCAxOS43NjU0IDE5Ljg0NzhDMTkuMzk3OCAyMCAxOC45MzE5IDIwIDE4IDIwSDZDNS4wNjgxMiAyMCA0LjYwMjE4IDIwIDQuMjM0NjMgMTkuODQ3OEMzLjc0NDU4IDE5LjY0NDggMy4zNTUyMyAxOS4yNTU0IDMuMTUyMjQgMTguNzY1NEMzIDE4LjM5NzggMyAxNy45MzE5IDMgMTdDMyAxNi4wNjgxIDMgMTUuNjAyMiAzLjE1MjI0IDE1LjIzNDZDMy4zNTUyMyAxNC43NDQ2IDMuNzQ0NTggMTQuMzU1MiA0LjIzNDYzIDE0LjE1MjJDNC42MDIxOCAxNCA1LjA2ODEyIDE0IDYgMTRIOC40TTEyIDE1VjRNMTIgNEwxNSA3TTEyIDRMOSA3IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+DQo8L3N2Zz4=";
873
886
 
874
887
  // src/kyc/FaceCaptureModal.tsx
875
- var fileToBase64 = (file) => {
876
- return new Promise((resolve, reject) => {
877
- const reader = new FileReader();
878
- reader.readAsDataURL(file);
879
- reader.onload = () => resolve(reader.result);
880
- reader.onerror = (error) => reject(error);
881
- });
882
- };
888
+ var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
889
+ var LIVENESS_MESSAGES = [
890
+ "Position your face inside the circle",
891
+ "Make sure your face is well lit",
892
+ "Remove glasses or hats if possible",
893
+ "Look straight at the camera, then tap Capture"
894
+ ];
895
+ function headerSubtitle(stageKind) {
896
+ switch (stageKind) {
897
+ case "qr":
898
+ return "Use your phone to continue";
899
+ case "accepted":
900
+ return "All set";
901
+ case "declined":
902
+ return "Verification didn't match, try once more";
903
+ case "max_attempts":
904
+ return "We couldn't verify you";
905
+ default:
906
+ return "Please capture a clear photo of your face";
907
+ }
908
+ }
909
+ function defaultRenderQR(payload) {
910
+ const url = `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(payload)}`;
911
+ return /* @__PURE__ */ React.createElement("img", { src: url, alt: "QR code", width: 220, height: 220 });
912
+ }
883
913
  function FaceCaptureModal({
884
- onCapture,
885
- onCancel
914
+ client,
915
+ session,
916
+ onComplete,
917
+ defaultDevice,
918
+ renderQR
886
919
  }) {
887
- const inputRef = useRef(null);
888
- const [isProcessing, setIsProcessing] = useState(false);
889
- const [isHoveringPrimary, setIsHoveringPrimary] = useState(false);
890
- const [isHoveringCancel, setIsHoveringCancel] = useState(false);
891
- const [isHoveringClose, setIsHoveringClose] = useState(false);
892
- const isMobile = /Mobi|Android/i.test(navigator.userAgent);
893
- const handleFileChange = async (e) => {
894
- const file = e.target.files?.[0];
895
- if (!file) return;
896
- setIsProcessing(true);
897
- try {
898
- const base64 = await fileToBase64(file);
899
- setTimeout(() => {
900
- onCapture(base64);
901
- }, 300);
902
- } catch (error) {
903
- console.error("[Vesant SDK] Error processing image:", error instanceof Error ? error.message : "Unknown error");
904
- setIsProcessing(false);
920
+ const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
921
+ const initialStage = (() => {
922
+ const choice = defaultDevice ?? (isMobile ? "this" : "ask");
923
+ if (choice === "this") return { kind: "capture" };
924
+ if (choice === "mobile") return { kind: "qr", mobileConnected: false };
925
+ return { kind: "choose" };
926
+ })();
927
+ const [stage, setStage] = useState(initialStage);
928
+ const [attempts, setAttempts] = useState(session.attempts);
929
+ const maxAttempts = session.max_attempts || 1;
930
+ const [qrDeclinedReason, setQrDeclinedReason] = useState(null);
931
+ const [captureMode, setCaptureMode] = useState("idle");
932
+ const [capturedPreview, setCapturedPreview] = useState(null);
933
+ const [capturedBase64, setCapturedBase64] = useState(null);
934
+ const [livenessMessage, setLivenessMessage] = useState(LIVENESS_MESSAGES[0]);
935
+ const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
936
+ const cancelledRef = useRef(false);
937
+ const submittingRef = useRef(false);
938
+ useEffect(() => {
939
+ if (stage.kind !== "qr") return;
940
+ let stopped = false;
941
+ const intervalMs = 2e3;
942
+ const deadline = Date.now() + 15 * 60 * 1e3;
943
+ let mobileSeen = stage.mobileConnected;
944
+ const tick = async () => {
945
+ if (stopped || cancelledRef.current) return;
946
+ try {
947
+ if (mobileSeen) {
948
+ const result = await client.getReuseKycSessionStatus(session.reference);
949
+ if (result.status === "accepted") {
950
+ setStage({ kind: "accepted", result });
951
+ stopped = true;
952
+ return;
953
+ }
954
+ if (result.status === "declined") {
955
+ setAttempts(maxAttempts - (result.data?.retries_remaining ?? 0));
956
+ if (result.data?.retry_limit_exceeded) {
957
+ setStage({ kind: "max_attempts", result });
958
+ stopped = true;
959
+ return;
960
+ }
961
+ if (result.declined_reason) {
962
+ setQrDeclinedReason(result.declined_reason);
963
+ }
964
+ }
965
+ } else {
966
+ const handoff = await client.getHandoffSession(session.token);
967
+ if (handoff.mobile_connected) {
968
+ mobileSeen = true;
969
+ setStage({ kind: "qr", mobileConnected: true });
970
+ }
971
+ }
972
+ } catch {
973
+ }
974
+ if (Date.now() < deadline) {
975
+ setTimeout(tick, intervalMs);
976
+ }
977
+ };
978
+ tick();
979
+ return () => {
980
+ stopped = true;
981
+ };
982
+ }, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
983
+ const applyResult = (result) => {
984
+ if (result.status === "accepted") {
985
+ setStage({ kind: "accepted", result });
986
+ return true;
905
987
  }
988
+ if (result.status === "declined") {
989
+ const nextAttempts = maxAttempts - (result.data?.retries_remaining ?? 0);
990
+ setAttempts(nextAttempts);
991
+ setCapturedPreview(null);
992
+ setCapturedBase64(null);
993
+ if (result.data?.retry_limit_exceeded) {
994
+ setStage({ kind: "max_attempts", result });
995
+ } else {
996
+ setStage({ kind: "declined", result });
997
+ }
998
+ return true;
999
+ }
1000
+ return false;
906
1001
  };
907
- const overlayStyle = {
908
- position: "fixed",
909
- inset: 0,
910
- background: "rgba(0, 0, 0, 0.6)",
911
- backdropFilter: "blur(4px)",
912
- display: "flex",
913
- alignItems: "center",
914
- justifyContent: "center",
915
- zIndex: 999999,
916
- padding: "16px",
917
- animation: "fadeIn 0.2s ease-out"
918
- };
919
- const modalStyle = {
920
- background: "#ffffff",
921
- borderRadius: "16px",
922
- boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
923
- width: "100%",
924
- maxWidth: "448px",
925
- animation: "zoomIn 0.2s ease-out"
926
- };
927
- const headerStyle = {
928
- position: "relative",
929
- padding: "24px 24px 16px",
930
- borderBottom: "1px solid #f3f4f6"
931
- };
932
- const closeButtonStyle = {
933
- position: "absolute",
934
- top: "16px",
935
- right: "16px",
936
- padding: "8px",
937
- background: isHoveringClose ? "#f3f4f6" : "transparent",
938
- border: "none",
939
- borderRadius: "9999px",
940
- cursor: "pointer",
941
- transition: "background-color 0.2s",
942
- display: "flex",
943
- alignItems: "center",
944
- justifyContent: "center"
945
- };
946
- const titleStyle = {
947
- fontSize: "24px",
948
- fontWeight: 600,
949
- color: "#111827",
950
- margin: 0
951
- };
952
- const subtitleStyle = {
953
- fontSize: "14px",
954
- color: "#6b7280",
955
- marginTop: "4px"
956
- };
957
- const contentStyle = {
958
- padding: "32px 24px"
959
- };
960
- const visualGuideContainerStyle = {
961
- marginBottom: "32px",
962
- display: "flex",
963
- justifyContent: "center"
964
- };
965
- const visualGuideStyle = {
966
- position: "relative"
967
- };
968
- const circleStyle = {
969
- width: "128px",
970
- height: "128px",
971
- borderRadius: "50%",
972
- background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
973
- display: "flex",
974
- alignItems: "center",
975
- justifyContent: "center"
976
- };
977
- const badgeStyle = {
978
- position: "absolute",
979
- top: "-4px",
980
- right: "-4px",
981
- width: "32px",
982
- height: "32px",
983
- background: "#ffffff",
984
- borderRadius: "50%",
985
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
986
- display: "flex",
987
- alignItems: "center",
988
- justifyContent: "center"
989
- };
990
- const instructionsBoxStyle = {
991
- background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
992
- borderRadius: "12px",
993
- padding: "16px",
994
- marginBottom: "24px"
995
- };
996
- const instructionsTitleStyle = {
997
- fontSize: "14px",
998
- fontWeight: 500,
999
- color: "#111827",
1000
- marginBottom: "8px"
1001
- };
1002
- const instructionsListStyle = {
1003
- fontSize: "12px",
1004
- color: "#6b7280",
1005
- listStyle: "none",
1006
- padding: 0,
1007
- margin: 0
1008
- };
1009
- const instructionItemStyle = {
1010
- display: "flex",
1011
- alignItems: "flex-start",
1012
- marginBottom: "6px"
1013
- };
1014
- const bulletStyle = {
1015
- color: "#00bc7d",
1016
- marginRight: "8px",
1017
- flexShrink: 0
1018
- };
1019
- const buttonsContainerStyle = {
1020
- display: "flex",
1021
- flexDirection: "column",
1022
- gap: "12px"
1002
+ const runSubmit = async (proof) => {
1003
+ if (submittingRef.current) return;
1004
+ submittingRef.current = true;
1005
+ setStage({ kind: "submitting" });
1006
+ let settled = false;
1007
+ const ref = session.reference;
1008
+ const baselineAttempts = attempts;
1009
+ const isFreshPollResult = (r) => {
1010
+ if (r.status === "accepted") return true;
1011
+ if (r.status === "declined") {
1012
+ const derivedAttempts = maxAttempts - (r.data?.retries_remaining ?? 0);
1013
+ return derivedAttempts > baselineAttempts;
1014
+ }
1015
+ return false;
1016
+ };
1017
+ try {
1018
+ const postPromise = (async () => {
1019
+ try {
1020
+ const data = await client.submitReuseKycSession({
1021
+ token: session.token,
1022
+ reference: ref,
1023
+ proof
1024
+ });
1025
+ return { ok: true, data };
1026
+ } catch (err2) {
1027
+ return { ok: false, error: err2 };
1028
+ }
1029
+ })();
1030
+ const pollPromise = (async () => {
1031
+ await new Promise((r) => setTimeout(r, 12e3));
1032
+ const deadline = Date.now() + 12e4;
1033
+ while (Date.now() < deadline) {
1034
+ if (settled || cancelledRef.current) return null;
1035
+ try {
1036
+ const r = await client.getReuseKycSessionStatus(ref);
1037
+ if (isFreshPollResult(r)) return r;
1038
+ } catch {
1039
+ }
1040
+ if (settled || cancelledRef.current) return null;
1041
+ await new Promise((r) => setTimeout(r, 3e3));
1042
+ }
1043
+ return null;
1044
+ })();
1045
+ const winner = await Promise.race([
1046
+ postPromise.then(
1047
+ (r) => r.ok ? { from: "post", data: r.data } : { from: "post_err" }
1048
+ ),
1049
+ pollPromise.then(
1050
+ (data) => data ? { from: "poll", data } : { from: "poll_done" }
1051
+ )
1052
+ ]);
1053
+ settled = true;
1054
+ if (winner.from === "post" && applyResult(winner.data)) return;
1055
+ if (winner.from === "poll" && applyResult(winner.data)) return;
1056
+ const [postResult, pollResult] = await Promise.all([postPromise, pollPromise]);
1057
+ if (postResult.ok && applyResult(postResult.data)) return;
1058
+ if (pollResult && applyResult(pollResult)) return;
1059
+ const err = postResult.ok ? null : postResult.error;
1060
+ setStage({
1061
+ kind: "error",
1062
+ message: err instanceof Error ? err.message : "Submission failed. Please try again."
1063
+ });
1064
+ } finally {
1065
+ settled = true;
1066
+ submittingRef.current = false;
1067
+ }
1023
1068
  };
1024
- const primaryButtonStyle = {
1025
- width: "100%",
1026
- background: isProcessing ? "#d1d5db" : isHoveringPrimary ? "#00a86d" : "#00bc7d",
1027
- color: "#ffffff",
1028
- fontWeight: 500,
1029
- padding: "14px 24px",
1030
- borderRadius: "12px",
1031
- border: "none",
1032
- cursor: isProcessing ? "not-allowed" : "pointer",
1033
- transition: "all 0.2s",
1034
- display: "flex",
1035
- alignItems: "center",
1036
- justifyContent: "center",
1037
- gap: "12px",
1038
- boxShadow: isProcessing ? "none" : isHoveringPrimary ? "0 20px 25px -5px rgba(0, 188, 125, 0.3)" : "0 10px 15px -3px rgba(0, 188, 125, 0.2)",
1039
- fontSize: "16px"
1069
+ const handleCameraCapture = (dataUrl) => {
1070
+ const raw = dataUrl.split(",")[1] ?? "";
1071
+ setCapturedPreview(dataUrl);
1072
+ setCapturedBase64(raw);
1073
+ setCaptureMode("preview");
1040
1074
  };
1041
- const cancelButtonStyle = {
1042
- width: "100%",
1043
- background: isProcessing ? "#f3f4f6" : isHoveringCancel ? "#f9fafb" : "#ffffff",
1044
- color: "#374151",
1045
- fontWeight: 500,
1046
- padding: "14px 24px",
1047
- borderRadius: "12px",
1048
- border: "2px solid #e5e7eb",
1049
- cursor: isProcessing ? "not-allowed" : "pointer",
1050
- transition: "all 0.2s",
1051
- fontSize: "16px"
1075
+ const handleCameraCancel = () => setCaptureMode("idle");
1076
+ const handleRetake = () => {
1077
+ setCapturedPreview(null);
1078
+ setCapturedBase64(null);
1079
+ setCaptureMode("live");
1052
1080
  };
1053
- const footerStyle = {
1054
- padding: "0 24px 24px"
1081
+ const handleConfirmSubmit = () => {
1082
+ if (!capturedBase64) return;
1083
+ void runSubmit(capturedBase64);
1055
1084
  };
1056
- const footerTextStyle = {
1057
- fontSize: "12px",
1058
- textAlign: "center",
1059
- color: "#9ca3af",
1060
- margin: 0
1085
+ const handleOpenCamera = () => {
1086
+ if (submittingRef.current) return;
1087
+ setCaptureMode("live");
1061
1088
  };
1062
- const spinnerStyle = {
1063
- width: "20px",
1064
- height: "20px",
1065
- border: "2px solid rgba(255, 255, 255, 0.3)",
1066
- borderTopColor: "#ffffff",
1067
- borderRadius: "50%",
1068
- animation: "spin 0.6s linear infinite"
1089
+ const handleRetakeAfterDecline = () => {
1090
+ setCapturedPreview(null);
1091
+ setCapturedBase64(null);
1092
+ setCaptureMode("live");
1093
+ setStage({ kind: "capture" });
1069
1094
  };
1070
- const hiddenInputStyle = {
1071
- display: "none"
1095
+ useEffect(() => {
1096
+ if (captureMode !== "live") return;
1097
+ setLivenessMessage(LIVENESS_MESSAGES[0]);
1098
+ let i = 0;
1099
+ const interval = setInterval(() => {
1100
+ i = (i + 1) % LIVENESS_MESSAGES.length;
1101
+ setLivenessMessage(LIVENESS_MESSAGES[i]);
1102
+ }, 3e3);
1103
+ return () => clearInterval(interval);
1104
+ }, [captureMode]);
1105
+ const close = (result) => {
1106
+ cancelledRef.current = true;
1107
+ onComplete(result);
1072
1108
  };
1073
- const styleTag = /* @__PURE__ */ React.createElement("style", null, `
1074
- @keyframes fadeIn {
1075
- from {
1076
- opacity: 0;
1077
- }
1078
- to {
1079
- opacity: 1;
1080
- }
1081
- }
1082
-
1083
- @keyframes zoomIn {
1084
- from {
1085
- opacity: 0;
1086
- transform: scale(0.95);
1087
- }
1088
- to {
1089
- opacity: 1;
1090
- transform: scale(1);
1091
- }
1092
- }
1093
-
1094
- @keyframes spin {
1095
- from {
1096
- transform: rotate(0deg);
1097
- }
1098
- to {
1099
- transform: rotate(360deg);
1100
- }
1101
- }
1102
- `);
1103
- return /* @__PURE__ */ React.createElement(React.Fragment, null, styleTag, /* @__PURE__ */ React.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React.createElement("div", { style: modalStyle }, /* @__PURE__ */ React.createElement("div", { style: headerStyle }, /* @__PURE__ */ React.createElement(
1109
+ 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(
1104
1110
  "button",
1105
1111
  {
1106
- onClick: onCancel,
1107
- onMouseEnter: () => setIsHoveringClose(true),
1108
- onMouseLeave: () => setIsHoveringClose(false),
1109
- style: closeButtonStyle,
1110
- "aria-label": "Close"
1112
+ style: primaryButtonStyle,
1113
+ onClick: () => setStage({ kind: "capture" })
1111
1114
  },
1112
- /* @__PURE__ */ React.createElement("img", { src: Close, alt: "Close Icon", width: 16, height: 16 })
1113
- ), /* @__PURE__ */ React.createElement("h2", { style: titleStyle }, "Face Verification"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Please capture or upload a clear photo of your face")), /* @__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: "Camera Icon", width: 48, height: 48 })), /* @__PURE__ */ React.createElement("div", { style: badgeStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "Check Icon", width: 16, height: 16 })))), /* @__PURE__ */ React.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React.createElement(
1114
- "input",
1115
+ /* @__PURE__ */ React.createElement("img", { src: isMobile ? Camera : Upload, alt: "", width: 20, height: 20 }),
1116
+ /* @__PURE__ */ React.createElement("span", null, "Continue on this device")
1117
+ ), /* @__PURE__ */ React.createElement(
1118
+ "button",
1115
1119
  {
1116
- ref: inputRef,
1117
- type: "file",
1118
- accept: "image/*",
1119
- capture: isMobile ? "user" : void 0,
1120
- style: hiddenInputStyle,
1121
- onChange: handleFileChange,
1122
- disabled: isProcessing
1120
+ style: secondaryButtonStyle,
1121
+ onClick: () => setStage({ kind: "qr", mobileConnected: false })
1122
+ },
1123
+ "Continue on mobile (scan QR)"
1124
+ ), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1125
+ const renderQRStage = (mobileConnected) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: qrBoxStyle }, (renderQR ?? defaultRenderQR)(qrPayload)), /* @__PURE__ */ React.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.createElement("div", { style: { ...spinnerStyle, margin: "12px auto" } }), qrDeclinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Last attempt was declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, qrDeclinedReason), /* @__PURE__ */ React.createElement("p", { style: { ...alertTextStyle, marginTop: 4 } }, "Try again on your phone. Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts, ".")), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement(
1126
+ "button",
1127
+ {
1128
+ style: secondaryButtonStyle,
1129
+ onClick: () => setStage({ kind: "capture" })
1130
+ },
1131
+ "Use this device instead"
1132
+ ), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1133
+ const renderCapture = (declinedReason) => {
1134
+ if (captureMode === "live") {
1135
+ return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement(
1136
+ LiveCamera,
1137
+ {
1138
+ onCapture: handleCameraCapture,
1139
+ onCancel: handleCameraCancel,
1140
+ message: livenessMessage
1141
+ }
1142
+ ), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1143
+ }
1144
+ if (captureMode === "preview" && capturedPreview) {
1145
+ return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Looks good? Submit this selfie or retake it."), /* @__PURE__ */ React.createElement("div", { style: previewBoxStyle }, /* @__PURE__ */ React.createElement("img", { src: capturedPreview, alt: "Captured selfie preview", style: previewImgStyle })), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleConfirmSubmit }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, "Submit")), /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: handleRetake }, "Retake"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1123
1146
  }
1124
- ), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement(
1147
+ return /* @__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("div", { style: badgeStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, declinedReason ? "Try again" : "Open Camera")), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1148
+ };
1149
+ 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."));
1150
+ 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")));
1151
+ const renderDeclined = (result) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__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));
1152
+ 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."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1153
+ const renderError = (message) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Something went wrong"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, message), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try again"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1154
+ const body = (() => {
1155
+ switch (stage.kind) {
1156
+ case "choose":
1157
+ return renderChoose();
1158
+ case "qr":
1159
+ return renderQRStage(stage.mobileConnected);
1160
+ case "capture":
1161
+ return renderCapture(stage.declinedReason);
1162
+ case "submitting":
1163
+ return renderSubmitting();
1164
+ case "accepted":
1165
+ return renderAccepted(stage.result);
1166
+ case "declined":
1167
+ return renderDeclined(stage.result);
1168
+ case "max_attempts":
1169
+ return renderMaxAttempts(stage.result);
1170
+ case "error":
1171
+ return renderError(stage.message);
1172
+ }
1173
+ })();
1174
+ 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(
1125
1175
  "button",
1126
1176
  {
1127
- onClick: () => inputRef.current?.click(),
1128
- onMouseEnter: () => setIsHoveringPrimary(true),
1129
- onMouseLeave: () => setIsHoveringPrimary(false),
1130
- disabled: isProcessing,
1131
- style: primaryButtonStyle
1177
+ onClick: () => close(null),
1178
+ style: closeButtonStyle,
1179
+ "aria-label": "Close"
1132
1180
  },
1133
- isProcessing ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: spinnerStyle }), /* @__PURE__ */ React.createElement("span", null, "Processing...")) : /* @__PURE__ */ React.createElement(React.Fragment, null, isMobile ? /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "Camera Icon", width: 20, height: 20 }) : /* @__PURE__ */ React.createElement("img", { src: Upload, alt: "Upload Icon", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, isMobile ? "Capture Photo" : "Upload Photo"))
1134
- ), /* @__PURE__ */ React.createElement(
1181
+ /* @__PURE__ */ React.createElement("img", { src: Close, alt: "", width: 16, height: 16 })
1182
+ ), /* @__PURE__ */ React.createElement("h2", { style: titleStyle }, "Face Verification"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, headerSubtitle(stage.kind))), body, /* @__PURE__ */ React.createElement("div", { style: footerStyle }, /* @__PURE__ */ React.createElement("p", { style: footerTextStyle }, "Your photo is securely processed and used only for verification.")))));
1183
+ }
1184
+ var keyframes = `
1185
+ @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
1186
+ @keyframes zoomIn { from { transform: scale(.96); opacity: 0 } to { transform: scale(1); opacity: 1 } }
1187
+ @keyframes spin { to { transform: rotate(360deg) } }
1188
+ @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }
1189
+ `;
1190
+ var overlayStyle = {
1191
+ position: "fixed",
1192
+ inset: 0,
1193
+ background: "rgba(0,0,0,.6)",
1194
+ backdropFilter: "blur(4px)",
1195
+ display: "flex",
1196
+ alignItems: "center",
1197
+ justifyContent: "center",
1198
+ zIndex: 999999,
1199
+ padding: 16,
1200
+ animation: "fadeIn .2s ease-out"
1201
+ };
1202
+ var modalStyle = {
1203
+ background: "#fff",
1204
+ borderRadius: 16,
1205
+ boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
1206
+ width: "100%",
1207
+ maxWidth: 448,
1208
+ animation: "zoomIn .2s ease-out"
1209
+ };
1210
+ var headerStyle = {
1211
+ position: "relative",
1212
+ padding: "24px 24px 16px",
1213
+ borderBottom: "1px solid #f3f4f6"
1214
+ };
1215
+ var closeButtonStyle = {
1216
+ position: "absolute",
1217
+ top: 16,
1218
+ right: 16,
1219
+ padding: 8,
1220
+ background: "transparent",
1221
+ border: "none",
1222
+ borderRadius: 8,
1223
+ cursor: "pointer"
1224
+ };
1225
+ var titleStyle = { margin: "0 0 4px", fontSize: 18, fontWeight: 600, color: "#111827" };
1226
+ var subtitleStyle = { margin: 0, fontSize: 14, color: "#6b7280" };
1227
+ var contentStyle = {
1228
+ padding: 24,
1229
+ display: "flex",
1230
+ flexDirection: "column",
1231
+ gap: 16
1232
+ };
1233
+ var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
1234
+ var visualGuideStyle = { position: "relative" };
1235
+ var circleStyle = {
1236
+ width: 96,
1237
+ height: 96,
1238
+ borderRadius: "50%",
1239
+ background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
1240
+ display: "flex",
1241
+ alignItems: "center",
1242
+ justifyContent: "center"
1243
+ };
1244
+ var badgeStyle = {
1245
+ position: "absolute",
1246
+ bottom: -4,
1247
+ right: -4,
1248
+ width: 28,
1249
+ height: 28,
1250
+ borderRadius: "50%",
1251
+ background: "#ffffff",
1252
+ border: "2px solid #00bc7d",
1253
+ display: "flex",
1254
+ alignItems: "center",
1255
+ justifyContent: "center"
1256
+ };
1257
+ var successCircleStyle = {
1258
+ ...circleStyle,
1259
+ background: "linear-gradient(135deg, rgba(0, 188, 125, 0.25) 0%, rgba(0, 188, 125, 0.1) 100%)",
1260
+ margin: "0 auto"
1261
+ };
1262
+ var errorCircleStyle = {
1263
+ width: 64,
1264
+ height: 64,
1265
+ borderRadius: "50%",
1266
+ background: "#fee2e2",
1267
+ color: "#b91c1c",
1268
+ fontSize: 32,
1269
+ fontWeight: 700,
1270
+ display: "flex",
1271
+ alignItems: "center",
1272
+ justifyContent: "center",
1273
+ margin: "0 auto"
1274
+ };
1275
+ var qrBoxStyle = {
1276
+ display: "flex",
1277
+ justifyContent: "center",
1278
+ padding: 12,
1279
+ background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1280
+ borderRadius: 12
1281
+ };
1282
+ var alertBoxStyle = {
1283
+ background: "#fef3c7",
1284
+ border: "1px solid #fde68a",
1285
+ color: "#92400e",
1286
+ borderRadius: 8,
1287
+ padding: 12,
1288
+ fontSize: 14
1289
+ };
1290
+ var alertTextStyle = { margin: "4px 0 0", fontSize: 13, color: "#92400e" };
1291
+ var instructionsBoxStyle = {
1292
+ background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1293
+ borderRadius: 8,
1294
+ padding: 16
1295
+ };
1296
+ var instructionsTitleStyle = { margin: "0 0 8px", fontSize: 14, fontWeight: 600, color: "#374151" };
1297
+ var instructionsListStyle = { margin: 0, padding: 0, listStyle: "none" };
1298
+ var instructionItemStyle = { display: "flex", gap: 8, fontSize: 13, color: "#4b5563", lineHeight: 1.5 };
1299
+ var bulletStyle = { color: "#00bc7d" };
1300
+ var previewBoxStyle = {
1301
+ borderRadius: 12,
1302
+ overflow: "hidden",
1303
+ border: "2px solid rgba(0, 188, 125, 0.4)",
1304
+ background: "#0f172a",
1305
+ display: "flex",
1306
+ justifyContent: "center"
1307
+ };
1308
+ var previewImgStyle = {
1309
+ width: "100%",
1310
+ maxHeight: 320,
1311
+ objectFit: "contain",
1312
+ background: "#0f172a"
1313
+ };
1314
+ var buttonsContainerStyle = {
1315
+ display: "flex",
1316
+ flexDirection: "column",
1317
+ gap: 8
1318
+ };
1319
+ var primaryButtonStyle = {
1320
+ display: "flex",
1321
+ alignItems: "center",
1322
+ justifyContent: "center",
1323
+ gap: 8,
1324
+ padding: "12px 16px",
1325
+ background: "#00bc7d",
1326
+ color: "#ffffff",
1327
+ border: "none",
1328
+ borderRadius: 8,
1329
+ fontSize: 14,
1330
+ fontWeight: 600,
1331
+ cursor: "pointer"
1332
+ };
1333
+ var secondaryButtonStyle = {
1334
+ ...primaryButtonStyle,
1335
+ background: "#ffffff",
1336
+ color: "#374151",
1337
+ border: "2px solid #e5e7eb"
1338
+ };
1339
+ var cancelButtonStyle = {
1340
+ ...primaryButtonStyle,
1341
+ background: "transparent",
1342
+ color: "#6b7280",
1343
+ border: "none"
1344
+ };
1345
+ var spinnerStyle = {
1346
+ width: 16,
1347
+ height: 16,
1348
+ border: "2px solid rgba(0, 188, 125, 0.25)",
1349
+ borderTopColor: "#00bc7d",
1350
+ borderRadius: "50%",
1351
+ animation: "spin .8s linear infinite"
1352
+ };
1353
+ var attemptsTextStyle = {
1354
+ margin: "8px 0 0",
1355
+ fontSize: 12,
1356
+ color: "#9ca3af",
1357
+ textAlign: "center"
1358
+ };
1359
+ var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1360
+ var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1361
+ function LiveCamera({ onCapture, onCancel, message }) {
1362
+ const videoRef = useRef(null);
1363
+ const streamRef = useRef(null);
1364
+ const [ready, setReady] = useState(false);
1365
+ const [error, setError] = useState(null);
1366
+ useEffect(() => {
1367
+ let cancelled = false;
1368
+ async function init() {
1369
+ try {
1370
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
1371
+ throw new Error("Camera is not available in this browser");
1372
+ }
1373
+ const stream = await navigator.mediaDevices.getUserMedia({
1374
+ video: {
1375
+ facingMode: "user",
1376
+ width: { ideal: 1280 },
1377
+ height: { ideal: 1280 }
1378
+ },
1379
+ audio: false
1380
+ });
1381
+ if (cancelled) {
1382
+ stream.getTracks().forEach((t) => t.stop());
1383
+ return;
1384
+ }
1385
+ streamRef.current = stream;
1386
+ const video = videoRef.current;
1387
+ if (video) {
1388
+ video.srcObject = stream;
1389
+ video.onloadedmetadata = () => {
1390
+ video.play().catch(() => {
1391
+ });
1392
+ setReady(true);
1393
+ };
1394
+ }
1395
+ } catch (err) {
1396
+ setError(err instanceof Error ? err.message : "Unable to access camera");
1397
+ }
1398
+ }
1399
+ init();
1400
+ return () => {
1401
+ cancelled = true;
1402
+ streamRef.current?.getTracks().forEach((t) => t.stop());
1403
+ streamRef.current = null;
1404
+ };
1405
+ }, []);
1406
+ const capture = () => {
1407
+ const video = videoRef.current;
1408
+ if (!video || !video.videoWidth) return;
1409
+ const size = Math.min(video.videoWidth, video.videoHeight);
1410
+ const sx = (video.videoWidth - size) / 2;
1411
+ const sy = (video.videoHeight - size) / 2;
1412
+ const canvas = document.createElement("canvas");
1413
+ canvas.width = size;
1414
+ canvas.height = size;
1415
+ const ctx = canvas.getContext("2d");
1416
+ if (!ctx) return;
1417
+ ctx.translate(size, 0);
1418
+ ctx.scale(-1, 1);
1419
+ ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1420
+ streamRef.current?.getTracks().forEach((t) => t.stop());
1421
+ streamRef.current = null;
1422
+ onCapture(canvas.toDataURL("image/jpeg", 0.9));
1423
+ };
1424
+ if (error) {
1425
+ 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")));
1426
+ }
1427
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { style: videoFrameStyle }, /* @__PURE__ */ React.createElement(
1428
+ "video",
1429
+ {
1430
+ ref: videoRef,
1431
+ playsInline: true,
1432
+ muted: true,
1433
+ autoPlay: true,
1434
+ style: videoStyle
1435
+ }
1436
+ ), /* @__PURE__ */ React.createElement("div", { style: faceOverlayContainerStyle, "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("div", { style: faceRingWrapStyle }, /* @__PURE__ */ React.createElement("div", { style: faceDashedRingStyle }), /* @__PURE__ */ React.createElement("div", { style: faceCircleStyle }))), message && /* @__PURE__ */ React.createElement("div", { style: liveMessagePillContainerStyle }, /* @__PURE__ */ React.createElement("div", { style: liveMessagePillStyle }, /* @__PURE__ */ React.createElement("span", { style: liveMessagePillTextStyle }, message))), !ready && /* @__PURE__ */ React.createElement("div", { style: videoLoadingStyle }, "Starting camera\u2026")), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement(
1135
1437
  "button",
1136
1438
  {
1137
- onClick: onCancel,
1138
- onMouseEnter: () => setIsHoveringCancel(true),
1139
- onMouseLeave: () => setIsHoveringCancel(false),
1140
- disabled: isProcessing,
1141
- style: cancelButtonStyle
1439
+ style: ready ? primaryButtonStyle : { ...primaryButtonStyle, opacity: 0.5, cursor: "not-allowed" },
1440
+ onClick: capture,
1441
+ disabled: !ready
1142
1442
  },
1143
- "Cancel"
1144
- ))), /* @__PURE__ */ React.createElement("div", { style: footerStyle }, /* @__PURE__ */ React.createElement("p", { style: footerTextStyle }, "Your photo will be securely processed and used only for verification purposes")))));
1443
+ /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }),
1444
+ /* @__PURE__ */ React.createElement("span", null, "Capture")
1445
+ ), /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Cancel")));
1145
1446
  }
1447
+ var videoFrameStyle = {
1448
+ position: "relative",
1449
+ width: "100%",
1450
+ aspectRatio: "1 / 1",
1451
+ borderRadius: 12,
1452
+ overflow: "hidden",
1453
+ background: "#0f172a"
1454
+ };
1455
+ var videoStyle = {
1456
+ width: "100%",
1457
+ height: "100%",
1458
+ objectFit: "cover",
1459
+ transform: "scaleX(-1)"
1460
+ // mirror so the preview matches the user's POV
1461
+ };
1462
+ var faceOverlayContainerStyle = {
1463
+ position: "absolute",
1464
+ inset: 0,
1465
+ pointerEvents: "none",
1466
+ display: "flex",
1467
+ alignItems: "center",
1468
+ justifyContent: "center"
1469
+ };
1470
+ var faceRingWrapStyle = {
1471
+ position: "relative",
1472
+ width: "72%",
1473
+ aspectRatio: "1 / 1"
1474
+ };
1475
+ var faceDashedRingStyle = {
1476
+ position: "absolute",
1477
+ inset: -6,
1478
+ borderRadius: "50%",
1479
+ border: "6px dashed rgba(255, 255, 255, 0.4)",
1480
+ animation: "spin 10s linear infinite"
1481
+ };
1482
+ var faceCircleStyle = {
1483
+ position: "absolute",
1484
+ inset: 0,
1485
+ borderRadius: "50%",
1486
+ border: "4px solid #00bc7d",
1487
+ boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
1488
+ };
1489
+ var videoLoadingStyle = {
1490
+ position: "absolute",
1491
+ inset: 0,
1492
+ display: "flex",
1493
+ alignItems: "center",
1494
+ justifyContent: "center",
1495
+ color: "#e5e7eb",
1496
+ fontSize: 14,
1497
+ fontWeight: 500
1498
+ };
1499
+ var liveMessagePillContainerStyle = {
1500
+ position: "absolute",
1501
+ top: 12,
1502
+ left: "50%",
1503
+ transform: "translateX(-50%)",
1504
+ maxWidth: "84%",
1505
+ pointerEvents: "none"
1506
+ };
1507
+ var liveMessagePillStyle = {
1508
+ background: "rgba(0, 0, 0, 0.7)",
1509
+ backdropFilter: "blur(6px)",
1510
+ border: "1px solid rgba(255, 255, 255, 0.2)",
1511
+ borderRadius: 999,
1512
+ padding: "6px 12px",
1513
+ textAlign: "center",
1514
+ boxShadow: "0 10px 25px -5px rgba(0,0,0,.3)"
1515
+ };
1516
+ var liveMessagePillTextStyle = {
1517
+ margin: 0,
1518
+ fontSize: 12,
1519
+ fontWeight: 500,
1520
+ color: "#ffffff",
1521
+ animation: "pulse 2s ease-in-out infinite"
1522
+ };
1146
1523
  var FaceCaptureModal_default = FaceCaptureModal;
1147
1524
 
1148
1525
  // src/kyc/hooks.ts
@@ -1426,7 +1803,7 @@ function useKycPreferences(client, autoFetch = true) {
1426
1803
  refresh: fetchPreferences
1427
1804
  };
1428
1805
  }
1429
- function fileToBase642(file) {
1806
+ function fileToBase64(file) {
1430
1807
  return new Promise((resolve, reject) => {
1431
1808
  const reader = new FileReader();
1432
1809
  reader.onload = () => {
@@ -1483,41 +1860,50 @@ function getRiskColor(risk) {
1483
1860
  return colorMap[risk] || "#6b7280";
1484
1861
  }
1485
1862
  function useReuseKYCSubmission(client) {
1486
- const verifyFace = (reference, token) => {
1487
- return new Promise((resolve, reject) => {
1863
+ const startSession = useCallback(
1864
+ (request) => client.createReuseKycSession(request),
1865
+ [client]
1866
+ );
1867
+ const verifyFace = useCallback(
1868
+ (session, opts = {}) => new Promise((resolve) => {
1488
1869
  const container = document.createElement("div");
1489
1870
  document.body.appendChild(container);
1490
1871
  const root = createRoot(container);
1491
1872
  const cleanup = () => {
1492
1873
  root.unmount();
1493
- document.body.removeChild(container);
1874
+ container.remove();
1494
1875
  };
1495
1876
  const modal = createElement(FaceCaptureModal_default, {
1496
- onCapture: async (base64) => {
1497
- try {
1498
- const resp = await client.submitReuseKycSession({
1499
- proof: base64,
1500
- reference,
1501
- token
1502
- });
1503
- resolve(resp);
1504
- } catch (error) {
1505
- reject(error instanceof Error ? error : new Error("Face verification failed"));
1506
- } finally {
1507
- cleanup();
1508
- }
1509
- },
1510
- onCancel: () => {
1877
+ client,
1878
+ session,
1879
+ defaultDevice: opts.defaultDevice,
1880
+ renderQR: opts.renderQR,
1881
+ onComplete: (result) => {
1511
1882
  cleanup();
1512
- resolve(null);
1883
+ resolve(result);
1513
1884
  }
1514
1885
  });
1515
1886
  root.render(modal);
1516
- });
1517
- };
1518
- return { verifyFace };
1887
+ }),
1888
+ [client]
1889
+ );
1890
+ const runFlow = useCallback(
1891
+ async (request, opts = {}) => {
1892
+ const session = await startSession(request);
1893
+ if (!session.is_required) {
1894
+ return { kind: "not_required", session };
1895
+ }
1896
+ const result = await verifyFace(session, opts);
1897
+ if (result === null) {
1898
+ return { kind: "cancelled", session };
1899
+ }
1900
+ return { kind: "completed", session, result };
1901
+ },
1902
+ [startSession, verifyFace]
1903
+ );
1904
+ return { startSession, verifyFace, runFlow };
1519
1905
  }
1520
1906
 
1521
- export { createDeviceFingerprint, fileToBase642 as fileToBase64, formatKycStatus, getBrowserInfo, getRiskColor, getStatusColor, isValidFileSize, isValidFileType, useCipherText, useCustomerProfile, useGeolocation, useKycAlerts, useKycOverview, useKycPreferences, useKycRequests, useKycSubmission, useLocationCapture, useLocationRequests, useLoginVerification, useRegistration, useReuseKYCSubmission, useTransactionVerification };
1907
+ export { createDeviceFingerprint, fileToBase64, formatKycStatus, getBrowserInfo, getRiskColor, getStatusColor, isValidFileSize, isValidFileType, useCipherText, useCustomerProfile, useGeolocation, useKycAlerts, useKycOverview, useKycPreferences, useKycRequests, useKycSubmission, useLocationCapture, useLocationRequests, useLoginVerification, useRegistration, useReuseKYCSubmission, useTransactionVerification };
1522
1908
  //# sourceMappingURL=react.mjs.map
1523
1909
  //# sourceMappingURL=react.mjs.map