Skip to content

JOIN-124 Add Supporter Mode to donation form#59

Open
conatus wants to merge 39 commits intomasterfrom
feature/join-124-add-one-off-donation-functionality-to-supporter-form
Open

JOIN-124 Add Supporter Mode to donation form#59
conatus wants to merge 39 commits intomasterfrom
feature/join-124-add-one-off-donation-functionality-to-supporter-form

Conversation

@conatus
Copy link
Member

@conatus conatus commented Mar 18, 2026

Summary

Adds a new Donation Supporter Mode checkbox to the CK Join Form block. When enabled, donation becomes the primary purpose of the form rather than a bolt-on upsell after membership selection.

  • Donation step (with monthly/one-off frequency toggle) comes first, before personal details and payment
  • Membership plan step is skipped entirely
  • Donation tiers are driven by the membership plans configured on the block itself, not global settings. If none are configured, a clear error message is shown
  • Recurring donations use the membership plan price directly as a Stripe subscription (the plan IS the donation)
  • One-off donations processed via a standalone Stripe PaymentIntent — no subscription created
  • One-off toggle disabled with explanation when Stripe card payments are unavailable (Direct Debit only)
  • Mailchimp errors are non-fatal (matching the existing Zetkin behaviour)
  • Existing join flow is completely unchanged when the setting is off

Prerequisites

  • WordPress site with the plugin active
  • At least one CK Join Form block on a page
  • Stripe configured (for card and one-off tests)
  • GoCardless configured (for Direct Debit tests)
  • Test cards and bank details to hand

Test plan

Automated checks

  • cd packages/join-flow && npm test — all 18 tests pass
  • npm run build — no TypeScript errors

1. Standard join flow (no upsell, no supporter mode)

Config: ASK_FOR_ADDITIONAL_DONATION off, DONATION_SUPPORTER_MODE off, Stripe enabled

  • Complete details → plan → payment (card) → confirm
  • Stripe dashboard shows one subscription item: Membership: <plan name>
  • No donation line on the invoice
  • Subsequent invoices show membership amount only

2. Standard join — upsell, one-off donation

Config: ASK_FOR_ADDITIONAL_DONATION on, DONATION_SUPPORTER_MODE off

  • After plan page, donation upsell page appears
  • Select a donation amount, leave "Make this donation recurring" unticked
  • Complete payment (card)
  • Stripe dashboard: first invoice = membership + donation amount
  • Subsequent invoices = membership only (donation does not recur)
  • "Not right now" skips donation and proceeds to payment with membership only

3. Standard join — upsell, recurring donation

Config: ASK_FOR_ADDITIONAL_DONATION on, DONATION_SUPPORTER_MODE off

  • Select a donation amount, tick "Make this donation recurring"
  • Complete payment (card)
  • Stripe dashboard: first invoice = membership + donation amount
  • Subsequent invoices = membership + donation amount (both recur)

4. Standard join — both flags on (ASK_FOR_ADDITIONAL_DONATION + DONATION_SUPPORTER_MODE)

Config: Both on

  • Supporter mode takes precedence — donation page is shown first (supporter layout)
  • No duplicate items in Stripe

5. Supporter mode — monthly, card

Config: DONATION_SUPPORTER_MODE on, Stripe card enabled, membership plans configured as donation tiers

  • First page is donation tier selector with Monthly / One-off toggle
  • Monthly is selected by default
  • Select a preset tier
  • Billing summary shows "Credit or Debit Card, monthly"
  • Complete payment
  • Stripe dashboard: single subscription item Donation: <tier name> at correct amount
  • No second "Membership:" or "Supporter Donation" line item
  • Subsequent invoices = same amount (recurring)

6. Supporter mode — monthly, Direct Debit

Config: DONATION_SUPPORTER_MODE on, STRIPE_DIRECT_DEBIT on

  • Monthly tab selected, complete with Bacs Direct Debit
  • Stripe dashboard: Donation: <tier name> subscription via BACS
  • No duplicate items

7. Supporter mode — monthly, custom amount

Config: DONATION_SUPPORTER_MODE on, at least one plan with allow_custom_amount

  • Enter a custom amount in the "Or enter another amount" field
  • CTA updates to reflect the custom amount
  • Complete monthly payment
  • Stripe dashboard: correct custom amount charged, price added to Supporter Donation

8. Supporter mode — one-off, card

Config: DONATION_SUPPORTER_MODE on, Stripe card enabled, STRIPE_DIRECT_DEBIT_ONLY off

  • Select One-off tab — button is enabled
  • Select a tier
  • CTA reads "Donate £X now"
  • Billing summary shows "Credit or Debit Card" with no "monthly" suffix
  • Complete payment with card
  • Stripe dashboard: no subscription created — one-time PaymentIntent only
  • Payment shows as a one-off charge, not recurring

9. Supporter mode — one-off disabled when Direct Debit only

Config: DONATION_SUPPORTER_MODE on, STRIPE_DIRECT_DEBIT_ONLY on

  • One-off tab is disabled/greyed out
  • Note shown: "One-off donations are not available with Direct Debit..."
  • Monthly tab still works and completes successfully

10. Supporter mode — no plans configured

Config: DONATION_SUPPORTER_MODE on, no membership plans on the block

  • Form shows: "No donation amounts configured" warning
  • No tier buttons or toggle rendered

11. Free membership (zero-price plan)

Config: Plan with amount = 0, no upsell

  • Payment stages are skipped entirely
  • Goes straight from plan → confirm
  • No Stripe customer or subscription created

12. Mailchimp errors are non-fatal

Config: Mailchimp enabled, use an address that triggers a Mailchimp error (e.g. someone@example.com)

  • Join completes successfully despite Mailchimp error
  • Error is logged but not shown to user
  • Other integrations (Zetkin etc.) proceed normally

13. Product naming

  • Standard join: Stripe product named Membership: <plan label>
  • Supporter mode monthly: Stripe product named Donation: <plan label>
  • Supporter mode one-off: no subscription product — PaymentIntent only, no product line

@linear
Copy link

linear bot commented Mar 18, 2026

@conatus conatus changed the title Add Supporter Mode to donation form JOIN-124 Add Supporter Mode to donation form Mar 19, 2026
conatus added 9 commits March 23, 2026 15:31
…ss in supporter mode

Tests verify that onCompleted receives native boolean for recurDonation and native
number for donationAmount — not the strings produced by the hidden input approach.
…r mode

Hidden inputs registered via ref produced string values ("true"/"false", "5") rather
than native booleans and numbers. Removed the hidden inputs; handleSubmit now derives
recurDonation (boolean) from isMonthly state and donationAmount (number) from
selectedTier/otherDonationAmount directly — matching the wire format used everywhere
else in the codebase.
Direct Debit (GoCardless) does not support one-off donations. When USE_STRIPE is
false, the One-off button is disabled and an inline note explains the limitation.
Copy link
Member Author

@conatus conatus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments, but mostly looks good.

conatus added 5 commits March 23, 2026 20:35
In supporter mode, donationAmount was passed non-zero to createSubscription,
which added a separate Supporter Donation item on top of the membership plan item.
The plan price IS the donation — no separate item should be created.

Also fixes membership not updating when the user selects a tier that differs
from the default: now resolves to the plan whose amount matches the selected tier.
Threads donationSupporterMode flag from frontend initial state through to
StripeService::getOrCreateProductForMembershipTier, which uses 'Donation:'
as the product name prefix instead of 'Membership:' when in supporter mode.
A Mailchimp failure should not block a successful join, matching the same
behaviour applied to Zetkin in 1.3.18. The member record can be retro-added
to Mailchimp once the underlying issue is resolved.
…ter mode subscription

Products are created during plan save without supporter mode context, so they
get 'Membership:' prefix. On first subscription creation in supporter mode,
trigger getOrCreateProductForMembershipTier to detect the name mismatch and
rename the product to 'Donation: <label>'.
conatus added 4 commits March 23, 2026 21:58
…tePaymentIntent

automatic_payment_methods conflicts with explicit paymentMethodTypes set in
Stripe Elements, causing confirmation to fail with an invalid_request_error.
One-off payments require card. Without this, a stale directDebit value in
session storage causes provider detection to fail in PaymentDetailsPage.

Also disable the One-off button when STRIPE_DIRECT_DEBIT_ONLY is set,
as card payments are unavailable in that configuration.
@conatus conatus force-pushed the feature/join-124-add-one-off-donation-functionality-to-supporter-form branch from 155bde2 to a92f853 Compare March 24, 2026 10:59
conatus added 3 commits March 24, 2026 11:02
One-off supporter donations go through a PaymentIntent, not a subscription,
so removeExistingSubscriptions and the amount comparison must be bypassed.
…Amount flag

In supporter mode the custom amount field is always meaningful, so remove the
allowCustomAmount gate on both the frontend (donation.page.tsx) and the backend
(StripeService::createSubscription) to ensure the entered amount flows through
to the Stripe price.
@conatus
Copy link
Member Author

conatus commented Mar 24, 2026

Doing pretty well with the testing, but Scenario 7, which is "Supporter mode — monthly, custom amount", is breaking, so looking into this.

conatus added 5 commits March 24, 2026 11:29
Custom amounts in supporter mode were creating prices under the base plan's
product (e.g. "Donation: Tenner a month") regardless of the amount entered.
Use getOrCreateDonationProduct() instead so custom amounts are grouped under
"Supporter Donation", keeping plan-named products for fixed tier amounts only.
Pure helper makes the custom-amount routing logic (supporter vs plan product,
bypass of allow_custom_amount gate) independently testable without Stripe API
calls. Seven assertions cover all branching paths.
Covers the bugs where custom amounts were silently dropped: verifies
customMembershipAmount is forwarded for non-tier amounts, donationAmount is
set correctly for one-off, paymentMethod is forced to creditCard on one-off,
and tier-matched plans do not set customMembershipAmount.
@conatus
Copy link
Member Author

conatus commented Mar 24, 2026

Got as far as Scenario 8.

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