From 09d5b8c92f8b6fb514313e42a0424aada9d9c57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanko=20Krtali=C4=87?= Date: Thu, 26 Mar 2026 14:19:35 +0100 Subject: [PATCH] Merge pull request #2756 from basecamp/friendly-error-message-on-duplicate-passkey Show separate error message when adding a Passkey twice --- app/javascript/lib/action_pack/passkey.js | 19 ++++++++++++++----- app/views/my/passkeys/index.html.erb | 2 +- lib/action_pack/passkey/form_helper.rb | 17 ++++++++++++++--- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/javascript/lib/action_pack/passkey.js b/app/javascript/lib/action_pack/passkey.js index 7ee20cb84..103cbc72e 100644 --- a/app/javascript/lib/action_pack/passkey.js +++ b/app/javascript/lib/action_pack/passkey.js @@ -77,9 +77,9 @@ class PasskeyButton extends HTMLElement { #handleError(error) { console.error("Passkey ceremony failed", error) - const cancelled = error.name === "AbortError" || error.name === "NotAllowedError" - this.#showError(cancelled ? "cancelled" : "error") - this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } })) + const type = errorType(error) + this.#showError(type) + this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, type } })) } #showError(type) { @@ -139,8 +139,8 @@ class PasskeySignInButton extends PasskeyButton { this.form.submit() } catch (error) { console.error("Passkey conditional mediation failed", error) - const cancelled = error.name === "AbortError" || error.name === "NotAllowedError" - this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } })) + const type = errorType(error) + this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, type } })) } } } @@ -157,6 +157,15 @@ customElements.define("rails-passkey-sign-in-button", PasskeySignInButton) // -- Shared helpers ---------------------------------------------------------- +function errorType(error) { + switch (error.name) { + case "AbortError": + case "NotAllowedError": return "cancelled" + case "InvalidStateError": return "duplicate" + default: return "error" + } +} + function passkeysAvailable() { return !!window.PublicKeyCredential } diff --git a/app/views/my/passkeys/index.html.erb b/app/views/my/passkeys/index.html.erb index de143eeb4..fea08c27b 100644 --- a/app/views/my/passkeys/index.html.erb +++ b/app/views/my/passkeys/index.html.erb @@ -18,7 +18,7 @@ <% end %>