From 97b37ce601c8e81cf33e43dede41ad938bca025c Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 7 Mar 2026 21:27:02 -0500 Subject: [PATCH] Keep View registration button lit for public users via reg param Public (non-authenticated) users who register for events lose the 'View registration' button when they navigate away, because they have no logged-in identity to look the registration up by. This threads the registration slug through the URL as a '?reg=' param so the button persists across the events index and event pages for the current browsing session. - Slugs are packed into a single reg param, period-delimited, so one session can carry 2-3 registrations (period never occurs in a urlsafe_base64 slug, keeping the URL clean and collision-free) - EventsHelper parses the param, matches registrations per event, and merges a new slug in after registering so earlier events stay lit - The param is forwarded across nav/register links and the ticket page, and survives the Stripe checkout round-trip for paid events - Purely URL-based: nothing persists server-side, so a shared computer never leaks a prior visitor's registration - Request spec coverage for single- and multi-slug display and merging Co-Authored-By: Claude Opus 4.8 --- .../events/public_registrations_controller.rb | 6 +-- app/helpers/events_helper.rb | 27 ++++++++++++ app/views/events/_card.html.erb | 13 +++++- .../events/_registration_section.html.erb | 6 +-- .../events/public_registrations/new.html.erb | 2 +- app/views/events/registrations/show.html.erb | 2 +- app/views/events/show.html.erb | 2 +- spec/requests/events/registrations_spec.rb | 6 +++ spec/requests/events_spec.rb | 43 +++++++++++++++++++ 9 files changed, 96 insertions(+), 11 deletions(-) diff --git a/app/controllers/events/public_registrations_controller.rb b/app/controllers/events/public_registrations_controller.rb index a4ea0e5979..42e8db55b0 100644 --- a/app/controllers/events/public_registrations_controller.rb +++ b/app/controllers/events/public_registrations_controller.rb @@ -80,7 +80,7 @@ def create checkout_session = create_stripe_checkout_session(registration, result.form_submission) redirect_to checkout_session.url, allow_other_host: true, status: :see_other else - redirect_to registration_ticket_path(registration.slug), + redirect_to registration_ticket_path(registration.slug, reg: params[:reg].presence), notice: "You have been successfully registered!" end else @@ -165,8 +165,8 @@ def create_stripe_checkout_session(registration, submission = nil) }, quantity: 1 } ], - success_url: registration_ticket_url(registration.slug, checkout: "success"), - cancel_url: registration_ticket_url(registration.slug, checkout: "cancelled") + success_url: registration_ticket_url(registration.slug, checkout: "success", reg: params[:reg].presence), + cancel_url: registration_ticket_url(registration.slug, checkout: "cancelled", reg: params[:reg].presence) ) registration.update!(checkout_session_id: checkout_session.id) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 1c6d6fe302..20adffe296 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,4 +1,31 @@ module EventsHelper + # Separates registration slugs packed into a single `?reg=` param so public + # (logged-out) users keep the "View registration" button lit across several + # events in one session. A period is used because it never appears in a + # urlsafe_base64 slug and stays unescaped in a URL, keeping it human-readable. + REG_PARAM_DELIMITER = "." + + # The registration slugs carried in the current request's `?reg=` param. + def reg_param_slugs + params[:reg].to_s.split(REG_PARAM_DELIMITER) + end + + # The registration for this event that the visitor has "unlocked" via their + # `?reg=` param, if any. Slugs are the authorization token, so no ownership + # check is needed. Returns nil when none of the param slugs belong to the event. + def reg_param_registration(event) + slugs = reg_param_slugs + return if slugs.empty? + EventRegistration.where(slug: slugs, event_id: event.id).first + end + + # The current `?reg=` slug set with `slug` merged in, for forwarding onward + # links after a registration so the new event's button stays lit alongside + # any events registered earlier this session. + def reg_param_with(slug) + (reg_param_slugs << slug).uniq.join(REG_PARAM_DELIMITER) + end + # Stable anchor id for a registrant's row on the Onboarding matrix, so back-links # from detail pages can scroll to (and highlight) the row they came from. def onboarding_row_id(record_or_id) diff --git a/app/views/events/_card.html.erb b/app/views/events/_card.html.erb index 5d78883136..c22baa7f69 100644 --- a/app/views/events/_card.html.erb +++ b/app/views/events/_card.html.erb @@ -24,7 +24,7 @@
- <%= link_to event_path(event), + <%= link_to event_path(event, reg: params[:reg].presence), class: "hover:underline block leading-tight", data: { turbo_prefetch: false, turbo: false } do %> <%= title_with_badges(event).html_safe %> @@ -34,6 +34,10 @@ <% registered = current_user && event.actively_registered?(current_user.person) %> + <% unless registered + slug_registration = reg_param_registration(event) + slug_registered = slug_registration&.active? + end %>
<% if event.location.present? %> @@ -58,6 +62,11 @@ class: "btn px-3 py-2 text-xs uppercase leading-tight text-white hover:bg-white event-view-registration-btn", style: "font-family: 'Telefon Bold', sans-serif; background-color: rgb(22, 101, 52); border: 2px solid rgb(22, 101, 52);" %> <% end %> + <% elsif slug_registered %> + <%= link_to "View registration", registration_ticket_path(slug_registration.slug), + data: { turbo_frame: "_top" }, + class: "btn px-3 py-2 text-xs uppercase leading-tight text-white hover:bg-white event-view-registration-btn", + style: "font-family: 'Telefon Bold', sans-serif; background-color: rgb(22, 101, 52); border: 2px solid rgb(22, 101, 52);" %> <% elsif event.ended? %> Event ended <% if allowed_to?(:manage?, event) %> @@ -82,7 +91,7 @@ <% end %> <% elsif event.object.public_registration_enabled? && event.object.event_forms.registration.exists? %> <%= link_to "Register", - new_event_public_registration_path(event), + new_event_public_registration_path(event, reg: params[:reg].presence), data: { turbo_frame: "_top" }, class: "btn px-3 py-2 text-xs uppercase leading-tight font-telefon text-accent border-2 border-accent hover:text-white hover:bg-orange-700" %> <% end %> diff --git a/app/views/events/_registration_section.html.erb b/app/views/events/_registration_section.html.erb index 836eee8020..af58c1c1a3 100644 --- a/app/views/events/_registration_section.html.erb +++ b/app/views/events/_registration_section.html.erb @@ -1,7 +1,7 @@ <% instance ||= 1 %> <% button_text ||= "Register" %> <% registered = event.actively_registered?(current_user&.person) %> -<% slug_registration = params[:reg].present? ? EventRegistration.find_by(slug: params[:reg], event_id: event.id) : nil %> +<% slug_registration = reg_param_registration(event) %> <% slug_registered = slug_registration&.active? %> <% slug_cancelled = slug_registration.present? && slug_registration.status == "cancelled" %> <%= tag.div id: dom_id(event.object, "registration_section_#{instance}"), class: "registration-section flex flex-col items-center gap-4 mb-6" do %> @@ -29,13 +29,13 @@ style: "font-family: 'Telefon Bold', sans-serif;" %> <% else %> <%= link_to button_text, - new_event_public_registration_path(event), + new_event_public_registration_path(event, reg: params[:reg].presence), class: "btn btn-accent px-10 py-2 text-2xl uppercase", style: "font-family: 'Telefon Bold', sans-serif;" %> <% end %> <% elsif event.object.public_registration_enabled? && event.object.event_forms.registration.exists? %> <%= link_to button_text, - new_event_public_registration_path(event), + new_event_public_registration_path(event, reg: params[:reg].presence), class: "btn btn-accent px-10 py-2 text-2xl uppercase", style: "font-family: 'Telefon Bold', sans-serif;" %> <% end %> diff --git a/app/views/events/public_registrations/new.html.erb b/app/views/events/public_registrations/new.html.erb index 719e2f3ed9..fb55b4e9f4 100644 --- a/app/views/events/public_registrations/new.html.erb +++ b/app/views/events/public_registrations/new.html.erb @@ -94,7 +94,7 @@
<% end %> - <%= form_with url: event_public_registration_path(@event), method: :post, local: true, class: "space-y-2", data: { turbo: !@event.cost_cents.to_i.positive? } do |f| %> + <%= form_with url: event_public_registration_path(@event, reg: params[:reg].presence), method: :post, local: true, class: "space-y-2", data: { turbo: !@event.cost_cents.to_i.positive? } do |f| %> <% if @scholarship %> diff --git a/app/views/events/registrations/show.html.erb b/app/views/events/registrations/show.html.erb index 8e57b33e54..e362c3fd25 100644 --- a/app/views/events/registrations/show.html.erb +++ b/app/views/events/registrations/show.html.erb @@ -1,6 +1,6 @@ <% content_for(:page_bg_class, "public") %>
- <%= link_to event_path(@event_registration.event, reg: @event_registration.slug), class: "inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 px-2 py-1" do %> + <%= link_to event_path(@event_registration.event, reg: reg_param_with(@event_registration.slug)), class: "inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 px-2 py-1" do %> Back to event <% end %>
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 390644248a..07c471a8da 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -17,7 +17,7 @@ <% else %>
- <%= link_to "← Events", events_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %> + <%= link_to "← Events", events_path(reg: params[:reg].presence), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
<%= link_to "Home", root_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %> <% if @event.event_registrations.active.exists? %> diff --git a/spec/requests/events/registrations_spec.rb b/spec/requests/events/registrations_spec.rb index a2b85ea598..69e6805da6 100644 --- a/spec/requests/events/registrations_spec.rb +++ b/spec/requests/events/registrations_spec.rb @@ -18,6 +18,12 @@ get registration_ticket_path(registration.slug) expect(response).to have_http_status(:success) end + + it "merges this slug into the reg param on the back-to-event link" do + prior = create(:event_registration, registrant: create(:person)) + get registration_ticket_path(registration.slug, reg: prior.slug) + expect(response.body).to include("reg=#{prior.slug}.#{registration.slug}") + end end context "as an admin" do diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index e2854544a6..07b974cf1f 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -36,6 +36,33 @@ def offer_ce!(target_event) expect(response).to have_http_status(:ok) end + it "shows a View registration link on the card when an active reg param is present" do + published_event = create(:event, :published) + sign_in user + registration = create(:event_registration, event: published_event, registrant: create(:person)) + get events_path(reg: registration.slug) + expect(response.body).to include("href=\"#{registration_ticket_path(registration.slug)}\"") + end + + it "does not show a View registration link for a cancelled reg param" do + published_event = create(:event, :published) + sign_in user + registration = create(:event_registration, event: published_event, registrant: create(:person), status: "cancelled") + get events_path(reg: registration.slug) + expect(response.body).not_to include("href=\"#{registration_ticket_path(registration.slug)}\"") + end + + it "shows View registration links for several events packed into one reg param" do + event_a = create(:event, :published) + event_b = create(:event, :published) + sign_in user + reg_a = create(:event_registration, event: event_a, registrant: create(:person)) + reg_b = create(:event_registration, event: event_b, registrant: create(:person)) + get events_path(reg: "#{reg_a.slug}.#{reg_b.slug}") + expect(response.body).to include("href=\"#{registration_ticket_path(reg_a.slug)}\"") + expect(response.body).to include("href=\"#{registration_ticket_path(reg_b.slug)}\"") + end + context "when user time_zone is set" do # 19:00 UTC = 12:00 noon PT = 15:00 (3 pm) ET (June 15, 2031 with DST) let(:utc_start) { Time.utc(2031, 6, 15, 19, 0, 0) } @@ -71,6 +98,22 @@ def offer_ce!(target_event) end describe "GET /show" do + it "shows a View registration link when an active reg param is present" do + published_event = create(:event, :published) + sign_in user + registration = create(:event_registration, event: published_event, registrant: create(:person)) + get event_path(published_event, reg: registration.slug) + expect(response.body).to include("href=\"#{registration_ticket_path(registration.slug)}\"") + end + + it "does not show a View registration link for a cancelled reg param" do + published_event = create(:event, :published) + sign_in user + registration = create(:event_registration, event: published_event, registrant: create(:person), status: "cancelled") + get event_path(published_event, reg: registration.slug) + expect(response.body).not_to include("href=\"#{registration_ticket_path(registration.slug)}\"") + end + context "when event has ended" do let(:ended_event) { create(:event, :published, :ended) }