Skip to content

Commit b2aa8d0

Browse files
authored
Merge pull request #62 from commonknowledge/feature/join-142-allow-cards-on-certain-forms
JOIN-142 Allow cards on individual forms when Direct Debit Only is set globally
2 parents 662da56 + 3c1229f commit b2aa8d0

7 files changed

Lines changed: 218 additions & 52 deletions

File tree

packages/join-block/join.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* Plugin Name: Common Knowledge Join Flow
55
* Description: Common Knowledge join flow plugin.
6-
* Version: 1.4.0
6+
* Version: 1.4.1
77
* Author: Common Knowledge <hello@commonknowledge.coop>
88
* Text Domain: common-knowledge-join-flow
99
* License: GPLv2 or later

packages/join-block/readme.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Tags: membership, subscription, join
44
Contributors: commonknowledgecoop
55
Requires at least: 5.4
66
Tested up to: 6.8
7-
Stable tag: 1.4.0
7+
Stable tag: 1.4.1
88
Requires PHP: 8.1
99
License: GPLv2 or later
1010
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -107,6 +107,8 @@ Need help? Contact us at [hello@commonknowledge.coop](mailto:hello@commonknowled
107107

108108
== Changelog ==
109109

110+
= 1.4.1 =
111+
* Allow cards on individual forms when Direct Debit Only is set globally
110112
= 1.4.0 =
111113
* Add Donation Supporter Mode: a new block setting that puts donation first, before personal details and payment, skipping the membership plan step entirely
112114
* Supporter mode: donation frequency (monthly/one-off) and tier selection driven by block-level membership plans

packages/join-block/src/Blocks.php

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -168,49 +168,58 @@ private static function registerJoinFormBlock()
168168

169169
$custom_fields = self::createCustomFieldsField();
170170

171+
$block_fields = [
172+
Field::make('separator', 'ck_join_form', 'CK Join Form'),
173+
$joined_page_association,
174+
Field::make('checkbox', 'require_address')->set_default_value(true),
175+
Field::make('checkbox', 'hide_address')
176+
->set_help_text('Check to completely hide the address section from the form.'),
177+
Field::make('checkbox', 'require_phone_number')->set_default_value(true),
178+
Field::make('checkbox', 'ask_for_additional_donation')
179+
->set_help_text('Has no effect when Donation Supporter Mode is enabled.'),
180+
Field::make('checkbox', 'donation_supporter_mode')
181+
->set_help_text(
182+
'Enable Supporter Mode: shows donation frequency and amount first, ' .
183+
'before personal details and payment. Skips the membership plan step. ' .
184+
'Requires block-level membership plans to be configured (used as donation tiers). ' .
185+
'One-off donations require Stripe. They are not available with Direct Debit only.'
186+
),
187+
Field::make('checkbox', 'hide_home_address_copy')
188+
->set_help_text('Check to hide the copy that explains why the address is collected.'),
189+
Field::make('checkbox', 'include_skip_payment_button')
190+
->set_help_text(
191+
'Check to include an additional button on the first page to skip to the thank you page ' .
192+
'(which could include a form for additional questions)'
193+
),
194+
Field::make('checkbox', 'is_update_flow', 'Is Update Flow (e.g. for existing members)')
195+
->set_help_text(
196+
'Check to skip collecting member details (e.g. name, address). If checked, this page must ' .
197+
'be linked to with the email URL search parameter set, e.g. /become-paid-member/?email=someone@example.com. ' .
198+
'This can be achieved by using the CK Join Form Link block on a landing page, and linking to this page.'
199+
),
200+
$custom_fields,
201+
$custom_membership_plans,
202+
Field::make('text', 'custom_webhook_url')
203+
->set_help_text('Leave blank to use the default Join Complete webhook from the settings page.'),
204+
Field::make('text', 'custom_sidebar_heading')
205+
->set_help_text('Leave blank to use the default from settings page.'),
206+
Field::make('text', 'custom_membership_stage_label')
207+
->set_help_text('Leave blank to use the default from settings page.'),
208+
Field::make('text', 'custom_joining_verb')
209+
->set_help_text('Leave blank to use the default from settings page (e.g., "Joining").'),
210+
];
211+
212+
if (Settings::get("STRIPE_DIRECT_DEBIT_ONLY")) {
213+
$block_fields[] = Field::make('checkbox', 'allow_cards_override')
214+
->set_help_text(
215+
'Override the global "Direct Debit Only" setting for this block, ' .
216+
'allowing card payments alongside Direct Debit.'
217+
);
218+
}
219+
171220
/** @var Block_Container $join_form_block */
172221
$join_form_block = Block::make(__('CK Join Form', 'common-knowledge-join-flow'))
173-
->add_fields(array(
174-
Field::make('separator', 'ck_join_form', 'CK Join Form'),
175-
$joined_page_association,
176-
Field::make('checkbox', 'require_address')->set_default_value(true),
177-
Field::make('checkbox', 'hide_address')
178-
->set_help_text('Check to completely hide the address section from the form.'),
179-
Field::make('checkbox', 'require_phone_number')->set_default_value(true),
180-
Field::make('checkbox', 'ask_for_additional_donation')
181-
->set_help_text('Has no effect when Donation Supporter Mode is enabled.'),
182-
Field::make('checkbox', 'donation_supporter_mode')
183-
->set_help_text(
184-
'Enable Supporter Mode: shows donation frequency and amount first, ' .
185-
'before personal details and payment. Skips the membership plan step. ' .
186-
'Requires block-level membership plans to be configured (used as donation tiers). ' .
187-
'One-off donations require Stripe. They are not available with Direct Debit only.'
188-
),
189-
Field::make('checkbox', 'hide_home_address_copy')
190-
->set_help_text('Check to hide the copy that explains why the address is collected.'),
191-
Field::make('checkbox', 'include_skip_payment_button')
192-
->set_help_text(
193-
'Check to include an additional button on the first page to skip to the thank you page ' .
194-
'(which could include a form for additional questions)'
195-
),
196-
Field::make('checkbox', 'is_update_flow', 'Is Update Flow (e.g. for existing members)')
197-
->set_help_text(
198-
'Check to skip collecting member details (e.g. name, address). If checked, this page must ' .
199-
'be linked to with the email URL search parameter set, e.g. /become-paid-member/?email=someone@example.com. ' .
200-
'This can be achieved by using the CK Join Form Link block on a landing page, and linking to this page.'
201-
),
202-
$custom_fields,
203-
$custom_membership_plans,
204-
Field::make('text', 'custom_webhook_url')
205-
->set_help_text('Leave blank to use the default Join Complete webhook from the settings page.'),
206-
Field::make('text', 'custom_sidebar_heading')
207-
->set_help_text('Leave blank to use the default from settings page.'),
208-
Field::make('text', 'custom_membership_stage_label')
209-
->set_help_text('Leave blank to use the default from settings page.'),
210-
Field::make('text', 'custom_joining_verb')
211-
->set_help_text('Leave blank to use the default from settings page (e.g., "Joining").'),
212-
213-
));
222+
->add_fields($block_fields);
214223
$join_form_block->set_render_callback(function ($fields, $attributes, $inner_blocks) {
215224
self::enqueueBlockCss();
216225
self::echoEnvironment($fields, self::NORMAL_BLOCK_MODE);
@@ -463,7 +472,9 @@ function ($o) {
463472
"REQUIRE_PHONE_NUMBER" => $fields["require_phone_number"] ?? true,
464473
"SENTRY_DSN" => Settings::get("SENTRY_DSN"),
465474
"STRIPE_DIRECT_DEBIT" => Settings::get("STRIPE_DIRECT_DEBIT"),
466-
"STRIPE_DIRECT_DEBIT_ONLY" => Settings::get("STRIPE_DIRECT_DEBIT_ONLY"),
475+
"STRIPE_DIRECT_DEBIT_ONLY" => !empty($fields['allow_cards_override'])
476+
? false
477+
: Settings::get("STRIPE_DIRECT_DEBIT_ONLY"),
467478
"STRIPE_PUBLISHABLE_KEY" => Settings::get("STRIPE_PUBLISHABLE_KEY"),
468479
"SUBSCRIPTION_DAY_OF_MONTH_COPY" => Settings::get("SUBSCRIPTION_DAY_OF_MONTH_COPY"),
469480
"USE_CHARGEBEE" => Settings::get("USE_CHARGEBEE"),

packages/join-e2e/scripts/setup.php

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,18 +212,41 @@ function ck_e2e_upsert_page(string $slug, string $title, string $content): int
212212
ck_e2e_make_block_content([], ['donation_supporter_mode' => true])
213213
);
214214

215+
// Supporter mode page with allow_cards_override enabled.
216+
// Used to test that a per-block override of the global STRIPE_DIRECT_DEBIT_ONLY
217+
// setting is reflected in the env JSON emitted by the PHP render callback.
218+
$allow_cards_override_page_id = ck_e2e_upsert_page(
219+
'e2e-allow-cards-override-supporter',
220+
'E2E Allow Cards Override Test',
221+
ck_e2e_make_block_content($supporter_plans, [
222+
'donation_supporter_mode' => true,
223+
'allow_cards_override' => true,
224+
])
225+
);
226+
227+
// Enable STRIPE_DIRECT_DEBIT_ONLY globally so that allow_cards_override has
228+
// something to override. Without this the global default is false and the
229+
// override would produce no observable difference.
230+
// NOTE: this is a persistent global side-effect. Any future spec that relies
231+
// on STRIPE_DIRECT_DEBIT_ONLY=false without injecting it via injectEnvOverrides
232+
// will unexpectedly receive true. Always inject the value explicitly in specs
233+
// that care about it.
234+
carbon_set_theme_option('stripe_direct_debit_only', true);
235+
215236
// Persist URLs as options so get-page-url.sh can retrieve them.
216237
update_option('ck_e2e_standard_page_url', get_permalink($standard_page_id));
217238
update_option('ck_e2e_free_page_url', get_permalink($free_page_id));
218239
update_option('ck_e2e_donation_upsell_page_url', get_permalink($donation_upsell_page_id));
219240
update_option('ck_e2e_supporter_page_url', get_permalink($supporter_page_id));
220241
update_option('ck_e2e_supporter_custom_page_url', get_permalink($supporter_custom_page_id));
221242
update_option('ck_e2e_supporter_no_plans_page_url', get_permalink($supporter_no_plans_page_id));
222-
223-
echo 'Standard page URL: ' . get_permalink($standard_page_id) . "\n";
224-
echo 'Free page URL: ' . get_permalink($free_page_id) . "\n";
225-
echo 'Donation upsell page URL: ' . get_permalink($donation_upsell_page_id) . "\n";
226-
echo 'Supporter page URL: ' . get_permalink($supporter_page_id) . "\n";
227-
echo 'Supporter custom page URL: ' . get_permalink($supporter_custom_page_id) . "\n";
228-
echo 'Supporter no-plans page URL: ' . get_permalink($supporter_no_plans_page_id) . "\n";
243+
update_option('ck_e2e_allow_cards_override_page_url', get_permalink($allow_cards_override_page_id));
244+
245+
echo 'Standard page URL: ' . get_permalink($standard_page_id) . "\n";
246+
echo 'Free page URL: ' . get_permalink($free_page_id) . "\n";
247+
echo 'Donation upsell page URL: ' . get_permalink($donation_upsell_page_id) . "\n";
248+
echo 'Supporter page URL: ' . get_permalink($supporter_page_id) . "\n";
249+
echo 'Supporter custom page URL: ' . get_permalink($supporter_custom_page_id) . "\n";
250+
echo 'Supporter no-plans page URL: ' . get_permalink($supporter_no_plans_page_id) . "\n";
251+
echo 'Allow cards override page URL: ' . get_permalink($allow_cards_override_page_id) . "\n";
229252
echo "Setup complete.\n";
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { test, expect } from '@playwright/test';
2+
import { mockRestEndpoints, captureJoinBodyViaStripeRedirect, injectEnvOverrides, CONTINUE } from './helpers';
3+
4+
/**
5+
* allow_cards_override — per-block card payment override
6+
*
7+
* When the global "Direct Debit Only" Stripe setting is enabled
8+
* (STRIPE_DIRECT_DEBIT_ONLY=true), the block editor exposes an
9+
* "allow_cards_override" checkbox. Checking it causes the PHP render
10+
* callback to emit STRIPE_DIRECT_DEBIT_ONLY: false in the page's env JSON,
11+
* re-enabling card (Stripe Elements) payments for that specific form instance.
12+
*
13+
* The seed script (setup.php) sets STRIPE_DIRECT_DEBIT_ONLY=true globally via
14+
* carbon_set_theme_option and creates:
15+
* - e2e-allow-cards-override-supporter: supporter mode + allow_cards_override=true
16+
* - e2e-supporter: supporter mode, no override (global DD-only applies)
17+
*
18+
* Tests verify:
19+
* 1. The PHP env script output is correct (full-stack, no injectEnvOverrides).
20+
* 2. The one-off tab is enabled on the override page and disabled without it.
21+
* 3. The /join request body for a one-off donation on the override page is correct.
22+
*
23+
* For behavioural tests, USE_STRIPE=true is injected (a global plugin setting
24+
* unavailable in the test environment) but STRIPE_DIRECT_DEBIT_ONLY is NOT
25+
* injected — it is left to the real PHP output to supply the value, which is
26+
* what these tests are exercising.
27+
*/
28+
29+
const OVERRIDE_PAGE = '/e2e-allow-cards-override-supporter/';
30+
const SUPPORTER_PAGE = '/e2e-supporter/';
31+
32+
// ---------------------------------------------------------------------------
33+
// PHP env script output — no injectEnvOverrides, reads raw PHP output
34+
// ---------------------------------------------------------------------------
35+
36+
test.describe('PHP env script: allow_cards_override overrides STRIPE_DIRECT_DEBIT_ONLY', () => {
37+
test('page with allow_cards_override emits STRIPE_DIRECT_DEBIT_ONLY: false', async ({ page }) => {
38+
await page.goto(OVERRIDE_PAGE);
39+
const envJson = await page.locator('script#env').textContent();
40+
const env = JSON.parse(envJson || '{}');
41+
// PHP must have emitted false (not the global true) because allow_cards_override=true.
42+
expect(env.STRIPE_DIRECT_DEBIT_ONLY).toBe(false);
43+
});
44+
45+
test('page without allow_cards_override emits STRIPE_DIRECT_DEBIT_ONLY: true (global applies)', async ({ page }) => {
46+
await page.goto(SUPPORTER_PAGE);
47+
const envJson = await page.locator('script#env').textContent();
48+
const env = JSON.parse(envJson || '{}');
49+
// No override — the global STRIPE_DIRECT_DEBIT_ONLY=true must be present.
50+
expect(env.STRIPE_DIRECT_DEBIT_ONLY).toBe(true);
51+
});
52+
});
53+
54+
// ---------------------------------------------------------------------------
55+
// One-off tab availability — STRIPE_DIRECT_DEBIT_ONLY supplied by real PHP
56+
// ---------------------------------------------------------------------------
57+
58+
test.describe('One-off tab enabled on override page, disabled without it', () => {
59+
test('one-off tab is enabled on page with allow_cards_override', async ({ page }) => {
60+
// Inject USE_STRIPE=true (global plugin setting not available in test env).
61+
// STRIPE_DIRECT_DEBIT_ONLY is intentionally NOT injected — it comes from PHP.
62+
await injectEnvOverrides(page, `**${OVERRIDE_PAGE}`, { USE_STRIPE: true });
63+
await mockRestEndpoints(page);
64+
await page.goto(OVERRIDE_PAGE);
65+
await page.waitForSelector('h2:has-text("Support us")');
66+
67+
const oneOffBtn = page.locator('.btn-group button:has-text("One-off")');
68+
await expect(oneOffBtn).toBeVisible();
69+
await expect(oneOffBtn).toBeEnabled();
70+
});
71+
72+
test('one-off tab is disabled on page without allow_cards_override (global DD-only applies)', async ({ page }) => {
73+
await injectEnvOverrides(page, `**${SUPPORTER_PAGE}`, { USE_STRIPE: true });
74+
await mockRestEndpoints(page);
75+
await page.goto(SUPPORTER_PAGE);
76+
await page.waitForSelector('h2:has-text("Support us")');
77+
78+
const oneOffBtn = page.locator('.btn-group button:has-text("One-off")');
79+
await expect(oneOffBtn).toBeVisible();
80+
await expect(oneOffBtn).toBeDisabled();
81+
});
82+
});
83+
84+
// ---------------------------------------------------------------------------
85+
// /join request body for a one-off donation via the override page
86+
// ---------------------------------------------------------------------------
87+
88+
test.describe('/join body for one-off donation on override page', () => {
89+
test('recurDonation=false and donationAmount>0 in /join body', async ({ page }) => {
90+
await injectEnvOverrides(page, `**${OVERRIDE_PAGE}`, { USE_STRIPE: true });
91+
await mockRestEndpoints(page);
92+
await page.goto(OVERRIDE_PAGE);
93+
await page.waitForSelector('h2:has-text("Support us")');
94+
95+
// Select one-off, pick a tier, advance through details.
96+
await page.locator('.btn-group button:has-text("One-off")').click();
97+
await page.locator('button[type="button"]:has-text("£5")').click();
98+
await page.locator('button[type="submit"]').click();
99+
await page.waitForSelector('input#firstName');
100+
await page.locator(CONTINUE).click();
101+
102+
const joinBody = await captureJoinBodyViaStripeRedirect(page, OVERRIDE_PAGE);
103+
104+
expect(Object.keys(joinBody).length).toBeGreaterThan(0);
105+
expect(joinBody.recurDonation).toBe(false);
106+
expect(Number(joinBody.donationAmount)).toBeGreaterThan(0);
107+
// paymentMethod must be creditCard (the allow_cards_override unlocks card for one-off).
108+
expect(joinBody.paymentMethod).toBe('creditCard');
109+
});
110+
});

packages/join-flow/src/env.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ describe('getPaymentProviders — Stripe direct debit flags', () => {
4242
expect(getPaymentProviders().stripe).toEqual(['creditCard']);
4343
});
4444
});
45+
46+
it('STRIPE_DIRECT_DEBIT_ONLY explicitly false (allow_cards_override) produces creditCard only', () => {
47+
withEnv({ USE_STRIPE: true, STRIPE_DIRECT_DEBIT_ONLY: false }, () => {
48+
expect(getPaymentProviders().stripe).toEqual(['creditCard']);
49+
});
50+
});
51+
52+
it('STRIPE_DIRECT_DEBIT_ONLY explicitly false with STRIPE_DIRECT_DEBIT true produces both methods', () => {
53+
withEnv({ USE_STRIPE: true, STRIPE_DIRECT_DEBIT_ONLY: false, STRIPE_DIRECT_DEBIT: true }, () => {
54+
const methods = getPaymentProviders().stripe;
55+
expect(methods).toContain('creditCard');
56+
expect(methods).toContain('directDebit');
57+
});
58+
});
4559
});
4660

4761
describe('getPaymentMethods — STRIPE_DIRECT_DEBIT_ONLY', () => {
@@ -90,4 +104,10 @@ describe('resolveStripePaymentMethodTypes', () => {
90104
expect(resolveStripePaymentMethodTypes(false, 'eur')).toEqual(['card']);
91105
});
92106
});
107+
108+
it('subscription with STRIPE_DIRECT_DEBIT_ONLY explicitly false (allow_cards_override) returns card-only', () => {
109+
withEnv({ STRIPE_DIRECT_DEBIT_ONLY: false }, () => {
110+
expect(resolveStripePaymentMethodTypes(false, 'gbp')).toEqual(['card']);
111+
});
112+
});
93113
});

packages/join-flow/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const init = () => {
2424
const sentryDsn = getEnvStr("SENTRY_DSN")
2525
Sentry.init({
2626
dsn: sentryDsn,
27-
release: "1.4.0"
27+
release: "1.4.1"
2828
});
2929

3030
if (getEnv('USE_CHARGEBEE')) {

0 commit comments

Comments
 (0)