Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b4337aa
Add DONATION_SUPPORTER_MODE block setting and env var
conatus Mar 18, 2026
93c09a9
Supporter mode: reorder stages and update routing in app.tsx
conatus Mar 18, 2026
a09bf42
Supporter mode: add frequency/tier UI to donation page
conatus Mar 18, 2026
8819a38
Add donation support to Stripe subscription creation
conatus Mar 18, 2026
79cce9e
Add recurring donation support to GoCardless subscription amount
conatus Mar 18, 2026
92c58c7
Supporter mode: add frequency/tier UI to donation page
conatus Mar 18, 2026
90bc446
Add Jest test infrastructure and donation page tests
conatus Mar 18, 2026
831e69b
Remove skip button from supporter mode donation page
conatus Mar 18, 2026
3261ab7
Merge branch 'master' into feature/join-124-add-one-off-donation-func…
conatus Mar 18, 2026
d3745b0
Supporter mode: derive donation tiers from membership plans, same tie…
conatus Mar 18, 2026
f2427ed
Show configuration error in supporter mode when no membership plans a…
conatus Mar 18, 2026
69b72bb
Suppress global membership plan fallback in supporter mode
conatus Mar 18, 2026
5937afe
Remove em dash from supporter mode error message
conatus Mar 18, 2026
8a440e7
Add failing tests for recurDonation and donationAmount type correctne…
conatus Mar 23, 2026
841cc77
Fix recurDonation and donationAmount submitted as strings in supporte…
conatus Mar 23, 2026
9018643
Add failing tests for one-off toggle disabled state when Stripe is un…
conatus Mar 23, 2026
13461ff
Disable one-off donation toggle when Stripe is not available
conatus Mar 23, 2026
0301a88
Document one-off donation and plan requirements in supporter mode hel…
conatus Mar 23, 2026
eb38d25
Bump version to 1.4.0
conatus Mar 23, 2026
afee2e3
Correct changelog entry wording for 1.4.0
conatus Mar 23, 2026
e2a8690
Expand Direct Debit abbreviation in changelog
conatus Mar 23, 2026
77580fe
Merge master into feature branch
conatus Mar 23, 2026
f7083c1
Address PR review comments: remove stale comments
conatus Mar 23, 2026
57f5f92
Fix double subscription item in supporter mode
conatus Mar 23, 2026
042722e
Use 'Donation:' product prefix in Stripe when in supporter mode
conatus Mar 23, 2026
0d54bec
Make Mailchimp errors non-fatal
conatus Mar 23, 2026
a9e0fb7
Rename existing Stripe products to 'Donation:' prefix on first suppor…
conatus Mar 23, 2026
10d9b12
Note that ask_for_additional_donation has no effect in supporter mode
conatus Mar 23, 2026
bca459f
Use payment_method_types instead of automatic_payment_methods in crea…
conatus Mar 23, 2026
52f205e
Skip frequency suffix in billing summary for one-off supporter donations
conatus Mar 23, 2026
a92f853
Force paymentMethod to creditCard for one-off supporter donations
conatus Mar 23, 2026
74d4e4f
Add /stripe/create-payment-intent REST endpoint for one-off supporter…
conatus Mar 24, 2026
6aa4f58
Skip subscription validation for one-off supporter donations
conatus Mar 24, 2026
c49c678
Allow custom amounts in supporter mode regardless of plan allowCustom…
conatus Mar 24, 2026
dca36fc
Use generic Donation product for supporter mode custom amounts
conatus Mar 24, 2026
fdc3b18
Extract resolveSubscriptionPriceStrategy and add unit tests
conatus Mar 24, 2026
efb33bf
Add tests for supporter mode custom amount routing in DonationPage
conatus Mar 24, 2026
d65f6e4
Document that supporter mode custom amounts share one Stripe product
conatus Mar 24, 2026
27c3d46
Correct product-sharing note: applies to all donations, not just supp…
conatus Mar 24, 2026
418ff82
Use invoice + stored one-time price for all one-off donations
conatus Mar 24, 2026
bd0f0aa
Route one-off supporter donations through PaymentIntent in StripeForm
conatus Mar 24, 2026
525a165
Fix Invoice finalization: retrieve PaymentIntent separately after fin…
conatus Mar 24, 2026
fcc0434
Await elements.submit() and surface validation errors before server call
conatus Mar 24, 2026
6aa7953
Fix finalizeInvoice: call as instance method, surface real error mess…
conatus Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions packages/join-block/join.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* Plugin Name: Common Knowledge Join Flow
* Description: Common Knowledge join flow plugin.
* Version: 1.3.19
* Version: 1.4.0
* Author: Common Knowledge <hello@commonknowledge.coop>
* Text Domain: common-knowledge-join-flow
* License: GPLv2 or later
Expand Down Expand Up @@ -499,7 +499,14 @@
StripeService::initialise();
[$customer, $newCustomer] = StripeService::upsertCustomer($email);

$subscription = StripeService::createSubscription($customer, $plan, $data["customMembershipAmount"] ?? null);
$subscription = StripeService::createSubscription(
$customer,
$plan,
$data["customMembershipAmount"] ?? null,
$data["donationAmount"] ?? null,
$data["recurDonation"] ?? false,
!empty($data["donationSupporterMode"])
);

return $subscription;
} catch (\Exception $e) {
Expand All @@ -512,6 +519,49 @@
}
));

register_rest_route('join/v1', '/stripe/create-payment-intent', array(
'methods' => 'POST',
'permission_callback' => function ($req) {
return true;
},
'callback' => function (WP_REST_Request $request) {
global $joinBlockLog;

$email = "";
try {
$data = json_decode($request->get_body(), true);
$email = $data['email'];

$joinBlockLog->info("Received /stripe/create-payment-intent request for $email");

$plan = Settings::getMembershipPlan($data['membership'] ?? '');
if (!$plan) {
return new WP_REST_Response(['error' => 'Invalid membership plan'], 400);
}

$amount = $data['donationAmount'] ?? 0;
$currency = $plan['currency'] ?? 'GBP';

StripeService::initialise();
[$customer] = StripeService::upsertCustomer($email);

$paymentIntent = StripeService::createPaymentIntent($customer, $amount, $currency);

return new WP_REST_Response([
'id' => $paymentIntent->id,
'client_secret' => $paymentIntent->client_secret,
'customer' => $customer->id,
], 200);
} catch (\Exception $e) {
$joinBlockLog->error(
'Failed to create Stripe PaymentIntent for user ' . $email . ': ' . $e->getMessage(),
['error' => $e]
);
throw $e;
}
}
));

register_rest_route('join/v1', '/stripe/create-confirm-subscription', array(
'methods' => 'POST',
'permission_callback' => function ($req) {
Expand Down Expand Up @@ -541,7 +591,14 @@
StripeService::initialise();
[$customer, $newCustomer] = StripeService::upsertCustomer($email);

$subscription = StripeService::createSubscription($customer, $plan);
$subscription = StripeService::createSubscription(
$customer,
$plan,
$data["customMembershipAmount"] ?? null,
$data["donationAmount"] ?? null,
$data["recurDonation"] ?? false,
!empty($data["donationSupporterMode"])
);

$confirmedPaymentIntent = StripeService::confirmSubscriptionPaymentIntent($subscription, $data['confirmationTokenId']);

Expand Down
9 changes: 7 additions & 2 deletions packages/join-block/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Tags: membership, subscription, join
Contributors: commonknowledgecoop
Requires at least: 5.4
Tested up to: 6.8
Stable tag: 1.3.19
Stable tag: 1.4.0
Requires PHP: 8.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Expand Down Expand Up @@ -107,6 +107,12 @@ Need help? Contact us at [hello@commonknowledge.coop](mailto:hello@commonknowled

== Changelog ==

= 1.4.0 =
* Add Donation Supporter Mode: a new block setting that puts donation first, before personal details and payment, skipping the membership plan step entirely
* Supporter mode: donation frequency (monthly/one-off) and tier selection driven by block-level membership plans
* Supporter mode: one-off donations processed via Stripe invoice item; recurring donations added as a second subscription item
* Supporter mode: GoCardless recurring donations added to the Direct Debit subscription total; one-off toggle disabled with explanation when Stripe is unavailable
* Supporter mode: donation amount and frequency submitted as correct native types to the payment backend
= 1.3.19 =
* Sync membership tier tag changes to Zetkin and Mailchimp when a member upgrades or downgrades their Stripe subscription plan.
* Tags shared across tiers (e.g. `member`) are preserved during a tier change and never incorrectly removed.
Expand All @@ -118,7 +124,6 @@ Need help? Contact us at [hello@commonknowledge.coop](mailto:hello@commonknowled
* Added `ck_join_flow_should_unlapse_member` filter to control whether an unlapse should proceed.
* Added `ck_join_flow_member_lapsed` action fired after a member is successfully lapsed.
* Added `ck_join_flow_member_unlapsed` action fired after a member is successfully unlapsed.

= 1.3.18 =
* Make Zetkin errors non-fatal so a Zetkin failure does not block a successful join.
* Improve Zetkin 403 error message to indicate expired JWT credentials and remediation steps.
Expand Down
14 changes: 12 additions & 2 deletions packages/join-block/src/Blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,15 @@ private static function registerJoinFormBlock()
Field::make('checkbox', 'hide_address')
->set_help_text('Check to completely hide the address section from the form.'),
Field::make('checkbox', 'require_phone_number')->set_default_value(true),
Field::make('checkbox', 'ask_for_additional_donation'),
Field::make('checkbox', 'ask_for_additional_donation')
->set_help_text('Has no effect when Donation Supporter Mode is enabled.'),
Field::make('checkbox', 'donation_supporter_mode')
->set_help_text(
'Enable Supporter Mode: shows donation frequency and amount first, ' .
'before personal details and payment. Skips the membership plan step. ' .
'Requires block-level membership plans to be configured (used as donation tiers). ' .
'One-off donations require Stripe. They are not available with Direct Debit only.'
),
Field::make('checkbox', 'hide_home_address_copy')
->set_help_text('Check to hide the copy that explains why the address is collected.'),
Field::make('checkbox', 'include_skip_payment_button')
Expand Down Expand Up @@ -327,7 +335,8 @@ private static function echoEnvironment($fields, $block_mode)
}

$membership_plans = $fields['custom_membership_plans'] ?? [];
if (!$membership_plans) {
$is_supporter_mode = !empty($fields['donation_supporter_mode']);
if (!$membership_plans && !$is_supporter_mode) {
$membership_plans = Settings::get("MEMBERSHIP_PLANS") ?? [];
}

Expand Down Expand Up @@ -414,6 +423,7 @@ function ($o) {
'ABOUT_YOU_COPY' => wpautop(Settings::get("ABOUT_YOU_COPY")),
'ABOUT_YOU_HEADING' => Settings::get("ABOUT_YOU_HEADING"),
"ASK_FOR_ADDITIONAL_DONATION" => $fields['ask_for_additional_donation'] ?? false,
"DONATION_SUPPORTER_MODE" => $fields['donation_supporter_mode'] ?? false,
'CHARGEBEE_SITE_NAME' => Settings::get('CHARGEBEE_SITE_NAME'),
"CHARGEBEE_API_PUBLISHABLE_KEY" => Settings::get('CHARGEBEE_API_PUBLISHABLE_KEY'),
"COLLECT_COUNTY" => Settings::get("COLLECT_COUNTY"),
Expand Down
6 changes: 6 additions & 0 deletions packages/join-block/src/Services/GocardlessService.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ public static function createCustomerSubscription($data)
}
$amountInPence = round(((float) $data['membershipPlan']['amount']) * 100);

// Add recurring donation to subscription amount.
$donationAmount = (float) ($data['donationAmount'] ?? 0);
if ($donationAmount > 0 && !empty($data['recurDonation'])) {
$amountInPence += round($donationAmount * 100);
}

$subscriptions = $client->subscriptions()->list([
"params" => ["mandate" => $mandate->id]
]);
Expand Down
8 changes: 5 additions & 3 deletions packages/join-block/src/Services/JoinService.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ private static function tryHandleJoin($data)
$data['gocardlessMandate'] = $subscription ? $subscription->links->mandate : null;
$data['gocardlessCustomer'] = $subscription ? $subscription->links->customer : null;

if (Settings::get("USE_STRIPE")) {
$isOneOffSupporterDonation = !empty($data["donationSupporterMode"]) && empty($data["recurDonation"]);

if (Settings::get("USE_STRIPE") && !$isOneOffSupporterDonation) {
StripeService::initialise();
$subscriptionInfo = StripeService::removeExistingSubscriptions($data["email"], $data["stripeCustomerId"] ?? null, $data["stripeSubscriptionId"] ?? null);
if ($subscriptionInfo["amount"] !== $membershipAmount) {
Expand Down Expand Up @@ -230,8 +232,9 @@ private static function tryHandleJoin($data)
MailchimpService::signup($data);
$joinBlockLog->info("Completed Mailchimp signup request for $email");
} catch (\Exception $exception) {
// A Mailchimp failure should not block a successful join.
// The member record can be retro-added to Mailchimp once the underlying issue is resolved.
$joinBlockLog->error("Mailchimp error for email $email: " . $exception->getMessage());
throw $exception;
}
}

Expand Down Expand Up @@ -398,7 +401,6 @@ public static function toggleMemberLapsed($email, $lapsed = true, $paymentDate =
$joinBlockLog->info("$done member $email as lapsed in Mailchimp");
} catch (\Exception $exception) {
$joinBlockLog->error("Mailchimp error for email $email: " . $exception->getMessage());
throw $exception;
}
}

Expand Down
Loading