Skip to content

Untangle Step 2 of the signup form (opt-ins vs intent) - #1014

Draft
RisingOrange wants to merge 22 commits into
mainfrom
prototype/onboarding-v2-two-axis
Draft

Untangle Step 2 of the signup form (opt-ins vs intent)#1014
RisingOrange wants to merge 22 commits into
mainfrom
prototype/onboarding-v2-two-axis

Conversation

@RisingOrange

@RisingOrange RisingOrange commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

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).

Before — current formAfter — this proposal
Before — current Step 2 After — reworked Step 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:

  • Opt-ins — do you want our emails? The 🔔 "Keep me informed" and 📰 Substack checkboxes.
  • Intent — how active do you want to be? Act now / Volunteer / Lead.

The proposal cleans up the specifics:

  • 🔔 copy — "Get global campaign updates, plus news from your local chapter," replacing "connect me with my local chapter", which wrongly implied chapter contact was gated by the checkbox (it isn't — a volunteer who skips it is still reachable about their volunteering via legitimate interest).
  • 📰 Substack copy — "AI news from the PauseAI team's perspective," replacing "general news on AI, delivered via our Substack newsletter." The two boxes now read as clearly different things (🔔 = what PauseAI does; 📰 = news about AI, our take), rather than two vaguely-newsletter-ish checkboxes side by side.
  • The newsletter stays a genuine opt-in for everyone, including Volunteers/Leads — nothing pre-checked or forced (marketing consent must be freely given).
  • Critical-alert floor notice under the opt-ins (copy from Show the critical-alert notice on the signup form #1010, plus a list-unsubscribe reassurance).
  • No "tick at least one" requirement, so a critical-alerts-only sign-up is reachable — the state the notice's premise assumes.
  • Volunteer / Lead disclosure — both options now note "(Includes local chapter updates.)", so a volunteer is told up front they'll hear from their chapter regardless of the 🔔 box. This is the transparency that grounds the legitimate-interest basis (see the table's ‡ note).

What each choice results in

Intent (pick one, optional) Critical alerts Global newsletter Chapter email † Personal outreach
None if 🔔 if 🔔
Act now if 🔔 if 🔔
Volunteer if 🔔 ✓ ‡ ✓ ‡
Lead if 🔔 ✓ ‡ ✓ ‡ + leadership
None (via /subscribe) if 📍

🔔 "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)

  • Chapter email targeting / the split: no informational/CTA audience split for now. One audience per chapter; a bell-ticked signup gets everything the chapter sends, local CTAs included. Revisit if chapter-mail unsubscribe rates run high (visible per mailing in the chapter workspace report). Recorded on PauseAI/pauseai-civicrm#139.
  • Personal (1:1) outreach: Volunteer and Lead signups only — "Act now" is excluded. An onboarding-side policy Irina owns and is socializing with the onboarders; the Airtable alert automation is being updated to match.
  • Lead volunteer agreement: handled out of band by onboarding, so the form keeps ending the Lead path at the mailto hand-off.

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:

  • Chapter scope + critical-alerts tier + one chapter audience — already live in CiviCRM (enforcement per PauseAI/pauseai-civicrm#139); the table above describes that system, this PR doesn't build it.
  • Onboarder alert filtering + the outreach-policy copy in the alert emails — Airtable automation changes, pending a UI session (customScript nodes block the API). The alert emails also quote the bell's copy, which this PR changes, so that re-quote lands in the same session.
  • None in 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.

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.
@netlify

netlify Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy Preview for pauseai ready!

Name Link
🔨 Latest commit d0e127b
🔍 Latest deploy log https://app.netlify.com/projects/pauseai/deploys/6a875c6de70c0a000877b4fd
😎 Deploy Preview https://deploy-preview-1014--pauseai.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 49 (🔴 down 15 from production)
Accessibility: 91 (no change from production)
Best Practices: 100 (no change from production)
SEO: 82 (no change from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

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.
@RisingOrange RisingOrange changed the title Prototype: two-axis onboarding intent step (design review) Prototype: untangle Step 2 of the signup form (opt-ins vs involvement) Aug 1, 2026
@RisingOrange RisingOrange changed the title Prototype: untangle Step 2 of the signup form (opt-ins vs involvement) Prototype: untangle Step 2 of the signup form (opt-ins vs intent) Aug 1, 2026
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.
@RisingOrange RisingOrange changed the title Prototype: untangle Step 2 of the signup form (opt-ins vs intent) Untangle Step 2 of the signup form (opt-ins vs intent) Aug 20, 2026
@RisingOrange
RisingOrange marked this pull request as ready for review August 20, 2026 17:48
@RisingOrange
RisingOrange marked this pull request as draft August 20, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant