From 01ff7c9ef3108f94faa896820853ee35cd33d8c5 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 25 Jun 2024 14:52:22 -0400 Subject: [PATCH 001/190] chore: prepare for release 3.13.0 --- give.php | 4 ++-- readme.txt | 9 ++++++++- src/FormMigration/FormMetaDecorator.php | 8 ++++---- src/FormMigration/Steps/CurrencySwitcher.php | 4 ++-- .../stripePaymentElementGateway.tsx | 2 +- .../PayPalCommerce/AjaxRequestHandler.php | 2 +- .../PayPalCommerce/ScriptLoader.php | 2 +- src/Promotions/InPluginUpsells/SaleBanners.php | 16 ++++++++-------- .../InPluginUpsells/StellarSaleBanners.php | 14 +++++++------- .../InPluginUpsells/resources/js/sale-banner.js | 2 +- .../resources/views/stellarwp-sale-banner.php | 2 +- .../ReportsWidgetBanner/ReportsWidgetBanner.php | 8 ++++---- .../ReportsWidgetBanner/window/widgetWindow.ts | 2 +- src/Promotions/ServiceProvider.php | 2 +- .../FormMigration/Steps/TestCurrencySwitcher.php | 12 ++++++------ .../FormMigration/TestFormMetaDecorator.php | 8 ++++---- 16 files changed, 52 insertions(+), 45 deletions(-) diff --git a/give.php b/give.php index cb8be54031..d291ca9584 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.12.3 + * Version: 3.13.0 * Requires at least: 6.3 * Requires PHP: 7.2 * Text Domain: give @@ -404,7 +404,7 @@ private function setup_constants() { // Plugin version. if (!defined('GIVE_VERSION')) { - define('GIVE_VERSION', '3.12.3'); + define('GIVE_VERSION', '3.13.0'); } // Plugin Root File. diff --git a/readme.txt b/readme.txt index 0f41a209e8..00a455f6d0 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: donation, donate, recurring donations, fundraising, crowdfunding Requires at least: 6.3 Tested up to: 6.5 Requires PHP: 7.2 -Stable tag: 3.12.3 +Stable tag: 3.13.0 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -262,6 +262,13 @@ 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.13.0: June 25th, 2024 = +* New: Added option to PayPal settings to keep webhooks when disconnecting account +* Enhancement: Updated donor comment block active state border color to be the primary color +* Enhancement: Updated form builder global settings links to open in new tabs +* Fix: Resolved an issue with some validation errors using Stripe Payment Element Gateway not displaying information correctly +* Fix: Resolved an issue with Stripe accounts using API keys and the Stripe Payment Element Gateway + = 3.12.3: June 19th, 2024 = * Fix: Resolved an issue where PayPal was not processing donations due to missing billing address fields diff --git a/src/FormMigration/FormMetaDecorator.php b/src/FormMigration/FormMetaDecorator.php index 960cb825b0..5db8770d4e 100644 --- a/src/FormMigration/FormMetaDecorator.php +++ b/src/FormMigration/FormMetaDecorator.php @@ -984,7 +984,7 @@ public function getActiveCampaignTags(): array } /** - * @unreleased + * @since 3.13.0 */ public function getCurrencySwitcherStatus(): string { @@ -992,7 +992,7 @@ public function getCurrencySwitcherStatus(): string } /** - * @unreleased + * @since 3.13.0 */ public function getCurrencySwitcherMessage(): string { @@ -1008,7 +1008,7 @@ public function getCurrencySwitcherMessage(): string } /** - * @unreleased + * @since 3.13.0 */ public function getCurrencySwitcherDefaultCurrency(): string { @@ -1016,7 +1016,7 @@ public function getCurrencySwitcherDefaultCurrency(): string } /** - * @unreleased + * @since 3.13.0 */ public function getCurrencySwitcherSupportedCurrencies(): array { diff --git a/src/FormMigration/Steps/CurrencySwitcher.php b/src/FormMigration/Steps/CurrencySwitcher.php index 06e0fb5e75..86af2234a0 100644 --- a/src/FormMigration/Steps/CurrencySwitcher.php +++ b/src/FormMigration/Steps/CurrencySwitcher.php @@ -5,12 +5,12 @@ use Give\FormMigration\Contracts\FormMigrationStep; /** - * @unreleased + * @since 3.13.0 */ class CurrencySwitcher extends FormMigrationStep { /** - * @unreleased + * @since 3.13.0 */ public function process() { diff --git a/src/PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx b/src/PaymentGateways/Gateways/Stripe/StripePaymentElementGateway/stripePaymentElementGateway.tsx index 3e5a0573cb..3103d4e08c 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 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.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/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php b/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php index d7cfde3d05..eb872ec95c 100644 --- a/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php +++ b/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php @@ -184,7 +184,7 @@ public function onGetPartnerUrlAjaxRequestHandler() /** * give_paypal_commerce_disconnect_account ajax request handler. * - * @unreleased Add new $keepWebhooks option + * @since 3.13.0 Add new $keepWebhooks option * @since 2.30.0 Add support for mode param. * @since 2.25.0 Remove merchant seller token. * @since 2.9.0 diff --git a/src/PaymentGateways/PayPalCommerce/ScriptLoader.php b/src/PaymentGateways/PayPalCommerce/ScriptLoader.php index 60d0d80279..c229bcf247 100644 --- a/src/PaymentGateways/PayPalCommerce/ScriptLoader.php +++ b/src/PaymentGateways/PayPalCommerce/ScriptLoader.php @@ -89,7 +89,7 @@ public function __construct(PayPalCommerceGateway $payPalCommerceGateway) /** * Load admin scripts * - * @unreleased Add new "keepWebhooksAfterDisconnect" string + * @since 3.13.0 Add new "keepWebhooksAfterDisconnect" string * @since 2.9.0 */ public function loadAdminScripts() diff --git a/src/Promotions/InPluginUpsells/SaleBanners.php b/src/Promotions/InPluginUpsells/SaleBanners.php index afd60c95f3..1a08f9bcb7 100644 --- a/src/Promotions/InPluginUpsells/SaleBanners.php +++ b/src/Promotions/InPluginUpsells/SaleBanners.php @@ -184,7 +184,7 @@ public static function isShowing(): bool } /** - * @unreleased remove all_access_pass. + * @since 3.13.0 remove all_access_pass. * @since 3.1.0 retrieve licensed plugin slugs. */ public static function getLicensedPluginSlugs(): array @@ -227,7 +227,7 @@ public static function getUserPricingPlan(): string } /** - * @unreleased add type for $data. + * @since 3.13.0 add type for $data. * @since 3.1.0 return data by user pricing plan. */ public static function getDataByPricingPlan(array $data): string @@ -243,7 +243,7 @@ public static function getDataByPricingPlan(array $data): string } /** - * @unreleased + * @since 3.13.0 * * This method cycles through the visible banners, selecting the next banner in the list * on each call. If no banners are visible, or if the session index is not set, it returns @@ -275,7 +275,7 @@ public function alternateVisibleBanners(): array } /** - * @unreleased + * @since 3.13.0 */ public function startSession(): void { @@ -285,7 +285,7 @@ public function startSession(): void } /** - * @unreleased + * @since 3.13.0 */ public function destroySession(): void { @@ -296,7 +296,7 @@ public function destroySession(): void /** - * @unreleased + * @since 3.13.0 */ public static function getBasicLicenseSlugs(): array { @@ -335,7 +335,7 @@ public static function getBasicLicenseSlugs(): array } /** - * @unreleased + * @since 3.13.0 */ public static function getPlusLicenseSlugs(): array { @@ -360,7 +360,7 @@ public static function getPlusLicenseSlugs(): array } /** - * @unreleased + * @since 3.13.0 */ public static function getProLicenseSlugs(): array { diff --git a/src/Promotions/InPluginUpsells/StellarSaleBanners.php b/src/Promotions/InPluginUpsells/StellarSaleBanners.php index 12b04f3c6c..9ddf392324 100644 --- a/src/Promotions/InPluginUpsells/StellarSaleBanners.php +++ b/src/Promotions/InPluginUpsells/StellarSaleBanners.php @@ -3,12 +3,12 @@ namespace Give\Promotions\InPluginUpsells; /** - * @unreleased + * @since 3.13.0 */ class StellarSaleBanners extends SaleBanners { /** - * @unreleased + * @since 3.13.0 */ public function getBanners(): array { @@ -53,7 +53,7 @@ public function getBanners(): array } /** - * @unreleased + * @since 3.13.0 */ public function getP2PBanners(): array { @@ -83,7 +83,7 @@ public function getP2PBanners(): array } /** - * @unreleased + * @since 3.13.0 */ public function getAddonBanners(): array { @@ -101,7 +101,7 @@ public function getAddonBanners(): array } /** - * @unreleased + * @since 3.13.0 */ public function loadScripts(): void { @@ -116,7 +116,7 @@ public function loadScripts(): void } /** - * @unreleased + * @since 3.13.0 */ public function render(): void { @@ -128,7 +128,7 @@ public function render(): void } /** - * @unreleased + * @since 3.13.0 */ public static function isShowing(): bool { diff --git a/src/Promotions/InPluginUpsells/resources/js/sale-banner.js b/src/Promotions/InPluginUpsells/resources/js/sale-banner.js index 29c000c788..815c75436b 100644 --- a/src/Promotions/InPluginUpsells/resources/js/sale-banner.js +++ b/src/Promotions/InPluginUpsells/resources/js/sale-banner.js @@ -5,7 +5,7 @@ const listTable = document.querySelector('#give-admin-donations-root, #give-admi const settings = document.querySelector('.give-settings-header'); /** - * @unreleased move placement of banner on Reports page. + * @since 3.13.0 move placement of banner on Reports page. * @since 3.1.0 show banner on ListTable pages. */ const hideBanner = ({target: dismissAction}) => { diff --git a/src/Promotions/InPluginUpsells/resources/views/stellarwp-sale-banner.php b/src/Promotions/InPluginUpsells/resources/views/stellarwp-sale-banner.php index 7f97d12441..0b72fc2f4b 100644 --- a/src/Promotions/InPluginUpsells/resources/views/stellarwp-sale-banner.php +++ b/src/Promotions/InPluginUpsells/resources/views/stellarwp-sale-banner.php @@ -1,6 +1,6 @@ Date: Tue, 25 Jun 2024 14:54:02 -0400 Subject: [PATCH 002/190] chore: update release date --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 00a455f6d0..192fe80dde 100644 --- a/readme.txt +++ b/readme.txt @@ -262,7 +262,7 @@ 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.13.0: June 25th, 2024 = += 3.13.0: June 26th, 2024 = * New: Added option to PayPal settings to keep webhooks when disconnecting account * Enhancement: Updated donor comment block active state border color to be the primary color * Enhancement: Updated form builder global settings links to open in new tabs From 84d4b2819af0df255cbe01fa533166429fba040c Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:57:22 -0700 Subject: [PATCH 003/190] refactor: improve the styling of the anonymous donation checkbox description (#7413) --- src/DonationForms/resources/styles/elements/_fields.scss | 8 +++++++- .../form-builder/src/blocks/fields/anonymous/styles.scss | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/DonationForms/resources/styles/elements/_fields.scss b/src/DonationForms/resources/styles/elements/_fields.scss index a1379eabcd..6c824d0fb7 100644 --- a/src/DonationForms/resources/styles/elements/_fields.scss +++ b/src/DonationForms/resources/styles/elements/_fields.scss @@ -1,7 +1,13 @@ -.givewp-fields-checkbox { +.givewp-fields-checkbox { &__description { margin-left: calc(1.25em + 0.375em); } + + &-anonymous { + .givewp-fields__description{ + margin-left: 1.78rem; + } + } } .givewp-fields-amount { diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss index f55e137c18..31b443a8f2 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss @@ -2,6 +2,14 @@ .components-checkbox-control__label { font-size: 1rem; font-weight: 500; + line-height: 24px; + color: var(--givewp-gray-100); + } + + .components-base-control__help { + margin-left: 2rem; + font-size: 0.875rem; + line-height: 1rem; color: var(--givewp-grey-700); } } From e641b2dee2eb0c6b018024bc45a22f3f0ddb04fe Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Tue, 9 Jul 2024 15:27:40 +0200 Subject: [PATCH 004/190] Fix: Made strings translatable in html-admin-page-system-info.php file (#7402) --- includes/admin/tools/views/html-admin-page-system-info.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/tools/views/html-admin-page-system-info.php b/includes/admin/tools/views/html-admin-page-system-info.php index cfc62adad2..a3d42a58d4 100644 --- a/includes/admin/tools/views/html-admin-page-system-info.php +++ b/includes/admin/tools/views/html-admin-page-system-info.php @@ -167,7 +167,7 @@ class="dashicons dashicons-external"> : tooltips->render_help( __( 'The status of the table prefix used in your WordPress database.', 'give' ) ); ?> - prefix ) > 16 ? esc_html( 'Error: Too long', 'give' ) : esc_html( 'Acceptable', 'give' ); ?> + prefix ) > 16 ? esc_html__( 'Error: Too long', 'give' ) : esc_html__( 'Acceptable', 'give' ); ?> : From aaac24a970687f9a19c110a4eb0555192483ff61 Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Tue, 9 Jul 2024 09:58:57 -0700 Subject: [PATCH 005/190] Fix: improve block insertion point indicator's position when inserting from the block menu (#7414) --- .../js/form-builder/src/blocks/section/styles.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/section/styles.scss b/src/FormBuilder/resources/js/form-builder/src/blocks/section/styles.scss index baf5481c1e..4c188a9a8d 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/section/styles.scss +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/section/styles.scss @@ -15,3 +15,9 @@ } } } + +.block-editor-block-popover__inbetween-container{ + position: relative; + top: .5rem; + height: 100%; +} From c34d3a8576daa0441617fecc5a75d9d61e701915 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 15:25:34 -0300 Subject: [PATCH 006/190] Refactor: Remove radio button for single active gateway (#7429) --- .../resources/styles/components/_gateways.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/DonationForms/resources/styles/components/_gateways.scss b/src/DonationForms/resources/styles/components/_gateways.scss index 1effc94628..48128995ce 100644 --- a/src/DonationForms/resources/styles/components/_gateways.scss +++ b/src/DonationForms/resources/styles/components/_gateways.scss @@ -18,6 +18,12 @@ list-style: none; margin: 0; + &:first-child:last-child { + label > input { + display: none; + } + } + label { display: flex; flex-direction: row; From 04ef772db6663a6462e8af71b63ea8be289934d5 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 15:26:04 -0300 Subject: [PATCH 007/190] Refactor: Change to use the selected default period in notice (#7434) --- .../js/form-builder/src/blocks/fields/amount/Edit.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/Edit.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/Edit.tsx index aa9a10fc5b..83cb6e970f 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/Edit.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/amount/Edit.tsx @@ -99,6 +99,7 @@ const Edit = ({attributes, setAttributes}) => { const displayFixedMessage = isFixedAmount && !customAmount; const displayFixedRecurringMessage = isRecurring && + recurringOptInDefaultBillingPeriod !== 'one-time' && (displayFixedMessage || isRecurringAdmin || Number(recurringLengthOfTime) > 0 || @@ -134,7 +135,7 @@ const Edit = ({attributes, setAttributes}) => { From 28238c154e804daa8c3d8f936b0bcdc16ffe1270 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 15:26:23 -0300 Subject: [PATCH 008/190] Refactor: Re-style Login block (#7436) --- .../templates/groups/Authentication.tsx | 47 ++++------ .../registrars/templates/styles.module.scss | 63 +++++++++++++ .../src/blocks/fields/login/index.tsx | 90 +++++++++++++++---- src/Framework/FieldsAPI/Authentication.php | 4 +- 4 files changed, 156 insertions(+), 48 deletions(-) diff --git a/src/DonationForms/resources/registrars/templates/groups/Authentication.tsx b/src/DonationForms/resources/registrars/templates/groups/Authentication.tsx index 8bc31c0981..d142e93c7f 100644 --- a/src/DonationForms/resources/registrars/templates/groups/Authentication.tsx +++ b/src/DonationForms/resources/registrars/templates/groups/Authentication.tsx @@ -6,6 +6,7 @@ import getWindowData from '@givewp/forms/app/utilities/getWindowData'; import postData from '@givewp/forms/app/utilities/postData'; import getCurrentFormUrlData from '@givewp/forms/app/utilities/getCurrentFormUrlData'; import FieldError from '../layouts/FieldError'; +import styles from '../styles.module.scss'; const {originUrl, isEmbed, embedId} = getCurrentFormUrlData(); @@ -76,6 +77,11 @@ export default function Authentication({ const [isAuth, setIsAuth] = useState(isAuthenticated); const [showLogin, setShowLogin] = useState(required); const toggleShowLogin = () => setShowLogin(!showLogin); + const redirectToLoginPage = (e) => { + e.preventDefault(); + const loginUrl = getRedirectUrl(new URL(loginRedirectUrl)); + window.top.location.assign(loginUrl); + }; return ( <> @@ -91,18 +97,9 @@ export default function Authentication({ )} {!isAuth && !showLogin && ( -
+
{loginRedirect ? ( - + {loginNotice} ) : ( {loginNotice} )} @@ -147,24 +144,17 @@ const LoginForm = ({children, success, lostPasswordUrl, nodeName}) => { }; return ( -
-
{children}
+ @@ -181,11 +171,8 @@ const LoginForm = ({children, success, lostPasswordUrl, nodeName}) => { const LoginNotice = ({children, onClick}) => { return ( - - ) -} + ); +}; diff --git a/src/DonationForms/resources/registrars/templates/styles.module.scss b/src/DonationForms/resources/registrars/templates/styles.module.scss index 2d0b14c1d9..2178f8c0a8 100644 --- a/src/DonationForms/resources/registrars/templates/styles.module.scss +++ b/src/DonationForms/resources/registrars/templates/styles.module.scss @@ -91,3 +91,66 @@ } } } + +.authentication { + &__login-notice { + background-color: transparent; + border: 0; + color: var(--primary); + font-size: 14px; + font-weight: 500; + margin: 0; + padding: 0; + width: auto; + } + + &__login-form { + display: flex; + flex-direction: column; + gap: 15px; + + &__fields-wrapper { + display: flex; + flex-direction: row; + gap: 15px; + } + + &__buttons-wrapper { + display: flex; + flex-direction: row-reverse; + gap: 15px; + justify-content: space-between; + align-items: baseline; + } + + &__login-button { + border-radius: 5px; + color: var(--givewp-grey-5); + font-size: 1rem; + font-weight: 600; + line-height: 1.5; + margin: 0; + min-width: 154px; + padding: var(--givewp-spacing-2) var(--givewp-spacing-6); + width: auto; + + @supports (background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black)) { + &:hover { + background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black); + } + } + } + + &__reset-button { + color: var(--givewp-grey-500); + cursor: pointer; + font-size: 14px; + line-height: 1.43; + + span { + color: var(--primary); + font-weight: 500; + } + } + } +} diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/login/index.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/login/index.tsx index 508dbb629a..595266d218 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/login/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/login/index.tsx @@ -13,7 +13,7 @@ const login: FieldBlock = { ...defaultSettings, icon: BlockIcon, title: __('User Login', 'give'), - description: __('...', 'give'), + description: __('Provides the donor the option to log in to complete donation', 'give'), supports: { multiple: false, }, @@ -44,39 +44,81 @@ const login: FieldBlock = {
null} value={''} - placeholder={__('Username or Email Address', 'give')} + placeholder={__('Enter your username or email', 'give')} /> null} - value={'password123'} + value={''} + placeholder={__('Enter your password', 'give')} />
- - + +
)} {!required && ( -
+
@@ -90,8 +132,25 @@ const login: FieldBlock = { label={__('Require donor login', 'give')} checked={required} onChange={() => setAttributes({required: !required})} + help={__( + 'Enable this option if you want to require the donor login by default.', + 'give' + )} /> + {!required && ( + + setAttributes({loginNotice})} + help={__( + 'Add your own to customize or leave blank to use the default text placeholder.', + 'give' + )} + /> + + )} setAttributes({loginRedirect})} /> - - setAttributes({loginNotice})} - /> - setAttributes({loginConfirmation})} + help={__( + 'Add your own to customize or leave blank to use the default text placeholder.', + 'give' + )} /> diff --git a/src/Framework/FieldsAPI/Authentication.php b/src/Framework/FieldsAPI/Authentication.php index a9eee75144..438f0913dc 100644 --- a/src/Framework/FieldsAPI/Authentication.php +++ b/src/Framework/FieldsAPI/Authentication.php @@ -21,11 +21,13 @@ public static function make($name) return parent::make($name) ->append( Text::make('login') - ->label(__('Username or Email Address', 'give')) + ->label(__('Username', 'give')) + ->placeholder(__('Enter your username or email', 'give')) ) ->append( Password::make('password') ->label(__('Password', 'give')) + ->placeholder(__('Enter your password', 'give')) ); } From 95fcc1bb9ba6aef228f624c3ee1e6ef0266d7fce Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 15:26:37 -0300 Subject: [PATCH 009/190] Refactor: Improve Donor Title Prefix field styles (#7431) --- .../ClassicFormDesign/css/_inputs.scss | 6 +- .../src/blocks/fields/donor-name/Edit.tsx | 112 +++++++++--------- .../src/styles/_block-editor.scss | 2 +- 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss b/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss index ca4856449c..331d81c1d8 100644 --- a/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss +++ b/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss @@ -7,7 +7,7 @@ } .givewp-fields-select-honorific { - flex-basis: 25%; + flex-basis: 35%; } .givewp-fields-hidden { @@ -23,13 +23,13 @@ select:not(.givewp-fields-amount__currency-select) { font-family: inherit; font-weight: 500; line-height: 1.2; - padding: 0.9rem; + padding: 0.9rem var(--givewp-spacing-8) 0.9rem 1.1875rem; margin-bottom: 0.5rem; appearance: none; background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='13' height='8' viewBox='0 0 13 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.66016 7.19531C5.90625 7.44141 6.31641 7.44141 6.5625 7.19531L11.8945 1.89062C12.1406 1.61719 12.1406 1.20703 11.8945 0.960938L11.2656 0.332031C11.0195 0.0859375 10.6094 0.0859375 10.3359 0.332031L6.125 4.54297L1.88672 0.332031C1.61328 0.0859375 1.20312 0.0859375 0.957031 0.332031L0.328125 0.960938C0.0820312 1.20703 0.0820312 1.61719 0.328125 1.89062L5.66016 7.19531Z' fill='%23A2A3A2'/%3E%3C/svg%3E"), linear-gradient(to bottom, #fff 0%, #fff 100%); background-repeat: no-repeat, repeat; - background-position: right 0.5em top 50%, 0 0; + background-position: right var(--givewp-spacing-4) top 50%, 0 0; background-size: 0.65em auto, 100%; &[aria-invalid="true"], 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 a06150192e..159f907397 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 @@ -79,6 +79,7 @@ export default function Edit({ options={honorificOptions} value={selectedTitle} onChange={setSelectedTitle} + style={{padding: '16px 38px 16px 16px'}} /> )} -
-
- {/* Wrapper added to control spacing between control and help text. */} - 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.", - 'give' - )} - /> - {!!showHonorific && ( - <> - setAttributes({useGlobalSettings: !useGlobalSettings})} - value={useGlobalSettings} - options={[ - {label: __('Global', 'give'), value: 'true'}, - {label: __('Customize', 'give'), value: 'false'}, - ]} - /> - {useGlobalSettings && ( -

- {__(' Go to the settings to change the ')} - - {__('Global Title Prefixes options.')} - -

- )} - + 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.", + 'give' + )} + /> + + {!!showHonorific && ( + +
+
+ setAttributes({useGlobalSettings: !useGlobalSettings})} + value={useGlobalSettings} + options={[ + {label: __('Global', 'give'), value: 'true'}, + {label: __('Customize', 'give'), value: 'false'}, + ]} + /> +
+ {useGlobalSettings && ( +

+ {__(' Go to the settings to change the ')} + + {__('Global Title Prefixes options.')} + +

)}
+
+ )} - {!!showHonorific && !useGlobalSettings && ( - - )} + {!!showHonorific && !useGlobalSettings && ( +
+
- + )} diff --git a/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss b/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss index 981186e52f..e9a08c1da2 100644 --- a/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss +++ b/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss @@ -74,7 +74,7 @@ } .components-select-control__input { - height: 52px !important; + height: auto !important; & ~ .components-input-control__suffix { & > div { From d6b77c0b8f673b0ad489151a6f24ae1ef05e7c41 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 15:26:51 -0300 Subject: [PATCH 010/190] Refactor: Set line-height to Section block in the VFB (#7432) --- .../resources/js/form-builder/src/blocks/section/Edit.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/section/Edit.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/section/Edit.tsx index 6b9748d51d..8a14e253f1 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/section/Edit.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/section/Edit.tsx @@ -4,7 +4,7 @@ import {InnerBlocks, InspectorControls, RichText, store as blockEditorStore} fro import {PanelBody, PanelRow, TextareaControl, TextControl} from '@wordpress/components'; import {useSelect} from '@wordpress/data'; import {BlockEditProps} from '@wordpress/blocks'; -import {getBlockRegistrar} from "@givewp/form-builder/common/getWindowData"; +import {getBlockRegistrar} from '@givewp/form-builder/common/getWindowData'; import BaseEmptyBlockInserter from './EmptyBlockInserter'; import './styles.scss'; @@ -44,7 +44,7 @@ export default function Edit(props: BlockEditProps) { tagName="h2" value={title} onChange={(val) => setAttributes({title: val})} - style={{margin: '0', fontSize: '22px', fontWeight: 700}} + style={{margin: '0', fontSize: '22px', fontWeight: 700, lineHeight: '1.6'}} allowedFormats={[]} /> )} @@ -53,7 +53,7 @@ export default function Edit(props: BlockEditProps) { tagName="p" value={description} onChange={(val) => setAttributes({description: val})} - style={{fontSize: '16px', fontWeight: 500}} + style={{fontSize: '16px', fontWeight: 500, marginTop: '0.25rem'}} allowedFormats={[]} /> )} From d02660e19412376eed8b3886a1b3152c890d7209 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 15:27:06 -0300 Subject: [PATCH 011/190] Refactor: Improve form footer style (#7430) --- .../ClassicFormDesign/css/_inputs.scss | 11 +++----- .../MultiStepForm/components/StepsWrapper.tsx | 6 ++--- .../styles/elements/_multi-step-form.scss | 27 ++++++++++++------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss b/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss index 331d81c1d8..55d86533c1 100644 --- a/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss +++ b/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss @@ -86,18 +86,15 @@ button[type="submit"] { font-size: 1.375rem; font-weight: 600; line-height: 1.2; - transition-property: filter, box-shadow, transform; - transition-duration: 0.3s; - transition-timing-function: ease-in-out; cursor: pointer; inline-size: 100%; width: 100%; margin: 1rem 0; - &:hover { - filter: brightness(1.2); - transform: scale(1.01); - box-shadow: 0 0.0625rem 0.25rem rgb(0 0 0 / 25%); + @supports (background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black)) { + &:hover { + background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black); + } } } diff --git a/src/DonationForms/resources/app/form/MultiStepForm/components/StepsWrapper.tsx b/src/DonationForms/resources/app/form/MultiStepForm/components/StepsWrapper.tsx index e63b40cd7d..c155fbdb54 100644 --- a/src/DonationForms/resources/app/form/MultiStepForm/components/StepsWrapper.tsx +++ b/src/DonationForms/resources/app/form/MultiStepForm/components/StepsWrapper.tsx @@ -37,9 +37,9 @@ export default function StepsWrapper({steps, children}: {steps: StepObject[]; ch
- - {__('Secure Donation', 'give')} - + + {__('100% Secure Donation', 'give')} +
diff --git a/src/DonationForms/resources/styles/elements/_multi-step-form.scss b/src/DonationForms/resources/styles/elements/_multi-step-form.scss index 76188c58d6..bbca1eb591 100644 --- a/src/DonationForms/resources/styles/elements/_multi-step-form.scss +++ b/src/DonationForms/resources/styles/elements/_multi-step-form.scss @@ -83,8 +83,9 @@ &-footer-secure { display: flex; - column-gap: 0.5rem; + column-gap: 1rem; width: 100%; + margin-top: var(--givewp-spacing-4); padding: var(--givewp-spacing-4); font-size: var(--givewp-font-size-paragraph-md); color: var(--givewp-grey-900); @@ -92,12 +93,17 @@ justify-content: center; } - &-footer-secure-icon, - &-footer-secure-text { + &-footer-secure-icon { color: var(--givewp-secondary-color); + translate: 0 -1px; + } + + &-footer-secure-text { + font-weight: 500; } } + button[type="submit"], &__steps-button-next { display: flex; justify-content: center; @@ -106,7 +112,16 @@ color: var(--givewp-grey-5); line-height: 1.5; font-weight: 600; + margin: 0; + + @supports (background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black)) { + &:hover { + background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black); + } + } + } + &__steps-button-next { &:after { content: ""; display: inline-block; @@ -117,12 +132,6 @@ background-position: center; background-repeat: no-repeat; } - - @supports (background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black)) { - &:hover { - background-color: color-mix(in lab, var(--givewp-primary-color) 90%, black); - } - } } &__step { From c86d20a0f5e1912190fa4aebb1005b1d989d56ea Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Tue, 9 Jul 2024 16:41:07 -0300 Subject: [PATCH 012/190] Refactor: Prevent File Upload description from triggering file selection modal (#7433) --- .../registrars/templates/fields/File.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/DonationForms/resources/registrars/templates/fields/File.tsx b/src/DonationForms/resources/registrars/templates/fields/File.tsx index 5511bd06da..aedbf0c078 100644 --- a/src/DonationForms/resources/registrars/templates/fields/File.tsx +++ b/src/DonationForms/resources/registrars/templates/fields/File.tsx @@ -1,23 +1,19 @@ import {FileProps} from '@givewp/forms/propTypes'; -export default function File({ - Label, - allowedMimeTypes, - ErrorMessage, - fieldError, - description, - inputProps, -}: FileProps) { +export default function File({Label, allowedMimeTypes, ErrorMessage, fieldError, description, inputProps}: FileProps) { const FieldDescription = window.givewp.form.templates.layouts.fieldDescription; const {setValue} = window.givewp.form.hooks.useFormContext(); const {name} = inputProps; return ( -
- - + +
} diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/email/template-options/settings.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/email/template-options/settings.tsx index 8926d9ef7c..4a77720dc1 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/email/template-options/settings.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/email/template-options/settings.tsx @@ -62,7 +62,7 @@ const EmailTemplateSettings = ({notification, templateTagsRef, settings, setSett updateEmailTemplateOption('email_subject', value)} value={option.email_subject || config.defaultValues.email_subject} /> @@ -88,8 +88,8 @@ const EmailTemplateSettings = ({notification, templateTagsRef, settings, setSett updateEmailTemplateOption('email_header', value)} // @ts-ignore value={option.email_header || config.defaultValues.email_header} @@ -100,12 +100,12 @@ const EmailTemplateSettings = ({notification, templateTagsRef, settings, setSett updateEmailTemplateOption('email_content_type', value)} - label={__('Email content type', 'givewp')} - help={__('Choose email type', 'givewp')} + label={__('Email content type', 'give')} + help={__('Choose email type', 'give')} value={option.email_content_type || config.defaultValues.email_content_type} options={[ - {label: __('HTML', 'givewp'), value: 'text/html'}, - {label: __('Plain', 'givewp'), value: 'text/plain'}, + {label: __('HTML', 'give'), value: 'text/html'}, + {label: __('Plain', 'give'), value: 'text/plain'}, ]} /> @@ -141,7 +141,7 @@ const EmailTemplateSettings = ({notification, templateTagsRef, settings, setSett > {config.supportsRecipients && (
- + {recipients.map((recipientEmail: string, index) => { return (
  • updateEmailTemplateOption('recipient', [...recipients, ''])} > - {__('Add email', 'givewp')} + {__('Add email', 'give')}
  • @@ -184,10 +184,10 @@ const EmailTemplateSettings = ({notification, templateTagsRef, settings, setSett {!config.supportsRecipients && ( null} value="{donor_email}" diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/general/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/general/index.tsx index 302a90fb8c..09da75c226 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/general/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/group-email-settings/general/index.tsx @@ -36,11 +36,11 @@ export default function EmailGeneralSettings({ settings, setSettings }) { > setSettings({ emailTemplate })} @@ -48,10 +48,10 @@ export default function EmailGeneralSettings({ settings, setSettings }) { setSettings({ emailFromName })} @@ -59,10 +59,10 @@ export default function EmailGeneralSettings({ settings, setSettings }) { setSettings({ emailFromEmail })} diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/group-general/form-summary/index.jsx b/src/FormBuilder/resources/js/form-builder/src/settings/group-general/form-summary/index.jsx index 6a5bd56f85..fbe3ac4f7f 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/group-general/form-summary/index.jsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/group-general/form-summary/index.jsx @@ -28,7 +28,7 @@ const FormSummarySettings = ({settings, setSettings}) => { <> { !isPublished && setSettings({pageSlug: cleanForSlug(formTitle)}); @@ -59,7 +59,7 @@ const FormSummarySettings = ({settings, setSettings}) => { {isExcerptEnabled && ( '0', - 'text' => 'Any', + 'text' => __('Any', 'give'), ] ], $options); } diff --git a/src/Subscriptions/resources/components/SubscriptionsListTable.tsx b/src/Subscriptions/resources/components/SubscriptionsListTable.tsx index 26fe0ef876..90d1ffc8bd 100644 --- a/src/Subscriptions/resources/components/SubscriptionsListTable.tsx +++ b/src/Subscriptions/resources/components/SubscriptionsListTable.tsx @@ -141,7 +141,7 @@ export default function SubscriptionsListTable() { listTableBlankSlate={ListTableBlankSlate} > ); From cfca4cc10129eca3f80d91836032a13a04ad72f8 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Mon, 26 Aug 2024 16:04:08 -0400 Subject: [PATCH 094/190] chore: merge develop and update readme --- readme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.txt b/readme.txt index a45cf828f3..d1ee1078e9 100644 --- a/readme.txt +++ b/readme.txt @@ -267,6 +267,7 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri * New: Added a setting to the visual form builder to enable redirecting to an individual donation confirmation page * Enhancement: Multi-step form designs now scroll to the top of the form on step change * Enhancement: Added individual form migration links to the donation form list table +* Enhancement: Updated various strings throughout GiveWP to be translatable (Open-source contribution by @DAnn2012) * Security: Resolved security issues related to file paths and permissions (CVE-2024-6551) * Security: Added protection against invalid donations on Legacy forms when using Stripe credit card gateway * Security: Resolved security issue related to the PayPal disconnect button From a5ef4c99b132dc61ad49c849216ca9d3ed43ba93 Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Mon, 26 Aug 2024 17:52:28 -0300 Subject: [PATCH 095/190] Fix: sites with a lot of forms can cause slow queries on pages with form list views (#7483) --- includes/admin/forms/dashboard-columns.php | 13 +- includes/forms/functions.php | 21 +- includes/misc-functions.php | 24 +- .../Actions/GetAsyncFormDataForListView.php | 99 +++++ .../Actions/GiveGoalProgressStats.php | 46 +++ .../AsyncData/Actions/LoadAsyncDataAssets.php | 65 ++++ .../AdminFormListView/AdminFormListView.php | 69 ++++ .../AdminFormListViewOptions.php | 65 ++++ .../AsyncData/AsyncDataHelpers.php | 36 ++ .../AsyncData/FormGrid/FormGridView.php | 55 +++ .../FormGrid/FormGridViewOptions.php | 53 +++ .../AsyncData/resources/loadAsyncData.js | 350 ++++++++++++++++++ .../AsyncData/resources/loadAsyncData.scss | 15 + src/DonationForms/ServiceProvider.php | 126 ++++++- .../ListTable/Columns/DonationCountColumn.php | 10 +- .../Columns/DonationRevenueColumn.php | 5 +- .../V2/ListTable/Columns/GoalColumn.php | 11 +- src/Framework/ListTable/ListTable.php | 11 +- src/MultiFormGoals/ProgressBar/Model.php | 12 +- .../Components/ListTable/ListTable/index.tsx | 2 +- templates/shortcode-form-grid.php | 24 +- webpack.mix.js | 17 +- 22 files changed, 1062 insertions(+), 67 deletions(-) create mode 100644 src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php create mode 100644 src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php create mode 100644 src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php create mode 100644 src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php create mode 100644 src/DonationForms/AsyncData/AdminFormListView/AdminFormListViewOptions.php create mode 100644 src/DonationForms/AsyncData/AsyncDataHelpers.php create mode 100644 src/DonationForms/AsyncData/FormGrid/FormGridView.php create mode 100644 src/DonationForms/AsyncData/FormGrid/FormGridViewOptions.php create mode 100644 src/DonationForms/AsyncData/resources/loadAsyncData.js create mode 100644 src/DonationForms/AsyncData/resources/loadAsyncData.scss diff --git a/includes/admin/forms/dashboard-columns.php b/includes/admin/forms/dashboard-columns.php index 4d61fe5a3d..e30c968d52 100644 --- a/includes/admin/forms/dashboard-columns.php +++ b/includes/admin/forms/dashboard-columns.php @@ -59,6 +59,7 @@ function give_form_columns( $give_form_columns ) { /** * Render Give Form Columns * + * @unreleased Add new filters for the "donations count" and "revenue" columns * @since 1.0 * * @param string $column_name Column name @@ -86,6 +87,7 @@ function give_render_form_columns( $column_name, $post_id ) { break; case 'goal': if ( give_is_setting_enabled( give_get_meta( $post_id, '_give_goal_option', true ) ) ) { + do_action('give_admin_form_list_view_donations_goal_column_before', $post_id); echo give_admin_form_goal_stats( $post_id ); @@ -105,7 +107,7 @@ function give_render_form_columns( $column_name, $post_id ) { printf( '
    %2$s', esc_url( admin_url( 'edit.php?post_type=give_forms&page=give-payment-history&form_id=' . $post_id ) ), - give_get_form_sales_stats( $post_id ) + apply_filters('give_admin_form_list_view_donations_count_column_value', give_get_form_sales_stats( $post_id ), $post_id) ); } else { echo '-'; @@ -116,7 +118,7 @@ function give_render_form_columns( $column_name, $post_id ) { printf( '%2$s', esc_url( admin_url( 'edit.php?post_type=give_forms&page=give-reports&tab=forms&form-id=' . $post_id ) ), - give_currency_filter( give_format_amount( give_get_form_earnings_stats( $post_id ), [ 'sanitize' => false ] ) ) + apply_filters('give_admin_form_list_view_revenue_column_value', give_currency_filter( give_format_amount( give_get_form_earnings_stats( $post_id ), [ 'sanitize' => false ] ) ), $post_id) ); } else { echo '-'; @@ -168,7 +170,8 @@ function give_sortable_form_columns( $columns ) { /** * Sorts Columns in the Forms List Table * - * @since 3.14.0 Use the 'give_donate_form_get_sales" filter to ensure the correct donation count will be used + * @unreleased Remove "give_donate_form_get_sales" filter logic + * @since 3.14.0 Use the "give_donate_form_get_sales" filter to ensure the correct donation count will be used * @since 1.0 * * @param array $vars Array of all the sort variables. @@ -181,10 +184,6 @@ function give_sort_forms( $vars ) { return $vars; } - add_filter('give_donate_form_get_sales', function ($sales, $donationFormId) { - return (new Give\MultiFormGoals\ProgressBar\Model(['ids' => [$donationFormId]]))->getDonationCount(); - }, 10, 2); - switch ( $vars['orderby'] ) { // Check if 'orderby' is set to "sales". case 'sales': diff --git a/includes/forms/functions.php b/includes/forms/functions.php index 619c4c0e6a..8bc897dc59 100644 --- a/includes/forms/functions.php +++ b/includes/forms/functions.php @@ -14,7 +14,6 @@ exit; } -use Give\DonationForms\DonationQuery; use Give\Helpers\Form\Utils as FormUtils; /** @@ -1249,7 +1248,8 @@ function give_set_form_closed_status( $form_id ) { /** * Show Form Goal Stats in Admin ( Listing and Detail page ) * - * @since 3.14.0 Use the 'give_get_form_earnings_stats" filter to ensure the correct value will be displayed in the form progress bar + * @unreleased Remove "give_donate_form_get_sales" filter logic + * @since 3.14.0 Use the "give_get_form_earnings_stats" filter to ensure the correct value will be displayed in the form progress bar * @since 2.19.0 Prevent divide by zero issue in goal percentage calculation logic. * * @since 2.1.0 @@ -1259,13 +1259,11 @@ function give_set_form_closed_status( $form_id ) { * @return string */ function give_admin_form_goal_stats( $form_id ) { - add_filter('give_get_form_earnings_stats', function ($earnings, $donationFormId) { - return (new DonationQuery())->form($donationFormId)->sumAmount(); - }, 10, 2); - $html = ''; $goal_stats = give_goal_progress_stats( $form_id ); - $percent_complete = $goal_stats['raw_goal'] ? round( ( $goal_stats['raw_actual'] / $goal_stats['raw_goal'] ), 3 ) * 100 : 0; + $percent_complete = $goal_stats['raw_goal'] && is_numeric($goal_stats['raw_actual']) && is_numeric($goal_stats['raw_goal']) + ? round(($goal_stats['raw_actual'] / $goal_stats['raw_goal']), 3) * 100 + : 0; $html .= sprintf( '
    @@ -1285,9 +1283,12 @@ function give_admin_form_goal_stats( $form_id ) { ( 'donors' === $goal_stats['format'] ? __( 'donors', 'give' ) : ( 'donation' === $goal_stats['format'] ? __( 'donations', 'give' ) : '' ) ) ); - if ( $goal_stats['raw_actual'] >= $goal_stats['raw_goal'] ) { - $html .= sprintf( ' %s', __( 'Goal achieved', 'give' ) ); - } + $opacity = $goal_stats['raw_actual'] >= $goal_stats['raw_goal'] ? 1 : 0; + $html .= sprintf( + ' %s', + apply_filters('give_admin_goal_progress_achieved_opacity', $opacity), + __('Goal achieved', 'give') + ); $html .= '
    '; diff --git a/includes/misc-functions.php b/includes/misc-functions.php index 9db62ba714..19d1d8f46a 100644 --- a/includes/misc-functions.php +++ b/includes/misc-functions.php @@ -9,10 +9,10 @@ * @since 1.0 */ -// Exit if accessed directly. -use Give\DonationForms\DonationQuery; +use Give\DonationForms\AsyncData\AsyncDataHelpers; use Give\License\PremiumAddonsListManager; +// Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -1928,10 +1928,12 @@ function give_get_nonce_field( $action, $name, $referer = false ) { /** * Display/Return a formatted goal for a donation form * + * @unreleased Add form_id to the array return + * @since 2.1 + * * @param int|Give_Donate_Form $form Form ID or Form Object. * * @return array - * @since 2.1 */ function give_goal_progress_stats( $form ) { @@ -1943,7 +1945,6 @@ function give_goal_progress_stats( $form ) { /** * Filter the form. - * @since 3.14.0 Replace "$form->earnings" with (new DonationQuery())->form($form->ID)->sumIntendedAmount() * @since 1.8.8 */ $total_goal = apply_filters( 'give_goal_amount_target_output', round( give_maybe_sanitize_amount( $form->goal ), 2 ), $form->ID, $form ); @@ -1971,12 +1972,14 @@ function give_goal_progress_stats( $form ) { $actual = apply_filters( 'give_goal_donors_target_output', give_get_form_donor_count( $form->ID ), $form->ID, $form ); break; default: - /** - * Filter the form income. - * - * @since 1.8.8 - */ - $actual = apply_filters( 'give_goal_amount_raised_output', (new DonationQuery())->form($form->ID)->sumIntendedAmount(), $form->ID, $form ); + /** + * Filter the form income. + * + * @unreleased Revert changes implemented on the 3.14.0 version + * @since 3.14.0 Replace "$form->earnings" with (new DonationQuery())->form($form->ID)->sumIntendedAmount() + * @since 1.8.8 + */ + $actual = apply_filters( 'give_goal_amount_raised_output', $form->earnings, $form->ID, $form ); break; } @@ -2018,6 +2021,7 @@ function give_goal_progress_stats( $form ) { 'actual' => $actual, 'goal' => $total_goal, 'format' => $goal_format, + 'form_id' => $form->ID ], $stats_array ); diff --git a/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php b/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php new file mode 100644 index 0000000000..760967b649 --- /dev/null +++ b/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php @@ -0,0 +1,99 @@ + __('The current user does not have permission to execute this operation.', 'give'), + ]); + } + + if ( ! isset($options['formId'])) { + wp_send_json_error(['errorMsg' => __('Missing Form ID.', 'give')]); + } + + $formId = absint($options['formId']); + if ('give_forms' !== get_post_type($formId)) { + wp_send_json_error(['errorMsg' => __('Invalid post type.', 'give')]); + } + + $transientName = 'give_async_data_for_list_view_form_' . $formId; + + $data = get_transient($transientName); + + if ($data) { + wp_send_json_success($data); + } + + $amountRaised = 0; + $percentComplete = 0; + if ($this->isAsyncProgressBar()) { + $goalStats = give_goal_progress_stats($formId); + $amountRaised = $goalStats['actual']; + $percentComplete = ('percentage' === $goalStats['format']) ? str_replace('%', '', + $goalStats['actual']) : max(min($goalStats['progress'], 100), 0); + } + + $donationsCount = 0; + if ($this->isAsyncDonationCount()) { + $donationsCount = AsyncDataHelpers::getFormDonationsCountValue($formId); + } + + $revenue = $amountRaised; + if (0 === $revenue && $this->isAsyncRevenue()) { + $revenue = AsyncDataHelpers::getFormRevenueValue($formId); + $revenue = give_currency_filter(give_format_amount($revenue)); + } + + $response = [ + 'amountRaised' => $amountRaised, + 'percentComplete' => $percentComplete, + 'donationsCount' => $donationsCount, + 'revenue' => $revenue, + ]; + + set_transient($transientName, $response, MINUTE_IN_SECONDS * 5); + + wp_send_json_success($response); + } + + /** + * @unreleased + */ + private function isAsyncProgressBar(): bool + { + return AdminFormListViewOptions::isGoalColumnAsync() || FormGridViewOptions::isProgressBarAmountRaisedAsync(); + } + + /** + * @unreleased + */ + private function isAsyncDonationCount(): bool + { + return AdminFormListViewOptions::isDonationColumnAsync() || FormGridViewOptions::isProgressBarDonationsCountAsync(); + } + + /** + * @unreleased + */ + private function isAsyncRevenue(): bool + { + return AdminFormListViewOptions::isRevenueColumnAsync(); + } +} diff --git a/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php b/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php new file mode 100644 index 0000000000..1a50c275ba --- /dev/null +++ b/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php @@ -0,0 +1,46 @@ +isSingleForm() && ! wp_doing_ajax() && AdminFormListViewOptions::useCachedMetaKeys()) { + return $stats; + } + + if ('amount' === $stats['format']) { + $actual = AsyncDataHelpers::getFormRevenueValue($stats['form_id']); + $stats['actual'] = give_currency_filter(give_format_amount($actual)); + } + + return $stats; + } + + /** + * @unreleased + */ + private function isSingleForm(): bool + { + $isIframeFormPage = isset($_GET['giveDonationFormInIframe']) && '1' === $_GET['giveDonationFormInIframe']; + $isSingleFormPage = 'give_forms' === get_post_type() && is_single(); + $isFormEditPage = 'give_forms' === get_post_type() && isset($_GET['action']) && 'edit' === $_GET['action']; + + return $isIframeFormPage || $isSingleFormPage || $isFormEditPage; + } +} diff --git a/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php b/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php new file mode 100644 index 0000000000..be7d4ecd1a --- /dev/null +++ b/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php @@ -0,0 +1,65 @@ + admin_url('admin-ajax.php'), + 'ajaxNonce' => wp_create_nonce('GiveDonationFormsAsyncDataAjaxNonce'), + 'scriptDebug' => defined('SCRIPT_DEBUG') && SCRIPT_DEBUG, + 'throttlingEnabled' => ! defined('GIVE_ASYNC_DATA_THROTTLING') || GIVE_ASYNC_DATA_THROTTLING, + ] + ); + } + + /** + * @unreleased + */ + public static function enqueueAssets() + { + wp_enqueue_style(LoadAsyncDataAssets::handleName()); + wp_enqueue_script(LoadAsyncDataAssets::handleName()); + } +} diff --git a/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php new file mode 100644 index 0000000000..9a4fee1328 --- /dev/null +++ b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php @@ -0,0 +1,69 @@ + [$formId], 'statusList' => ['any']]))->getDonationCount(); + } + + /** + * @unreleased + */ + public static function getFormRevenueValue($formId): int + { + return (new DonationQuery())->form($formId)->sumIntendedAmount(); + } + + /** + * @unreleased + */ + public static function getSkeletonPlaceholder($width = '100%', $height = '0.7rem'): string + { + return ''; + } +} diff --git a/src/DonationForms/AsyncData/FormGrid/FormGridView.php b/src/DonationForms/AsyncData/FormGrid/FormGridView.php new file mode 100644 index 0000000000..e4c4382135 --- /dev/null +++ b/src/DonationForms/AsyncData/FormGrid/FormGridView.php @@ -0,0 +1,55 @@ + { + /** + * We are declaring it at the top to use it in more than one function. + */ + let throttleTimer = false; + let abortLoadAsyncData = false; + const giveListTable = document.querySelector('.giveListTable'); + const giveListTableIsLoadingEvent = new Event('giveListTableIsLoading'); + + /** + * This function check if the element is visible on the screen. + * + * @unreleased + */ + function isInViewport(element) { + const {top, bottom} = element.getBoundingClientRect(); + const vHeight = window.innerHeight || document.documentElement.clientHeight; + + return (top > 0 || bottom > 0) && top < vHeight; + } + + /** + * Check if an element is a placeholder waiting to have the value updated. + * + * @unreleased + */ + function isPlaceholder(element) { + return !!element && Boolean(element.querySelector('.js-give-async-data')); + } + + /** + * This function fetch the async data from the server and set the values to the proper elements in the DOM. + * + * @unreleased + */ + const loadFormData = ( + formId, + itemElement, + amountRaisedElement = null, + progressBarElement = null, + goalAchievedElement = null, + donationsElement = null, + earningsElement = null + ) => { + // If we don't have any of these elements with a placeholder waiting to be updated, then return. + if ( + !isPlaceholder(amountRaisedElement) && + !isPlaceholder(donationsElement) && + !isPlaceholder(earningsElement) + ) { + return; + } + + // Limit requests to run one per time. + if (window.GiveDonationFormsAsyncData.throttlingEnabled && throttleTimer) { + window.GiveDonationFormsAsyncData.scriptDebug && console.log('throttleTimer start: ', throttleTimer); + return; + } + + throttleTimer = true; + window.GiveDonationFormsAsyncData.scriptDebug && + console.log('request start: ', new Date().toLocaleTimeString()); + + window.GiveDonationFormsAsyncData.scriptDebug && console.log('item: ', itemElement); + + // This class ensures that once the element has the fetch request triggered we'll not try to fetch it again. + itemElement.classList.add('give-async-data-fetch-triggered'); + + // It can be used to abort the async request when necessary. + const controller = new AbortController(); + const signal = controller.signal; + + fetch( + `${window.GiveDonationFormsAsyncData.ajaxUrl}?action=givewp_get_form_async_data_for_list_view&formId=${formId}&nonce=${window.GiveDonationFormsAsyncData.ajaxNonce}`, + {signal} + ) + .then(function (response) { + return response.json(); + }) + .then(function (response) { + window.GiveDonationFormsAsyncData.scriptDebug && console.log('Response: ', response); + + // Replace the placeholders with the real data returned by the server. + if (response.success) { + if (isPlaceholder(amountRaisedElement)) { + amountRaisedElement.innerHTML = response.data.amountRaised; + } + + if ( + !!progressBarElement && + progressBarElement.style.width !== response.data.percentComplete + '%' + ) { + progressBarElement.style.width = response.data.percentComplete + '%'; + } + + if (!!goalAchievedElement && response.data.percentComplete >= 100) { + goalAchievedElement.style.opacity = '1'; + } + + if (isPlaceholder(donationsElement)) { + donationsElement.innerHTML = response.data.donationsCount; + } + + if (isPlaceholder(earningsElement)) { + earningsElement.innerHTML = response.data.revenue; + } + } + }) + .catch((error) => { + // When there is an error remove the class that prevents fetch request duplication, so we can try fetching it again in the next try. + itemElement.classList.remove('give-async-data-fetch-triggered'); + window.GiveDonationFormsAsyncData.scriptDebug && console.log('Error: ', error); + }) + .finally(() => { + window.GiveDonationFormsAsyncData.scriptDebug && + console.log('request end: ', new Date().toLocaleTimeString()); + if (window.GiveDonationFormsAsyncData.throttlingEnabled && throttleTimer) { + throttleTimer = false; + window.GiveDonationFormsAsyncData.scriptDebug && console.log('throttleTimer end: ', throttleTimer); + maybeLoadAsyncData(); + } + window.GiveDonationFormsAsyncData.scriptDebug && console.log('Request finalized.'); + }); + + // Make sure to abort all unfinished async requests when leave or refresh the page. + addEventListener('beforeunload', (event) => { + abortLoadAsyncData = true; + controller.abort('Async request aborted due to exit page.'); + }); + + // Make sure to abort all unfinished async requests when changing the giveListTable pagination. + if (giveListTable) { + giveListTable.addEventListener('giveListTableIsLoading', (event) => { + abortLoadAsyncData = true; + controller.abort('Async request aborted due to table loading.'); + }); + } + }; + + /** + * Handle the async data logic for ALL form list views available. + * + * @unreleased + */ + const maybeLoadAsyncData = () => { + // If the async requests were aborted on the "beforeunload" or "giveListTableIsLoading" event, we don't want to create more async requests + if (abortLoadAsyncData) { + window.GiveDonationFormsAsyncData.scriptDebug && console.log('abortLoadAsyncData'); + return; + } + + handleAdminFormsListViewItems(); + handleAdminLegacyFormsListViewItems(); + handleFormGridItems(); + }; + + /** + * Check for changes in the "giveListTable" classes to trigger the "giveListTableIsLoadingEvent" when appropriated. + * + * @unreleased + */ + function maybeTriggerGiveListTableIsLoadingEvent() { + if (giveListTable) { + const observer = new MutationObserver(function (mutations) { + if (giveListTable.classList.contains('giveListTableIsLoading')) { + giveListTable.dispatchEvent(giveListTableIsLoadingEvent); + } + + if (giveListTable.classList.contains('giveListTableIsLoaded')) { + abortLoadAsyncData = false; + maybeLoadAsyncData(); + } + }); + + // Configuration of the observer + const config = { + attributes: true, + childList: false, + characterData: false, + }; + + // Pass in the target node, as well as the observer options + observer.observe(giveListTable, config); + } + } + + /** + * Load the async data of all forms (visible on the screen) from the NEW admin form list view - giveListTable. + * + * @unreleased + */ + function handleAdminFormsListViewItems() { + const adminFormsListViewItems = document.querySelectorAll('tr:not(.give-async-data-fetch-triggered)'); + if (adminFormsListViewItems.length > 0) { + maybeTriggerGiveListTableIsLoadingEvent(); + + adminFormsListViewItems.forEach((itemElement) => { + const select = itemElement.querySelector('.giveListTableSelect'); + + if (!select) { + return; + } + + const formId = select.getAttribute('data-id'); + const amountRaisedElement = itemElement.querySelector("[id^='giveDonationFormsProgressBar'] > span"); + const progressBarElement = itemElement.querySelector('.goalProgress > span'); + const goalAchievedElement = itemElement.querySelector('.goalProgress--achieved'); + const donationsElement = itemElement.querySelector('.column-donations-count-value'); + const earningsElement = itemElement.querySelector('.column-earnings-value'); + + if (isInViewport(itemElement)) { + loadFormData( + formId, + itemElement, + amountRaisedElement, + progressBarElement, + goalAchievedElement, + donationsElement, + earningsElement + ); + } + }); + } + } + + /** + * Load the async data of all forms (visible on the screen) from the LEGACY admin form list view. + * + * @unreleased + */ + function handleAdminLegacyFormsListViewItems() { + const adminLegacyFormsListViewItems = document.querySelectorAll( + '.type-give_forms:not(.give-async-data-fetch-triggered)' + ); + if (adminLegacyFormsListViewItems.length > 0) { + adminLegacyFormsListViewItems.forEach((itemElement) => { + if (!itemElement.hasAttribute('id') || !itemElement.id.includes('post-')) { + return; + } + + const formId = itemElement.id.split('post-')[1]; + const goalElement = itemElement.querySelector('.column-goal'); + const amountRaisedElement = goalElement.querySelector('.give-goal-text > span'); + const progressBarElement = goalElement.querySelector('.give-admin-progress-bar > span'); + const goalAchievedElement = goalElement.querySelector('.give-admin-goal-achieved'); + const donationsElement = itemElement.querySelector('.column-donations > a'); + const earningsElement = itemElement.querySelector('.column-earnings > a'); + + if (isInViewport(itemElement)) { + loadFormData( + formId, + itemElement, + amountRaisedElement, + progressBarElement, + goalAchievedElement, + donationsElement, + earningsElement + ); + } + }); + } + } + + /** + * Load the async data in all form grid items that have the progress bar enabled. + * + * @unreleased + */ + function handleFormGridItems() { + const formGridItems = document.querySelectorAll('.give-grid__item:not(.give-async-data-fetch-triggered)'); + + if (formGridItems.length > 0) { + formGridItems.forEach((itemElement) => { + const giveCard = itemElement.querySelector('.give-card'); + + if (!giveCard || !giveCard.hasAttribute('id') || !giveCard.id.includes('give-card-')) { + return; + } + + const formId = giveCard.id.split('give-card-')[1]; + const formGridRaised = itemElement.querySelector('.form-grid-raised'); + + if (!formGridRaised) { + return; + } + + const amountRaisedElement = formGridRaised + .querySelector('div:nth-child(1)') + .querySelector('span:nth-child(1)'); + const progressBarElement = itemElement.querySelector('.give-progress-bar').querySelector('span'); + const donationsElement = formGridRaised + .querySelector('div:nth-child(2)') + .querySelector('span:nth-child(1)'); + + if (isInViewport(itemElement)) { + loadFormData(formId, itemElement, amountRaisedElement, progressBarElement, null, donationsElement); + } + }); + } + } + + // Trigger the async logic at the page's first load. + maybeLoadAsyncData(); + + // Trigger the async logic every time the user scrolls the mouse. + window.addEventListener( + 'scroll', + () => { + maybeLoadAsyncData(); + }, + true + ); + + // Trigger the async logic every time the user resize the screen. + window.addEventListener( + 'resize', + () => { + maybeLoadAsyncData(); + }, + true + ); + + // Trigger the async logic every time the user add a new Form Grid Block to the WordPress Block Editor - Gutenberg. + window.onload = function () { + const wpBlockEditorContent = document.querySelector('.wp-block-post-content'); + if (!!wpBlockEditorContent) { + // create an Observer instance + const resizeObserver = new ResizeObserver((entries) => { + window.GiveDonationFormsAsyncData.scriptDebug && + console.log('WP Block Editor height changed:', entries[0].target.clientHeight); + maybeLoadAsyncData(); + }); + + // start observing a DOM node + resizeObserver.observe(wpBlockEditorContent); + } + }; +}); diff --git a/src/DonationForms/AsyncData/resources/loadAsyncData.scss b/src/DonationForms/AsyncData/resources/loadAsyncData.scss new file mode 100644 index 0000000000..8577702e2a --- /dev/null +++ b/src/DonationForms/AsyncData/resources/loadAsyncData.scss @@ -0,0 +1,15 @@ +.give-skeleton { + display: inline-block; + position: relative; + top: 0.1rem; + animation: give-skeleton-loading 1s linear infinite alternate; +} + +@keyframes give-skeleton-loading { + 0% { + background-color: hsl(200, 20%, 80%); + } + 100% { + background-color: hsl(200, 20%, 95%); + } +} diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index db437f8082..0fdff942a6 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -9,6 +9,12 @@ use Give\DonationForms\Actions\PrintFormMetaTags; use Give\DonationForms\Actions\SanitizeDonationFormPreviewRequest; use Give\DonationForms\Actions\StoreBackwardsCompatibleFormMeta; +use Give\DonationForms\AsyncData\Actions\GetAsyncFormDataForListView; +use Give\DonationForms\AsyncData\Actions\GiveGoalProgressStats; +use Give\DonationForms\AsyncData\Actions\LoadAsyncDataAssets; +use Give\DonationForms\AsyncData\AdminFormListView\AdminFormListView; +use Give\DonationForms\AsyncData\AsyncDataHelpers; +use Give\DonationForms\AsyncData\FormGrid\FormGridView; use Give\DonationForms\Blocks\DonationFormBlock\Block as DonationFormBlock; use Give\DonationForms\Controllers\DonationConfirmationReceiptViewController; use Give\DonationForms\Controllers\DonationFormViewController; @@ -27,6 +33,10 @@ use Give\DonationForms\Routes\DonateRoute; use Give\DonationForms\Routes\ValidationRoute; use Give\DonationForms\Shortcodes\GiveFormShortcode; +use Give\DonationForms\V2\ListTable\Columns\DonationCountColumn; +use Give\DonationForms\V2\ListTable\Columns\DonationRevenueColumn; +use Give\DonationForms\V2\ListTable\Columns\GoalColumn; +use Give\DonationForms\V2\Models\DonationForm; use Give\DonationForms\ValueObjects\DonationFormStatus; use Give\Framework\FormDesigns\Registrars\FormDesignRegistrar; use Give\Framework\Migrations\MigrationsRegister; @@ -35,7 +45,6 @@ use Give\Log\Log; use Give\ServiceProviders\ServiceProvider as ServiceProviderInterface; - class ServiceProvider implements ServiceProviderInterface { @@ -88,11 +97,126 @@ public function boot() * Print form meta tags */ Hooks::addAction('wp_head', PrintFormMetaTags::class); + + $this->registerAsyncData(); } /** * @since 3.15.0 */ + private function registerAsyncData() + { + // Only register assets on the frontend, but not enqueue to prevent loading them in unnecessary places + Hooks::addAction('wp_enqueue_scripts', LoadAsyncDataAssets::class, 'registerAssets'); + add_action('give_before_template_part', function ($templateName) { + if ('shortcode-form-grid' === $templateName) { + // Enqueue assets previously registered on demand - only when the shortcode gets rendered + LoadAsyncDataAssets::enqueueAssets(); + } + }); + + // Load assets on the admin form list pages + $isLegacyAdminFormListPage = isset($_GET['post_type']) && 'give_forms' === $_GET['post_type'] && ! isset($_GET['page']); + $isAdminFormListPage = isset($_GET['page']) && 'give-forms' === $_GET['page']; + if ($isLegacyAdminFormListPage || $isAdminFormListPage) { + Hooks::addAction('admin_enqueue_scripts', LoadAsyncDataAssets::class); + } + + // Load assets on the WordPress Block Editor - Gutenberg + Hooks::addAction('enqueue_block_editor_assets', LoadAsyncDataAssets::class); + + // Async ajax request + Hooks::addAction('wp_ajax_givewp_get_form_async_data_for_list_view', GetAsyncFormDataForListView::class); + Hooks::addAction('wp_ajax_nopriv_givewp_get_form_async_data_for_list_view', GetAsyncFormDataForListView::class); + + // Filter from give_goal_progress_stats() function which is used by the admin form list views and form grid view + Hooks::addFilter('give_goal_progress_stats', GiveGoalProgressStats::class, + 'maybeChangeGoalProgressStatsActualValue', 999, + 2); + + // Form Grid + add_filter('give_form_grid_goal_progress_stats_before', function () { + $usePlaceholder = give(FormGridView::class)->maybeUsePlaceholderOnGoalAmountRaised(); + + if ($usePlaceholder) { + //Enable placeholder on the give_goal_progress_stats() function + add_filter('give_goal_progress_stats', function ($stats) { + $stats['actual'] = AsyncDataHelpers::getSkeletonPlaceholder('1rem'); + + return $stats; + }); + add_filter('give_goal_shortcode_stats', function ($stats) { + $stats['income'] = 0; + + return $stats; + }); + } + }); + Hooks::addFilter('give_form_grid_progress_bar_amount_raised_value', FormGridView::class, 'maybeSetProgressBarAmountRaisedAsync',10,2); + Hooks::addFilter('give_form_grid_progress_bar_donations_count_value', FormGridView::class, 'maybeSetProgressBarDonationsCountAsync',10,2); + + // Legacy Admin Form List View Columns + Hooks::addFilter('give_admin_goal_progress_achieved_opacity', AdminFormListView::class, 'maybeChangeAchievedIconOpacity'); + add_action( + 'give_admin_form_list_view_donations_goal_column_before', + function () { + $usePlaceholder = give(AdminFormListView::class)->maybeUsePlaceholderOnGoalAmountRaised(); + + if ($usePlaceholder) { + //Enable placeholder on the give_goal_progress_stats() function + add_filter('give_goal_progress_stats', function ($stats) { + $stats['actual'] = AsyncDataHelpers::getSkeletonPlaceholder('1rem'); + + return $stats; + }); + } + }, + 10, + 2 + ); + Hooks::addFilter('give_admin_form_list_view_donations_count_column_value', AdminFormListView::class, 'maybeSetDonationsColumnAsync',10,2); + Hooks::addFilter('give_admin_form_list_view_revenue_column_value', AdminFormListView::class, 'maybeSetRevenueColumnAsync',10,2); + + // Admin Form List View Columns + Hooks::addFilter('givewp_list_table_goal_progress_achieved_opacity', AdminFormListView::class, 'maybeChangeAchievedIconOpacity'); + add_action( + sprintf("givewp_list_table_cell_value_%s_before", GoalColumn::getId()), + function () { + $usePlaceholder = give(AdminFormListView::class)->maybeUsePlaceholderOnGoalAmountRaised(); + + if ($usePlaceholder) { + //Enable placeholder on the give_goal_progress_stats() function + add_filter('give_goal_progress_stats', function ($stats) { + $stats['actual'] = AsyncDataHelpers::getSkeletonPlaceholder('1rem'); + + return $stats; + }); + } + }, + 10, + 2 + ); + add_filter( + sprintf("givewp_list_table_cell_value_%s_content", DonationCountColumn::getId()), + function ($value, DonationForm $form){ + return give(AdminFormListView::class)->maybeSetDonationsColumnAsync($value, $form->id); + }, + 10, + 2 + ); + add_filter( + sprintf("givewp_list_table_cell_value_%s_content", DonationRevenueColumn::getId()), + function ($value, DonationForm $form){ + return give(AdminFormListView::class)->maybeSetRevenueColumnAsync($value, $form->id); + }, + 10, + 2 + ); + } + + /** + * @unreleased + */ private function registerAddFormSubmenuLink() { Hooks::addAction('admin_menu', DonationFormsAdminPage::class, 'addFormSubmenuLink', 999); diff --git a/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php b/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php index 52cfed13c1..f36a3b592b 100644 --- a/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php +++ b/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php @@ -6,7 +6,6 @@ use Give\DonationForms\V2\Models\DonationForm; use Give\Framework\ListTable\ModelColumn; -use Give\MultiFormGoals\ProgressBar\Model as ProgressBarModel; /** * @since 2.24.0 @@ -39,7 +38,8 @@ public function getLabel(): string } /** - * @since 3.14.0 Use the 'getDonationCount()" method from progress bar model to ensure the correct donation count will be used + * @unreleased Add filter to change the cell value content + * @since 3.14.0 Use the "getDonationCount()" method from progress bar model to ensure the correct donation count will be used * @since 2.24.0 * * @inheritDoc @@ -48,7 +48,7 @@ public function getLabel(): string */ public function getCellValue($model): string { - $totalDonations = (new ProgressBarModel(['ids' => [$model->id]]))->getDonationCount(); + $totalDonations = $model->totalNumberOfDonations; $label = $totalDonations > 0 ? sprintf( @@ -62,10 +62,10 @@ public function getCellValue($model): string ) : __('No donations', 'give'); return sprintf( - '%s', + '%s', admin_url("edit.php?post_type=give_forms&page=give-payment-history&form_id=$model->id"), __('Visit donations page', 'give'), - $label + apply_filters("givewp_list_table_cell_value_{$this::getId()}_content", $label, $model, $this) ); } } diff --git a/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php b/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php index 54f7628c86..80b6a6aed3 100644 --- a/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php +++ b/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php @@ -36,6 +36,7 @@ public function getLabel(): string } /** + * @unreleased Add filter to change the cell value content * @since 2.24.0 * * @inheritDoc @@ -45,10 +46,10 @@ public function getLabel(): string public function getCellValue($model, $locale = ''): string { return sprintf( - '%s', + '%s', admin_url("edit.php?post_type=give_forms&page=give-reports&tab=forms&legacy=true&form-id=$model->id"), __('Visit form reports page', 'give'), - $model->totalAmountDonated->formatToLocale($locale) + apply_filters("givewp_list_table_cell_value_{$this::getId()}_content", $model->totalAmountDonated->formatToLocale($locale), $model, $this) ); } } diff --git a/src/DonationForms/V2/ListTable/Columns/GoalColumn.php b/src/DonationForms/V2/ListTable/Columns/GoalColumn.php index c9cf53dacc..fe065a950c 100644 --- a/src/DonationForms/V2/ListTable/Columns/GoalColumn.php +++ b/src/DonationForms/V2/ListTable/Columns/GoalColumn.php @@ -4,7 +4,6 @@ namespace Give\DonationForms\V2\ListTable\Columns; -use Give\DonationForms\DonationQuery; use Give\DonationForms\V2\Models\DonationForm; use Give\Framework\ListTable\ModelColumn; @@ -36,7 +35,8 @@ public function getLabel(): string } /** - * @since 3.14.0 Use the 'give_get_form_earnings_stats" filter to ensure the correct value will be displayed in the form progress bar + * @unreleased Remove "give_get_form_earnings_stats" filter logic and add filters to change the cell value content + * @since 3.14.0 Use the "give_get_form_earnings_stats" filter to ensure the correct value will be displayed in the form progress bar * @since 2.24.0 * * @inheritDoc @@ -49,10 +49,6 @@ public function getCellValue($model): string return __('No Goal Set', 'give'); } - add_filter('give_get_form_earnings_stats', function ($earnings, $donationFormId) { - return (new DonationQuery())->form($donationFormId)->sumAmount(); - }, 10, 2); - $goal = give_goal_progress_stats($model->id); $goalPercentage = ('percentage' === $goal['format']) ? str_replace('%', '', $goal['actual']) : max(min($goal['progress'], 100), 0); @@ -84,7 +80,8 @@ class="goalProgress" $goal['goal'] ), sprintf( - ($goal['progress'] >= 100 ? '%2$s%3$s' : ''), + '%3$s%4$s', + apply_filters('givewp_list_table_goal_progress_achieved_opacity', $goal['progress'] >= 100 ? 1 : 0), GIVE_PLUGIN_URL . 'assets/dist/images/list-table/star-icon.svg', __('Goal achieved icon', 'give'), __('Goal achieved!', 'give') diff --git a/src/Framework/ListTable/ListTable.php b/src/Framework/ListTable/ListTable.php index dce00aad31..6d60f080eb 100644 --- a/src/Framework/ListTable/ListTable.php +++ b/src/Framework/ListTable/ListTable.php @@ -123,7 +123,16 @@ public function getItems(): array private function safelyGetCellValue(ModelColumn $column, Model $model, string $locale) { try { - $cellValue = $column->getCellValue($model, $locale); + /** + * @unreleased + */ + do_action("givewp_list_table_cell_value_{$column::getId()}_before", $column, $model, $locale); + + /** + * @unreleased + */ + $cellValue = apply_filters("givewp_list_table_cell_value_{$column::getId()}", + $column->getCellValue($model, $locale), $column, $model, $locale); } catch (Exception $exception) { Log::error( sprintf( diff --git a/src/MultiFormGoals/ProgressBar/Model.php b/src/MultiFormGoals/ProgressBar/Model.php index 289b113374..505555e350 100644 --- a/src/MultiFormGoals/ProgressBar/Model.php +++ b/src/MultiFormGoals/ProgressBar/Model.php @@ -3,8 +3,11 @@ namespace Give\MultiFormGoals\ProgressBar; use Give\DonationForms\DonationQuery; -use Give\ValueObjects\Money; +/** + * @unreleased Add $statusList property + * @since 2.9.0 + */ class Model { @@ -20,9 +23,12 @@ class Model protected $forms = []; protected $donationRevenueResults; + protected $statusList = []; + /** * Constructs and sets up setting variables for a new Progress Bar model * + * @unreleased Add statusList support for $args * @since 2.9.0 **@param array $args Arguments for new Progress Bar, including 'ids' */ @@ -34,11 +40,13 @@ public function __construct(array $args) isset($args['goal']) ? $this->goal = $args['goal'] : $this->goal = '1000'; isset($args['enddate']) ? $this->enddate = $args['enddate'] : $this->enddate = ''; isset($args['color']) ? $this->color = $args['color'] : $this->color = '#28c77b'; + isset($args['statusList']) ? $this->statusList = $args['statusList'] : $this->statusList = ['publish']; } /** * Get forms associated with Progress Bar * + * @unreleased Use $statusList property * @since 3.0.3 Return empty array instead of false * @since 2.9.0 */ @@ -50,7 +58,7 @@ public function getForms(): array $query_args = [ 'post_type' => 'give_forms', - 'post_status' => 'publish', + 'post_status' => $this->statusList, 'post__in' => $this->ids, 'posts_per_page' => -1, 'fields' => 'ids', diff --git a/src/Views/Components/ListTable/ListTable/index.tsx b/src/Views/Components/ListTable/ListTable/index.tsx index 9f41bda93a..05792773f8 100644 --- a/src/Views/Components/ListTable/ListTable/index.tsx +++ b/src/Views/Components/ListTable/ListTable/index.tsx @@ -144,7 +144,7 @@ export const ListTable = ({
    )} - +
    diff --git a/templates/shortcode-form-grid.php b/templates/shortcode-form-grid.php index a96287055b..085a2e0126 100644 --- a/templates/shortcode-form-grid.php +++ b/templates/shortcode-form-grid.php @@ -3,11 +3,10 @@ * This template is used to display the donation grid with [donation_grid] */ -// Exit if accessed directly. -use Give\DonationForms\DonationQuery; use Give\Helpers\Form\Template; use Give\Helpers\Form\Utils as FormUtils; +// Exit if accessed directly. if (!defined('ABSPATH')) { exit; } @@ -15,6 +14,7 @@ /** * List of changes * + * @unreleased Add filters to enable the async mode and to change the values of the "amount raised" and "donations count" on the progress bar * @since 2.27.1 Use get_the_excerpt function to get short description of donation form to display in form grid. */ @@ -237,19 +237,23 @@ static function ($term) use ($tag_text_color, $tag_bg_color) { || !give_is_setting_enabled($goal_option) || 0 === $form->goal; // Maybe display the goal progress bar. - if (!$hide_goal) : + + do_action('give_form_grid_goal_progress_stats_before', $form_id); + $goal_progress_stats = give_goal_progress_stats($form); $goal_format = $goal_progress_stats['format']; $color = $atts['progress_bar_color']; $show_goal = isset($atts['show_goal']) ? filter_var($atts['show_goal'], FILTER_VALIDATE_BOOLEAN) : true; + /** + * @unreleased Revert changes implemented on the 3.14.0 version * @since 3.14.0 Replace "$form->get_earnings()" with (new DonationQuery())->form($form->ID)->sumIntendedAmount() */ $shortcode_stats = apply_filters( 'give_goal_shortcode_stats', [ - 'income' => (new DonationQuery())->form($form->ID)->sumIntendedAmount(), + 'income' => $form->get_earnings(), 'goal' => $goal_progress_stats['raw_goal'], ], $form_id, @@ -260,13 +264,6 @@ static function ($term) use ($tag_text_color, $tag_bg_color) { $income = $shortcode_stats['income']; $goal = $shortcode_stats['goal']; - /** - * @since 3.14.0 Use the 'give_donate_form_get_sales" filter to ensure the correct donation count will be used - */ - add_filter('give_donate_form_get_sales', function ($sales, $donationFormId) { - return (new Give\MultiFormGoals\ProgressBar\Model(['ids' => [$donationFormId]]))->getDonationCount(); - }, 10, 2); - switch ($goal_format) { case 'donation': $progress = $goal ? round(($form->get_sales() / $goal) * 100, 2) : 0; @@ -412,7 +409,7 @@ static function ($term) use ($tag_text_color, $tag_bg_color) { 'give' ), esc_attr(wp_json_encode($income_amounts, JSON_PRETTY_PRINT)), - esc_attr($formatted_income), + apply_filters('give_form_grid_progress_bar_amount_raised_value', esc_attr($formatted_income), $form_id), esc_attr(wp_json_encode($goal_amounts, JSON_PRETTY_PRINT)), esc_attr($formatted_goal) ); @@ -462,8 +459,7 @@ static function ($term) use ($tag_text_color, $tag_bg_color) {
    - get_sales() ?> + get_sales(), $form_id) ?> Date: Mon, 26 Aug 2024 16:54:44 -0400 Subject: [PATCH 096/190] chore: merge develop and update readme --- includes/admin/forms/dashboard-columns.php | 4 ++-- includes/forms/functions.php | 2 +- includes/misc-functions.php | 4 ++-- readme.txt | 1 + .../Actions/GetAsyncFormDataForListView.php | 10 +++++----- .../Actions/GiveGoalProgressStats.php | 6 +++--- .../AsyncData/Actions/LoadAsyncDataAssets.php | 10 +++++----- .../AdminFormListView/AdminFormListView.php | 10 +++++----- .../AdminFormListViewOptions.php | 12 ++++++------ .../AsyncData/AsyncDataHelpers.php | 8 ++++---- .../AsyncData/FormGrid/FormGridView.php | 8 ++++---- .../AsyncData/FormGrid/FormGridViewOptions.php | 10 +++++----- .../AsyncData/resources/loadAsyncData.js | 18 +++++++++--------- src/DonationForms/ServiceProvider.php | 2 +- .../ListTable/Columns/DonationCountColumn.php | 2 +- .../Columns/DonationRevenueColumn.php | 2 +- .../V2/ListTable/Columns/GoalColumn.php | 2 +- src/Framework/ListTable/ListTable.php | 4 ++-- src/MultiFormGoals/ProgressBar/Model.php | 6 +++--- templates/shortcode-form-grid.php | 4 ++-- 20 files changed, 63 insertions(+), 62 deletions(-) diff --git a/includes/admin/forms/dashboard-columns.php b/includes/admin/forms/dashboard-columns.php index e30c968d52..d9029642aa 100644 --- a/includes/admin/forms/dashboard-columns.php +++ b/includes/admin/forms/dashboard-columns.php @@ -59,7 +59,7 @@ function give_form_columns( $give_form_columns ) { /** * Render Give Form Columns * - * @unreleased Add new filters for the "donations count" and "revenue" columns + * @since 3.16.0 Add new filters for the "donations count" and "revenue" columns * @since 1.0 * * @param string $column_name Column name @@ -170,7 +170,7 @@ function give_sortable_form_columns( $columns ) { /** * Sorts Columns in the Forms List Table * - * @unreleased Remove "give_donate_form_get_sales" filter logic + * @since 3.16.0 Remove "give_donate_form_get_sales" filter logic * @since 3.14.0 Use the "give_donate_form_get_sales" filter to ensure the correct donation count will be used * @since 1.0 * diff --git a/includes/forms/functions.php b/includes/forms/functions.php index 8bc897dc59..f7339e33d8 100644 --- a/includes/forms/functions.php +++ b/includes/forms/functions.php @@ -1248,7 +1248,7 @@ function give_set_form_closed_status( $form_id ) { /** * Show Form Goal Stats in Admin ( Listing and Detail page ) * - * @unreleased Remove "give_donate_form_get_sales" filter logic + * @since 3.16.0 Remove "give_donate_form_get_sales" filter logic * @since 3.14.0 Use the "give_get_form_earnings_stats" filter to ensure the correct value will be displayed in the form progress bar * @since 2.19.0 Prevent divide by zero issue in goal percentage calculation logic. * diff --git a/includes/misc-functions.php b/includes/misc-functions.php index 19d1d8f46a..ab879cc18d 100644 --- a/includes/misc-functions.php +++ b/includes/misc-functions.php @@ -1928,7 +1928,7 @@ function give_get_nonce_field( $action, $name, $referer = false ) { /** * Display/Return a formatted goal for a donation form * - * @unreleased Add form_id to the array return + * @since 3.16.0 Add form_id to the array return * @since 2.1 * * @param int|Give_Donate_Form $form Form ID or Form Object. @@ -1975,7 +1975,7 @@ function give_goal_progress_stats( $form ) { /** * Filter the form income. * - * @unreleased Revert changes implemented on the 3.14.0 version + * @since 3.16.0 Revert changes implemented on the 3.14.0 version * @since 3.14.0 Replace "$form->earnings" with (new DonationQuery())->form($form->ID)->sumIntendedAmount() * @since 1.8.8 */ diff --git a/readme.txt b/readme.txt index d1ee1078e9..41db56163e 100644 --- a/readme.txt +++ b/readme.txt @@ -272,6 +272,7 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri * Security: Added protection against invalid donations on Legacy forms when using Stripe credit card gateway * Security: Resolved security issue related to the PayPal disconnect button * Fix: Added prevention of subscription renewals with gateway transaction IDs already used previously +* Fix: Resolved an issue where the donation form list table and form grid not loading properly on sites with a large number of forms and donations * Fix: Resolved an issue with the form grid not showing header images and link previews * Fix: Resolved an issue with the subscription payment failed email not saving the supported gateways information diff --git a/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php b/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php index 760967b649..adbede9c82 100644 --- a/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php +++ b/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php @@ -7,12 +7,12 @@ use Give\DonationForms\AsyncData\FormGrid\FormGridViewOptions; /** - * @unreleased + * @since 3.16.0 */ class GetAsyncFormDataForListView { /** - * @unreleased + * @since 3.16.0 */ public function __invoke() { @@ -74,7 +74,7 @@ public function __invoke() } /** - * @unreleased + * @since 3.16.0 */ private function isAsyncProgressBar(): bool { @@ -82,7 +82,7 @@ private function isAsyncProgressBar(): bool } /** - * @unreleased + * @since 3.16.0 */ private function isAsyncDonationCount(): bool { @@ -90,7 +90,7 @@ private function isAsyncDonationCount(): bool } /** - * @unreleased + * @since 3.16.0 */ private function isAsyncRevenue(): bool { diff --git a/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php b/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php index 1a50c275ba..339bcc3e0e 100644 --- a/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php +++ b/src/DonationForms/AsyncData/Actions/GiveGoalProgressStats.php @@ -6,12 +6,12 @@ use Give\DonationForms\AsyncData\AsyncDataHelpers; /** - * @unreleased + * @since 3.16.0 */ class GiveGoalProgressStats { /** - * @unreleased + * @since 3.16.0 */ public function maybeChangeGoalProgressStatsActualValue($stats) { @@ -33,7 +33,7 @@ public function maybeChangeGoalProgressStatsActualValue($stats) } /** - * @unreleased + * @since 3.16.0 */ private function isSingleForm(): bool { diff --git a/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php b/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php index be7d4ecd1a..6a83c90201 100644 --- a/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php +++ b/src/DonationForms/AsyncData/Actions/LoadAsyncDataAssets.php @@ -3,12 +3,12 @@ namespace Give\DonationForms\AsyncData\Actions; /** - * @unreleased + * @since 3.16.0 */ class LoadAsyncDataAssets { /** - * @unreleased + * @since 3.16.0 */ public function __invoke() { @@ -17,7 +17,7 @@ public function __invoke() } /** - * @unreleased + * @since 3.16.0 */ public static function handleName(): string { @@ -25,7 +25,7 @@ public static function handleName(): string } /** - * @unreleased + * @since 3.16.0 */ public static function registerAssets() { @@ -55,7 +55,7 @@ public static function registerAssets() } /** - * @unreleased + * @since 3.16.0 */ public static function enqueueAssets() { diff --git a/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php index 9a4fee1328..c17521d3d8 100644 --- a/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php +++ b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php @@ -5,12 +5,12 @@ use Give\DonationForms\AsyncData\AsyncDataHelpers; /** - * @unreleased + * @since 3.16.0 */ class AdminFormListView { /** - * @unreleased + * @since 3.16.0 */ public function maybeChangeAchievedIconOpacity($achievedIconOpacity) { @@ -22,7 +22,7 @@ public function maybeChangeAchievedIconOpacity($achievedIconOpacity) } /** - * @unreleased + * @since 3.16.0 */ public function maybeUsePlaceholderOnGoalAmountRaised(bool $usePlaceholder = false): bool { @@ -34,7 +34,7 @@ public function maybeUsePlaceholderOnGoalAmountRaised(bool $usePlaceholder = fal } /** - * @unreleased + * @since 3.16.0 */ public function maybeSetDonationsColumnAsync($donationsCountCachedValue, $formId) { @@ -50,7 +50,7 @@ public function maybeSetDonationsColumnAsync($donationsCountCachedValue, $formId } /** - * @unreleased + * @since 3.16.0 */ public function maybeSetRevenueColumnAsync($revenueCachedValue, $formId) { diff --git a/src/DonationForms/AsyncData/AdminFormListView/AdminFormListViewOptions.php b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListViewOptions.php index 627d421986..b70d84710c 100644 --- a/src/DonationForms/AsyncData/AdminFormListView/AdminFormListViewOptions.php +++ b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListViewOptions.php @@ -3,12 +3,12 @@ namespace Give\DonationForms\AsyncData\AdminFormListView; /** - * @unreleased + * @since 3.16.0 */ class AdminFormListViewOptions { /** - * @unreleased + * @since 3.16.0 */ public static function isAllStatsColumnsAsync(): bool { @@ -20,7 +20,7 @@ public static function isAllStatsColumnsAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function isGoalColumnAsync(): bool { @@ -32,7 +32,7 @@ public static function isGoalColumnAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function isDonationColumnAsync(): bool { @@ -44,7 +44,7 @@ public static function isDonationColumnAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function isRevenueColumnAsync(): bool { @@ -56,7 +56,7 @@ public static function isRevenueColumnAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function useCachedMetaKeys() { diff --git a/src/DonationForms/AsyncData/AsyncDataHelpers.php b/src/DonationForms/AsyncData/AsyncDataHelpers.php index 926b29be64..8b6d162fa9 100644 --- a/src/DonationForms/AsyncData/AsyncDataHelpers.php +++ b/src/DonationForms/AsyncData/AsyncDataHelpers.php @@ -6,12 +6,12 @@ use Give\MultiFormGoals\ProgressBar\Model as ProgressBarModel; /** - * @unreleased + * @since 3.16.0 */ class AsyncDataHelpers { /** - * @unreleased + * @since 3.16.0 */ public static function getFormDonationsCountValue($formId): int { @@ -19,7 +19,7 @@ public static function getFormDonationsCountValue($formId): int } /** - * @unreleased + * @since 3.16.0 */ public static function getFormRevenueValue($formId): int { @@ -27,7 +27,7 @@ public static function getFormRevenueValue($formId): int } /** - * @unreleased + * @since 3.16.0 */ public static function getSkeletonPlaceholder($width = '100%', $height = '0.7rem'): string { diff --git a/src/DonationForms/AsyncData/FormGrid/FormGridView.php b/src/DonationForms/AsyncData/FormGrid/FormGridView.php index e4c4382135..d060c14d68 100644 --- a/src/DonationForms/AsyncData/FormGrid/FormGridView.php +++ b/src/DonationForms/AsyncData/FormGrid/FormGridView.php @@ -5,12 +5,12 @@ use Give\DonationForms\AsyncData\AsyncDataHelpers; /** - * @unreleased + * @since 3.16.0 */ class FormGridView { /** - * @unreleased + * @since 3.16.0 */ public function maybeUsePlaceholderOnGoalAmountRaised(bool $usePlaceholder = false): bool { @@ -22,7 +22,7 @@ public function maybeUsePlaceholderOnGoalAmountRaised(bool $usePlaceholder = fal } /** - * @unreleased + * @since 3.16.0 */ public function maybeSetProgressBarAmountRaisedAsync($amountRaisedCachedValue, $formId) { @@ -38,7 +38,7 @@ public function maybeSetProgressBarAmountRaisedAsync($amountRaisedCachedValue, $ } /** - * @unreleased + * @since 3.16.0 */ public function maybeSetProgressBarDonationsCountAsync($donationsCountCachedValue, $formId) { diff --git a/src/DonationForms/AsyncData/FormGrid/FormGridViewOptions.php b/src/DonationForms/AsyncData/FormGrid/FormGridViewOptions.php index aa5cf1737c..c57c418b59 100644 --- a/src/DonationForms/AsyncData/FormGrid/FormGridViewOptions.php +++ b/src/DonationForms/AsyncData/FormGrid/FormGridViewOptions.php @@ -3,12 +3,12 @@ namespace Give\DonationForms\AsyncData\FormGrid; /** - * @unreleased + * @since 3.16.0 */ class FormGridViewOptions { /** - * @unreleased + * @since 3.16.0 */ public static function isAllProgressBarStatsAsync(): bool { @@ -20,7 +20,7 @@ public static function isAllProgressBarStatsAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function isProgressBarAmountRaisedAsync(): bool { @@ -32,7 +32,7 @@ public static function isProgressBarAmountRaisedAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function isProgressBarDonationsCountAsync(): bool { @@ -44,7 +44,7 @@ public static function isProgressBarDonationsCountAsync(): bool } /** - * @unreleased + * @since 3.16.0 */ public static function useCachedMetaKeys() { diff --git a/src/DonationForms/AsyncData/resources/loadAsyncData.js b/src/DonationForms/AsyncData/resources/loadAsyncData.js index 47ecb2925f..b7fc2048b7 100644 --- a/src/DonationForms/AsyncData/resources/loadAsyncData.js +++ b/src/DonationForms/AsyncData/resources/loadAsyncData.js @@ -8,7 +8,7 @@ * 3) When the user scrolls the mouse * 4) When the user resizes the screen * - * @unreleased + * @since 3.16.0 */ document.addEventListener('DOMContentLoaded', () => { /** @@ -22,7 +22,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * This function check if the element is visible on the screen. * - * @unreleased + * @since 3.16.0 */ function isInViewport(element) { const {top, bottom} = element.getBoundingClientRect(); @@ -34,7 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Check if an element is a placeholder waiting to have the value updated. * - * @unreleased + * @since 3.16.0 */ function isPlaceholder(element) { return !!element && Boolean(element.querySelector('.js-give-async-data')); @@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * This function fetch the async data from the server and set the values to the proper elements in the DOM. * - * @unreleased + * @since 3.16.0 */ const loadFormData = ( formId, @@ -152,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Handle the async data logic for ALL form list views available. * - * @unreleased + * @since 3.16.0 */ const maybeLoadAsyncData = () => { // If the async requests were aborted on the "beforeunload" or "giveListTableIsLoading" event, we don't want to create more async requests @@ -169,7 +169,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Check for changes in the "giveListTable" classes to trigger the "giveListTableIsLoadingEvent" when appropriated. * - * @unreleased + * @since 3.16.0 */ function maybeTriggerGiveListTableIsLoadingEvent() { if (giveListTable) { @@ -199,7 +199,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Load the async data of all forms (visible on the screen) from the NEW admin form list view - giveListTable. * - * @unreleased + * @since 3.16.0 */ function handleAdminFormsListViewItems() { const adminFormsListViewItems = document.querySelectorAll('tr:not(.give-async-data-fetch-triggered)'); @@ -238,7 +238,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Load the async data of all forms (visible on the screen) from the LEGACY admin form list view. * - * @unreleased + * @since 3.16.0 */ function handleAdminLegacyFormsListViewItems() { const adminLegacyFormsListViewItems = document.querySelectorAll( @@ -276,7 +276,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Load the async data in all form grid items that have the progress bar enabled. * - * @unreleased + * @since 3.16.0 */ function handleFormGridItems() { const formGridItems = document.querySelectorAll('.give-grid__item:not(.give-async-data-fetch-triggered)'); diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index dcdcd04885..89fde327dc 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -215,7 +215,7 @@ function ($value, DonationForm $form){ } /** - * @unreleased + * @since 3.16.0 */ private function registerAddFormSubmenuLink() { diff --git a/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php b/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php index f36a3b592b..6e9bb85578 100644 --- a/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php +++ b/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php @@ -38,7 +38,7 @@ public function getLabel(): string } /** - * @unreleased Add filter to change the cell value content + * @since 3.16.0 Add filter to change the cell value content * @since 3.14.0 Use the "getDonationCount()" method from progress bar model to ensure the correct donation count will be used * @since 2.24.0 * diff --git a/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php b/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php index 80b6a6aed3..b5fe554e60 100644 --- a/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php +++ b/src/DonationForms/V2/ListTable/Columns/DonationRevenueColumn.php @@ -36,7 +36,7 @@ public function getLabel(): string } /** - * @unreleased Add filter to change the cell value content + * @since 3.16.0 Add filter to change the cell value content * @since 2.24.0 * * @inheritDoc diff --git a/src/DonationForms/V2/ListTable/Columns/GoalColumn.php b/src/DonationForms/V2/ListTable/Columns/GoalColumn.php index fe065a950c..2fc13da635 100644 --- a/src/DonationForms/V2/ListTable/Columns/GoalColumn.php +++ b/src/DonationForms/V2/ListTable/Columns/GoalColumn.php @@ -35,7 +35,7 @@ public function getLabel(): string } /** - * @unreleased Remove "give_get_form_earnings_stats" filter logic and add filters to change the cell value content + * @since 3.16.0 Remove "give_get_form_earnings_stats" filter logic and add filters to change the cell value content * @since 3.14.0 Use the "give_get_form_earnings_stats" filter to ensure the correct value will be displayed in the form progress bar * @since 2.24.0 * diff --git a/src/Framework/ListTable/ListTable.php b/src/Framework/ListTable/ListTable.php index 6d60f080eb..2579eed329 100644 --- a/src/Framework/ListTable/ListTable.php +++ b/src/Framework/ListTable/ListTable.php @@ -124,12 +124,12 @@ private function safelyGetCellValue(ModelColumn $column, Model $model, string $l { try { /** - * @unreleased + * @since 3.16.0 */ do_action("givewp_list_table_cell_value_{$column::getId()}_before", $column, $model, $locale); /** - * @unreleased + * @since 3.16.0 */ $cellValue = apply_filters("givewp_list_table_cell_value_{$column::getId()}", $column->getCellValue($model, $locale), $column, $model, $locale); diff --git a/src/MultiFormGoals/ProgressBar/Model.php b/src/MultiFormGoals/ProgressBar/Model.php index 505555e350..ea024438e2 100644 --- a/src/MultiFormGoals/ProgressBar/Model.php +++ b/src/MultiFormGoals/ProgressBar/Model.php @@ -5,7 +5,7 @@ use Give\DonationForms\DonationQuery; /** - * @unreleased Add $statusList property + * @since 3.16.0 Add $statusList property * @since 2.9.0 */ class Model @@ -28,7 +28,7 @@ class Model /** * Constructs and sets up setting variables for a new Progress Bar model * - * @unreleased Add statusList support for $args + * @since 3.16.0 Add statusList support for $args * @since 2.9.0 **@param array $args Arguments for new Progress Bar, including 'ids' */ @@ -46,7 +46,7 @@ public function __construct(array $args) /** * Get forms associated with Progress Bar * - * @unreleased Use $statusList property + * @since 3.16.0 Use $statusList property * @since 3.0.3 Return empty array instead of false * @since 2.9.0 */ diff --git a/templates/shortcode-form-grid.php b/templates/shortcode-form-grid.php index 085a2e0126..76cf35a0bb 100644 --- a/templates/shortcode-form-grid.php +++ b/templates/shortcode-form-grid.php @@ -14,7 +14,7 @@ /** * List of changes * - * @unreleased Add filters to enable the async mode and to change the values of the "amount raised" and "donations count" on the progress bar + * @since 3.16.0 Add filters to enable the async mode and to change the values of the "amount raised" and "donations count" on the progress bar * @since 2.27.1 Use get_the_excerpt function to get short description of donation form to display in form grid. */ @@ -247,7 +247,7 @@ static function ($term) use ($tag_text_color, $tag_bg_color) { $show_goal = isset($atts['show_goal']) ? filter_var($atts['show_goal'], FILTER_VALIDATE_BOOLEAN) : true; /** - * @unreleased Revert changes implemented on the 3.14.0 version + * @since 3.16.0 Revert changes implemented on the 3.14.0 version * @since 3.14.0 Replace "$form->get_earnings()" with (new DonationQuery())->form($form->ID)->sumIntendedAmount() */ $shortcode_stats = apply_filters( From 19de65d81d71074fb5ec96563e2bc7e20af20c0e Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Tue, 27 Aug 2024 15:13:10 -0400 Subject: [PATCH 097/190] Fix: Update function for getting terms (#7522) --- src/FormMigration/Steps/FormTaxonomies.php | 2 +- .../Steps/FormTaxonomiesTest.php | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/FormMigration/Steps/FormTaxonomies.php b/src/FormMigration/Steps/FormTaxonomies.php index 1caca22ba6..8014121680 100644 --- a/src/FormMigration/Steps/FormTaxonomies.php +++ b/src/FormMigration/Steps/FormTaxonomies.php @@ -28,7 +28,7 @@ public function process() */ public function migrateTaxonomy($taxonomy): void { - $terms = get_terms(['post' => $this->formV2->id, 'taxonomy' => $taxonomy]); + $terms = wp_get_post_terms($this->formV2->id, $taxonomy); wp_set_post_terms($this->formV3->id, array_column($terms, 'term_id'), $taxonomy); } } diff --git a/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php b/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php index 99464f4f19..7679288852 100644 --- a/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php +++ b/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php @@ -122,4 +122,30 @@ public function testDoesNotMigrateFormCategoriesWhenCategoriesDisabled() wp_list_pluck(get_the_terms($donationFormV3->id, 'give_forms_category'), 'term_id') ); } + + /** + * @since 3.16.0 + */ + public function testMigratesOnlySelectedTerms() + { + $donationFormV2 = $this->createSimpleDonationForm(); + $donationFormV3 = DonationForm::factory()->create(); + + give_update_option('tags', 'enabled'); + give_setup_taxonomies(); + + $term1 = wp_create_term('aye', 'give_forms_tag'); + $term2 = wp_create_term('bee', 'give_forms_tag'); + wp_set_post_terms($donationFormV2->id, [$term1['term_id']], 'give_forms_tag'); + + $step = new FormTaxonomies( + new FormMigrationPayload($donationFormV2, $donationFormV3) + ); + $step->process(); + + $migratedTermIds = wp_list_pluck(get_the_terms($donationFormV3->id, 'give_forms_tag'), 'term_id'); + + $this->assertContains($term1['term_id'], $migratedTermIds); + $this->assertNotContains($term2['term_id'], $migratedTermIds); + } } From 9377c1a4269e37e17c4d7b23e9f94241e6d979f1 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 27 Aug 2024 16:14:26 -0400 Subject: [PATCH 098/190] Revert "Fix: Validate Stripe card info in legacy adapter (#7480)" This reverts commit b93b93e079e24e7c9577085ec5ed61a6f2c5ea37. --- .../Stripe/LegacyStripeAdapterTest.php | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php diff --git a/tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php b/tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php deleted file mode 100644 index 615775dace..0000000000 --- a/tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php +++ /dev/null @@ -1,41 +0,0 @@ -paymentGateway = 'stripe'; - $formData->cardInfo = CardInfo::fromArray([ - 'name' => 'Tester T. Test', - 'cvc' => '123', - 'expMonth' => '01', - 'expYear' => '99', - 'number' => '4242 4242 4242 4242', - ]); - - do_action('give_donation_form_processing_start', $formData); - - $this->assertTrue(true); // No exception thrown - } - - public function testThrowsExceptionForEmptyCardInformation() - { - $formData = new FormData; - $formData->paymentGateway = 'stripe'; - $formData->cardInfo = CardInfo::fromArray([ - 'name' => 'Spammer P. Spam', - 'cvc' => '', - 'expMonth' => '', - 'expYear' => '', - 'number' => '', - ]); - - $this->expectException(WPDieException::class); - - do_action('give_donation_form_processing_start', $formData); - } -} From 7ffa5c89e545ad5e19148d3abbe1f2ac4c1daa13 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 27 Aug 2024 16:19:58 -0400 Subject: [PATCH 099/190] Revert "Fix: Validate Stripe card info in legacy adapter (#7480)" This reverts commit b93b93e079e24e7c9577085ec5ed61a6f2c5ea37. --- .../Gateways/ServiceProvider.php | 10 +++---- .../Gateways/Stripe/LegacyStripeAdapter.php | 28 ++----------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/PaymentGateways/Gateways/ServiceProvider.php b/src/PaymentGateways/Gateways/ServiceProvider.php index 0c75e33f95..4eec231eba 100644 --- a/src/PaymentGateways/Gateways/ServiceProvider.php +++ b/src/PaymentGateways/Gateways/ServiceProvider.php @@ -49,8 +49,8 @@ public function boot() $this->registerGateways(); } catch (Exception $e) { Log::error('Error Registering Gateways', [ - 'message' => $e->getMessage() - ]); + 'message' => $e->getMessage() + ]); } } @@ -77,8 +77,8 @@ private function registerGateways() $this->bootOfflineDonations(); } - /** - * @since 3.0.0 + /** + * @since 3.0.0 */ private function addStripeWebhookListeners() { @@ -120,7 +120,6 @@ private function addStripeWebhookListeners() /** * @since 3.0.0 - * @since 3.16.0 Add validation of card info. */ private function addLegacyStripeAdapter() { @@ -129,7 +128,6 @@ private function addLegacyStripeAdapter() $legacyStripeAdapter->addDonationDetails(); $legacyStripeAdapter->loadLegacyStripeWebhooksAndFilters(); - $legacyStripeAdapter->validateCardInformation(); } /** diff --git a/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php b/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php index b5f7970a64..8c233e75ff 100644 --- a/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php +++ b/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php @@ -4,7 +4,6 @@ use Give\Donations\Models\Donation; use Give\Helpers\Gateways\Stripe; -use Give\PaymentGateways\DataTransferObjects\FormData; use Give\PaymentGateways\Gateways\Stripe\StripePaymentElementGateway\StripePaymentElementGateway; use Give_Stripe; @@ -87,16 +86,16 @@ public function addDonationDetails() $account = 'connect' === $accountDetail['type'] ? "{$accountDetail['account_name']} ({$accountId})" : give_stripe_convert_slug_to_title($accountId); - ?> +?> - paymentGateway) { - /** - * Validate card information to prevent spam donations and fake donors. - */ - if(! $data->cardInfo->number || ! $data->cardInfo->expMonth || ! $data->cardInfo->expYear || ! $data->cardInfo->cvc) { - wp_die( - esc_html__('Incomplete card information.', 'give'), - esc_html__('Error', 'give'), - ['response' => 403] - ); - } - } - }); - } } From a48422a0b3a6489fefe73a50ddd437cc0899861b Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 27 Aug 2024 16:22:10 -0400 Subject: [PATCH 100/190] Revert "Fix: Validate Stripe card info in legacy adapter (#7480)" This reverts commit b93b93e079e24e7c9577085ec5ed61a6f2c5ea37. --- .../Gateways/ServiceProvider.php | 2 - .../Gateways/Stripe/LegacyStripeAdapter.php | 22 ---------- .../Stripe/LegacyStripeAdapterTest.php | 41 ------------------- 3 files changed, 65 deletions(-) delete mode 100644 tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php diff --git a/src/PaymentGateways/Gateways/ServiceProvider.php b/src/PaymentGateways/Gateways/ServiceProvider.php index bf4bf50b18..522e33a2cb 100644 --- a/src/PaymentGateways/Gateways/ServiceProvider.php +++ b/src/PaymentGateways/Gateways/ServiceProvider.php @@ -120,7 +120,6 @@ private function addStripeWebhookListeners() /** * @since 3.0.0 - * @unreleased Add validation of card info. */ private function addLegacyStripeAdapter() { @@ -129,7 +128,6 @@ private function addLegacyStripeAdapter() $legacyStripeAdapter->addDonationDetails(); $legacyStripeAdapter->loadLegacyStripeWebhooksAndFilters(); - $legacyStripeAdapter->validateCardInformation(); } /** diff --git a/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php b/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php index 40e8e90165..81c751c833 100644 --- a/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php +++ b/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php @@ -4,7 +4,6 @@ use Give\Donations\Models\Donation; use Give\Helpers\Gateways\Stripe; -use Give\PaymentGateways\DataTransferObjects\FormData; use Give\PaymentGateways\Gateways\Stripe\StripePaymentElementGateway\StripePaymentElementGateway; use Give_Stripe; @@ -112,25 +111,4 @@ public function addDonationDetails() } }); } - - /** - * @unreleased - */ - public function validateCardInformation() - { - add_action('give_donation_form_processing_start', function(FormData $data) { - if('stripe' === $data->paymentGateway) { - /** - * Validate card information to prevent spam donations and fake donors. - */ - if(! $data->cardInfo->number || ! $data->cardInfo->expMonth || ! $data->cardInfo->expYear || ! $data->cardInfo->cvc) { - wp_die( - esc_html__('Incomplete card information.', 'give'), - esc_html__('Error', 'give'), - ['response' => 403] - ); - } - } - }); - } } diff --git a/tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php b/tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php deleted file mode 100644 index 615775dace..0000000000 --- a/tests/Unit/PaymentGateways/Gateways/Stripe/LegacyStripeAdapterTest.php +++ /dev/null @@ -1,41 +0,0 @@ -paymentGateway = 'stripe'; - $formData->cardInfo = CardInfo::fromArray([ - 'name' => 'Tester T. Test', - 'cvc' => '123', - 'expMonth' => '01', - 'expYear' => '99', - 'number' => '4242 4242 4242 4242', - ]); - - do_action('give_donation_form_processing_start', $formData); - - $this->assertTrue(true); // No exception thrown - } - - public function testThrowsExceptionForEmptyCardInformation() - { - $formData = new FormData; - $formData->paymentGateway = 'stripe'; - $formData->cardInfo = CardInfo::fromArray([ - 'name' => 'Spammer P. Spam', - 'cvc' => '', - 'expMonth' => '', - 'expYear' => '', - 'number' => '', - ]); - - $this->expectException(WPDieException::class); - - do_action('give_donation_form_processing_start', $formData); - } -} From 5fbfa63d701c995637bd59a1258313f59ae6242d Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 27 Aug 2024 16:24:22 -0400 Subject: [PATCH 101/190] chore: update readme --- readme.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/readme.txt b/readme.txt index 41db56163e..0e8e709369 100644 --- a/readme.txt +++ b/readme.txt @@ -269,7 +269,6 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri * Enhancement: Added individual form migration links to the donation form list table * Enhancement: Updated various strings throughout GiveWP to be translatable (Open-source contribution by @DAnn2012) * Security: Resolved security issues related to file paths and permissions (CVE-2024-6551) -* Security: Added protection against invalid donations on Legacy forms when using Stripe credit card gateway * Security: Resolved security issue related to the PayPal disconnect button * Fix: Added prevention of subscription renewals with gateway transaction IDs already used previously * Fix: Resolved an issue where the donation form list table and form grid not loading properly on sites with a large number of forms and donations From 8163875e044e52d9ac3244f022705b739510f161 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 30 Aug 2024 08:59:12 -0400 Subject: [PATCH 102/190] refactor: remove visible field error states --- src/DonationForms/Rules/HoneyPotRule.php | 11 +---------- src/DonationForms/ServiceProvider.php | 4 ++-- .../registrars/templates/fields/Honeypot.tsx | 19 ++++++++++++++++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/DonationForms/Rules/HoneyPotRule.php b/src/DonationForms/Rules/HoneyPotRule.php index 38837fcf17..71e65dd4ad 100644 --- a/src/DonationForms/Rules/HoneyPotRule.php +++ b/src/DonationForms/Rules/HoneyPotRule.php @@ -3,13 +3,12 @@ use Closure; use Give\Log\Log; -use Give\Vendors\StellarWP\Validation\Contracts\ValidatesOnFrontEnd; use Give\Vendors\StellarWP\Validation\Contracts\ValidationRule; /** * @unreleased */ -class HoneyPotRule implements ValidationRule, ValidatesOnFrontEnd +class HoneyPotRule implements ValidationRule { /** @@ -43,12 +42,4 @@ public function __invoke($value, Closure $fail, string $key, array $values) ); } } - - /** - * @unreleased - */ - public function serializeOption() - { - return null; - } } diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index e8d96a8271..992a05ddf4 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -39,7 +39,7 @@ use Give\DonationForms\V2\ListTable\Columns\GoalColumn; use Give\DonationForms\V2\Models\DonationForm; use Give\DonationForms\ValueObjects\DonationFormStatus; -use Give\Framework\FieldsAPI\DonationForm; +use Give\Framework\FieldsAPI\DonationForm as DonationFormModel; use Give\Framework\FieldsAPI\Exceptions\EmptyNameException; use Give\Framework\FormDesigns\Registrars\FormDesignRegistrar; use Give\Framework\Migrations\MigrationsRegister; @@ -362,7 +362,7 @@ protected function registerPostStatus() */ private function registerHoneyPotField(): void { - add_action('givewp_donation_form_schema', function (DonationForm $form, int $formId) { + add_action('givewp_donation_form_schema', function (DonationFormModel $form, int $formId) { if (apply_filters('givewp_donation_forms_honeypot_enabled', false, $formId)) { (new AddHoneyPotFieldToDonationForms())($form); } diff --git a/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx b/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx index 73f832a1d2..76292d1b32 100644 --- a/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx +++ b/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx @@ -1,4 +1,6 @@ import {FieldHasDescriptionProps} from '@givewp/forms/propTypes'; +import {useEffect} from 'react'; +import {__} from '@wordpress/i18n'; /** * @unreleased @@ -13,14 +15,25 @@ export default function Honeypot({ }: FieldHasDescriptionProps) { const FieldDescription = window.givewp.form.templates.layouts.fieldDescription; const Wrapper = window.givewp.form.templates.layouts.wrapper; + const {setError, clearErrors} = window.givewp.form.hooks.useFormContext(); + + useEffect(() => { + if (fieldError) { + clearErrors(inputProps.name); + + setError('FORM_ERROR', { + message: __('Something went wrong, please try again or contact support.', 'give') + }); + } + + }, [fieldError]); return ( -
    + + + ); } diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/index.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/index.tsx index 551b1e5651..4a7210e0cd 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/index.tsx @@ -1,9 +1,13 @@ import settings from './settings'; import {FieldBlock} from '@givewp/form-builder/types'; +import {addFilter} from "@wordpress/hooks"; +import withAdditionalPaymentGatewayNotice from './withAdditionalPaymentGatewayNotice'; const paymentGateways: FieldBlock = { name: 'givewp/payment-gateways', settings, }; +addFilter('editor.BlockEdit', 'givewp/stripe-payment-element', withAdditionalPaymentGatewayNotice, 999); + export default paymentGateways; diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/withAdditionalPaymentGatewayNotice.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/withAdditionalPaymentGatewayNotice.tsx new file mode 100644 index 0000000000..1f9b905560 --- /dev/null +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/withAdditionalPaymentGatewayNotice.tsx @@ -0,0 +1,51 @@ +import {createHigherOrderComponent} from "@wordpress/compose"; +import {PanelRow} from "@wordpress/components"; +import InspectorNotice from "@givewp/form-builder/components/settings/InspectorNotice"; +import {__} from "@wordpress/i18n"; +import {InspectorControls} from "@wordpress/block-editor"; +import {useState} from "react"; + +declare const window: { + additionalPaymentGatewaysNotificationData: { + actionUrl: string; + isDismissed: boolean; + }; +} & Window; + +const AdditionalPaymentGatewaysNotice = () => { + const {actionUrl, isDismissed} = window.additionalPaymentGatewaysNotificationData; + const [showNotification, setShowNotification] = useState(!window.additionalPaymentGatewaysNotificationData.isDismissed); + const onDismissNotification = () => fetch(actionUrl, {method: 'POST'}).then(() => setShowNotification(false)) + + return ( + + {showNotification && ( + + + + )} + + ) +} + +const withAdditionalPaymentGatewayNotice = createHigherOrderComponent((BlockEdit) => { + return (props) => { + if (props.name === 'givewp/payment-gateways') { + return ( + <> + + + + ); + } + return ; + }; +}, 'withAdditionalPaymentGatewayNotice'); + +export default withAdditionalPaymentGatewayNotice; diff --git a/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx new file mode 100644 index 0000000000..0c3e52a0ec --- /dev/null +++ b/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx @@ -0,0 +1,29 @@ +import {Icon} from "@wordpress/components"; +import {close, external} from "@wordpress/icons"; +import './styles.scss' + +/** + * @unreleased + */ +const InspectorNotice = ({title, description, helpText, helpUrl, onDismiss}) => { + + return ( +
    + + {title} + + + + {description} + + + + + {helpText} + + +
    + ) +} + +export default InspectorNotice; diff --git a/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/styles.scss b/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/styles.scss new file mode 100644 index 0000000000..036ec47004 --- /dev/null +++ b/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/styles.scss @@ -0,0 +1,36 @@ +.givewp-inspector-notice { + &__container { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + border-radius: 2px; + background-color: #f2f2f2; + color: #0e0e0e; + font-size: 12px; + margin: var(--givewp-spacing-4) + } + + &__title { + font-weight: 600; + } + + &__closeIcon { + cursor: pointer; + height: 16px; + width: 16px; + position: absolute; + right: 12px; + top: 12px; + } + + &__externalIcon { + height: 18px; + width: 18px; + fill: #2271b1; + float: left; + margin-top: 2px; + margin-right: 8px; + } +} diff --git a/tests/Unit/FormBuilder/ServiceProviderTest.php b/tests/Unit/FormBuilder/ServiceProviderTest.php new file mode 100644 index 0000000000..4c9e81eb4f --- /dev/null +++ b/tests/Unit/FormBuilder/ServiceProviderTest.php @@ -0,0 +1,29 @@ +factory()->user->create(); + wp_set_current_user($userId); + + do_action('wp_ajax_givewp_additional_payment_gateways_hide_notice'); + + $this->assertTrue( + (bool) get_user_meta($userId, 'givewp-additional-payment-gateways-notice-dismissed', true) + ); + } +} From abec9a7c6857c86b43561da8d7e2153eb1cddcab Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 30 Aug 2024 13:46:39 -0400 Subject: [PATCH 105/190] Refactor: add support for React 18+ createRoot (#7482) Co-authored-by: Jon Waldstein --- .../src/js/admin/onboarding-wizard/index.js | 10 +++--- assets/src/js/admin/reports/app.js | 16 +++++---- assets/src/js/admin/reports/widget.js | 8 ++--- .../DonationFormBlock/resources/app/index.tsx | 34 ++++++------------- src/DonationForms/V2/resources/add-v2form.tsx | 7 ++-- .../V2/resources/admin-donation-forms.tsx | 12 ++++--- .../V2/resources/edit-v2form.tsx | 9 ++--- .../resources/app/DonationFormApp.tsx | 8 ++--- .../DonationConfirmationReceiptApp.tsx | 6 +--- src/Donations/resources/index.tsx | 11 +++--- src/DonorDashboards/resources/js/app/index.js | 7 ++-- src/Donors/resources/admin-donors.tsx | 7 ++-- src/Log/Admin/index.js | 5 +-- src/MigrationLog/Admin/index.js | 5 +-- .../WelcomeBanner/resources/js/index.tsx | 4 +-- .../resources/admin-subscriptions.tsx | 17 ++++++---- 16 files changed, 78 insertions(+), 88 deletions(-) diff --git a/assets/src/js/admin/onboarding-wizard/index.js b/assets/src/js/admin/onboarding-wizard/index.js index 7ce676dcc4..903317d446 100644 --- a/assets/src/js/admin/onboarding-wizard/index.js +++ b/assets/src/js/admin/onboarding-wizard/index.js @@ -4,7 +4,7 @@ // Vendor dependencies import React from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; // Onboarding Wizard app import App from './app/index.js'; @@ -13,7 +13,7 @@ import App from './app/index.js'; import './style.scss'; // Render application -ReactDOM.render( - , - document.getElementById( 'onboarding-wizard-app' ) -); +const element = document.getElementById('onboarding-wizard-app'); +if (element) { + createRoot(element).render(); +} diff --git a/assets/src/js/admin/reports/app.js b/assets/src/js/admin/reports/app.js index bf71dad651..09ace170c6 100644 --- a/assets/src/js/admin/reports/app.js +++ b/assets/src/js/admin/reports/app.js @@ -3,14 +3,16 @@ // Vendor dependencies import { HashRouter as Router } from 'react-router-dom'; import React from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; // Reports app import App from './app/index.js'; -ReactDOM.render( - - - , - document.getElementById( 'reports-app' ) -); +const element = document.getElementById('reports-app'); +if (element) { + createRoot(element).render( + + + + ); +} diff --git a/assets/src/js/admin/reports/widget.js b/assets/src/js/admin/reports/widget.js index 6174d7b19d..7ab993b0a3 100644 --- a/assets/src/js/admin/reports/widget.js +++ b/assets/src/js/admin/reports/widget.js @@ -1,7 +1,7 @@ // Entry point for dashboard widget // Vendor dependencies -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import moment from 'moment'; // Reports widget @@ -30,10 +30,8 @@ const initialState = { const container = document.getElementById('givewp-reports-widget'); if (container) { - ReactDOM.render( - + createRoot(container).render( - , - document.getElementById('givewp-reports-widget') + ); } diff --git a/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx b/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx index 3ae0e8b8cc..32d8d4fbca 100644 --- a/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx +++ b/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx @@ -92,28 +92,14 @@ roots.forEach((root) => { const formUrl = root.getAttribute('data-form-url'); const formViewUrl = root.getAttribute('data-form-view-url'); - if (createRoot) { - createRoot(root).render( - - ); - } else { - render( - , - root - ); - } + createRoot(root).render( + + ); }); diff --git a/src/DonationForms/V2/resources/add-v2form.tsx b/src/DonationForms/V2/resources/add-v2form.tsx index 7b77dcb1a1..ed8e3aeb1c 100644 --- a/src/DonationForms/V2/resources/add-v2form.tsx +++ b/src/DonationForms/V2/resources/add-v2form.tsx @@ -1,5 +1,5 @@ import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import AddForm from './components/Onboarding/Components/AddForm'; import './colors.scss'; @@ -7,9 +7,8 @@ const appContainer = document.createElement('div'); const target = document.querySelector('.wp-header-end'); target.parentNode.insertBefore(appContainer, target); -ReactDOM.render( +createRoot(appContainer).render( - , - appContainer + ); diff --git a/src/DonationForms/V2/resources/admin-donation-forms.tsx b/src/DonationForms/V2/resources/admin-donation-forms.tsx index 7304bee71b..6fde451ccd 100644 --- a/src/DonationForms/V2/resources/admin-donation-forms.tsx +++ b/src/DonationForms/V2/resources/admin-donation-forms.tsx @@ -1,11 +1,13 @@ import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; -import DonationFormsListTable from "./components/DonationFormsListTable"; +import {createRoot} from 'react-dom/client'; +import DonationFormsListTable from './components/DonationFormsListTable'; import './colors.scss'; -ReactDOM.render( +const root = document.getElementById('give-admin-donation-forms-root'); + +createRoot(root).render( - , - document.getElementById('give-admin-donation-forms-root') + ); + diff --git a/src/DonationForms/V2/resources/edit-v2form.tsx b/src/DonationForms/V2/resources/edit-v2form.tsx index 7c1b4a813e..50da0b3cee 100644 --- a/src/DonationForms/V2/resources/edit-v2form.tsx +++ b/src/DonationForms/V2/resources/edit-v2form.tsx @@ -1,11 +1,12 @@ import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import EditForm from './components/Onboarding/Components/EditForm'; import './colors.scss'; -ReactDOM.render( +const root = createRoot(document.getElementById('give-admin-edit-v2form')); + +root.render( - , - document.getElementById('give-admin-edit-v2form') + ); diff --git a/src/DonationForms/resources/app/DonationFormApp.tsx b/src/DonationForms/resources/app/DonationFormApp.tsx index 768adc4875..c8e107bd1e 100644 --- a/src/DonationForms/resources/app/DonationFormApp.tsx +++ b/src/DonationForms/resources/app/DonationFormApp.tsx @@ -1,4 +1,4 @@ -import {createRoot, render} from '@wordpress/element'; +import {createRoot} from '@wordpress/element'; import getDefaultValuesFromSections from './utilities/getDefaultValuesFromSections'; import Form from './form/Form'; import {DonationFormStateProvider} from './store'; @@ -146,8 +146,4 @@ function AppPreview() { const root = document.getElementById('root-givewp-donation-form'); const style = document.getElementById('root-givewp-donation-form-style'); -if (createRoot) { - createRoot(root).render(previewMode ? : ); -} else { - render(previewMode ? : , root); -} +createRoot(root).render(previewMode ? : ); diff --git a/src/DonationForms/resources/receipt/DonationConfirmationReceiptApp.tsx b/src/DonationForms/resources/receipt/DonationConfirmationReceiptApp.tsx index 7ee43b554d..98431a80bc 100644 --- a/src/DonationForms/resources/receipt/DonationConfirmationReceiptApp.tsx +++ b/src/DonationForms/resources/receipt/DonationConfirmationReceiptApp.tsx @@ -79,11 +79,7 @@ function DonationConfirmationReceiptApp() { const root = document.getElementById('root-givewp-donation-confirmation-receipt'); -if (createRoot) { - createRoot(root).render(); -} else { - render(, root); -} +createRoot(root).render(); root.scrollIntoView({ behavior: 'smooth', diff --git a/src/Donations/resources/index.tsx b/src/Donations/resources/index.tsx index f15c5ed22a..e35c80a70f 100644 --- a/src/Donations/resources/index.tsx +++ b/src/Donations/resources/index.tsx @@ -1,8 +1,11 @@ import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import DonationsListTable from './components/DonationsListTable'; -ReactDOM.render( - {}, - document.getElementById('give-admin-donations-root') +const root = createRoot(document.getElementById('give-admin-donations-root')); + +root.render( + + + ); diff --git a/src/DonorDashboards/resources/js/app/index.js b/src/DonorDashboards/resources/js/app/index.js index 8abd91eb69..902a0042f3 100644 --- a/src/DonorDashboards/resources/js/app/index.js +++ b/src/DonorDashboards/resources/js/app/index.js @@ -2,7 +2,7 @@ // Vendor dependencies import {HashRouter as Router} from 'react-router-dom'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import React from 'react'; import {Provider} from 'react-redux'; @@ -39,12 +39,13 @@ window.giveDonorDashboard = { window.addEventListener('DOMContentLoaded', (event) => { registerDefaultTabs(); - ReactDOM.render( + const root = createRoot(document.getElementById('give-donor-dashboard')); + + root.render( , - document.getElementById('give-donor-dashboard') ); }); diff --git a/src/Donors/resources/admin-donors.tsx b/src/Donors/resources/admin-donors.tsx index d2140fa5de..65139eb320 100644 --- a/src/Donors/resources/admin-donors.tsx +++ b/src/Donors/resources/admin-donors.tsx @@ -1,10 +1,11 @@ import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import DonorsListTable from './components/DonorsListTable'; -ReactDOM.render( +const root = createRoot(document.getElementById('give-admin-donors-root')); + +root.render( , - document.getElementById('give-admin-donors-root') ); diff --git a/src/Log/Admin/index.js b/src/Log/Admin/index.js index 01e3c0b2c6..6051065fe3 100644 --- a/src/Log/Admin/index.js +++ b/src/Log/Admin/index.js @@ -1,5 +1,6 @@ -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import Logs from './Logs'; -ReactDOM.render(, document.getElementById('give-logs-list-table-app')); +const root = createRoot(document.getElementById('give-logs-list-table-app')); +root.render(); diff --git a/src/MigrationLog/Admin/index.js b/src/MigrationLog/Admin/index.js index 5745eeaee1..816e25b904 100644 --- a/src/MigrationLog/Admin/index.js +++ b/src/MigrationLog/Admin/index.js @@ -1,5 +1,6 @@ -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import Migrations from './Migrations'; -ReactDOM.render(, document.getElementById('give_migrations_table_app')); +const root = createRoot(document.getElementById('give_migrations_table_app')); +root.render(); diff --git a/src/Promotions/WelcomeBanner/resources/js/index.tsx b/src/Promotions/WelcomeBanner/resources/js/index.tsx index fe7ee8ffc4..5f9e5ddb30 100644 --- a/src/Promotions/WelcomeBanner/resources/js/index.tsx +++ b/src/Promotions/WelcomeBanner/resources/js/index.tsx @@ -1,6 +1,6 @@ import App from './app/app'; import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; type windowData = { assets: string; @@ -33,5 +33,5 @@ if (root) { // Place banner underneath Plugin header pluginHeader.insertAdjacentElement('afterend', root); - ReactDOM.render(, root); + createRoot(root).render(); } diff --git a/src/Subscriptions/resources/admin-subscriptions.tsx b/src/Subscriptions/resources/admin-subscriptions.tsx index 559e2b2a9d..68b314627a 100644 --- a/src/Subscriptions/resources/admin-subscriptions.tsx +++ b/src/Subscriptions/resources/admin-subscriptions.tsx @@ -1,10 +1,13 @@ import {StrictMode} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import SubscriptionsListTable from './components/SubscriptionsListTable'; -ReactDOM.render( - - - , - document.getElementById('give-admin-subscriptions-root') -); +const element = document.getElementById('give-admin-subscriptions-root'); + +if (element) { + createRoot(element).render( + + + + ); +} From 3fb43fb77a7f6c226e8f3db183428787976f70c8 Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Sat, 31 Aug 2024 11:05:34 -0300 Subject: [PATCH 106/190] Fix: the "Offline donations" section in the controls of the Payment Gateways block should be visible when Offline gateway for v3 forms is enabled (#7492) --- .../Offline/Actions/EnqueueOfflineFormBuilderScripts.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php b/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php index ffc3b83032..827aaa11f9 100644 --- a/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php +++ b/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php @@ -9,6 +9,8 @@ class EnqueueOfflineFormBuilderScripts /** * Enqueues the Stripe scripts and styles for the Form Builder. * + * @unreleased On the "offlineEnabled" option check if the offline gateway is enabled for v3 forms instead of v2 forms + * * @return void */ public function __invoke() @@ -34,7 +36,7 @@ public function __invoke() 'givewp-offline-gateway-form-builder', 'window.giveOfflineGatewaySettings = ' . wp_json_encode( [ - 'offlineEnabled' => give_is_gateway_active(OfflineGateway::id()), + 'offlineEnabled' => give_is_gateway_active(OfflineGateway::id(), 3), 'offlineSettingsUrl' => admin_url( 'edit.php?post_type=give_forms&page=give-settings&tab=gateways§ion=offline-donations' ), From 5bbb79ba57b18bfb7db634faa21d9db50dfbc26b Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Tue, 3 Sep 2024 11:23:42 -0400 Subject: [PATCH 107/190] Refactor: Replace defaultProps with ES6 defaults (#7490) --- .../js/app/components/select-control/index.js | 12 +------- .../authorize-control/index.js | 8 +---- .../card-control/index.js | 8 +---- .../square-control/index.js | 8 +---- .../js/app/components/text-control/index.js | 10 +------ .../resources/js/block/color-control/index.js | 7 +---- .../js/components/color-control/index.js | 7 +---- .../js/components/date-time-control/index.js | 6 +--- .../js/components/image-control/index.js | 7 +---- .../components/multi-select-control/index.js | 18 ++++-------- .../components/server-side-render-x/index.js | 29 +++++++++---------- src/Views/Components/Card/index.js | 8 +---- .../Components/ListTable/Pagination/index.tsx | 13 ++++----- src/Views/Components/Modal/index.js | 10 +------ src/Views/Components/Pagination/index.js | 9 +----- src/Views/Components/PeriodSelector/index.js | 10 +------ src/Views/Components/Spinner/index.js | 6 +--- src/Views/Components/Table/index.js | 11 +------ 18 files changed, 39 insertions(+), 148 deletions(-) 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 4974d178be..f641589889 100644 --- a/src/DonorDashboards/resources/js/app/components/select-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/select-control/index.js @@ -11,7 +11,7 @@ import './style.scss'; import {__} from '@wordpress/i18n'; -const SelectControl = ({label, value, isLoading, onChange, options, placeholder, width, isClearable}) => { +const SelectControl = ({value, options, isLoading, label = null, onChange = null, placeholder = __('Select...', 'give'), width = null, isClearable = false}) => { if (options && options.length < 2) { return null; } @@ -107,14 +107,4 @@ SelectControl.propTypes = { isClearable: PropTypes.bool, }; -SelectControl.defaultProps = { - label: null, - value: null, - onChange: null, - options: null, - placeholder: __('Select...', 'give'), - width: null, - isClearable: false, -}; - export default SelectControl; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/authorize-control/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/authorize-control/index.js index 63be2238bb..7e02984a75 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/authorize-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/authorize-control/index.js @@ -7,7 +7,7 @@ import {useAcceptJs} from 'react-acceptjs'; import './style.scss'; -const AuthorizeControl = ({label, value, forwardedRef, gateway}) => { +const AuthorizeControl = ({label = null, value = null, forwardedRef, gateway}) => { const [cardNumber, setCardNumber] = useState(value ? value.card_number : ''); const [cardExpiryDate, setCardExpiryDate] = useState( value ? `${value.card_exp_month} \ ${value.card_exp_year}` : '', @@ -156,10 +156,4 @@ AuthorizeControl.propTypes = { onChange: PropTypes.func, }; -AuthorizeControl.defaultProps = { - label: null, - value: null, - onChange: null, -}; - export default AuthorizeControl; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/card-control/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/card-control/index.js index ec3c060e32..196adc1dca 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/card-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/card-control/index.js @@ -6,7 +6,7 @@ import {useAccentColor} from '../../../../hooks'; import './style.scss'; -const CardControl = ({label, value, forwardedRef}) => { +const CardControl = ({label = null, value = null, forwardedRef}) => { const [cardNumber, setCardNumber] = useState(value ? value.card_number : ''); const [cardExpiryDate, setCardExpiryDate] = useState( value ? `${value.card_exp_month} \ ${value.card_exp_year}` : '' @@ -124,10 +124,4 @@ CardControl.propTypes = { onChange: PropTypes.func, }; -CardControl.defaultProps = { - label: null, - value: null, - onChange: null, -}; - export default CardControl; diff --git a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/square-control/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/square-control/index.js index 4f757c17c2..af402787b3 100644 --- a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/square-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/square-control/index.js @@ -7,7 +7,7 @@ import './style.scss'; const cardTokenizeResponse = {}; let cardBrand = 'unknown'; -const SquareControl = ({label, value, forwardedRef, gateway}) => { +const SquareControl = ({label = null, value, forwardedRef, gateway}) => { const {applicationID, locationID} = gateway; @@ -78,10 +78,4 @@ SquareControl.propTypes = { onChange: PropTypes.func, }; -SquareControl.defaultProps = { - label: null, - value: null, - onChange: null, -}; - export default SquareControl; diff --git a/src/DonorDashboards/resources/js/app/components/text-control/index.js b/src/DonorDashboards/resources/js/app/components/text-control/index.js index c98aa61459..41198f465a 100644 --- a/src/DonorDashboards/resources/js/app/components/text-control/index.js +++ b/src/DonorDashboards/resources/js/app/components/text-control/index.js @@ -3,7 +3,7 @@ import {toUniqueId} from '../../utils'; import './style.scss'; -const TextControl = ({label, value, onChange, icon, type}) => { +const TextControl = ({label = null, value = '', onChange = null, icon = null, type = 'text'}) => { const id = toUniqueId(label); return ( @@ -21,12 +21,4 @@ const TextControl = ({label, value, onChange, icon, type}) => { ); }; -TextControl.defaultProps = { - label: null, - value: '', - onChange: null, - icon: null, - type: 'text', -}; - export default TextControl; diff --git a/src/DonorDashboards/resources/js/block/color-control/index.js b/src/DonorDashboards/resources/js/block/color-control/index.js index 05540e3a4a..d6d6123936 100644 --- a/src/DonorDashboards/resources/js/block/color-control/index.js +++ b/src/DonorDashboards/resources/js/block/color-control/index.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; const {useInstanceId} = wp.compose; const {BaseControl, ColorPalette} = wp.components; -const ColorControl = ({name, label, help, className, value, hideLabelFromVision, onChange, colors}) => { +const ColorControl = ({name, label, help, className, value, hideLabelFromVision, onChange = null, colors}) => { const instanceId = useInstanceId(ColorControl); const id = `give-color-control-${name}-${instanceId}`; return ( @@ -29,9 +29,4 @@ ColorControl.propTypes = { hideLabelFromVision: PropTypes.bool, }; -ColorControl.defaultProps = { - onChange: null, - options: null, -}; - export default ColorControl; diff --git a/src/MultiFormGoals/resources/js/components/color-control/index.js b/src/MultiFormGoals/resources/js/components/color-control/index.js index 05540e3a4a..d6d6123936 100644 --- a/src/MultiFormGoals/resources/js/components/color-control/index.js +++ b/src/MultiFormGoals/resources/js/components/color-control/index.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; const {useInstanceId} = wp.compose; const {BaseControl, ColorPalette} = wp.components; -const ColorControl = ({name, label, help, className, value, hideLabelFromVision, onChange, colors}) => { +const ColorControl = ({name, label, help, className, value, hideLabelFromVision, onChange = null, colors}) => { const instanceId = useInstanceId(ColorControl); const id = `give-color-control-${name}-${instanceId}`; return ( @@ -29,9 +29,4 @@ ColorControl.propTypes = { hideLabelFromVision: PropTypes.bool, }; -ColorControl.defaultProps = { - onChange: null, - options: null, -}; - export default ColorControl; diff --git a/src/MultiFormGoals/resources/js/components/date-time-control/index.js b/src/MultiFormGoals/resources/js/components/date-time-control/index.js index 4baf604e1d..1cc82b62c9 100644 --- a/src/MultiFormGoals/resources/js/components/date-time-control/index.js +++ b/src/MultiFormGoals/resources/js/components/date-time-control/index.js @@ -11,7 +11,7 @@ const {DateTimePicker, BaseControl, Button, Dropdown} = wp.components; const {__experimentalGetSettings, date} = wp.date; import { __ } from '@wordpress/i18n' -const DateTimeControl = ({name, label, help, className, value, onChange}) => { +const DateTimeControl = ({name, label, help, className, value, onChange = null}) => { const instanceId = useInstanceId(DateTimeControl); const id = `give-date-time-control-${name}-${instanceId}`; const settings = __experimentalGetSettings(); // eslint-disable-line no-restricted-syntax @@ -59,8 +59,4 @@ DateTimeControl.propTypes = { className: PropTypes.string, }; -DateTimeControl.defaultProps = { - onChange: null, -}; - export default DateTimeControl; diff --git a/src/MultiFormGoals/resources/js/components/image-control/index.js b/src/MultiFormGoals/resources/js/components/image-control/index.js index f09e7b9104..0aba95ce7c 100644 --- a/src/MultiFormGoals/resources/js/components/image-control/index.js +++ b/src/MultiFormGoals/resources/js/components/image-control/index.js @@ -11,7 +11,7 @@ const {BaseControl, Button} = wp.components; const {MediaUpload} = wp.blockEditor; import { __ } from '@wordpress/i18n' -const ImageControl = ({name, label, help, className, value, hideLabelFromVision, onChange}) => { +const ImageControl = ({name, label, help, className, value, hideLabelFromVision, onChange = null}) => { const instanceId = useInstanceId(ImageControl); const id = `give-image-control-${name}-${instanceId}`; return ( @@ -53,9 +53,4 @@ ImageControl.propTypes = { hideLabelFromVision: PropTypes.bool, }; -ImageControl.defaultProps = { - onChange: null, - options: null, -}; - export default ImageControl; diff --git a/src/MultiFormGoals/resources/js/components/multi-select-control/index.js b/src/MultiFormGoals/resources/js/components/multi-select-control/index.js index d7b58395f7..ca8f535e95 100644 --- a/src/MultiFormGoals/resources/js/components/multi-select-control/index.js +++ b/src/MultiFormGoals/resources/js/components/multi-select-control/index.js @@ -18,16 +18,16 @@ import './style.scss'; const MultiSelectControl = ({ name, - label, + label = null, help, className, - value, - placeholder, + value = null, + placeholder = `${__('Select', 'give')}...`, hideLabelFromVision, isLoading, isDisabled, - onChange, - options, + onChange = null, + options = null, }) => { const instanceId = useInstanceId(MultiSelectControl); const id = `give-multi-select-control-${name}-${instanceId}`; @@ -76,12 +76,4 @@ MultiSelectControl.propTypes = { placeholder: PropTypes.string, }; -MultiSelectControl.defaultProps = { - label: null, - value: null, - onChange: null, - placeholder: `${__('Select', 'give')}...`, - options: null, -}; - export default MultiSelectControl; diff --git a/src/MultiFormGoals/resources/js/components/server-side-render-x/index.js b/src/MultiFormGoals/resources/js/components/server-side-render-x/index.js index ac4f0f0e81..899c36f736 100644 --- a/src/MultiFormGoals/resources/js/components/server-side-render-x/index.js +++ b/src/MultiFormGoals/resources/js/components/server-side-render-x/index.js @@ -98,7 +98,20 @@ export class ServerSideRenderX extends Component { prevResponseHTML = `
    ${prevResponse}
    `; } - const {className, EmptyResponsePlaceholder, ErrorResponsePlaceholder} = this.props; + const { + className, + EmptyResponsePlaceholder = ({className}) => ( + {__('Block rendered as empty.')} + ), + ErrorResponsePlaceholder = ({response, className}) => { + const errorMessage = sprintf( + // translators: %s: error message describing the problem + __('Error loading block: %s'), + response.errorMsg + ); + return {errorMessage}; + }, + } = this.props; if (response === '') { return ; @@ -122,18 +135,4 @@ export class ServerSideRenderX extends Component { } } -ServerSideRenderX.defaultProps = { - EmptyResponsePlaceholder: ({className}) => ( - {__('Block rendered as empty.')} - ), - ErrorResponsePlaceholder: ({response, className}) => { - const errorMessage = sprintf( - // translators: %s: error message describing the problem - __('Error loading block: %s'), - response.errorMsg - ); - return {errorMessage}; - }, -}; - export default ServerSideRenderX; diff --git a/src/Views/Components/Card/index.js b/src/Views/Components/Card/index.js index ed2d1e6b92..4977fe4380 100644 --- a/src/Views/Components/Card/index.js +++ b/src/Views/Components/Card/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; // Import styles import styles from './style.module.scss'; -const Card = ({width, title, children}) => { +const Card = ({width = 4, title = null, children = null}) => { return (
    {title &&
    {title}
    } @@ -21,10 +21,4 @@ Card.propTypes = { children: PropTypes.node.isRequired, }; -Card.defaultProps = { - width: 4, - title: null, - children: null, -}; - export default Card; diff --git a/src/Views/Components/ListTable/Pagination/index.tsx b/src/Views/Components/ListTable/Pagination/index.tsx index fef67b3345..afa1d5f4fd 100644 --- a/src/Views/Components/ListTable/Pagination/index.tsx +++ b/src/Views/Components/ListTable/Pagination/index.tsx @@ -4,14 +4,16 @@ import styles from './Pagination.module.scss'; import cx from 'classnames'; import {__, sprintf} from '@wordpress/i18n'; -const Pagination = ({currentPage, totalPages, totalItems = -1, disabled, setPage, singleName, pluralName}) => { +const Pagination = ({currentPage = 1, totalPages = 0, totalItems = -1, disabled = false, setPage = (n) => {}, singleName, pluralName}) => { const [pageInput, setPageInput] = useState(1); useEffect(() => { setPageInput(currentPage); }, [currentPage]); + // @ts-ignore const nextPage = parseInt(currentPage) + 1; + // @ts-ignore const previousPage = parseInt(currentPage) - 1; return ( @@ -42,6 +44,7 @@ const Pagination = ({currentPage, totalPages, totalItems = -1, disabled, setPage aria-label={__('previous page')} onClick={(e) => { if (e.currentTarget.getAttribute('aria-disabled') === 'false') { + // @ts-ignore setPage(parseInt(currentPage) - 1); } }} @@ -80,6 +83,7 @@ const Pagination = ({currentPage, totalPages, totalItems = -1, disabled, setPage aria-label={__('next page')} onClick={(e) => { if (e.currentTarget.getAttribute('aria-disabled') === 'false') { + // @ts-ignore setPage(parseInt(currentPage) + 1); } }} @@ -117,11 +121,4 @@ Pagination.propTypes = { disabled: PropTypes.bool.isRequired, }; -Pagination.defaultProps = { - currentPage: 1, - totalPages: 0, - setPage: () => {}, - disabled: false, -}; - export default Pagination; diff --git a/src/Views/Components/Modal/index.js b/src/Views/Components/Modal/index.js index aece6874f7..fdeffd7cbe 100644 --- a/src/Views/Components/Modal/index.js +++ b/src/Views/Components/Modal/index.js @@ -7,7 +7,7 @@ import styles from './styles.module.scss'; import {__} from '@wordpress/i18n'; -const Modal = ({visible, type, children, isLoading, handleClose, ...rest}) => { +const Modal = ({visible = true, type = 'notice', children = {}, isLoading = false, handleClose = () => {}, ...rest}) => { const closeModal = useCallback((event) => { if (event.keyCode === 27 && typeof handleClose === 'function') { handleClose(); @@ -156,12 +156,4 @@ Modal.AdditionalContext.propTypes = { context: PropTypes.any.isRequired, }; -Modal.defaultProps = { - visible: true, - isLoading: false, - type: 'notice', - children: {}, - handleClose: () => {}, -}; - export default Modal; diff --git a/src/Views/Components/Pagination/index.js b/src/Views/Components/Pagination/index.js index 8edaeca2d7..9be6bc49e1 100644 --- a/src/Views/Components/Pagination/index.js +++ b/src/Views/Components/Pagination/index.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import { __ } from '@wordpress/i18n' -const Pagination = ({currentPage, totalPages, disabled, setPage}) => { +const Pagination = ({currentPage = 1, totalPages = 0, disabled = false, setPage = () => {}}) => { if (1 >= totalPages) { return false; } @@ -100,11 +100,4 @@ Pagination.propTypes = { disabled: PropTypes.bool.isRequired, }; -Pagination.defaultProps = { - currentPage: 1, - totalPages: 0, - setPage: () => {}, - disabled: false, -}; - export default Pagination; diff --git a/src/Views/Components/PeriodSelector/index.js b/src/Views/Components/PeriodSelector/index.js index 95c30ddaf2..6822e87467 100644 --- a/src/Views/Components/PeriodSelector/index.js +++ b/src/Views/Components/PeriodSelector/index.js @@ -9,7 +9,7 @@ import 'react-dates/lib/css/_datepicker.css'; import styles from './style.module.scss'; -const PeriodSelector = ({period, setDates}) => { +const PeriodSelector = ({period = {startDate: null, endDate: null}, setDates = () => {}}) => { const [focusedInput, setFocusedInput] = useState(null); const icon = ( @@ -59,12 +59,4 @@ PeriodSelector.propTypes = { setDates: PropTypes.func.isRequired, }; -PeriodSelector.defaultProps = { - period: { - startDate: null, - endDate: null, - }, - setDates: () => {}, -}; - export default PeriodSelector; diff --git a/src/Views/Components/Spinner/index.js b/src/Views/Components/Spinner/index.js index 66bda80094..c149a28da6 100644 --- a/src/Views/Components/Spinner/index.js +++ b/src/Views/Components/Spinner/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import styles from './style.module.scss'; -const Spinner = ({size, ...rest}) => { +const Spinner = ({size = 'small', ...rest}) => { const spinnerClasses = classNames({ [styles.spinner]: true, [styles.large]: size === 'large', @@ -24,8 +24,4 @@ Spinner.propTypes = { size: PropTypes.string, }; -Spinner.defaultProps = { - size: 'small', -}; - export default Spinner; diff --git a/src/Views/Components/Table/index.js b/src/Views/Components/Table/index.js index c41bb2f598..7547d6010c 100644 --- a/src/Views/Components/Table/index.js +++ b/src/Views/Components/Table/index.js @@ -7,7 +7,7 @@ import styles from './style.module.scss'; import {__} from '@wordpress/i18n'; -const Table = ({title, columns, data, columnFilters, stripped, isLoading, isSorting, ...rest}) => { +const Table = ({title = null, columns = [], data = [], columnFilters = {}, stripped = false, isLoading = false, isSorting, ...rest}) => { const [state, setState] = useState({}); const [cachedData, setCachedData] = useState([]); @@ -161,13 +161,4 @@ Table.propTypes = { isLoading: PropTypes.bool, }; -Table.defaultProps = { - title: null, - columns: [], - data: [], - columnFilters: {}, - stripped: true, - isLoading: false, -}; - export default Table; From e2f14ca4f8773ac9a31b06c3cac37c7cc04d2faf Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:05:48 -0700 Subject: [PATCH 108/190] Fix: standardize field error border styling across form templates (#7486) --- .../FormDesigns/ClassicFormDesign/css/_inputs.scss | 6 ------ src/DonationForms/resources/styles/_base-overrides.scss | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss b/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss index e29ab4055e..decc52860a 100644 --- a/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss +++ b/src/DonationForms/FormDesigns/ClassicFormDesign/css/_inputs.scss @@ -43,7 +43,6 @@ input[type="email"], textarea { border-width: 0.078rem; border-style: solid; - border-color: rgb(102, 102, 102); border-radius: 0.25rem; padding: 1.1875rem; width: 100%; @@ -56,11 +55,6 @@ textarea { font-weight: 500; line-height: 1.2; - &[aria-invalid="true"], - &:invalid { - border-color: red; - } - &::placeholder { opacity: 0.6; } diff --git a/src/DonationForms/resources/styles/_base-overrides.scss b/src/DonationForms/resources/styles/_base-overrides.scss index 494eb8e836..dfc9f8f4b8 100644 --- a/src/DonationForms/resources/styles/_base-overrides.scss +++ b/src/DonationForms/resources/styles/_base-overrides.scss @@ -83,10 +83,15 @@ input[type="password"], input[type="email"], input[type="checkbox"], textarea { - border-color: var(--givewp-primary-color); + border-color: rgb(102, 102, 102); &:focus { border-color: transparent; --box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color); } + + &[aria-invalid="true"], + &:invalid { + border-color: red; + } } From 4f6d50313372d71f4f3c629bbe7359f49bd5039f Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Fri, 6 Sep 2024 16:35:34 -0300 Subject: [PATCH 109/190] Enhancement: use give_maybe_safe_unserialize for user data (#7533) Co-authored-by: Jon Waldstein --- includes/process-donation.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index 0125137e1f..6c3e5f3188 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -20,6 +20,7 @@ * Handles the donation form process. * * @access private + * @unreleased Use give_maybe_safe_unserialize() on $user_info data * @since 1.0 * * @throws ReflectionException Exception Handling. @@ -151,12 +152,13 @@ function give_process_donation_form() { ); // Setup donation information. + $user_info = array_map('give_maybe_safe_unserialize', stripslashes_deep( $user_info )); $donation_data = [ 'price' => $price, 'purchase_key' => $purchase_key, 'user_email' => $user['user_email'], 'date' => date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ), - 'user_info' => stripslashes_deep( $user_info ), + 'user_info' => $user_info, 'post_data' => $post_data, 'gateway' => $valid_data['gateway'], 'card_info' => $valid_data['cc_info'], From bf89314bdbb52833dab2f155437745b713e721be Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 6 Sep 2024 15:41:41 -0400 Subject: [PATCH 110/190] chore: prepare for release 3.16.1 --- give.php | 4 ++-- includes/process-donation.php | 2 +- readme.txt | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/give.php b/give.php index 0f832200b3..d675ed1e1a 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.0 + * Version: 3.16.1 * 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.0'); + define('GIVE_VERSION', '3.16.1'); } // Plugin Root File. diff --git a/includes/process-donation.php b/includes/process-donation.php index 6c3e5f3188..f8690f30c3 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -20,7 +20,7 @@ * Handles the donation form process. * * @access private - * @unreleased Use give_maybe_safe_unserialize() on $user_info data + * @since 3.16.1 Use give_maybe_safe_unserialize() on $user_info data * @since 1.0 * * @throws ReflectionException Exception Handling. diff --git a/readme.txt b/readme.txt index 0e8e709369..dee7f5fac2 100644 --- a/readme.txt +++ b/readme.txt @@ -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.1: September 9th, 2024 = +* Security: Added additional protection to the option-based donation form request (CVE-2024-8353) + = 3.16.0: Aug 28th, 2024 = * New: Added support for form taxonomy tags and categories in the visual form builder settings * New: Added a setting to the visual form builder to enable redirecting to an individual donation confirmation page From 7b3d09b85ab772f2cd3322bd094684bb1392798a Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 6 Sep 2024 15:42:16 -0400 Subject: [PATCH 111/190] chore: update stable tag to 3.16.1 --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index dee7f5fac2..f2098c41fb 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.0 +Stable tag: 3.16.1 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html From 2ef2d39b82eaa6d769206446a6f419a628d0817c Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Mon, 9 Sep 2024 17:36:09 -0300 Subject: [PATCH 112/190] fix: array_map missing callback --- includes/admin/admin-actions.php | 6 +++--- includes/process-donation.php | 2 +- src/Helpers/Utils.php | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/includes/admin/admin-actions.php b/includes/admin/admin-actions.php index 81861a1a7c..8e3f6d2264 100644 --- a/includes/admin/admin-actions.php +++ b/includes/admin/admin-actions.php @@ -1,6 +1,7 @@ false]) - : $data; + return Utils::maybeSafeUnserialize($data); } /** diff --git a/includes/process-donation.php b/includes/process-donation.php index f8690f30c3..cef182b944 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -152,7 +152,7 @@ function give_process_donation_form() { ); // Setup donation information. - $user_info = array_map('give_maybe_safe_unserialize', stripslashes_deep( $user_info )); + $user_info = array_map('\Give\Helpers\Utils::maybeSafeUnserialize', stripslashes_deep( $user_info )); $donation_data = [ 'price' => $price, 'purchase_key' => $purchase_key, diff --git a/src/Helpers/Utils.php b/src/Helpers/Utils.php index 9470ab5389..3776c10e8d 100644 --- a/src/Helpers/Utils.php +++ b/src/Helpers/Utils.php @@ -111,4 +111,20 @@ public static function isPluginActive($plugin) return is_plugin_active($plugin); } + + /** + * Avoid insecure usage of `unserialize` when the data could be submitted by the user. + * + * @since 3.16.1 + * + * @param string $data Data that might be unserialized. + * + * @return mixed Unserialized data can be any type. + */ + public static function maybeSafeUnserialize($data) + { + return is_serialized($data) + ? @unserialize(trim($data), ['allowed_classes' => false]) + : $data; + } } From 04eb44dd96673e3f1c916ac02c7009fe23914ab9 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 10 Sep 2024 09:21:10 -0400 Subject: [PATCH 113/190] chore: update readme --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index f2098c41fb..ce7e84e85c 100644 --- a/readme.txt +++ b/readme.txt @@ -262,7 +262,7 @@ 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.1: September 9th, 2024 = += 3.16.1: September 10th, 2024 = * Security: Added additional protection to the option-based donation form request (CVE-2024-8353) = 3.16.0: Aug 28th, 2024 = From ada60729253a132b338c789a5e03c574696b87b1 Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Tue, 17 Sep 2024 07:22:42 -0700 Subject: [PATCH 114/190] Fix: Adjust design selector card height for Safari compatibility (#7530) --- .../resources/js/form-builder/src/styles/_onboarding.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/styles/_onboarding.scss b/src/FormBuilder/resources/js/form-builder/src/styles/_onboarding.scss index 2adb54a267..3865186224 100644 --- a/src/FormBuilder/resources/js/form-builder/src/styles/_onboarding.scss +++ b/src/FormBuilder/resources/js/form-builder/src/styles/_onboarding.scss @@ -119,10 +119,9 @@ } .givewp-design-selector--card { - position: relative; - height: 100%; + height: auto; border-radius: 2px; border: solid 1px var(--givewp-grey-50); From cdf17ebd006d6005147e66723ceba9f3638f5480 Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Tue, 17 Sep 2024 11:57:32 -0300 Subject: [PATCH 115/190] Refactor: replace textarea with rich text editor on Visual Form Builder header description (#7494) --- .../templates/layouts/HeaderDescription.tsx | 4 +++- .../design/general-controls/header/index.tsx | 15 +++++++++++---- .../js/form-builder/src/styles/_components.scss | 6 ++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx b/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx index 3f6a9cd991..d4927bd5e6 100644 --- a/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx +++ b/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx @@ -1,8 +1,10 @@ import type {HeaderDescriptionProps} from '@givewp/forms/propTypes'; +import {Interweave} from 'interweave'; /** + * @unreleased Replace

    tag with Interweave to be able to render the content generated through the ClassicEditor component * @since 3.0.0 */ export default function HeaderDescription({text}: HeaderDescriptionProps) { - return

    {text}

    ; + return ; } diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx index d327a0a0dc..5843333d99 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx @@ -1,9 +1,13 @@ import {__} from '@wordpress/i18n'; -import {PanelBody, PanelRow, SelectControl, TextareaControl, TextControl, ToggleControl} from '@wordpress/components'; +import {PanelBody, PanelRow, SelectControl, TextControl, ToggleControl} from '@wordpress/components'; import {setFormSettings, useFormState} from '@givewp/form-builder/stores/form-state'; import MediaLibrary from '@givewp/form-builder/components/settings/MediaLibrary'; import {upload} from '@wordpress/icons'; +import {ClassicEditor} from '@givewp/form-builder-library'; +/** + * @unreleased Replace TextareaControl component with ClassicEditor component on the description option + */ export default function Header({dispatch, publishSettings}) { const { settings: { @@ -99,10 +103,12 @@ export default function Header({dispatch, publishSettings}) { )} {showDescription && ( - { + content={description} + setContent={(description) => { dispatch( setFormSettings({ description, @@ -112,6 +118,7 @@ export default function Header({dispatch, publishSettings}) { description, }); }} + rows={10} /> )} diff --git a/src/FormBuilder/resources/js/form-builder/src/styles/_components.scss b/src/FormBuilder/resources/js/form-builder/src/styles/_components.scss index b0dacc687f..043ed76b64 100644 --- a/src/FormBuilder/resources/js/form-builder/src/styles/_components.scss +++ b/src/FormBuilder/resources/js/form-builder/src/styles/_components.scss @@ -473,3 +473,9 @@ This creates a consistent separator between the tabs and the rest of the sidebar } } } + +/* Ensure that the link editor modal from the ClassicEditor component will be visible. */ +#wp-link-backdrop, +#wp-link-wrap { + z-index: 9999999999 !important; +} From 538b118ec367afb5cd9c6023e73513536bceaeb6 Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:37:11 -0700 Subject: [PATCH 116/190] Fix: remove extra focus outline in block preview (#7528) --- .../resources/js/form-builder/src/styles/_block-editor.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss b/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss index c2e7452aad..7f1fe0cd38 100644 --- a/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss +++ b/src/FormBuilder/resources/js/form-builder/src/styles/_block-editor.scss @@ -218,6 +218,8 @@ &:not([contenteditable]):focus::after { box-shadow: none; + content: none; + outline-width: 0; } &.is-highlighted { From 2c12fb7ae4fa650c1609fc738478576f21b6b6f8 Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Wed, 18 Sep 2024 22:33:56 +0200 Subject: [PATCH 117/190] Fix: Made string translatable in withButtons.tsx (#7517) --- .../onboarding/steps/filters/withButtons.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/filters/withButtons.tsx b/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/filters/withButtons.tsx index 0a40af8eb1..459b54998a 100644 --- a/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/filters/withButtons.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/filters/withButtons.tsx @@ -1,31 +1,33 @@ +import {__} from '@wordpress/i18n'; + const withButtons = (steps) => { const previous = { classes: 'shepherd-button-secondary', - text: 'Previous', + text: __('Previous', 'give'), type: 'back', }; const next = { classes: 'shepherd-button-primary', - text: 'Next', + text: __('Next', 'give'), type: 'next', }; const nextVariant = { classes: 'shepherd-button-primary', - text: 'Got it', + text: __('Got it', 'give'), type: 'next', }; const complete = { classes: 'shepherd-button-primary', - text: 'Got it', + text: __('Got it', 'give'), type: 'complete', }; const okay = { classes: 'shepherd-button-primary shepherd-button-primary--tools', - text: 'Okay', + text: __('Okay', 'give'), type: 'complete', }; From f630c6923bbf33cc6e31ad7a317b8d62d2b5b369 Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Wed, 18 Sep 2024 22:57:19 +0200 Subject: [PATCH 118/190] Fix: Made the default Chosen placeholder translatable in class-admin-settings.php (#7521) --- includes/admin/class-admin-settings.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/admin/class-admin-settings.php b/includes/admin/class-admin-settings.php index 7b3235ee83..c409c39c9e 100644 --- a/includes/admin/class-admin-settings.php +++ b/includes/admin/class-admin-settings.php @@ -956,6 +956,7 @@ class="give-select-chosen give-chosen-settings" style="" name="" id="" + data-placeholder="" Date: Fri, 20 Sep 2024 12:23:35 -0400 Subject: [PATCH 119/190] Fun: honeypot field (#7485) Co-authored-by: Jon Waldstein --- .../AddHoneyPotFieldToDonationForms.php | 35 ++++++++++++ src/DonationForms/Rules/HoneyPotRule.php | 45 ++++++++++++++++ src/DonationForms/ServiceProvider.php | 17 ++++++ .../resources/app/fields/FieldNode.tsx | 8 ++- .../registrars/templates/fields/Honeypot.tsx | 43 +++++++++++++++ .../resources/registrars/templates/index.ts | 6 +++ src/DonationForms/resources/styles/base.scss | 1 + .../styles/components/_honeypot.scss | 4 ++ src/Framework/FieldsAPI/Honeypot.php | 13 +++++ .../TestAddHoneyPotFieldToDonationForms.php | 39 ++++++++++++++ .../DonationForms/Rules/TestHoneyPotRule.php | 54 +++++++++++++++++++ .../TestTraits/HasValidationRules.php | 54 +++++++++++++++++++ 12 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php create mode 100644 src/DonationForms/Rules/HoneyPotRule.php create mode 100644 src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx create mode 100644 src/DonationForms/resources/styles/components/_honeypot.scss create mode 100644 src/Framework/FieldsAPI/Honeypot.php create mode 100644 tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php create mode 100644 tests/Unit/DonationForms/Rules/TestHoneyPotRule.php create mode 100644 tests/Unit/DonationForms/TestTraits/HasValidationRules.php diff --git a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php new file mode 100644 index 0000000000..58dfb4ab1d --- /dev/null +++ b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php @@ -0,0 +1,35 @@ +all(); + $lastSection = $form->count() ? $formNodes[$form->count() - 1] : null; + + if ($lastSection) { + $field = Honeypot::make('donationBirthday') + ->label('Donation Birthday') + ->scope('honeypot') + ->showInAdmin(false) + ->showInReceipt(false) + ->rules(new HoneyPotRule()); + + $lastSection->append($field); + } + } +} diff --git a/src/DonationForms/Rules/HoneyPotRule.php b/src/DonationForms/Rules/HoneyPotRule.php new file mode 100644 index 0000000000..56eb51209e --- /dev/null +++ b/src/DonationForms/Rules/HoneyPotRule.php @@ -0,0 +1,45 @@ + $values['formId'] ?? null, + ]); + + throw new SpamDonationException(__('Thank you for the submission!', 'give')); + } + } +} diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index 89fde327dc..992a05ddf4 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -3,6 +3,7 @@ namespace Give\DonationForms; use Exception; +use Give\DonationForms\Actions\AddHoneyPotFieldToDonationForms; use Give\DonationForms\Actions\DispatchDonateControllerDonationCreatedListeners; use Give\DonationForms\Actions\DispatchDonateControllerSubscriptionCreatedListeners; use Give\DonationForms\Actions\ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe; @@ -38,6 +39,8 @@ use Give\DonationForms\V2\ListTable\Columns\GoalColumn; use Give\DonationForms\V2\Models\DonationForm; use Give\DonationForms\ValueObjects\DonationFormStatus; +use Give\Framework\FieldsAPI\DonationForm as DonationFormModel; +use Give\Framework\FieldsAPI\Exceptions\EmptyNameException; use Give\Framework\FormDesigns\Registrars\FormDesignRegistrar; use Give\Framework\Migrations\MigrationsRegister; use Give\Framework\Routes\Route; @@ -80,6 +83,7 @@ public function boot() $this->registerShortcodes(); $this->registerPostStatus(); $this->registerAddFormSubmenuLink(); + $this->registerHoneyPotField(); Hooks::addAction('givewp_donation_form_created', StoreBackwardsCompatibleFormMeta::class); Hooks::addAction('givewp_donation_form_updated', StoreBackwardsCompatibleFormMeta::class); @@ -351,4 +355,17 @@ protected function registerPostStatus() register_post_status(DonationFormStatus::UPGRADED); }); } + + /** + * @unreleased + * @throws EmptyNameException + */ + 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); + } + }, 10, 2); + } } diff --git a/src/DonationForms/resources/app/fields/FieldNode.tsx b/src/DonationForms/resources/app/fields/FieldNode.tsx index abd3830d87..d178cce5fe 100644 --- a/src/DonationForms/resources/app/fields/FieldNode.tsx +++ b/src/DonationForms/resources/app/fields/FieldNode.tsx @@ -6,11 +6,17 @@ import memoNode from '@givewp/forms/app/utilities/memoNode'; const formTemplates = window.givewp.form.templates; +const excludeFromTemplateWrapper = ['hidden', 'honeypot']; + +/** + * @unreleased added excludeFromTemplateWrapper + * @since 3.0.0 + */ function FieldNode({node}: {node: Field}) { const {register} = window.givewp.form.hooks.useFormContext(); const {errors} = window.givewp.form.hooks.useFormState(); const Field = - node.type !== 'hidden' + !excludeFromTemplateWrapper.includes(node.type) ? useTemplateWrapper(formTemplates.fields[node.type], 'div', node.name) : formTemplates.fields[node.type]; const fieldProps = registerFieldAndBuildProps(node, register, errors); diff --git a/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx b/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx new file mode 100644 index 0000000000..f890d5abdf --- /dev/null +++ b/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx @@ -0,0 +1,43 @@ +import {FieldHasDescriptionProps} from '@givewp/forms/propTypes'; +import {useEffect} from 'react'; +import {__} from '@wordpress/i18n'; + +/** + * @unreleased + */ +export default function Honeypot({ + Label, + ErrorMessage, + fieldError, + description, + placeholder, + inputProps + }: FieldHasDescriptionProps) { + const FieldDescription = window.givewp.form.templates.layouts.fieldDescription; + const Wrapper = window.givewp.form.templates.layouts.wrapper; + const {setError, clearErrors} = window.givewp.form.hooks.useFormContext(); + + useEffect(() => { + // relocate the field error to a form error if the field error is present + if (fieldError) { + clearErrors(inputProps.name); + + setError('FORM_ERROR', { + message: __('Something went wrong, please try again or contact support.', 'give') + }); + } + + }, [fieldError]); + + return ( + + + ); +} diff --git a/src/DonationForms/resources/registrars/templates/index.ts b/src/DonationForms/resources/registrars/templates/index.ts index 1074914ebf..fa672d2afd 100644 --- a/src/DonationForms/resources/registrars/templates/index.ts +++ b/src/DonationForms/resources/registrars/templates/index.ts @@ -37,7 +37,12 @@ import MultiStepForm from './layouts/MultiStepForm'; import DonationSummaryItems from './layouts/DonationSummaryItems'; import FormError from './layouts/FormError'; import HeaderImage from './layouts/HeaderImage'; +import Honeypot from '@givewp/forms/registrars/templates/fields/Honeypot'; +/** + * @unreleased added Honeypot + * @since 3.0.0 + */ const defaultFormTemplates = { fields: { amount: AmountField, @@ -56,6 +61,7 @@ const defaultFormTemplates = { phone: PhoneField, file: FileField, url: UrlField, + honeypot: Honeypot, }, elements: { paragraph: Paragraph, diff --git a/src/DonationForms/resources/styles/base.scss b/src/DonationForms/resources/styles/base.scss index 11bbcef8fe..9a903028d7 100644 --- a/src/DonationForms/resources/styles/base.scss +++ b/src/DonationForms/resources/styles/base.scss @@ -24,6 +24,7 @@ @import "components/pagination"; @import "components/billingAddress"; @import "components/form-errors"; + @import "components/honeypot"; } @layer base-overrides { diff --git a/src/DonationForms/resources/styles/components/_honeypot.scss b/src/DonationForms/resources/styles/components/_honeypot.scss new file mode 100644 index 0000000000..9fdeb33b07 --- /dev/null +++ b/src/DonationForms/resources/styles/components/_honeypot.scss @@ -0,0 +1,4 @@ +.givewp-fields-badger { + position: absolute; + left: -9999px; +} diff --git a/src/Framework/FieldsAPI/Honeypot.php b/src/Framework/FieldsAPI/Honeypot.php new file mode 100644 index 0000000000..28a2fed16d --- /dev/null +++ b/src/Framework/FieldsAPI/Honeypot.php @@ -0,0 +1,13 @@ +append(Section::make('section-1'), Section::make('section-2'), Section::make('section-3')); + $action = new AddHoneyPotFieldToDonationForms(); + $action($formNode, 1); + + + /** @var Section $lastSection */ + $lastSection = $formNode->getNodeByName('section-3'); + $this->assertNotNull($lastSection->getNodeByName('donationBirthday')); + $this->assertInstanceOf(Honeypot::class, $lastSection->getNodeByName('donationBirthday')); + } +} diff --git a/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php b/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php new file mode 100644 index 0000000000..2085b24077 --- /dev/null +++ b/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php @@ -0,0 +1,54 @@ +expectException(SpamDonationException::class); + } + + $rule = new HoneyPotRule(); + + self::assertValidationRulePassed($rule, $value); + } + + /** + * @unreleased + * + * @return array> + */ + public function honeyPotProvider(): array + { + return [ + // Valid + ['', true], + [null, true], + + // Invalid + ['123', false], + ['anything', false], + [123, false], + [['123'], false], + [[123], false], + ]; + } +} diff --git a/tests/Unit/DonationForms/TestTraits/HasValidationRules.php b/tests/Unit/DonationForms/TestTraits/HasValidationRules.php new file mode 100644 index 0000000000..6c3708838d --- /dev/null +++ b/tests/Unit/DonationForms/TestTraits/HasValidationRules.php @@ -0,0 +1,54 @@ + Date: Tue, 24 Sep 2024 11:21:15 -0400 Subject: [PATCH 120/190] Enhancement: add additional check for stripslashes_deep in serialized function (#7535) Co-authored-by: Jon Waldstein --- includes/process-donation.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/includes/process-donation.php b/includes/process-donation.php index cef182b944..7caa84379f 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -417,6 +417,7 @@ function give_donation_form_validate_fields() { /** * Detect serialized fields. * + * @unreleased added additional check for stripslashes_deep * @since 3.14.2 add give-form-title, give_title * @since 3.5.0 */ @@ -450,6 +451,10 @@ function give_donation_form_has_serialized_fields(array $post_data): bool continue; } + if (is_serialized(stripslashes_deep($value))) { + return true; + } + if (is_serialized($value)) { return true; } From e409b782988bb411a9a6d098a6efdf1ba4559f8f Mon Sep 17 00:00:00 2001 From: Joshua Dinh <75056371+JoshuaHungDinh@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:54:07 -0700 Subject: [PATCH 121/190] Fix: Adjust Anonymous block styles for WP <= 6.6 (#7529) --- .../form-builder/src/blocks/fields/anonymous/styles.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss index 31b443a8f2..4a2fd2d91e 100644 --- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss +++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/anonymous/styles.scss @@ -7,9 +7,12 @@ } .components-base-control__help { - margin-left: 2rem; font-size: 0.875rem; - line-height: 1rem; + margin: 0.25rem 0 0 calc(var(--checkbox-input-size, 20px) + 12px);; color: var(--givewp-grey-700); + + .components-checkbox-control__help { + margin-inline-start: 0; + } } } From c4fb6a0cd5863c13f6ea3fd577a98c363f36c5e6 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 24 Sep 2024 18:21:15 -0400 Subject: [PATCH 122/190] chore: prepare for 3.16.2 --- give.php | 4 ++-- includes/process-donation.php | 2 +- readme.txt | 11 ++++++++++- .../Actions/AddHoneyPotFieldToDonationForms.php | 4 ++-- src/DonationForms/Rules/HoneyPotRule.php | 8 ++++---- src/DonationForms/ServiceProvider.php | 2 +- src/DonationForms/resources/app/fields/FieldNode.tsx | 2 +- .../registrars/templates/fields/Honeypot.tsx | 2 +- .../resources/registrars/templates/index.ts | 2 +- .../templates/layouts/HeaderDescription.tsx | 2 +- .../Routes/RegisterFormBuilderPageRoute.php | 2 +- src/FormBuilder/ServiceProvider.php | 2 +- .../src/components/settings/InspectorNotice/index.tsx | 2 +- .../settings/design/general-controls/header/index.tsx | 2 +- src/Framework/FieldsAPI/Honeypot.php | 2 +- .../Actions/EnqueueOfflineFormBuilderScripts.php | 2 +- .../Actions/TestAddHoneyPotFieldToDonationForms.php | 4 ++-- tests/Unit/DonationForms/Rules/TestHoneyPotRule.php | 6 +++--- .../DonationForms/TestTraits/HasValidationRules.php | 6 +++--- tests/Unit/FormBuilder/ServiceProviderTest.php | 4 ++-- 20 files changed, 40 insertions(+), 31 deletions(-) diff --git a/give.php b/give.php index d675ed1e1a..e0b1dd93d2 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.1 + * Version: 3.16.2 * 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.1'); + define('GIVE_VERSION', '3.16.2'); } // Plugin Root File. diff --git a/includes/process-donation.php b/includes/process-donation.php index 7caa84379f..08c5ef73a9 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -417,7 +417,7 @@ function give_donation_form_validate_fields() { /** * Detect serialized fields. * - * @unreleased added additional check for stripslashes_deep + * @since 3.16.2 added additional check for stripslashes_deep * @since 3.14.2 add give-form-title, give_title * @since 3.5.0 */ diff --git a/readme.txt b/readme.txt index ce7e84e85c..981ac39af6 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.1 +Stable tag: 3.16.2 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -262,6 +262,15 @@ 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.2: September 24th, 2024 = +* Enhancement: Updated the visual builder header description field to use the rich text editor +* Enhancement: Updated the strings in the form builder onboarding buttons to be translatable (Open source submission by @DAnn2012) +* Enhancement: Updated strings in give settings to be translatable (Open source submission by @DAnn2012) +* Security: Added additional prevention for serialized data in the option-based donation form request +* Fix: Resolved a styling issue with some text fields not respecting error border styling +* Fix: Resolved a styling issue with the anonymous block for WP 6.6 compatibility +* Dev: Removed defaultProps in favor of ES6 default parameters for React 19 compatibility + = 3.16.1: September 10th, 2024 = * Security: Added additional protection to the option-based donation form request (CVE-2024-8353) diff --git a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php index 58dfb4ab1d..14b16c5e85 100644 --- a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php +++ b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php @@ -8,12 +8,12 @@ use Give\Framework\FieldsAPI\Honeypot; /** - * @unreleased + * @since 3.16.2 */ class AddHoneyPotFieldToDonationForms { /** - * @unreleased + * @since 3.16.2 * @throws EmptyNameException */ public function __invoke(DonationForm $form): void diff --git a/src/DonationForms/Rules/HoneyPotRule.php b/src/DonationForms/Rules/HoneyPotRule.php index 56eb51209e..38cb3a2127 100644 --- a/src/DonationForms/Rules/HoneyPotRule.php +++ b/src/DonationForms/Rules/HoneyPotRule.php @@ -7,13 +7,13 @@ use Give\Vendors\StellarWP\Validation\Contracts\ValidationRule; /** - * @unreleased + * @since 3.16.2 */ class HoneyPotRule implements ValidationRule { /** - * @unreleased + * @since 3.16.2 */ public static function id(): string { @@ -21,7 +21,7 @@ public static function id(): string } /** - * @unreleased + * @since 3.16.2 */ public static function fromString(string $options = null): ValidationRule { @@ -29,7 +29,7 @@ public static function fromString(string $options = null): ValidationRule } /** - * @unreleased + * @since 3.16.2 * @throws SpamDonationException */ public function __invoke($value, Closure $fail, string $key, array $values) diff --git a/src/DonationForms/ServiceProvider.php b/src/DonationForms/ServiceProvider.php index 992a05ddf4..28b36adbb6 100644 --- a/src/DonationForms/ServiceProvider.php +++ b/src/DonationForms/ServiceProvider.php @@ -357,7 +357,7 @@ protected function registerPostStatus() } /** - * @unreleased + * @since 3.16.2 * @throws EmptyNameException */ private function registerHoneyPotField(): void diff --git a/src/DonationForms/resources/app/fields/FieldNode.tsx b/src/DonationForms/resources/app/fields/FieldNode.tsx index d178cce5fe..e28f2e8eff 100644 --- a/src/DonationForms/resources/app/fields/FieldNode.tsx +++ b/src/DonationForms/resources/app/fields/FieldNode.tsx @@ -9,7 +9,7 @@ const formTemplates = window.givewp.form.templates; const excludeFromTemplateWrapper = ['hidden', 'honeypot']; /** - * @unreleased added excludeFromTemplateWrapper + * @since 3.16.2 added excludeFromTemplateWrapper * @since 3.0.0 */ function FieldNode({node}: {node: Field}) { diff --git a/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx b/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx index f890d5abdf..ebadddfa63 100644 --- a/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx +++ b/src/DonationForms/resources/registrars/templates/fields/Honeypot.tsx @@ -3,7 +3,7 @@ import {useEffect} from 'react'; import {__} from '@wordpress/i18n'; /** - * @unreleased + * @since 3.16.2 */ export default function Honeypot({ Label, diff --git a/src/DonationForms/resources/registrars/templates/index.ts b/src/DonationForms/resources/registrars/templates/index.ts index fa672d2afd..328f29dfd9 100644 --- a/src/DonationForms/resources/registrars/templates/index.ts +++ b/src/DonationForms/resources/registrars/templates/index.ts @@ -40,7 +40,7 @@ import HeaderImage from './layouts/HeaderImage'; import Honeypot from '@givewp/forms/registrars/templates/fields/Honeypot'; /** - * @unreleased added Honeypot + * @since 3.16.2 added Honeypot * @since 3.0.0 */ const defaultFormTemplates = { diff --git a/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx b/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx index d4927bd5e6..37d99f151a 100644 --- a/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx +++ b/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx @@ -2,7 +2,7 @@ import type {HeaderDescriptionProps} from '@givewp/forms/propTypes'; import {Interweave} from 'interweave'; /** - * @unreleased Replace

    tag with Interweave to be able to render the content generated through the ClassicEditor component + * @since 3.16.2 Replace

    tag with Interweave to be able to render the content generated through the ClassicEditor component * @since 3.0.0 */ export default function HeaderDescription({text}: HeaderDescriptionProps) { diff --git a/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php b/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php index 077fab9612..b61ace3ead 100644 --- a/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php +++ b/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php @@ -158,7 +158,7 @@ public function renderPage() ]); /** - * @unreleased + * @since 3.16.2 */ wp_localize_script('@givewp/form-builder/script', 'additionalPaymentGatewaysNotificationData', [ 'actionUrl' => admin_url('admin-ajax.php?action=givewp_additional_payment_gateways_hide_notice'), diff --git a/src/FormBuilder/ServiceProvider.php b/src/FormBuilder/ServiceProvider.php index 68e9985f53..8d78b43b88 100644 --- a/src/FormBuilder/ServiceProvider.php +++ b/src/FormBuilder/ServiceProvider.php @@ -90,7 +90,7 @@ protected function setupOnboardingTour() }); /** - * @unreleased + * @since 3.16.2 */ add_action('wp_ajax_givewp_additional_payment_gateways_hide_notice', static function () { add_user_meta(get_current_user_id(), 'givewp-additional-payment-gateways-notice-dismissed', time(), true); diff --git a/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx index 0c3e52a0ec..b0e3725bb7 100644 --- a/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/components/settings/InspectorNotice/index.tsx @@ -3,7 +3,7 @@ import {close, external} from "@wordpress/icons"; import './styles.scss' /** - * @unreleased + * @since 3.16.2 */ const InspectorNotice = ({title, description, helpText, helpUrl, onDismiss}) => { diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx index 5843333d99..b64c9de519 100644 --- a/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx +++ b/src/FormBuilder/resources/js/form-builder/src/settings/design/general-controls/header/index.tsx @@ -6,7 +6,7 @@ import {upload} from '@wordpress/icons'; import {ClassicEditor} from '@givewp/form-builder-library'; /** - * @unreleased Replace TextareaControl component with ClassicEditor component on the description option + * @since 3.16.2 Replace TextareaControl component with ClassicEditor component on the description option */ export default function Header({dispatch, publishSettings}) { const { diff --git a/src/Framework/FieldsAPI/Honeypot.php b/src/Framework/FieldsAPI/Honeypot.php index 28a2fed16d..156c278019 100644 --- a/src/Framework/FieldsAPI/Honeypot.php +++ b/src/Framework/FieldsAPI/Honeypot.php @@ -3,7 +3,7 @@ namespace Give\Framework\FieldsAPI; /** - * @unreleased + * @since 3.16.2 */ class Honeypot extends Field { diff --git a/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php b/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php index 827aaa11f9..684f1b0a95 100644 --- a/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php +++ b/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php @@ -9,7 +9,7 @@ class EnqueueOfflineFormBuilderScripts /** * Enqueues the Stripe scripts and styles for the Form Builder. * - * @unreleased On the "offlineEnabled" option check if the offline gateway is enabled for v3 forms instead of v2 forms + * @since 3.16.2 On the "offlineEnabled" option check if the offline gateway is enabled for v3 forms instead of v2 forms * * @return void */ diff --git a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php index b0e73c3115..fbee568eab 100644 --- a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php +++ b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php @@ -13,14 +13,14 @@ use Give\Tests\TestTraits\RefreshDatabase; /** - * @unreleased + * @since 3.16.2 */ class TestAddHoneyPotFieldToDonationForms extends TestCase { use RefreshDatabase; /** - * @unreleased + * @since 3.16.2 * @throws NameCollisionException|EmptyNameException|TypeNotSupported */ public function testShouldAddHoneyPotFieldToDonationForms(): void diff --git a/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php b/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php index 2085b24077..c4db7135f4 100644 --- a/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php +++ b/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php @@ -9,7 +9,7 @@ use Give\Tests\Unit\DonationForms\TestTraits\HasValidationRules; /** - * @unreleased + * @since 3.16.2 */ class TestHoneyPotRule extends TestCase { @@ -17,7 +17,7 @@ class TestHoneyPotRule extends TestCase use HasValidationRules; /** - * @unreleased + * @since 3.16.2 * @dataProvider honeyPotProvider */ public function testHoneyPotRule($value, bool $shouldBeValid): void @@ -32,7 +32,7 @@ public function testHoneyPotRule($value, bool $shouldBeValid): void } /** - * @unreleased + * @since 3.16.2 * * @return array> */ diff --git a/tests/Unit/DonationForms/TestTraits/HasValidationRules.php b/tests/Unit/DonationForms/TestTraits/HasValidationRules.php index 6c3708838d..c1cf3e3f72 100644 --- a/tests/Unit/DonationForms/TestTraits/HasValidationRules.php +++ b/tests/Unit/DonationForms/TestTraits/HasValidationRules.php @@ -5,13 +5,13 @@ use Give\Vendors\StellarWP\Validation\Contracts\ValidationRule; /** - * @unreleased + * @since 3.16.2 */ trait HasValidationRules { /** * Asserts that a given validation rule passes. * - * @unreleased + * @since 3.16.2 * * @param mixed $value */ @@ -39,7 +39,7 @@ public static function assertValidationRulePassed( /** * Asserts that a given validation rule fails. * - * @unreleased + * @since 3.16.2 * * @param mixed $value */ diff --git a/tests/Unit/FormBuilder/ServiceProviderTest.php b/tests/Unit/FormBuilder/ServiceProviderTest.php index 4c9e81eb4f..cb295dfb17 100644 --- a/tests/Unit/FormBuilder/ServiceProviderTest.php +++ b/tests/Unit/FormBuilder/ServiceProviderTest.php @@ -6,14 +6,14 @@ use Give\Tests\TestTraits\RefreshDatabase; /** - * @unreleased + * @since 3.16.2 */ class ServiceProviderTest extends TestCase { use RefreshDatabase; /** - * @unreleased + * @since 3.16.2 */ public function testItDismissesTheAdditionalPaymentGatewaysNotice() { From cabf5a38c51f598b2c57f927c530c618d00ff5c7 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 24 Sep 2024 18:23:32 -0400 Subject: [PATCH 123/190] docs: update changelog date --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 981ac39af6..906b86ba88 100644 --- a/readme.txt +++ b/readme.txt @@ -262,7 +262,7 @@ 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.2: September 24th, 2024 = += 3.16.2: September 25th, 2024 = * Enhancement: Updated the visual builder header description field to use the rich text editor * Enhancement: Updated the strings in the form builder onboarding buttons to be translatable (Open source submission by @DAnn2012) * Enhancement: Updated strings in give settings to be translatable (Open source submission by @DAnn2012) From dc42aacbeee76cfa5cc6f00a3fdc839b8226fd85 Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Wed, 25 Sep 2024 16:08:38 -0400 Subject: [PATCH 124/190] Chore: Update donor query order parameter (#7550) --- includes/donors/class-give-donors-query.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/donors/class-give-donors-query.php b/includes/donors/class-give-donors-query.php index 107e2b7175..dc2a7e8f4c 100644 --- a/includes/donors/class-give-donors-query.php +++ b/includes/donors/class-give-donors-query.php @@ -481,14 +481,19 @@ private function get_order_query() { // Create query. foreach ( $ordersby as $orderby => $order ) { + /** + * @unreleased Prevent SQL Injection by not using the user defined order value directly in the query. + */ + $sanitizedOrder = $order === 'ASC' ? 'ASC' : 'DESC'; + switch ( $table_columns[ $orderby ] ) { case '%d': case '%f': - $query[] = "{$this->table_name}.{$orderby}+0 {$order}"; + $query[] = "{$this->table_name}.{$orderby}+0 {$sanitizedOrder}"; break; default: - $query[] = "{$this->table_name}.{$orderby} {$order}"; + $query[] = "{$this->table_name}.{$orderby} {$sanitizedOrder}"; } } From d4084a778ff3e33451df99556369c78fe362586f Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 25 Sep 2024 16:13:24 -0400 Subject: [PATCH 125/190] chore: update readme --- includes/donors/class-give-donors-query.php | 2 +- readme.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/donors/class-give-donors-query.php b/includes/donors/class-give-donors-query.php index dc2a7e8f4c..57e0008da5 100644 --- a/includes/donors/class-give-donors-query.php +++ b/includes/donors/class-give-donors-query.php @@ -482,7 +482,7 @@ private function get_order_query() { // Create query. foreach ( $ordersby as $orderby => $order ) { /** - * @unreleased Prevent SQL Injection by not using the user defined order value directly in the query. + * @since 3.16.2 Prevent SQL Injection by not using the user defined order value directly in the query. */ $sanitizedOrder = $order === 'ASC' ? 'ASC' : 'DESC'; diff --git a/readme.txt b/readme.txt index 906b86ba88..a08298abe2 100644 --- a/readme.txt +++ b/readme.txt @@ -267,6 +267,7 @@ The 2% fee on Stripe donations only applies to donations taken via our free Stri * Enhancement: Updated the strings in the form builder onboarding buttons to be translatable (Open source submission by @DAnn2012) * Enhancement: Updated strings in give settings to be translatable (Open source submission by @DAnn2012) * Security: Added additional prevention for serialized data in the option-based donation form request +* Security: Added additional security measures to the legacy donor list table request (CVE-2024-9130) * Fix: Resolved a styling issue with some text fields not respecting error border styling * Fix: Resolved a styling issue with the anonymous block for WP 6.6 compatibility * Dev: Removed defaultProps in favor of ES6 default parameters for React 19 compatibility From 57b8faeb949a1c2c538e16b1b7d643e43c0b4935 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 4 Oct 2024 17:36:38 -0400 Subject: [PATCH 126/190] chore: prepare for release 3.16.3 --- give.php | 4 ++-- readme.txt | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/give.php b/give.php index e0b1dd93d2..62a75e5cb9 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.2 + * Version: 3.16.3 * 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.2'); + define('GIVE_VERSION', '3.16.3'); } // Plugin Root File. diff --git a/readme.txt b/readme.txt index a08298abe2..46e1c91634 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.2 +Stable tag: 3.16.3 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.3: October 7th, 2024 = +* Security: Added additional validation to the donor title field, further protecting the option-based donation form request + = 3.16.2: September 25th, 2024 = * Enhancement: Updated the visual builder header description field to use the rich text editor * Enhancement: Updated the strings in the form builder onboarding buttons to be translatable (Open source submission by @DAnn2012) From 5ab4e64c5193d5cd223a3ebfe3637eefbb68efe6 Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Fri, 4 Oct 2024 18:37:00 -0300 Subject: [PATCH 127/190] Enhancement: add additional validations for name title prefix field (#7563) Co-authored-by: Jon Waldstein --- includes/process-donation.php | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index 08c5ef73a9..c53ec32a37 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -341,6 +341,7 @@ function give_process_form_login() { function give_donation_form_validate_fields() { $post_data = give_clean( $_POST ); // WPCS: input var ok, sanitization ok, CSRF ok. + give_donation_form_validate_name_fields($post_data); // Validate Honeypot First. if ( ! empty( $post_data['give-honeypot'] ) ) { @@ -1643,16 +1644,28 @@ function give_validate_required_form_fields( $form_id ) { * * @param array $post_data List of post data. * + * @unreleased Add additional validations for name title prefix field * @since 2.1 * * @return void */ 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'] ) ); + $formId = absint( $post_data['give-form-id'] ); - if ( ! $is_alpha_first_name || ( ! empty( $post_data['give_last'] ) && ! $is_alpha_last_name ) ) { - give_set_error( 'invalid_name', esc_html__( 'The First Name and Last Name fields cannot contain an email address or numbers.', 'give' ) ); - } + if (!give_is_name_title_prefix_enabled($formId) && isset($post_data['give_title'])) { + give_set_error( 'disabled_name_title', esc_html__( 'The name title prefix 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' ) ); + } + + $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'] ) ); + + 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 a981575be0f1055771797af331e4b3feacad7465 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 4 Oct 2024 17:37:34 -0400 Subject: [PATCH 128/190] chore: update since tags --- includes/process-donation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/process-donation.php b/includes/process-donation.php index c53ec32a37..41be334326 100644 --- a/includes/process-donation.php +++ b/includes/process-donation.php @@ -1644,7 +1644,7 @@ function give_validate_required_form_fields( $form_id ) { * * @param array $post_data List of post data. * - * @unreleased Add additional validations for name title prefix field + * @since 3.16.3 Add additional validations for name title prefix field * @since 2.1 * * @return void From 23412c577454d51fc72649b76a4cad2ec58837e6 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 9 Oct 2024 16:25:46 -0400 Subject: [PATCH 129/190] 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 130/190] 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 131/190] 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 132/190] 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 133/190] 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 134/190] 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 135/190] 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 136/190] 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 137/190] 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 138/190] 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 139/190] 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 140/190] 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 141/190] 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 142/190] 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 143/190] 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 144/190] 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 145/190] 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 146/190] 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 147/190] 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 148/190] 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 149/190] 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 150/190] 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 151/190] 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 152/190] 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 153/190] 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 154/190] 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 155/190] 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 156/190] 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 157/190] 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 158/190] 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 159/190] 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 174/190] 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 175/190] 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 176/190] 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 177/190] 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 178/190] 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 179/190] 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 180/190] 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
    > > - - > + + - - + + is_role( 'give_donor' ) ) && ! empty( $give_donor ) ? 'give_donor' : 'subscriber' ); @@ -366,9 +414,41 @@ public function sanitize_option_donor_default_user_role($value) { } } } + return $value; } - } + + /** + * @unreleased + */ + public function _render_give_based_form_editor_notice($field, $value) + { + if (OptionBasedFormEditor::isEnabled()) { + ?> + > + + + + ' . __('Enabled Gateways', 'give') . ''; echo '
    '; - echo '
    '; - echo '
      '; - foreach ($groups as $slug => $group) { - $current_group = !empty($_GET['group']) ? give_clean($_GET['group']) : $defaultGroup; - $active_class = ($slug === $current_group) ? 'active' : ''; - - if ($group['helper']) { - $helper = sprintf( - '
      - -
      - -
      %3$s
      -

      %4$s

      -
      -
      ', - esc_url(GIVE_PLUGIN_URL . 'assets/dist/images/admin/help-circle.svg'), - esc_url($group['helper']['image']), + + if (count($groups) > 1) { + echo '
      '; + echo '
        '; + foreach ($groups as $slug => $group) { + $current_group = ! empty($_GET['group']) ? give_clean($_GET['group']) : $defaultGroup; + $active_class = ($slug === $current_group) ? 'active' : ''; + + if ($group['helper']) { + $helper = sprintf( + '
        + +
        + +
        %3$s
        +

        %4$s

        +
        +
        ', + esc_url(GIVE_PLUGIN_URL . 'assets/dist/images/admin/help-circle.svg'), + esc_url($group['helper']['image']), + esc_html($group['label']), + esc_html($group['helper']['text']) + ); + } + + echo sprintf( + '
      • %4$s %5$s
      • ', + esc_html($active_class), + esc_url( + admin_url( + "edit.php?post_type=give_forms&page={$current_page}&tab={$current_tab}§ion={$current_section}&group={$slug}" + ) + ), + esc_html($slug), esc_html($group['label']), - esc_html($group['helper']['text']) + $helper ?? '' ); } - - echo sprintf( - '
      • %4$s %5$s
      • ', - esc_html($active_class), - esc_url( - admin_url( - "edit.php?post_type=give_forms&page={$current_page}&tab={$current_tab}§ion={$current_section}&group={$slug}" - ) - ), - esc_html($slug), - esc_html($group['label']), - $helper ?? '' - ); + echo '
      '; + echo '
      '; } - echo '
    '; - echo '
    '; echo '
    '; foreach ($groups as $slug => $group) : @@ -622,10 +631,10 @@ class="give-payment-gateway-settings-dialog__close"
    -
    - diff --git a/includes/admin/settings/class-settings-general.php b/includes/admin/settings/class-settings-general.php index 6990a498d6..f8ea838453 100644 --- a/includes/admin/settings/class-settings-general.php +++ b/includes/admin/settings/class-settings-general.php @@ -667,7 +667,7 @@ public function _render_give_currency_preview($field, $value) + echo wp_kses_post($field['name']); ?>
    {title}
    - +
    - - - - + /** + * Render remove_cache_button field type + * + * @since 2.25.2 add nonce field + * @since 2.1 + * @access public + * + * @param array $field + * + */ + public function render_remove_cache_button($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 181/190] 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 182/190] 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 183/190] 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 184/190] 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 185/190] 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}) => { )} -