Module: Clickwrap::FormBuilderExtensions

Defined in:
lib/clickwrap/form_builder_extensions.rb

Overview

form.clickwrap and form.clickwrap_fields, mixed into the standard Rails form builder by the engine so the one-line happy path works inside an ordinary form_with.

Both methods do the same three things: ask the server-owned presenter for a presentation, render the reference partial with it, and put the signed presentation token in the form. The difference is only who renders the submit button.

form.clickwrap is the recommended one because it renders the controls AND the action as a single presentation. The call to action recorded in the manifest is then, by construction, the words on the button the person can actually press — there is no second place for that string to live and drift.

What this helper deliberately never renders is a hidden field carrying a decision the server owns: no IP address, no browser user-agent, no geolocation, no policy version, no document digest, no validity window, no retention rule, no subject binding. All of those are resolved server-side and rechecked at submit. A form field is not a safe place to keep a security decision, and a client that tries to send one gets a loud failure from Clickwrap::Submission rather than a quiet acceptance.

Instance Method Summary collapse

Instance Method Details

#clickwrap(policy_key, submit:, actor: NOT_GIVEN, subject: nil, tenant: NOT_GIVEN, acting_for: nil, locale: nil, capture_channel: nil, errors: nil, combined: true, **html_options) ⇒ Object

The strongest path, and the one the README leads with:

<%= form.clickwrap :signup, submit: "Create account" %>

<%= form.clickwrap :signup,
    actor: current_user,
    subject: @organization,
    locale: I18n.locale,
    submit: {
      text: "Create organization",
      class: "button button--primary",
      data: { turbo_submits_with: "Creating…" }
    } %>

submit: takes a String, or a Hash of text: plus any ordinary HTML options for the button. Everything else in **html_options decorates the wrapper element, so a design system can hang its own classes on the block.

What it renders, by default, is ONE line:

[ ] I agree to the Terms of Service and I acknowledge the Privacy Policy.

…whenever the policy's statements are all ordinary, required, default-worded agreements and acknowledgments. Anything the sentence cannot honestly absorb keeps a control of its own below it, and a policy with nothing composable is untouched. combined: false asks for the itemized shape regardless — one boolean, no style registry — and it reaches the PRESENTER rather than the template, so the manifest signs the shape that was actually offered.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/clickwrap/form_builder_extensions.rb', line 61

def clickwrap(policy_key, submit:, actor: NOT_GIVEN, subject: nil, tenant: NOT_GIVEN,
              acting_for: nil, locale: nil, capture_channel: nil, errors: nil,
              combined: true, **html_options)
  clickwrap_reserve_form_presentation!(policy_key)
  text, button_options = clickwrap_split_submit(submit)

  presentation = clickwrap_present(
    policy_key,
    submit_button_text: text,
    actor: actor,
    subject: subject,
    tenant: tenant,
    acting_for: acting_for,
    locale: locale,
    capture_channel: capture_channel,
    combined: combined
  )
  clickwrap_render_fields(
    presentation,
    submit: { text: text, options: button_options },
    errors: errors,
    html_options: html_options
  )
end

#clickwrap_fields(policy_key, submit_button_text:, actor: NOT_GIVEN, subject: nil, tenant: NOT_GIVEN, acting_for: nil, locale: nil, capture_channel: nil, errors: nil, combined: true, **html_options, &block) ⇒ Object

The split API, for design systems that render the action somewhere this helper cannot reach:

<%= form.clickwrap_fields :signup, submit_button_text: "Create account" %>
<%= form.clickwrap_submit %>

The manifest records the call to action the person was offered, so the wording is declared here. form.clickwrap_submit reuses it without a second string. An ordinary form.submit "Create account" is supported too, but Clickwrap verifies that it says exactly what the manifest says.

A design system that renders its OWN button markup — not form.submit, so not something Clickwrap can check — takes the block form and reads the wording off the presentation instead of retyping it:

<%= form.clickwrap_fields :signup, submit_button_text: "Create account" do |clickwrap| %>
<button class="btn btn--primary" data-turbo-submits-with="Creating…">
  <%= clickwrap.submit_button_text %>
</button>
<% end %>

The block is yielded the whole Presenter::Result — submit_button_text, statements, policy_key, locale — and its output is rendered inside the same wrapper, after the controls. This is the shape that makes drift impossible rather than merely detected: there is one string, it is the signed one, and nothing has to compare two copies of it afterwards.

submit: on form.clickwrap and submit_button_text: here are a deliberate pair, not a duplication. submit: says "render the button too"; submit_button_text: says "bind these exact words into the manifest, and I will render the button myself".



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/clickwrap/form_builder_extensions.rb', line 117

def clickwrap_fields(policy_key, submit_button_text:, actor: NOT_GIVEN, subject: nil,
                     tenant: NOT_GIVEN, acting_for: nil, locale: nil, capture_channel: nil, errors: nil,
                     combined: true, **html_options, &block)
  clickwrap_reserve_form_presentation!(policy_key)
  presentation = clickwrap_present(
    policy_key,
    submit_button_text: submit_button_text,
    actor: actor,
    subject: subject,
    tenant: tenant,
    acting_for: acting_for,
    locale: locale,
    capture_channel: capture_channel,
    combined: combined
  )

  # A block that renders the action itself has already single-sourced the
  # wording off the signed presentation, so there is no second copy left to
  # verify — and arming the `form.submit` check would then reject a form
  # that never called `form.submit` at all.
  @clickwrap_expected_submit_button_text = presentation.submit_button_text unless block

  clickwrap_render_fields(
    presentation,
    submit: nil,
    errors: errors,
    html_options: html_options,
    after: (@template.capture(presentation, &block) if block)
  )
end

#clickwrap_submit(**options) ⇒ Object

The DRY split-form action. clickwrap_fields already declared and signed the exact wording, so this helper renders that same wording without asking the host to repeat it:

<%= form.clickwrap_fields :signup, submit_button_text: "Create account" %>
<%= form.clickwrap_submit class: "button" %>

Ordinary form.submit "Create account" remains supported and is checked below. This helper simply removes the second string and therefore removes the possibility of drift by construction.



158
159
160
161
162
163
164
165
166
# File 'lib/clickwrap/form_builder_extensions.rb', line 158

def clickwrap_submit(**options)
  unless @clickwrap_expected_submit_button_text
    raise ConfigurationError,
          "form.clickwrap_submit needs form.clickwrap_fields earlier in the same form. " \
          "The fields declare the exact call to action that Clickwrap signs into evidence."
  end

  submit(@clickwrap_expected_submit_button_text, options)
end

#submit(value = nil, options = {}) ⇒ Object

When a split integration uses Rails' ordinary form.submit, compare the button Rails actually rendered with the words already signed into the presentation, refused in every environment rather than left as a development log. Honest bound: this hook covers form.submit — a raw



175
176
177
178
179
# File 'lib/clickwrap/form_builder_extensions.rb', line 175

def submit(value = nil, options = {})
  html = super
  clickwrap_verify_split_submit_button!(html) if @clickwrap_expected_submit_button_text
  html
end