vesant-sdk 1.6.6 → 1.7.0-dev.254dc8c

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 +302 -296
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +301 -295
  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 +86 -27
  35. package/dist/kyc/core.js.map +1 -1
  36. package/dist/kyc/core.mjs +86 -28
  37. package/dist/kyc/core.mjs.map +1 -1
  38. package/dist/kyc/index.d.mts +280 -50
  39. package/dist/kyc/index.d.ts +280 -50
  40. package/dist/kyc/index.js +86 -27
  41. package/dist/kyc/index.js.map +1 -1
  42. package/dist/kyc/index.mjs +86 -28
  43. package/dist/kyc/index.mjs.map +1 -1
  44. package/dist/react.d.mts +46 -9
  45. package/dist/react.d.ts +46 -9
  46. package/dist/react.js +891 -276
  47. package/dist/react.js.map +1 -1
  48. package/dist/react.mjs +890 -275
  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.js CHANGED
@@ -10,7 +10,19 @@ var React__default = /*#__PURE__*/_interopDefault(React);
10
10
  // src/geolocation/hooks.ts
11
11
 
12
12
  // src/core/version.ts
13
- var SDK_VERSION = "1.6.6";
13
+ var SDK_VERSION = "1.7.0";
14
+
15
+ // src/core/errors.ts
16
+ var VesantError = class _VesantError extends Error {
17
+ constructor(message, code, statusCode, details) {
18
+ super(message);
19
+ this.code = code;
20
+ this.statusCode = statusCode;
21
+ this.details = details;
22
+ this.name = "VesantError";
23
+ Object.setPrototypeOf(this, _VesantError.prototype);
24
+ }
25
+ };
14
26
 
15
27
  // src/shared/browser-utils.ts
16
28
  function generateUUID() {
@@ -200,7 +212,7 @@ function encodePayload(payload) {
200
212
  } else if (typeof Buffer !== "undefined") {
201
213
  return Buffer.from(json, "utf-8").toString("base64");
202
214
  }
203
- throw new Error("No base64 encoding method available");
215
+ throw new VesantError("No base64 encoding method available", "BASE64_UNAVAILABLE");
204
216
  }
205
217
  async function generateCipherText(options, config) {
206
218
  const warnings = [];
@@ -225,8 +237,9 @@ async function generateCipherText(options, config) {
225
237
  if (location) {
226
238
  locationData = location;
227
239
  } else if (gpsRequiredByConfig) {
228
- throw new Error(
229
- `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`
240
+ throw new VesantError(
241
+ `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
242
+ "GPS_REQUIRED"
230
243
  );
231
244
  } else {
232
245
  warnings.push("GPS location not available or permission denied");
@@ -839,7 +852,7 @@ function useCustomerProfile(client, customerId, options = {}) {
839
852
  profileRef.current = profile;
840
853
  const updateProfile = React.useCallback(
841
854
  async (updates) => {
842
- if (!profileRef.current) throw new Error("Profile not loaded");
855
+ if (!profileRef.current) throw new VesantError("Profile not loaded", "PROFILE_NOT_LOADED");
843
856
  setLoading(true);
844
857
  setError(null);
845
858
  try {
@@ -875,280 +888,872 @@ function useCustomerProfile(client, customerId, options = {}) {
875
888
  var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjAvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMtU1ZHLTIwMDEwOTA0L0RURC9zdmcxMC5kdGQiPg0KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4NCjxzdmcgdmVyc2lvbj0iMS4wIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgDQoJIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDY0IDY0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA2NCA2NCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBmaWxsPSIjMjMxRjIwIiBkPSJNNjAsMTBINDkuNjU2bC02LjgyOC02LjgyOEM0Mi4wNzgsMi40MjIsNDEuMDYyLDIsNDAsMkgyNGMtMS4wNjIsMC0yLjA3OCwwLjQyMi0yLjgyOCwxLjE3MkwxNC4zNDQsMTBINA0KCQljLTIuMjExLDAtNCwxLjc4OS00LDR2NDRjMCwyLjIxMSwxLjc4OSw0LDQsNGg1NmMyLjIxMSwwLDQtMS43ODksNC00VjE0QzY0LDExLjc4OSw2Mi4yMTEsMTAsNjAsMTB6IE0zMiw1MA0KCQljLTguODM2LDAtMTYtNy4xNjQtMTYtMTZzNy4xNjQtMTYsMTYtMTZzMTYsNy4xNjQsMTYsMTZTNDAuODM2LDUwLDMyLDUweiIvPg0KCTxjaXJjbGUgZmlsbD0iIzIzMUYyMCIgY3g9IjMyIiBjeT0iMzQiIHI9IjgiLz4NCjwvZz4NCjwvc3ZnPg==";
876
889
  var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
877
890
  var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
878
- var Upload = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNyAxN0gxNy4wMU0xNS42IDE0SDE4QzE4LjkzMTkgMTQgMTkuMzk3OCAxNCAxOS43NjU0IDE0LjE1MjJDMjAuMjU1NCAxNC4zNTUyIDIwLjY0NDggMTQuNzQ0NiAyMC44NDc4IDE1LjIzNDZDMjEgMTUuNjAyMiAyMSAxNi4wNjgxIDIxIDE3QzIxIDE3LjkzMTkgMjEgMTguMzk3OCAyMC44NDc4IDE4Ljc2NTRDMjAuNjQ0OCAxOS4yNTU0IDIwLjI1NTQgMTkuNjQ0OCAxOS43NjU0IDE5Ljg0NzhDMTkuMzk3OCAyMCAxOC45MzE5IDIwIDE4IDIwSDZDNS4wNjgxMiAyMCA0LjYwMjE4IDIwIDQuMjM0NjMgMTkuODQ3OEMzLjc0NDU4IDE5LjY0NDggMy4zNTUyMyAxOS4yNTU0IDMuMTUyMjQgMTguNzY1NEMzIDE4LjM5NzggMyAxNy45MzE5IDMgMTdDMyAxNi4wNjgxIDMgMTUuNjAyMiAzLjE1MjI0IDE1LjIzNDZDMy4zNTUyMyAxNC43NDQ2IDMuNzQ0NTggMTQuMzU1MiA0LjIzNDYzIDE0LjE1MjJDNC42MDIxOCAxNCA1LjA2ODEyIDE0IDYgMTRIOC40TTEyIDE1VjRNMTIgNEwxNSA3TTEyIDRMOSA3IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+DQo8L3N2Zz4=";
879
891
 
880
892
  // src/kyc/FaceCaptureModal.tsx
881
- var fileToBase64 = (file) => {
882
- return new Promise((resolve, reject) => {
883
- const reader = new FileReader();
884
- reader.readAsDataURL(file);
885
- reader.onload = () => resolve(reader.result);
886
- reader.onerror = (error) => reject(error);
887
- });
888
- };
893
+ var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
894
+ var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
895
+ var LIVENESS_MESSAGES = [
896
+ "Position your face inside the circle",
897
+ "Make sure your face is well lit",
898
+ "Remove glasses or hats if possible",
899
+ "Look straight at the camera, then tap Capture"
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 Declined";
909
+ case "max_attempts":
910
+ return "We couldn't verify your identity";
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
+ }
889
919
  function FaceCaptureModal({
890
- onCapture,
891
- onCancel
920
+ client,
921
+ session,
922
+ onComplete,
923
+ onCancel,
924
+ defaultDevice,
925
+ renderQR
892
926
  }) {
893
- const inputRef = React.useRef(null);
894
- const [isProcessing, setIsProcessing] = React.useState(false);
895
- const [isHoveringPrimary, setIsHoveringPrimary] = React.useState(false);
896
- const [isHoveringCancel, setIsHoveringCancel] = React.useState(false);
897
- const [isHoveringClose, setIsHoveringClose] = React.useState(false);
898
- const isMobile = /Mobi|Android/i.test(navigator.userAgent);
899
- const handleFileChange = async (e) => {
900
- const file = e.target.files?.[0];
901
- if (!file) return;
902
- setIsProcessing(true);
903
- try {
904
- const base64 = await fileToBase64(file);
905
- setTimeout(() => {
906
- onCapture(base64);
907
- }, 300);
908
- } catch (error) {
909
- console.error("[Vesant SDK] Error processing image:", error instanceof Error ? error.message : "Unknown error");
910
- setIsProcessing(false);
927
+ const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
928
+ const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
929
+ const initialStage = (() => {
930
+ if (initialChoice === "this") return { kind: "capture" };
931
+ if (initialChoice === "mobile") return { kind: "qr", mobileConnected: false };
932
+ return { kind: "choose" };
933
+ })();
934
+ const hasMethodChoice = initialChoice === "ask";
935
+ const [stage, setStage] = React.useState(initialStage);
936
+ const [attempts, setAttempts] = React.useState(session.attempts);
937
+ const maxAttempts = session.max_attempts || 1;
938
+ const [qrDeclinedReason, setQrDeclinedReason] = React.useState(null);
939
+ const [captureMode, setCaptureMode] = React.useState("idle");
940
+ const [capturedPreview, setCapturedPreview] = React.useState(null);
941
+ const [capturedBase64, setCapturedBase64] = React.useState(null);
942
+ const [livenessMessage, setLivenessMessage] = React.useState(LIVENESS_MESSAGES[0]);
943
+ const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
944
+ const cancelledRef = React.useRef(false);
945
+ const submittingRef = React.useRef(false);
946
+ React.useEffect(() => {
947
+ const timer = setTimeout(() => {
948
+ setStage(
949
+ (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
950
+ );
951
+ }, SESSION_EXPIRY_MS);
952
+ return () => clearTimeout(timer);
953
+ }, [session.token]);
954
+ React.useEffect(() => {
955
+ if (stage.kind !== "qr") return;
956
+ let stopped = false;
957
+ const intervalMs = 2e3;
958
+ const deadline = Date.now() + SESSION_EXPIRY_MS;
959
+ let mobileSeen = stage.mobileConnected;
960
+ const tick = async () => {
961
+ if (stopped || cancelledRef.current) return;
962
+ try {
963
+ if (mobileSeen) {
964
+ const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
965
+ if (result.status === "accepted") {
966
+ setStage({ kind: "accepted", result });
967
+ stopped = true;
968
+ return;
969
+ }
970
+ if (result.status === "declined") {
971
+ setAttempts(maxAttempts - (result.data?.retries_remaining ?? 0));
972
+ if (result.data?.retry_limit_exceeded) {
973
+ setStage({ kind: "max_attempts", result });
974
+ stopped = true;
975
+ return;
976
+ }
977
+ if (result.declined_reason) {
978
+ setQrDeclinedReason(result.declined_reason);
979
+ }
980
+ }
981
+ } else {
982
+ const handoff = await client.getHandoffSession(session.token);
983
+ if (handoff.mobile_connected) {
984
+ mobileSeen = true;
985
+ setStage({ kind: "qr", mobileConnected: true });
986
+ }
987
+ }
988
+ } catch {
989
+ }
990
+ if (Date.now() < deadline) {
991
+ setTimeout(tick, intervalMs);
992
+ }
993
+ };
994
+ tick();
995
+ return () => {
996
+ stopped = true;
997
+ };
998
+ }, [stage.kind, stage.kind === "qr" ? stage.mobileConnected : false]);
999
+ const applyResult = (result) => {
1000
+ if (result.status === "accepted") {
1001
+ setStage({ kind: "accepted", result });
1002
+ return true;
911
1003
  }
1004
+ if (result.status === "declined") {
1005
+ const nextAttempts = maxAttempts - (result.data?.retries_remaining ?? 0);
1006
+ setAttempts(nextAttempts);
1007
+ setCapturedPreview(null);
1008
+ setCapturedBase64(null);
1009
+ if (result.data?.retry_limit_exceeded) {
1010
+ setStage({ kind: "max_attempts", result });
1011
+ } else {
1012
+ setStage({ kind: "declined", result });
1013
+ }
1014
+ return true;
1015
+ }
1016
+ return false;
912
1017
  };
913
- const overlayStyle = {
914
- position: "fixed",
915
- inset: 0,
916
- background: "rgba(0, 0, 0, 0.6)",
917
- backdropFilter: "blur(4px)",
918
- display: "flex",
919
- alignItems: "center",
920
- justifyContent: "center",
921
- zIndex: 999999,
922
- padding: "16px",
923
- animation: "fadeIn 0.2s ease-out"
924
- };
925
- const modalStyle = {
926
- background: "#ffffff",
927
- borderRadius: "16px",
928
- boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
929
- width: "100%",
930
- maxWidth: "448px",
931
- animation: "zoomIn 0.2s ease-out"
932
- };
933
- const headerStyle = {
934
- position: "relative",
935
- padding: "24px 24px 16px",
936
- borderBottom: "1px solid #f3f4f6"
937
- };
938
- const closeButtonStyle = {
939
- position: "absolute",
940
- top: "16px",
941
- right: "16px",
942
- padding: "8px",
943
- background: isHoveringClose ? "#f3f4f6" : "transparent",
944
- border: "none",
945
- borderRadius: "9999px",
946
- cursor: "pointer",
947
- transition: "background-color 0.2s",
948
- display: "flex",
949
- alignItems: "center",
950
- justifyContent: "center"
951
- };
952
- const titleStyle = {
953
- fontSize: "24px",
954
- fontWeight: 600,
955
- color: "#111827",
956
- margin: 0
957
- };
958
- const subtitleStyle = {
959
- fontSize: "14px",
960
- color: "#6b7280",
961
- marginTop: "4px"
962
- };
963
- const contentStyle = {
964
- padding: "32px 24px"
965
- };
966
- const visualGuideContainerStyle = {
967
- marginBottom: "32px",
968
- display: "flex",
969
- justifyContent: "center"
970
- };
971
- const visualGuideStyle = {
972
- position: "relative"
973
- };
974
- const circleStyle = {
975
- width: "128px",
976
- height: "128px",
977
- borderRadius: "50%",
978
- background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
979
- display: "flex",
980
- alignItems: "center",
981
- justifyContent: "center"
982
- };
983
- const badgeStyle = {
984
- position: "absolute",
985
- top: "-4px",
986
- right: "-4px",
987
- width: "32px",
988
- height: "32px",
989
- background: "#ffffff",
990
- borderRadius: "50%",
991
- boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
992
- display: "flex",
993
- alignItems: "center",
994
- justifyContent: "center"
995
- };
996
- const instructionsBoxStyle = {
997
- background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
998
- borderRadius: "12px",
999
- padding: "16px",
1000
- marginBottom: "24px"
1001
- };
1002
- const instructionsTitleStyle = {
1003
- fontSize: "14px",
1004
- fontWeight: 500,
1005
- color: "#111827",
1006
- marginBottom: "8px"
1007
- };
1008
- const instructionsListStyle = {
1009
- fontSize: "12px",
1010
- color: "#6b7280",
1011
- listStyle: "none",
1012
- padding: 0,
1013
- margin: 0
1014
- };
1015
- const instructionItemStyle = {
1016
- display: "flex",
1017
- alignItems: "flex-start",
1018
- marginBottom: "6px"
1018
+ const runSubmit = async (proof) => {
1019
+ if (submittingRef.current) return;
1020
+ submittingRef.current = true;
1021
+ setStage({ kind: "submitting" });
1022
+ let settled = false;
1023
+ const ref = session.reference;
1024
+ const baselineAttempts = attempts;
1025
+ const isFreshPollResult = (r) => {
1026
+ if (r.status === "accepted") return true;
1027
+ if (r.status === "declined") {
1028
+ const derivedAttempts = maxAttempts - (r.data?.retries_remaining ?? 0);
1029
+ return derivedAttempts > baselineAttempts;
1030
+ }
1031
+ return false;
1032
+ };
1033
+ try {
1034
+ const postPromise = (async () => {
1035
+ try {
1036
+ const data = await client.submitEventBasedFaceVerificationSession({
1037
+ token: session.token,
1038
+ reference: ref,
1039
+ proof
1040
+ });
1041
+ return { ok: true, data };
1042
+ } catch (err2) {
1043
+ return { ok: false, error: err2 };
1044
+ }
1045
+ })();
1046
+ const pollPromise = (async () => {
1047
+ await new Promise((r) => setTimeout(r, 12e3));
1048
+ const deadline = Date.now() + 12e4;
1049
+ while (Date.now() < deadline) {
1050
+ if (settled || cancelledRef.current) return null;
1051
+ try {
1052
+ const r = await client.getEventBasedFaceVerificationSessionStatus(session.token);
1053
+ if (isFreshPollResult(r)) return r;
1054
+ } catch {
1055
+ }
1056
+ if (settled || cancelledRef.current) return null;
1057
+ await new Promise((r) => setTimeout(r, 3e3));
1058
+ }
1059
+ return null;
1060
+ })();
1061
+ const winner = await Promise.race([
1062
+ postPromise.then(
1063
+ (r) => r.ok ? { from: "post", data: r.data } : { from: "post_err" }
1064
+ ),
1065
+ pollPromise.then(
1066
+ (data) => data ? { from: "poll", data } : { from: "poll_done" }
1067
+ )
1068
+ ]);
1069
+ settled = true;
1070
+ if (winner.from === "post" && applyResult(winner.data)) return;
1071
+ if (winner.from === "poll" && applyResult(winner.data)) return;
1072
+ const [postResult, pollResult] = await Promise.all([postPromise, pollPromise]);
1073
+ if (postResult.ok && applyResult(postResult.data)) return;
1074
+ if (pollResult && applyResult(pollResult)) return;
1075
+ const err = postResult.ok ? null : postResult.error;
1076
+ setStage({
1077
+ kind: "error",
1078
+ message: err instanceof Error ? err.message : "Submission failed. Please try again."
1079
+ });
1080
+ } finally {
1081
+ settled = true;
1082
+ submittingRef.current = false;
1083
+ }
1019
1084
  };
1020
- const bulletStyle = {
1021
- color: "#00bc7d",
1022
- marginRight: "8px",
1023
- flexShrink: 0
1085
+ const handleCameraCapture = (dataUrl) => {
1086
+ const raw = dataUrl.split(",")[1] ?? "";
1087
+ setCapturedPreview(dataUrl);
1088
+ setCapturedBase64(raw);
1089
+ setCaptureMode("preview");
1024
1090
  };
1025
- const buttonsContainerStyle = {
1026
- display: "flex",
1027
- flexDirection: "column",
1028
- gap: "12px"
1091
+ const handleCameraCancel = () => setCaptureMode("idle");
1092
+ const handleRetake = () => {
1093
+ setCapturedPreview(null);
1094
+ setCapturedBase64(null);
1095
+ setCaptureMode("live");
1029
1096
  };
1030
- const primaryButtonStyle = {
1031
- width: "100%",
1032
- background: isProcessing ? "#d1d5db" : isHoveringPrimary ? "#00a86d" : "#00bc7d",
1033
- color: "#ffffff",
1034
- fontWeight: 500,
1035
- padding: "14px 24px",
1036
- borderRadius: "12px",
1037
- border: "none",
1038
- cursor: isProcessing ? "not-allowed" : "pointer",
1039
- transition: "all 0.2s",
1040
- display: "flex",
1041
- alignItems: "center",
1042
- justifyContent: "center",
1043
- gap: "12px",
1044
- boxShadow: isProcessing ? "none" : isHoveringPrimary ? "0 20px 25px -5px rgba(0, 188, 125, 0.3)" : "0 10px 15px -3px rgba(0, 188, 125, 0.2)",
1045
- fontSize: "16px"
1097
+ const handleConfirmSubmit = () => {
1098
+ if (!capturedBase64) return;
1099
+ void runSubmit(capturedBase64);
1046
1100
  };
1047
- const cancelButtonStyle = {
1048
- width: "100%",
1049
- background: isProcessing ? "#f3f4f6" : isHoveringCancel ? "#f9fafb" : "#ffffff",
1050
- color: "#374151",
1051
- fontWeight: 500,
1052
- padding: "14px 24px",
1053
- borderRadius: "12px",
1054
- border: "2px solid #e5e7eb",
1055
- cursor: isProcessing ? "not-allowed" : "pointer",
1056
- transition: "all 0.2s",
1057
- fontSize: "16px"
1101
+ const handleOpenCamera = () => {
1102
+ if (submittingRef.current) return;
1103
+ setCaptureMode("live");
1058
1104
  };
1059
- const footerStyle = {
1060
- padding: "0 24px 24px"
1105
+ const handleRetakeAfterDecline = () => {
1106
+ setCapturedPreview(null);
1107
+ setCapturedBase64(null);
1108
+ setCaptureMode("live");
1109
+ setStage({ kind: "capture" });
1061
1110
  };
1062
- const footerTextStyle = {
1063
- fontSize: "12px",
1064
- textAlign: "center",
1065
- color: "#9ca3af",
1066
- margin: 0
1111
+ React.useEffect(() => {
1112
+ if (captureMode !== "live") return;
1113
+ setLivenessMessage(LIVENESS_MESSAGES[0]);
1114
+ let i = 0;
1115
+ const interval = setInterval(() => {
1116
+ i = (i + 1) % LIVENESS_MESSAGES.length;
1117
+ setLivenessMessage(LIVENESS_MESSAGES[i]);
1118
+ }, 3e3);
1119
+ return () => clearInterval(interval);
1120
+ }, [captureMode]);
1121
+ const close = (result) => {
1122
+ cancelledRef.current = true;
1123
+ if (result === null && onCancel) {
1124
+ onCancel();
1125
+ }
1126
+ onComplete(result);
1067
1127
  };
1068
- const spinnerStyle = {
1069
- width: "20px",
1070
- height: "20px",
1071
- border: "2px solid rgba(255, 255, 255, 0.3)",
1072
- borderTopColor: "#ffffff",
1073
- borderRadius: "50%",
1074
- animation: "spin 0.6s linear infinite"
1128
+ 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(
1129
+ "button",
1130
+ {
1131
+ style: primaryButtonStyle,
1132
+ onClick: () => setStage({ kind: "capture" })
1133
+ },
1134
+ /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }),
1135
+ /* @__PURE__ */ React__default.default.createElement("span", null, "Continue on this device")
1136
+ ), /* @__PURE__ */ React__default.default.createElement(
1137
+ "button",
1138
+ {
1139
+ style: secondaryButtonStyle,
1140
+ onClick: () => setStage({ kind: "qr", mobileConnected: false })
1141
+ },
1142
+ "Continue on mobile (scan QR)"
1143
+ ), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1144
+ 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(
1145
+ "button",
1146
+ {
1147
+ style: secondaryButtonStyle,
1148
+ onClick: () => setStage({ kind: "capture" })
1149
+ },
1150
+ "Use this device instead"
1151
+ ), hasMethodChoice && /* @__PURE__ */ React__default.default.createElement(
1152
+ "button",
1153
+ {
1154
+ style: secondaryButtonStyle,
1155
+ onClick: () => setStage({ kind: "choose" })
1156
+ },
1157
+ "Change verification method"
1158
+ ), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")));
1159
+ const renderCapture = (declinedReason) => {
1160
+ if (captureMode === "live") {
1161
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement(
1162
+ LiveCamera,
1163
+ {
1164
+ onCapture: handleCameraCapture,
1165
+ onCancel: handleCameraCancel,
1166
+ message: livenessMessage
1167
+ }
1168
+ ), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1169
+ }
1170
+ if (captureMode === "preview" && capturedPreview) {
1171
+ 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));
1172
+ }
1173
+ 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")), hasMethodChoice && /* @__PURE__ */ React__default.default.createElement(
1174
+ "button",
1175
+ {
1176
+ style: secondaryButtonStyle,
1177
+ onClick: () => setStage({ kind: "choose" })
1178
+ },
1179
+ "Change verification method"
1180
+ ), /* @__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));
1075
1181
  };
1076
- const hiddenInputStyle = {
1077
- display: "none"
1182
+ 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."));
1183
+ 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")));
1184
+ const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Retake")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1185
+ const renderMaxAttempts = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "For your security, your account has been temporarily restricted", result.data.freeze_duration_minutes ? ` for ${result.data.freeze_duration_minutes} minutes` : "", ". Please contact support if you need immediate help."), result.data?.enforce_logout && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), result.data?.block && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "This action has been blocked for security reasons."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1186
+ const renderError = (message) => {
1187
+ const sessionExpired = /session\s+expired/i.test(message);
1188
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try Again"), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1078
1189
  };
1079
- const styleTag = /* @__PURE__ */ React__default.default.createElement("style", null, `
1080
- @keyframes fadeIn {
1081
- from {
1082
- opacity: 0;
1083
- }
1084
- to {
1085
- opacity: 1;
1086
- }
1087
- }
1088
-
1089
- @keyframes zoomIn {
1090
- from {
1091
- opacity: 0;
1092
- transform: scale(0.95);
1093
- }
1094
- to {
1095
- opacity: 1;
1096
- transform: scale(1);
1097
- }
1098
- }
1099
-
1100
- @keyframes spin {
1101
- from {
1102
- transform: rotate(0deg);
1103
- }
1104
- to {
1105
- transform: rotate(360deg);
1106
- }
1107
- }
1108
- `);
1109
- 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(
1190
+ const body = (() => {
1191
+ switch (stage.kind) {
1192
+ case "choose":
1193
+ return renderChoose();
1194
+ case "qr":
1195
+ return renderQRStage(stage.mobileConnected);
1196
+ case "capture":
1197
+ return renderCapture(stage.declinedReason);
1198
+ case "submitting":
1199
+ return renderSubmitting();
1200
+ case "accepted":
1201
+ return renderAccepted(stage.result);
1202
+ case "declined":
1203
+ return renderDeclined(stage.result);
1204
+ case "max_attempts":
1205
+ return renderMaxAttempts(stage.result);
1206
+ case "error":
1207
+ return renderError(stage.message);
1208
+ }
1209
+ })();
1210
+ 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(
1110
1211
  "button",
1111
1212
  {
1112
- onClick: onCancel,
1113
- onMouseEnter: () => setIsHoveringClose(true),
1114
- onMouseLeave: () => setIsHoveringClose(false),
1213
+ onClick: () => close(null),
1115
1214
  style: closeButtonStyle,
1116
1215
  "aria-label": "Close"
1117
1216
  },
1118
- /* @__PURE__ */ React__default.default.createElement("img", { src: Close, alt: "Close Icon", width: 16, height: 16 })
1119
- ), /* @__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(
1120
- "input",
1217
+ /* @__PURE__ */ React__default.default.createElement("img", { src: Close, alt: "", width: 16, height: 16 })
1218
+ ), /* @__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.")))));
1219
+ }
1220
+ var keyframes = `
1221
+ @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
1222
+ @keyframes zoomIn { from { transform: scale(.96); opacity: 0 } to { transform: scale(1); opacity: 1 } }
1223
+ @keyframes spin { to { transform: rotate(360deg) } }
1224
+ @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }
1225
+ `;
1226
+ var overlayStyle = {
1227
+ position: "fixed",
1228
+ inset: 0,
1229
+ background: "rgba(0,0,0,.6)",
1230
+ backdropFilter: "blur(4px)",
1231
+ display: "flex",
1232
+ alignItems: "center",
1233
+ justifyContent: "center",
1234
+ zIndex: 999999,
1235
+ padding: 16,
1236
+ animation: "fadeIn .2s ease-out"
1237
+ };
1238
+ var modalStyle = {
1239
+ background: "#fff",
1240
+ borderRadius: 16,
1241
+ boxShadow: "0 25px 50px -12px rgba(0,0,0,.25)",
1242
+ width: "100%",
1243
+ maxWidth: 448,
1244
+ animation: "zoomIn .2s ease-out"
1245
+ };
1246
+ var headerStyle = {
1247
+ position: "relative",
1248
+ padding: "24px 24px 16px",
1249
+ borderBottom: "1px solid #f3f4f6"
1250
+ };
1251
+ var closeButtonStyle = {
1252
+ position: "absolute",
1253
+ top: 16,
1254
+ right: 16,
1255
+ padding: 8,
1256
+ background: "transparent",
1257
+ border: "none",
1258
+ borderRadius: 8,
1259
+ cursor: "pointer"
1260
+ };
1261
+ var titleStyle = { margin: "0 0 4px", fontSize: 18, fontWeight: 600, color: "#111827" };
1262
+ var subtitleStyle = { margin: 0, fontSize: 14, color: "#6b7280" };
1263
+ var contentStyle = {
1264
+ padding: 24,
1265
+ display: "flex",
1266
+ flexDirection: "column",
1267
+ gap: 16
1268
+ };
1269
+ var visualGuideContainerStyle = { display: "flex", justifyContent: "center" };
1270
+ var visualGuideStyle = { position: "relative" };
1271
+ var circleStyle = {
1272
+ width: 96,
1273
+ height: 96,
1274
+ borderRadius: "50%",
1275
+ background: "linear-gradient(135deg, rgba(0, 188, 125, 0.2) 0%, rgba(0, 188, 125, 0.05) 100%)",
1276
+ display: "flex",
1277
+ alignItems: "center",
1278
+ justifyContent: "center"
1279
+ };
1280
+ var badgeStyle = {
1281
+ position: "absolute",
1282
+ bottom: -4,
1283
+ right: -4,
1284
+ width: 28,
1285
+ height: 28,
1286
+ borderRadius: "50%",
1287
+ background: "#ffffff",
1288
+ border: "2px solid #00bc7d",
1289
+ display: "flex",
1290
+ alignItems: "center",
1291
+ justifyContent: "center"
1292
+ };
1293
+ var successCircleStyle = {
1294
+ ...circleStyle,
1295
+ background: "linear-gradient(135deg, rgba(0, 188, 125, 0.25) 0%, rgba(0, 188, 125, 0.1) 100%)",
1296
+ margin: "0 auto"
1297
+ };
1298
+ var errorCircleStyle = {
1299
+ width: 64,
1300
+ height: 64,
1301
+ borderRadius: "50%",
1302
+ background: "#fee2e2",
1303
+ color: "#b91c1c",
1304
+ fontSize: 32,
1305
+ fontWeight: 700,
1306
+ display: "flex",
1307
+ alignItems: "center",
1308
+ justifyContent: "center",
1309
+ margin: "0 auto"
1310
+ };
1311
+ var qrBoxStyle = {
1312
+ display: "flex",
1313
+ justifyContent: "center",
1314
+ padding: 12,
1315
+ background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1316
+ borderRadius: 12
1317
+ };
1318
+ var alertBoxStyle = {
1319
+ background: "#fef3c7",
1320
+ border: "1px solid #fde68a",
1321
+ color: "#92400e",
1322
+ borderRadius: 8,
1323
+ padding: 12,
1324
+ fontSize: 14
1325
+ };
1326
+ var alertTextStyle = { margin: "4px 0 0", fontSize: 13, color: "#92400e" };
1327
+ var instructionsBoxStyle = {
1328
+ background: "linear-gradient(135deg, #f9fafb 0%, rgba(243, 244, 246, 0.5) 100%)",
1329
+ borderRadius: 8,
1330
+ padding: 16
1331
+ };
1332
+ var instructionsTitleStyle = { margin: "0 0 8px", fontSize: 14, fontWeight: 600, color: "#374151" };
1333
+ var instructionsListStyle = { margin: 0, padding: 0, listStyle: "none" };
1334
+ var instructionItemStyle = { display: "flex", gap: 8, fontSize: 13, color: "#4b5563", lineHeight: 1.5 };
1335
+ var bulletStyle = { color: "#00bc7d" };
1336
+ var previewBoxStyle = {
1337
+ borderRadius: 12,
1338
+ overflow: "hidden",
1339
+ border: "2px solid rgba(0, 188, 125, 0.4)",
1340
+ background: "#0f172a",
1341
+ display: "flex",
1342
+ justifyContent: "center"
1343
+ };
1344
+ var previewImgStyle = {
1345
+ width: "100%",
1346
+ maxHeight: 320,
1347
+ objectFit: "contain",
1348
+ background: "#0f172a"
1349
+ };
1350
+ var buttonsContainerStyle = {
1351
+ display: "flex",
1352
+ flexDirection: "column",
1353
+ gap: 8
1354
+ };
1355
+ var primaryButtonStyle = {
1356
+ display: "flex",
1357
+ alignItems: "center",
1358
+ justifyContent: "center",
1359
+ gap: 8,
1360
+ padding: "12px 16px",
1361
+ background: "#00bc7d",
1362
+ color: "#ffffff",
1363
+ border: "none",
1364
+ borderRadius: 8,
1365
+ fontSize: 14,
1366
+ fontWeight: 600,
1367
+ cursor: "pointer"
1368
+ };
1369
+ var secondaryButtonStyle = {
1370
+ ...primaryButtonStyle,
1371
+ background: "#ffffff",
1372
+ color: "#374151",
1373
+ border: "2px solid #e5e7eb"
1374
+ };
1375
+ var cancelButtonStyle = {
1376
+ ...primaryButtonStyle,
1377
+ background: "transparent",
1378
+ color: "#6b7280",
1379
+ border: "none"
1380
+ };
1381
+ var spinnerStyle = {
1382
+ width: 16,
1383
+ height: 16,
1384
+ border: "2px solid rgba(0, 188, 125, 0.25)",
1385
+ borderTopColor: "#00bc7d",
1386
+ borderRadius: "50%",
1387
+ animation: "spin .8s linear infinite"
1388
+ };
1389
+ var attemptsTextStyle = {
1390
+ margin: "8px 0 0",
1391
+ fontSize: 12,
1392
+ color: "#9ca3af",
1393
+ textAlign: "center"
1394
+ };
1395
+ var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1396
+ var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1397
+ var FACE_MESSAGES = {
1398
+ idle: "Initializing camera...",
1399
+ loading: "Loading face detection...",
1400
+ scanning: "Position your face in the circle",
1401
+ too_far: "Face too far. Please move closer to the camera",
1402
+ too_close: "Too close. Please move back a little",
1403
+ off_center: "Center your face in the circle",
1404
+ ok: "Looking great! Tap the button to capture"
1405
+ };
1406
+ var FACE_COLORS = {
1407
+ idle: "#9ca3af",
1408
+ loading: "#3b82f6",
1409
+ scanning: "#3b82f6",
1410
+ too_far: "#ef4444",
1411
+ too_close: "#f59e0b",
1412
+ off_center: "#f59e0b",
1413
+ ok: "#10b981"
1414
+ };
1415
+ var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
1416
+ var mediapipeLoaderPromise = null;
1417
+ function loadMediaPipeFaceDetection() {
1418
+ if (typeof window === "undefined" || typeof document === "undefined") {
1419
+ return Promise.reject(new Error("MediaPipe requires a browser environment"));
1420
+ }
1421
+ const w = window;
1422
+ if (w.FaceDetection) {
1423
+ return Promise.resolve(w.FaceDetection);
1424
+ }
1425
+ if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
1426
+ mediapipeLoaderPromise = new Promise((resolve, reject) => {
1427
+ const script = document.createElement("script");
1428
+ script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
1429
+ script.async = true;
1430
+ script.crossOrigin = "anonymous";
1431
+ script.onload = () => {
1432
+ if (w.FaceDetection) {
1433
+ resolve(w.FaceDetection);
1434
+ } else {
1435
+ mediapipeLoaderPromise = null;
1436
+ reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1437
+ }
1438
+ };
1439
+ script.onerror = () => {
1440
+ mediapipeLoaderPromise = null;
1441
+ reject(new Error("Failed to load MediaPipe face_detection script"));
1442
+ };
1443
+ document.head.appendChild(script);
1444
+ });
1445
+ return mediapipeLoaderPromise;
1446
+ }
1447
+ function LiveCamera({ onCapture, onCancel, message }) {
1448
+ const videoRef = React.useRef(null);
1449
+ const streamRef = React.useRef(null);
1450
+ const [ready, setReady] = React.useState(false);
1451
+ const [error, setError] = React.useState(null);
1452
+ const [faceStatus, setFaceStatus] = React.useState("idle");
1453
+ const [faceDetectionFailed, setFaceDetectionFailed] = React.useState(false);
1454
+ React.useEffect(() => {
1455
+ let cancelled = false;
1456
+ async function init() {
1457
+ try {
1458
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
1459
+ throw new Error("Camera is not available in this browser");
1460
+ }
1461
+ const stream = await navigator.mediaDevices.getUserMedia({
1462
+ video: {
1463
+ facingMode: "user",
1464
+ width: { ideal: 1280 },
1465
+ height: { ideal: 1280 }
1466
+ },
1467
+ audio: false
1468
+ });
1469
+ if (cancelled) {
1470
+ stream.getTracks().forEach((t) => t.stop());
1471
+ return;
1472
+ }
1473
+ streamRef.current = stream;
1474
+ const video = videoRef.current;
1475
+ if (video) {
1476
+ video.srcObject = stream;
1477
+ video.onloadedmetadata = () => {
1478
+ video.play().catch(() => {
1479
+ });
1480
+ setReady(true);
1481
+ };
1482
+ }
1483
+ } catch (err) {
1484
+ setError(err instanceof Error ? err.message : "Unable to access camera");
1485
+ }
1486
+ }
1487
+ init();
1488
+ return () => {
1489
+ cancelled = true;
1490
+ streamRef.current?.getTracks().forEach((t) => t.stop());
1491
+ streamRef.current = null;
1492
+ };
1493
+ }, []);
1494
+ React.useEffect(() => {
1495
+ if (!ready) {
1496
+ setFaceStatus("idle");
1497
+ return;
1498
+ }
1499
+ let cancelled = false;
1500
+ let rafId = null;
1501
+ let detector = null;
1502
+ setFaceStatus("loading");
1503
+ setFaceDetectionFailed(false);
1504
+ (async () => {
1505
+ try {
1506
+ const FaceDetection = await loadMediaPipeFaceDetection();
1507
+ if (cancelled) return;
1508
+ const d = new FaceDetection({
1509
+ locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1510
+ });
1511
+ d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1512
+ d.onResults((results) => {
1513
+ if (cancelled) return;
1514
+ const detections = results.detections ?? [];
1515
+ let next;
1516
+ if (detections.length === 0) {
1517
+ next = "scanning";
1518
+ } else {
1519
+ const bb = detections[0].boundingBox;
1520
+ if (bb.width < 0.18) next = "too_far";
1521
+ else if (bb.width > 0.72) next = "too_close";
1522
+ else if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
1523
+ next = "off_center";
1524
+ } else {
1525
+ next = "ok";
1526
+ }
1527
+ }
1528
+ setFaceStatus(next);
1529
+ });
1530
+ await d.initialize();
1531
+ if (cancelled) return;
1532
+ detector = d;
1533
+ setFaceStatus("scanning");
1534
+ const tick = async () => {
1535
+ if (cancelled) return;
1536
+ const v = videoRef.current;
1537
+ if (v?.readyState === 4 && detector) {
1538
+ try {
1539
+ await detector.send({ image: v });
1540
+ } catch {
1541
+ }
1542
+ }
1543
+ if (!cancelled) {
1544
+ rafId = requestAnimationFrame(tick);
1545
+ }
1546
+ };
1547
+ tick();
1548
+ } catch (err) {
1549
+ console.error("Face detection failed to initialize:", err);
1550
+ if (!cancelled) {
1551
+ setFaceDetectionFailed(true);
1552
+ setFaceStatus("scanning");
1553
+ }
1554
+ }
1555
+ })();
1556
+ return () => {
1557
+ cancelled = true;
1558
+ if (rafId !== null) cancelAnimationFrame(rafId);
1559
+ try {
1560
+ detector?.close?.();
1561
+ } catch {
1562
+ }
1563
+ };
1564
+ }, [ready]);
1565
+ const capture = () => {
1566
+ const video = videoRef.current;
1567
+ if (!video || !video.videoWidth) return;
1568
+ const size = Math.min(video.videoWidth, video.videoHeight);
1569
+ const sx = (video.videoWidth - size) / 2;
1570
+ const sy = (video.videoHeight - size) / 2;
1571
+ const canvas = document.createElement("canvas");
1572
+ canvas.width = size;
1573
+ canvas.height = size;
1574
+ const ctx = canvas.getContext("2d");
1575
+ if (!ctx) return;
1576
+ ctx.translate(size, 0);
1577
+ ctx.scale(-1, 1);
1578
+ ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1579
+ streamRef.current?.getTracks().forEach((t) => t.stop());
1580
+ streamRef.current = null;
1581
+ onCapture(canvas.toDataURL("image/jpeg", 0.9));
1582
+ };
1583
+ if (error) {
1584
+ 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")));
1585
+ }
1586
+ const detectionActive = ready && !faceDetectionFailed;
1587
+ const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1588
+ const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1589
+ const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1590
+ const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1591
+ const dynamicCircleStyle = {
1592
+ ...faceCircleStyle,
1593
+ ...liveFaceColor ? { borderColor: liveFaceColor } : null,
1594
+ ...ringPulse ? { animation: ringPulse } : null
1595
+ };
1596
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: videoFrameStyle }, /* @__PURE__ */ React__default.default.createElement(
1597
+ "video",
1121
1598
  {
1122
- ref: inputRef,
1123
- type: "file",
1124
- accept: "image/*",
1125
- capture: isMobile ? "user" : void 0,
1126
- style: hiddenInputStyle,
1127
- onChange: handleFileChange,
1128
- disabled: isProcessing
1599
+ ref: videoRef,
1600
+ playsInline: true,
1601
+ muted: true,
1602
+ autoPlay: true,
1603
+ style: videoStyle
1129
1604
  }
1130
- ), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement(
1605
+ ), /* @__PURE__ */ React__default.default.createElement("div", { style: faceOverlayContainerStyle, "aria-hidden": "true" }, /* @__PURE__ */ React__default.default.createElement("div", { style: faceRingWrapStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: faceDashedRingStyle }), /* @__PURE__ */ React__default.default.createElement("div", { style: dynamicCircleStyle }))), liveMessage && /* @__PURE__ */ React__default.default.createElement("div", { style: liveMessagePillContainerStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: liveMessagePillStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: liveMessagePillTextStyle }, liveMessage))), /* @__PURE__ */ React__default.default.createElement(
1131
1606
  "button",
1132
1607
  {
1133
- onClick: () => inputRef.current?.click(),
1134
- onMouseEnter: () => setIsHoveringPrimary(true),
1135
- onMouseLeave: () => setIsHoveringPrimary(false),
1136
- disabled: isProcessing,
1137
- style: primaryButtonStyle
1608
+ type: "button",
1609
+ onClick: onCancel,
1610
+ "aria-label": "Close camera",
1611
+ style: overlayCloseButtonStyle
1138
1612
  },
1139
- 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"))
1140
- ), /* @__PURE__ */ React__default.default.createElement(
1613
+ /* @__PURE__ */ React__default.default.createElement("img", { src: Close, alt: "", width: 16, height: 16 })
1614
+ ), !ready && /* @__PURE__ */ React__default.default.createElement("div", { style: videoLoadingStyle }, "Starting camera\u2026"), /* @__PURE__ */ React__default.default.createElement("div", { style: bottomGradientStyle }, /* @__PURE__ */ React__default.default.createElement(
1141
1615
  "button",
1142
1616
  {
1143
- onClick: onCancel,
1144
- onMouseEnter: () => setIsHoveringCancel(true),
1145
- onMouseLeave: () => setIsHoveringCancel(false),
1146
- disabled: isProcessing,
1147
- style: cancelButtonStyle
1617
+ type: "button",
1618
+ onClick: capture,
1619
+ disabled: captureDisabled,
1620
+ "aria-label": "Capture",
1621
+ style: captureDisabled ? { ...shutterButtonStyle, opacity: 0.5, cursor: "not-allowed" } : shutterButtonStyle
1148
1622
  },
1149
- "Cancel"
1150
- ))), /* @__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")))));
1623
+ /* @__PURE__ */ React__default.default.createElement("span", { style: shutterInnerStyle })
1624
+ )));
1151
1625
  }
1626
+ var videoFrameStyle = {
1627
+ position: "relative",
1628
+ width: "100%",
1629
+ aspectRatio: "3 / 4",
1630
+ borderRadius: 24,
1631
+ overflow: "hidden",
1632
+ background: "#000000",
1633
+ border: "4px solid rgba(255, 255, 255, 0.08)",
1634
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)"
1635
+ };
1636
+ var overlayCloseButtonStyle = {
1637
+ position: "absolute",
1638
+ top: 12,
1639
+ right: 12,
1640
+ width: 32,
1641
+ height: 32,
1642
+ display: "inline-flex",
1643
+ alignItems: "center",
1644
+ justifyContent: "center",
1645
+ padding: 0,
1646
+ border: "none",
1647
+ borderRadius: "50%",
1648
+ background: "rgba(0, 0, 0, 0.4)",
1649
+ backdropFilter: "blur(6px)",
1650
+ color: "#ffffff",
1651
+ cursor: "pointer",
1652
+ zIndex: 10
1653
+ };
1654
+ var bottomGradientStyle = {
1655
+ position: "absolute",
1656
+ bottom: 0,
1657
+ left: 0,
1658
+ right: 0,
1659
+ padding: "16px",
1660
+ background: "linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%)",
1661
+ display: "flex",
1662
+ alignItems: "center",
1663
+ justifyContent: "center",
1664
+ zIndex: 10
1665
+ };
1666
+ var shutterButtonStyle = {
1667
+ width: 64,
1668
+ height: 64,
1669
+ borderRadius: "50%",
1670
+ background: "rgba(255, 255, 255, 0.15)",
1671
+ backdropFilter: "blur(6px)",
1672
+ border: "2px solid rgba(255, 255, 255, 0.7)",
1673
+ display: "inline-flex",
1674
+ alignItems: "center",
1675
+ justifyContent: "center",
1676
+ cursor: "pointer",
1677
+ padding: 0,
1678
+ transition: "transform 120ms ease-out, background 120ms"
1679
+ };
1680
+ var shutterInnerStyle = {
1681
+ display: "block",
1682
+ width: 44,
1683
+ height: 44,
1684
+ borderRadius: "50%",
1685
+ background: "rgba(255, 255, 255, 0.85)"
1686
+ };
1687
+ var videoStyle = {
1688
+ width: "100%",
1689
+ height: "100%",
1690
+ objectFit: "cover",
1691
+ transform: "scaleX(-1)"
1692
+ // mirror so the preview matches the user's POV
1693
+ };
1694
+ var faceOverlayContainerStyle = {
1695
+ position: "absolute",
1696
+ inset: 0,
1697
+ pointerEvents: "none",
1698
+ display: "flex",
1699
+ alignItems: "center",
1700
+ justifyContent: "center"
1701
+ };
1702
+ var faceRingWrapStyle = {
1703
+ position: "relative",
1704
+ width: "72%",
1705
+ aspectRatio: "1 / 1"
1706
+ };
1707
+ var faceDashedRingStyle = {
1708
+ position: "absolute",
1709
+ inset: -6,
1710
+ borderRadius: "50%",
1711
+ border: "6px dashed rgba(255, 255, 255, 0.4)",
1712
+ animation: "spin 10s linear infinite"
1713
+ };
1714
+ var faceCircleStyle = {
1715
+ position: "absolute",
1716
+ inset: 0,
1717
+ borderRadius: "50%",
1718
+ border: "4px solid #00bc7d",
1719
+ boxShadow: "0 0 0 1000px rgba(0, 0, 0, 0.5)"
1720
+ };
1721
+ var videoLoadingStyle = {
1722
+ position: "absolute",
1723
+ inset: 0,
1724
+ display: "flex",
1725
+ alignItems: "center",
1726
+ justifyContent: "center",
1727
+ color: "#e5e7eb",
1728
+ fontSize: 14,
1729
+ fontWeight: 500,
1730
+ background: "rgba(0, 0, 0, 0.4)",
1731
+ zIndex: 5
1732
+ };
1733
+ var liveMessagePillContainerStyle = {
1734
+ position: "absolute",
1735
+ top: 12,
1736
+ left: "50%",
1737
+ transform: "translateX(-50%)",
1738
+ maxWidth: "84%",
1739
+ pointerEvents: "none"
1740
+ };
1741
+ var liveMessagePillStyle = {
1742
+ background: "rgba(0, 0, 0, 0.7)",
1743
+ backdropFilter: "blur(6px)",
1744
+ border: "1px solid rgba(255, 255, 255, 0.2)",
1745
+ borderRadius: 999,
1746
+ padding: "6px 12px",
1747
+ textAlign: "center",
1748
+ boxShadow: "0 10px 25px -5px rgba(0,0,0,.3)"
1749
+ };
1750
+ var liveMessagePillTextStyle = {
1751
+ margin: 0,
1752
+ fontSize: 12,
1753
+ fontWeight: 500,
1754
+ color: "#ffffff",
1755
+ animation: "pulse 2s ease-in-out infinite"
1756
+ };
1152
1757
  var FaceCaptureModal_default = FaceCaptureModal;
1153
1758
 
1154
1759
  // src/kyc/hooks.ts
@@ -1432,7 +2037,7 @@ function useKycPreferences(client, autoFetch = true) {
1432
2037
  refresh: fetchPreferences
1433
2038
  };
1434
2039
  }
1435
- function fileToBase642(file) {
2040
+ function fileToBase64(file) {
1436
2041
  return new Promise((resolve, reject) => {
1437
2042
  const reader = new FileReader();
1438
2043
  reader.onload = () => {
@@ -1488,44 +2093,54 @@ function getRiskColor(risk) {
1488
2093
  };
1489
2094
  return colorMap[risk] || "#6b7280";
1490
2095
  }
1491
- function useReuseKYCSubmission(client$1) {
1492
- const verifyFace = (reference, token) => {
1493
- return new Promise((resolve, reject) => {
2096
+ function useEventBasedFaceVerificationSubmission(client$1) {
2097
+ const startSession = React.useCallback(
2098
+ (request) => client$1.createEventBasedFaceVerificationSession(request),
2099
+ [client$1]
2100
+ );
2101
+ const verifyFace = React.useCallback(
2102
+ (session, opts = {}) => new Promise((resolve) => {
1494
2103
  const container = document.createElement("div");
1495
2104
  document.body.appendChild(container);
1496
2105
  const root = client.createRoot(container);
1497
2106
  const cleanup = () => {
1498
2107
  root.unmount();
1499
- document.body.removeChild(container);
2108
+ container.remove();
1500
2109
  };
1501
2110
  const modal = React.createElement(FaceCaptureModal_default, {
1502
- onCapture: async (base64) => {
1503
- try {
1504
- const resp = await client$1.submitReuseKycSession({
1505
- proof: base64,
1506
- reference,
1507
- token
1508
- });
1509
- resolve(resp);
1510
- } catch (error) {
1511
- reject(error instanceof Error ? error : new Error("Face verification failed"));
1512
- } finally {
1513
- cleanup();
1514
- }
1515
- },
1516
- onCancel: () => {
2111
+ client: client$1,
2112
+ session,
2113
+ defaultDevice: opts.defaultDevice,
2114
+ renderQR: opts.renderQR,
2115
+ onCancel: opts.onCancel,
2116
+ onComplete: (result) => {
1517
2117
  cleanup();
1518
- resolve(null);
2118
+ resolve(result);
1519
2119
  }
1520
2120
  });
1521
2121
  root.render(modal);
1522
- });
1523
- };
1524
- return { verifyFace };
2122
+ }),
2123
+ [client$1]
2124
+ );
2125
+ const runFlow = React.useCallback(
2126
+ async (request, opts = {}) => {
2127
+ const session = await startSession(request);
2128
+ if (!session.is_required) {
2129
+ return { kind: "not_required", session };
2130
+ }
2131
+ const result = await verifyFace(session, opts);
2132
+ if (result === null) {
2133
+ return { kind: "cancelled", session };
2134
+ }
2135
+ return { kind: "completed", session, result };
2136
+ },
2137
+ [startSession, verifyFace]
2138
+ );
2139
+ return { startSession, verifyFace, runFlow };
1525
2140
  }
1526
2141
 
1527
2142
  exports.createDeviceFingerprint = createDeviceFingerprint;
1528
- exports.fileToBase64 = fileToBase642;
2143
+ exports.fileToBase64 = fileToBase64;
1529
2144
  exports.formatKycStatus = formatKycStatus;
1530
2145
  exports.getBrowserInfo = getBrowserInfo;
1531
2146
  exports.getRiskColor = getRiskColor;
@@ -1534,6 +2149,7 @@ exports.isValidFileSize = isValidFileSize;
1534
2149
  exports.isValidFileType = isValidFileType;
1535
2150
  exports.useCipherText = useCipherText;
1536
2151
  exports.useCustomerProfile = useCustomerProfile;
2152
+ exports.useEventBasedFaceVerificationSubmission = useEventBasedFaceVerificationSubmission;
1537
2153
  exports.useGeolocation = useGeolocation;
1538
2154
  exports.useKycAlerts = useKycAlerts;
1539
2155
  exports.useKycOverview = useKycOverview;
@@ -1544,7 +2160,6 @@ exports.useLocationCapture = useLocationCapture;
1544
2160
  exports.useLocationRequests = useLocationRequests;
1545
2161
  exports.useLoginVerification = useLoginVerification;
1546
2162
  exports.useRegistration = useRegistration;
1547
- exports.useReuseKYCSubmission = useReuseKYCSubmission;
1548
2163
  exports.useTransactionVerification = useTransactionVerification;
1549
2164
  //# sourceMappingURL=react.js.map
1550
2165
  //# sourceMappingURL=react.js.map