Merge pull request #2756 from basecamp/friendly-error-message-on-duplicate-passkey

Show separate error message when adding a Passkey twice
This commit is contained in:
Stanko Krtalić
2026-03-26 14:19:35 +01:00
committed by GitHub
parent 527f2e07ff
commit 09d5b8c92f
3 changed files with 29 additions and 9 deletions
+14 -5
View File
@@ -77,9 +77,9 @@ class PasskeyButton extends HTMLElement {
#handleError(error) { #handleError(error) {
console.error("Passkey ceremony failed", error) console.error("Passkey ceremony failed", error)
const cancelled = error.name === "AbortError" || error.name === "NotAllowedError" const type = errorType(error)
this.#showError(cancelled ? "cancelled" : "error") this.#showError(type)
this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } })) this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, type } }))
} }
#showError(type) { #showError(type) {
@@ -139,8 +139,8 @@ class PasskeySignInButton extends PasskeyButton {
this.form.submit() this.form.submit()
} catch (error) { } catch (error) {
console.error("Passkey conditional mediation failed", error) console.error("Passkey conditional mediation failed", error)
const cancelled = error.name === "AbortError" || error.name === "NotAllowedError" const type = errorType(error)
this.button.dispatchEvent(new CustomEvent("passkey:error", { bubbles: true, detail: { error, cancelled } })) 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 ---------------------------------------------------------- // -- Shared helpers ----------------------------------------------------------
function errorType(error) {
switch (error.name) {
case "AbortError":
case "NotAllowedError": return "cancelled"
case "InvalidStateError": return "duplicate"
default: return "error"
}
}
function passkeysAvailable() { function passkeysAvailable() {
return !!window.PublicKeyCredential return !!window.PublicKeyCredential
} }
+1 -1
View File
@@ -18,7 +18,7 @@
<% end %> <% end %>
<footer> <footer>
<%= passkey_registration_button my_passkeys_path, options: @registration_options, error: { class: "txt-negative margin-block-half" }, cancellation: { class: "txt-subtle margin-block-half" }, class: "btn btn--link center txt-medium" do %> <%= passkey_registration_button my_passkeys_path, options: @registration_options, error: { class: "txt-negative margin-block-half" }, cancellation: { class: "txt-subtle margin-block-half" }, duplicate: { class: "txt-negative margin-block-half" }, class: "btn btn--link center txt-medium" do %>
<%= icon_tag "add" %> <%= icon_tag "add" %>
<span>Register a passkey</span> <span>Register a passkey</span>
<% end %> <% end %>
+14 -3
View File
@@ -9,6 +9,7 @@
module ActionPack::Passkey::FormHelper module ActionPack::Passkey::FormHelper
REGISTRATION_ERROR_MESSAGE = "Something went wrong while registering your passkey." REGISTRATION_ERROR_MESSAGE = "Something went wrong while registering your passkey."
REGISTRATION_CANCELLED_MESSAGE = "Passkey registration was cancelled. Try again when you are ready." REGISTRATION_CANCELLED_MESSAGE = "Passkey registration was cancelled. Try again when you are ready."
REGISTRATION_DUPLICATE_MESSAGE = "You already have a passkey registered on this device. Remove the existing one first to re-register."
SIGN_IN_ERROR_MESSAGE = "Something went wrong while signing in with your passkey." SIGN_IN_ERROR_MESSAGE = "Something went wrong while signing in with your passkey."
SIGN_IN_CANCELLED_MESSAGE = "Passkey sign in was cancelled. Try again when you are ready." SIGN_IN_CANCELLED_MESSAGE = "Passkey sign in was cancelled. Try again when you are ready."
@@ -33,6 +34,7 @@ module ActionPack::Passkey::FormHelper
component_options, form_options, button_options, error_options = partition_passkey_options(url, options) component_options, form_options, button_options, error_options = partition_passkey_options(url, options)
error_options[:error][:message] ||= REGISTRATION_ERROR_MESSAGE error_options[:error][:message] ||= REGISTRATION_ERROR_MESSAGE
error_options[:cancellation][:message] ||= REGISTRATION_CANCELLED_MESSAGE error_options[:cancellation][:message] ||= REGISTRATION_CANCELLED_MESSAGE
error_options[:duplicate][:message] ||= REGISTRATION_DUPLICATE_MESSAGE
param = form_options.delete(:param) param = form_options.delete(:param)
content_tag("rails-passkey-registration-button", **component_options.transform_keys { |key| key.to_s.dasherize }) do content_tag("rails-passkey-registration-button", **component_options.transform_keys { |key| key.to_s.dasherize }) do
@@ -91,7 +93,7 @@ module ActionPack::Passkey::FormHelper
form_options = options form_options = options
.fetch(:form, {}) .fetch(:form, {})
.reverse_merge(method: :post, action: url, class: "button_to", param: :passkey) .reverse_merge(method: :post, action: url, class: "button_to", param: :passkey)
error_options = options.slice(:error, :cancellation).reverse_merge(error: {}, cancellation: {}) error_options = options.slice(:error, :cancellation, :duplicate).reverse_merge(error: {}, cancellation: {}, duplicate: {})
button_options = options.except(:options, :form, *component_options.keys, *error_options.keys) button_options = options.except(:options, :form, *component_options.keys, *error_options.keys)
@@ -106,7 +108,7 @@ module ActionPack::Passkey::FormHelper
end end
end end
def passkey_error_messages(error: {}, cancellation: {}) def passkey_error_messages(error: {}, cancellation: {}, duplicate: {})
error_message = error[:message] error_message = error[:message]
error_attributes = error.except(:message) error_attributes = error.except(:message)
error_attributes[:data] ||= {} error_attributes[:data] ||= {}
@@ -117,6 +119,15 @@ module ActionPack::Passkey::FormHelper
cancellation_attributes[:data] ||= {} cancellation_attributes[:data] ||= {}
cancellation_attributes[:data][:passkey_error] = "cancelled" cancellation_attributes[:data][:passkey_error] = "cancelled"
tag.div(error_message, hidden: true, **error_attributes) + tag.div(cancellation_message, hidden: true, **cancellation_attributes) messages = tag.div(error_message, hidden: true, **error_attributes) +
tag.div(cancellation_message, hidden: true, **cancellation_attributes)
if duplicate[:message]
duplicate_attributes = duplicate.except(:message)
duplicate_attributes[:data] = { passkey_error: "duplicate" }
messages += tag.div(duplicate[:message], hidden: true, **duplicate_attributes)
end
messages
end end
end end