@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 +160 -116
- package/dist/sdk.js +160 -116
- package/package.json +1 -1
- package/src/PolyguardWebsocketClientImpl.js +40 -179
- package/src/messageHandler.js +77 -0
- package/src/ticketService.js +24 -0
- package/src/ui.js +87 -0
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/
|
|
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="
|
|
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="#
|
|
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;">×</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>🔒 Encrypted</span>
|
|
10885
|
+
<span>📱 On-device</span>
|
|
10886
|
+
<span>⏲ 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
|
-
|
|
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;">×</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 =
|
|
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
|
|
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
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
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
|
|
11011
|
-
|
|
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/
|
|
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="
|
|
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="#
|
|
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;">×</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>🔒 Encrypted</span>
|
|
10918
|
+
<span>📱 On-device</span>
|
|
10919
|
+
<span>⏲ 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
|
-
|
|
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;">×</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 =
|
|
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
|
|
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
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
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
|
|
11044
|
-
|
|
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,24 +1,8 @@
|
|
|
1
1
|
import ReconnectingWebSocket from 'reconnecting-websocket';
|
|
2
2
|
import * as PolyguardApi from './generated/src';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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;">×</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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}
|
|
140
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
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;">×</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>🔒 Encrypted</span>
|
|
48
|
+
<span>📱 On-device</span>
|
|
49
|
+
<span>⏲ 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
|
+
}
|