@polyguard/sdk 1.2.4 → 1.3.1

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,9 +10837,9 @@ 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
- var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
10842
+ var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 200 200" fill="none">
10843
10843
  <style>
10844
10844
  .spinner-circle {
10845
10845
  animation: spin 1s linear infinite;
@@ -10851,10 +10851,154 @@ 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: 26px 19px 19px 19px; max-width: 272px; 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: 10px; right: 10px; background: none; border: none; font-size: 18px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10874
+ <h2 style="margin-top: 0; font-size: 1.04rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
10875
+ <div style="font-size: 10px; color: #888; margin-bottom: 10px; font-weight: 500;">Powered by Polyguard</div>
10876
+ <div id="polyguard-qr" style="width: 160px; height: 160px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
10877
+ <div style="margin-bottom: 10px; font-weight: 600; font-size: 13px; color: #222;">Scan this QR code to verify your identity.</div>
10878
+ <ul style="text-align: left; margin: 0 0 13px 0; padding-left: 16px; font-size: 11px; color: #444;">
10879
+ <li>We use the Polyguard service to verify your identity.</li>
10880
+ <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>
10881
+ <li>Your credentials remain private on your device.</li>
10882
+ </ul>
10883
+ <div style="display: flex; justify-content: center; gap: 10px; font-size: 10px; color: #5a6c7d; margin-bottom: 10px; flex-wrap: wrap;">
10884
+ <span>&#128274; Encrypted</span>
10885
+ <span>&#128241; On-device</span>
10886
+ <span>&#9202; Expires</span>
10887
+ </div>
10888
+ <div id="polyguard-error" style="color: #b31d28; font-size: 11px; margin-bottom: 6px; display: none;"></div>
10889
+ <button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 8px 26px; font-size: 13px; cursor: pointer; margin-top: 6px; width: 100%; max-width: 192px;">Cancel</button>
10890
+ </div>
10891
+ `;
10892
+ return modal;
10893
+ }
10894
+ function showSpinner(qrDiv) {
10895
+ qrDiv.innerHTML = LOADING_SPINNER;
10896
+ }
10897
+ function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
10898
+ 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>`;
10899
+ qrDiv.style.background = "transparent";
10900
+ qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(qrUrl);
10901
+ if (!isTargetMode) {
10902
+ const instructionText = qrDiv.nextElementSibling;
10903
+ if (instructionText) {
10904
+ instructionText.textContent = "Tap the button to verify with the Polyguard app.";
10905
+ }
10906
+ const instructionList = instructionText.nextElementSibling;
10907
+ if (instructionList && instructionList.children[1]) {
10908
+ instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
10909
+ }
10910
+ }
10911
+ }
10912
+ function renderQRCode(qrDiv, qrUrl) {
10913
+ const startTime = Date.now();
10914
+ console.log("time before qr code", startTime);
10915
+ import_qrcode.default.toString(qrUrl, { type: "svg" }, (err, svg) => {
10916
+ if (!err) qrDiv.innerHTML = svg;
10917
+ });
10918
+ console.log("time to generate qr code", Date.now() - startTime);
10919
+ }
10920
+
10921
+ // src/ticketService.js
10922
+ async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
10923
+ const ticketUrl = link_uuid ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}` : `https://${apiServer}/v2/ticket/${appId}`;
10924
+ const ticketRes = await fetch(ticketUrl, {
10925
+ method: "POST",
10926
+ headers: { "Content-Type": "application/json" },
10927
+ body: JSON.stringify({ requiredProofs, scanType })
10928
+ });
10929
+ if (!ticketRes.ok) {
10930
+ throw new Error("Failed to get ticket");
10931
+ }
10932
+ const ticketData = await ticketRes.json();
10933
+ const ticket = ticketData.ticket;
10934
+ if (!ticket) {
10935
+ throw new Error("No ticket returned from server");
10936
+ }
10937
+ return ticket;
10938
+ }
10939
+
10940
+ // src/messageHandler.js
10941
+ function handleWebSocketMessage(event, ctx) {
10942
+ const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
10943
+ let data;
10944
+ try {
10945
+ data = JSON.parse(event.data);
10946
+ } catch (e) {
10947
+ console.error("Invalid message format from server", e);
10948
+ returnError("Invalid message format from server");
10949
+ return;
10950
+ }
10951
+ if (!data) {
10952
+ console.error("Unknown message type from server", data);
10953
+ returnError("Unknown message type from server");
10954
+ ws.close();
10955
+ return;
10956
+ }
10957
+ if (data.type === "ping" && typeof data.seq !== "undefined") {
10958
+ ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
10959
+ return;
10960
+ }
10961
+ if (data.url) {
10962
+ window.location.assign(data.url);
10963
+ return;
10964
+ }
10965
+ if (data.qr_url) {
10966
+ const pcre = data.qr_url.match(/pcre=([^&]*)/);
10967
+ console.log("pcre", pcre);
10968
+ if (pcre) {
10969
+ ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
10970
+ }
10971
+ if (qrDiv) {
10972
+ const isMobile = /Mobi|Android/i.test(navigator.userAgent);
10973
+ if (isMobile) {
10974
+ renderMobileButton(qrDiv, data.qr_url, isTargetMode);
10975
+ } else {
10976
+ renderQRCode(qrDiv, data.qr_url);
10977
+ }
10978
+ }
10979
+ return;
10980
+ }
10981
+ if (data.jwt) {
10982
+ cleanup();
10983
+ ws.close();
10984
+ resolve(rawJwt ? data : data.jwt);
10985
+ return;
10986
+ }
10987
+ if (data.status) {
10988
+ if (qrDiv) showSpinner(qrDiv);
10989
+ return;
10990
+ }
10991
+ if (data.error) {
10992
+ returnError(data.error);
10993
+ ws.close();
10994
+ return;
10995
+ }
10996
+ console.error("Unknown message type from server", data);
10997
+ returnError("Unknown message type from server");
10998
+ ws.close();
10999
+ }
11000
+
11001
+ // src/PolyguardWebsocketClientImpl.js
10858
11002
  var PolyguardWebsocketClientImpl = class {
10859
11003
  constructor(params = {}) {
10860
11004
  this.apiClient = new ApiClient_default();
@@ -10884,35 +11028,7 @@ var PolyguardWebsocketClientImpl = class {
10884
11028
  this.link_uuid = link_uuid;
10885
11029
  }
10886
11030
  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;
11031
+ return buildModal();
10916
11032
  }
10917
11033
  async verify(target = null, rawJwt = false) {
10918
11034
  let modal = null;
@@ -10925,14 +11041,14 @@ var PolyguardWebsocketClientImpl = class {
10925
11041
  throw new Error(`Target element with ID '${target}' not found`);
10926
11042
  }
10927
11043
  } else {
10928
- modal = this.buildModal();
11044
+ modal = buildModal();
10929
11045
  }
10930
11046
  if (!isTargetMode) {
10931
11047
  document.body.appendChild(modal);
10932
11048
  qrDiv = modal.querySelector("#polyguard-qr");
10933
11049
  }
10934
11050
  if (qrDiv) {
10935
- qrDiv.innerHTML = LOADING_SPINNER;
11051
+ showSpinner(qrDiv);
10936
11052
  }
10937
11053
  function cleanup() {
10938
11054
  if (isTargetMode) {
@@ -10982,22 +11098,13 @@ var PolyguardWebsocketClientImpl = class {
10982
11098
  try {
10983
11099
  clearError();
10984
11100
  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 })
11101
+ const newTicket = await fetchTicket({
11102
+ apiServer: this.apiServer,
11103
+ appId: this.appId,
11104
+ link_uuid: this.link_uuid,
11105
+ requiredProofs: this.requiredProofs,
11106
+ scanType: this.scanType
10990
11107
  });
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
11108
  const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
11002
11109
  const options = {
11003
11110
  maxRetries: 10,
@@ -11007,71 +11114,8 @@ var PolyguardWebsocketClientImpl = class {
11007
11114
  reconnectionDelayGrowFactor: 1.5
11008
11115
  };
11009
11116
  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
- });
11117
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
11118
+ ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
11075
11119
  ws.addEventListener("error", () => {
11076
11120
  console.error("WebSocket error");
11077
11121
  returnError("WebSocket error");
@@ -11080,7 +11124,7 @@ var PolyguardWebsocketClientImpl = class {
11080
11124
  if (!closed) cleanup();
11081
11125
  });
11082
11126
  } catch (err) {
11083
- returnError("Failed to connect to WebSocket");
11127
+ returnError(err.message === "Failed to get ticket" || err.message === "No ticket returned from server" ? err.message : "Failed to connect to WebSocket");
11084
11128
  }
11085
11129
  });
11086
11130
  }
package/dist/sdk.js CHANGED
@@ -10870,9 +10870,9 @@ 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
- var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
10875
+ var LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 200 200" fill="none">
10876
10876
  <style>
10877
10877
  .spinner-circle {
10878
10878
  animation: spin 1s linear infinite;
@@ -10884,10 +10884,154 @@ 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: 26px 19px 19px 19px; max-width: 272px; 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: 10px; right: 10px; background: none; border: none; font-size: 18px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
10907
+ <h2 style="margin-top: 0; font-size: 1.04rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
10908
+ <div style="font-size: 10px; color: #888; margin-bottom: 10px; font-weight: 500;">Powered by Polyguard</div>
10909
+ <div id="polyguard-qr" style="width: 160px; height: 160px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
10910
+ <div style="margin-bottom: 10px; font-weight: 600; font-size: 13px; color: #222;">Scan this QR code to verify your identity.</div>
10911
+ <ul style="text-align: left; margin: 0 0 13px 0; padding-left: 16px; font-size: 11px; color: #444;">
10912
+ <li>We use the Polyguard service to verify your identity.</li>
10913
+ <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>
10914
+ <li>Your credentials remain private on your device.</li>
10915
+ </ul>
10916
+ <div style="display: flex; justify-content: center; gap: 10px; font-size: 10px; color: #5a6c7d; margin-bottom: 10px; flex-wrap: wrap;">
10917
+ <span>&#128274; Encrypted</span>
10918
+ <span>&#128241; On-device</span>
10919
+ <span>&#9202; Expires</span>
10920
+ </div>
10921
+ <div id="polyguard-error" style="color: #b31d28; font-size: 11px; margin-bottom: 6px; display: none;"></div>
10922
+ <button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 8px 26px; font-size: 13px; cursor: pointer; margin-top: 6px; width: 100%; max-width: 192px;">Cancel</button>
10923
+ </div>
10924
+ `;
10925
+ return modal;
10926
+ }
10927
+ function showSpinner(qrDiv) {
10928
+ qrDiv.innerHTML = LOADING_SPINNER;
10929
+ }
10930
+ function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
10931
+ 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>`;
10932
+ qrDiv.style.background = "transparent";
10933
+ qrDiv.querySelector("#polyguard-open-app-button").onclick = () => window.location.assign(qrUrl);
10934
+ if (!isTargetMode) {
10935
+ const instructionText = qrDiv.nextElementSibling;
10936
+ if (instructionText) {
10937
+ instructionText.textContent = "Tap the button to verify with the Polyguard app.";
10938
+ }
10939
+ const instructionList = instructionText.nextElementSibling;
10940
+ if (instructionList && instructionList.children[1]) {
10941
+ instructionList.children[1].textContent = "If you do not have the Polyguard app, you will be redirected to download it.";
10942
+ }
10943
+ }
10944
+ }
10945
+ function renderQRCode(qrDiv, qrUrl) {
10946
+ const startTime = Date.now();
10947
+ console.log("time before qr code", startTime);
10948
+ import_qrcode.default.toString(qrUrl, { type: "svg" }, (err, svg) => {
10949
+ if (!err) qrDiv.innerHTML = svg;
10950
+ });
10951
+ console.log("time to generate qr code", Date.now() - startTime);
10952
+ }
10953
+
10954
+ // src/ticketService.js
10955
+ async function fetchTicket({ apiServer, appId, link_uuid, requiredProofs, scanType }) {
10956
+ const ticketUrl = link_uuid ? `https://${apiServer}/v2/ticket/${appId}/${link_uuid}` : `https://${apiServer}/v2/ticket/${appId}`;
10957
+ const ticketRes = await fetch(ticketUrl, {
10958
+ method: "POST",
10959
+ headers: { "Content-Type": "application/json" },
10960
+ body: JSON.stringify({ requiredProofs, scanType })
10961
+ });
10962
+ if (!ticketRes.ok) {
10963
+ throw new Error("Failed to get ticket");
10964
+ }
10965
+ const ticketData = await ticketRes.json();
10966
+ const ticket = ticketData.ticket;
10967
+ if (!ticket) {
10968
+ throw new Error("No ticket returned from server");
10969
+ }
10970
+ return ticket;
10971
+ }
10972
+
10973
+ // src/messageHandler.js
10974
+ function handleWebSocketMessage(event, ctx) {
10975
+ const { ws, qrDiv, isTargetMode, cleanup, returnError, resolve, rawJwt } = ctx;
10976
+ let data;
10977
+ try {
10978
+ data = JSON.parse(event.data);
10979
+ } catch (e) {
10980
+ console.error("Invalid message format from server", e);
10981
+ returnError("Invalid message format from server");
10982
+ return;
10983
+ }
10984
+ if (!data) {
10985
+ console.error("Unknown message type from server", data);
10986
+ returnError("Unknown message type from server");
10987
+ ws.close();
10988
+ return;
10989
+ }
10990
+ if (data.type === "ping" && typeof data.seq !== "undefined") {
10991
+ ws.send(JSON.stringify({ type: "pong", seq: data.seq }));
10992
+ return;
10993
+ }
10994
+ if (data.url) {
10995
+ window.location.assign(data.url);
10996
+ return;
10997
+ }
10998
+ if (data.qr_url) {
10999
+ const pcre = data.qr_url.match(/pcre=([^&]*)/);
11000
+ console.log("pcre", pcre);
11001
+ if (pcre) {
11002
+ ws.send(JSON.stringify({ type: "pong", seq: pcre[1] }));
11003
+ }
11004
+ if (qrDiv) {
11005
+ const isMobile = /Mobi|Android/i.test(navigator.userAgent);
11006
+ if (isMobile) {
11007
+ renderMobileButton(qrDiv, data.qr_url, isTargetMode);
11008
+ } else {
11009
+ renderQRCode(qrDiv, data.qr_url);
11010
+ }
11011
+ }
11012
+ return;
11013
+ }
11014
+ if (data.jwt) {
11015
+ cleanup();
11016
+ ws.close();
11017
+ resolve(rawJwt ? data : data.jwt);
11018
+ return;
11019
+ }
11020
+ if (data.status) {
11021
+ if (qrDiv) showSpinner(qrDiv);
11022
+ return;
11023
+ }
11024
+ if (data.error) {
11025
+ returnError(data.error);
11026
+ ws.close();
11027
+ return;
11028
+ }
11029
+ console.error("Unknown message type from server", data);
11030
+ returnError("Unknown message type from server");
11031
+ ws.close();
11032
+ }
11033
+
11034
+ // src/PolyguardWebsocketClientImpl.js
10891
11035
  var PolyguardWebsocketClientImpl = class {
10892
11036
  constructor(params = {}) {
10893
11037
  this.apiClient = new ApiClient_default();
@@ -10917,35 +11061,7 @@ var Polyguard = (() => {
10917
11061
  this.link_uuid = link_uuid;
10918
11062
  }
10919
11063
  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;
11064
+ return buildModal();
10949
11065
  }
10950
11066
  async verify(target = null, rawJwt = false) {
10951
11067
  let modal = null;
@@ -10958,14 +11074,14 @@ var Polyguard = (() => {
10958
11074
  throw new Error(`Target element with ID '${target}' not found`);
10959
11075
  }
10960
11076
  } else {
10961
- modal = this.buildModal();
11077
+ modal = buildModal();
10962
11078
  }
10963
11079
  if (!isTargetMode) {
10964
11080
  document.body.appendChild(modal);
10965
11081
  qrDiv = modal.querySelector("#polyguard-qr");
10966
11082
  }
10967
11083
  if (qrDiv) {
10968
- qrDiv.innerHTML = LOADING_SPINNER;
11084
+ showSpinner(qrDiv);
10969
11085
  }
10970
11086
  function cleanup() {
10971
11087
  if (isTargetMode) {
@@ -11015,22 +11131,13 @@ var Polyguard = (() => {
11015
11131
  try {
11016
11132
  clearError();
11017
11133
  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 })
11134
+ const newTicket = await fetchTicket({
11135
+ apiServer: this.apiServer,
11136
+ appId: this.appId,
11137
+ link_uuid: this.link_uuid,
11138
+ requiredProofs: this.requiredProofs,
11139
+ scanType: this.scanType
11023
11140
  });
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
11141
  const wsUrl = `${wsProtocol}://${this.apiServer}/v2/realtime/${newTicket}`;
11035
11142
  const options = {
11036
11143
  maxRetries: 10,
@@ -11040,71 +11147,8 @@ var Polyguard = (() => {
11040
11147
  reconnectionDelayGrowFactor: 1.5
11041
11148
  };
11042
11149
  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
- });
11150
+ const ctx = { ws, qrDiv, isTargetMode, modal, cleanup, returnError, clearError, resolve, rawJwt };
11151
+ ws.addEventListener("message", (event) => handleWebSocketMessage(event, ctx));
11108
11152
  ws.addEventListener("error", () => {
11109
11153
  console.error("WebSocket error");
11110
11154
  returnError("WebSocket error");
@@ -11113,7 +11157,7 @@ var Polyguard = (() => {
11113
11157
  if (!closed) cleanup();
11114
11158
  });
11115
11159
  } catch (err) {
11116
- returnError("Failed to connect to WebSocket");
11160
+ returnError(err.message === "Failed to get ticket" || err.message === "No ticket returned from server" ? err.message : "Failed to connect to WebSocket");
11117
11161
  }
11118
11162
  });
11119
11163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polyguard/sdk",
3
- "version": "1.2.4",
3
+ "version": "1.3.1",
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,87 @@
1
+ import QRCode from 'qrcode';
2
+
3
+ // Animated spinner SVG
4
+ export const LOADING_SPINNER = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" 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: 26px 19px 19px 19px; max-width: 272px; 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: 10px; right: 10px; background: none; border: none; font-size: 18px; color: #222; cursor: pointer; z-index: 2;">&times;</button>
37
+ <h2 style="margin-top: 0; font-size: 1.04rem; font-weight: 700; color: #222;">Quick Identity Verification</h2>
38
+ <div style="font-size: 10px; color: #888; margin-bottom: 10px; font-weight: 500;">Powered by Polyguard</div>
39
+ <div id="polyguard-qr" style="width: 160px; height: 160px; margin: 0 auto 13px auto; display: flex; align-items: center; justify-content: center; background: #f4f8fb; border-radius: 10px;"></div>
40
+ <div style="margin-bottom: 10px; font-weight: 600; font-size: 13px; color: #222;">Scan this QR code to verify your identity.</div>
41
+ <ul style="text-align: left; margin: 0 0 13px 0; padding-left: 16px; font-size: 11px; color: #444;">
42
+ <li>We use the Polyguard service to verify your identity.</li>
43
+ <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>
44
+ <li>Your credentials remain private on your device.</li>
45
+ </ul>
46
+ <div style="display: flex; justify-content: center; gap: 10px; font-size: 10px; color: #5a6c7d; margin-bottom: 10px; flex-wrap: wrap;">
47
+ <span>&#128274; Encrypted</span>
48
+ <span>&#128241; On-device</span>
49
+ <span>&#9202; Expires</span>
50
+ </div>
51
+ <div id="polyguard-error" style="color: #b31d28; font-size: 11px; margin-bottom: 6px; display: none;"></div>
52
+ <button id="polyguard-modal-cancel" style="background: #407796; color: #fff; font-weight: 600; border-radius: 8px; border: none; padding: 8px 26px; font-size: 13px; cursor: pointer; margin-top: 6px; width: 100%; max-width: 192px;">Cancel</button>
53
+ </div>
54
+ `;
55
+ return modal;
56
+ }
57
+
58
+ export function showSpinner(qrDiv) {
59
+ qrDiv.innerHTML = LOADING_SPINNER;
60
+ }
61
+
62
+ export function renderMobileButton(qrDiv, qrUrl, isTargetMode) {
63
+ 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>`;
64
+ qrDiv.style.background = 'transparent';
65
+ qrDiv.querySelector('#polyguard-open-app-button').onclick = () => window.location.assign(qrUrl);
66
+
67
+ // Update surrounding text only in modal mode
68
+ if (!isTargetMode) {
69
+ const instructionText = qrDiv.nextElementSibling;
70
+ if (instructionText) {
71
+ instructionText.textContent = 'Tap the button to verify with the Polyguard app.';
72
+ }
73
+ const instructionList = instructionText.nextElementSibling;
74
+ if (instructionList && instructionList.children[1]) {
75
+ instructionList.children[1].textContent = 'If you do not have the Polyguard app, you will be redirected to download it.';
76
+ }
77
+ }
78
+ }
79
+
80
+ export function renderQRCode(qrDiv, qrUrl) {
81
+ const startTime = Date.now();
82
+ console.log('time before qr code', startTime);
83
+ QRCode.toString(qrUrl, { type: 'svg' }, (err, svg) => {
84
+ if (!err) qrDiv.innerHTML = svg;
85
+ });
86
+ console.log('time to generate qr code', Date.now() - startTime);
87
+ }