Untangle Step 2 of the signup form (opt-ins vs intent) - #1014
Draft
RisingOrange wants to merge 22 commits into
Draft
Untangle Step 2 of the signup form (opt-ins vs intent)#1014RisingOrange wants to merge 22 commits into
RisingOrange wants to merge 22 commits into
Conversation
Reworks Step 2 of the signup form into the two-axis model for the onboarding design meeting (suggestion only, not intended to merge as-is): - 'Keep me informed' becomes an honest INFORMATIONAL opt-in (global + local chapter updates), replacing the 'connect me with my local chapter' wording that gated nothing. - Involvement tier (Act now / Volunteer / Lead) gates active chapter contact; Volunteer/Lead auto-include the informational opt-in (shown, not asked). - Critical-alert floor notice shown under the opt-ins. - Drops the 'at least one tick' requirement so a critical-alerts-only signup (privacy box only) is reachable, which the notice's premise assumes.
The auto-checked, locked 'Keep me informed' for the Volunteer/Lead tiers was a forced marketing-consent, which can't be freely given (worse than a pre-tick: the user can't refuse). The informational opt-in is now a genuine, uncheckable opt-in for everyone; operational volunteer comms would ride legitimate interest separately rather than a forced newsletter tick. Keeps the honest informational wording, the critical-alert floor notice, and the no-mandatory-one submit.
Reuse the shipping notice string from PR #1010 ('...even if you don't opt into any of these...') instead of a bespoke variant, so the prototype and the in-flight notice read the same. The wording fits V2 even better than the current form, since V2 drops the tick-at-least-one rule that made 'opt into nothing' impossible.
✅ Deploy Preview for pauseai ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…ree-to-share Matches the policy's actual mechanism (chapter sharing disclosed on a legitimate-interest basis = notice, not consent). Model-agnostic: acknowledgement works under joint controllership too. The 'shared with a separate entity' framing is left for the joint-controllership / privacy-policy V3 pass.
…owse page and confirmations
One conflict, on the step-2 submit: main added the anti-spam widget and its canSubmit gate there, while this branch had removed the tick-at-least-one requirement so a critical-alerts-only signup stays reachable. Keeps both — the widget and !canSubmit, gated only on consent.
RisingOrange
added a commit
that referenced
this pull request
Aug 19, 2026
"We'll connect you with your local PauseAI chapter" promises contact that may not happen: whether a chapter reaches out to a newsletter signup is the open question in #1014. The label already says what the box does.
RisingOrange
added a commit
that referenced
this pull request
Aug 20, 2026
* feat(nav): add "Get updates" newsletter entry to the header
Adds a "Get updates" nav item (mail icon) between "Get involved" and
Donate, pointing at /join for now. Rendered leading-icon on desktop
(WideNavbar) and trailing-icon on mobile (NarrowNavbar) so the label
lines up with the other items. Also centers the nav row (align-items)
so the taller item does not push the others up.
* feat(newsletter): route the homepage subscribe box to the /join movement signup
The homepage newsletter box posted straight to Substack, which is a
different thing from the CRM movement newsletter. Adds a joinHandoff mode
to NewsletterSignup that instead sends the visitor to /join with the email
prefilled (?subscribe-email=...), wiring OnboardingFlow to consume the
previously-no-op initialEmail. Collage/learn keep the Substack behaviour.
* copy(newsletter): relabel the homepage box for the /join handoff
Now that the box leads to the signup form, "Subscribe to our newsletter"
+ "Subscribe" over-promised a one-click action. Heading -> "Get updates"
(matches the nav), button -> "Sign up" (signals it continues to a form).
Scoped to the homepage instance; Substack uses keep their copy.
* feat: dedicated /subscribe landing for the newsletter entry points
Points "Get updates" (nav) and the homepage box at a new /subscribe post
instead of /join. It reuses the same OnboardingFlow but with a newsletter
title/subtitle and reads ?subscribe-email to prefill. Fixes the two
/join-inheritance issues: wrong hero copy, and the nav highlighting
"Get involved". Also gives "Get updates" its own active-state highlight.
NewsletterSignup.joinHandoff -> handoffHref (destination configurable).
* copy: /subscribe h1 -> "Get PauseAI updates"
* feat(subscribe): dedicated newsletter form with local-group + Substack opt-ins
Reworks /subscribe from reusing the full /join flow into a lightweight
single-step signup, per Jonathan's design review:
- SubscribeFlow: name/email/country/city, optional local-group and Substack
opt-ins, privacy-policy microcopy (no consent checkbox — signing up is the
consent)
- global newsletter by default; chapter sharing only on the explicit
local-group tick. Decouples chapter sharing from the privacy consent on the
subscribe path; the /join form still bundles the two, untouched.
- "do more" continues in place into the intent step instead of linking to
/join, carrying the created record id so it updates rather than creating a
duplicate
- the continuation hides the email opt-in cards and second consent, so it can
only add involvement, never re-ask or silently undo the subscribe-time
choices. Chapter sharing is preserved on update, escalating only for
Volunteer/Lead.
- rename the nav entry "Get updates" -> "Subscribe"
* fix(subscribe): address review findings on the do-more continuation
- The continuation posted through /join's step-2 form (no subscribe_form
marker), so its update hit the strict required-field validation and 400'd
for any subscriber who omitted an optional field (name/city/country) — the
common email-only case. Updates now skip the create-time required-field
check, since they patch an existing record.
- The continuation's step-2 Back button led to step 1 and its "just browse"
path, which had no record_id and so created a duplicate record with chapter
sharing forced true, overriding a declined opt-in. Hide Back on the
continuation so that path is unreachable.
- gdprConsent is no longer seeded true on the continuation (it was pre-checking
visible consent boxes on reachable sub-forms); instead the continuation's
step-2 submit is exempted from the consent gate, since consent was captured
on the subscribe form.
- SubscribeFlow's email prefill now applies once into an empty field, so a
prefilled address (?subscribe-email) can be cleared to fix a typo.
- Move the lookupChapter fetch into the stub branch, its only consumer; the
live path never read the result.
* copy: align homepage newsletter box heading to Subscribe
* fix(subscribe): stop form inputs overflowing the card on mobile
The .field inputs set width:100% with their own padding, and the site's
reset doesn't apply box-sizing to form controls, so on narrow screens the
inputs rendered content-box and spilled past the card's right edge. Add
box-sizing:border-box to the card and inputs, and tighten the card padding
on mobile.
* fix(subscribe): reframe the do-more step as its own "Get involved" flow
The "do more" continuation reused the /join flow but sat under the /subscribe
page's "Subscribe" heading and newsletter intro, and its stepper showed a
"✓ About you" step the subscriber never saw (the subscribe form). Both read as
off.
- Add a `showTitle` frontmatter flag (mirrors `showImage`) so a page can suppress
the template h1 and own its heading. subscribe.md sets it and moves the intro
into the component.
- SubscribeFlow now renders a phase-aware heading: "Subscribe" (+ intro) for the
signup, "Get involved" once they continue. No more newsletter framing over the
volunteer form.
- Drop the "About you" step from the stepper on the continuation, so it reads
Intent -> (Volunteer form ->) Confirmed.
* fix(subscribe): put opt-in labels beside the checkbox, not below
The global `form label { flex-direction: column }` rule made the .opt-in label
stack under the checkbox. Force flex-direction: row so the checkbox sits beside
its label.
* fix(subscribe): use 'chapter' wording and mark the required email field
- Match the /join form's wording ("your local PauseAI chapter") on the
local-updates opt-in, instead of the nav's "local group".
- Add the required-field star to the Email label, as /join step 1 does.
* feat(subscribe): require name, country and city like the /join form
Per review, the subscribe form now requires all four fields (name, email,
country, city) with the same required-field stars as /join step 1, rather than
the lighter email-only form. Server validation and the submit gate updated to
match; chapter sharing still keys off the explicit local-chapter tick.
* fix(nav): render Subscribe as a normal nav item to stop the header wrapping
The bespoke desktop treatment (a bare LinkWithoutIcon with inline-flex, custom
padding/negative margins and a larger font, plus align-items:center on
.nav-links) disturbed the header's flex layout enough that the logo wrapped
above the nav even with room to spare. Render it through the standard Navlink
like every other item and drop the custom styles. The mail icon stays on the
mobile panel; can revisit a desktop icon without the bespoke markup.
* fix(nav): give the header its own width so all six top-level items fit inline
The header was capped at --page-width (the 768px content column), so the sixth
top-level item ("Subscribe") pushed the nav links past the row and the logo
wrapped above them on every page, regardless of viewport width. Add a
--header-width (48rem) and apply it where the nav is width-capped: the hero
.menu-band nav, and the .wide-navbar inside the width-capped .layout grid on
other pages. The header now gets slightly more room than the content column, so
all six items sit inline beside the logo; genuinely narrow widths still center
the logo above the nav as before. Restores the desktop mail icon on Subscribe.
* fix(newsletter): make the handoff work without JS, and latch the /join prefill
- The homepage box's native form still posted straight to Substack, so a submit
before hydration (or with JS off) bypassed the signup flow entirely. When a
handoff target is set, the form's native action/method/field now mirror the
hydrated goto(), landing on /subscribe with the email prefilled.
- Apply the prefill latch to OnboardingFlow too: /join?subscribe-email=... could
not be cleared to fix a typo, because the effect restored it as soon as the
field went empty.
* fix(subscribe): review round 2 — hover contrast, consent restore, update hardening
- The Subscribe nav item turned brand-orange on hover, which is invisible on the
hero's orange band (the :hover rule outranked .inverted at equal specificity).
Go black on hover when inverted, matching .navlink.inverted:hover.
- Backing out of Volunteer/Lead in the "do more" step left "GDPR chapter share
permission" true for someone who had declined chapter updates on the subscribe
form. The continuation now reposts that signup-time choice, so a non-escalating
intent restores it; /join's own volunteer updates are unaffected.
- Updates only write name/country/city when non-empty, so a partial post can't
blank fields the create collected.
- Localize the homepage handoff target so a visitor on a localized homepage stays
in their locale instead of landing on the English page.
* style(subscribe): match the /join form width
The subscribe card was 560px against /join's 800px, so the card visibly jumped
width when the "do more" step swapped one for the other in place. Match the
width, and override the global 30rem form cap as OnboardingFlow does so the
fields fill the card instead of leaving the extra width empty.
* fix(subscribe): match /join's heading rhythm, and drop confirmations that misstate the signup
- The heading spacing was inverted relative to /join (19px under the title and
48px before the card, where /join has 48 and 19). Mirror the page template's
hgroup rhythm the page suppresses, so it now measures identically.
- The "do more" confirmation repeated "You're in, thanks for joining us" moments
after the subscribe thanks screen, and its opt-in confirmations claimed "we'll
connect you with your local PauseAI chapter" even for someone who declined the
chapter box on the subscribe form. Both are hidden on the continuation, where
those opt-ins were already made and confirmed.
* copy(subscribe): label the do-more step 'Continue', not 'Submit'
On the continuation the person has already signed up, so that step records their
intent and moves them on rather than submitting a signup. /join is unchanged.
* fix(subscribe): hide the second thank-you on the no-intent path too
The act-now path stopped repeating 'You're in, thanks for joining us' on the
continuation, but the no-intent confirmation still greeted them with 'You're
in.' moments after the subscribe thanks screen. Gate it the same way, and
correct a comment that misdescribed why the inverted hover rule wins.
* fix(subscribe): restart the flow when Subscribe is clicked from the page itself
- Clicking "Subscribe" in the nav while already on /subscribe did nothing: the
finished flow stayed on screen, so the link looked broken. It now returns to
the start, cleared — which also keeps a second signup from being one click
away from duplicating the last one. Only same-path navigations reset, so
arriving from the homepage box still prefills the handed-off email.
- Require an intent to continue on the "do more" step. It was clickable with
nothing selected (the newsletter opt-in is seeded on, which satisfied the
/join rule), submitting a no-op update and landing on a confirmation for a
choice never made.
* refactor(subscribe): keep the form fields in one object
The reset that runs when Subscribe is clicked from the page itself had to
re-list every field, so a new field would silently keep its value across a
restart — and that reset is what stops a stale signup being resubmitted. One
object with a blank factory means the two can't drift apart. Also drops an
unused honeypot binding: the field posts natively and nothing reads it.
* feat(subscribe): record which form a signup came from
/join and /subscribe ask for chapter consent differently, and once a row is
written nothing else says which form produced it — so the distinction has to be
recorded at create time or not at all. Give /subscribe its own Signup source.
Also stop writing Signup source on updates: it is provenance, and the volunteer
step carries no subscribe marker, so it would have rewritten a /subscribe row as
a /join one.
* fix(onboarding): don't let the anti-spam check block deploy previews
The Turnstile site key only allows the production hostname, so on a deploy
preview the widget refuses to render (Cloudflare error 110200) and no token can
ever exist. The submit button then stays disabled and the server 403s, which
makes the signup form untestable on every preview — the one place reviewers
click through it.
Gate the check on whether the submission actually writes. In stub mode there is
no record, no subscription and no mail to protect, so there is nothing for the
check to defend; live deploys verify exactly as before. The client mirrors the
same rule, and assumes live if it cannot reach the mode endpoint, so an
unanswered request keeps the check required rather than dropping it.
Deliberately not done: pointing previews at Cloudflare's test keys. The verifier
refuses those outside dev on purpose, because a test secret accepts every token
and would silently reopen the form to bots.
* review: make the liveness invariant structural, and document the flip
- Read isOnboardingLive() once and use it for both the check and the writes, so
'they cannot disagree' holds by construction rather than by relying on the
platform keeping env vars stable within a request.
- Only an explicit false relaxes the client-side check, so a malformed mode
response can't drop it by being falsy.
- join-form-flow.md said Turnstile runs regardless of mode, which is no longer
true, and now records that ONBOARDING_LIVE=true has to be scoped to the
Production context: set site-wide it makes previews live, which both brings
the untestable-preview problem back and lets a preview run write real data.
* fix(onboarding): don't render the widget when no token is required
In stub mode the check is skipped, but the widget still mounted — and on a
deploy preview it can't render, so reviewers saw 'The anti-spam check could not
load' next to a working, enabled button. Render it only when a token is actually
required.
* copy(subscribe): Jonathan's wording for the Substack opt-in and the homepage box
The homepage box carried the generic newsletter description, which described the
Substack it no longer signs people up for; it now has its own text.
* copy(subscribe): drop the chapter opt-in's sub-line
"We'll connect you with your local PauseAI chapter" promises contact that may
not happen: whether a chapter reaches out to a newsletter signup is the open
question in #1014. The label already says what the box does.
…critical-alert notice, None intent, de/fr strings, flow-doc sync
RisingOrange
marked this pull request as ready for review
August 20, 2026 17:48
…llback intent stays Keep informed
…oc lead-in precision
RisingOrange
marked this pull request as draft
August 20, 2026 18:17
…ing, None in the INTENTS bullet)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

For reviewers: the model in the "What each choice results in" table is decided (2026-08-20 onboarding meeting), so it needs no sign-off — it documents what each choice leads to. What still wants review eyes: the implementation itself, and the de/fr translations of the changed strings, which want a native-speaker read. To try the form live: open the preview (the reworked step is page 2).
What it changes (Step 2)
Mostly it untangles the current intent step rather than redesigning it. Two different kinds of choice are jumbled together there today:
The proposal cleans up the specifics:
What each choice results in
🔔 "Keep me informed" is an opt-in, independent of the intent choice. Ticking it adds the global newsletter (everyone) and chapter email (for non-volunteers). None + 🔔 off = critical-alerts only; None + 🔔 on = the "keep me informed" signup. Act now gets the same treatment as None here — it's a signup-experience distinction (they see the action cards), not a contact tier.
📍 asked separately on the subscribe form. Here on
/join, ticking 🔔 signs you up for global and chapter updates together — right for a form about joining the movement. The dedicated newsletter entry asks about chapter updates in its own checkbox instead, so someone who came only for the newsletter isn't handed to a chapter unless they say yes.‡ legitimate interest: Volunteers/Leads signed up to volunteer, so we can contact them about it without a 🔔 tick (the bell gates only their global newsletter; list-scoped unsubscribe still applies). Chapter email and personal outreach rest on that basis without a 🔔 tick, which is why the Volunteer and Lead options say chapter updates are included.
✓ yes · — no · "if 🔔" = only if Keep me informed is ticked · 📰 Substack is a separate-platform opt-in.
† one audience per chapter (decided 2026-08-20): chapter email is a single stream — informational updates and local calls to action go to the same recipients. The earlier draft split these into two columns; the split was decided against for now, to be revisited only if chapter-mail unsubscribe rates run high. Personal outreach is Volunteer/Lead only (same decision round).
Chapter-scope note: anyone who neither ticked 🔔 nor picked Volunteer or Lead is kept out of chapter scope entirely: a chapter never sees or contacts them, and they receive only Global's critical alerts. That covers both "None + no 🔔" and "Act now + no 🔔", since the table above gives neither cohort anything from their chapter, so including them in scope would mean a chapter sees people it can never email. Chapter scope = 🔔 ticked, or intent of Volunteer or Lead. (Adopted: PauseAI/pauseai-civicrm#139 resolved its chapter-scope question 2026-08-20 with exactly this rule, and the enforcement is built.)
Open questions about the table — all answered (meeting with Irina, 2026-08-20)
What this PR implements, and what lands elsewhere
This PR is only the form: the step-2 copy and layout, the submit gate, and the values written to Airtable. The rest of the decided model is implemented (or pending) in other systems:
Nonein the CRM's intent vocabulary — PauseAI/pauseai-civicrm#530, soft-ordered: the importer only warn-flags unknown values until it lands, and the Airtable select option self-creates on first write (typecast: true).So merging this PR changes what signups see and what their rows say, and nothing else by itself.
Green-lit in the 2026-08-20 onboarding meeting; being finished for merge (rebase over #1031's intent-step reuse, de/fr strings, docs/join-form-flow.md sync). The preview is safe to open: the form performs no Airtable write without an API key configured.