Cleanup form helpers

This commit is contained in:
Stanko K.R.
2026-03-25 15:00:49 +01:00
parent 1983014be6
commit 8e4f9ce5e7
6 changed files with 123 additions and 122 deletions
+61 -36
View File
@@ -7,6 +7,11 @@
# - +passkey_sign_in_button+ — render a <rails-passkey-sign-in-button> web component with
# a form, hidden fields, and error messages for the authentication ceremony.
module ActionPack::Passkey::FormHelper
CREATION_ERROR_MESSAGE = "Something went wrong while registering your passkey."
CREATION_CANCELLED_MESSAGE = "Passkey registration was cancelled. Try again when you are ready."
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."
# Renders a +<rails-passkey-creation-button>+ web component containing a form with hidden
# fields for the passkey registration ceremony and error messages. The form POSTs to +url+ and
# includes hidden fields for +client_data_json+, +attestation_object+, and +transports+ —
@@ -16,30 +21,28 @@ module ActionPack::Passkey::FormHelper
# Options:
# - +options+: WebAuthn creation options (JSON-serializable hash)
# - +challenge_url+: endpoint to refresh the challenge nonce
# - +param+: the form parameter namespace (default: +:passkey+)
# - +error_message+: message shown on ceremony failure
# - +cancelled_message+: message shown when ceremony is cancelled
# - +form+: additional HTML attributes for the +<form>+ tag
# - +form+: additional HTML attributes for the +<form>+ tag. Supports a +:param+ key
# to set the form parameter namespace (default: +:passkey+)
# - +error+: HTML attributes for the error message +<div>+. Supports a +:message+ key
# to override the default error text
# - +cancellation+: HTML attributes for the cancellation message +<div>+. Supports a
# +:message+ key to override the default cancellation text
# - All other options are passed to the +<button>+ tag
def passkey_creation_button(name = nil, url = nil, options:, challenge_url: nil, param: :passkey, error_message: "Something went wrong while registering your passkey.", cancelled_message: "Passkey registration was cancelled. Try again when you are ready.", form: {}, **button_options, &block)
def passkey_creation_button(name = nil, url = nil, **options, &block)
url, name = name, block ? capture(&block) : nil if block_given?
form_options = form.reverse_merge(method: :post, action: url, class: "button_to")
component_options, form_options, button_options, error_options = extract_passkey_component_options(url, options)
error_options[:error][:message] ||= CREATION_ERROR_MESSAGE
error_options[:cancellation][:message] ||= CREATION_CANCELLED_MESSAGE
param = form_options.delete(:param)
component_options = {
"creation-options": options.to_json,
"challenge-url": challenge_url || default_passkey_challenge_url
}
tag.send(:"rails-passkey-creation-button", **component_options) do
form_tag = tag.form(**form_options) do
content_tag("rails-passkey-creation-button", **component_options.transform_keys { |key| key.to_s.dasherize }) do
tag.form(**form_options) do
hidden_field_tag(:authenticity_token, form_authenticity_token) +
hidden_field_tag("#{param}[client_data_json]", nil, id: nil, data: { passkey_field: "client_data_json" }) +
hidden_field_tag("#{param}[attestation_object]", nil, id: nil, data: { passkey_field: "attestation_object" }) +
hidden_field_tag("#{param}[transports][]", nil, id: nil, data: { passkey_field: "transports" }) +
tag.button(name, type: :button, data: { passkey: "create" }, **button_options)
end
form_tag + passkey_error_messages(error_message, cancelled_message)
end + passkey_error_messages(**error_options)
end
end
@@ -51,37 +54,50 @@ module ActionPack::Passkey::FormHelper
# Options:
# - +options+: WebAuthn request options (JSON-serializable hash)
# - +challenge_url+: endpoint to refresh the challenge nonce
# - +param+: the form parameter namespace (default: +:passkey+)
# - +mediation+: WebAuthn mediation hint (e.g. +"conditional"+ for autofill-assisted sign in)
# - +error_message+: message shown on ceremony failure
# - +cancelled_message+: message shown when ceremony is cancelled
# - +form+: additional HTML attributes for the +<form>+ tag
# - +form+: additional HTML attributes for the +<form>+ tag. Supports a +:param+ key
# to set the form parameter namespace (default: +:passkey+)
# - +error+: HTML attributes for the error message +<div>+. Supports a +:message+ key
# to override the default error text
# - +cancellation+: HTML attributes for the cancellation message +<div>+. Supports a
# +:message+ key to override the default cancellation text
# - All other options are passed to the +<button>+ tag
def passkey_sign_in_button(name = nil, url = nil, options:, challenge_url: nil, param: :passkey, mediation: nil, error_message: "Something went wrong while signing in with your passkey.", cancelled_message: "Passkey sign in was cancelled. Try again when you are ready.", form: {}, **button_options, &block)
def passkey_sign_in_button(name = nil, url = nil, **options, &block)
url, name = name, block ? capture(&block) : nil if block_given?
form_options = form.reverse_merge(method: :post, action: url, class: "button_to")
component_options, form_options, button_options, error_options = extract_passkey_component_options(url, options)
error_options[:error][:message] ||= SIGN_IN_ERROR_MESSAGE
error_options[:cancellation][:message] ||= SIGN_IN_CANCELLED_MESSAGE
param = form_options.delete(:param)
component_options = {
"request-options": options.to_json,
"challenge-url": challenge_url || default_passkey_challenge_url
}
component_options[:mediation] = mediation if mediation
tag.send(:"rails-passkey-sign-in-button", **component_options) do
form_tag = tag.form(**form_options) do
content_tag("rails-passkey-sign-in-button", **component_options.transform_keys { |key| key.to_s.dasherize }) do
tag.form(**form_options) do
hidden_field_tag(:authenticity_token, form_authenticity_token) +
hidden_field_tag("#{param}[id]", nil, id: nil, data: { passkey_field: "id" }) +
hidden_field_tag("#{param}[client_data_json]", nil, id: nil, data: { passkey_field: "client_data_json" }) +
hidden_field_tag("#{param}[authenticator_data]", nil, id: nil, data: { passkey_field: "authenticator_data" }) +
hidden_field_tag("#{param}[signature]", nil, id: nil, data: { passkey_field: "signature" }) +
tag.button(name, type: :button, data: { passkey: "sign_in" }, **button_options)
end
form_tag + passkey_error_messages(error_message, cancelled_message)
end + passkey_error_messages(**error_options)
end
end
private
def extract_passkey_component_options(url, options)
passkey_options = options.fetch(:options, {})
component_options = options
.slice(:challenge_url, :mediation)
.reverse_merge(challenge_url: default_passkey_challenge_url, options: passkey_options.to_json(except: :challenge))
form_options = options
.fetch(:form, {})
.reverse_merge(method: :post, action: url, class: "button_to", param: :passkey)
error_options = options.slice(:error, :cancellation).reverse_merge(error: {}, cancellation: {})
button_options = options.except(:options, :form, *component_options.keys, *error_options.keys)
[ component_options, form_options, button_options, error_options ]
end
def default_passkey_challenge_url
if challenge_url = Rails.configuration.action_pack.passkey.challenge_url
instance_exec(&challenge_url)
@@ -90,8 +106,17 @@ module ActionPack::Passkey::FormHelper
end
end
def passkey_error_messages(error_message, cancelled_message)
tag.p(error_message, data: { passkey_error: "error" }, class: "txt-negative") +
tag.p(cancelled_message, data: { passkey_error: "cancelled" }, class: "txt-subtle")
def passkey_error_messages(error: {}, cancellation: {})
error_message = error[:message]
error_attributes = error.except(:message)
error_attributes[:data] ||= {}
error_attributes[:data][:passkey_error] = "error"
cancellation_message = cancellation[:message]
cancellation_attributes = cancellation.except(:message)
cancellation_attributes[:data] ||= {}
cancellation_attributes[:data][:passkey_error] = "cancelled"
tag.div(error_message, hidden: true, **error_attributes) + tag.div(cancellation_message, hidden: true, **cancellation_attributes)
end
end
@@ -67,7 +67,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptions < ActionPack::W
# Returns a Hash suitable for JSON serialization and passing to the
# WebAuthn JavaScript API.
def as_json(*)
def as_json(options = {})
json = {
challenge: challenge,
rp: relying_party.as_json,
@@ -96,7 +96,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptions < ActionPack::W
json[:attestation] = attestation.to_s
end
json
json.as_json(options)
end
private
@@ -34,13 +34,13 @@ class ActionPack::WebAuthn::PublicKeyCredential::RequestOptions < ActionPack::We
# Returns a Hash suitable for JSON serialization and passing to the
# WebAuthn JavaScript API.
def as_json(*)
def as_json(options = {})
{
challenge: challenge,
rpId: relying_party.id,
allowCredentials: credentials.map { |credential| allow_credential_json(credential) },
userVerification: user_verification.to_s
}
}.as_json(options)
end
private