Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion 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.4.44
* Version: 1.4.45
* Author: Common Knowledge <hello@commonknowledge.coop>
* Text Domain: common-knowledge-join-flow
* License: GPLv2 or later
Expand Down
4 changes: 3 additions & 1 deletion 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: 7.0
Stable tag: 1.4.44
Stable tag: 1.4.45
Requires PHP: 8.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Expand Down Expand Up @@ -107,6 +107,8 @@ Need help? Contact us at [hello@commonknowledge.coop](mailto:hello@commonknowled

== Changelog ==

= 1.4.45 =
* Add optional income guidance: a table of income bands against membership plans on the membership step, switched on by an add-on
= 1.4.44 =
* Fix: keep members lapsed when they pay an overdue invoice after Stripe has cancelled their subscription
= 1.4.43 =
Expand Down
4 changes: 2 additions & 2 deletions packages/join-block/src/Blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ private static function echoEnvironment($fields, $block_mode)
"frequency" => $plan["frequency"],
"description" => $plan["description"],
"redirectUrl" => $is_supporter_mode ? null : Settings::getMembershipPlanRedirectUrl($plan)
];
] + Settings::getMembershipPlanIncomeBands($plan);
}

$webhook_url = $fields['custom_webhook_url'] ?? '';
Expand Down Expand Up @@ -538,7 +538,7 @@ function ($o) {
"USE_STRIPE" => Settings::get("USE_STRIPE"),
"USE_VARIABLE_MEMBERSHIP_PLAN" => $fields['use_variable_membership_plan'] ?? false,
"WEBHOOK_UUID" => $webhook_uuid ? $webhook_uuid : '',
];
] + Settings::getIncomeGuidanceEnv();
?>
<script type="application/json" id="env">
<?php echo wp_json_encode($environment); ?>
Expand Down
57 changes: 57 additions & 0 deletions packages/join-block/src/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ public static function init()
->set_default_value("Joining")
->set_help_text("The verb used in the loading spinner (e.g., 'Joining Organisation Name')"),
];
if (self::isIncomeGuidanceEnabled()) {
$after_tiers_copy = 1 + array_search(
'currency_selection_copy',
array_map(fn($field) => $field->get_base_name(), $copy_fields),
true
);
array_splice($copy_fields, $after_tiers_copy, 0, [
Field::make('text', 'income_guidance_heading', 'Income guidance heading')
->set_default_value('Below are some recommended monthly dues amounts, based on income')
->set_help_text('Shown above the income guidance table on the membership step.'),
Field::make('rich_text', 'income_guidance_note', 'Income guidance note')
->set_help_text("Optional. Shown below the income guidance table, e.g. \"Can't afford dues? Email us.\""),
]);
}

$integration_fields = [
Field::make('separator', 'zetkin', 'Zetkin'),
Field::make('text', 'zetkin_organisation_id', 'Zetkin Organisation ID')->set_attribute('type', 'number'),
Expand Down Expand Up @@ -564,6 +579,15 @@ public static function createMembershipPlansField($name = 'membership_plans')
self::describePlanRedirectCheckboxes();
/** @var Complex_Field $membership_plans */
$membership_plans = Field::make('complex', $name);
$income_band_fields = [];
if (self::isIncomeGuidanceEnabled()) {
$income_band_fields = [
Field::make('text', 'income_weekly', 'Weekly income')
->set_help_text('Shown in the income guidance table, e.g. "£600 to £800". Leave both incomes blank to leave this plan out of the table.'),
Field::make('text', 'income_monthly', 'Monthly income')
->set_help_text('E.g. "£2,400 to £3,200".'),
];
}
$membership_plans->add_fields([
Field::make('text', 'label', "Name")->set_required(true),
Field::make('text', 'id', "Price Point ID")->set_help_text("Membership price point ID (required for Chargebee)"),
Expand All @@ -575,6 +599,7 @@ public static function createMembershipPlansField($name = 'membership_plans')
$payment_currency_select,
Field::make('text', 'description'),
Field::make('text', 'add_tags')->set_help_text("Comma-separated tags to add to this member in Action Network, Mailchimp and Zetkin."),
...$income_band_fields,
$redirect_checkbox,
$redirect_page,
])->set_min(1)
Expand Down Expand Up @@ -679,6 +704,38 @@ private static function describePlanRedirectCheckboxes()
}
}

// Income guidance is off unless an add-on switches it on.
public static function isIncomeGuidanceEnabled()
{
return (bool) apply_filters('ck_join_flow_income_guidance_enabled', false);
}

// A plan's income bands, for the form's income guidance table.
public static function getMembershipPlanIncomeBands($membership_plan)
{
if (!self::isIncomeGuidanceEnabled()) {
return [];
}

return [
'incomeWeekly' => trim((string) ($membership_plan['income_weekly'] ?? '')),
'incomeMonthly' => trim((string) ($membership_plan['income_monthly'] ?? '')),
];
}

public static function getIncomeGuidanceEnv()
{
if (!self::isIncomeGuidanceEnabled()) {
return ['INCOME_GUIDANCE_ENABLED' => false];
}

return [
'INCOME_GUIDANCE_ENABLED' => true,
'INCOME_GUIDANCE_HEADING' => (string) self::get('INCOME_GUIDANCE_HEADING'),
'INCOME_GUIDANCE_NOTE' => wp_kses_post(wpautop((string) self::get('INCOME_GUIDANCE_NOTE'))),
];
}

// Names the CRMs this site uses, so admins can see where redirected people
// will not appear.
public static function getMembershipPlanRedirectHelpText()
Expand Down
140 changes: 140 additions & 0 deletions packages/join-block/tests/SettingsIncomeGuidanceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

namespace CommonKnowledge\JoinBlock\Tests;

use Brain\Monkey;
use CommonKnowledge\JoinBlock\Settings;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;

/**
* Income guidance shows income bands against membership plans on the plan
* step. It is off unless an add-on switches it on with the
* ck_join_flow_income_guidance_enabled filter.
*/
class SettingsIncomeGuidanceTest extends TestCase
{
use MockeryPHPUnitIntegration;

protected function setUp(): void
{
parent::setUp();
Monkey\setUp();
}

protected function tearDown(): void
{
Monkey\tearDown();
parent::tearDown();
}

public function test_is_off_by_default()
{
$this->assertFalse(Settings::isIncomeGuidanceEnabled());
}

public function test_an_add_on_can_switch_it_on()
{
Monkey\Filters\expectApplied('ck_join_flow_income_guidance_enabled')
->once()
->with(false)
->andReturn(true);

$this->assertTrue(Settings::isIncomeGuidanceEnabled());
}

public function test_a_truthy_filter_result_counts_as_on()
{
Monkey\Filters\expectApplied('ck_join_flow_income_guidance_enabled')->andReturn('1');

$this->assertTrue(Settings::isIncomeGuidanceEnabled());
}

private function switchOn(): void
{
Monkey\Filters\expectApplied('ck_join_flow_income_guidance_enabled')->andReturn(true);
}

private function withSettings(array $settings): void
{
Monkey\Functions\when('carbon_get_theme_option')->alias(fn($key) => $settings[$key] ?? null);
Monkey\Functions\when('wpautop')->alias(fn($text) => $text === '' ? '' : "<p>$text</p>");
Monkey\Functions\when('wp_kses_post')->alias(fn($html) => str_replace(' onerror="steal()"', '', $html));
}

public function test_plans_carry_no_income_bands_when_off()
{
$plan = ['label' => 'Higher', 'income_weekly' => '£600 to £800', 'income_monthly' => '£2,400 to £3,200'];

$this->assertSame([], Settings::getMembershipPlanIncomeBands($plan));
}

public function test_plans_carry_their_income_bands_when_on()
{
$this->switchOn();
$plan = ['label' => 'Higher', 'income_weekly' => ' £600 to £800 ', 'income_monthly' => '£2,400 to £3,200'];

$this->assertSame(
['incomeWeekly' => '£600 to £800', 'incomeMonthly' => '£2,400 to £3,200'],
Settings::getMembershipPlanIncomeBands($plan)
);
}

public function test_a_plan_without_bands_carries_empty_ones_when_on()
{
$this->switchOn();

$this->assertSame(
['incomeWeekly' => '', 'incomeMonthly' => ''],
Settings::getMembershipPlanIncomeBands(['label' => 'Other'])
);
}

public function test_the_form_is_only_told_it_is_off_when_off()
{
$this->withSettings(['income_guidance_heading' => 'Heading', 'income_guidance_note' => 'Note']);

$this->assertSame(['INCOME_GUIDANCE_ENABLED' => false], Settings::getIncomeGuidanceEnv());
}

public function test_the_form_gets_the_heading_and_note_when_on()
{
$this->switchOn();
$this->withSettings([
'income_guidance_heading' => 'Suggested dues by income',
'income_guidance_note' => "Can't afford dues? Email us.",
]);

$this->assertSame([
'INCOME_GUIDANCE_ENABLED' => true,
'INCOME_GUIDANCE_HEADING' => 'Suggested dues by income',
'INCOME_GUIDANCE_NOTE' => "<p>Can't afford dues? Email us.</p>",
], Settings::getIncomeGuidanceEnv());
}

/**
* The note is shown as HTML on the join form. An admin without the
* unfiltered_html capability must not be able to put script in it.
*/
public function test_the_note_is_sanitised_before_reaching_the_form()
{
$this->switchOn();
$this->withSettings([
'income_guidance_heading' => 'Suggested dues by income',
'income_guidance_note' => 'Email us <img src="x" onerror="steal()">',
]);

$this->assertSame(
'<p>Email us <img src="x"></p>',
Settings::getIncomeGuidanceEnv()['INCOME_GUIDANCE_NOTE']
);
}

public function test_an_unset_note_is_sent_empty()
{
$this->switchOn();
$this->withSettings(['income_guidance_heading' => 'Suggested dues by income']);

$this->assertSame('', Settings::getIncomeGuidanceEnv()['INCOME_GUIDANCE_NOTE']);
}
}
3 changes: 2 additions & 1 deletion packages/join-e2e/.wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"core": "https://wordpress.org/wordpress-6.9.4.zip",
"plugins": ["../../packages/join-block"],
"mappings": {
"wp-content/e2e-scripts": "../../packages/join-e2e/scripts"
"wp-content/e2e-scripts": "../../packages/join-e2e/scripts",
"wp-content/mu-plugins/ck-e2e-income-guidance.php": "../../packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php"
},
"config": {
"WP_DEBUG": true,
Expand Down
12 changes: 12 additions & 0 deletions packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
/**
* Plugin Name: CK Join Flow E2E: income guidance switch
* Description: Switches income guidance on, as an add-on would, unless the ck_e2e_income_guidance_enabled option is set to off.
*/

// On unless a spec has turned it off. Defaulting to on matters: the seed
// saves income guidance settings, and those fields only exist if the flag is
// on when the request starts, before the seed can set any option.
add_filter('ck_join_flow_income_guidance_enabled', function ($enabled) {
return $enabled || (bool) get_option('ck_e2e_income_guidance_enabled', '1');
});
40 changes: 38 additions & 2 deletions packages/join-e2e/scripts/setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ function ck_e2e_make_block_content(array $plans, array $overrides = []): string

/**
* Create or update a page by post_name slug.
*
* WordPress unslashes post content on save, which would turn the \u escapes
* in the block's JSON (e.g. \u00a3 for £) into plain text, so slash it first.
* Returns the page ID on success; exits with status 1 on failure.
*/
function ck_e2e_upsert_page(string $slug, string $title, string $content): int
Expand All @@ -55,7 +58,7 @@ function ck_e2e_upsert_page(string $slug, string $title, string $content): int

if ($existing) {
$page_id = $existing[0]->ID;
wp_update_post(['ID' => $page_id, 'post_content' => $content]);
wp_update_post(['ID' => $page_id, 'post_content' => wp_slash($content)]);
echo "Updated page '{$slug}' (ID: {$page_id}).\n";
return $page_id;
}
Expand All @@ -65,7 +68,7 @@ function ck_e2e_upsert_page(string $slug, string $title, string $content): int
'post_title' => $title,
'post_status' => 'publish',
'post_type' => 'page',
'post_content' => $content,
'post_content' => wp_slash($content),
], true);

if (is_wp_error($page_id)) {
Expand All @@ -87,6 +90,11 @@ function ck_e2e_upsert_page(string $slug, string $title, string $content): int
carbon_set_theme_option('organisation_bank_name', 'E2E ORG');
carbon_set_theme_option('organisation_email_address', 'e2e@example.org');

// Income guidance is on unless a spec turns it off (see mu-plugins/). It has
// to be on before this script starts for the guidance settings below to save,
// so this only puts it back for the next run after an interrupted spec.
update_option('ck_e2e_income_guidance_enabled', '1');

// Configure pretty permalinks so test URLs are predictable.
update_option('permalink_structure', '/%postname%/');
flush_rewrite_rules(true);
Expand Down Expand Up @@ -335,6 +343,34 @@ function ck_e2e_upsert_page(string $slug, string $title, string $content): int
ck_e2e_make_block_content([])
);

// Income guidance: three banded tiers, and a custom-amount tier with no band
// that stays out of the table.
$income_guidance_page_id = ck_e2e_upsert_page(
'e2e-income-guidance-join',
'E2E Income Guidance Test',
ck_e2e_make_block_content([
$ck_e2e_plan('Higher', 'higher', '25', [
'income_weekly' => 'More than £800',
'income_monthly' => 'More than £3,200',
]),
$ck_e2e_plan('Middle', 'middle', '12', [
'income_weekly' => '£400 to £800',
'income_monthly' => '£1,600 to £3,200',
]),
$ck_e2e_plan('Lower', 'lower', '5', [
'income_weekly' => 'Less than £400',
'income_monthly' => 'Less than £1,600',
]),
$ck_e2e_plan('Other', 'other', '1', ['allow_custom_amount' => '1']),
])
);
// The image's handler is script an admin without unfiltered_html could save;
// the form must strip it rather than run it.
carbon_set_theme_option(
'income_guidance_note',
"Can't afford dues? Email dues@example.org.<img src=\"x\" onerror=\"document.body.dataset.injected='yes'\">"
);

// Enable STRIPE_DIRECT_DEBIT_ONLY globally so that allow_cards_override has
// something to override. Without this the global default is false and the
// override would produce no observable difference.
Expand Down
Loading
Loading