@polyguard/sdk 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sdk.esm.js CHANGED
@@ -10837,7 +10837,7 @@ var ReconnectingWebSocket = (
10837
10837
  );
10838
10838
  var reconnecting_websocket_mjs_default = ReconnectingWebSocket;
10839
10839
 
10840
- // src/PolyguardWebsocketClientImpl.js
10840
+ // src/ui.js
10841
10841
  var import_qrcode = __toESM(require_browser(), 1);
10842
10842
  var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
10843
10843
  <style>
@@ -10851,10 +10851,149 @@ var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" heigh
10851
10851
  }
10852
10852
  </style>
10853
10853
  <circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
10854
- <circle cx="100" cy="100" r="80" stroke="#7be7c2" stroke-width="8" fill="none"
10855
- stroke-dasharray="251.2" stroke-dashoffset="188.4"
10854
+ <circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
10855
+ stroke-dasharray="251.2" stroke-dashoffset="188.4"
10856
10856
  class="spinner-circle" stroke-linecap="round" />
10857
10857
  </svg>`;
10858
+ function buildModal() {
10859
+ const modal = document.createElement("div");
10860
+ modal.style.position = "fixed";
10861
+ modal.style.top = "0";
10862
+ modal.style.left = "0";
10863
+ modal.style.width = "100vw";
10864
+ modal.style.height = "100vh";
10865
+ modal.style.background = "rgba(0,0,0,0.45)";
10866
+ modal.style.display = "flex";
10867
+ modal.style.alignItems = "flex-start";
10868
+ modal.style.justifyContent = "center";
10869
+ modal.style.overflowY = "auto";
10870
+ modal.style.paddingTop = "24px";
10871
+ modal.innerHTML = `
10872
+ <div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10873
+ <button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10874
+ <h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
10875
+ <div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
10876
+ <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
10877
+ <div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
10878
+ <div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
10879
+ <span>&#128274; Encrypted</span>
10880
+ <span>&#128241; On-device</span>
10881
+ <span>&#9202; Expires</span>
10882
+ </div>
10883
+ <div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
10884
+ <button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
10885
+ </div>
10886
+ `;
10887
+ return modal;
10888
+ }
10889
+ function showSpinner(qrDiv) {
10890
+ qrDiv.innerHTML = LOADING_SPINNER;
10891
+ }
10892
+ function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
10893
+ qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
10894
+ qrDiv.style.background = "transparent";
10895
+ qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(qrUrl);
10896
+ if (!isTargetMode) {
10897
+ const instructionText = qrDiv.nextElementSibling;
10898
+ if (instructionText) {
10899
+ instructionText.textContent = "Tap the button to verify with the Polyguard app.";
10900
+ }
10901
+ const instructionList = instructionText.nextElementSibling;
10902
+ if (instructionList && instructionList.children[1]) {
10903
+ instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
10904
+ }
10905
+ }
10906
+ }
10907
+ function renderQRCode(qrDiv, qrUrl) {
10908
+ const startTime = Date.now();
10909
+ console.log("time before qr code", startTime);
10910
+ import_qrcode.default.toString(qrUrl, { type: "svg" }, (err, svg) => {
10911
+ if (!err) qrDiv.innerHTML = svg;
10912
+ });
10913
+ console.log("time to generate qr code", Date.now() - startTime);
10914
+ }
10915
+
10916
+ // src/ticketService.js
10917
+ async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
10918
+ const ticketUrl = link_uuid ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}` : `https://${apiServer}/v2/ticket/${appId}`;
10919
+ const ticketRes = await fetch(ticketUrl, {
10920
+ method: "POST",
10921
+ headers: { "Content-Type": "application/json" },
10922
+ body: JSON.stringify({ requiredProofs, scanType })
10923
+ });
10924
+ if (!ticketRes.ok) {
10925
+ throw new Error("Failed to get ticket");
10926
+ }
10927
+ const ticketData = await ticketRes.json();
10928
+ const ticket = ticketData.ticket;
10929
+ if (!ticket) {
10930
+ throw new Error("No ticket returned from server");
10931
+ }
10932
+ return ticket;
10933
+ }
10934
+
10935
+ // src/messageHandler.js
10936
+ function handleWebSocketMessage(event, ctx) {
10937
+ const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
10938
+ let data;
10939
+ try {
10940
+ data = JSON.parse(event.data);
10941
+ } catch (e) {
10942
+ console.error("Invalid message format from server", e);
10943
+ returnError("Invalid message format from server");
10944
+ return;
10945
+ }
10946
+ if (!data) {
10947
+ console.error("Unknown message type from server", data);
10948
+ returnError("Unknown message type from server");
10949
+ ws.close();
10950
+ return;
10951
+ }
10952
+ if (data.type === "ping" && typeof data.seq !== "undefined") {
10953
+ ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
10954
+ return;
10955
+ }
10956
+ if (data.url) {
10957
+ window.location.assign(data.url);
10958
+ return;
10959
+ }
10960
+ if (data.qr_url) {
10961
+ const pcre = data.qr_url.match(/pcre=([^&]*)/);
10962
+ console.log("pcre", pcre);
10963
+ if (pcre) {
10964
+ ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
10965
+ }
10966
+ if (qrDiv) {
10967
+ const isMobile = /Mobi|Android/i.test(navigator.userAgent);
10968
+ if (isMobile) {
10969
+ renderMobileButton(qrDiv, data.qr_url, isTargetMode);
10970
+ } else {
10971
+ renderQRCode(qrDiv, data.qr_url);
10972
+ }
10973
+ }
10974
+ return;
10975
+ }
10976
+ if (data.jwt) {
10977
+ cleanup();
10978
+ ws.close();
10979
+ resolve(rawJwt ? data : data.jwt);
10980
+ return;
10981
+ }
10982
+ if (data.status) {
10983
+ if (qrDiv) showSpinner(qrDiv);
10984
+ return;
10985
+ }
10986
+ if (data.error) {
10987
+ returnError(data.error);
10988
+ ws.close();
10989
+ return;
10990
+ }
10991
+ console.error("Unknown message type from server", data);
10992
+ returnError("Unknown message type from server");
10993
+ ws.close();
10994
+ }
10995
+
10996
+ // src/PolyguardWebsocketClientImpl.js
10858
10997
  var PolyguardWebsocketClientImpl = class {
10859
10998
  constructor(params = {}) {
10860
10999
  this.apiClient = new ApiClient_default();
@@ -10884,35 +11023,7 @@ var PolyguardWebsocketClientImpl = class {
10884
11023
  this.link_uuid = link_uuid;
10885
11024
  }
10886
11025
  buildModal() {
10887
- const modal = document.createElement("div");
10888
- modal.style.position = "fixed";
10889
- modal.style.top = "0";
10890
- modal.style.left = "0";
10891
- modal.style.width = "100vw";
10892
- modal.style.height = "100vh";
10893
- modal.style.background = "rgba(0,0,0,0.45)";
10894
- modal.style.display = "flex";
10895
- modal.style.alignItems = "flex-start";
10896
- modal.style.justifyContent = "center";
10897
- modal.style.overflowY = "auto";
10898
- modal.style.paddingTop = "24px";
10899
- modal.innerHTML = `
10900
- <div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10901
- <button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10902
- <h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
10903
- <div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
10904
- <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
10905
- <div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
10906
- <ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
10907
- <li>We use the Polyguard service to verify your identity.</li>
10908
- <li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
10909
- <li>Your credentials remain private on your device.</li>
10910
- </ul>
10911
- <div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
10912
- <button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
10913
- </div>
10914
- `;
10915
- return modal;
11026
+ return buildModal();
10916
11027
  }
10917
11028
  async verify(target = null, rawJwt = false) {
10918
11029
  let modal = null;
@@ -10925,14 +11036,14 @@ var PolyguardWebsocketClientImpl = class {
10925
11036
  throw new Error(`Target element with ID '${target}' not found`);
10926
11037
  }
10927
11038
  } else {
10928
- modal = this.buildModal();
11039
+ modal = buildModal();
10929
11040
  }
10930
11041
  if (!isTargetMode) {
10931
11042
  document.body.appendChild(modal);
10932
11043
  qrDiv = modal.querySelector("#polyguard-qr");
10933
11044
  }
10934
11045
  if (qrDiv) {
10935
- qrDiv.innerHTML = LOADING_SPINNER;
11046
+ showSpinner(qrDiv);
10936
11047
  }
10937
11048
  function cleanup() {
10938
11049
  if (isTargetMode) {
@@ -10982,22 +11093,13 @@ var PolyguardWebsocketClientImpl = class {
10982
11093
  try {
10983
11094
  clearError();
10984
11095
  const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
10985
- const ticketUrl = this.link_uuid ? `https://${this.apiServer}/v2/ticket/${this.appId}/${this.link_uuid}` : `https://${this.apiServer}/v2/ticket/${this.appId}`;
10986
- const ticketRes = await fetch(ticketUrl, {
10987
- method: "POST",
10988
- headers: { "Content-Type": "application/json" },
10989
- body: JSON.stringify({ requiredProofs: this.requiredProofs, scanType: this.scanType })
11096
+ const newTicket = await fetchTicket({
11097
+ apiServer: this.apiServer,
11098
+ appId: this.appId,
11099
+ link_uuid: this.link_uuid,
11100
+ requiredProofs: this.requiredProofs,
11101
+ scanType: this.scanType
10990
11102
  });
10991
- if (!ticketRes.ok) {
10992
- returnError("Failed to get ticket");
10993
- return;
10994
- }
10995
- const ticketData = await ticketRes.json();
10996
- const newTicket = ticketData.ticket;
10997
- if (!newTicket) {
10998
- returnError("No ticket returned from server");
10999
- return;
11000
- }
11001
11103
  const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
11002
11104
  const options = {
11003
11105
  maxRetries: 10,
@@ -11007,71 +11109,8 @@ var PolyguardWebsocketClientImpl = class {
11007
11109
  reconnectionDelayGrowFactor: 1.5
11008
11110
  };
11009
11111
  ws = new reconnecting_websocket_mjs_default(wsUrl, [], options);
11010
- ws.addEventListener("message", (event) => {
11011
- try {
11012
- const data = JSON.parse(event.data);
11013
- if (data && data.type === "ping" && typeof data.seq !== "undefined") {
11014
- ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
11015
- return;
11016
- }
11017
- if (data && data.url) {
11018
- window.location.assign(data.url);
11019
- return;
11020
- } else if (data && data.qr_url) {
11021
- const pcre = data.qr_url.match(/pcre=([^&]*)/);
11022
- console.log("pcre", pcre);
11023
- if (pcre) {
11024
- ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
11025
- }
11026
- if (!qrDiv) return;
11027
- const isMobile = /Mobi|Android/i.test(navigator.userAgent);
11028
- if (isMobile) {
11029
- qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
11030
- qrDiv.style.background = "transparent";
11031
- qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(data.qr_url);
11032
- if (!isTargetMode) {
11033
- const instructionText = qrDiv.nextElementSibling;
11034
- if (instructionText) {
11035
- instructionText.textContent = "Tap the button to verify with the Polyguard app.";
11036
- }
11037
- const instructionList = instructionText.nextElementSibling;
11038
- if (instructionList && instructionList.children[1]) {
11039
- instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
11040
- }
11041
- }
11042
- } else {
11043
- const startTime = Date.now();
11044
- console.log("time before qr code", startTime);
11045
- import_qrcode.default.toString(data.qr_url, { type: "svg" }, (err, svg) => {
11046
- if (!err) qrDiv.innerHTML = svg;
11047
- });
11048
- console.log("time to generate qr code", Date.now() - startTime);
11049
- }
11050
- return;
11051
- } else if (data && data.jwt) {
11052
- cleanup();
11053
- ws.close();
11054
- resolve(rawJwt ? data : data.jwt);
11055
- return;
11056
- } else if (data && data.status) {
11057
- if (!qrDiv) return;
11058
- qrDiv.innerHTML = LOADING_SPINNER;
11059
- return;
11060
- } else if (data && data.error) {
11061
- returnError(data.error);
11062
- ws.close();
11063
- return;
11064
- } else {
11065
- console.error("Unknown message type from server", data);
11066
- returnError(`Unknown message type from server`);
11067
- ws.close();
11068
- return;
11069
- }
11070
- } catch (e) {
11071
- console.error("Invalid message format from server", e);
11072
- returnError("Invalid message format from server");
11073
- }
11074
- });
11112
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
11113
+ ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
11075
11114
  ws.addEventListener("error", () => {
11076
11115
  console.error("WebSocket error");
11077
11116
  returnError("WebSocket error");
@@ -11080,7 +11119,7 @@ var PolyguardWebsocketClientImpl = class {
11080
11119
  if (!closed) cleanup();
11081
11120
  });
11082
11121
  } catch (err) {
11083
- returnError("Failed to connect to WebSocket");
11122
+ returnError(err.message === "Failed to get ticket" || err.message === "No ticket returned from server" ? err.message : "Failed to connect to WebSocket");
11084
11123
  }
11085
11124
  });
11086
11125
  }
package/dist/sdk.js CHANGED
@@ -10870,7 +10870,7 @@ var Polyguard = (() => {
10870
10870
  }
10871
10871
  };
10872
10872
 
10873
- // src/PolyguardWebsocketClientImpl.js
10873
+ // src/ui.js
10874
10874
  var import_qrcode = __toESM(require_browser(), 1);
10875
10875
  var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
10876
10876
  <style>
@@ -10884,10 +10884,149 @@ var Polyguard = (() => {
10884
10884
  }
10885
10885
  </style>
10886
10886
  <circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
10887
- <circle cx="100" cy="100" r="80" stroke="#7be7c2" stroke-width="8" fill="none"
10888
- stroke-dasharray="251.2" stroke-dashoffset="188.4"
10887
+ <circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
10888
+ stroke-dasharray="251.2" stroke-dashoffset="188.4"
10889
10889
  class="spinner-circle" stroke-linecap="round" />
10890
10890
  </svg>`;
10891
+ function buildModal() {
10892
+ const modal = document.createElement("div");
10893
+ modal.style.position = "fixed";
10894
+ modal.style.top = "0";
10895
+ modal.style.left = "0";
10896
+ modal.style.width = "100vw";
10897
+ modal.style.height = "100vh";
10898
+ modal.style.background = "rgba(0,0,0,0.45)";
10899
+ modal.style.display = "flex";
10900
+ modal.style.alignItems = "flex-start";
10901
+ modal.style.justifyContent = "center";
10902
+ modal.style.overflowY = "auto";
10903
+ modal.style.paddingTop = "24px";
10904
+ modal.innerHTML = `
10905
+ <div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10906
+ <button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10907
+ <h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
10908
+ <div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
10909
+ <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
10910
+ <div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
10911
+ <div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
10912
+ <span>&#128274; Encrypted</span>
10913
+ <span>&#128241; On-device</span>
10914
+ <span>&#9202; Expires</span>
10915
+ </div>
10916
+ <div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
10917
+ <button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
10918
+ </div>
10919
+ `;
10920
+ return modal;
10921
+ }
10922
+ function showSpinner(qrDiv) {
10923
+ qrDiv.innerHTML = LOADING_SPINNER;
10924
+ }
10925
+ function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
10926
+ qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
10927
+ qrDiv.style.background = "transparent";
10928
+ qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(qrUrl);
10929
+ if (!isTargetMode) {
10930
+ const instructionText = qrDiv.nextElementSibling;
10931
+ if (instructionText) {
10932
+ instructionText.textContent = "Tap the button to verify with the Polyguard app.";
10933
+ }
10934
+ const instructionList = instructionText.nextElementSibling;
10935
+ if (instructionList && instructionList.children[1]) {
10936
+ instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
10937
+ }
10938
+ }
10939
+ }
10940
+ function renderQRCode(qrDiv, qrUrl) {
10941
+ const startTime = Date.now();
10942
+ console.log("time before qr code", startTime);
10943
+ import_qrcode.default.toString(qrUrl, { type: "svg" }, (err, svg) => {
10944
+ if (!err) qrDiv.innerHTML = svg;
10945
+ });
10946
+ console.log("time to generate qr code", Date.now() - startTime);
10947
+ }
10948
+
10949
+ // src/ticketService.js
10950
+ async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
10951
+ const ticketUrl = link_uuid ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}` : `https://${apiServer}/v2/ticket/${appId}`;
10952
+ const ticketRes = await fetch(ticketUrl, {
10953
+ method: "POST",
10954
+ headers: { "Content-Type": "application/json" },
10955
+ body: JSON.stringify({ requiredProofs, scanType })
10956
+ });
10957
+ if (!ticketRes.ok) {
10958
+ throw new Error("Failed to get ticket");
10959
+ }
10960
+ const ticketData = await ticketRes.json();
10961
+ const ticket = ticketData.ticket;
10962
+ if (!ticket) {
10963
+ throw new Error("No ticket returned from server");
10964
+ }
10965
+ return ticket;
10966
+ }
10967
+
10968
+ // src/messageHandler.js
10969
+ function handleWebSocketMessage(event, ctx) {
10970
+ const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
10971
+ let data;
10972
+ try {
10973
+ data = JSON.parse(event.data);
10974
+ } catch (e) {
10975
+ console.error("Invalid message format from server", e);
10976
+ returnError("Invalid message format from server");
10977
+ return;
10978
+ }
10979
+ if (!data) {
10980
+ console.error("Unknown message type from server", data);
10981
+ returnError("Unknown message type from server");
10982
+ ws.close();
10983
+ return;
10984
+ }
10985
+ if (data.type === "ping" && typeof data.seq !== "undefined") {
10986
+ ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
10987
+ return;
10988
+ }
10989
+ if (data.url) {
10990
+ window.location.assign(data.url);
10991
+ return;
10992
+ }
10993
+ if (data.qr_url) {
10994
+ const pcre = data.qr_url.match(/pcre=([^&]*)/);
10995
+ console.log("pcre", pcre);
10996
+ if (pcre) {
10997
+ ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
10998
+ }
10999
+ if (qrDiv) {
11000
+ const isMobile = /Mobi|Android/i.test(navigator.userAgent);
11001
+ if (isMobile) {
11002
+ renderMobileButton(qrDiv, data.qr_url, isTargetMode);
11003
+ } else {
11004
+ renderQRCode(qrDiv, data.qr_url);
11005
+ }
11006
+ }
11007
+ return;
11008
+ }
11009
+ if (data.jwt) {
11010
+ cleanup();
11011
+ ws.close();
11012
+ resolve(rawJwt ? data : data.jwt);
11013
+ return;
11014
+ }
11015
+ if (data.status) {
11016
+ if (qrDiv) showSpinner(qrDiv);
11017
+ return;
11018
+ }
11019
+ if (data.error) {
11020
+ returnError(data.error);
11021
+ ws.close();
11022
+ return;
11023
+ }
11024
+ console.error("Unknown message type from server", data);
11025
+ returnError("Unknown message type from server");
11026
+ ws.close();
11027
+ }
11028
+
11029
+ // src/PolyguardWebsocketClientImpl.js
10891
11030
  var PolyguardWebsocketClientImpl = class {
10892
11031
  constructor(params = {}) {
10893
11032
  this.apiClient = new ApiClient_default();
@@ -10917,35 +11056,7 @@ var Polyguard = (() => {
10917
11056
  this.link_uuid = link_uuid;
10918
11057
  }
10919
11058
  buildModal() {
10920
- const modal = document.createElement("div");
10921
- modal.style.position = "fixed";
10922
- modal.style.top = "0";
10923
- modal.style.left = "0";
10924
- modal.style.width = "100vw";
10925
- modal.style.height = "100vh";
10926
- modal.style.background = "rgba(0,0,0,0.45)";
10927
- modal.style.display = "flex";
10928
- modal.style.alignItems = "flex-start";
10929
- modal.style.justifyContent = "center";
10930
- modal.style.overflowY = "auto";
10931
- modal.style.paddingTop = "24px";
10932
- modal.innerHTML = `
10933
- <div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
10934
- <button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10935
- <h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
10936
- <div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
10937
- <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
10938
- <div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
10939
- <ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
10940
- <li>We use the Polyguard service to verify your identity.</li>
10941
- <li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
10942
- <li>Your credentials remain private on your device.</li>
10943
- </ul>
10944
- <div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
10945
- <button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
10946
- </div>
10947
- `;
10948
- return modal;
11059
+ return buildModal();
10949
11060
  }
10950
11061
  async verify(target = null, rawJwt = false) {
10951
11062
  let modal = null;
@@ -10958,14 +11069,14 @@ var Polyguard = (() => {
10958
11069
  throw new Error(`Target element with ID '${target}' not found`);
10959
11070
  }
10960
11071
  } else {
10961
- modal = this.buildModal();
11072
+ modal = buildModal();
10962
11073
  }
10963
11074
  if (!isTargetMode) {
10964
11075
  document.body.appendChild(modal);
10965
11076
  qrDiv = modal.querySelector("#polyguard-qr");
10966
11077
  }
10967
11078
  if (qrDiv) {
10968
- qrDiv.innerHTML = LOADING_SPINNER;
11079
+ showSpinner(qrDiv);
10969
11080
  }
10970
11081
  function cleanup() {
10971
11082
  if (isTargetMode) {
@@ -11015,22 +11126,13 @@ var Polyguard = (() => {
11015
11126
  try {
11016
11127
  clearError();
11017
11128
  const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
11018
- const ticketUrl = this.link_uuid ? `https://${this.apiServer}/v2/ticket/${this.appId}/${this.link_uuid}` : `https://${this.apiServer}/v2/ticket/${this.appId}`;
11019
- const ticketRes = await fetch(ticketUrl, {
11020
- method: "POST",
11021
- headers: { "Content-Type": "application/json" },
11022
- body: JSON.stringify({ requiredProofs: this.requiredProofs, scanType: this.scanType })
11129
+ const newTicket = await fetchTicket({
11130
+ apiServer: this.apiServer,
11131
+ appId: this.appId,
11132
+ link_uuid: this.link_uuid,
11133
+ requiredProofs: this.requiredProofs,
11134
+ scanType: this.scanType
11023
11135
  });
11024
- if (!ticketRes.ok) {
11025
- returnError("Failed to get ticket");
11026
- return;
11027
- }
11028
- const ticketData = await ticketRes.json();
11029
- const newTicket = ticketData.ticket;
11030
- if (!newTicket) {
11031
- returnError("No ticket returned from server");
11032
- return;
11033
- }
11034
11136
  const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
11035
11137
  const options = {
11036
11138
  maxRetries: 10,
@@ -11040,71 +11142,8 @@ var Polyguard = (() => {
11040
11142
  reconnectionDelayGrowFactor: 1.5
11041
11143
  };
11042
11144
  ws = new reconnecting_websocket_mjs_default(wsUrl, [], options);
11043
- ws.addEventListener("message", (event) => {
11044
- try {
11045
- const data = JSON.parse(event.data);
11046
- if (data && data.type === "ping" && typeof data.seq !== "undefined") {
11047
- ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
11048
- return;
11049
- }
11050
- if (data && data.url) {
11051
- window.location.assign(data.url);
11052
- return;
11053
- } else if (data && data.qr_url) {
11054
- const pcre = data.qr_url.match(/pcre=([^&]*)/);
11055
- console.log("pcre", pcre);
11056
- if (pcre) {
11057
- ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
11058
- }
11059
- if (!qrDiv) return;
11060
- const isMobile = /Mobi|Android/i.test(navigator.userAgent);
11061
- if (isMobile) {
11062
- qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
11063
- qrDiv.style.background = "transparent";
11064
- qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(data.qr_url);
11065
- if (!isTargetMode) {
11066
- const instructionText = qrDiv.nextElementSibling;
11067
- if (instructionText) {
11068
- instructionText.textContent = "Tap the button to verify with the Polyguard app.";
11069
- }
11070
- const instructionList = instructionText.nextElementSibling;
11071
- if (instructionList && instructionList.children[1]) {
11072
- instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
11073
- }
11074
- }
11075
- } else {
11076
- const startTime = Date.now();
11077
- console.log("time before qr code", startTime);
11078
- import_qrcode.default.toString(data.qr_url, { type: "svg" }, (err, svg) => {
11079
- if (!err) qrDiv.innerHTML = svg;
11080
- });
11081
- console.log("time to generate qr code", Date.now() - startTime);
11082
- }
11083
- return;
11084
- } else if (data && data.jwt) {
11085
- cleanup();
11086
- ws.close();
11087
- resolve(rawJwt ? data : data.jwt);
11088
- return;
11089
- } else if (data && data.status) {
11090
- if (!qrDiv) return;
11091
- qrDiv.innerHTML = LOADING_SPINNER;
11092
- return;
11093
- } else if (data && data.error) {
11094
- returnError(data.error);
11095
- ws.close();
11096
- return;
11097
- } else {
11098
- console.error("Unknown message type from server", data);
11099
- returnError(`Unknown message type from server`);
11100
- ws.close();
11101
- return;
11102
- }
11103
- } catch (e) {
11104
- console.error("Invalid message format from server", e);
11105
- returnError("Invalid message format from server");
11106
- }
11107
- });
11145
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
11146
+ ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
11108
11147
  ws.addEventListener("error", () => {
11109
11148
  console.error("WebSocket error");
11110
11149
  returnError("WebSocket error");
@@ -11113,7 +11152,7 @@ var Polyguard = (() => {
11113
11152
  if (!closed) cleanup();
11114
11153
  });
11115
11154
  } catch (err) {
11116
- returnError("Failed to connect to WebSocket");
11155
+ returnError(err.message === "Failed to get ticket" || err.message === "No ticket returned from server" ? err.message : "Failed to connect to WebSocket");
11117
11156
  }
11118
11157
  });
11119
11158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polyguard/sdk",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/sdk.esm.js",
6
6
  "module": "dist/sdk.esm.js",
@@ -1,24 +1,8 @@
1
1
  import ReconnectingWebSocket from 'reconnecting-websocket';
2
2
  import * as PolyguardApi from './generated/src';
3
- import QRCode from 'qrcode';
4
-
5
- // Animated spinner SVG
6
- const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
7
- <style>
8
- .spinner-circle {
9
- animation: spin 1s linear infinite;
10
- transform-origin: 100px 100px;
11
- }
12
- @keyframes spin {
13
- from { transform: rotate(0deg); }
14
- to { transform: rotate(360deg); }
15
- }
16
- </style>
17
- <circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
18
- <circle cx="100" cy="100" r="80" stroke="#7be7c2" stroke-width="8" fill="none"
19
- stroke-dasharray="251.2" stroke-dashoffset="188.4"
20
- class="spinner-circle" stroke-linecap="round" />
21
- </svg>`;
3
+ import { buildModal, showSpinner } from './ui.js';
4
+ import { fetchTicket } from './ticketService.js';
5
+ import { handleWebSocketMessage } from './messageHandler.js';
22
6
 
23
7
  // Implementation class for websocket integration
24
8
  export class PolyguardWebsocketClientImpl {
@@ -53,71 +37,35 @@ export class PolyguardWebsocketClientImpl {
53
37
  }
54
38
 
55
39
  buildModal() {
56
- const modal = document.createElement('div');
57
- modal.style.position = 'fixed';
58
- modal.style.top = '0';
59
- modal.style.left = '0';
60
- modal.style.width = '100vw';
61
- modal.style.height = '100vh';
62
- modal.style.background = 'rgba(0,0,0,0.45)';
63
- modal.style.display = 'flex';
64
- modal.style.alignItems = 'flex-start';
65
- modal.style.justifyContent = 'center';
66
- modal.style.overflowY = 'auto';
67
- modal.style.paddingTop = '24px';
68
- modal.innerHTML = `
69
- <div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
70
- <button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
71
- <h2 style="margin-top: 0; font-size: 1.5rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
72
- <div style="font-size: 15px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
73
- <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
74
- <div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
75
- <ul style="text-align: left; margin: 0 0 16px 0; padding-left: 20px; font-size: 14px; color: #444;">
76
- <li>We use the Polyguard service to verify your identity.</li>
77
- <li>If you do not have the Polyguard app, the QR code will redirect you to download it from the App Store or Google Play.</li>
78
- <li>Your credentials remain private on your device.</li>
79
- </ul>
80
- <div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
81
- <button id="polyguard-modal-cancel" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
82
- </div>
83
- `;
84
- return modal;
40
+ return buildModal();
85
41
  }
86
42
 
87
43
  async verify(target = null, rawJwt = false) {
88
- // Only websocket integration is supported for modal
89
44
  let modal = null;
90
45
  let qrDiv = null;
91
46
  let isTargetMode = false;
92
-
47
+
93
48
  if (target) {
94
- // Target mode: render QR code in specified element
95
49
  isTargetMode = true;
96
50
  qrDiv = document.getElementById(target);
97
51
  if (!qrDiv) {
98
52
  throw new Error(`Target element with ID '${target}' not found`);
99
53
  }
100
54
  } else {
101
- // Modal mode: create modal DOM
102
- modal = this.buildModal();
55
+ modal = buildModal();
103
56
  }
104
-
57
+
105
58
  if (!isTargetMode) {
106
- // Show modal immediately with spinner
107
59
  document.body.appendChild(modal);
108
-
109
- // Initialize QR div with spinner
110
60
  qrDiv = modal.querySelector('#polyguard-qr');
111
61
  }
112
-
62
+
113
63
  if (qrDiv) {
114
- qrDiv.innerHTML = LOADING_SPINNER;
64
+ showSpinner(qrDiv);
115
65
  }
116
-
117
- // Helper to cleanup modal or target
66
+
118
67
  function cleanup() {
119
68
  if (isTargetMode) {
120
- // Clear target element content
121
69
  if (qrDiv) {
122
70
  qrDiv.innerHTML = '';
123
71
  }
@@ -125,28 +73,27 @@ export class PolyguardWebsocketClientImpl {
125
73
  modal.parentNode.removeChild(modal);
126
74
  }
127
75
  }
128
- // Promise for JWT
76
+
129
77
  return new Promise(async (resolve, reject) => {
130
78
  let ws = null;
131
79
  let closed = false;
132
-
80
+
133
81
  function returnError(msg, score = "OFFLINE") {
134
- if (isTargetMode) {
135
- // In target mode, just log the error and resolve with error data
136
- console.error('Polyguard Error:', msg);
82
+ if (isTargetMode) {
83
+ console.error('Polyguard Error:', msg);
84
+ cleanup();
85
+ resolve({presence: { score: score, msg: msg }});
86
+ } else {
87
+ const errDiv = modal.querySelector('#polyguard-error');
88
+ if (errDiv) {
89
+ errDiv.textContent = msg;
90
+ errDiv.style.display = 'block';
91
+ }
92
+ setTimeout(() => {
137
93
  cleanup();
138
94
  resolve({presence: { score: score, msg: msg }});
139
- } else {
140
- const errDiv = modal.querySelector('#polyguard-error');
141
- if (errDiv) {
142
- errDiv.textContent = msg;
143
- errDiv.style.display = 'block';
144
- }
145
- setTimeout(() => {
146
- cleanup();
147
- resolve({presence: { score: score, msg: msg }});
148
- }, 1250);
149
- }
95
+ }, 1250);
96
+ }
150
97
  }
151
98
  function clearError() {
152
99
  if (!isTargetMode) {
@@ -154,42 +101,30 @@ export class PolyguardWebsocketClientImpl {
154
101
  if (errDiv) errDiv.style.display = 'none';
155
102
  }
156
103
  }
157
- // Close/cancel handler
158
104
  function handleClose() {
159
105
  closed = true;
160
106
  if (ws) ws.close();
161
107
  cleanup();
162
108
  reject(new Error('User cancelled'));
163
109
  }
164
-
165
- // Set up close/cancel handlers (only in modal mode)
110
+
166
111
  if (!isTargetMode) {
167
112
  modal.querySelector('#polyguard-modal-close').onclick = handleClose;
168
113
  modal.querySelector('#polyguard-modal-cancel').onclick = handleClose;
169
114
  }
170
-
171
- // Start ticket/ws flow
115
+
172
116
  try {
173
117
  clearError();
174
118
  const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
175
- const ticketUrl = this.link_uuid
176
- ? `https://${this.apiServer}/v2/ticket/${this.appId}/${this.link_uuid}`
177
- : `https://${this.apiServer}/v2/ticket/${this.appId}`;
178
- const ticketRes = await fetch(ticketUrl, {
179
- method: 'POST',
180
- headers: { 'Content-Type': 'application/json' },
181
- body: JSON.stringify({ requiredProofs: this.requiredProofs, scanType: this.scanType }),
119
+
120
+ const newTicket = await fetchTicket({
121
+ apiServer: this.apiServer,
122
+ appId: this.appId,
123
+ link_uuid: this.link_uuid,
124
+ requiredProofs: this.requiredProofs,
125
+ scanType: this.scanType,
182
126
  });
183
- if (!ticketRes.ok) {
184
- returnError('Failed to get ticket');
185
- return;
186
- }
187
- const ticketData = await ticketRes.json();
188
- const newTicket = ticketData.ticket;
189
- if (!newTicket) {
190
- returnError('No ticket returned from server');
191
- return;
192
- }
127
+
193
128
  const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
194
129
  const options = {
195
130
  maxRetries: 10,
@@ -199,86 +134,10 @@ export class PolyguardWebsocketClientImpl {
199
134
  reconnectionDelayGrowFactor: 1.5,
200
135
  };
201
136
  ws = new ReconnectingWebSocket(wsUrl, [], options);
202
- ws.addEventListener('message', (event) => {
203
- try {
204
- const data = JSON.parse(event.data);
205
- // --- BEGIN: Backend-initiated latency measurement ---
206
- if (data && data.type === 'ping' && typeof data.seq !== 'undefined') {
207
- ws.send(JSON.stringify({ type: 'pong', seq: data.seq }));
208
- return;
209
- }
210
- // --- END: Backend-initiated latency measurement ---
211
- if (data && data.url) {
212
- window.location.assign(data.url);
213
- return;
214
- } else if (data && data.qr_url) {
215
- // Replace spinner with QR code content
216
-
217
- const pcre = data.qr_url.match(/pcre=([^&]*)/);
218
- console.log('pcre', pcre);
219
- if (pcre) {
220
- ws.send(JSON.stringify({ type: 'pong', seq: pcre[1] }));
221
- }
222
-
223
- if (!qrDiv) return;
224
-
225
- const isMobile = /Mobi|Android/i.test(navigator.userAgent);
226
137
 
227
- if (isMobile) {
228
- // For mobile, display a button to open the app
229
- qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #7be7c2; color: #222; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
230
- qrDiv.style.background = 'transparent';
231
- qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(data.qr_url);
138
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
139
+ ws.addEventListener('message', (event) => handleWebSocketMessage(event, ctx));
232
140
 
233
- // Update surrounding text only in modal mode
234
- if (!isTargetMode) {
235
- const instructionText = qrDiv.nextElementSibling;
236
- if (instructionText) {
237
- instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
238
- }
239
- const instructionList = instructionText.nextElementSibling;
240
- if (instructionList && instructionList.children[1]) {
241
- instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
242
- }
243
- }
244
- } else {
245
- const startTime = Date.now();
246
- console.log('time before qr code', startTime);
247
- // For desktop, display the QR code
248
- QRCode.toString(data.qr_url, { type: 'svg' }, (err, svg) => {
249
- if (!err) qrDiv.innerHTML = svg;
250
- });
251
- console.log('time to generate qr code', Date.now() - startTime);
252
- }
253
- return;
254
- } else if (data && data.jwt) {
255
- cleanup();
256
- ws.close();
257
- resolve(rawJwt ? data : data.jwt);
258
- return;
259
- } else if (data && data.status) {
260
- // ignore
261
- // generate a spinner in svg and set the qr code to it
262
- if (!qrDiv) return;
263
- qrDiv.innerHTML = LOADING_SPINNER;
264
- return;
265
-
266
- } else if (data && data.error) {
267
- returnError(data.error);
268
- ws.close();
269
- return;
270
- } else {
271
- console.error('Unknown message type from server', data);
272
- returnError(`Unknown message type from server`);
273
- ws.close();
274
- return;
275
- }
276
- } catch (e) {
277
- console.error('Invalid message format from server', e);
278
- returnError('Invalid message format from server');
279
- // ws.close();
280
- }
281
- });
282
141
  ws.addEventListener('error', () => {
283
142
  console.error('WebSocket error');
284
143
  returnError('WebSocket error');
@@ -287,7 +146,9 @@ export class PolyguardWebsocketClientImpl {
287
146
  if (!closed) cleanup();
288
147
  });
289
148
  } catch (err) {
290
- returnError('Failed to connect to WebSocket');
149
+ returnError(err.message === 'Failed to get ticket' || err.message === 'No ticket returned from server'
150
+ ? err.message
151
+ : 'Failed to connect to WebSocket');
291
152
  }
292
153
  });
293
154
  }
@@ -0,0 +1,77 @@
1
+ import { showSpinner, renderMobileButton, renderQRCode } from './ui.js';
2
+
3
+ /**
4
+ * Handle a WebSocket message event.
5
+ * @param {MessageEvent} event
6
+ * @param {Object} ctx - Context object with references from the verify() closure:
7
+ * { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt }
8
+ */
9
+ export function handleWebSocketMessage(event, ctx) {
10
+ const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
11
+
12
+ let data;
13
+ try {
14
+ data = JSON.parse(event.data);
15
+ } catch (e) {
16
+ console.error('Invalid message format from server', e);
17
+ returnError('Invalid message format from server');
18
+ return;
19
+ }
20
+
21
+ if (!data) {
22
+ console.error('Unknown message type from server', data);
23
+ returnError('Unknown message type from server');
24
+ ws.close();
25
+ return;
26
+ }
27
+
28
+ // Backend-initiated latency measurement
29
+ if (data.type === 'ping' && typeof data.seq !== 'undefined') {
30
+ ws.send(JSON.stringify({ type: 'pong', seq: data.seq }));
31
+ return;
32
+ }
33
+
34
+ if (data.url) {
35
+ window.location.assign(data.url);
36
+ return;
37
+ }
38
+
39
+ if (data.qr_url) {
40
+ const pcre = data.qr_url.match(/pcre=([^&]*)/);
41
+ console.log('pcre', pcre);
42
+ if (pcre) {
43
+ ws.send(JSON.stringify({ type: 'pong', seq: pcre[1] }));
44
+ }
45
+ if (qrDiv) {
46
+ const isMobile = /Mobi|Android/i.test(navigator.userAgent);
47
+ if (isMobile) {
48
+ renderMobileButton(qrDiv, data.qr_url, isTargetMode);
49
+ } else {
50
+ renderQRCode(qrDiv, data.qr_url);
51
+ }
52
+ }
53
+ return;
54
+ }
55
+
56
+ if (data.jwt) {
57
+ cleanup();
58
+ ws.close();
59
+ resolve(rawJwt ? data : data.jwt);
60
+ return;
61
+ }
62
+
63
+ if (data.status) {
64
+ if (qrDiv) showSpinner(qrDiv);
65
+ return;
66
+ }
67
+
68
+ if (data.error) {
69
+ returnError(data.error);
70
+ ws.close();
71
+ return;
72
+ }
73
+
74
+ console.error('Unknown message type from server', data);
75
+ returnError('Unknown message type from server');
76
+ ws.close();
77
+ }
@@ -0,0 +1,24 @@
1
+ export async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
2
+ const ticketUrl = link_uuid
3
+ ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}`
4
+ : `https://${apiServer}/v2/ticket/${appId}`;
5
+
6
+ const ticketRes = await fetch(ticketUrl, {
7
+ method: 'POST',
8
+ headers: { 'Content-Type': 'application/json' },
9
+ body: JSON.stringify({ requiredProofs, scanType }),
10
+ });
11
+
12
+ if (!ticketRes.ok) {
13
+ throw new Error('Failed to get ticket');
14
+ }
15
+
16
+ const ticketData = await ticketRes.json();
17
+ const ticket = ticketData.ticket;
18
+
19
+ if (!ticket) {
20
+ throw new Error('No ticket returned from server');
21
+ }
22
+
23
+ return ticket;
24
+ }
package/src/ui.js ADDED
@@ -0,0 +1,82 @@
1
+ import QRCode from 'qrcode';
2
+
3
+ // Animated spinner SVG
4
+ export const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
5
+ <style>
6
+ .spinner-circle {
7
+ animation: spin 1s linear infinite;
8
+ transform-origin: 100px 100px;
9
+ }
10
+ @keyframes spin {
11
+ from { transform: rotate(0deg); }
12
+ to { transform: rotate(360deg); }
13
+ }
14
+ </style>
15
+ <circle cx="100" cy="100" r="80" stroke="#e0e0e0" stroke-width="8" fill="none" />
16
+ <circle cx="100" cy="100" r="80" stroke="#407796" stroke-width="8" fill="none"
17
+ stroke-dasharray="251.2" stroke-dashoffset="188.4"
18
+ class="spinner-circle" stroke-linecap="round" />
19
+ </svg>`;
20
+
21
+ export function buildModal() {
22
+ const modal = document.createElement('div');
23
+ modal.style.position = 'fixed';
24
+ modal.style.top = '0';
25
+ modal.style.left = '0';
26
+ modal.style.width = '100vw';
27
+ modal.style.height = '100vh';
28
+ modal.style.background = 'rgba(0,0,0,0.45)';
29
+ modal.style.display = 'flex';
30
+ modal.style.alignItems = 'flex-start';
31
+ modal.style.justifyContent = 'center';
32
+ modal.style.overflowY = 'auto';
33
+ modal.style.paddingTop = '24px';
34
+ modal.innerHTML = `
35
+ <div id="polyguard-modal-content" style="background: #fff; color: #222; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); padding: 32px 24px 24px 24px; max-width: 340px; width: 100%; text-align: center; position: relative; font-family: 'IBM Plex Sans', 'Inter', 'Helvetica', 'Arial', sans-serif; margin: 0 auto; box-sizing: border-box;">
36
+ <button id="polyguard-modal-close" style="position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 22px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
37
+ <h2 style="margin-top: 0; font-size: 1.3rem; font-weight: 700; color: #222;">Identity Verification</h2>
38
+ <div style="font-size: 13px; color: #888; margin-bottom: 12px; font-weight: 500;">Powered by Polyguard</div>
39
+ <div id="polyguard-qr" style="width: 200px; height: 200px; margin: 0 auto 16px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 12px;"></div>
40
+ <div style="margin-bottom: 12px; font-weight: 600; color: #222;">Scan this QR code to verify your identity.</div>
41
+ <div style="display: flex; justify-content: center; gap: 12px; font-size: 12px; color: #5a6c7d; margin-bottom: 12px; flex-wrap: wrap;">
42
+ <span>&#128274; Encrypted</span>
43
+ <span>&#128241; On-device</span>
44
+ <span>&#9202; Expires</span>
45
+ </div>
46
+ <div id="polyguard-error" style="color: #b31d28; font-size: 14px; margin-bottom: 8px; display: none;"></div>
47
+ <button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer; margin-top: 8px; width: 100%; max-width: 240px;">Cancel</button>
48
+ </div>
49
+ `;
50
+ return modal;
51
+ }
52
+
53
+ export function showSpinner(qrDiv) {
54
+ qrDiv.innerHTML = LOADING_SPINNER;
55
+ }
56
+
57
+ export function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
58
+ qrDiv.innerHTML = `<button id="polyguard-open-app-button" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 10px 32px; font-size: 16px; cursor: pointer;">Open Polyguard App</button>`;
59
+ qrDiv.style.background = 'transparent';
60
+ qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(qrUrl);
61
+
62
+ // Update surrounding text only in modal mode
63
+ if (!isTargetMode) {
64
+ const instructionText = qrDiv.nextElementSibling;
65
+ if (instructionText) {
66
+ instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
67
+ }
68
+ const instructionList = instructionText.nextElementSibling;
69
+ if (instructionList && instructionList.children[1]) {
70
+ instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
71
+ }
72
+ }
73
+ }
74
+
75
+ export function renderQRCode(qrDiv, qrUrl) {
76
+ const startTime = Date.now();
77
+ console.log('time before qr code', startTime);
78
+ QRCode.toString(qrUrl, { type: 'svg' }, (err, svg) => {
79
+ if (!err) qrDiv.innerHTML = svg;
80
+ });
81
+ console.log('time to generate qr code', Date.now() - startTime);
82
+ }