diff --git a/internal/ui/static/js/.prettierrc.json b/internal/ui/static/js/.prettierrc.json new file mode 100644 index 00000000..4c8c3f9f --- /dev/null +++ b/internal/ui/static/js/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "printWidth": 120 +} \ No newline at end of file diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 690f3e23..2a3b636f 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -1,4 +1,3 @@ - /** * Open a new tab with the given URL. * @@ -34,7 +33,11 @@ function scrollPageTo(element, evenIfOnScreen) { const viewportPosition = windowScrollPosition + windowHeight; const itemBottomPosition = element.offsetTop + element.offsetHeight; - if (evenIfOnScreen || viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) { + if ( + evenIfOnScreen || + viewportPosition - itemBottomPosition < 0 || + viewportPosition - element.offsetTop > windowHeight + ) { window.scrollTo(0, element.offsetTop - 10); } } @@ -76,7 +79,7 @@ function checkMenuToggleModeByLayout() { logoElement.setAttribute("role", "button"); logoElement.setAttribute("tabindex", "0"); logoElement.setAttribute("aria-label", logoToggleButtonLabel); - logoElement.setAttribute("aria-expanded", navMenuElementIsExpanded?"true":"false"); + logoElement.setAttribute("aria-expanded", navMenuElementIsExpanded ? "true" : "false"); homePageLinkElement.setAttribute("tabindex", "-1"); } else { logoElement.removeAttribute("role"); @@ -91,10 +94,10 @@ function fixVoiceOverDetailsSummaryBug() { document.querySelectorAll("details").forEach((details) => { const summaryElement = details.querySelector("summary"); summaryElement.setAttribute("role", "button"); - summaryElement.setAttribute("aria-expanded", details.open? "true": "false"); + summaryElement.setAttribute("aria-expanded", details.open ? "true" : "false"); details.addEventListener("toggle", () => { - summaryElement.setAttribute("aria-expanded", details.open? "true": "false"); + summaryElement.setAttribute("aria-expanded", details.open ? "true" : "false"); }); }); } @@ -182,6 +185,7 @@ function markPageAsRead() { /** * Handle entry status changes from the list view and entry view. * Focus the next or the previous entry if it exists. + * * @param {string} item Item to focus: "previous" or "next". * @param {Element} element * @param {boolean} setToRead @@ -193,14 +197,14 @@ function handleEntryStatus(item, element, setToRead) { if (!setToRead || currentEntry.querySelector(":is(a, button)[data-toggle-status]").dataset.value === "unread") { toggleEntryStatus(currentEntry, toasting); } - if (isListView() && currentEntry.classList.contains('current-item')) { + if (isListView() && currentEntry.classList.contains("current-item")) { switch (item) { - case "previous": - goToListItem(-1); - break; - case "next": - goToListItem(1); - break; + case "previous": + goToListItem(-1); + break; + case "next": + goToListItem(1); + break; } } } @@ -208,8 +212,8 @@ function handleEntryStatus(item, element, setToRead) { // Add an icon-label span element. function appendIconLabel(element, labelTextContent) { - const span = document.createElement('span'); - span.classList.add('icon-label'); + const span = document.createElement("span"); + span.classList.add("icon-label"); span.textContent = labelTextContent; element.appendChild(span); } @@ -276,7 +280,7 @@ function updateEntriesStatus(entryIDs, status, callback) { const request = new RequestBuilder(url); request.withBody({ entry_ids: entryIDs, status: status }); request.withCallback((resp) => { - resp.json().then(count => { + resp.json().then((count) => { if (callback) { callback(resp); } @@ -386,7 +390,7 @@ function handleFetchOriginalContent() { const request = new RequestBuilder(buttonElement.dataset.fetchContentUrl); request.withCallback((response) => { - buttonElement.textContent = ''; + buttonElement.textContent = ""; buttonElement.appendChild(previousElement); response.json().then((data) => { @@ -419,7 +423,7 @@ function openOriginalLink(openLinkInCurrentTab) { const currentItem = document.querySelector(".current-item"); // If we are not on the list of starred items, move to the next item - if (document.location.href !== document.querySelector(':is(a, button)[data-page=starred]').href) { + if (document.location.href !== document.querySelector(":is(a, button)[data-page=starred]").href) { goToListItem(1); } markEntryAsRead(currentItem); @@ -516,7 +520,7 @@ function goToFeedOrFeeds() { if (isEntry()) { goToFeed(); } else { - goToPage('feeds'); + goToPage("feeds"); } } @@ -601,16 +605,13 @@ function updateUnreadCounterValue(callback) { element.textContent = callback(oldValue); }); - if (window.location.href.endsWith('/unread')) { - const oldValue = parseInt(document.title.split('(')[1], 10); + if (window.location.href.endsWith("/unread")) { + const oldValue = parseInt(document.title.split("(")[1], 10); const newValue = callback(oldValue); - document.title = document.title.replace( - /(.*?)\(\d+\)(.*?)/, - function (match, prefix, suffix, offset, string) { - return prefix + '(' + newValue + ')' + suffix; - } - ); + document.title = document.title.replace(/(.*?)\(\d+\)(.*?)/, function (match, prefix, suffix, offset, string) { + return prefix + "(" + newValue + ")" + suffix; + }); } } @@ -633,7 +634,7 @@ function findEntry(element) { } function handleConfirmationMessage(linkElement, callback) { - if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") { + if (linkElement.tagName !== "A" && linkElement.tagName !== "BUTTON") { linkElement = linkElement.parentNode; } @@ -696,9 +697,9 @@ function showToast(label, iconElement) { appendIconLabel(toastMsgElement, label); const toastElementWrapper = document.getElementById("toast-wrapper"); - toastElementWrapper.classList.remove('toast-animate'); + toastElementWrapper.classList.remove("toast-animate"); setTimeout(() => { - toastElementWrapper.classList.add('toast-animate'); + toastElementWrapper.classList.add("toast-animate"); }, 100); } @@ -721,8 +722,9 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) { const recordInterval = 10; // we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds - if (currentPositionInSeconds >= (lastKnownPositionInSeconds + recordInterval) || - currentPositionInSeconds <= (lastKnownPositionInSeconds - recordInterval) + if ( + currentPositionInSeconds >= lastKnownPositionInSeconds + recordInterval || + currentPositionInSeconds <= lastKnownPositionInSeconds - recordInterval ) { playerElement.dataset.lastPosition = currentPositionInSeconds.toString(); const request = new RequestBuilder(playerElement.dataset.saveUrl); @@ -730,7 +732,7 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) { request.execute(); // Handle the mark as read on completion if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) { - const completion = currentPositionInSeconds / playerElement.duration; + const completion = currentPositionInSeconds / playerElement.duration; if (completion >= markAsReadOnCompletion) { handleEntryStatus("none", document.querySelector(":is(a, button)[data-toggle-status]"), true); } @@ -744,18 +746,14 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) { * @returns {boolean} */ function isPlayerPlaying(element) { - return element && - element.currentTime > 0 && - !element.paused && - !element.ended && - element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState + return element && element.currentTime > 0 && !element.paused && !element.ended && element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState } /** * handle new share entires and already shared entries */ async function handleShare() { - const link = document.querySelector(':is(a, button)[data-share-status]'); + const link = document.querySelector(":is(a, button)[data-share-status]"); const title = document.querySelector(".entry-header > h1 > a"); if (link.dataset.shareStatus === "shared") { await checkShareAPI(title, link.href); @@ -771,8 +769,8 @@ async function handleShare() { } /** -* wrapper for Web Share API -*/ + * wrapper for Web Share API + */ async function checkShareAPI(title, url) { if (!navigator.canShare) { console.error("Your browser doesn't support the Web Share API."); @@ -782,7 +780,7 @@ async function checkShareAPI(title, url) { try { await navigator.share({ title: title ? title.textContent : url, - url: url + url: url, }); } catch (err) { console.error(err); @@ -810,31 +808,33 @@ function handleMediaControl(button) { const action = button.dataset.enclosureAction; const value = parseFloat(button.dataset.actionValue); const targetEnclosureId = button.dataset.enclosureId; - const enclosures = document.querySelectorAll(`audio[data-enclosure-id="${targetEnclosureId}"],video[data-enclosure-id="${targetEnclosureId}"]`); + const enclosures = document.querySelectorAll( + `audio[data-enclosure-id="${targetEnclosureId}"],video[data-enclosure-id="${targetEnclosureId}"]` + ); const speedIndicator = document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${targetEnclosureId}"]`); enclosures.forEach((enclosure) => { switch (action) { - case "seek": - enclosure.currentTime = Math.max(enclosure.currentTime + value, 0); - break; - case "speed": - // I set a floor speed of 0.25 to avoid too slow speed where it gives the impression it stopped. - // 0.25 was chosen because it will allow to get back to 1x in two "faster" click, and lower value with same property would be 0. - enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value); - speedIndicator.forEach((speedI) => { - // Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters. - // The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature - speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; - }); - break; - case "speed-reset": - enclosure.playbackRate = value ; - speedIndicator.forEach((speedI) => { - // Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters. - // The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature - speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; - }); - break; + case "seek": + enclosure.currentTime = Math.max(enclosure.currentTime + value, 0); + break; + case "speed": + // I set a floor speed of 0.25 to avoid too slow speed where it gives the impression it stopped. + // 0.25 was chosen because it will allow to get back to 1x in two "faster" click, and lower value with same property would be 0. + enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value); + speedIndicator.forEach((speedI) => { + // Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters. + // The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature + speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; + }); + break; + case "speed-reset": + enclosure.playbackRate = value; + speedIndicator.forEach((speedI) => { + // Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters. + // The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature + speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; + }); + break; } }); } diff --git a/internal/ui/static/js/bootstrap.js b/internal/ui/static/js/bootstrap.js index 82b08d7d..944865cf 100644 --- a/internal/ui/static/js/bootstrap.js +++ b/internal/ui/static/js/bootstrap.js @@ -39,9 +39,9 @@ document.addEventListener("DOMContentLoaded", () => { keyboardHandler.on("#", unsubscribeFromFeed); keyboardHandler.on("/", () => goToPage("search")); keyboardHandler.on("a", () => { - const enclosureElement = document.querySelector('.entry-enclosures'); + const enclosureElement = document.querySelector(".entry-enclosures"); if (enclosureElement) { - enclosureElement.toggleAttribute('open'); + enclosureElement.toggleAttribute("open"); } }); keyboardHandler.on("Escape", () => ModalHandler.close()); @@ -54,7 +54,9 @@ document.addEventListener("DOMContentLoaded", () => { if (WebAuthnHandler.isWebAuthnSupported()) { const webauthnHandler = new WebAuthnHandler(); - onClick("#webauthn-delete", () => { webauthnHandler.removeAllCredentials(); }); + onClick("#webauthn-delete", () => { + webauthnHandler.removeAllCredentials(); + }); const registerButton = document.getElementById("webauthn-register"); if (registerButton !== null) { @@ -74,11 +76,11 @@ document.addEventListener("DOMContentLoaded", () => { const usernameField = document.getElementById("form-username"); if (usernameField !== null) { abortController.abort(); - webauthnHandler.login(usernameField.value).catch(err => WebAuthnHandler.showErrorMessage(err)); + webauthnHandler.login(usernameField.value).catch((err) => WebAuthnHandler.showErrorMessage(err)); } }); - webauthnHandler.conditionalLogin(abortController).catch(err => WebAuthnHandler.showErrorMessage(err)); + webauthnHandler.conditionalLogin(abortController).catch((err) => WebAuthnHandler.showErrorMessage(err)); } } @@ -86,32 +88,44 @@ document.addEventListener("DOMContentLoaded", () => { onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmark(event.target)); onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent); onClick(":is(a, button)[data-share-status]", handleShare); - onClick(":is(a, button)[data-action=markPageAsRead]", (event) => handleConfirmationMessage(event.target, markPageAsRead)); + onClick(":is(a, button)[data-action=markPageAsRead]", (event) => + handleConfirmationMessage(event.target, markPageAsRead) + ); onClick(":is(a, button)[data-toggle-status]", (event) => handleEntryStatus("next", event.target)); - onClick(":is(a, button)[data-confirm]", (event) => handleConfirmationMessage(event.target, (url, redirectURL) => { - const request = new RequestBuilder(url); + onClick(":is(a, button)[data-confirm]", (event) => + handleConfirmationMessage(event.target, (url, redirectURL) => { + const request = new RequestBuilder(url); - request.withCallback((response) => { - if (redirectURL) { - window.location.href = redirectURL; - } else if (response && response.redirected && response.url) { - window.location.href = response.url; - } else { - window.location.reload(); - } - }); + request.withCallback((response) => { + if (redirectURL) { + window.location.href = redirectURL; + } else if (response && response.redirected && response.url) { + window.location.href = response.url; + } else { + window.location.reload(); + } + }); - request.execute(); - })); + request.execute(); + }) + ); - onClick("a[data-original-link='true']", (event) => { - handleEntryStatus("next", event.target, true); - }, true); - onAuxClick("a[data-original-link='true']", (event) => { - if (event.button === 1) { + onClick( + "a[data-original-link='true']", + (event) => { handleEntryStatus("next", event.target, true); - } - }, true); + }, + true + ); + onAuxClick( + "a[data-original-link='true']", + (event) => { + if (event.button === 1) { + handleEntryStatus("next", event.target, true); + } + }, + true + ); checkMenuToggleModeByLayout(); window.addEventListener("resize", checkMenuToggleModeByLayout, { passive: true }); @@ -129,19 +143,19 @@ document.addEventListener("DOMContentLoaded", () => { if ("serviceWorker" in navigator) { const scriptElement = document.getElementById("service-worker-script"); if (scriptElement) { - navigator.serviceWorker.register(ttpolicy.createScriptURL(scriptElement.src)); + navigator.serviceWorker.register(ttpolicy.createScriptURL(scriptElement.src)); } } - window.addEventListener('beforeinstallprompt', (e) => { + window.addEventListener("beforeinstallprompt", (e) => { let deferredPrompt = e; - const promptHomeScreen = document.getElementById('prompt-home-screen'); + const promptHomeScreen = document.getElementById("prompt-home-screen"); if (promptHomeScreen) { promptHomeScreen.style.display = "block"; - const btnAddToHomeScreen = document.getElementById('btn-add-to-home-screen'); + const btnAddToHomeScreen = document.getElementById("btn-add-to-home-screen"); if (btnAddToHomeScreen) { - btnAddToHomeScreen.addEventListener('click', (e) => { + btnAddToHomeScreen.addEventListener("click", (e) => { e.preventDefault(); deferredPrompt.prompt(); deferredPrompt.userChoice.then(() => { @@ -167,13 +181,15 @@ document.addEventListener("DOMContentLoaded", () => { playbackRateElements.forEach((element) => { if (element.dataset.playbackRate) { element.playbackRate = element.dataset.playbackRate; - if (element.dataset.enclosureId){ + if (element.dataset.enclosureId) { // In order to display properly the speed we need to do it on bootstrap. // Could not do it backend side because I didn't know how to do it because of the template inclusion and // the way the initial playback speed is handled. See enclosure_media_controls.html if you want to try to fix this - document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${element.dataset.enclosureId}"]`).forEach((speedI)=>{ - speedI.innerText = `${parseFloat(element.dataset.playbackRate).toFixed(2)}x`; - }); + document + .querySelectorAll(`span.speed-indicator[data-enclosure-id="${element.dataset.enclosureId}"]`) + .forEach((speedI) => { + speedI.innerText = `${parseFloat(element.dataset.playbackRate).toFixed(2)}x`; + }); } } }); diff --git a/internal/ui/static/js/keyboard_handler.js b/internal/ui/static/js/keyboard_handler.js index cf3fea01..c835f850 100644 --- a/internal/ui/static/js/keyboard_handler.js +++ b/internal/ui/static/js/keyboard_handler.js @@ -46,9 +46,11 @@ class KeyboardHandler { } isEventIgnored(event, key) { - return event.target.tagName === "INPUT" || + return ( + event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA" || - (this.queue.length < 1 && !this.triggers.has(key)); + (this.queue.length < 1 && !this.triggers.has(key)) + ); } static isModifierKeyDown(event) { @@ -57,12 +59,18 @@ class KeyboardHandler { static getKey(event) { switch (event.key) { - case 'Esc': return 'Escape'; - case 'Up': return 'ArrowUp'; - case 'Down': return 'ArrowDown'; - case 'Left': return 'ArrowLeft'; - case 'Right': return 'ArrowRight'; - default: return event.key; + case "Esc": + return "Escape"; + case "Up": + return "ArrowUp"; + case "Down": + return "ArrowDown"; + case "Left": + return "ArrowLeft"; + case "Right": + return "ArrowRight"; + default: + return event.key; } } } diff --git a/internal/ui/static/js/modal_handler.js b/internal/ui/static/js/modal_handler.js index 536cea3e..401b78e6 100644 --- a/internal/ui/static/js/modal_handler.js +++ b/internal/ui/static/js/modal_handler.js @@ -28,7 +28,7 @@ class ModalHandler { const lastFocusableElement = focusableElements[focusableElements.length - 1]; this.getModalContainer().onkeydown = (e) => { - if (e.key !== 'Tab') { + if (e.key !== "Tab") { return; } diff --git a/internal/ui/static/js/request_builder.js b/internal/ui/static/js/request_builder.js index c72cfb4e..c66b15af 100644 --- a/internal/ui/static/js/request_builder.js +++ b/internal/ui/static/js/request_builder.js @@ -9,8 +9,8 @@ class RequestBuilder { body: null, headers: new Headers({ "Content-Type": "application/json", - "X-Csrf-Token": getCsrfToken() - }) + "X-Csrf-Token": getCsrfToken(), + }), }; } diff --git a/internal/ui/static/js/service_worker.js b/internal/ui/static/js/service_worker.js index 37cce257..d3482617 100644 --- a/internal/ui/static/js/service_worker.js +++ b/internal/ui/static/js/service_worker.js @@ -1,4 +1,3 @@ - // Incrementing OFFLINE_VERSION will kick off the install event and force // previously cached resources to be updated from the network. const OFFLINE_VERSION = 1; diff --git a/internal/ui/static/js/touch_handler.js b/internal/ui/static/js/touch_handler.js index c9f0aaed..46dbfa94 100644 --- a/internal/ui/static/js/touch_handler.js +++ b/internal/ui/static/js/touch_handler.js @@ -9,7 +9,7 @@ class TouchHandler { move: { x: -1, y: -1 }, moved: false, time: 0, - element: null + element: null, }; } @@ -18,7 +18,7 @@ class TouchHandler { const horizontalDistance = Math.abs(this.touch.move.x - this.touch.start.x); const verticalDistance = Math.abs(this.touch.move.y - this.touch.start.y); - if (horizontalDistance > 30 && verticalDistance < 70 || this.touch.moved) { + if ((horizontalDistance > 30 && verticalDistance < 70) || this.touch.moved) { return this.touch.move.x - this.touch.start.x; } } diff --git a/internal/ui/static/js/tt.js b/internal/ui/static/js/tt.js index f42cc47a..d24b1af0 100644 --- a/internal/ui/static/js/tt.js +++ b/internal/ui/static/js/tt.js @@ -2,14 +2,14 @@ let ttpolicy; if (window.trustedTypes && trustedTypes.createPolicy) { //TODO: use an allow-list for `createScriptURL` if (!ttpolicy) { - ttpolicy = trustedTypes.createPolicy('ttpolicy', { - createScriptURL: src => src, - createHTML: html => html, + ttpolicy = trustedTypes.createPolicy("ttpolicy", { + createScriptURL: (src) => src, + createHTML: (html) => html, }); } } else { ttpolicy = { - createScriptURL: src => src, - createHTML: html => html, + createScriptURL: (src) => src, + createHTML: (html) => html, }; } diff --git a/internal/ui/static/js/webauthn_handler.js b/internal/ui/static/js/webauthn_handler.js index c12c9646..cc7f9f84 100644 --- a/internal/ui/static/js/webauthn_handler.js +++ b/internal/ui/static/js/webauthn_handler.js @@ -23,9 +23,11 @@ class WebAuthnHandler { } async isConditionalLoginSupported() { - return WebAuthnHandler.isWebAuthnSupported() && + return ( + WebAuthnHandler.isWebAuthnSupported() && window.PublicKeyCredential.isConditionalMediationAvailable && - window.PublicKeyCredential.isConditionalMediationAvailable(); + window.PublicKeyCredential.isConditionalMediationAvailable() + ); } async conditionalLogin(abortController) { @@ -35,7 +37,7 @@ class WebAuthnHandler { } decodeBuffer(value) { - return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)); + return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)); } encodeBuffer(value) { @@ -55,7 +57,7 @@ class WebAuthnHandler { method: "POST", headers: { "Content-Type": "application/json", - "X-Csrf-Token": getCsrfToken() + "X-Csrf-Token": getCsrfToken(), }, body: JSON.stringify(data), }); @@ -90,10 +92,14 @@ class WebAuthnHandler { } const credentialCreationOptions = await registerBeginResponse.json(); - credentialCreationOptions.publicKey.challenge = this.decodeBuffer(credentialCreationOptions.publicKey.challenge); + credentialCreationOptions.publicKey.challenge = this.decodeBuffer( + credentialCreationOptions.publicKey.challenge + ); credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id); - if (Object.hasOwn(credentialCreationOptions.publicKey, 'excludeCredentials')) { - credentialCreationOptions.publicKey.excludeCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id)); + if (Object.hasOwn(credentialCreationOptions.publicKey, "excludeCredentials")) { + credentialCreationOptions.publicKey.excludeCredentials.forEach( + (credential) => (credential.id = this.decodeBuffer(credential.id)) + ); } const attestation = await navigator.credentials.create(credentialCreationOptions); @@ -134,8 +140,10 @@ class WebAuthnHandler { const credentialRequestOptions = await loginBeginResponse.json(); credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge); - if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) { - credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id)); + if (Object.hasOwn(credentialRequestOptions.publicKey, "allowCredentials")) { + credentialRequestOptions.publicKey.allowCredentials.forEach( + (credential) => (credential.id = this.decodeBuffer(credential.id)) + ); } if (abortController) { @@ -146,8 +154,7 @@ class WebAuthnHandler { let assertion; try { assertion = await navigator.credentials.get(credentialRequestOptions); - } - catch (err) { + } catch (err) { // Swallow aborted conditional logins if (err instanceof DOMException && err.name === "AbortError") { return;