From 23412c577454d51fc72649b76a4cad2ec58837e6 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 9 Oct 2024 16:25:46 -0400 Subject: [PATCH 01/58] chore: prepare for release 3.16.4 --- give.php | 2 +- readme.txt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/give.php b/give.php index 62a75e5cb9..e0dbfc1742 100644 --- a/give.php +++ b/give.php @@ -6,7 +6,7 @@ * Description: The most robust, flexible, and intuitive way to accept donations on WordPress. * Author: GiveWP * Author URI: https://givewp.com/ - * Version: 3.16.3 + * Version: 3.16.4 * Requires at least: 6.4 * Requires PHP: 7.2 * Text Domain: give diff --git a/readme.txt b/readme.txt index 46e1c91634..539831690f 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: donation, donate, recurring donations, fundraising, crowdfunding Requires at least: 6.4 Tested up to: 6.6 Requires PHP: 7.2 -Stable tag: 3.16.3 +Stable tag: 3.16.4 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -262,6 +262,9 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri 10. Use almost any payment gateway integration with GiveWP through our add-ons or by creating your own add-on. == Changelog == += 3.16.4: October 10th, 2024 = +* Security: Added additional protection against serialized data in the option-based donation form request (CVE-2024-9634) + = 3.16.3: October 7th, 2024 = * Security: Added additional validation to the donor title field, further protecting the option-based donation form request From db0ea4f062faa04bf60f692c1422b0452af398bd Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 9 Oct 2024 16:26:23 -0400 Subject: [PATCH 02/58] Refactor: disallow any serialized input in v2 forms (#7566) Co-authored-by: Jon Waldstein --- includes/process-donation.php | 35 ++++++----------------- tests/includes/legacy/tests-functions.php | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index 41be334326..abf570b4eb 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -418,38 +418,16 @@ function give_donation_form_validate_fields() { /** * Detect serialized fields. * + * @unreleased updated to check all values for serialized fields * @since 3.16.2 added additional check for stripslashes_deep * @since 3.14.2 add give-form-title, give_title * @since 3.5.0 */ function give_donation_form_has_serialized_fields(array $post_data): bool { - $post_data_keys = [ - 'give-form-id', - 'give-gateway', - 'card_name', - 'card_number', - 'card_cvc', - 'card_exp_month', - 'card_exp_year', - 'card_address', - 'card_address_2', - 'card_city', - 'card_state', - 'billing_country', - 'card_zip', - 'give_email', - 'give_first', - 'give_last', - 'give_user_login', - 'give_user_pass', - 'give-form-title', - 'give_title', - ]; - - foreach ($post_data as $key => $value) { - if ( ! in_array($key, $post_data_keys, true)) { - continue; + foreach ($post_data as $value) { + if (is_serialized(ltrim($value, '\\'))) { + return true; } if (is_serialized(stripslashes_deep($value))) { @@ -1644,6 +1622,7 @@ function give_validate_required_form_fields( $form_id ) { * * @param array $post_data List of post data. * + * @unreleased Add additional validation for company name field * @since 3.16.3 Add additional validations for name title prefix field * @since 2.1 * @@ -1657,6 +1636,10 @@ function give_donation_form_validate_name_fields( $post_data ) { give_set_error( 'disabled_name_title', esc_html__( 'The name title prefix field is not enabled.', 'give' ) ); } + if (!give_is_company_field_enabled($formId) && isset($post_data['give_company_name'])) { + give_set_error( 'disabled_company', esc_html__( 'The company field is not enabled.', 'give' ) ); + } + if (give_is_name_title_prefix_enabled($formId) && isset($post_data['give_title']) && !in_array($post_data['give_title'], array_values(give_get_name_title_prefixes($formId)))) { give_set_error( 'invalid_name_title', esc_html__( 'The name title prefix field is not valid.', 'give' ) ); } diff --git a/tests/includes/legacy/tests-functions.php b/tests/includes/legacy/tests-functions.php index c6162a8cf2..f150a03fd3 100644 --- a/tests/includes/legacy/tests-functions.php +++ b/tests/includes/legacy/tests-functions.php @@ -86,4 +86,33 @@ public function test_give_form_get_default_level() { // When passing invalid form id, it should return null. $this->assertEquals( give_form_get_default_level( 123 ), null ); } + + /** + * @unreleased + * @dataProvider give_donation_form_has_serialized_fields_data + */ + public function test_give_donation_form_has_serialized_fields(array $fields, bool $expected): void + { + if ($expected) { + $this->assertTrue(give_donation_form_has_serialized_fields($fields)); + } else { + $this->assertFalse(give_donation_form_has_serialized_fields($fields)); + } + } + + /** + * @unreleased + */ + public function give_donation_form_has_serialized_fields_data(): array + { + return [ + [['foo' => serialize('bar')], true], + [['foo' => 'bar', 'baz' => '\\' . serialize('backslash-bypass')], true], + [['foo' => 'bar', 'baz' => '\\\\' . serialize('double-backslash-bypass')], true], + [['foo' => 'bar'], false], + [['foo' => 'bar', 'baz' => serialize('qux')], true], + [['foo' => 'bar', 'baz' => 'qux'], false], + [['foo' => 'bar', 'baz' => 1], false], + ]; + } } From 4ea54ac938cc099e1dfbde1db4d3c155cfb28339 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 9 Oct 2024 16:28:38 -0400 Subject: [PATCH 03/58] chore: prepere for release 3.16.4 --- includes/process-donation.php | 4 ++-- tests/includes/legacy/tests-functions.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index abf570b4eb..73059c62be 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -418,7 +418,7 @@ function give_donation_form_validate_fields() { /** * Detect serialized fields. * - * @unreleased updated to check all values for serialized fields + * @since 3.16.4 updated to check all values for serialized fields * @since 3.16.2 added additional check for stripslashes_deep * @since 3.14.2 add give-form-title, give_title * @since 3.5.0 @@ -1622,7 +1622,7 @@ function give_validate_required_form_fields( $form_id ) { * * @param array $post_data List of post data. * - * @unreleased Add additional validation for company name field + * @since 3.16.4 Add additional validation for company name field * @since 3.16.3 Add additional validations for name title prefix field * @since 2.1 * diff --git a/tests/includes/legacy/tests-functions.php b/tests/includes/legacy/tests-functions.php index f150a03fd3..bdcb11a0e7 100644 --- a/tests/includes/legacy/tests-functions.php +++ b/tests/includes/legacy/tests-functions.php @@ -88,7 +88,7 @@ public function test_give_form_get_default_level() { } /** - * @unreleased + * @since 3.16.4 * @dataProvider give_donation_form_has_serialized_fields_data */ public function test_give_donation_form_has_serialized_fields(array $fields, bool $expected): void @@ -101,7 +101,7 @@ public function test_give_donation_form_has_serialized_fields(array $fields, boo } /** - * @unreleased + * @since 3.16.4 */ public function give_donation_form_has_serialized_fields_data(): array { From 338a2054bbf4acfb1bd3eb27ca44b329c25fe6a1 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Thu, 10 Oct 2024 18:44:36 -0400 Subject: [PATCH 04/58] chore: update const version to 3.16.4 --- give.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/give.php b/give.php index e0dbfc1742..fedf53c1ea 100644 --- a/give.php +++ b/give.php @@ -406,7 +406,7 @@ private function setup_constants() { // Plugin version. if (!defined('GIVE_VERSION')) { - define('GIVE_VERSION', '3.16.3'); + define('GIVE_VERSION', '3.16.4'); } // Plugin Root File. From 39721a436f54b8411e1185e602e001ef8cd97da8 Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:46:24 -0700 Subject: [PATCH 05/58] Fix: ensure DonorDashboard mobile navigation opens as expected (#7560) --- .../js/app/components/mobile-menu/index.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/DonorDashboards/resources/js/app/components/mobile-menu/index.js b/src/DonorDashboards/resources/js/app/components/mobile-menu/index.js index 7a17884623..ebe4b5fa2a 100644 --- a/src/DonorDashboards/resources/js/app/components/mobile-menu/index.js +++ b/src/DonorDashboards/resources/js/app/components/mobile-menu/index.js @@ -8,11 +8,11 @@ import './style.scss'; const MobileMenu = ({children}) => { const [isOpen, setIsOpen] = useState(false); - const contentRef = useRef(null); + const toggleRef = useRef(null); useEffect(() => { const handleClick = (evt) => { - if (contentRef.current && !contentRef.current.contains(evt.target)) { + if (toggleRef.current && !toggleRef.current.contains(evt.target)) { setIsOpen(false); } }; @@ -26,7 +26,7 @@ const MobileMenu = ({children}) => { document.removeEventListener('click', handleClick); } }; - }, [isOpen, contentRef]); + }, [isOpen, toggleRef]); const location = useLocation(); const tabsSelector = useSelector((state) => state.tabs); @@ -39,16 +39,19 @@ const MobileMenu = ({children}) => {
{label}
setIsOpen(!isOpen)} + onClick={() => { + setIsOpen(!isOpen); + }} >
{isOpen && ( -
+
{children}
)} From 1de05ae37e308d28888dde194125d7e1e54c06f6 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 15 Oct 2024 10:21:41 -0400 Subject: [PATCH 06/58] chore: prepeare for release 3.16.5 --- give.php | 4 ++-- readme.txt | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/give.php b/give.php index fedf53c1ea..e67832a60e 100644 --- a/give.php +++ b/give.php @@ -6,7 +6,7 @@ * Description: The most robust, flexible, and intuitive way to accept donations on WordPress. * Author: GiveWP * Author URI: https://givewp.com/ - * Version: 3.16.4 + * Version: 3.16.5 * Requires at least: 6.4 * Requires PHP: 7.2 * Text Domain: give @@ -406,7 +406,7 @@ private function setup_constants() { // Plugin version. if (!defined('GIVE_VERSION')) { - define('GIVE_VERSION', '3.16.4'); + define('GIVE_VERSION', '3.16.5'); } // Plugin Root File. diff --git a/readme.txt b/readme.txt index 539831690f..68a0eb98e1 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: donation, donate, recurring donations, fundraising, crowdfunding Requires at least: 6.4 Tested up to: 6.6 Requires PHP: 7.2 -Stable tag: 3.16.4 +Stable tag: 3.16.5 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -262,6 +262,9 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri 10. Use almost any payment gateway integration with GiveWP through our add-ons or by creating your own add-on. == Changelog == += 3.16.5: October 15th, 2024 = +* Fix: Resolved an issue with the donor dashboard menu not opening on mobile devices + = 3.16.4: October 10th, 2024 = * Security: Added additional protection against serialized data in the option-based donation form request (CVE-2024-9634) From 9bfae6c84d49dcb67faf16b9e93a593521cd1559 Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Tue, 15 Oct 2024 13:45:49 -0300 Subject: [PATCH 07/58] Fix: prevent PHP 8+ fatal errors when Tributes add-on is enabled (#7572) --- includes/process-donation.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index 73059c62be..79d50f4853 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -418,6 +418,7 @@ function give_donation_form_validate_fields() { /** * Detect serialized fields. * + * @unreleased Make sure only string parameters are used with the ltrim() method to prevent PHP 8+ fatal errors * @since 3.16.4 updated to check all values for serialized fields * @since 3.16.2 added additional check for stripslashes_deep * @since 3.14.2 add give-form-title, give_title @@ -426,7 +427,7 @@ function give_donation_form_validate_fields() { function give_donation_form_has_serialized_fields(array $post_data): bool { foreach ($post_data as $value) { - if (is_serialized(ltrim($value, '\\'))) { + if (is_string($value) && is_serialized(ltrim($value, '\\'))) { return true; } @@ -1622,6 +1623,7 @@ function give_validate_required_form_fields( $form_id ) { * * @param array $post_data List of post data. * + * @unreleased Check if "give_title" is set to prevent PHP warnings * @since 3.16.4 Add additional validation for company name field * @since 3.16.3 Add additional validations for name title prefix field * @since 2.1 @@ -1646,7 +1648,7 @@ function give_donation_form_validate_name_fields( $post_data ) { $is_alpha_first_name = ( ! is_email( $post_data['give_first'] ) && ! preg_match( '~[0-9]~', $post_data['give_first'] ) ); $is_alpha_last_name = ( ! is_email( $post_data['give_last'] ) && ! preg_match( '~[0-9]~', $post_data['give_last'] ) ); - $is_alpha_title = ( ! is_email( $post_data['give_title'] ) && ! preg_match( '~[0-9]~', $post_data['give_title'] ) ); + $is_alpha_title = ( isset($post_data['give_title']) && ! is_email( $post_data['give_title'] ) && ! preg_match( '~[0-9]~', $post_data['give_title'] ) ); if (!$is_alpha_first_name || ( ! empty( $post_data['give_last'] ) && ! $is_alpha_last_name) || ( ! empty( $post_data['give_title'] ) && ! $is_alpha_title) ) { give_set_error( 'invalid_name', esc_html__( 'The First Name and Last Name fields cannot contain an email address or numbers.', 'give' ) ); From 3858a84f97f003813bc2b629e11fb26aa45f27fe Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 15 Oct 2024 12:49:05 -0400 Subject: [PATCH 08/58] chore: prepare for release 3.16.5 --- includes/process-donation.php | 4 ++-- readme.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index 79d50f4853..72b2fdef4f 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -418,7 +418,7 @@ function give_donation_form_validate_fields() { /** * Detect serialized fields. * - * @unreleased Make sure only string parameters are used with the ltrim() method to prevent PHP 8+ fatal errors + * @since 3.16.5 Make sure only string parameters are used with the ltrim() method to prevent PHP 8+ fatal errors * @since 3.16.4 updated to check all values for serialized fields * @since 3.16.2 added additional check for stripslashes_deep * @since 3.14.2 add give-form-title, give_title @@ -1623,7 +1623,7 @@ function give_validate_required_form_fields( $form_id ) { * * @param array $post_data List of post data. * - * @unreleased Check if "give_title" is set to prevent PHP warnings + * @since 3.16.5 Check if "give_title" is set to prevent PHP warnings * @since 3.16.4 Add additional validation for company name field * @since 3.16.3 Add additional validations for name title prefix field * @since 2.1 diff --git a/readme.txt b/readme.txt index 68a0eb98e1..482b5a6c74 100644 --- a/readme.txt +++ b/readme.txt @@ -263,6 +263,7 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri == Changelog == = 3.16.5: October 15th, 2024 = +* Fix: Resolved a PHP v8+ fatal error on option-based forms when the Tributes add-on was enabled * Fix: Resolved an issue with the donor dashboard menu not opening on mobile devices = 3.16.4: October 10th, 2024 = From 1837c3427632b79116216fbf81ba6f096ff33834 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 15 Oct 2024 16:37:31 -0400 Subject: [PATCH 09/58] chore: update version to 3.17.0 and add security faq --- give.php | 4 ++-- readme.txt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/give.php b/give.php index e67832a60e..e50c777f38 100644 --- a/give.php +++ b/give.php @@ -6,7 +6,7 @@ * Description: The most robust, flexible, and intuitive way to accept donations on WordPress. * Author: GiveWP * Author URI: https://givewp.com/ - * Version: 3.16.5 + * Version: 3.17.0 * Requires at least: 6.4 * Requires PHP: 7.2 * Text Domain: give @@ -406,7 +406,7 @@ private function setup_constants() { // Plugin version. if (!defined('GIVE_VERSION')) { - define('GIVE_VERSION', '3.16.5'); + define('GIVE_VERSION', '3.17.0'); } // Plugin Root File. diff --git a/readme.txt b/readme.txt index 482b5a6c74..c303d9176d 100644 --- a/readme.txt +++ b/readme.txt @@ -239,6 +239,10 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri [Read our release announcement](https://go.givewp.com/version2-5) for all the details, and if you have further questions feel free to reach out via [our contact page](https://go.givewp.com/contact). += How can I report security bugs? = + +You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team help validate, triage and handle any security vulnerabilities. [Report a security vulnerability.](https://patchstack.com/database/vdp/give) + == Screenshots == 1. Creating powerful donation forms is easy with GiveWP. Simply install the plugin, create a new donation form, set the desired giving options, and publish! @@ -262,6 +266,8 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri 10. Use almost any payment gateway integration with GiveWP through our add-ons or by creating your own add-on. == Changelog == += 3.17.0: October 16th, 2024 = + = 3.16.5: October 15th, 2024 = * Fix: Resolved a PHP v8+ fatal error on option-based forms when the Tributes add-on was enabled * Fix: Resolved an issue with the donor dashboard menu not opening on mobile devices From 29ba304db9203a87abb1c2b8b7f3a1450ba9e07f Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 15 Oct 2024 16:40:22 -0400 Subject: [PATCH 10/58] Chore: update base enum class with forked version from myclabs (#7383) Co-authored-by: Jon Waldstein Co-authored-by: Jon Waldstein --- composer.json | 1 - composer.lock | 68 +--- src/Donations/ValueObjects/DonationMode.php | 2 +- src/Donors/ValueObjects/DonorMetaKeys.php | 8 +- .../Models/ValueObjects/Relationship.php | 10 +- .../Support/ValueObjects/BaseEnum.php | 326 +++++++++++++++ src/Framework/Support/ValueObjects/Enum.php | 20 +- .../ValueObjects/SubscriptionMode.php | 2 +- src/Tracking/Enum/EventType.php | 2 +- .../ValueObjects/Enum/EnumConflict.php | 21 + .../Support/ValueObjects/Enum/EnumFixture.php | 36 ++ .../Enum/InheritedEnumFixture.php | 15 + .../Support/ValueObjects/TestBaseEnum.php | 375 ++++++++++++++++++ 13 files changed, 798 insertions(+), 88 deletions(-) create mode 100644 src/Framework/Support/ValueObjects/BaseEnum.php create mode 100644 tests/Framework/Support/ValueObjects/Enum/EnumConflict.php create mode 100644 tests/Framework/Support/ValueObjects/Enum/EnumFixture.php create mode 100644 tests/Framework/Support/ValueObjects/Enum/InheritedEnumFixture.php create mode 100644 tests/Framework/Support/ValueObjects/TestBaseEnum.php diff --git a/composer.json b/composer.json index 872dad4c0d..f44db27a1d 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,6 @@ "paypal/paypal-checkout-sdk": "^1.0", "kjohnson/format-object-list": "^0.1.0", "fakerphp/faker": "^1.9", - "myclabs/php-enum": "^1.6", "symfony/http-foundation": "^v3.4.47", "moneyphp/money": "v3.3.1", "stellarwp/field-conditions": "^1.1", diff --git a/composer.lock b/composer.lock index e8a2030294..6ff27f46f5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "710d2ab2c1fe36ccdb02fe9d59fa4d93", + "content-hash": "43cef1ed6b67b39654509b96e53093c7", "packages": [ { "name": "composer/installers", @@ -344,66 +344,6 @@ }, "time": "2020-03-18T17:49:59+00:00" }, - { - "name": "myclabs/php-enum", - "version": "1.7.7", - "source": { - "type": "git", - "url": "https://github.com/myclabs/php-enum.git", - "reference": "d178027d1e679832db9f38248fcc7200647dc2b7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/d178027d1e679832db9f38248fcc7200647dc2b7", - "reference": "d178027d1e679832db9f38248fcc7200647dc2b7", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7", - "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^3.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "MyCLabs\\Enum\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP Enum contributors", - "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" - } - ], - "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", - "keywords": [ - "enum" - ], - "support": { - "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.7.7" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", - "type": "tidelift" - } - ], - "time": "2020-11-14T18:14:52+00:00" - }, { "name": "paypal/paypal-checkout-sdk", "version": "1.0.2", @@ -7276,10 +7216,12 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-json": "*" + }, "platform-dev": [], "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Donations/ValueObjects/DonationMode.php b/src/Donations/ValueObjects/DonationMode.php index ba01c10114..2120e0c207 100644 --- a/src/Donations/ValueObjects/DonationMode.php +++ b/src/Donations/ValueObjects/DonationMode.php @@ -2,7 +2,7 @@ namespace Give\Donations\ValueObjects; -use MyCLabs\Enum\Enum; +use Give\Framework\Support\ValueObjects\Enum; /** * @since 2.19.6 diff --git a/src/Donors/ValueObjects/DonorMetaKeys.php b/src/Donors/ValueObjects/DonorMetaKeys.php index 985a4d43ad..91c12b8fd7 100644 --- a/src/Donors/ValueObjects/DonorMetaKeys.php +++ b/src/Donors/ValueObjects/DonorMetaKeys.php @@ -8,10 +8,10 @@ /** * @since 2.19.6 * - * @method static FIRST_NAME() - * @method static LAST_NAME() - * @method static ADDITIONAL_EMAILS() - * @method static PREFIX() + * @method static DonorMetaKeys FIRST_NAME() + * @method static DonorMetaKeys LAST_NAME() + * @method static DonorMetaKeys ADDITIONAL_EMAILS() + * @method static DonorMetaKeys PREFIX() */ class DonorMetaKeys extends Enum { diff --git a/src/Framework/Models/ValueObjects/Relationship.php b/src/Framework/Models/ValueObjects/Relationship.php index e98a067cc9..5bfab27e6c 100644 --- a/src/Framework/Models/ValueObjects/Relationship.php +++ b/src/Framework/Models/ValueObjects/Relationship.php @@ -9,11 +9,11 @@ * * @since 2.19.6 * - * @method static HAS_ONE(); - * @method static HAS_MANY(); - * @method static MANY_TO_MANY(); - * @method static BELONGS_TO(); - * @method static BELONGS_TO_MANY(); + * @method static Relationship HAS_ONE(); + * @method static Relationship HAS_MANY(); + * @method static Relationship MANY_TO_MANY(); + * @method static Relationship BELONGS_TO(); + * @method static Relationship BELONGS_TO_MANY(); */ class Relationship extends Enum { diff --git a/src/Framework/Support/ValueObjects/BaseEnum.php b/src/Framework/Support/ValueObjects/BaseEnum.php new file mode 100644 index 0000000000..a215797798 --- /dev/null +++ b/src/Framework/Support/ValueObjects/BaseEnum.php @@ -0,0 +1,326 @@ + + * @author Daniel Costa + * @author Mirosław Filip + * + * @psalm-template T + * @psalm-immutable + * @psalm-consistent-constructor + */ +abstract class BaseEnum implements \JsonSerializable +{ + /** + * Enum value + * + * @var mixed + * @psalm-var T + */ + protected $value; + + /** + * Enum key, the constant name + * + * @var string + */ + private $key; + + /** + * Store existing constants in a static cache per object. + * + * + * @var array + * @psalm-var array> + */ + protected static $cache = []; + + /** + * Cache of instances of the Enum class + * + * @var array + * @psalm-var array> + */ + protected static $instances = []; + + /** + * Creates a new value of some type + * + * @psalm-pure + * @param mixed $value + * + * @psalm-param T $value + * @throws \UnexpectedValueException if incompatible type is given. + */ + public function __construct($value) + { + if ($value instanceof static) { + /** @psalm-var T */ + $value = $value->getValue(); + } + + /** @psalm-suppress ImplicitToStringCast assertValidValueReturningKey returns always a string but psalm has currently an issue here */ + $this->key = static::assertValidValueReturningKey($value); + + /** @psalm-var T */ + $this->value = $value; + } + + /** + * This method exists only for the compatibility reason when deserializing a previously serialized version + * that didn't had the key property + */ + public function __wakeup() + { + /** @psalm-suppress DocblockTypeContradiction key can be null when deserializing an enum without the key */ + if ($this->key === null) { + /** + * @psalm-suppress InaccessibleProperty key is not readonly as marked by psalm + * @psalm-suppress PossiblyFalsePropertyAssignmentValue deserializing a case that was removed + */ + $this->key = static::search($this->value); + } + } + + /** + * @param mixed $value + * @return static + */ + public static function from($value): self + { + $key = static::assertValidValueReturningKey($value); + + return self::__callStatic($key, []); + } + + /** + * @psalm-pure + * @return mixed + * @psalm-return T + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the enum key (i.e. the constant name). + * + * @psalm-pure + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * @psalm-pure + * @psalm-suppress InvalidCast + * @return string + */ + public function __toString() + { + return (string)$this->value; + } + + /** + * Determines if Enum should be considered equal with the variable passed as a parameter. + * Returns false if an argument is an object of different class or not an object. + * + * This method is final, for more information read https://github.com/myclabs/php-enum/issues/4 + * + * @psalm-pure + * @psalm-param mixed $variable + * @return bool + */ + final public function equals($variable = null): bool + { + return $variable instanceof self + && $this->getValue() === $variable->getValue() + && static::class === \get_class($variable); + } + + /** + * Returns the names (keys) of all constants in the Enum class + * + * @psalm-pure + * @psalm-return list + * @return array + */ + public static function keys() + { + return \array_keys(static::toArray()); + } + + /** + * Returns instances of the Enum class of all Enum constants + * + * @psalm-pure + * @psalm-return array + * @return static[] Constant name in key, Enum instance in value + */ + public static function values() + { + $values = array(); + + /** @psalm-var T $value */ + foreach (static::toArray() as $key => $value) { + /** @psalm-suppress UnsafeGenericInstantiation */ + $values[$key] = new static($value); + } + + return $values; + } + + /** + * Returns all possible values as an array + * + * @psalm-pure + * @psalm-suppress ImpureStaticProperty + * + * @psalm-return array + * @return array Constant name in key, constant value in value + */ + public static function toArray() + { + $class = static::class; + + if (!isset(static::$cache[$class])) { + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + $reflection = new \ReflectionClass($class); + /** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */ + static::$cache[$class] = $reflection->getConstants(); + } + + return static::$cache[$class]; + } + + /** + * Check if is valid enum value + * + * @param $value + * @psalm-param mixed $value + * @psalm-pure + * @psalm-assert-if-true T $value + * @return bool + */ + public static function isValid($value) + { + return \in_array($value, static::toArray(), true); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @psalm-assert T $value + * @param mixed $value + */ + public static function assertValidValue($value): void + { + self::assertValidValueReturningKey($value); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @psalm-assert T $value + * @param mixed $value + * @return string + */ + private static function assertValidValueReturningKey($value): string + { + if (false === ($key = static::search($value))) { + throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class); + } + + return $key; + } + + /** + * Check if is valid enum key + * + * @param $key + * @psalm-param string $key + * @psalm-pure + * @return bool + */ + public static function isValidKey($key) + { + $array = static::toArray(); + + return isset($array[$key]) || \array_key_exists($key, $array); + } + + /** + * Return key for value + * + * @param mixed $value + * + * @psalm-param mixed $value + * @psalm-pure + * @return string|false + */ + public static function search($value) + { + return \array_search($value, static::toArray(), true); + } + + /** + * Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant + * + * @param string $name + * @param array $arguments + * + * @return static + * @throws \BadMethodCallException + * + * @psalm-pure + */ + public static function __callStatic($name, $arguments) + { + $class = static::class; + if (!isset(self::$instances[$class][$name])) { + $array = static::toArray(); + if (!isset($array[$name]) && !\array_key_exists($name, $array)) { + $message = "No static method or enum constant '$name' in class " . static::class; + throw new \BadMethodCallException($message); + } + /** @psalm-suppress UnsafeGenericInstantiation */ + return self::$instances[$class][$name] = new static($array[$name]); + } + return clone self::$instances[$class][$name]; + } + + /** + * Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode() + * natively. + * + * @return mixed + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->getValue(); + } +} diff --git a/src/Framework/Support/ValueObjects/Enum.php b/src/Framework/Support/ValueObjects/Enum.php index cd9fa00e79..8b114830cf 100644 --- a/src/Framework/Support/ValueObjects/Enum.php +++ b/src/Framework/Support/ValueObjects/Enum.php @@ -6,9 +6,9 @@ use Give\Framework\Support\Facades\Str; /** - * @method public getKeyAsCamelCase() + * @since 2.10.0 */ -abstract class Enum extends \MyCLabs\Enum\Enum +abstract class Enum extends BaseEnum { /** * @since 2.20.0 @@ -37,11 +37,9 @@ public function __call($name, $arguments) } /** - * @param ...$enums - * - * @return bool + * @since 2.20.0 */ - public function isOneOf(...$enums) { + public function isOneOf(Enum...$enums): bool { foreach($enums as $enum) { if ( $this->equals($enum) ) { return true; @@ -52,19 +50,17 @@ public function isOneOf(...$enums) { } /** - * @return string + * @since 2.20.0 */ - public function getKeyAsCamelCase() + public function getKeyAsCamelCase(): string { return Str::camel($this->getKey()); } /** - * @param string $name - * - * @return bool + * @since 2.20.0 */ - protected static function hasConstant($name) + protected static function hasConstant(string $name): bool { return array_key_exists($name, static::toArray()); } diff --git a/src/Subscriptions/ValueObjects/SubscriptionMode.php b/src/Subscriptions/ValueObjects/SubscriptionMode.php index 46d5c38b84..fde18753f5 100644 --- a/src/Subscriptions/ValueObjects/SubscriptionMode.php +++ b/src/Subscriptions/ValueObjects/SubscriptionMode.php @@ -2,7 +2,7 @@ namespace Give\Subscriptions\ValueObjects; -use MyCLabs\Enum\Enum; +use Give\Framework\Support\ValueObjects\Enum; /** * @since 2.19.6 diff --git a/src/Tracking/Enum/EventType.php b/src/Tracking/Enum/EventType.php index adc82be0ff..09e898cfc9 100644 --- a/src/Tracking/Enum/EventType.php +++ b/src/Tracking/Enum/EventType.php @@ -2,7 +2,7 @@ namespace Give\Tracking\Enum; -use MyCLabs\Enum\Enum; +use Give\Framework\Support\ValueObjects\Enum; /** * Class EventType diff --git a/tests/Framework/Support/ValueObjects/Enum/EnumConflict.php b/tests/Framework/Support/ValueObjects/Enum/EnumConflict.php new file mode 100644 index 0000000000..41ca8d9791 --- /dev/null +++ b/tests/Framework/Support/ValueObjects/Enum/EnumConflict.php @@ -0,0 +1,21 @@ + + * @author Mirosław Filip + */ +class EnumConflict extends Enum +{ + const FOO = "foo"; + const BAR = "bar"; +} diff --git a/tests/Framework/Support/ValueObjects/Enum/EnumFixture.php b/tests/Framework/Support/ValueObjects/Enum/EnumFixture.php new file mode 100644 index 0000000000..8c75071ec1 --- /dev/null +++ b/tests/Framework/Support/ValueObjects/Enum/EnumFixture.php @@ -0,0 +1,36 @@ + + * @author Mirosław Filip + */ +class EnumFixture extends Enum +{ + const FOO = "foo"; + const BAR = "bar"; + const NUMBER = 42; + + /** + * Values that are known to cause problems when used with soft typing + */ + const PROBLEMATIC_NUMBER = 0; + const PROBLEMATIC_NULL = null; + const PROBLEMATIC_EMPTY_STRING = ''; + const PROBLEMATIC_BOOLEAN_FALSE = false; +} diff --git a/tests/Framework/Support/ValueObjects/Enum/InheritedEnumFixture.php b/tests/Framework/Support/ValueObjects/Enum/InheritedEnumFixture.php new file mode 100644 index 0000000000..7bd1ed64c2 --- /dev/null +++ b/tests/Framework/Support/ValueObjects/Enum/InheritedEnumFixture.php @@ -0,0 +1,15 @@ +assertEquals(EnumFixture::FOO, $value->getValue()); + + $value = new EnumFixture(EnumFixture::BAR); + $this->assertEquals(EnumFixture::BAR, $value->getValue()); + + $value = new EnumFixture(EnumFixture::NUMBER); + $this->assertEquals(EnumFixture::NUMBER, $value->getValue()); + } + + /** + * getKey() + */ + public function testGetKey() + { + $value = new EnumFixture(EnumFixture::FOO); + $this->assertEquals('FOO', $value->getKey()); + $this->assertNotEquals('BA', $value->getKey()); + } + + /** @dataProvider invalidValueProvider */ + public function testCreatingEnumWithInvalidValue($value) + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('is not part of the enum ' . EnumFixture::class); + + new EnumFixture($value); + } + + /** + * @dataProvider invalidValueProvider + * @param mixed $value + */ + public function testFailToCreateEnumWithInvalidValueThroughNamedConstructor($value): void + { + $this->expectException(\UnexpectedValueException::class); + + EnumFixture::from($value); + } + + public function testFailToCreateEnumWithEnumItselfThroughNamedConstructor(): void + { + $this->expectException(\UnexpectedValueException::class); + + EnumFixture::from(EnumFixture::FOO()); + } + + /** + * Contains values not existing in EnumFixture + * @return array + */ + public function invalidValueProvider() + { + return array( + "string" => array('test'), + "int" => array(1234), + ); + } + + /** + * __toString() + * @dataProvider toStringProvider + */ + public function testToString($expected, $enumObject) + { + $this->assertSame($expected, (string) $enumObject); + } + + public function toStringProvider() + { + return array( + array(EnumFixture::FOO, new EnumFixture(EnumFixture::FOO)), + array(EnumFixture::BAR, new EnumFixture(EnumFixture::BAR)), + array((string) EnumFixture::NUMBER, new EnumFixture(EnumFixture::NUMBER)), + ); + } + + /** + * keys() + */ + public function testKeys() + { + $values = EnumFixture::keys(); + $expectedValues = array( + "FOO", + "BAR", + "NUMBER", + "PROBLEMATIC_NUMBER", + "PROBLEMATIC_NULL", + "PROBLEMATIC_EMPTY_STRING", + "PROBLEMATIC_BOOLEAN_FALSE", + ); + + $this->assertSame($expectedValues, $values); + } + + /** + * values() + */ + public function testValues() + { + $values = EnumFixture::values(); + $expectedValues = array( + "FOO" => new EnumFixture(EnumFixture::FOO), + "BAR" => new EnumFixture(EnumFixture::BAR), + "NUMBER" => new EnumFixture(EnumFixture::NUMBER), + "PROBLEMATIC_NUMBER" => new EnumFixture(EnumFixture::PROBLEMATIC_NUMBER), + "PROBLEMATIC_NULL" => new EnumFixture(EnumFixture::PROBLEMATIC_NULL), + "PROBLEMATIC_EMPTY_STRING" => new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING), + "PROBLEMATIC_BOOLEAN_FALSE" => new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE), + ); + + $this->assertEquals($expectedValues, $values); + } + + /** + * toArray() + */ + public function testToArray() + { + $values = EnumFixture::toArray(); + $expectedValues = array( + "FOO" => EnumFixture::FOO, + "BAR" => EnumFixture::BAR, + "NUMBER" => EnumFixture::NUMBER, + "PROBLEMATIC_NUMBER" => EnumFixture::PROBLEMATIC_NUMBER, + "PROBLEMATIC_NULL" => EnumFixture::PROBLEMATIC_NULL, + "PROBLEMATIC_EMPTY_STRING" => EnumFixture::PROBLEMATIC_EMPTY_STRING, + "PROBLEMATIC_BOOLEAN_FALSE" => EnumFixture::PROBLEMATIC_BOOLEAN_FALSE, + ); + + $this->assertSame($expectedValues, $values); + } + + /** + * __callStatic() + */ + public function testStaticAccess() + { + $this->assertEquals(new EnumFixture(EnumFixture::FOO), EnumFixture::FOO()); + $this->assertEquals(new EnumFixture(EnumFixture::BAR), EnumFixture::BAR()); + $this->assertEquals(new EnumFixture(EnumFixture::NUMBER), EnumFixture::NUMBER()); + $this->assertNotSame(EnumFixture::NUMBER(), EnumFixture::NUMBER()); + } + + public function testBadStaticAccess() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('No static method or enum constant \'UNKNOWN\' in class ' . EnumFixture::class); + + EnumFixture::UNKNOWN(); + } + + /** + * isValid() + * @dataProvider isValidProvider + */ + public function testIsValid($value, $isValid) + { + $this->assertSame($isValid, EnumFixture::isValid($value)); + } + + public function isValidProvider() + { + return [ + /** + * Valid values + */ + ['foo', true], + [42, true], + [null, true], + [0, true], + ['', true], + [false, true], + /** + * Invalid values + */ + ['baz', false] + ]; + } + + /** + * isValidKey() + */ + public function testIsValidKey() + { + $this->assertTrue(EnumFixture::isValidKey('FOO')); + $this->assertFalse(EnumFixture::isValidKey('BAZ')); + $this->assertTrue(EnumFixture::isValidKey('PROBLEMATIC_NULL')); + } + + /** + * search() + * @see https://github.com/myclabs/php-enum/issues/13 + * @dataProvider searchProvider + */ + public function testSearch($value, $expected) + { + $this->assertSame($expected, EnumFixture::search($value)); + } + + public function searchProvider() + { + return array( + array('foo', 'FOO'), + array(0, 'PROBLEMATIC_NUMBER'), + array(null, 'PROBLEMATIC_NULL'), + array('', 'PROBLEMATIC_EMPTY_STRING'), + array(false, 'PROBLEMATIC_BOOLEAN_FALSE'), + array('bar I do not exist', false), + array(array(), false), + ); + } + + /** + * equals() + */ + public function testEquals() + { + $foo = new EnumFixture(EnumFixture::FOO); + $number = new EnumFixture(EnumFixture::NUMBER); + $anotherFoo = new EnumFixture(EnumFixture::FOO); + $objectOfDifferentClass = new \stdClass(); + $notAnObject = 'foo'; + + $this->assertTrue($foo->equals($foo)); + $this->assertFalse($foo->equals($number)); + $this->assertTrue($foo->equals($anotherFoo)); + $this->assertFalse($foo->equals(null)); + $this->assertFalse($foo->equals($objectOfDifferentClass)); + $this->assertFalse($foo->equals($notAnObject)); + } + + /** + * equals() + */ + public function testEqualsComparesProblematicValuesProperly() + { + $false = new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE); + $emptyString = new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING); + $null = new EnumFixture(EnumFixture::PROBLEMATIC_NULL); + + $this->assertTrue($false->equals($false)); + $this->assertFalse($false->equals($emptyString)); + $this->assertFalse($emptyString->equals($null)); + $this->assertFalse($null->equals($false)); + } + + /** + * equals() + */ + public function testEqualsConflictValues() + { + $this->assertFalse(EnumFixture::FOO()->equals(EnumConflict::FOO())); + } + + /** + * jsonSerialize() + */ + public function testJsonSerialize() + { + $this->assertJsonEqualsJson('"foo"', json_encode(new EnumFixture(EnumFixture::FOO))); + $this->assertJsonEqualsJson('"bar"', json_encode(new EnumFixture(EnumFixture::BAR))); + $this->assertJsonEqualsJson('42', json_encode(new EnumFixture(EnumFixture::NUMBER))); + $this->assertJsonEqualsJson('0', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_NUMBER))); + $this->assertJsonEqualsJson('null', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_NULL))); + $this->assertJsonEqualsJson('""', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_EMPTY_STRING))); + $this->assertJsonEqualsJson('false', json_encode(new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE))); + } + + public function testNullableEnum() + { + $this->assertNull(EnumFixture::PROBLEMATIC_NULL()->getValue()); + $this->assertNull((new EnumFixture(EnumFixture::PROBLEMATIC_NULL))->getValue()); + $this->assertNull((new EnumFixture(EnumFixture::PROBLEMATIC_NULL))->jsonSerialize()); + } + + public function testBooleanEnum() + { + $this->assertFalse(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE()->getValue()); + $this->assertFalse((new EnumFixture(EnumFixture::PROBLEMATIC_BOOLEAN_FALSE))->jsonSerialize()); + } + + public function testConstructWithSameEnumArgument() + { + $enum = new EnumFixture(EnumFixture::FOO); + + $enveloped = new EnumFixture($enum); + + $this->assertEquals($enum, $enveloped); + } + + private function assertJsonEqualsJson($json1, $json2) + { + $this->assertJsonStringEqualsJsonString($json1, $json2); + } + + public function testSerialize() + { + $bin = bin2hex(serialize(EnumFixture::FOO())); + + $this->assertEquals($bin, bin2hex(serialize(EnumFixture::FOO()))); + } + + public function testUnserializeVersionWithoutKey() + { + $bin = bin2hex(serialize(EnumFixture::FOO())); + + /* @var $value EnumFixture */ + $value = unserialize(pack('H*', $bin)); + + $this->assertEquals(EnumFixture::FOO, $value->getValue()); + $this->assertTrue(EnumFixture::FOO()->equals($value)); + $this->assertTrue(EnumFixture::FOO() == $value); + } + + public function testUnserialize() + { + $bin = bin2hex(serialize(EnumFixture::FOO())); + + /* @var $value EnumFixture */ + $value = unserialize(pack('H*', $bin)); + + $this->assertEquals(EnumFixture::FOO, $value->getValue()); + $this->assertTrue(EnumFixture::FOO()->equals($value)); + $this->assertTrue(EnumFixture::FOO() == $value); + } + + /** + * @see https://github.com/myclabs/php-enum/issues/95 + */ + public function testEnumValuesInheritance() + { + $this->expectException(\UnexpectedValueException::class); + $inheritedEnumFixture = InheritedEnumFixture::VALUE(); + new EnumFixture($inheritedEnumFixture); + } + + /** + * @dataProvider isValidProvider + */ + public function testAssertValidValue($value, $isValid): void + { + if (!$isValid) { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("Value '$value' is not part of the enum " . EnumFixture::class); + } + + EnumFixture::assertValidValue($value); + + self::assertTrue(EnumFixture::isValid($value)); + } +} From 2b5a79e2ddcdba5b3765c607b3294c019e405eef Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 15 Oct 2024 16:40:43 -0400 Subject: [PATCH 11/58] Feature: add global settings for honeypot field (#7546) Co-authored-by: Jon Waldstein Co-authored-by: Jon Waldstein --- give.php | 2 + .../AddHoneyPotFieldToDonationForms.php | 17 ++++-- src/DonationForms/ServiceProvider.php | 20 ++++++- .../Security/Actions/RegisterPage.php | 21 +++++++ .../Security/Actions/RegisterSection.php | 19 ++++++ .../Security/Actions/RegisterSettings.php | 60 +++++++++++++++++++ .../Security/SecuritySettingsPage.php | 22 +++++++ src/Settings/ServiceProvider.php | 42 +++++++++++++ .../DonateFormDataTest.php | 3 + .../DonateFormRouteDataTest.php | 6 ++ .../TestAddHoneyPotFieldToDonationForms.php | 16 +++-- .../TestDonationFormRepository.php | 3 + 12 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 src/Settings/Security/Actions/RegisterPage.php create mode 100644 src/Settings/Security/Actions/RegisterSection.php create mode 100644 src/Settings/Security/Actions/RegisterSettings.php create mode 100644 src/Settings/Security/SecuritySettingsPage.php create mode 100644 src/Settings/ServiceProvider.php diff --git a/give.php b/give.php index e67832a60e..e3c6c03e7d 100644 --- a/give.php +++ b/give.php @@ -190,6 +190,7 @@ final class Give private $container; /** + * @unreleased added Settings service provider * @since 2.25.0 added HttpServiceProvider * @since 2.19.6 added Donors, Donations, and Subscriptions * @since 2.8.0 @@ -241,6 +242,7 @@ final class Give Give\BetaFeatures\ServiceProvider::class, Give\FormTaxonomies\ServiceProvider::class, Give\DonationSpam\ServiceProvider::class, + Give\Settings\ServiceProvider::class ]; /** diff --git a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php index 14b16c5e85..1e6ff7d054 100644 --- a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php +++ b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php @@ -13,17 +13,18 @@ class AddHoneyPotFieldToDonationForms { /** + * @unreleased added parameter $honeypotFieldName * @since 3.16.2 * @throws EmptyNameException */ - public function __invoke(DonationForm $form): void + public function __invoke(DonationForm $form, string $honeypotFieldName): void { $formNodes = $form->all(); $lastSection = $form->count() ? $formNodes[$form->count() - 1] : null; - if ($lastSection) { - $field = Honeypot::make('donationBirthday') - ->label('Donation Birthday') + if ($lastSection && is_null($form->getNodeByName($honeypotFieldName))) { + $field = Honeypot::make($honeypotFieldName) + ->label($this->generateLabelFromFieldName($honeypotFieldName)) ->scope('honeypot') ->showInAdmin(false) ->showInReceipt(false) @@ -32,4 +33,12 @@ public function __invoke(DonationForm $form): void $lastSection->append($field); } } + + /** + * @unreleased + */ + private function generateLabelFromFieldName(string $honeypotFieldName): string + { + return ucwords(trim(implode(" ", preg_split("/(?=[A-Z])/", $honeypotFieldName)))); + } } diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index 28b36adbb6..475bdc17d0 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -363,8 +363,24 @@ protected function registerPostStatus() private function registerHoneyPotField(): void { add_action('givewp_donation_form_schema', function (DonationFormModel $form, int $formId) { - if (apply_filters('givewp_donation_forms_honeypot_enabled', false, $formId)) { - (new AddHoneyPotFieldToDonationForms())($form); + /** + * Check if the honeypot field is enabled + * @param bool $enabled + * @param int $formId + * + * @since 3.16.2 + */ + if (apply_filters('givewp_donation_forms_honeypot_enabled', give_is_setting_enabled(give_get_option( 'givewp_donation_forms_honeypot_enabled', 'enabled')), $formId)) { + /** + * Filter the honeypot field name + * @param string $honeypotFieldName + * @param int $formId + * + * @unreleased + */ + $honeypotFieldName = (string)apply_filters('givewp_donation_forms_honeypot_field_name', 'donationBirthday', $formId); + + (new AddHoneyPotFieldToDonationForms())($form, $honeypotFieldName); } }, 10, 2); } diff --git a/src/Settings/Security/Actions/RegisterPage.php b/src/Settings/Security/Actions/RegisterPage.php new file mode 100644 index 0000000000..714796746d --- /dev/null +++ b/src/Settings/Security/Actions/RegisterPage.php @@ -0,0 +1,21 @@ +getSettings(); + } + + /** + * @unreleased + */ + protected function getSettings(): array + { + return [ + [ + 'id' => 'give_title_settings_security_1', + 'type' => 'title', + ], + $this->getHoneypotSettings(), + [ + 'id' => 'give_title_settings_security_1', + 'type' => 'sectionend', + ], + ]; + } + + /** + * @unreleased + */ + public function getHoneypotSettings(): array + { + return [ + 'name' => __('Enable Honeypot Field', 'give'), + 'desc' => __( + 'If enabled, this option will add a honeypot security measure to all donation forms', + 'give' + ), + 'id' => 'givewp_donation_forms_honeypot_enabled', + 'type' => 'radio_inline', + 'default' => 'disabled', + 'options' => [ + 'enabled' => __('Enabled', 'give'), + 'disabled' => __('Disabled', 'give'), + ], + ]; + } +} diff --git a/src/Settings/Security/SecuritySettingsPage.php b/src/Settings/Security/SecuritySettingsPage.php new file mode 100644 index 0000000000..d401d17131 --- /dev/null +++ b/src/Settings/Security/SecuritySettingsPage.php @@ -0,0 +1,22 @@ +id = 'security'; + $this->label = __( 'Security', 'give' ); + $this->default_tab = 'security'; + + parent::__construct(); + } +} diff --git a/src/Settings/ServiceProvider.php b/src/Settings/ServiceProvider.php new file mode 100644 index 0000000000..7848073271 --- /dev/null +++ b/src/Settings/ServiceProvider.php @@ -0,0 +1,42 @@ +registerSecuritySettings(); + } + + /** + * @unreleased + */ + private function registerSecuritySettings(): void + { + Hooks::addFilter('give-settings_get_settings_pages', RegisterPage::class); + Hooks::addFilter('give_get_sections_security', RegisterSection::class); + Hooks::addFilter('give_get_settings_security', RegisterSettings::class); + } +} diff --git a/tests/Unit/DataTransferObjects/DonateFormDataTest.php b/tests/Unit/DataTransferObjects/DonateFormDataTest.php index 6e325cef5a..6e521b9d15 100644 --- a/tests/Unit/DataTransferObjects/DonateFormDataTest.php +++ b/tests/Unit/DataTransferObjects/DonateFormDataTest.php @@ -26,6 +26,7 @@ class DonateFormDataTest extends TestCase use RefreshDatabase; /** + * @unreleased updated to ignore honeypot field * @since 3.0.0 * * @return void @@ -44,6 +45,8 @@ public function testShouldTransformToDonationModel() return TestGateway::id(); }); + add_filter("givewp_donation_forms_honeypot_enabled", "__return_false"); + $data = (object)[ 'gatewayId' => TestGateway::id(), 'amount' => 50, diff --git a/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php b/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php index 06031a2625..5aec682817 100644 --- a/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php +++ b/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php @@ -23,6 +23,7 @@ class DonateFormRouteDataTest extends TestCase { /** + * @unreleased updated to ignore honeypot field * @since 3.0.0 */ public function testValidatedShouldReturnValidatedData() @@ -38,6 +39,8 @@ public function testValidatedShouldReturnValidatedData() return TestGateway::id(); }); + add_filter("givewp_donation_forms_honeypot_enabled", "__return_false"); + $customFieldBlockModel = BlockModel::make([ 'name' => 'givewp/section', 'attributes' => ['title' => '', 'description' => ''], @@ -96,6 +99,7 @@ public function testValidatedShouldReturnValidatedData() } /** + * @unreleased updated to ignore honeypot field * @since 3.0.0 */ public function testValidatedShouldReturnValidatedDataWithSubscriptionData() @@ -111,6 +115,8 @@ public function testValidatedShouldReturnValidatedDataWithSubscriptionData() return TestGateway::id(); }); + add_filter("givewp_donation_forms_honeypot_enabled", "__return_false"); + $customFieldBlockModel = BlockModel::make([ 'name' => 'givewp/section', 'attributes' => ['title' => '', 'description' => ''], diff --git a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php index fbee568eab..80dc31844e 100644 --- a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php +++ b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php @@ -20,20 +20,28 @@ class TestAddHoneyPotFieldToDonationForms extends TestCase use RefreshDatabase; /** + * @unreleased updated to assert field attributes * @since 3.16.2 * @throws NameCollisionException|EmptyNameException|TypeNotSupported */ public function testShouldAddHoneyPotFieldToDonationForms(): void { + $fieldName = 'myHoneypotFieldName'; $formNode = new DonationForm('donation-form'); $formNode->append(Section::make('section-1'), Section::make('section-2'), Section::make('section-3')); $action = new AddHoneyPotFieldToDonationForms(); - $action($formNode, 1); - + $action($formNode, $fieldName); /** @var Section $lastSection */ $lastSection = $formNode->getNodeByName('section-3'); - $this->assertNotNull($lastSection->getNodeByName('donationBirthday')); - $this->assertInstanceOf(Honeypot::class, $lastSection->getNodeByName('donationBirthday')); + + /** @var Honeypot $field */ + $field = $lastSection->getNodeByName($fieldName); + $this->assertNotNull($field); + $this->assertInstanceOf(Honeypot::class, $field); + $this->assertSame('My Honeypot Field Name', $field->getLabel()); + $this->assertTrue($field->hasRule('honeypot')); + $this->assertFalse($field->shouldShowInAdmin()); + $this->assertFalse($field->shouldShowInReceipt()); } } diff --git a/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php b/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php index 143f7e6443..7946a26486 100644 --- a/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php +++ b/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php @@ -252,6 +252,7 @@ public function testIsLegacyFormShouldReturnFalseIfNotLegacy() /** + * @unreleased updated to disable honeypot * @since 3.0.0 * * @return void @@ -286,6 +287,8 @@ public function testGetFormSchemaFromBlocksShouldReturnFormSchema() $blocks = BlockCollection::make([$block]); + add_filter("givewp_donation_forms_honeypot_enabled", "__return_false"); + /** @var Form $formSchema */ $formSchema = $this->repository->getFormSchemaFromBlocks($formId, $blocks); From 9d76baab373bd19a5211b1ac789391f271c26119 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 15 Oct 2024 16:46:41 -0400 Subject: [PATCH 12/58] chore: merge develop, update since tags and readme --- readme.txt | 2 ++ .../Actions/AddHoneyPotFieldToDonationForms.php | 4 ++-- src/DonationForms/ServiceProvider.php | 2 +- src/Framework/Support/ValueObjects/BaseEnum.php | 2 +- src/Settings/Security/Actions/RegisterPage.php | 4 ++-- src/Settings/Security/Actions/RegisterSection.php | 4 ++-- src/Settings/Security/Actions/RegisterSettings.php | 8 ++++---- src/Settings/Security/SecuritySettingsPage.php | 4 ++-- src/Settings/ServiceProvider.php | 8 ++++---- tests/Framework/Support/ValueObjects/TestBaseEnum.php | 2 +- tests/Unit/DataTransferObjects/DonateFormDataTest.php | 2 +- .../Unit/DataTransferObjects/DonateFormRouteDataTest.php | 4 ++-- .../Actions/TestAddHoneyPotFieldToDonationForms.php | 2 +- .../Repositories/TestDonationFormRepository.php | 2 +- 14 files changed, 26 insertions(+), 24 deletions(-) diff --git a/readme.txt b/readme.txt index c303d9176d..da1365a35a 100644 --- a/readme.txt +++ b/readme.txt @@ -267,6 +267,8 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == = 3.17.0: October 16th, 2024 = +* New: Added new security tab with option to enable a honeypot field for visual builder forms +* Dev: Resolved php 8.1 compatability conflict with MyCLabs\Enum\Enum::jsonSerialize() = 3.16.5: October 15th, 2024 = * Fix: Resolved a PHP v8+ fatal error on option-based forms when the Tributes add-on was enabled diff --git a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php index 1e6ff7d054..c4d45963eb 100644 --- a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php +++ b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php @@ -13,7 +13,7 @@ class AddHoneyPotFieldToDonationForms { /** - * @unreleased added parameter $honeypotFieldName + * @since 3.17.0 added parameter $honeypotFieldName * @since 3.16.2 * @throws EmptyNameException */ @@ -35,7 +35,7 @@ public function __invoke(DonationForm $form, string $honeypotFieldName): void } /** - * @unreleased + * @since 3.17.0 */ private function generateLabelFromFieldName(string $honeypotFieldName): string { diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index 475bdc17d0..80f81a0384 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -376,7 +376,7 @@ private function registerHoneyPotField(): void * @param string $honeypotFieldName * @param int $formId * - * @unreleased + * @since 3.17.0 */ $honeypotFieldName = (string)apply_filters('givewp_donation_forms_honeypot_field_name', 'donationBirthday', $formId); diff --git a/src/Framework/Support/ValueObjects/BaseEnum.php b/src/Framework/Support/ValueObjects/BaseEnum.php index a215797798..ea3f6f8725 100644 --- a/src/Framework/Support/ValueObjects/BaseEnum.php +++ b/src/Framework/Support/ValueObjects/BaseEnum.php @@ -3,7 +3,7 @@ namespace Give\Framework\Support\ValueObjects; /** - * @unreleased + * @since 3.17.0 * * This is a fork of the myclabs/php-enum library 1.8.4 with the Stringable interface removed. * diff --git a/src/Settings/Security/Actions/RegisterPage.php b/src/Settings/Security/Actions/RegisterPage.php index 714796746d..378bca8601 100644 --- a/src/Settings/Security/Actions/RegisterPage.php +++ b/src/Settings/Security/Actions/RegisterPage.php @@ -5,12 +5,12 @@ use Give\Settings\Security\SecuritySettingsPage; /** - * @unreleased + * @since 3.17.0 */ class RegisterPage { /** - * @unreleased + * @since 3.17.0 */ public function __invoke(array $settingsPages): array { diff --git a/src/Settings/Security/Actions/RegisterSection.php b/src/Settings/Security/Actions/RegisterSection.php index 0b9e4502f8..5c5c11017a 100644 --- a/src/Settings/Security/Actions/RegisterSection.php +++ b/src/Settings/Security/Actions/RegisterSection.php @@ -3,12 +3,12 @@ namespace Give\Settings\Security\Actions; /** - * @unreleased + * @since 3.17.0 */ class RegisterSection { /** - * @unreleased + * @since 3.17.0 */ public function __invoke(array $sections): array { diff --git a/src/Settings/Security/Actions/RegisterSettings.php b/src/Settings/Security/Actions/RegisterSettings.php index 718964cb2c..e6c3cf8721 100644 --- a/src/Settings/Security/Actions/RegisterSettings.php +++ b/src/Settings/Security/Actions/RegisterSettings.php @@ -3,12 +3,12 @@ namespace Give\Settings\Security\Actions; /** - * @unreleased + * @since 3.17.0 */ class RegisterSettings { /** - * @unreleased + * @since 3.17.0 */ public function __invoke(array $settings): array { @@ -20,7 +20,7 @@ public function __invoke(array $settings): array } /** - * @unreleased + * @since 3.17.0 */ protected function getSettings(): array { @@ -38,7 +38,7 @@ protected function getSettings(): array } /** - * @unreleased + * @since 3.17.0 */ public function getHoneypotSettings(): array { diff --git a/src/Settings/Security/SecuritySettingsPage.php b/src/Settings/Security/SecuritySettingsPage.php index d401d17131..9147d1b82c 100644 --- a/src/Settings/Security/SecuritySettingsPage.php +++ b/src/Settings/Security/SecuritySettingsPage.php @@ -5,12 +5,12 @@ use Give_Settings_Page; /** - * @unreleased + * @since 3.17.0 */ class SecuritySettingsPage extends Give_Settings_Page { /** - * @unreleased + * @since 3.17.0 */ public function __construct() { $this->id = 'security'; diff --git a/src/Settings/ServiceProvider.php b/src/Settings/ServiceProvider.php index 7848073271..776aa5e27f 100644 --- a/src/Settings/ServiceProvider.php +++ b/src/Settings/ServiceProvider.php @@ -11,19 +11,19 @@ /** * Class ServiceProvider * - * @unreleased + * @since 3.17.0 */ class ServiceProvider implements ServiceProviderInterface { /** - * @unreleased + * @since 3.17.0 */ public function register() { } /** - * @unreleased + * @since 3.17.0 */ public function boot() { @@ -31,7 +31,7 @@ public function boot() } /** - * @unreleased + * @since 3.17.0 */ private function registerSecuritySettings(): void { diff --git a/tests/Framework/Support/ValueObjects/TestBaseEnum.php b/tests/Framework/Support/ValueObjects/TestBaseEnum.php index c508dd8576..f9d354f82a 100644 --- a/tests/Framework/Support/ValueObjects/TestBaseEnum.php +++ b/tests/Framework/Support/ValueObjects/TestBaseEnum.php @@ -11,7 +11,7 @@ * * Updated for GiveWP use case. * - * @unreleased + * @since 3.17.0 */ class TestBaseEnum extends TestCase { diff --git a/tests/Unit/DataTransferObjects/DonateFormDataTest.php b/tests/Unit/DataTransferObjects/DonateFormDataTest.php index 6e521b9d15..f8616e0b52 100644 --- a/tests/Unit/DataTransferObjects/DonateFormDataTest.php +++ b/tests/Unit/DataTransferObjects/DonateFormDataTest.php @@ -26,7 +26,7 @@ class DonateFormDataTest extends TestCase use RefreshDatabase; /** - * @unreleased updated to ignore honeypot field + * @since 3.17.0 updated to ignore honeypot field * @since 3.0.0 * * @return void diff --git a/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php b/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php index 5aec682817..8e8133328b 100644 --- a/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php +++ b/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php @@ -23,7 +23,7 @@ class DonateFormRouteDataTest extends TestCase { /** - * @unreleased updated to ignore honeypot field + * @since 3.17.0 updated to ignore honeypot field * @since 3.0.0 */ public function testValidatedShouldReturnValidatedData() @@ -99,7 +99,7 @@ public function testValidatedShouldReturnValidatedData() } /** - * @unreleased updated to ignore honeypot field + * @since 3.17.0 updated to ignore honeypot field * @since 3.0.0 */ public function testValidatedShouldReturnValidatedDataWithSubscriptionData() diff --git a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php index 80dc31844e..6deb2fcc46 100644 --- a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php +++ b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php @@ -20,7 +20,7 @@ class TestAddHoneyPotFieldToDonationForms extends TestCase use RefreshDatabase; /** - * @unreleased updated to assert field attributes + * @since 3.17.0 updated to assert field attributes * @since 3.16.2 * @throws NameCollisionException|EmptyNameException|TypeNotSupported */ diff --git a/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php b/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php index 7946a26486..79ea8175cf 100644 --- a/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php +++ b/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php @@ -252,7 +252,7 @@ public function testIsLegacyFormShouldReturnFalseIfNotLegacy() /** - * @unreleased updated to disable honeypot + * @since 3.17.0 updated to disable honeypot * @since 3.0.0 * * @return void From 68949f75c3c899aa29d3b5efa4855c3a3ac83e5e Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 16 Oct 2024 09:58:36 -0400 Subject: [PATCH 13/58] chore: prevent php warning --- src/DonationForms/Actions/PrintFormMetaTags.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/DonationForms/Actions/PrintFormMetaTags.php b/src/DonationForms/Actions/PrintFormMetaTags.php index 47f1377628..877496ef51 100644 --- a/src/DonationForms/Actions/PrintFormMetaTags.php +++ b/src/DonationForms/Actions/PrintFormMetaTags.php @@ -6,6 +6,7 @@ use Give\Helpers\Form\Utils; /** + * @since 3.17.0 updated to account for $post being null * @since 3.16.0 */ class PrintFormMetaTags @@ -15,13 +16,15 @@ public function __invoke() global $post; if ( + isset($post->post_type) && $post->post_type === 'give_forms' && Utils::isV3Form($post->ID) ) { + /** @var $form $form */ $form = DonationForm::find($post->ID); // og:image - if ( ! empty($form->settings->designSettingsImageUrl)) { + if ($form && !empty($form->settings->designSettingsImageUrl)) { printf('', esc_url($form->settings->designSettingsImageUrl)); } } From b3d615c3b5557cb43f9fe52582035d04c03f3af2 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 16 Oct 2024 15:29:04 -0400 Subject: [PATCH 14/58] Refactor: update Honorific block to only accept labels and add field validation (#7564) Co-authored-by: Jon Waldstein Co-authored-by: Jon Waldstein --- package-lock.json | 14 +-- package.json | 2 +- .../ConvertDonationFormBlocksToFieldsApi.php | 16 ++- .../ViewModels/FormBuilderViewModel.php | 2 +- .../src/blocks/fields/donor-name/Edit.tsx | 110 +++++++++--------- .../src/blocks/fields/donor-name/settings.tsx | 2 +- .../ViewModels/FormBuilderViewModelTest.php | 2 +- 7 files changed, 82 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82ec065d65..8a1a72f537 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.12", "@givewp/design-system-foundation": "^1.1.0", - "@givewp/form-builder-library": "^1.6.0", + "@givewp/form-builder-library": "^1.7.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^2.9.10", "@paypal/paypal-js": "^5.1.4", @@ -2549,9 +2549,9 @@ "integrity": "sha512-SOAS98QQOytIGsyDX55y4TCS0DeKijjmOPnNaG0YbClTL2u7HFNthqRHk246BXZ0s6U+CUzqZQ8mf/+3NY4Z1g==" }, "node_modules/@givewp/form-builder-library": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@givewp/form-builder-library/-/form-builder-library-1.6.0.tgz", - "integrity": "sha512-I/ZLIFHbWSZU+PR3urCyvFR/kiSV0YZI2rBqjBT8/sYbEzh/IXNTbGdp5/1hvzpsGzVN/aThGDut71JxW0oKvA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@givewp/form-builder-library/-/form-builder-library-1.7.0.tgz", + "integrity": "sha512-7u0wbs27xizgKQCodA3ZPyuaVfX5VFYv+mknRY5x5RO4GJ/FfUQhbAr6G6t0doCKii0NePABs0l4rrCU05Q5wQ==", "dependencies": { "@wordpress/components": "^25.10.0", "@wordpress/compose": "^6.21.0", @@ -37054,9 +37054,9 @@ "integrity": "sha512-SOAS98QQOytIGsyDX55y4TCS0DeKijjmOPnNaG0YbClTL2u7HFNthqRHk246BXZ0s6U+CUzqZQ8mf/+3NY4Z1g==" }, "@givewp/form-builder-library": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@givewp/form-builder-library/-/form-builder-library-1.6.0.tgz", - "integrity": "sha512-I/ZLIFHbWSZU+PR3urCyvFR/kiSV0YZI2rBqjBT8/sYbEzh/IXNTbGdp5/1hvzpsGzVN/aThGDut71JxW0oKvA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@givewp/form-builder-library/-/form-builder-library-1.7.0.tgz", + "integrity": "sha512-7u0wbs27xizgKQCodA3ZPyuaVfX5VFYv+mknRY5x5RO4GJ/FfUQhbAr6G6t0doCKii0NePABs0l4rrCU05Q5wQ==", "requires": { "@wordpress/components": "^25.10.0", "@wordpress/compose": "^6.21.0", diff --git a/package.json b/package.json index c84655c258..de52cc064d 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.12", "@givewp/design-system-foundation": "^1.1.0", - "@givewp/form-builder-library": "^1.6.0", + "@givewp/form-builder-library": "^1.7.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^2.9.10", "@paypal/paypal-js": "^5.1.4", diff --git a/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php b/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php index 34c18dd32a..4de295d50f 100644 --- a/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php +++ b/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php @@ -261,6 +261,8 @@ protected function createNodeFromBlockWithUniqueAttributes(BlockModel $block, in } /** + * @unreleased updated honorific field with validation, global options, and user defaults + * * @since 3.0.0 */ protected function createNodeFromDonorNameBlock(BlockModel $block): Node @@ -293,9 +295,17 @@ protected function createNodeFromDonorNameBlock(BlockModel $block): Node if ($block->hasAttribute('showHonorific') && $block->getAttribute('showHonorific') === true) { - $group->getNodeByName('honorific') - ->label('Title') - ->options(...array_values($block->getAttribute('honorifics'))); + $options = array_filter(array_values((array)$block->getAttribute('honorifics'))); + if ($block->hasAttribute('useGlobalSettings') && $block->getAttribute('useGlobalSettings') === true) { + $options = give_get_option('title_prefixes', give_get_default_title_prefixes()); + } + + if (!empty($options)){ + $group->getNodeByName('honorific') + ->label(__('Title', 'give')) + ->options(...$options) + ->rules('max:255', 'in:' . implode(',', $options)); + } } else { $group->remove('honorific'); } diff --git a/src/FormBuilder/ViewModels/FormBuilderViewModel.php b/src/FormBuilder/ViewModels/FormBuilderViewModel.php index 769238033b..df9924eabb 100644 --- a/src/FormBuilder/ViewModels/FormBuilderViewModel.php +++ b/src/FormBuilder/ViewModels/FormBuilderViewModel.php @@ -81,7 +81,7 @@ public function storageData(int $donationFormId): array ], 'goalTypeOptions' => $this->getGoalTypeOptions(), 'goalProgressOptions' => $this->getGoalProgressOptions(), - 'nameTitlePrefixes' => give_get_option('title_prefixes'), + 'nameTitlePrefixes' => give_get_option('title_prefixes', array_values(give_get_default_title_prefixes())), 'isExcerptEnabled' => give_is_setting_enabled(give_get_option('forms_excerpt')), 'intlTelInputSettings' => IntlTelInput::getSettings(), ]; diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/Edit.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/Edit.tsx index 159f907397..4db4f5f7d3 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/Edit.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/Edit.tsx @@ -2,16 +2,48 @@ import {__} from '@wordpress/i18n'; import {BlockEditProps} from '@wordpress/blocks'; import {PanelBody, PanelRow, SelectControl, TextControl, ToggleControl} from '@wordpress/components'; import {InspectorControls} from '@wordpress/block-editor'; -import {useEffect, useState} from 'react'; +import {useState} from 'react'; import {OptionsPanel} from '@givewp/form-builder-library'; import type {OptionProps} from '@givewp/form-builder-library/build/OptionsPanel/types'; import {getFormBuilderWindowData} from '@givewp/form-builder/common/getWindowData'; const titleLabelTransform = (token = '') => token.charAt(0).toUpperCase() + token.slice(1); const titleValueTransform = (token = '') => token.trim().toLowerCase(); +const convertHonorificsToOptions = (honorifics: string[], defaultValue?: string) => + honorifics?.filter(label => label.length > 0).map((honorific: string) => ({ + label: titleLabelTransform(honorific), + value: titleValueTransform(honorific), + checked: defaultValue ? defaultValue === honorific : honorifics[0] === honorific + }) as OptionProps + ); + +const convertOptionsToHonorifics = (options: OptionProps[]) => { + const honorifics = []; + Object.values(options).forEach((option) => { + if (option.label.length > 0) { + honorifics.push(option.label); + } + }); + + return honorifics; +} + +type Attributes = { + showHonorific: boolean; + useGlobalSettings: boolean; + honorifics: string[]; + firstNameLabel: string; + firstNamePlaceholder: string; + lastNameLabel: string; + lastNamePlaceholder: string; + requireLastName: boolean; +} export default function Edit({ - attributes: { + attributes, + setAttributes + }: BlockEditProps) { + const { showHonorific, useGlobalSettings, honorifics, @@ -19,67 +51,40 @@ export default function Edit({ firstNamePlaceholder, lastNameLabel, lastNamePlaceholder, - requireLastName, - }, - setAttributes, -}: BlockEditProps) { + requireLastName + } = attributes as Attributes; + const globalHonorifics = getFormBuilderWindowData().nameTitlePrefixes; const [selectedTitle, setSelectedTitle] = useState((Object.values(honorifics)[0] as string) ?? ''); const [honorificOptions, setHonorificOptions] = useState( - Object.values(honorifics).map((token: string) => { - return { - label: titleLabelTransform(token), - value: titleValueTransform(token), - checked: selectedTitle === token, - } as OptionProps; - }) + convertHonorificsToOptions(Object.values(honorifics), selectedTitle) ); const setOptions = (options: OptionProps[]) => { setHonorificOptions(options); - const filtered = {}; - // Filter options - Object.values(options).forEach((option) => { - Object.assign(filtered, {[option.label]: option.label}); - }); - - setAttributes({honorifics: filtered}); + setAttributes({ honorifics: convertOptionsToHonorifics(options) }); }; if (typeof useGlobalSettings === 'undefined') { - setAttributes({useGlobalSettings: true}); + setAttributes({ useGlobalSettings: true }); } - useEffect(() => { - const options = !!useGlobalSettings ? getFormBuilderWindowData().nameTitlePrefixes : ['Mr', 'Ms', 'Mrs']; - - setOptions( - Object.values(options).map((token: string) => { - return { - label: titleLabelTransform(token), - value: titleValueTransform(token), - checked: selectedTitle === token, - } as OptionProps; - }) - ); - }, [useGlobalSettings]); - return ( <>
0 ? '1fr 2fr 2fr' : '1fr 1fr', - gap: '15px', + gap: '15px' }} > {!!showHonorific && ( )} setAttributes({showHonorific: !showHonorific})} + onChange={() => setAttributes({ showHonorific: !showHonorific })} help={__( - "Do you want to add a name title prefix dropdown field before the donor's first name field? This will display a dropdown with options such as Mrs, Miss, Ms, Sir, and Dr for the donor to choose from.", + 'Do you want to add a name title prefix dropdown field before the donor\'s first name field? This will display a dropdown with options such as Mrs, Miss, Ms, Sir, and Dr for the donor to choose from.', 'give' )} /> {!!showHonorific && ( -
+
setAttributes({useGlobalSettings: !useGlobalSettings})} - value={useGlobalSettings} + onChange={() => setAttributes({ useGlobalSettings: !useGlobalSettings })} + value={useGlobalSettings ? 'true' : 'false'} options={[ - {label: __('Global', 'give'), value: 'true'}, - {label: __('Customize', 'give'), value: 'false'}, + { label: __('Global', 'give'), value: 'true' }, + { label: __('Customize', 'give'), value: 'false' } ]} />
@@ -137,7 +142,7 @@ export default function Edit({ fontSize: '0.75rem', lineHeight: '120%', fontWeight: 400, - marginTop: '0.5rem', + marginTop: '0.5rem' }} > {__(' Go to the settings to change the ')} @@ -155,12 +160,13 @@ export default function Edit({ )} {!!showHonorific && !useGlobalSettings && ( -
+
@@ -171,14 +177,14 @@ export default function Edit({ setAttributes({firstNameLabel: value})} + onChange={(value) => setAttributes({ firstNameLabel: value })} /> setAttributes({firstNamePlaceholder: value})} + onChange={(value) => setAttributes({ firstNamePlaceholder: value })} /> @@ -187,21 +193,21 @@ export default function Edit({ setAttributes({lastNameLabel: value})} + onChange={(value) => setAttributes({ lastNameLabel: value })} /> setAttributes({lastNamePlaceholder: value})} + onChange={(value) => setAttributes({ lastNamePlaceholder: value })} /> setAttributes({requireLastName: !requireLastName})} + onChange={() => setAttributes({ requireLastName: !requireLastName })} help={__('Do you want to force the Last Name field to be required?', 'give')} /> diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/settings.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/settings.tsx index a5fcbcce57..3add6edfb2 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/settings.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/donor-name/settings.tsx @@ -23,7 +23,7 @@ const settings: FieldBlock['settings'] = { }, honorifics: { type: 'array', - default: ['Mr', 'Ms', 'Mrs'], + default: [__('Mr.', 'give'), __('Ms.', 'give'), __('Mrs.', 'give')], }, firstNameLabel: { type: 'string', diff --git a/tests/Unit/ViewModels/FormBuilderViewModelTest.php b/tests/Unit/ViewModels/FormBuilderViewModelTest.php index 34895f2fc7..2ef60a3b02 100644 --- a/tests/Unit/ViewModels/FormBuilderViewModelTest.php +++ b/tests/Unit/ViewModels/FormBuilderViewModelTest.php @@ -88,7 +88,7 @@ public function testShouldReturnStorageData() ], 'goalTypeOptions' => $viewModel->getGoalTypeOptions(), 'goalProgressOptions' => $viewModel->getGoalProgressOptions(), - 'nameTitlePrefixes' => give_get_option('title_prefixes'), + 'nameTitlePrefixes' => give_get_option('title_prefixes', array_values(give_get_default_title_prefixes())), 'isExcerptEnabled' => give_is_setting_enabled(give_get_option('forms_excerpt')), 'intlTelInputSettings' => IntlTelInput::getSettings(), ], From 8dc5789789d70ed1b1a59114129d6a3e82a14889 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 16 Oct 2024 15:30:06 -0400 Subject: [PATCH 15/58] Feature: Add support for pausing/resuming subscriptions (#7551) Co-authored-by: Paulo Iankoski Co-authored-by: Jon Waldstein Co-authored-by: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> --- .../class-give-stripe-admin-settings.php | 22 +-- .../components/button/{index.js => index.tsx} | 22 ++- .../js/app/components/button/style.scss | 49 ++++- .../app/components/dashboard-content/index.js | 1 + .../dashboard-loading-spinner/index.tsx | 29 +++ .../dashboard-loading-spinner/style.scss | 38 ++++ .../js/app/components/error-message/index.tsx | 33 ++++ .../app/components/error-message/style.scss | 42 +++++ .../js/app/components/heading/style.scss | 2 +- .../js/app/components/logout-modal/index.js | 21 +-- .../js/app/components/logout-modal/style.scss | 84 ++++----- .../js/app/components/select-control/index.js | 4 + .../app/components/select-control/style.scss | 2 + .../subscription-cancel-modal/index.js | 57 ------ .../subscription-cancel-modal/index.tsx | 59 ++++++ .../subscription-cancel-modal/style.scss | 110 ++++++----- .../subscription-cancel-modal/utils/index.js | 19 +- .../amount-control/index.js | 4 +- .../amount-control/style.scss | 17 ++ .../hooks/pause-subscription.ts | 46 +++++ .../components/subscription-manager/index.js | 122 ------------- .../components/subscription-manager/index.tsx | 172 ++++++++++++++++++ .../pause-duration-dropdown/index.tsx | 59 ++++++ .../pause-duration-dropdown/style.scss | 83 +++++++++ .../subscription-manager/style.scss | 56 +++++- .../subscription-status/index.tsx | 16 ++ .../subscription-status/style.scss | 24 +++ .../subscription-manager/utils/index.js | 74 ++++++-- .../app/components/subscription-row/index.js | 79 -------- .../app/components/subscription-row/index.tsx | 81 +++++++++ .../components/subscription-row/style.scss | 5 + .../js/app/components/tab-menu/index.js | 19 +- .../app/tabs/recurring-donations/content.js | 10 +- .../Subscription/SubscriptionPausable.php | 32 ++++ .../Contracts/SubscriptionModuleInterface.php | 7 + .../PaymentGateways/PaymentGateway.php | 47 +++++ .../PaymentGateways/SubscriptionModule.php | 10 + .../includes/give-subscription.php | 31 +++- .../Repositories/SubscriptionRepository.php | 2 + .../ValueObjects/SubscriptionStatus.php | 6 + .../ListTable/InterweaveSSR/styles.scss | 3 +- 41 files changed, 1186 insertions(+), 413 deletions(-) rename src/DonorDashboards/resources/js/app/components/button/{index.js => index.tsx} (57%) create mode 100644 src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/style.scss create mode 100644 src/DonorDashboards/resources/js/app/components/error-message/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/error-message/style.scss delete mode 100644 src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.js create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/hooks/pause-subscription.ts delete mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/index.js create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/style.scss create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/style.scss delete mode 100644 src/DonorDashboards/resources/js/app/components/subscription-row/index.js create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-row/index.tsx create mode 100644 src/DonorDashboards/resources/js/app/components/subscription-row/style.scss create mode 100644 src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php diff --git a/includes/gateways/stripe/includes/admin/class-give-stripe-admin-settings.php b/includes/gateways/stripe/includes/admin/class-give-stripe-admin-settings.php index 039c3aab12..9a254833e8 100644 --- a/includes/gateways/stripe/includes/admin/class-give-stripe-admin-settings.php +++ b/includes/gateways/stripe/includes/admin/class-give-stripe-admin-settings.php @@ -200,6 +200,17 @@ public function register_settings( $settings ) { 'type' => 'checkbox', ]; + $settings['general'][] = [ + 'name' => esc_html__( 'Stripe Receipt Emails', 'give' ), + 'desc' => sprintf( + /* translators: 1. GiveWP Support URL */ + __( 'Check this option if you would like donors to receive receipt emails directly from Stripe. By default, donors will receive GiveWP generated receipt emails. Checking this option does not disable GiveWP emails.', 'give' ), + admin_url( '/edit.php?post_type=give_forms&page=give-settings&tab=emails' ) + ), + 'id' => 'stripe_receipt_emails', + 'type' => 'checkbox', + ]; + /** * This filter hook is used to add fields after Stripe General fields. * @@ -209,17 +220,6 @@ public function register_settings( $settings ) { */ $settings = apply_filters( 'give_stripe_add_after_general_fields', $settings ); - $settings['general'][] = [ - 'name' => esc_html__( 'Stripe Receipt Emails', 'give' ), - 'desc' => sprintf( - /* translators: 1. GiveWP Support URL */ - __( 'Check this option if you would like donors to receive receipt emails directly from Stripe. By default, donors will receive GiveWP generated receipt emails. Checking this option does not disable GiveWP emails.', 'give' ), - admin_url( '/edit.php?post_type=give_forms&page=give-settings&tab=emails' ) - ), - 'id' => 'stripe_receipt_emails', - 'type' => 'checkbox', - ]; - $settings['general'][] = [ 'name' => esc_html__( 'Stripe Gateway Documentation', 'give' ), 'id' => 'display_settings_general_docs_link', diff --git a/src/DonorDashboards/resources/js/app/components/button/index.js b/src/DonorDashboards/resources/js/app/components/button/index.tsx similarity index 57% rename from src/DonorDashboards/resources/js/app/components/button/index.js rename to src/DonorDashboards/resources/js/app/components/button/index.tsx index b37c0f662e..e74d7e198f 100644 --- a/src/DonorDashboards/resources/js/app/components/button/index.js +++ b/src/DonorDashboards/resources/js/app/components/button/index.tsx @@ -1,7 +1,20 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import cx from 'classnames'; + import './style.scss'; -const Button = ({icon, children, onClick, href, type, ...rest}) => { +type ButtonProps = { + classnames?: string; + icon?: any; + children: React.ReactNode; + onClick?: () => void; + href?: string; + type?: 'button' | 'submit' | 'reset'; + variant?: boolean; + disabled?: boolean; +}; + +const Button = ({icon, children, onClick, href, type, variant,classnames, ...rest}: ButtonProps) => { const handleHrefClick = (e) => { e.preventDefault(); window.parent.location = href; @@ -22,12 +35,15 @@ const Button = ({icon, children, onClick, href, type, ...rest}) => { } return ( ); diff --git a/src/DonorDashboards/resources/js/app/components/button/style.scss b/src/DonorDashboards/resources/js/app/components/button/style.scss index a4e3b35f2e..201cadf4cb 100644 --- a/src/DonorDashboards/resources/js/app/components/button/style.scss +++ b/src/DonorDashboards/resources/js/app/components/button/style.scss @@ -9,6 +9,7 @@ border: 1px solid var(--give-donor-dashboard-accent-color); border-radius: 3px; display: inline-flex; + width: fit-content; align-items: center; box-shadow: 0 0 0 0 #7ec980, 0 0 0 0 #4fa651; transition: box-shadow 0.1s ease, background-color ease-in 0.3s; @@ -29,6 +30,52 @@ &.give-donor-dashboard-button--primary { color: #fff !important; - background: var(--give-donor-dashboard-accent-color); + position: relative; + background: none; + box-shadow: none; + justify-content: center; + overflow: hidden; + + &:before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: var(--give-donor-dashboard-accent-color); + z-index: 0; + transition: filter 0.3s ease; + } + + &:hover:before { + filter: brightness(90%); // Darkens the background on hover + } + + &.disabled:before { + display: none; + } + + span { + position: relative; + z-index: 1; + } + } + + &.give-donor-dashboard-button--variant { + color: var(--give-donor-dashboard-accent-color) !important; + background: var(--givewp-shades-white); + box-shadow: none; + border: 1px solid var(--give-donor-dashboard-accent-color); + margin: 0; + justify-content: center; + + &:hover { + filter: brightness(90%); + } + + span { + color: inherit; + } } } diff --git a/src/DonorDashboards/resources/js/app/components/dashboard-content/index.js b/src/DonorDashboards/resources/js/app/components/dashboard-content/index.js index 0dec747695..d45e2dcc6c 100644 --- a/src/DonorDashboards/resources/js/app/components/dashboard-content/index.js +++ b/src/DonorDashboards/resources/js/app/components/dashboard-content/index.js @@ -2,6 +2,7 @@ import {useState, useEffect} from 'react'; import {useSelector} from 'react-redux'; import './style.scss'; +import '../subscription-manager/style.scss'; const DashboardContent = () => { const tabsSelector = useSelector((state) => state.tabs); diff --git a/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx b/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx new file mode 100644 index 0000000000..b0b80ffe44 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import "./style.scss"; + +/** + * @unreleased reference givewp/src/DonorDashboards/resources/views/donordashboardloader.php + */ +export default function DashboardLoadingSpinner() { + + + return ( +
+
+ + + + + +
+
+ ); +}; + diff --git a/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/style.scss b/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/style.scss new file mode 100644 index 0000000000..5ab2258266 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/style.scss @@ -0,0 +1,38 @@ +.givewp-donordashboard-loader { + width: 100%; + height: 100%; + min-height: 790px; + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + +.givewp-donordashboard-loader_wrapper { + width: calc(90% - 12px); + height: 100%; + max-width: 920px; + margin: 8px auto; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + + +.givewp-donordashboard-loader_spinner { + width: 90px; + height: 90px; + animation: spin 0.6s linear infinite; + .st0 { + fill: var(--give-donor-dashboard-accent-color); + } +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/DonorDashboards/resources/js/app/components/error-message/index.tsx b/src/DonorDashboards/resources/js/app/components/error-message/index.tsx new file mode 100644 index 0000000000..5a95c7c539 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/error-message/index.tsx @@ -0,0 +1,33 @@ +import {__} from '@wordpress/i18n'; +import ModalDialog from '@givewp/components/AdminUI/ModalDialog'; +import {store} from '../../tabs/recurring-donations/store'; +import {setError} from '../../tabs/recurring-donations/store/actions'; + +import './style.scss'; + +type ErrorMessageProps = { + error: string; +}; + +export default function ErrorMessage({error}: ErrorMessageProps) { + const {dispatch} = store; + + const toggleModal = () => { + dispatch(setError(null)); + }; + + return ( + +

{error}

+ +
+ ); +} diff --git a/src/DonorDashboards/resources/js/app/components/error-message/style.scss b/src/DonorDashboards/resources/js/app/components/error-message/style.scss new file mode 100644 index 0000000000..4f028ea988 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/error-message/style.scss @@ -0,0 +1,42 @@ +.givewp-modal-wrapper.give-donor-dashboard__error-modal { + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + background-color: rgba(0, 0, 0, 0.5) !important; + + .givewp-modal-dialog { + border-radius: 8px; + + .givewp-modal-header { + border-radius: 8px 8px 0 0; + background-color: #fafafa; + padding: 1rem 1.5rem; + } + + .givewp-modal-close { + right: 1rem; + } + + .givewp-modal-content { + padding: 1.5rem 1.5rem 2rem 1.5rem; + + .give-donor-dashboard__error-close { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + align-self: stretch; + width: 100%; + margin-top: 3rem; + padding: 1rem 2rem; + border-radius: 4px; + background: #0073aa; + color: #fff; + font-size: 1rem; + font-weight: 500; + border: none; + outline: none; + cursor: pointer; + } + } + } +} diff --git a/src/DonorDashboards/resources/js/app/components/heading/style.scss b/src/DonorDashboards/resources/js/app/components/heading/style.scss index c4a5127bc8..d55fda4aa4 100644 --- a/src/DonorDashboards/resources/js/app/components/heading/style.scss +++ b/src/DonorDashboards/resources/js/app/components/heading/style.scss @@ -5,7 +5,7 @@ font-size: 16px; margin: 20px 0 10px 0; display: flex; - align-items: center; + align-items: flex-end; svg { margin-right: 8px; diff --git a/src/DonorDashboards/resources/js/app/components/logout-modal/index.js b/src/DonorDashboards/resources/js/app/components/logout-modal/index.js index 66bc3e68f5..3e62937fb8 100644 --- a/src/DonorDashboards/resources/js/app/components/logout-modal/index.js +++ b/src/DonorDashboards/resources/js/app/components/logout-modal/index.js @@ -11,22 +11,15 @@ const LogoutModal = ({onRequestClose}) => { }; return ( -
-
-
- {__('Are you sure you want to logout?', 'give')} -
-
-
- - onRequestClose()}> - {__('Nevermind', 'give')} - -
-
+ <> +
+ + onRequestClose()}> + {__('Nevermind', 'give')} +
onRequestClose()} /> -
+ ); }; diff --git a/src/DonorDashboards/resources/js/app/components/logout-modal/style.scss b/src/DonorDashboards/resources/js/app/components/logout-modal/style.scss index 4928bcfa68..3488eab630 100644 --- a/src/DonorDashboards/resources/js/app/components/logout-modal/style.scss +++ b/src/DonorDashboards/resources/js/app/components/logout-modal/style.scss @@ -1,61 +1,49 @@ /* stylelint-disable selector-class-pattern */ +.givewp-modal-wrapper.give-donor-dashboard-logout-modal { + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + background-color: rgba(0, 0, 0, 0.5) !important; -.give-donor-dashboard-logout-modal { - display: flex; - align-items: center; - justify-content: center; - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: 99; - - .give-donor-dashboard-logout-modal__frame { - background: #fff; - z-index: 2; - box-shadow: 0 2px 5px rgb(0 0 0 / 30%); + .givewp-modal-dialog { border-radius: 8px; - overflow: hidden; - width: 90%; - max-width: 480px; - .give-donor-dashboard-logout-modal__header { - background: var(--give-donor-dashboard-accent-color); - font-size: 21px; - font-weight: 500; - padding: 26px 36px; - line-height: 26px; - color: #fff; + .givewp-modal-header { + border-radius: 8px 8px 0 0; + background-color: #fafafa; + padding: 1rem 1.5rem; } - .give-donor-dashboard-logout-modal__body { - padding: 30px 36px 20px 36px; - font-size: 15px; - font-weight: 500; - line-height: 24px; - color: #555; + .givewp-modal-close { + right: 1rem; } - .give-donor-dashboard-logout-modal__buttons { - display: flex; - align-items: center; - justify-content: space-between; + .givewp-modal-content { + padding: 1.5rem 1.5rem 2rem 1.5rem; + + .give-donor-dashboard-logout-modal__buttons { + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; - a.give-donor-dashboard-logout-modal__cancel { - cursor: pointer; + .give-donor-dashboard-logout-modal__cancel { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + max-width: 240px; + padding: 12px 20px; + font-size: 16px; + font-weight: 600; + border-radius: 3px; + color: var(--give-donor-dashboard-accent-color) !important; + background: var(--givewp-shades-white); + box-shadow: none; + border: 1px solid var(--give-donor-dashboard-accent-color); + margin: 0; + cursor: pointer; + } } } } - - .give-donor-dashboard-logout-modal__bg { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: 1; - background: rgba(255, 255, 255, 0.6); - border-radius: 8px; - } } diff --git a/src/DonorDashboards/resources/js/app/components/select-control/index.js b/src/DonorDashboards/resources/js/app/components/select-control/index.js index f641589889..778fc0cb86 100644 --- a/src/DonorDashboards/resources/js/app/components/select-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/select-control/index.js @@ -22,6 +22,10 @@ const SelectControl = ({value, options, isLoading, label = null, onChange = null const selectedOptionValue = options !== null ? options.filter((option) => option.value === value) : null; const selectStyles = { + menu: (provided) => ({ + ...provided, + zIndex: '9999', + }), control: (provided) => ({ ...provided, fontSize: '14px', diff --git a/src/DonorDashboards/resources/js/app/components/select-control/style.scss b/src/DonorDashboards/resources/js/app/components/select-control/style.scss index fd32a75f92..042ea3cdc1 100644 --- a/src/DonorDashboards/resources/js/app/components/select-control/style.scss +++ b/src/DonorDashboards/resources/js/app/components/select-control/style.scss @@ -1,5 +1,7 @@ /* stylelint-disable function-url-quotes, selector-class-pattern */ .give-donor-dashboard-select-control { + display: flex; + flex-direction: column; margin-top: 10px; .give-donor-dashboard-select-control__label { diff --git a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.js b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.js deleted file mode 100644 index f829a5addd..0000000000 --- a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.js +++ /dev/null @@ -1,57 +0,0 @@ -import Button from '../button'; -import {cancelSubscriptionWithAPI} from './utils'; - -import {__} from '@wordpress/i18n'; -import './style.scss'; -import {useState} from 'react'; - -const responseIsError = (response) => { - return response?.data?.code?.includes('error'); -}; - -const getErrorMessageFromResponse = (response) => { - if (response?.data?.code === 'internal_server_error' || !response?.data?.message) { - return __('An error occurred while cancelling your subscription.', 'give'); - } - - return response?.data?.message; -}; - -const SubscriptionCancelModal = ({id, onRequestClose}) => { - const [cancelling, setCancelling] = useState(false); - const handleCancel = async () => { - setCancelling(true); - const response = await cancelSubscriptionWithAPI(id); - - if (responseIsError(response)) { - const errorMessage = getErrorMessageFromResponse(response); - - window.alert(errorMessage); - } - - setCancelling(false); - - onRequestClose(); - }; - - return ( -
-
-
{__('Cancel Subscription?', 'give')}
-
-
- - onRequestClose()}> - {__('Nevermind', 'give')} - -
-
-
-
onRequestClose()} /> -
- ); -}; - -export default SubscriptionCancelModal; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.tsx new file mode 100644 index 0000000000..b0845929e6 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/index.tsx @@ -0,0 +1,59 @@ +import {useState} from 'react'; +import {__} from '@wordpress/i18n'; +import Button from '../button'; +import {cancelSubscriptionWithAPI} from './utils'; + +import ModalDialog from '@givewp/components/AdminUI/ModalDialog'; +import DashboardLoadingSpinner from '../dashboard-loading-spinner'; +import './style.scss'; + +type SubscriptionCancelProps = { + isOpen: boolean; + toggleModal: () => void; + id: number; +}; + +const SubscriptionCancelModal = ({isOpen, toggleModal, id}: SubscriptionCancelProps) => { + const [loading, setLoading] = useState(false); + + const handleCancel = async () => { + setLoading(true); + await cancelSubscriptionWithAPI(id); + setLoading(false); + toggleModal(); + }; + + return ( + + + + } + title={__('Cancel Subscription', 'give')} + showHeader={true} + isOpen={isOpen} + handleClose={toggleModal} + > +

+ {__('Are you sure you want to cancel your subscription?', 'give')} +

+
+ + +
+ + {loading && } +
+ ); +}; + +export default SubscriptionCancelModal; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/style.scss index 2897270987..36a637dcfd 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/style.scss +++ b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/style.scss @@ -1,61 +1,73 @@ /* stylelint-disable selector-class-pattern */ +.givewp-modal-wrapper.give-donor-dashboard-cancel-modal { + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + background-color: rgba(0, 0, 0, 0.5) !important; -.give-donor-dashboard-cancel-modal { - display: flex; - align-items: center; - justify-content: center; - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: 99; - - .give-donor-dashboard-cancel-modal__frame { - background: #fff; - z-index: 2; - box-shadow: 0 2px 5px rgb(0 0 0 / 30%); + .givewp-modal-dialog { border-radius: 8px; - overflow: hidden; - width: 90%; - max-width: 480px; - - .give-donor-dashboard-cancel-modal__header { - background: var(--give-donor-dashboard-accent-color); - font-size: 21px; - font-weight: 500; - padding: 26px 36px; - line-height: 26px; - color: #fff; + + .givewp-modal-header { + border-radius: 8px 8px 0 0; + background-color: #fafafa; + padding: 1rem 1.5rem; } - .give-donor-dashboard-cancel-modal__body { - padding: 30px 36px 20px 36px; - font-size: 15px; - font-weight: 500; - line-height: 24px; - color: #555; + .givewp-modal-close { + right: 1rem; } - .give-donor-dashboard-cancel-modal__buttons { - display: flex; - align-items: center; - justify-content: space-between; + .givewp-modal-content { + padding: 1.5rem; - a.give-donor-dashboard-cancel-modal__cancel { - cursor: pointer; + .give-donor-dashboard-cancel-modal__description { + margin: 0 0 1.5rem 0; + font-size: 1rem; + font-weight: 500; + color: #1F2937; } - } - } - .give-donor-dashboard-cancel-modal__bg { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: 1; - background: rgba(255, 255, 255, 0.6); - border-radius: 8px; + .give-donor-dashboard-cancel-modal__buttons { + display: flex; + align-items: center; + gap: 2rem; + width: auto; + margin: 0; + + &__button.give-donor-dashboard-button.give-donor-dashboard-button--primary { + flex: 1; + margin: 0; + background-color: #d92d0b; + border-color: inherit; + color: #fff; + + &:before { + display: none; + } + + &:hover { + background-color: #F2320C; + } + } + + &__button.give-donor-dashboard-button.give-donor-dashboard-button--variant { + flex: 1; + margin: 0; + border-color: #9CA0AF; + color: #000 !important; + filter: none; + + &:before { + display: none; + } + + &:hover { + background-color: #F9FAFB; + border-color: #9CA0AF; + color: #000 !important; + } + } + } + } } } diff --git a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/utils/index.js b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/utils/index.js index 9836942f30..45a99bff0c 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/utils/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-cancel-modal/utils/index.js @@ -1,7 +1,12 @@ import {donorDashboardApi} from '../../../utils'; import {fetchSubscriptionsDataFromAPI} from '../../../tabs/recurring-donations/utils'; +import {__} from '@wordpress/i18n'; +import {store} from '../../../tabs/recurring-donations/store'; +import {setError} from '../../../tabs/recurring-donations/store/actions'; export const cancelSubscriptionWithAPI = async (id) => { + const {dispatch} = store; + try { const response = await donorDashboardApi.post( 'recurring-donations/subscription/cancel', @@ -13,8 +18,20 @@ export const cancelSubscriptionWithAPI = async (id) => { await fetchSubscriptionsDataFromAPI(); - return await response; + return response; } catch (error) { + if (error.response.status === 500) { + dispatch( + setError( + __( + 'An error occurred while processing your request. Please try again later, or contact support if the issue persists.', + 'give' + ) + ) + ); + } else { + dispatch(setError(error.response.data.message)); + } return error.response; } }; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js index 084749b3da..9bd9b56346 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js @@ -99,14 +99,12 @@ const AmountControl = ({currency, onChange, value, options, min, max}) => { return (
-
- -
{selectValue === CUSTOM_AMOUNT && (
diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss index cf0f2ec23f..0c8a143597 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss @@ -9,6 +9,23 @@ $errorColor: #c91f1f; color: $errorColor; } +.give-donor-dashboard-amount-inputs { + display: flex; + flex-direction: column; + + + .give-donor-dashboard-field-row { + display: flex; + padding: 0; + + .give-donor-dashboard-select-control { + display: flex; + min-width: 100%; + margin: 0; + } + } +} + .give-donor-dashboard-currency-control { margin-top: 10px; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/hooks/pause-subscription.ts b/src/DonorDashboards/resources/js/app/components/subscription-manager/hooks/pause-subscription.ts new file mode 100644 index 0000000000..c3f41b3f3c --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/hooks/pause-subscription.ts @@ -0,0 +1,46 @@ +import {useState} from 'react'; +import {managePausingSubscriptionWithAPI} from '../utils'; + +export type pauseDuration = number; +type id = string; + +const usePauseSubscription = (id: id) => { + const [loading, setLoading] = useState(false); + + const handlePause = async (pauseDuration: pauseDuration) => { + setLoading(true); + try { + await managePausingSubscriptionWithAPI({ + id, + intervalInMonths: pauseDuration, + }); + } catch (error) { + console.error('Error pausing subscription:', error); + } finally { + setLoading(false); + } + }; + + const handleResume = async () => { + setLoading(true); + try { + await managePausingSubscriptionWithAPI({ + id, + action: 'resume', + }); + } catch (error) { + console.error('Error resuming subscription:', error); + } finally { + setLoading(false); + } + }; + + return { + loading, + setLoading, + handlePause, + handleResume, + }; +}; + +export default usePauseSubscription; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.js deleted file mode 100644 index 6456d98a96..0000000000 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.js +++ /dev/null @@ -1,122 +0,0 @@ -import {Fragment, useMemo, useRef, useState} from 'react'; -import FieldRow from '../field-row'; -import Button from '../button'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; - -import {__} from '@wordpress/i18n'; - -import AmountControl from './amount-control'; -import PaymentMethodControl from './payment-method-control'; - -import {updateSubscriptionWithAPI} from './utils'; - -import './style.scss'; - -/** - * Normalize an amount - * - * @param {string} float - * @param {number} decimals - * @return {string|NaN} - */ -const normalizeAmount = (float, decimals) => Number.parseFloat(float).toFixed(decimals); - -// There is no error handling whatsoever, that will be necessary. -const SubscriptionManager = ({id, subscription}) => { - const gatewayRef = useRef(); - - const [amount, setAmount] = useState(() => - normalizeAmount(subscription.payment.amount.raw, subscription.payment.currency.numberDecimals) - ); - const [isUpdating, setIsUpdating] = useState(false); - const [updated, setUpdated] = useState(false); - - // Prepare data for amount control - const {max, min, options} = useMemo(() => { - const {numberDecimals} = subscription.payment.currency; - const {custom_amount} = subscription.form; - - const options = subscription.form.amounts.map((amount) => ({ - value: normalizeAmount(amount.raw, numberDecimals), - label: amount.formatted, - })); - - if (custom_amount) { - options.push({ - value: 'custom_amount', - label: __('Custom Amount', 'give'), - }); - } - - return { - max: normalizeAmount(custom_amount?.maximum, numberDecimals), - min: normalizeAmount(custom_amount?.minimum, numberDecimals), - options, - }; - }, [subscription]); - - const handleUpdate = async () => { - if (isUpdating) { - return; - } - - setIsUpdating(true); - - const paymentMethod = gatewayRef.current ? - await gatewayRef.current.getPaymentMethod() : - {}; - - if ('error' in paymentMethod) { - setIsUpdating(false); - return; - } - - await updateSubscriptionWithAPI({ - id, - amount, - paymentMethod, - }); - - setUpdated(true); - setIsUpdating(false); - }; - - return ( - - - - -
- -
-
-
- ); -}; -export default SubscriptionManager; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx new file mode 100644 index 0000000000..f7d8793ad0 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx @@ -0,0 +1,172 @@ +import {Fragment, useMemo, useRef, useState} from 'react'; +import FieldRow from '../field-row'; +import Button from '../button'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {__} from '@wordpress/i18n'; +import AmountControl from './amount-control'; +import PaymentMethodControl from './payment-method-control'; +import ModalDialog from '@givewp/components/AdminUI/ModalDialog'; +import {updateSubscriptionWithAPI} from './utils'; +import PauseDurationDropdown from './pause-duration-dropdown'; +import DashboardLoadingSpinner from '../dashboard-loading-spinner'; +import usePauseSubscription from './hooks/pause-subscription'; +import {cancelSubscriptionWithAPI} from '../subscription-cancel-modal/utils'; + +import './style.scss'; +import SubscriptionCancelModal from '../subscription-cancel-modal'; + +/** + * Normalize an amount + * + * @param {string} float + * @param {number} decimals + * @return {string|NaN} + */ +const normalizeAmount = (float, decimals) => Number.parseFloat(float).toFixed(decimals); + +const SubscriptionManager = ({id, subscription}) => { + const gatewayRef = useRef(); + const [isPauseModalOpen, setIsPauseModalOpen] = useState(false); + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); + + const [amount, setAmount] = useState(() => + normalizeAmount(subscription.payment.amount.raw, subscription.payment.currency.numberDecimals) + ); + const [isUpdating, setIsUpdating] = useState(false); + const [updated, setUpdated] = useState(false); + const {handlePause, handleResume, loading} = usePauseSubscription(id); + + const subscriptionStatus = subscription.payment.status.id; + + const showPausingControls = + subscription.gateway.can_pause && !['Quarterly', 'Yearly'].includes(subscription.payment.frequency); + + // Prepare data for amount control + const {max, min, options} = useMemo(() => { + const {numberDecimals} = subscription.payment.currency; + const {custom_amount} = subscription.form; + + const options = subscription.form.amounts.map((amount) => ({ + value: normalizeAmount(amount.raw, numberDecimals), + label: amount.formatted, + })); + + if (custom_amount) { + options.push({ + value: 'custom_amount', + label: __('Custom Amount', 'give'), + }); + } + + return { + max: normalizeAmount(custom_amount?.maximum, numberDecimals), + min: normalizeAmount(custom_amount?.minimum, numberDecimals), + options, + }; + }, [subscription]); + + const handleUpdate = async () => { + if (isUpdating) { + return; + } + + setIsUpdating(true); + + // @ts-ignore + const paymentMethod = gatewayRef.current ? await gatewayRef.current.getPaymentMethod() : {}; + + if ('error' in paymentMethod) { + setIsUpdating(false); + return; + } + + await updateSubscriptionWithAPI({ + id, + amount, + paymentMethod, + }); + + setUpdated(true); + setIsUpdating(false); + }; + + const toggleModal = () => { + setIsPauseModalOpen(!isPauseModalOpen); + }; + + return ( +
+ + + + {loading && } + + + {showPausingControls && ( + <> + + + + {subscriptionStatus === 'active' ? ( + + ) : ( + + )} + + )} + + + + {isCancelModalOpen && ( + setIsCancelModalOpen(!isCancelModalOpen)} + id={id} + /> + )} + +
+ ); +}; +export default SubscriptionManager; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/index.tsx new file mode 100644 index 0000000000..db48f4fdb3 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/index.tsx @@ -0,0 +1,59 @@ +import { __ } from '@wordpress/i18n'; +import { useState } from 'react'; + +import './style.scss'; + +type PauseDurationDropDownProps = { + handlePause: (pauseDuration: number) => void; + closeModal: () => void; +}; + +type durationOptions = { value: string; label: string }[]; + +const durationOptions: durationOptions = [ + { value: '1', label: __('1 month', 'give') }, + { value: '2', label: __('2 months', 'give') }, + { value: '3', label: __('3 months', 'give') }, +]; + +export default function PauseDurationDropdown({handlePause, closeModal}: PauseDurationDropDownProps) { + const [pauseDuration, setPauseDuration] = useState(1); + + const updateSubscription = () => { + closeModal(); + handlePause(pauseDuration); + }; + + return ( + + ); +} diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/style.scss new file mode 100644 index 0000000000..d53f971ce5 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/pause-duration-dropdown/style.scss @@ -0,0 +1,83 @@ +.givewp-modal-wrapper.give-donor-dashboard__subscription-manager-modal, .givewp-modal-wrapper.give-donor-dashboard-cancel-modal { + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + background-color: rgba(0, 0, 0, 0.5) !important; + + .givewp-modal-dialog { + border-radius: 8px; + + .givewp-modal-header { + border-radius: 8px 8px 0 0; + background-color: #fafafa; + padding: 1rem 1.5rem; + } + + .givewp-modal-close { + right: 1rem; + } + + .givewp-modal-content { + padding: 1.5rem 1.5rem 2rem 1.5rem; + + .give-donor-dashboard__subscription-manager-pause-label { + color: #888; + line-height: 2.5; + margin-bottom: .25rem; + + .give-donor-dashboard__subscription-manager-pause-container { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid #666; + border-radius: 4px; + } + + svg { + position: absolute; + right: 1rem; + pointer-events: none; + } + + .give-donor-dashboard__subscription-manager-pause-select { + display: block; + width: 100%; + appearance: none; /* for modern browsers */ + -webkit-appearance: none; /* for Safari/Chrome */ + -moz-appearance: none; /* for Firefox */ + padding: 0.75rem 1rem; + background: none; + font-size: 1rem; + font-weight: 500; + border-radius: 4px; + border: none; + outline: none; + + } + + .give-donor-dashboard__subscription-manager-pause-update { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + align-self: stretch; + width: 100%; + margin-top: 3rem; + padding: 1rem 2rem; + border-radius: 4px; + background: #2271B1; + color: #fff; + font-size: 1rem; + font-weight: 500; + border: none; + outline: none; + cursor: pointer; + + &:hover { + background-color: #135E96; + } + } + } + } + } +} diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-manager/style.scss index 3cbd5dfdaf..9cfef69f19 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/style.scss +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/style.scss @@ -1,7 +1,59 @@ /* stylelint-disable selector-class-pattern */ -.give-donor-dashboard__subscription-manager-spinner { - animation: spin infinite 1s linear; +.give-donor-dashboard__subscription-manager { + display: flex; + flex-direction: column; + + &-spinner { + animation: spin infinite 1s linear; + } + + .give-donor-dashboard-button--primary, .give-donor-dashboard-button--variant { + max-width: fit-content; + } + + .give-donor-dashboard-button.give-donor-dashboard-button--variant{ + position: relative; + + &:hover:before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: var(--give-donor-dashboard-accent-color); + z-index: 0; + transition: filter 0.3s ease; + filter: brightness(125%); + opacity: .15; + } + + span { + position: relative; + z-index: 1; + color: var(--give-donor-dashboard-accent-color); + } + } + + &__cancel { + color: #d92d0b; + padding: 0; + margin: 2rem 0 1.75rem 0; + background: none; + font-size: .873rem; + font-weight: 600; + text-align: right; + border: none; + outline: none; + cursor: pointer; + } + + .give-donor-dashboard-field-row { + display: flex; + justify-content: flex-end; + margin: 10px 0; + } } @keyframes spin { diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/index.tsx new file mode 100644 index 0000000000..1bfbc3e892 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import cx from 'classnames'; +import {__} from '@wordpress/i18n'; + +import './style.scss'; + +export default function SubscriptionStatus({subscription}) { + const status = subscription.payment.status.id; + const label = subscription.payment.status.label; + + return ( +
+ {label} +
+ ); +} diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/style.scss new file mode 100644 index 0000000000..b5d73afef8 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/subscription-status/style.scss @@ -0,0 +1,24 @@ +.givewp-dashboard-subscription-status { + position: absolute; + right: 30px; + color: #000; + padding: .25rem .75rem; + width: fit-content; + border-radius: 50px; + font-size: 12px; + font-weight: normal; + text-align: center; + + &--paused { + background-color: #e6e6e6; + } + + &--active { + background-color: #cef2cf; + } + + &--cancelled { + background-color: #FFB5A6; + + } +} diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/utils/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/utils/index.js index 1d16778c0f..6af79d8246 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/utils/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/utils/index.js @@ -1,26 +1,70 @@ +import {__} from '@wordpress/i18n'; import {store} from '../../../tabs/recurring-donations/store'; import {donorDashboardApi} from '../../../utils'; import {fetchSubscriptionsDataFromAPI} from '../../../tabs/recurring-donations/utils'; import {setError} from '../../../tabs/recurring-donations/store/actions'; -export const updateSubscriptionWithAPI = ({id, amount, paymentMethod}) => { +export const updateSubscriptionWithAPI = async ({id, amount, paymentMethod}) => { const {dispatch} = store; - return donorDashboardApi - .post( + + try { + const response = await donorDashboardApi.post( 'recurring-donations/subscription/update', { - id: id, - amount: amount, + id, + amount, payment_method: paymentMethod, }, - {}, - ) - .then(async (response) => { - if (response.data.status === 400) { - dispatch(setError(response.data.body_response.message)); - return; - } - await fetchSubscriptionsDataFromAPI(); - return response; - }); + {} + ); + + await fetchSubscriptionsDataFromAPI(); + + return response; + } catch (error) { + if (error.response.status === 500) { + dispatch( + setError( + __( + 'An error occurred while processing your request. Please try again later, or contact support if the issue persists.', + 'give' + ) + ) + ); + } else { + dispatch(setError(error.response.data.message)); + } + } +}; + +export const managePausingSubscriptionWithAPI = async ({id, action = 'pause', intervalInMonths = null}) => { + const {dispatch} = store; + try { + const response = await donorDashboardApi.post( + 'recurring-donations/subscription/manage-pausing', + { + id, + action, + interval_in_months: intervalInMonths, + }, + {} + ); + + await fetchSubscriptionsDataFromAPI(); + + return response; + } catch (error) { + if (error.response.status === 500) { + dispatch( + setError( + __( + 'An error occurred while processing your request. Please try again later, or contact support if the issue persists.', + 'give' + ) + ) + ); + } else { + dispatch(setError(error.response.data.message)); + } + } }; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-row/index.js b/src/DonorDashboards/resources/js/app/components/subscription-row/index.js deleted file mode 100644 index d9e1dedf37..0000000000 --- a/src/DonorDashboards/resources/js/app/components/subscription-row/index.js +++ /dev/null @@ -1,79 +0,0 @@ -import {Link} from 'react-router-dom'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {__} from '@wordpress/i18n'; - -import {useState, Fragment} from 'react'; -import SubscriptionCancelModal from '../subscription-cancel-modal'; -import {useWindowSize} from '../../hooks'; - -const SubscriptionRow = ({subscription}) => { - const {id, payment, form, gateway} = subscription; - const {width} = useWindowSize(); - - const [cancelModalOpen, setCancelModalOpen] = useState(false); - - return ( - - {cancelModalOpen && ( - setCancelModalOpen(false)} /> - )} -
-
- {width < 920 && ( -
{__('Amount', 'give')}
- )} -
- {payment.amount.formatted} / {payment.frequency} -
- {form.title} -
-
- {width < 920 && ( -
{__('Status', 'give')}
- )} -
-
-
{payment.status.label}
-
-
-
- {width < 920 && ( -
{__('Next Renewal', 'give')}
- )} - {payment.renewalDate} -
-
- {width < 920 && ( -
{__('Progress', 'give')}
- )} - {payment.progress} -
-
-
ID: {payment.serialCode}
-
- - {__('View Subscription', 'give')} - -
- {gateway.can_update && ( -
- - {__('Manage Subscription', 'give')} - -
- )} - {gateway.can_cancel && ( - - )} -
-
- - ); -}; - -export default SubscriptionRow; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-row/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-row/index.tsx new file mode 100644 index 0000000000..ade63a9017 --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-row/index.tsx @@ -0,0 +1,81 @@ +import {useState} from 'react'; +import {Link} from 'react-router-dom'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {__} from '@wordpress/i18n'; + +import {useWindowSize} from '../../hooks'; +import SubscriptionCancelModal from '../subscription-cancel-modal'; + +import "./style.scss"; + +const SubscriptionRow = ({subscription}) => { + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); + + const {width} = useWindowSize(); + const {id, payment, form, gateway} = subscription; + + return ( +
+
+ {width < 920 &&
{__('Amount', 'give')}
} +
+ {payment.amount.formatted} / {payment.frequency} +
+ {form.title} +
+
+ {width < 920 &&
{__('Status', 'give')}
} +
+
+
{payment.status.label}
+
+
+
+ {width < 920 && ( +
{__('Next Renewal', 'give')}
+ )} + {payment.renewalDate} +
+
+ {width < 920 && ( +
{__('Progress', 'give')}
+ )} + {payment.progress} +
+
+
ID: {payment.serialCode}
+
+ + {__('View Subscription', 'give')} + +
+ {gateway.can_update && ( +
+ + {__('Manage Subscription', 'give')} + +
+ )} + {gateway.can_cancel && !gateway.can_update && ( + <> + {isCancelModalOpen && ( + setIsCancelModalOpen(!isCancelModalOpen)} + /> + )} + + + )} +
+
+ ); +}; + +export default SubscriptionRow; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-row/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-row/style.scss new file mode 100644 index 0000000000..256364540c --- /dev/null +++ b/src/DonorDashboards/resources/js/app/components/subscription-row/style.scss @@ -0,0 +1,5 @@ +#give-donor-dashboard { + .give-donor-dashboard-table__donation-receipt__cancel { + color: #d92d0b; + } +} diff --git a/src/DonorDashboards/resources/js/app/components/tab-menu/index.js b/src/DonorDashboards/resources/js/app/components/tab-menu/index.js index efb993d61f..967e95721a 100644 --- a/src/DonorDashboards/resources/js/app/components/tab-menu/index.js +++ b/src/DonorDashboards/resources/js/app/components/tab-menu/index.js @@ -7,6 +7,7 @@ import {__} from '@wordpress/i18n'; // Internal dependencies import TabLink from '../tab-link'; import LogoutModal from '../logout-modal'; +import ModalDialog from '@givewp/components/AdminUI/ModalDialog'; import './style.scss'; @@ -18,13 +19,27 @@ const TabMenu = () => { return ; }); + const toggleModal = () => { + setLogoutModalOpen(!logoutModalOpen); + }; + return ( - {logoutModalOpen && setLogoutModalOpen(false)} />} + {logoutModalOpen && ( + + + + )}
{tabLinks}
-
setLogoutModalOpen(true)}> +
{__('Logout', 'give')}
diff --git a/src/DonorDashboards/resources/js/app/tabs/recurring-donations/content.js b/src/DonorDashboards/resources/js/app/tabs/recurring-donations/content.js index be7123c2be..7a81d97cac 100644 --- a/src/DonorDashboards/resources/js/app/tabs/recurring-donations/content.js +++ b/src/DonorDashboards/resources/js/app/tabs/recurring-donations/content.js @@ -8,12 +8,14 @@ import Divider from '../../components/divider'; import SubscriptionReceipt from '../../components/subscription-receipt'; import SubscriptionManager from '../../components/subscription-manager'; import SubscriptionTable from '../../components/subscription-table'; +import SubscriptionStatus from '../../components/subscription-manager/subscription-status'; import {useSelector} from './hooks'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import './style.scss'; +import ErrorMessage from '../../components/error-message'; const Content = () => { const subscriptions = useSelector((state) => state.subscriptions); @@ -37,8 +39,7 @@ const Content = () => { if (error) { return ( - {__('Error', 'give')} -

{error}

+
); } @@ -81,7 +82,10 @@ const Content = () => { ) : ( - {__('Manage Subscription', 'give')} + + {__('Manage Subscription', 'give')} + + diff --git a/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php b/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php new file mode 100644 index 0000000000..2e3ee129dd --- /dev/null +++ b/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php @@ -0,0 +1,32 @@ +subscriptionModule->cancelSubscription($subscription); } + /** + * @inheritDoc + * + * @unreleased + */ + public function pauseSubscription(Subscription $subscription, array $data = []): void + { + if ($this->subscriptionModule instanceof SubscriptionPausable) { + $this->subscriptionModule->pauseSubscription($subscription, $data); + + return; + } + + throw new Exception('Gateway does not support pausing the subscription.'); + } + + /** + * @inheritDoc + * + * @unreleased + */ + public function resumeSubscription(Subscription $subscription): void + { + if ($this->subscriptionModule instanceof SubscriptionPausable) { + $this->subscriptionModule->resumeSubscription($subscription); + + return; + } + + throw new Exception('Gateway does not support resuming the subscription.'); + } + + /** + * @inheritDoc + * + * @unreleased + */ + public function canPauseSubscription(): bool + { + if ($this->subscriptionModule instanceof SubscriptionPausable) { + return $this->subscriptionModule->canPauseSubscription(); + } + + return false; + } + /** * @since 2.21.2 * @inheritDoc diff --git a/src/Framework/PaymentGateways/SubscriptionModule.php b/src/Framework/PaymentGateways/SubscriptionModule.php index 558eb77192..7e17dc2b04 100644 --- a/src/Framework/PaymentGateways/SubscriptionModule.php +++ b/src/Framework/PaymentGateways/SubscriptionModule.php @@ -4,7 +4,9 @@ use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionAmountEditable; use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionDashboardLinkable; +use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionPausable; use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionPaymentMethodEditable; +use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionResumable; use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionTransactionsSynchronizable; use Give\Framework\PaymentGateways\Contracts\SubscriptionModuleInterface; use Give\Framework\PaymentGateways\Traits\HasRouteMethods; @@ -31,6 +33,14 @@ public function setGateway(PaymentGateway $gateway) $this->gateway = $gateway; } + /** + * @unreleased + */ + public function canPauseSubscription(): bool + { + return $this instanceof SubscriptionPausable; + } + /** * @inheritDoc */ diff --git a/src/LegacySubscriptions/includes/give-subscription.php b/src/LegacySubscriptions/includes/give-subscription.php index 88eb6cbda3..f53c8c7f65 100644 --- a/src/LegacySubscriptions/includes/give-subscription.php +++ b/src/LegacySubscriptions/includes/give-subscription.php @@ -790,6 +790,19 @@ public function can_cancel() { return apply_filters( 'give_subscription_can_cancel', false, $this ); } + /** + * Can Pause. + * + * This method is filtered by payment gateways in order to return true on subscriptions + * that can be paused with a profile ID through the merchant processor. + * + * @return mixed + */ + public function can_pause() + { + return apply_filters('give_subscription_can_pause', false, $this); + } + /** * Can Sync. * @@ -906,6 +919,22 @@ public function is_complete() { } + /** + * Is Paused. + * + * @return bool $ret Whether the subscription is paused or not. + */ + public function is_paused() + { + $ret = false; + + if ('paused' === $this->status) { + $ret = true; + } + + return apply_filters('give_subscription_is_paused', $ret, $this->id, $this); + } + /** * Is Expired. @@ -998,7 +1027,7 @@ public function get_renewal_date( $localized = true ) { $frequency = ! empty( $this->frequency ) ? intval( $this->frequency ) : 1; // If renewal date is already in the future it's set so return it. - if ( $expires > current_time( 'timestamp' ) && $this->is_active() ) { + if ($expires > current_time('timestamp') && ($this->is_active() || $this->is_paused())) { return $localized ? date_i18n( give_date_format(), strtotime( $this->expiration ) ) : date( 'Y-m-d H:i:s', strtotime( $this->expiration ) ); diff --git a/src/Subscriptions/Repositories/SubscriptionRepository.php b/src/Subscriptions/Repositories/SubscriptionRepository.php index e83df8f344..d5f18a8b62 100644 --- a/src/Subscriptions/Repositories/SubscriptionRepository.php +++ b/src/Subscriptions/Repositories/SubscriptionRepository.php @@ -186,6 +186,7 @@ public function insert(Subscription $subscription) } /** + * @unreleased add expiration column to update * @since 2.24.0 add payment_mode column to update * @since 2.21.0 replace actions with givewp_subscription_updating and givewp_subscription_updated * @since 2.19.6 @@ -209,6 +210,7 @@ public function update(Subscription $subscription) DB::table('give_subscriptions') ->where('id', $subscription->id) ->update([ + 'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt), 'status' => $subscription->status->getValue(), 'profile_id' => $subscription->gatewaySubscriptionId, 'customer_id' => $subscription->donorId, diff --git a/src/Subscriptions/ValueObjects/SubscriptionStatus.php b/src/Subscriptions/ValueObjects/SubscriptionStatus.php index b71dab27da..9f72b21cd6 100644 --- a/src/Subscriptions/ValueObjects/SubscriptionStatus.php +++ b/src/Subscriptions/ValueObjects/SubscriptionStatus.php @@ -5,6 +5,7 @@ use Give\Framework\Support\ValueObjects\Enum; /** + * @unreleased Added a new "paused" status * @since 2.19.6 * * @method static SubscriptionStatus PENDING() @@ -16,6 +17,7 @@ * @method static SubscriptionStatus FAILING() * @method static SubscriptionStatus CANCELLED() * @method static SubscriptionStatus SUSPENDED() + * @method static SubscriptionStatus PAUSED() * @method bool isPending() * @method bool isActive() * @method bool isExpired() @@ -25,6 +27,7 @@ * @method bool isFailing() * @method bool isCancelled() * @method bool isSuspended() + * @method bool isPaused() */ class SubscriptionStatus extends Enum { const PENDING = 'pending'; @@ -36,8 +39,10 @@ class SubscriptionStatus extends Enum { const CANCELLED = 'cancelled'; const ABANDONED = 'abandoned'; const SUSPENDED = 'suspended'; + const PAUSED = 'paused'; /** + * @unreleased Added a new "paused" status * @since 2.24.0 * * @return array @@ -54,6 +59,7 @@ public static function labels(): array self::CANCELLED => __( 'Cancelled', 'give' ), self::ABANDONED => __( 'Abandoned', 'give' ), self::SUSPENDED => __( 'Suspended', 'give' ), + self::PAUSED => __('Paused', 'give'), ]; } diff --git a/src/Views/Components/ListTable/InterweaveSSR/styles.scss b/src/Views/Components/ListTable/InterweaveSSR/styles.scss index f590e79056..b99f8b63b7 100644 --- a/src/Views/Components/ListTable/InterweaveSSR/styles.scss +++ b/src/Views/Components/ListTable/InterweaveSSR/styles.scss @@ -40,7 +40,8 @@ &--pending, &--processing, - &--upgraded { + &--upgraded, + &--paused { background: #0878b0; } From b5d57066d09ec19342f983da00cbafc5daea9b81 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 16 Oct 2024 15:34:31 -0400 Subject: [PATCH 16/58] chore: merge develop, update since tags and readme --- give.php | 2 +- readme.txt | 4 +++- .../Actions/ConvertDonationFormBlocksToFieldsApi.php | 2 +- .../js/app/components/dashboard-loading-spinner/index.tsx | 2 +- .../Contracts/Subscription/SubscriptionPausable.php | 8 ++++---- .../Contracts/SubscriptionModuleInterface.php | 2 +- src/Framework/PaymentGateways/PaymentGateway.php | 6 +++--- src/Framework/PaymentGateways/SubscriptionModule.php | 2 +- src/Subscriptions/Repositories/SubscriptionRepository.php | 2 +- src/Subscriptions/ValueObjects/SubscriptionStatus.php | 4 ++-- 10 files changed, 18 insertions(+), 16 deletions(-) diff --git a/give.php b/give.php index 991b2da106..80cfad3a21 100644 --- a/give.php +++ b/give.php @@ -190,7 +190,7 @@ final class Give private $container; /** - * @unreleased added Settings service provider + * @since 3.17.0 added Settings service provider * @since 2.25.0 added HttpServiceProvider * @since 2.19.6 added Donors, Donations, and Subscriptions * @since 2.8.0 diff --git a/readme.txt b/readme.txt index da1365a35a..f8dd416199 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: donation, donate, recurring donations, fundraising, crowdfunding Requires at least: 6.4 Tested up to: 6.6 Requires PHP: 7.2 -Stable tag: 3.16.5 +Stable tag: 3.17.0 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -268,7 +268,9 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == = 3.17.0: October 16th, 2024 = * New: Added new security tab with option to enable a honeypot field for visual builder forms +* Fix: Resolved an issue with the donor name prefix block not saving correctly * Dev: Resolved php 8.1 compatability conflict with MyCLabs\Enum\Enum::jsonSerialize() +* Dev: Added gateway api updates for pausing subscriptions = 3.16.5: October 15th, 2024 = * Fix: Resolved a PHP v8+ fatal error on option-based forms when the Tributes add-on was enabled diff --git a/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php b/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php index 4de295d50f..67a11112aa 100644 --- a/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php +++ b/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php @@ -261,7 +261,7 @@ protected function createNodeFromBlockWithUniqueAttributes(BlockModel $block, in } /** - * @unreleased updated honorific field with validation, global options, and user defaults + * @since 3.17.0 updated honorific field with validation, global options, and user defaults * * @since 3.0.0 */ diff --git a/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx b/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx index b0b80ffe44..f85bc162e8 100644 --- a/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx +++ b/src/DonorDashboards/resources/js/app/components/dashboard-loading-spinner/index.tsx @@ -3,7 +3,7 @@ import React from 'react'; import "./style.scss"; /** - * @unreleased reference givewp/src/DonorDashboards/resources/views/donordashboardloader.php + * @since 3.17.0 reference givewp/src/DonorDashboards/resources/views/donordashboardloader.php */ export default function DashboardLoadingSpinner() { diff --git a/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php b/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php index 2e3ee129dd..827a9ada86 100644 --- a/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php +++ b/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php @@ -5,28 +5,28 @@ use Give\Subscriptions\Models\Subscription; /** - * @unreleased + * @since 3.17.0 */ interface SubscriptionPausable { /** * Pause subscription. * - * @unreleased + * @since 3.17.0 */ public function pauseSubscription(Subscription $subscription, array $data): void; /** * Resume subscription. * - * @unreleased + * @since 3.17.0 */ public function resumeSubscription(Subscription $subscription): void; /** * Check if subscription can be paused. * - * @unreleased + * @since 3.17.0 */ public function canPauseSubscription(): bool; } diff --git a/src/Framework/PaymentGateways/Contracts/SubscriptionModuleInterface.php b/src/Framework/PaymentGateways/Contracts/SubscriptionModuleInterface.php index c26e97e079..f70d33d5d1 100644 --- a/src/Framework/PaymentGateways/Contracts/SubscriptionModuleInterface.php +++ b/src/Framework/PaymentGateways/Contracts/SubscriptionModuleInterface.php @@ -33,7 +33,7 @@ public function cancelSubscription(Subscription $subscription); /** * Whether the gateway supports pausing subscriptions. * - * @unreleased + * @since 3.17.0 */ public function canPauseSubscription(): bool; diff --git a/src/Framework/PaymentGateways/PaymentGateway.php b/src/Framework/PaymentGateways/PaymentGateway.php index f464c99a24..47eed6d65e 100644 --- a/src/Framework/PaymentGateways/PaymentGateway.php +++ b/src/Framework/PaymentGateways/PaymentGateway.php @@ -142,7 +142,7 @@ public function cancelSubscription(Subscription $subscription) /** * @inheritDoc * - * @unreleased + * @since 3.17.0 */ public function pauseSubscription(Subscription $subscription, array $data = []): void { @@ -158,7 +158,7 @@ public function pauseSubscription(Subscription $subscription, array $data = []): /** * @inheritDoc * - * @unreleased + * @since 3.17.0 */ public function resumeSubscription(Subscription $subscription): void { @@ -174,7 +174,7 @@ public function resumeSubscription(Subscription $subscription): void /** * @inheritDoc * - * @unreleased + * @since 3.17.0 */ public function canPauseSubscription(): bool { diff --git a/src/Framework/PaymentGateways/SubscriptionModule.php b/src/Framework/PaymentGateways/SubscriptionModule.php index 7e17dc2b04..9fb4314d1a 100644 --- a/src/Framework/PaymentGateways/SubscriptionModule.php +++ b/src/Framework/PaymentGateways/SubscriptionModule.php @@ -34,7 +34,7 @@ public function setGateway(PaymentGateway $gateway) } /** - * @unreleased + * @since 3.17.0 */ public function canPauseSubscription(): bool { diff --git a/src/Subscriptions/Repositories/SubscriptionRepository.php b/src/Subscriptions/Repositories/SubscriptionRepository.php index d5f18a8b62..0c99851181 100644 --- a/src/Subscriptions/Repositories/SubscriptionRepository.php +++ b/src/Subscriptions/Repositories/SubscriptionRepository.php @@ -186,7 +186,7 @@ public function insert(Subscription $subscription) } /** - * @unreleased add expiration column to update + * @since 3.17.0 add expiration column to update * @since 2.24.0 add payment_mode column to update * @since 2.21.0 replace actions with givewp_subscription_updating and givewp_subscription_updated * @since 2.19.6 diff --git a/src/Subscriptions/ValueObjects/SubscriptionStatus.php b/src/Subscriptions/ValueObjects/SubscriptionStatus.php index 9f72b21cd6..8ead12566d 100644 --- a/src/Subscriptions/ValueObjects/SubscriptionStatus.php +++ b/src/Subscriptions/ValueObjects/SubscriptionStatus.php @@ -5,7 +5,7 @@ use Give\Framework\Support\ValueObjects\Enum; /** - * @unreleased Added a new "paused" status + * @since 3.17.0 Added a new "paused" status * @since 2.19.6 * * @method static SubscriptionStatus PENDING() @@ -42,7 +42,7 @@ class SubscriptionStatus extends Enum { const PAUSED = 'paused'; /** - * @unreleased Added a new "paused" status + * @since 3.17.0 Added a new "paused" status * @since 2.24.0 * * @return array From 17ecb6df01bac5ae04d78748717970d1d0288a1d Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 18 Oct 2024 17:18:55 -0400 Subject: [PATCH 17/58] fix/all-settings-warning --- includes/admin/class-admin-settings.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/admin/class-admin-settings.php b/includes/admin/class-admin-settings.php index c409c39c9e..784c58c25e 100644 --- a/includes/admin/class-admin-settings.php +++ b/includes/admin/class-admin-settings.php @@ -68,11 +68,12 @@ public static function get_settings_pages() { * For example: if you register a setting page with give-settings menu slug * then filter will be give-settings_get_settings_pages * + * @unreleased cast to array * @since 1.8 * * @param array $settings Array of settings class object. */ - self::$settings = apply_filters( self::$setting_filter_prefix . '_get_settings_pages', [] ); + self::$settings = (array)apply_filters( self::$setting_filter_prefix . '_get_settings_pages', [] ); return self::$settings; } From 56d72dc208a162910cf1b418660a3f2fccbdfa08 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Mon, 21 Oct 2024 16:46:53 -0300 Subject: [PATCH 18/58] Fix: Hide donation form submit button to prevent errors with PayPal Commerce payment flow (#7576) Co-authored-by: Jon Waldstein --- includes/admin/class-admin-settings.php | 3 ++- .../PayPalCommerce/payPalCommerceGateway.tsx | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/includes/admin/class-admin-settings.php b/includes/admin/class-admin-settings.php index c409c39c9e..784c58c25e 100644 --- a/includes/admin/class-admin-settings.php +++ b/includes/admin/class-admin-settings.php @@ -68,11 +68,12 @@ public static function get_settings_pages() { * For example: if you register a setting page with give-settings menu slug * then filter will be give-settings_get_settings_pages * + * @unreleased cast to array * @since 1.8 * * @param array $settings Array of settings class object. */ - self::$settings = apply_filters( self::$setting_filter_prefix . '_get_settings_pages', [] ); + self::$settings = (array)apply_filters( self::$setting_filter_prefix . '_get_settings_pages', [] ); return self::$settings; } diff --git a/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx b/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx index 78925299e5..7b91b24ace 100644 --- a/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx +++ b/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx @@ -661,11 +661,30 @@ import {PayPalSubscriber} from './types'; throw new Error(sprintf(__('Paypal Donations Error: %s', 'give'), err.message)); } }, + + /** + * @unreleased Hide submit button when PayPal Commerce is selected. + */ Fields() { const {useWatch} = window.givewp.form.hooks; const donationType = useWatch({name: 'donationType'}); const isSubscription = donationType === 'subscription'; + useEffect(() => { + const submitButton = document.querySelector( + 'form#give-next-gen button[type="submit"]' + ); + + if (submitButton) { + submitButton.style.display = 'none'; + } + + return () => { + if (submitButton) { + submitButton.style.display = ''; + } + }; + }, []); return ( From dee8ef3c684ba366eb61618df5f59a2217487c1b Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 21 Oct 2024 15:47:05 -0400 Subject: [PATCH 19/58] Fix: add showToggle to amount descriptions option panel (#7577) Co-authored-by: Jon Waldstein --- .../js/form-builder/src/blocks/fields/amount/inspector/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/inspector/index.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/inspector/index.tsx index 22efe25f90..79230a3d1c 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/inspector/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/inspector/index.tsx @@ -227,6 +227,7 @@ const Inspector = ({attributes, setAttributes}) => { toggleEnabled={descriptionsEnabled} onHandleToggle={(value) => setAttributes({descriptionsEnabled: value})} maxLabelLength={120} + showToggle /> )} From ff612775c0ca40751f7eb0be3964147f1b589d5d Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 21 Oct 2024 15:48:31 -0400 Subject: [PATCH 20/58] chore: prepare for release 3.17.1 --- includes/admin/class-admin-settings.php | 2 +- .../Gateways/PayPalCommerce/payPalCommerceGateway.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/admin/class-admin-settings.php b/includes/admin/class-admin-settings.php index 784c58c25e..1d24c30b27 100644 --- a/includes/admin/class-admin-settings.php +++ b/includes/admin/class-admin-settings.php @@ -68,7 +68,7 @@ public static function get_settings_pages() { * For example: if you register a setting page with give-settings menu slug * then filter will be give-settings_get_settings_pages * - * @unreleased cast to array + * @since 3.17.1 cast to array * @since 1.8 * * @param array $settings Array of settings class object. diff --git a/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx b/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx index 7b91b24ace..d421306f3d 100644 --- a/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx +++ b/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx @@ -663,7 +663,7 @@ import {PayPalSubscriber} from './types'; }, /** - * @unreleased Hide submit button when PayPal Commerce is selected. + * @since 3.17.1 Hide submit button when PayPal Commerce is selected. */ Fields() { const {useWatch} = window.givewp.form.hooks; From 3b2b0d1f10df5da34cc974352ff9171c07c0d68d Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 21 Oct 2024 15:51:30 -0400 Subject: [PATCH 21/58] chore: update readme for 3.17.1 --- give.php | 4 ++-- readme.txt | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/give.php b/give.php index 80cfad3a21..3825afffe8 100644 --- a/give.php +++ b/give.php @@ -6,7 +6,7 @@ * Description: The most robust, flexible, and intuitive way to accept donations on WordPress. * Author: GiveWP * Author URI: https://givewp.com/ - * Version: 3.17.0 + * Version: 3.17.1 * Requires at least: 6.4 * Requires PHP: 7.2 * Text Domain: give @@ -408,7 +408,7 @@ private function setup_constants() { // Plugin version. if (!defined('GIVE_VERSION')) { - define('GIVE_VERSION', '3.17.0'); + define('GIVE_VERSION', '3.17.1'); } // Plugin Root File. diff --git a/readme.txt b/readme.txt index f8dd416199..8a65afdfa3 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: donation, donate, recurring donations, fundraising, crowdfunding Requires at least: 6.4 Tested up to: 6.6 Requires PHP: 7.2 -Stable tag: 3.17.0 +Stable tag: 3.17.1 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -266,6 +266,10 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro 10. Use almost any payment gateway integration with GiveWP through our add-ons or by creating your own add-on. == Changelog == += 3.17.1: October 22nd, 2024 = +* Fix: Resolved an issue with PayPal donation buttons where clicking the GiveWP donate button was causing an error. +* Fix: Resolved an issue where the donation amount level descriptions option was not visible in the form builder. + = 3.17.0: October 16th, 2024 = * New: Added new security tab with option to enable a honeypot field for visual builder forms * Fix: Resolved an issue with the donor name prefix block not saving correctly From cb7ef6b12c3d1ac6a52814836826fac2960436af Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 21 Oct 2024 15:58:36 -0400 Subject: [PATCH 22/58] Fix: update honeypot field setting to be enabled by default (#7575) Co-authored-by: Jon Waldstein --- src/Settings/Security/Actions/RegisterSettings.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Settings/Security/Actions/RegisterSettings.php b/src/Settings/Security/Actions/RegisterSettings.php index e6c3cf8721..3e0088a6b2 100644 --- a/src/Settings/Security/Actions/RegisterSettings.php +++ b/src/Settings/Security/Actions/RegisterSettings.php @@ -38,6 +38,7 @@ protected function getSettings(): array } /** + * @unreleased enable by default * @since 3.17.0 */ public function getHoneypotSettings(): array @@ -50,7 +51,7 @@ public function getHoneypotSettings(): array ), 'id' => 'givewp_donation_forms_honeypot_enabled', 'type' => 'radio_inline', - 'default' => 'disabled', + 'default' => 'enabled', 'options' => [ 'enabled' => __('Enabled', 'give'), 'disabled' => __('Disabled', 'give'), From 79a359527f855acf5f69d35b55d6e1c2a2591ac7 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 21 Oct 2024 15:59:05 -0400 Subject: [PATCH 23/58] chore: udpate since tag --- src/Settings/Security/Actions/RegisterSettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Settings/Security/Actions/RegisterSettings.php b/src/Settings/Security/Actions/RegisterSettings.php index 3e0088a6b2..84cb625604 100644 --- a/src/Settings/Security/Actions/RegisterSettings.php +++ b/src/Settings/Security/Actions/RegisterSettings.php @@ -38,7 +38,7 @@ protected function getSettings(): array } /** - * @unreleased enable by default + * @since 3.17.1 enable by default * @since 3.17.0 */ public function getHoneypotSettings(): array From 0f469fb1c70ae87e9f5234347d95fe402ca3cd13 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 22 Oct 2024 10:40:40 -0300 Subject: [PATCH 24/58] Fix: Resolve issue with "Update Subscription" button being always disabled for Stripe (#7578) --- .../resources/js/app/components/subscription-manager/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx index f7d8793ad0..b3fedf28d8 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx @@ -10,7 +10,6 @@ import {updateSubscriptionWithAPI} from './utils'; import PauseDurationDropdown from './pause-duration-dropdown'; import DashboardLoadingSpinner from '../dashboard-loading-spinner'; import usePauseSubscription from './hooks/pause-subscription'; -import {cancelSubscriptionWithAPI} from '../subscription-cancel-modal/utils'; import './style.scss'; import SubscriptionCancelModal from '../subscription-cancel-modal'; @@ -36,7 +35,7 @@ const SubscriptionManager = ({id, subscription}) => { const [updated, setUpdated] = useState(false); const {handlePause, handleResume, loading} = usePauseSubscription(id); - const subscriptionStatus = subscription.payment.status.id; + const subscriptionStatus = subscription.payment.status?.id || subscription.payment.status.label.toLowerCase(); const showPausingControls = subscription.gateway.can_pause && !['Quarterly', 'Yearly'].includes(subscription.payment.frequency); From af3e90a76ee3aa03a0a68b950ab93fa839e3e6fd Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 22 Oct 2024 09:41:38 -0400 Subject: [PATCH 25/58] chore: update readme --- readme.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 8a65afdfa3..f0307965ae 100644 --- a/readme.txt +++ b/readme.txt @@ -268,7 +268,8 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == = 3.17.1: October 22nd, 2024 = * Fix: Resolved an issue with PayPal donation buttons where clicking the GiveWP donate button was causing an error. -* Fix: Resolved an issue where the donation amount level descriptions option was not visible in the form builder. +* Fix: Resolved an issue where the donation amount level descriptions option was not visible in the form builder. +* Fix: Resolved an issue with the "Update Subscription" button being always disabled for Stripe in the donor dashboard. = 3.17.0: October 16th, 2024 = * New: Added new security tab with option to enable a honeypot field for visual builder forms From 7d96b45542d29c3cfa9d85407a0ec27925c4ed8c Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:26:15 -0700 Subject: [PATCH 26/58] Fix: adjust Subscription Manager styles for custom amount (#7579) --- .../subscription-manager/amount-control/index.js | 2 -- .../subscription-manager/amount-control/style.scss | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js index 9bd9b56346..040cdb3174 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/index.js @@ -105,7 +105,6 @@ const AmountControl = ({currency, onChange, value, options, min, max}) => { value={selectValue} onChange={setSelectValue} /> -
{selectValue === CUSTOM_AMOUNT && (
)} -
{validationError && ( diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss index 0c8a143597..d6d0898ff7 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/amount-control/style.scss @@ -10,24 +10,26 @@ $errorColor: #c91f1f; } .give-donor-dashboard-amount-inputs { + flex: 1; display: flex; flex-direction: column; - .give-donor-dashboard-field-row { + flex: 1; display: flex; + align-items: center; padding: 0; .give-donor-dashboard-select-control { display: flex; - min-width: 100%; + width: 100%; margin: 0; } } } .give-donor-dashboard-currency-control { - margin-top: 10px; + margin-bottom: 2px; .give-donor-dashboard-currency-control__label { font-family: Montserrat, Arial, Helvetica, sans-serif; @@ -47,10 +49,10 @@ $errorColor: #c91f1f; outline: 0 !important; min-width: 190px; width: 100%; - margin-top: 8px; + margin-top: 6px; border: 1px solid #b8b8b8; overflow: hidden; - padding: 0; + padding: 1px; box-shadow: 0 0 0 0 var(--give-donor-dashboard-accent-color); transition: box-shadow 0.1s ease; From 11dbd3e7d2b2928cf89e5237e24663c753fad9d5 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 22 Oct 2024 12:52:05 -0400 Subject: [PATCH 27/58] chore: update readme --- readme.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index f0307965ae..2e3993d83f 100644 --- a/readme.txt +++ b/readme.txt @@ -269,7 +269,8 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro = 3.17.1: October 22nd, 2024 = * Fix: Resolved an issue with PayPal donation buttons where clicking the GiveWP donate button was causing an error. * Fix: Resolved an issue where the donation amount level descriptions option was not visible in the form builder. -* Fix: Resolved an issue with the "Update Subscription" button being always disabled for Stripe in the donor dashboard. +* Fix: Resolved an issue with the "Update Subscription" button being always disabled for Stripe in the donor dashboard. +* Fix: Resolved a styling issue in the donor dashboard with Stripe subscription amount fields. = 3.17.0: October 16th, 2024 = * New: Added new security tab with option to enable a honeypot field for visual builder forms From e930df3a5d1623b68667d426057ee6149c0dda9e Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 22 Oct 2024 15:28:19 -0400 Subject: [PATCH 28/58] fix: add nullish for isMultiStep --- src/DonationForms/resources/app/form/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonationForms/resources/app/form/Header.tsx b/src/DonationForms/resources/app/form/Header.tsx index f5151379e4..fab355bde4 100644 --- a/src/DonationForms/resources/app/form/Header.tsx +++ b/src/DonationForms/resources/app/form/Header.tsx @@ -27,7 +27,7 @@ export default function Header({form}: {form: DonationForm}) { return ( form.settings?.designSettingsImageUrl && ( Date: Mon, 28 Oct 2024 15:06:13 -0400 Subject: [PATCH 29/58] chore: remove unused dynamic property assignment of $auto_updater_obj from Give_License constructor (#7586) Co-authored-by: Jon Waldstein --- includes/class-give-license-handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-give-license-handler.php b/includes/class-give-license-handler.php index 4d1dd4b866..c8632ee2ab 100644 --- a/includes/class-give-license-handler.php +++ b/includes/class-give-license-handler.php @@ -196,6 +196,7 @@ class Give_License * @param string $_account_url * @param int $_item_id * + * @unreleased removed unused auto_updater_obj property assignment * @since 1.0 */ public function __construct( @@ -230,7 +231,6 @@ public function __construct( self::$api_url = is_null( $_api_url ) ? self::$api_url : $_api_url; self::$checkout_url = is_null( $_checkout_url ) ? self::$checkout_url : $_checkout_url; self::$account_url = is_null( $_account_url ) ? self::$account_url : $_account_url; - $this->auto_updater_obj = null; // Add plugin to registered licenses list. array_push( self::$licensed_addons, plugin_basename( $this->file ) ); From 94bd65d435a79134791830176bc6fbd97070df47 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 28 Oct 2024 15:06:23 -0400 Subject: [PATCH 30/58] Fix: php dynamic property warning of $user_id in Give_Addon_Activation_Banner class (#7587) Co-authored-by: Jon Waldstein --- includes/admin/class-addon-activation-banner.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/admin/class-addon-activation-banner.php b/includes/admin/class-addon-activation-banner.php index bffcfaa853..cefb35ac00 100644 --- a/includes/admin/class-addon-activation-banner.php +++ b/includes/admin/class-addon-activation-banner.php @@ -16,11 +16,17 @@ /** * Class Give_Addon_Activation_Banner * + * @unreleased added $user_id property to class * @since 2.1.0 Added pleasing interface when multiple add-ons are activated. */ class Give_Addon_Activation_Banner { + /** + * @unreleased + * @var int + */ + protected $user_id; - /** + /** * Class constructor. * * @since 1.0 From a27f7aeadfa91222d576804a3051c45825a993f9 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Mon, 28 Oct 2024 16:08:08 -0300 Subject: [PATCH 31/58] Refactor: Update PayPal admin logo (#7581) --- assets/src/css/admin/settings.scss | 1 + assets/src/images/admin/paypal-logo.png | Bin 0 -> 126680 bytes assets/src/images/admin/paypal-logo.svg | 1 - .../PayPalCommerce/AdminSettingFields.php | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 assets/src/images/admin/paypal-logo.png delete mode 100644 assets/src/images/admin/paypal-logo.svg diff --git a/assets/src/css/admin/settings.scss b/assets/src/css/admin/settings.scss index a5707b5931..7d08523aae 100644 --- a/assets/src/css/admin/settings.scss +++ b/assets/src/css/admin/settings.scss @@ -917,6 +917,7 @@ a.give-delete { } img { + object-fit: contain; width: 100%; } diff --git a/assets/src/images/admin/paypal-logo.png b/assets/src/images/admin/paypal-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0e3bb21a5bf3e20139bdddd601f6754472aa716a GIT binary patch literal 126680 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%0AB@D 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 ZdGfV<1f~TG7z+52kx&q?dTSK?zW`vu4>SM( literal 0 HcmV?d00001 diff --git a/assets/src/images/admin/paypal-logo.svg b/assets/src/images/admin/paypal-logo.svg deleted file mode 100644 index 559c8fbf61..0000000000 --- a/assets/src/images/admin/paypal-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php b/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php index 858337fef3..0bde29e365 100644 --- a/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php +++ b/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php @@ -167,7 +167,7 @@ public function introductionSection()

"; + } elseif (($avatar = $donor->get_meta('_give_donor_avatar_id', true))) { + // Donor has an avatar + $imageUrl = wp_get_attachment_image_url($avatar, 'thumbnail'); + $alt = __('Donor Avatar', 'give'); + + echo " +
+ $alt +
+ "; + } elseif ($donation['_give_payment_donor_email'] && give_validate_gravatar( $donation['_give_payment_donor_email'] )) { From ef6f5f5e187258aa6da576a8a15dd0247131ee0d Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Fri, 15 Nov 2024 13:57:31 -0700 Subject: [PATCH 46/58] chore: add security considerations to contributing doc --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d2424c715..37ac1a764c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,11 @@ If you would like to submit a pull request, please follow the steps below: * When committing, reference your issue (if present) and include a note about the fix * Push the changes to your fork and [submit a pull request](https://help.github.com/articles/creating-a-pull-request) to the 'master' branch of the GiveWP repository +## Security Considerations + +* When integrating with payment gateways make sure that all data relevent to the gateway is going directly to the gateway an nowhere else, especially credit card data +* Under no circumstances should the payment method details (i.e. credit card deatails) be stored on the server + ## Code Documentation * We ensure that every GiveWP function is documented well and follows the standards set by phpDoc From f6d90c7f72939a5316fad2a01c0f4f6f78db6424 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Mon, 18 Nov 2024 20:36:05 +0100 Subject: [PATCH 47/58] Fix: PayPal connect (#7621) --- src/PaymentGateways/PayPalCommerce/AdminSettingFields.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php b/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php index 0bde29e365..7614bfe7aa 100644 --- a/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php +++ b/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php @@ -432,9 +432,7 @@ class="button-wrap connection-setting -
+ From a85edffa27280e477dd922260047f85a99286f84 Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Tue, 19 Nov 2024 07:45:47 -0800 Subject: [PATCH 48/58] Fix: Handle 8.2 depreciation warnings in the Donation `SessionObjects` (#7617) --- .../SessionObjects/Donation.php | 24 +++++ .../SessionObjects/FormEntry.php | 97 +++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/src/Session/SessionDonation/SessionObjects/Donation.php b/src/Session/SessionDonation/SessionObjects/Donation.php index 9d28a34050..e3c3cdda03 100644 --- a/src/Session/SessionDonation/SessionObjects/Donation.php +++ b/src/Session/SessionDonation/SessionObjects/Donation.php @@ -73,6 +73,30 @@ class Donation implements Objects */ public $paymentGateway; + /** + * Donation-related objects. + * + * @unreleased + * @var FormEntry + */ + public $formEntry; + + /** + * Donor information. + * + * @unreleased + * @var DonorInfo + */ + public $donorInfo; + + /** + * Card information. + * + * @unreleased + * @var CardInfo + */ + public $cardInfo; + /** * Array of properties and their cast type. * diff --git a/src/Session/SessionDonation/SessionObjects/FormEntry.php b/src/Session/SessionDonation/SessionObjects/FormEntry.php index 57721ba5fc..75e89a2e50 100644 --- a/src/Session/SessionDonation/SessionObjects/FormEntry.php +++ b/src/Session/SessionDonation/SessionObjects/FormEntry.php @@ -5,6 +5,8 @@ use Give\Framework\Exceptions\Primitives\InvalidArgumentException; use Give\Helpers\ArrayDataSet; use Give\Session\Objects; +use Give\ValueObjects\CardInfo; +use Give\ValueObjects\DonorInfo; /** * Class FormEntry @@ -92,6 +94,101 @@ class FormEntry implements Objects */ public $paymentGateway; + /** + * Donation-related session objects. + * + * @unreleased + * @var FormEntry + */ + public $formEntry; + + /** + * Donor information. + * + * @unreleased + * @var DonorInfo + */ + public $donorInfo; + + /** + * Card information. + * + * @unreleased + * @var CardInfo + */ + public $cardInfo; + + /** + * Honeypot value to detect spam submissions. + * + * @var string|null + */ + public $honeypot; + + /** + * Form ID prefix. + * + * @var string|null + */ + public $formIdPrefix; + + /** + * Form URL. + * + * @var string|null + */ + public $formUrl; + + /** + * Minimum donation amount. + * + * @var float|null + */ + public $formMinimum; + + /** + * Maximum donation amount. + * + * @var float|null + */ + public $formMaximum; + + /** + * Form hash. + * + * @var string|null + */ + public $formHash; + + /** + * Payment mode. + * + * @var string|null + */ + public $paymentMode; + + /** + * Stripe Payment Method. + * + * @var string|null + */ + public $stripePaymentMethod; + /* + + /** + * Constant Contact signup status. + * + * @var bool|null + */ + public $constantContactSignup; + + /** + * Action property. + * + * @var string|null + */ + public $action; + /** * Take array and return object. * From e702e8bb189c0ab0297f69efb7e4ad21c3be8d6a Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:45:48 -0800 Subject: [PATCH 49/58] Fix: remove factory method from form migration process (#7627) --- .../DataTransferObjects/FormMigrationPayload.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/FormMigration/DataTransferObjects/FormMigrationPayload.php b/src/FormMigration/DataTransferObjects/FormMigrationPayload.php index e4cb5cf569..3cc2df672b 100644 --- a/src/FormMigration/DataTransferObjects/FormMigrationPayload.php +++ b/src/FormMigration/DataTransferObjects/FormMigrationPayload.php @@ -2,9 +2,13 @@ namespace Give\FormMigration\DataTransferObjects; +use Give\DonationForms\FormDesigns\ClassicFormDesign\ClassicFormDesign; +use Give\DonationForms\Models\DonationForm; use Give\DonationForms\Models\DonationForm as DonationFormV3; +use Give\DonationForms\Properties\FormSettings; use Give\DonationForms\V2\Models\DonationForm as DonationFormV2; use Give\DonationForms\ValueObjects\DonationFormStatus; +use Give\FormBuilder\Actions\GenerateDefaultDonationFormBlockCollection; class FormMigrationPayload { @@ -22,8 +26,15 @@ public function __construct(DonationFormV2 $formV2, DonationFormV3 $formV3) public static function fromFormV2(DonationFormV2 $formV2): self { - return new self($formV2, DonationFormV3::factory()->create([ + $formV3 = DonationForm::create([ + 'title' => $formV2->title, 'status' => DonationFormStatus::DRAFT(), - ])); + 'settings' => FormSettings::fromArray([ + 'designId' => ClassicFormDesign::id(), + ]), + 'blocks' => (new GenerateDefaultDonationFormBlockCollection())(), + ]); + + return new self($formV2, $formV3); } } From 4e89033d65c0567ba7a8a84b1796c8c4f2a6212f Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Wed, 20 Nov 2024 18:26:49 +0100 Subject: [PATCH 50/58] Fix: Donation confirmation email - donation description (#7602) --- includes/payments/functions.php | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/includes/payments/functions.php b/includes/payments/functions.php index ad44c830ae..8a427a4044 100644 --- a/includes/payments/functions.php +++ b/includes/payments/functions.php @@ -10,6 +10,10 @@ */ // Exit if accessed directly. +use Give\Donations\Models\Donation; +use Give\Helpers\Form\Utils; +use Give\ValueObjects\Money; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -1487,6 +1491,7 @@ function give_filter_where_older_than_week( $where = '' ) { * enabled. b. separator = The separator between the Form Title and the Donation * Level. * + * @unreleased check if donation form is V3 form * @since 1.5 * * @return string $form_title Returns the full title if $only_level is false, otherwise returns the levels title. @@ -1508,9 +1513,27 @@ function give_get_donation_form_title( $donation_id, $args = [] ) { $args = wp_parse_args( $args, $defaults ); - $form_id = give_get_payment_form_id( $donation_id ); + $form_id = give_get_payment_form_id( $donation_id ); + $form_title = give_get_meta( $donation_id, '_give_payment_form_title', true ); + + // Check if the donation form is V3 form + if (Utils::isV3Form($form_id)) { + $currency = give_get_option('currency'); + $options = give()->form_meta->get_meta($form_id, '_give_donation_levels', true) ?? []; + $donation = Donation::find($donation_id); + + foreach ( $options as $option ) { + if (isset($option['_give_amount'], $option['_give_text'])) { + if (Money::of($option['_give_amount'], $currency )->getMinorAmount() == $donation->amount->getAmount()) { + $form_title = sprintf('%s %s %s', $form_title, $args['separator'], $option['_give_text']); + return apply_filters( 'give_get_donation_form_title', $form_title, $donation_id ); + } + } + } + } + $price_id = give_get_meta( $donation_id, '_give_payment_price_id', true ); - $form_title = give_get_meta( $donation_id, '_give_payment_form_title', true ); + $only_level = $args['only_level']; $separator = $args['separator']; $level_label = ''; From 576ceb914dad34958639347997146509e910dd43 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 20 Nov 2024 13:29:06 -0500 Subject: [PATCH 51/58] chore: prepare for release 3.18.0 --- give.php | 6 +++--- includes/payments/functions.php | 2 +- readme.txt | 15 ++++++++++++--- .../stripePaymentElementGateway.tsx | 2 +- .../SessionDonation/SessionObjects/Donation.php | 6 +++--- .../SessionDonation/SessionObjects/FormEntry.php | 6 +++--- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/give.php b/give.php index 0761287a59..3d19afac04 100644 --- a/give.php +++ b/give.php @@ -6,8 +6,8 @@ * Description: The most robust, flexible, and intuitive way to accept donations on WordPress. * Author: GiveWP * Author URI: https://givewp.com/ - * Version: 3.17.2 - * Requires at least: 6.4 + * Version: 3.18.0 + * Requires at least: 6.5 * Requires PHP: 7.2 * Text Domain: give * Domain Path: /languages @@ -408,7 +408,7 @@ private function setup_constants() { // Plugin version. if (!defined('GIVE_VERSION')) { - define('GIVE_VERSION', '3.17.2'); + define('GIVE_VERSION', '3.18.0'); } // Plugin Root File. diff --git a/includes/payments/functions.php b/includes/payments/functions.php index 8a427a4044..61e8fcb66c 100644 --- a/includes/payments/functions.php +++ b/includes/payments/functions.php @@ -1491,7 +1491,7 @@ function give_filter_where_older_than_week( $where = '' ) { * enabled. b. separator = The separator between the Form Title and the Donation * Level. * - * @unreleased check if donation form is V3 form + * @since 3.18.0 check if donation form is V3 form * @since 1.5 * * @return string $form_title Returns the full title if $only_level is false, otherwise returns the levels title. diff --git a/readme.txt b/readme.txt index 663afe3765..5fa9331a87 100644 --- a/readme.txt +++ b/readme.txt @@ -2,10 +2,10 @@ Contributors: givewp, dlocc, webdevmattcrom, ravinderk, mehul0810, kevinwhoffman, jason_the_adams, henryholtgeerts, kbjohnson90, alaca, benmeredithgmailcom, jonwaldstein, joshuadinh, glaubersilvawp, pauloiankoski Donate link: https://go.givewp.com/home Tags: donation, donate, recurring donations, fundraising, crowdfunding -Requires at least: 6.4 -Tested up to: 6.6 +Requires at least: 6.5 +Tested up to: 6.7 Requires PHP: 7.2 -Stable tag: 3.17.2 +Stable tag: 3.18.0 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -266,6 +266,15 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro 10. Use almost any payment gateway integration with GiveWP through our add-ons or by creating your own add-on. == Changelog == += 3.18.0: November 20th, 2024 = +* New: Added support to our form migration process for our upcoming Constant Contact add-on 3.0.0 version +* New: The donor wall now shows the donor's uploaded image avatar when available +* Fix: Resolved an issue with multi-step form designs growing extra space outside the form +* Fix: Resolved an issue where some people were not able to connect to PayPal +* Fix: Resolved an issue that was preventing the form migration process from completing +* Fix: Resolved an issue with the donation confirmation email sending the wrong donation description for visual form builder forms +* Dev: Addressed PHP 8.2 depreciation warnings in the Donation Session Object + = 3.17.2: November 6th, 2024 = * Fix: Resolved an issue with the Donor Wall shortcode and block filtering by only_comments * Fix: Resolved a WordPress 6.7 styling compatibility issue with the visual form builder diff --git a/src/PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx b/src/PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx index 8ba147dc53..f5e7d3b16d 100644 --- a/src/PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx +++ b/src/PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx @@ -91,7 +91,7 @@ interface StripeGateway extends Gateway { } /** - * @unreleased added fields conditional when donation amount is zero + * @since 3.18.0 added fields conditional when donation amount is zero * @since 3.13.0 Use only stripeKey to load the Stripe script (when stripeConnectedAccountId is missing) to prevent errors when the account is connected through API keys * @since 3.12.1 updated afterCreatePayment response type to include billing details address * @since 3.0.0 diff --git a/src/Session/SessionDonation/SessionObjects/Donation.php b/src/Session/SessionDonation/SessionObjects/Donation.php index e3c3cdda03..40d88a55ca 100644 --- a/src/Session/SessionDonation/SessionObjects/Donation.php +++ b/src/Session/SessionDonation/SessionObjects/Donation.php @@ -76,7 +76,7 @@ class Donation implements Objects /** * Donation-related objects. * - * @unreleased + * @since 3.18.0 * @var FormEntry */ public $formEntry; @@ -84,7 +84,7 @@ class Donation implements Objects /** * Donor information. * - * @unreleased + * @since 3.18.0 * @var DonorInfo */ public $donorInfo; @@ -92,7 +92,7 @@ class Donation implements Objects /** * Card information. * - * @unreleased + * @since 3.18.0 * @var CardInfo */ public $cardInfo; diff --git a/src/Session/SessionDonation/SessionObjects/FormEntry.php b/src/Session/SessionDonation/SessionObjects/FormEntry.php index 75e89a2e50..5a1f0f29a4 100644 --- a/src/Session/SessionDonation/SessionObjects/FormEntry.php +++ b/src/Session/SessionDonation/SessionObjects/FormEntry.php @@ -97,7 +97,7 @@ class FormEntry implements Objects /** * Donation-related session objects. * - * @unreleased + * @since 3.18.0 * @var FormEntry */ public $formEntry; @@ -105,7 +105,7 @@ class FormEntry implements Objects /** * Donor information. * - * @unreleased + * @since 3.18.0 * @var DonorInfo */ public $donorInfo; @@ -113,7 +113,7 @@ class FormEntry implements Objects /** * Card information. * - * @unreleased + * @since 3.18.0 * @var CardInfo */ public $cardInfo; From 50c9ed7a1222bd4aa4eb2de9e4fa4cfd3f168783 Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Wed, 20 Nov 2024 15:30:40 -0300 Subject: [PATCH 52/58] Feature: extract the feature flag for option-based form editor from campaigns domain into the core (#7593) Co-authored-by: Glauber Silva --- assets/src/css/admin/settings.scss | 84 ++- give.php | 3 +- includes/admin/class-admin-settings.php | 4 +- .../settings/class-settings-advanced.php | 686 ++++++++++-------- .../settings/class-settings-gateways.php | 81 ++- .../admin/settings/class-settings-general.php | 2 +- .../class-give-stripe-admin-settings.php | 3 +- includes/post-types.php | 4 +- .../V2/DonationFormsAdminPage.php | 2 + .../components/DonationFormsListTable.tsx | 15 +- src/FeatureFlags/FeatureFlags.php | 14 + .../OptionBasedFormEditor.php | 71 ++ .../OptionBasedFormEditor/ServiceProvider.php | 54 ++ .../AbstractOptionBasedFormEditorSettings.php | 100 +++ .../Settings/Advanced.php | 26 + .../Settings/DefaultOptions.php | 29 + .../Settings/General.php | 30 + tests/includes/legacy/tests-post-types.php | 4 +- 18 files changed, 858 insertions(+), 354 deletions(-) create mode 100644 src/FeatureFlags/FeatureFlags.php create mode 100644 src/FeatureFlags/OptionBasedFormEditor/OptionBasedFormEditor.php create mode 100644 src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php create mode 100644 src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php create mode 100644 src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php create mode 100644 src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php create mode 100644 src/FeatureFlags/OptionBasedFormEditor/Settings/General.php diff --git a/assets/src/css/admin/settings.scss b/assets/src/css/admin/settings.scss index b2d1a14945..2d2af68567 100644 --- a/assets/src/css/admin/settings.scss +++ b/assets/src/css/admin/settings.scss @@ -383,6 +383,89 @@ div.give-field-description { } } +.give_option_based_form_editor_notice { + display: flex; + margin: -2rem 0 0.5rem 0; + gap: 0.3rem; + padding: 0.5rem; + background-color: #fffaf2; + border-radius: 4px; + border: 1px solid #f29718; + border-left-width: 4px; + font-size: 0.875rem; + font-weight: 500; + color: #1a0f00; + line-height: 1.25rem; + max-width: 60rem; + width: 100%; + + svg { + margin: 0.4rem 0.3rem; + height: 1.25rem; + width: 1.25rem; + } +} + +.give-setting-tab-body-general, +.give-setting-tab-body-display, +.give-settings-advanced-tab { + label { + display: flex; + position: relative; + + .give-settings-section-group-helper { + padding-left: 0.2rem; + --popout-display: none; + display: flex; + cursor: help; + + img { + max-width: 18.9px; + } + + &:hover { + --popout-display: block; + } + + &__popout { + background-color: #fff; + border: 1px solid #e6e6e6; + border-radius: 4px; + box-shadow: 0 4px 8px 0 #0000000D; + color: #404040; + display: var(--popout-display, none); + left: 100%; + overflow: hidden; + position: absolute; + top: 0; + transform: translateX(10px); + z-index: 9999; + + img { + max-width: initial; + display: block; + } + + h5 { + font-size: 0.875rem; + font-weight: 700; + line-height: 1.5; + margin: 0; + padding: 1rem 1.5rem 0.5rem; + } + + p { + font-size: 0.75rem; + font-weight: 500; + line-height: 1.5; + margin: 0; + padding: 0 1.5rem 1.5rem; + } + } + } + } +} + .give-payment-gateways-settings { &.give-settings-section-content { .give-settings-section-group-menu { @@ -917,7 +1000,6 @@ a.give-delete { } img { - object-fit: contain; width: 100%; } diff --git a/give.php b/give.php index 0761287a59..3cadbc00b9 100644 --- a/give.php +++ b/give.php @@ -242,7 +242,8 @@ final class Give Give\BetaFeatures\ServiceProvider::class, Give\FormTaxonomies\ServiceProvider::class, Give\DonationSpam\ServiceProvider::class, - Give\Settings\ServiceProvider::class + Give\Settings\ServiceProvider::class, + Give\FeatureFlags\OptionBasedFormEditor\ServiceProvider::class, ]; /** diff --git a/includes/admin/class-admin-settings.php b/includes/admin/class-admin-settings.php index 1d24c30b27..cc73c84442 100644 --- a/includes/admin/class-admin-settings.php +++ b/includes/admin/class-admin-settings.php @@ -949,7 +949,9 @@ class="give-input-field > - + > diff --git a/includes/post-types.php b/includes/post-types.php index 44f556a905..e0a0e3c313 100644 --- a/includes/post-types.php +++ b/includes/post-types.php @@ -56,8 +56,8 @@ function give_setup_post_types() { 'name' => __( 'Donation Forms', 'give' ), 'singular_name' => __( 'Form', 'give' ), 'add_new' => __( 'Add Form', 'give' ), - 'add_new_item' => __( 'Add New Donation Form', 'give' ), - 'edit_item' => __( 'Edit Donation Form', 'give' ), + 'add_new_item' => __('Add New Donation Form', 'give'), + 'edit_item' => __('Edit Donation Form', 'give'), 'new_item' => __( 'New Form', 'give' ), 'all_items' => __( 'All Forms', 'give' ), 'view_item' => __( 'View Form', 'give' ), diff --git a/src/DonationForms/V2/DonationFormsAdminPage.php b/src/DonationForms/V2/DonationFormsAdminPage.php index f03e36ab75..2d33e7f278 100644 --- a/src/DonationForms/V2/DonationFormsAdminPage.php +++ b/src/DonationForms/V2/DonationFormsAdminPage.php @@ -3,6 +3,7 @@ namespace Give\DonationForms\V2; use Give\DonationForms\V2\ListTable\DonationFormsListTable; +use Give\FeatureFlags\OptionBasedFormEditor\OptionBasedFormEditor; use Give\Helpers\EnqueueScript; use WP_Post; use WP_REST_Request; @@ -105,6 +106,7 @@ public function loadScripts() 'showUpgradedTooltip' => !get_user_meta(get_current_user_id(), 'givewp-show-upgraded-tooltip', true), 'supportedAddons' => $this->getSupportedAddons(), 'supportedGateways' => $this->getSupportedGateways(), + 'isOptionBasedFormEditorEnabled' => OptionBasedFormEditor::isEnabled(), ]; EnqueueScript::make('give-admin-donation-forms', 'assets/dist/js/give-admin-donation-forms.js') diff --git a/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx b/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx index 2b9ffbd681..38993e202f 100644 --- a/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx +++ b/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx @@ -26,6 +26,7 @@ declare global { isMigrated: boolean; supportedAddons: Array; supportedGateways: Array; + isOptionBasedFormEditorEnabled: boolean; }; GiveNextGen?: { @@ -257,12 +258,14 @@ export default function DonationFormsListTable() { columnFilters={columnFilters} banner={Onboarding} > - + {window.GiveDonationForms.isOptionBasedFormEditorEnabled && ( + + )} + +
+ +
%3$s
+

%4$s

+
+
', + esc_url(GIVE_PLUGIN_URL . 'assets/dist/images/admin/help-circle.svg'), + esc_url(GIVE_PLUGIN_URL . 'assets/dist/images/admin/give-settings-gateways-v2.jpg'), + __('Only for Option-Based Form Editor', 'give'), + __('Uses the traditional settings options for creating and customizing a donation form.', + 'give') + ); + } + + /** + * @unreleased + */ + public static function existOptionBasedFormsOnDb(): bool + { + return (bool)give(DonationFormsRepository::class)->prepareQuery() + ->whereNotExists(function ( + QueryBuilder $builder + ) { + $builder + ->select(['meta_value', 'formBuilderSettings']) + ->from(DB::raw(DB::prefix('give_formmeta'))) + ->where('meta_key', 'formBuilderSettings') + ->whereRaw('AND form_id = ID'); + })->count(); + } +} diff --git a/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php b/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php new file mode 100644 index 0000000000..25e4b2cd94 --- /dev/null +++ b/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php @@ -0,0 +1,54 @@ +maybeDisableOptionBasedFormEditorSettings(); + } + + /** + * @return void + */ + private function maybeDisableOptionBasedFormEditorSettings() + { + // General Tab + Hooks::addFilter('give_get_settings_general', GeneralSettings::class, 'maybeDisableOptions', 999); + + // Payment Gateways Tab + add_filter('give_settings_payment_gateways_menu_groups', function ($groups) { + if ( ! OptionBasedFormEditor::isEnabled() && isset($groups['v2'])) { + unset($groups['v2']); + } + + return $groups; + }); + + // Default Options Tab + Hooks::addFilter('give_get_settings_display', DefaultOptionsSettings::class, 'maybeDisableOptions', 999); + + // Advance Tab + Hooks::addFilter('give_get_settings_advanced', AdvancedSettings::class, 'maybeDisableOptions', 999); + } +} diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php new file mode 100644 index 0000000000..14d621b2b6 --- /dev/null +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php @@ -0,0 +1,100 @@ + $value) { + if (in_array($key, $this->getDisabledSectionIds())) { + unset($sections[$key]); + } + } + + return $sections; + } + + /** + * @unreleased + */ + final public function maybeDisableOptions(array $options): array + { + foreach ($options as $key => $value) { + if ( ! $this->isOptionDisabled($value['id']) && ! $this->isCurrentSectionDisabled()) { + continue; + } + + if (OptionBasedFormEditor::isEnabled()) { + $options[$key]['name'] .= isset($value['name']) ? OptionBasedFormEditor::helperText() : ''; + } else { + unset($options[$key]); + } + } + + return $options; + } + + /** + * @unreleased + */ + final public function maybeSetNewDefaultSection($currentSection) + { + if (OptionBasedFormEditor::isEnabled()) { + return $currentSection; + } + + $newDefaultSection = $this->getNewDefaultSection(); + + return ! empty($newDefaultSection) && $newDefaultSection != $currentSection ? $newDefaultSection : $currentSection; + } + + /** + * @unreleased + */ + private function isOptionDisabled($option): bool + { + return $option && in_array($option, $this->getDisabledOptionIds()); + } + + /** + * @unreleased + */ + private function isCurrentSectionDisabled(): bool + { + return in_array(give_get_current_setting_section(), $this->getDisabledSectionIds()); + } +} diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php new file mode 100644 index 0000000000..746234eccf --- /dev/null +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php @@ -0,0 +1,26 @@ +routeForm->getOptionName(), + // Stripe Section + 'stripe_js_fallback', + 'stripe_styles', + ]; + } +} diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php new file mode 100644 index 0000000000..ac714a098c --- /dev/null +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php @@ -0,0 +1,29 @@ +assertEquals( 'Donation Forms', $wp_post_types['give_forms']->labels->name ); $this->assertEquals( 'Form', $wp_post_types['give_forms']->labels->singular_name ); $this->assertEquals( 'Add Form', $wp_post_types['give_forms']->labels->add_new ); - $this->assertEquals( 'Add New Donation Form', $wp_post_types['give_forms']->labels->add_new_item ); - $this->assertEquals( 'Edit Donation Form', $wp_post_types['give_forms']->labels->edit_item ); + $this->assertEquals('Add New Donation Form', $wp_post_types['give_forms']->labels->add_new_item); + $this->assertEquals('Edit Donation Form', $wp_post_types['give_forms']->labels->edit_item); $this->assertEquals( 'New Form', $wp_post_types['give_forms']->labels->new_item ); $this->assertEquals( 'All Forms', $wp_post_types['give_forms']->labels->all_items ); $this->assertEquals( 'View Form', $wp_post_types['give_forms']->labels->view_item ); From 1e8b664dcb81ccd08f852b22c589b2e8cfd0e321 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 20 Nov 2024 13:33:39 -0500 Subject: [PATCH 53/58] chore: merge develop and update readme --- .../admin/settings/class-settings-advanced.php | 2 +- .../admin/settings/class-settings-gateways.php | 2 +- readme.txt | 1 + src/FeatureFlags/FeatureFlags.php | 4 ++-- .../OptionBasedFormEditor.php | 8 ++++---- .../OptionBasedFormEditor/ServiceProvider.php | 6 +++--- .../AbstractOptionBasedFormEditorSettings.php | 18 +++++++++--------- .../Settings/Advanced.php | 4 ++-- .../Settings/DefaultOptions.php | 4 ++-- .../OptionBasedFormEditor/Settings/General.php | 4 ++-- 10 files changed, 27 insertions(+), 26 deletions(-) diff --git a/includes/admin/settings/class-settings-advanced.php b/includes/admin/settings/class-settings-advanced.php index af506ec445..2faf4ba85e 100644 --- a/includes/admin/settings/class-settings-advanced.php +++ b/includes/admin/settings/class-settings-advanced.php @@ -419,7 +419,7 @@ public function sanitize_option_donor_default_user_role($value) { } /** - * @unreleased + * @since 3.18.0 */ public function _render_give_based_form_editor_notice($field, $value) { diff --git a/includes/admin/settings/class-settings-gateways.php b/includes/admin/settings/class-settings-gateways.php index 178af6d485..e713ab7d51 100644 --- a/includes/admin/settings/class-settings-gateways.php +++ b/includes/admin/settings/class-settings-gateways.php @@ -412,7 +412,7 @@ static function ($value, $key) { ]; /** - * @unreleased + * @since 3.18.0 */ $groups = apply_filters('give_settings_payment_gateways_menu_groups', $groups); diff --git a/readme.txt b/readme.txt index 5fa9331a87..a14c9d358c 100644 --- a/readme.txt +++ b/readme.txt @@ -269,6 +269,7 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro = 3.18.0: November 20th, 2024 = * New: Added support to our form migration process for our upcoming Constant Contact add-on 3.0.0 version * New: The donor wall now shows the donor's uploaded image avatar when available +* New: Added a global setting to enable or disable the Option-Based Form Editor and settings. * Fix: Resolved an issue with multi-step form designs growing extra space outside the form * Fix: Resolved an issue where some people were not able to connect to PayPal * Fix: Resolved an issue that was preventing the form migration process from completing diff --git a/src/FeatureFlags/FeatureFlags.php b/src/FeatureFlags/FeatureFlags.php index 8298ca44bc..45aa47a8b6 100644 --- a/src/FeatureFlags/FeatureFlags.php +++ b/src/FeatureFlags/FeatureFlags.php @@ -3,12 +3,12 @@ namespace Give\FeatureFlags; /** - * @unreleased + * @since 3.18.0 */ interface FeatureFlags { /** - * @unreleased + * @since 3.18.0 */ public static function isEnabled(): bool; } diff --git a/src/FeatureFlags/OptionBasedFormEditor/OptionBasedFormEditor.php b/src/FeatureFlags/OptionBasedFormEditor/OptionBasedFormEditor.php index a6a8f71fd2..e086111c39 100644 --- a/src/FeatureFlags/OptionBasedFormEditor/OptionBasedFormEditor.php +++ b/src/FeatureFlags/OptionBasedFormEditor/OptionBasedFormEditor.php @@ -8,12 +8,12 @@ use Give\Framework\QueryBuilder\QueryBuilder; /** - * @unreleased + * @since 3.18.0 */ class OptionBasedFormEditor implements FeatureFlags { /** - * @unreleased + * @since 3.18.0 */ public static function isEnabled(): bool { @@ -30,7 +30,7 @@ public static function isEnabled(): bool } /** - * @unreleased + * @since 3.18.0 */ public static function helperText(): string { @@ -53,7 +53,7 @@ public static function helperText(): string } /** - * @unreleased + * @since 3.18.0 */ public static function existOptionBasedFormsOnDb(): bool { diff --git a/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php b/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php index 25e4b2cd94..e7afcb15a9 100644 --- a/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php +++ b/src/FeatureFlags/OptionBasedFormEditor/ServiceProvider.php @@ -9,19 +9,19 @@ use Give\ServiceProviders\ServiceProvider as ServiceProviderInterface; /** - * @unreleased + * @since 3.18.0 */ class ServiceProvider implements ServiceProviderInterface { /** - * @unreleased + * @since 3.18.0 */ public function register() { } /** - * @unreleased + * @since 3.18.0 */ public function boot() { diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php index 14d621b2b6..fdfd432671 100644 --- a/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/AbstractOptionBasedFormEditorSettings.php @@ -5,17 +5,17 @@ use Give\FeatureFlags\OptionBasedFormEditor\OptionBasedFormEditor; /** - * @unreleased + * @since 3.18.0 */ abstract class AbstractOptionBasedFormEditorSettings { /** - * @unreleased + * @since 3.18.0 */ abstract public function getDisabledOptionIds(): array; /** - * @unreleased + * @since 3.18.0 */ public function getDisabledSectionIds(): array { @@ -23,7 +23,7 @@ public function getDisabledSectionIds(): array } /** - * @unreleased + * @since 3.18.0 */ public function getNewDefaultSection(): string { @@ -31,7 +31,7 @@ public function getNewDefaultSection(): string } /** - * @unreleased + * @since 3.18.0 */ final public function maybeDisableSections(array $sections): array { @@ -49,7 +49,7 @@ final public function maybeDisableSections(array $sections): array } /** - * @unreleased + * @since 3.18.0 */ final public function maybeDisableOptions(array $options): array { @@ -69,7 +69,7 @@ final public function maybeDisableOptions(array $options): array } /** - * @unreleased + * @since 3.18.0 */ final public function maybeSetNewDefaultSection($currentSection) { @@ -83,7 +83,7 @@ final public function maybeSetNewDefaultSection($currentSection) } /** - * @unreleased + * @since 3.18.0 */ private function isOptionDisabled($option): bool { @@ -91,7 +91,7 @@ private function isOptionDisabled($option): bool } /** - * @unreleased + * @since 3.18.0 */ private function isCurrentSectionDisabled(): bool { diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php index 746234eccf..8d88292662 100644 --- a/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/Advanced.php @@ -3,12 +3,12 @@ namespace Give\FeatureFlags\OptionBasedFormEditor\Settings; /** - * @unreleased + * @since 3.18.0 */ class Advanced extends AbstractOptionBasedFormEditorSettings { /** - * @unreleased + * @since 3.18.0 */ public function getDisabledOptionIds(): array { diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php index ac714a098c..ab2c15509b 100644 --- a/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php @@ -3,12 +3,12 @@ namespace Give\FeatureFlags\OptionBasedFormEditor\Settings; /** - * @unreleased + * @since 3.18.0 */ class DefaultOptions extends AbstractOptionBasedFormEditorSettings { /** - * @unreleased + * @since 3.18.0 */ public function getDisabledOptionIds(): array { diff --git a/src/FeatureFlags/OptionBasedFormEditor/Settings/General.php b/src/FeatureFlags/OptionBasedFormEditor/Settings/General.php index 7dda34735c..53e9868e30 100644 --- a/src/FeatureFlags/OptionBasedFormEditor/Settings/General.php +++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/General.php @@ -3,12 +3,12 @@ namespace Give\FeatureFlags\OptionBasedFormEditor\Settings; /** - * @unreleased + * @since 3.18.0 */ class General extends AbstractOptionBasedFormEditorSettings { /** - * @unreleased + * @since 3.18.0 */ public function getDisabledOptionIds(): array { From 244944b277079f5121096b3e7c94c1f7c245fda3 Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:27:46 -0800 Subject: [PATCH 54/58] Refactor: update constantcontact form meta decorator (#7580) --- src/FormMigration/FormMetaDecorator.php | 8 ++++---- .../FormMigration/Steps/TestConstantContact.php | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/FormMigration/FormMetaDecorator.php b/src/FormMigration/FormMetaDecorator.php index dced76c47d..f98f328d21 100644 --- a/src/FormMigration/FormMetaDecorator.php +++ b/src/FormMigration/FormMetaDecorator.php @@ -520,7 +520,7 @@ public function isConstantContactEnabled(): bool $isFormDisabled = give_is_setting_enabled($this->getMeta('_give_constant_contact_disable'), 'true'); $isGloballyEnabled = give_is_setting_enabled( - give_get_option('give_constant_contact_show_checkout_signup'), + give_get_option('givewp_constant_contact_show_checkout_signup'), 'on' ); @@ -532,7 +532,7 @@ public function isConstantContactEnabled(): bool */ public function getConstantContactLabel(): string { - $defaultMeta = give_get_option('give_constant_contact_label', __('Subscribe to our newsletter?')); + $defaultMeta = give_get_option('givewp_constant_contact_label', __('Subscribe to our newsletter?')); return $this->getMeta('_give_constant_contact_custom_label', $defaultMeta); } @@ -544,7 +544,7 @@ public function getConstantContactDefaultChecked(): bool { $defaultMeta = give_is_setting_enabled( give_get_option( - 'give_constant_contact_checked_default', + 'givewp_constant_contact_checked_default', true ), 'on' @@ -558,7 +558,7 @@ public function getConstantContactDefaultChecked(): bool */ public function getConstantContactSelectedLists(): array { - $defaultMeta = give_get_option('give_constant_contact_list', []); + $defaultMeta = give_get_option('givewp_constant_contact_list', []); return (array)$this->getMeta('_give_constant_contact', $defaultMeta); } diff --git a/tests/Feature/FormMigration/Steps/TestConstantContact.php b/tests/Feature/FormMigration/Steps/TestConstantContact.php index 313e1280e4..ab133a52d5 100644 --- a/tests/Feature/FormMigration/Steps/TestConstantContact.php +++ b/tests/Feature/FormMigration/Steps/TestConstantContact.php @@ -26,10 +26,10 @@ public function testFormMigratesUsingGlobalSettingsWhenGloballyEnabled(): void { // Arrange $options = [ - 'give_constant_contact_show_checkout_signup' => 'on', - 'give_constant_contact_label' => 'Subscribe to our newsletter?', - 'give_constant_contact_checked_default' => 'on', - 'give_constant_contact_list' => ['1928414891'], + 'givewp_constant_contact_show_checkout_signup' => 'on', + 'givewp_constant_contact_label' => 'Subscribe to our newsletter?', + 'givewp_constant_contact_checked_default' => 'on', + 'givewp_constant_contact_list' => ['1928414891'], ]; foreach ($options as $key => $value) { give_update_option($key, $value); @@ -43,8 +43,8 @@ public function testFormMigratesUsingGlobalSettingsWhenGloballyEnabled(): void // Assert $block = $v3Form->blocks->findByName('givewp/constantcontact'); $this->assertTrue(true, $block->getAttribute('checked' === 'on')); - $this->assertSame($options['give_constant_contact_label'], $block->getAttribute('label')); - $this->assertSame($options['give_constant_contact_list'], $block->getAttribute('selectedEmailLists')); + $this->assertSame($options['givewp_constant_contact_label'], $block->getAttribute('label')); + $this->assertSame($options['givewp_constant_contact_list'], $block->getAttribute('selectedEmailLists')); } /** @@ -53,7 +53,7 @@ public function testFormMigratesUsingGlobalSettingsWhenGloballyEnabled(): void public function testFormConfiguredToDisableConstantContactIsMigratedWithoutConstantContactBlock() { // Arrange - give_update_option('give_constant_contact_show_checkout_signup', 'on'); + give_update_option('givewp_constant_contact_show_checkout_signup', 'on'); $meta = ['_give_constant_contact_disable' => 'true']; $v2Form = $this->createSimpleDonationForm(['meta' => $meta]); From d056e04cbb1b43664b2b13c1df6fb7328cc0db11 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 20 Nov 2024 16:17:53 -0500 Subject: [PATCH 55/58] chore: bump wp version --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index a14c9d358c..0f29bbd1a4 100644 --- a/readme.txt +++ b/readme.txt @@ -167,7 +167,7 @@ Here’s a few ways you can contribute to GiveWP: = Minimum Requirements = -* WordPress 6.4 or greater +* WordPress 6.5 or greater * PHP version 7.2 or greater * MySQL version 5.7 or greater * MariaDB version 10 or later From 9f915d2b83eac75988311358eda7ed8071403817 Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Mon, 25 Nov 2024 12:52:22 -0500 Subject: [PATCH 56/58] Fix: Load textdomain on `init` action hook (#7631) --- give.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/give.php b/give.php index 0183a566a0..be20699722 100644 --- a/give.php +++ b/give.php @@ -268,6 +268,7 @@ public function __construct() /** * Init Give when WordPress Initializes. * + * @unreleased Move the loading of the `give` textdomain to the `init` action hook. * @since 1.8.9 */ public function init() @@ -279,9 +280,6 @@ public function init() */ do_action('before_give_init'); - // Set up localization. - $this->load_textdomain(); - $this->bindClasses(); $this->setupExceptionHandler(); @@ -379,6 +377,7 @@ private function loadServiceProviders() /** * Bootstraps the Give Plugin * + * @unreleased Load the `give` textdomain on the `init` action hook. * @since 2.8.0 */ public function boot() @@ -392,6 +391,9 @@ public function boot() add_action('plugins_loaded', [$this, 'init'], 0); + // Set up localization. + add_action('init', [$this, 'load_textdomain']); + register_activation_hook(GIVE_PLUGIN_FILE, [$this, 'install']); do_action('give_loaded'); From a20c9d8f727adc9863bbd4bff098087f2af9a29c Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Mon, 2 Dec 2024 15:53:21 -0300 Subject: [PATCH 57/58] Feature: Add support for integrating Blink Payment with the Donor Dashboard (#7632) Co-authored-by: Jon Waldstein --- src/DonorDashboards/App.php | 5 ++- .../components/subscription-manager/index.tsx | 41 ++++++++++++------- .../payment-method-control/index.js | 10 +++++ .../app/components/subscription-row/index.tsx | 15 +++++-- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/DonorDashboards/App.php b/src/DonorDashboards/App.php index 8d5a605b7b..896aaba75b 100644 --- a/src/DonorDashboards/App.php +++ b/src/DonorDashboards/App.php @@ -129,8 +129,9 @@ public function getLoaderTemplatePath() /** * Enqueue assets for front-end donor dashboards * + * @unreleased Add action to allow enqueueing additional assets. + * @since 2.11.0 Set script translations. * @since 2.10.0 - * @since 2.11.0 Set script translations. * * @return void */ @@ -175,6 +176,8 @@ public function loadAssets() [], null ); + + do_action('give_donor_dashboard_enqueue_assets'); } /** diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx index b3fedf28d8..e1a9da90d9 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx @@ -23,6 +23,9 @@ import SubscriptionCancelModal from '../subscription-cancel-modal'; */ const normalizeAmount = (float, decimals) => Number.parseFloat(float).toFixed(decimals); +/** + * @unreleased Add support for hiding amount controls via filter + */ const SubscriptionManager = ({id, subscription}) => { const gatewayRef = useRef(); const [isPauseModalOpen, setIsPauseModalOpen] = useState(false); @@ -37,6 +40,8 @@ const SubscriptionManager = ({id, subscription}) => { const subscriptionStatus = subscription.payment.status?.id || subscription.payment.status.label.toLowerCase(); + const showAmountControls = subscription.gateway.can_update; + const showPaymentMethodControls = subscription.gateway.can_update_payment_method ?? showAmountControls; const showPausingControls = subscription.gateway.can_pause && !['Quarterly', 'Yearly'].includes(subscription.payment.frequency); @@ -95,19 +100,23 @@ const SubscriptionManager = ({id, subscription}) => { return (
- - + {showAmountControls && ( + + )} + {showPaymentMethodControls && ( + + )} {loading && } @@ -135,7 +144,11 @@ const SubscriptionManager = ({id, subscription}) => { )} -