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
diff --git a/assets/src/css/admin/settings.scss b/assets/src/css/admin/settings.scss
index a5707b5931..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 {
@@ -1050,18 +1133,18 @@ a.give-delete {
}
// copied from wp built-in .menu-counter
.givewp-beta-icon {
- display: inline-block;
- vertical-align: top;
- box-sizing: border-box;
- margin: 1px 0 -1px 2px;
- padding: 0 5px;
- min-width: 18px;
- height: 18px;
- border-radius: 9px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 2px 0 0px 0px;
+ padding: 2px 8px;
+ border-radius: 0.75rem;
background-color: #F29718;
color: #fff;
font-size: 11px;
- line-height: 1.6;
+ font-weight: 600;
+ font-family: 'Inter' ,sans-serif;
+ line-height: 0.9625rem;
text-align: center;
z-index: 26;
}
@@ -1072,8 +1155,17 @@ a.give-delete {
.give-admin-beta-features-message {
background-color: #fff;
- padding: 0.5rem;
+ padding: 1rem;
border: 1px solid #F29718;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ border-radius: 2px;
+ color: #0E0E0E;
+ font-size: 0.875rem;
+ font-family: 'Inter' ,sans-serif;
+ font-weight: 400;
+ line-height: 1.5rem;
}
.give-admin-beta-features-feedback-link {
diff --git a/assets/src/css/admin/setup.scss b/assets/src/css/admin/setup.scss
index 36a1f5bca6..be04b96252 100644
--- a/assets/src/css/admin/setup.scss
+++ b/assets/src/css/admin/setup.scss
@@ -9,201 +9,244 @@
*/
.give_forms_page_give-setup .wp-header-end {
- margin-bottom: 20px;
+ margin-bottom: 20px;
}
.wp-heading-inline {
- font-family: 'Open Sans', sans-serif;
+ font-family: 'Open Sans', sans-serif;
}
section,
section * {
- font-family: 'Open Sans', sans-serif;
+ font-family: 'Open Sans', sans-serif;
}
section {
- margin-bottom: 40px;
- color: #424242;
- background-color: #fff;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.09);
- border-radius: 6px;
- overflow: hidden;
+ margin-bottom: 40px;
+ color: #424242;
+ background-color: #fff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.09);
+ border-radius: 6px;
+ overflow: hidden;
}
.section-dismiss {
- text-align: center;
+ text-align: center;
- button {
- cursor: pointer;
- border: 0;
- background-color: inherit;
- color: #9ea3a8;
- font-size: 16px;
- font-weight: 600;
- text-decoration: underline;
- font-family: 'Open Sans', sans-serif;
- }
+ button {
+ cursor: pointer;
+ border: 0;
+ background-color: inherit;
+ color: #9ea3a8;
+ font-size: 16px;
+ font-weight: 600;
+ text-decoration: underline;
+ font-family: 'Open Sans', sans-serif;
+ }
}
header {
- padding: 20px 20px 20px 25px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #ddd;
-
- h2 {
- margin: 0;
- font-size: 22px;
- line-height: 22px;
- color: #424242;
- }
+ padding: 1.25rem 1.5rem;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ border-bottom: 1px solid #ddd;
+
+ &.current-step h2 {
+ color: #0E0E0E;
+ font-weight: 700;
+ }
+
+ h2 {
+ margin: 0;
+ font-size: 22px;
+ font-weight: 500;
+ line-height: 1.55;
+ color: #424242;
+ }
+
+ .badge {
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 22px;
+ padding: 0.25rem 0.5rem;
+ border-radius: 10px;
+
+ &.badge-completed {
+ color: #18694C;
+ background: #CEF2CF;
+ }
+
+ &.badge-not-completed {
+ color: #404040;
+ background: #F2F2F2;
+ }
+
+ &.badge-optional {
+ color: #0B72D9;
+ background: #F2F9FF;
+ }
+ }
+
+ .button.button-primary {
+ border-radius: 0.25rem;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1.5;
+ margin-left: auto;
+ padding: 0.5rem 1rem;
+ }
}
+
footer {
- background-color: #fafafa;
- padding: 20px 20px 20px 25px;
- font-size: 16px;
- display: flex;
- justify-content: space-between;
-}
-footer a {
- color: #0073aa;
- text-decoration: none;
+ background-color: #fafafa;
+ padding: 1.25rem 1.5rem;
+ font-size: 16px;
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ gap: 0.25rem;
- > .fa {
- margin-left: 6px;
- }
-}
-header .badge {
- font-weight: 600;
- font-size: 12px;
- line-height: 16px;
- padding: 10px 20px;
- border-radius: 3px;
-}
-header .badge.badge-complete {
- color: #18694c;
- background: #cbf4c9;
-}
-header .badge.badge-review {
- color: #696969;
- border: #ddd 1px solid;
- background: transparent;
+ p {
+ font-size: 1rem;
+ line-height: 1.5;
+ margin: 0;
+ }
+
+ a {
+ text-decoration: none;
+ margin-left: auto;
+
+ > .fa {
+ margin-left: 6px;
+ }
+ }
}
article {
- padding: 20px 0;
- border-bottom: 1px solid #ddd;
- display: grid;
- grid-template-columns: 125px 3fr 1fr;
- grid-template-areas: 'icon content action';
+ padding: 20px 0;
+ border-bottom: 1px solid #ddd;
+ display: grid;
+ grid-template-columns: 125px 3fr 1fr;
+ grid-template-areas: 'icon content action';
}
+
article .icon,
article .action {
- align-self: center;
- justify-content: center;
+ align-self: center;
+ justify-content: center;
}
+
article .icon {
- margin: 8px auto 0;
- align-self: start;
- flex: 1;
- width: 75px;
- grid-area: icon;
-}
-article .action a {
- color: inherit;
- font-size: 22px;
- text-align: right;
- margin-right: 70px;
- text-decoration: none;
-
- /*
- * Float the anchor to avoid expanding the focus state.
- */
- float: right;
+ margin: 8px auto 0;
+ align-self: start;
+ flex: 1;
+ width: 75px;
+ grid-area: icon;
+}
+
+article .action {
+ margin-left: auto;
+ padding: 1.5rem;
+
+ a {
+ font-size: 1rem;
+ line-height: 1.5;
+ text-decoration: none;
+
+ i {
+ margin-left: 6px;
+ }
+ }
}
+
article .content {
- grid-area: content;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding-right: 24px;
+ grid-area: content;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-right: 24px;
}
+
article .header {
- margin: 10px 0 10px; /* Enforce minimum spacing via bottom-margin to prevent crowding by grid-layout. */
- font-size: 20px;
- line-height: 25px;
+ margin: 10px 0 10px; /* Enforce minimum spacing via bottom-margin to prevent crowding by grid-layout. */
+ font-size: 20px;
+ line-height: 25px;
}
+
article .description {
- grid-area: description;
- color: #595959;
- font-size: 16px;
- line-height: 1.5;
- font-weight: 400;
+ grid-area: description;
+ color: #595959;
+ font-size: 16px;
+ line-height: 1.5;
+ font-weight: 400;
}
.configuration .icon {
- margin-top: -25px;
- margin-left: 15px;
+ margin-top: -25px;
+ margin-left: 15px;
}
article.paypal .action a,
article.paypal .action button,
article.stripe .action a,
article.stripe .action button {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-left: 15px;
- margin-right: 30px;
- width: 223px;
- height: 38px;
- font-size: 16px;
- font-weight: 600;
- line-height: 35px;
- text-align: center;
- color: #fff;
- border: 0;
- border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 15px;
+ width: 223px;
+ height: 38px;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 35px;
+ text-align: center;
+ color: #fff;
+ border: 0;
+ border-radius: 6px;
}
article.paypal .action a,
article.paypal .action button {
- background-color: #0070bc;
+ background-color: #0070bc;
}
article.stripe .action a,
article.stripe .action button {
- background: #6772e5;
+ background: #6772e5;
}
.stripe-webhooks {
- .fa-spinner {
- margin-right: 10px;
- }
+ .fa-spinner {
+ margin-right: 10px;
+ }
- .stripe-webhooks-url {
- cursor: pointer;
- display: inline-block;
- margin-top: 10px;
+ .stripe-webhooks-url {
+ cursor: pointer;
+ display: inline-block;
+ margin-top: 10px;
- input {
- width: 300px;
- margin-left: -2px; /* Align input with sibling p.description */
- border-color: transparent;
- outline-color: transparent;
- }
- }
+ input {
+ width: 300px;
+ margin-left: -2px; /* Align input with sibling p.description */
+ border-color: transparent;
+ outline-color: transparent;
+ }
+ }
}
.setup-item-sub-header {
- display: block; // Override inherited grid layout.
- padding-left: 20px;
+ font-size: 14px;
+ display: block; // Override inherited grid layout.
+ padding-left: 20px;
+
+ a {
+ text-decoration: none;
+ }
}
.setup-item-completed .header {
- text-decoration: line-through;
- text-decoration-color: rgba(66, 66, 66, 0.75);
+ text-decoration-color: rgba(66, 66, 66, 0.75);
}
/** Normalize icon sizes. */
@@ -214,9 +257,115 @@ article.setup-item-pdf-receipts .icon,
article.setup-item-currency-switcher .icon,
article.setup-item-recurring-donations .icon,
article.setup-item-form-fields-manager .icon {
- width: 70px;
+ width: 70px;
}
.hidden {
- display: none;
+ display: none;
+}
+
+#give-activate-license-modal {
+ animation: appear 112ms ease-in 0s;
+ background-color: #fff;
+ border: 0;
+ border-radius: 0.25rem;
+ box-shadow: 0 0.25rem 0.5rem 0 rgba(14, 14, 14, 0.15);
+ color: #404040;
+ font-family: "Open Sans", sans-serif;
+ max-width: 35rem;
+ padding: 0;
+ position: relative;
+ width: 100%;
+
+ &::backdrop {
+ background-color: rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(2px);
+ }
+
+ #give-license-activator-wrap {
+ padding: 0;
+ }
+
+ .give-license-widget-heading {
+ align-items: center;
+ border-bottom: 1px solid #E6E6E6;
+ color: #0e0e0e;
+ display: flex;
+ justify-content: space-between;
+ margin: 0;
+ padding: 1rem 1.25rem;
+
+ h2 {
+ font-size: 1rem;
+ font-weight: 700;
+ margin: 0;
+ }
+
+ button {
+ all: unset;
+ cursor: pointer;
+ fill: #737373;
+ }
+ }
+
+ .give-license-widget-content {
+ padding: 1rem 1.5rem 1.5rem;
+
+ .give-field-description {
+ margin-bottom: 1rem;
+
+ a {
+ text-decoration: underline;
+ }
+ }
+
+ .give-license-activation-form {
+ background: none;
+ padding: 0;
+
+ .give-license-notices {
+ .notice {
+ left: 1rem;
+ margin: 0;
+ position: absolute;
+ right: 1rem;
+ top: 50%;
+ translate: 0 -50%;
+ }
+ }
+
+ label {
+ color: #404040;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1.5;
+ margin-bottom: 0.25rem;
+ }
+
+ #give-license-activator {
+ border-radius: 0.25rem;
+ border: 1px solid #8C8C8C;
+ background: #FFF;
+ padding: 0.75rem 1rem;
+ font-size: 1rem;
+ height: auto;
+ line-height: 1.5;
+ margin-bottom: 2.5rem;
+
+ &::placeholder {
+ color: #8C8C8C;
+ }
+ }
+
+ .button {
+ border-radius: 0.25rem;
+ font-size: 1rem;
+ font-weight: 600;
+ height: auto;
+ line-height: 1.5;
+ padding: 0.75rem 2rem;
+ }
+ }
+ }
}
diff --git a/assets/src/images/admin/onboarding/header-image.jpg b/assets/src/images/admin/onboarding/header-image.jpg
new file mode 100644
index 0000000000..25c6061f4e
Binary files /dev/null and b/assets/src/images/admin/onboarding/header-image.jpg differ
diff --git a/assets/src/images/admin/paypal-logo.png b/assets/src/images/admin/paypal-logo.png
new file mode 100644
index 0000000000..0e3bb21a5b
Binary files /dev/null and b/assets/src/images/admin/paypal-logo.png differ
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/assets/src/images/setup-page/payment-gateway.svg b/assets/src/images/setup-page/payment-gateway.svg
new file mode 100644
index 0000000000..3a9b4ce2e6
--- /dev/null
+++ b/assets/src/images/setup-page/payment-gateway.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/assets/src/js/admin/admin-scripts.js b/assets/src/js/admin/admin-scripts.js
index e43e2e6bd6..3c0e916217 100644
--- a/assets/src/js/admin/admin-scripts.js
+++ b/assets/src/js/admin/admin-scripts.js
@@ -3149,7 +3149,7 @@ const gravatar = require('gravatar');
orderedOptions.push({
text: option.textContent,
value: option.value,
- selected: false,
+ selected: option.selected,
});
}
});
diff --git a/assets/src/js/admin/admin-setup.js b/assets/src/js/admin/admin-setup.js
index e7bf5afc18..5a80f5e774 100644
--- a/assets/src/js/admin/admin-setup.js
+++ b/assets/src/js/admin/admin-setup.js
@@ -7,16 +7,51 @@
* @link https://css-tricks.com/block-links-the-search-for-a-perfect-solution/
*/
-Array.from( document.querySelectorAll( '.setup-item' ) ).forEach( ( setupItem ) => {
- const actionAnchor = setupItem.querySelector( '.js-action-link' );
-
- if ( actionAnchor ) {
- actionAnchor.addEventListener( 'click', ( e ) => e.stopPropagation() );
- setupItem.style.cursor = 'pointer';
- setupItem.addEventListener( 'click', ( event ) => { // eslint-disable-line no-unused-vars
- if ( ! window.getSelection().toString() ) {
- actionAnchor.click();
- }
- } );
- }
-} );
+Array.from(document.querySelectorAll('.setup-item')).forEach((setupItem) => {
+ const actionAnchor = setupItem.querySelector('.js-action-link');
+
+ if (actionAnchor) {
+ actionAnchor.addEventListener('click', (e) => e.stopPropagation());
+ setupItem.style.cursor = 'pointer';
+ setupItem.addEventListener('click', (event) => {
+ // eslint-disable-line no-unused-vars
+ if (!window.getSelection().toString()) {
+ actionAnchor.click();
+ }
+ });
+ }
+});
+
+document.addEventListener('DOMContentLoaded', () => {
+ const trigger = document.querySelector('a.activate-license');
+ const dialog = document.querySelector('#give-activate-license-modal');
+ const dialogContent = dialog.querySelector('#give-license-activator-wrap');
+ const closeButton = dialog.querySelector('.givewp-modal-close');
+ const input = dialog.querySelector('input[type="text"]');
+ const submitButton = dialog.querySelector('input[type="submit"]');
+
+ trigger.addEventListener('click', (e) => {
+ e.preventDefault();
+ dialog.showModal();
+ });
+
+ dialog.addEventListener('click', () => {
+ dialog.close();
+ });
+
+ closeButton.addEventListener('click', () => {
+ dialog.close();
+ });
+
+ dialogContent.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ input.addEventListener('input', () => {
+ if (input.value) {
+ submitButton.removeAttribute('disabled');
+ } else {
+ submitButton.setAttribute('disabled', 'disabled');
+ }
+ });
+});
diff --git a/assets/src/js/admin/onboarding-wizard/app/index.js b/assets/src/js/admin/onboarding-wizard/app/index.js
index aa898a2a09..a6b4669958 100644
--- a/assets/src/js/admin/onboarding-wizard/app/index.js
+++ b/assets/src/js/admin/onboarding-wizard/app/index.js
@@ -1,9 +1,9 @@
// Import vendor dependencies
-import { __ } from '@wordpress/i18n'
+import {__} from '@wordpress/i18n';
// Import store dependencies
-import { StoreProvider } from './store';
-import { reducer } from './store/reducer';
+import {StoreProvider} from './store';
+import {reducer} from './store/reducer';
// Import styles
import './style.scss';
@@ -21,14 +21,14 @@ import DonationForm from './steps/donation-form';
import Addons from './steps/addons';
import {
- getCountryList,
- getDefaultStateList,
- getCurrencyList,
- getFeaturesEnabledDefault,
- getAddonsSelectedDefault,
- getDefaultCountry,
- getDefaultState,
- getDefaultCurrency,
+ getAddonsSelectedDefault,
+ getCountryList,
+ getCurrencyList,
+ getDefaultCountry,
+ getDefaultCurrency,
+ getDefaultState,
+ getDefaultStateList,
+ getFeaturesEnabledDefault,
} from '../utils';
/**
@@ -38,70 +38,72 @@ import {
* @returns {array} Array of React elements, comprising the Onboarding Wizard app
*/
const App = () => {
- // Initial app state (available in component through useStoreValue)
- const initialState = {
- currentStep: 0,
- lastStep: 5,
- configuration: {
- userType: 'individual',
- causeType: '',
- country: getDefaultCountry(),
- state: getDefaultState(),
- currency: getDefaultCurrency(),
- features: getFeaturesEnabledDefault(),
- addons: getAddonsSelectedDefault(),
- },
- countriesList: getCountryList(),
- currenciesList: getCurrencyList(),
- statesList: getDefaultStateList(),
- fetchingStatesList: false,
- };
+ // Initial app state (available in component through useStoreValue)
+ const initialState = {
+ currentStep: 0,
+ lastStep: 5,
+ configuration: {
+ userType: 'individual',
+ causeType: '',
+ usageTracking: true,
+ newsletterSubscription: true,
+ country: getDefaultCountry(),
+ state: getDefaultState(),
+ currency: getDefaultCurrency(),
+ features: getFeaturesEnabledDefault(),
+ addons: getAddonsSelectedDefault(),
+ },
+ countriesList: getCountryList(),
+ currenciesList: getCurrencyList(),
+ statesList: getDefaultStateList(),
+ fetchingStatesList: false,
+ };
- const steps = [
- {
- title: __( 'Introduction', 'give' ),
- component: ,
- showInNavigation: false,
- },
- {
- title: __( 'Cause', 'give' ),
- component: ,
- showInNavigation: true,
- },
- {
- title: __( 'Location', 'give' ),
- component: ,
- showInNavigation: true,
- },
- {
- title: __( 'Features', 'give' ),
- component: ,
- showInNavigation: true,
- },
- {
- title: __( 'Preview', 'give' ),
- component: ,
- showInNavigation: true,
- },
- {
- title: __( 'Add-ons', 'give' ),
- component: ,
- showInNavigation: true,
- },
- ];
+ const steps = [
+ {
+ title: __('Introduction', 'give'),
+ component: ,
+ showInNavigation: false,
+ },
+ {
+ title: __('Cause', 'give'),
+ component: ,
+ showInNavigation: true,
+ },
+ {
+ title: __('Location', 'give'),
+ component: ,
+ showInNavigation: true,
+ },
+ {
+ title: __('Features', 'give'),
+ component: ,
+ showInNavigation: true,
+ },
+ {
+ title: __('Preview', 'give'),
+ component: ,
+ showInNavigation: true,
+ },
+ {
+ title: __('Add-ons', 'give'),
+ component: ,
+ showInNavigation: true,
+ },
+ ];
- return (
-
-
- { steps.map( ( step, index ) => {
- return (
-
- { step.component }
-
- );
- } ) }
-
-
- );
+ return (
+
+
+ {steps.map((step, index) => {
+ return (
+
+ {step.component}
+
+ );
+ })}
+
+
+ );
};
export default App;
diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/addons/index.js b/assets/src/js/admin/onboarding-wizard/app/steps/addons/index.js
index ca19f6bba0..5a3b29f2fc 100644
--- a/assets/src/js/admin/onboarding-wizard/app/steps/addons/index.js
+++ b/assets/src/js/admin/onboarding-wizard/app/steps/addons/index.js
@@ -1,14 +1,15 @@
// Import vendor dependencies
-import { __ } from '@wordpress/i18n'
+import {__} from '@wordpress/i18n';
// Import store dependencies
-import { useStoreValue } from '../../store';
-import { setAddons } from '../../store/actions';
+import {useStoreValue} from '../../store';
+import {setAddons} from '../../store/actions';
// Import components
import Card from '../../../components/card';
import CardInput from '../../../components/card-input';
import ContinueButton from '../../../components/continue-button';
+import PreviousButton from '../../../components/previous-button';
import RecurringDonationsIcon from '../../../components/icons/recurring-donations';
import DonorsCoverFeesIcon from '../../../components/icons/donors-cover-fees';
import PDFReceiptsIcon from '../../../components/icons/pdf-receipts';
@@ -20,44 +21,51 @@ import DedicateDonationsIcon from '../../../components/icons/dedicate-donations'
import './style.scss';
const Addons = () => {
- const [ { configuration }, dispatch ] = useStoreValue();
- const addons = configuration.addons;
+ const [{configuration}, dispatch] = useStoreValue();
+ const addons = configuration.addons;
- return (
-
>
-
+
diff --git a/includes/misc-functions.php b/includes/misc-functions.php
index 9b4faac62a..ab879cc18d 100644
--- a/includes/misc-functions.php
+++ b/includes/misc-functions.php
@@ -9,9 +9,10 @@
* @since 1.0
*/
-// Exit if accessed directly.
+use Give\DonationForms\AsyncData\AsyncDataHelpers;
use Give\License\PremiumAddonsListManager;
+// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -1927,10 +1928,12 @@ function give_get_nonce_field( $action, $name, $referer = false ) {
/**
* Display/Return a formatted goal for a donation form
*
+ * @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.
*
* @return array
- * @since 2.1
*/
function give_goal_progress_stats( $form ) {
@@ -1942,7 +1945,6 @@ function give_goal_progress_stats( $form ) {
/**
* Filter the form.
- *
* @since 1.8.8
*/
$total_goal = apply_filters( 'give_goal_amount_target_output', round( give_maybe_sanitize_amount( $form->goal ), 2 ), $form->ID, $form );
@@ -1970,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', $form->earnings, $form->ID, $form );
+ /**
+ * Filter the form income.
+ *
+ * @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
+ */
+ $actual = apply_filters( 'give_goal_amount_raised_output', $form->earnings, $form->ID, $form );
break;
}
@@ -2017,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/includes/payments/actions.php b/includes/payments/actions.php
index 7cc64e11a5..c30ffefe1b 100644
--- a/includes/payments/actions.php
+++ b/includes/payments/actions.php
@@ -265,6 +265,7 @@ function give_refresh_thismonth_stat_transients( $payment_ID ) {
* Add support to get all payment meta.
* Note: only use for internal purpose
*
+ * @unreleased change $donor_data['address'] to array instead of false
* @since 2.0
*
* @param $check
@@ -362,7 +363,7 @@ function ( &$meta, $key ) {
// User ID.
$donor_data['id'] = $donation->user_id;
- $donor_data['address'] = false;
+ $donor_data['address'] = [];
// Address1.
$address1 = ! empty( $payment_meta['_give_donor_billing_address1'] ) ? $payment_meta['_give_donor_billing_address1'] : '';
diff --git a/includes/payments/functions.php b/includes/payments/functions.php
index ad44c830ae..61e8fcb66c 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.
*
+ * @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.
@@ -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 = '';
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/includes/process-donation.php b/includes/process-donation.php
index e2a46dab35..994de9effd 100644
--- a/includes/process-donation.php
+++ b/includes/process-donation.php
@@ -9,6 +9,8 @@
* @since 1.0
*/
+use Give\Helpers\Utils;
+
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -20,6 +22,7 @@
* Handles the donation form process.
*
* @access private
+ * @since 3.16.1 Use give_maybe_safe_unserialize() on $user_info data
* @since 1.0
*
* @throws ReflectionException Exception Handling.
@@ -151,12 +154,13 @@ function give_process_donation_form() {
);
// Setup donation information.
+ $user_info = array_map('\Give\Helpers\Utils::maybeSafeUnserialize', 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'],
@@ -339,6 +343,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'] ) ) {
@@ -415,37 +420,18 @@ function give_donation_form_validate_fields() {
/**
* Detect serialized fields.
*
+ * @since 3.17.2 Use Utils::isSerialized() method which add supports to find hidden serialized data in the middle of a string
+ * @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
* @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',
- ];
-
- foreach ($post_data as $key => $value) {
- if ( ! in_array($key, $post_data_keys, true)) {
- continue;
- }
+ foreach ($post_data as $value) {
- if (is_serialized($value)) {
+ if (Utils::isSerialized($value)) {
return true;
}
}
@@ -1633,16 +1619,34 @@ function give_validate_required_form_fields( $form_id ) {
*
* @param array $post_data List of post data.
*
+ * @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
*
* @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_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' ) );
+ }
+
+ $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 = ( 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' ) );
+ }
}
diff --git a/includes/shortcodes.php b/includes/shortcodes.php
index 3bf909c376..ef200f5123 100644
--- a/includes/shortcodes.php
+++ b/includes/shortcodes.php
@@ -10,6 +10,7 @@
*/
// Exit if accessed directly.
+use Give\DonationForms\DonationQuery;
use Give\Helpers\Form\Template\Utils\Frontend as FrontendFormTemplateUtils;
use Give\Helpers\Form\Utils as FormUtils;
use Give\Helpers\Frontend\ConfirmDonation;
@@ -338,6 +339,7 @@ function give_register_form_shortcode( $atts ) {
*
* Shows a donation receipt.
*
+ * @since 3.16.0 add give_donation_confirmation_page_enqueue_scripts
* @since 3.7.0 Sanitize and escape attributes
* @since 1.0
*
@@ -389,13 +391,15 @@ function give_receipt_shortcode( $atts ) {
if ( ! wp_doing_ajax() ) {
give_get_template_part( 'receipt/placeholder' );
- return sprintf(
+ do_action('give_donation_confirmation_page_enqueue_scripts');
+
+ return apply_filters('give_receipt_shortcode_output', sprintf(
'%4$s
',
htmlspecialchars( wp_json_encode( $give_receipt_args ) ),
esc_attr($receipt_type),
esc_attr($donation_id),
ob_get_clean()
- );
+ ));
}
return give_display_donation_receipt( $atts );
@@ -629,6 +633,7 @@ function give_process_profile_editor_updates( $data ) {
*
* Shows a donation total.
*
+ * @since 3.14.0 Replace "_give_form_earnings" form meta with $query->form($post)->sumAmount()
* @since 3.7.0 Sanitize attributes
* @since 2.1
*
@@ -738,7 +743,8 @@ static function ($id) {
if ( isset( $forms->posts ) ) {
$total = 0;
foreach ( $forms->posts as $post ) {
- $form_earning = give_get_meta( $post, '_give_form_earnings', true );
+ $query = new DonationQuery();
+ $form_earning = $query->form($post)->sumAmount();
$form_earning = ! empty( $form_earning ) ? $form_earning : 0;
/**
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/readme.txt b/readme.txt
index 0f41a209e8..daf8ff3cce 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.3
-Tested up to: 6.5
+Requires at least: 6.5
+Tested up to: 6.7
Requires PHP: 7.2
-Stable tag: 3.12.3
+Stable tag: 3.19.0
License: GPLv3
License URI: http://www.gnu.org/licenses/gpl-3.0.html
@@ -167,7 +167,7 @@ Here’s a few ways you can contribute to GiveWP:
= Minimum Requirements =
-* WordPress 6.3 or greater
+* WordPress 6.5 or greater
* PHP version 7.2 or greater
* MySQL version 5.7 or greater
* MariaDB version 10 or later
@@ -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,118 @@ 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.19.0: December 5th, 2024 =
+* New: Added support to the donor dashboard for managing recurring donations from our Blink Payment Gateway add-on
+* Fix: Resolved a compatability issue with loading translations on WordPress 6.7
+* Security: Added sanitization to the manual migrations parameters
+
+
+= 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
+* 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
+* Fix: Resolved an issue where Stripe Payment Element was causing an error when donation amount is zero
+* Security: Removed Faker PHP library from production to prevent malicious direct access
+* Security: Further improved our data sanitization and validation across all of GiveWP to prevent malicious serialized data
+* Dev: Resolved php 8.1 compatibility warnings for Give_Addon_Activation_Banner, Give_License, and CurrencySwitcherSetting classes
+
+= 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 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
+* 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
+* 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)
+
+= 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)
+* 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
+
+= 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 =
+* 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
+* 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: 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
+
+= 3.15.1: Aug 22nd, 2024 =
+* Fix: Resolved an issue with the Akismet integration preventing form submissions when settings are not yet configured
+
+= 3.15.0: Aug 14th, 2024 =
+* New: Added Akismet integration support to forms using the visual form builder
+* New: Updated the onboarding wizard to create a new form with the visual form builder
+* Changed: Updated the "Add Form" buttons to use the visual form builder by default
+* Fix: Resolved an issue with Give Subscribers accessing their donor dashboard history
+
+= 3.14.2: Aug 7th, 2024 =
+* Security: Added additional security measures to the option-based donation form and the donor dashboard (CVE-2024-37099)
+
+= 3.14.1: July 24th, 2024 =
+* Fix: Resolved an error with the give_totals shortcode when using multiple form IDs
+
+= 3.14.0: July 17th, 2024 =
+* Enhancement: Updated the visual donation form builder with various UI design improvements
+* Enhancement: Updated the form builder design tab preview to be more responsive
+* Enhancement: Improved the design of single active gateways on forms
+* Enhancement: Improved the login block design
+* Enhancement: Improved the Terms & Conditions block UI
+* Enhancement: Improved the donate button hover state & secure donation tag
+* Enhancement: Improved the donor title prefix setting styles
+* Enhancement: Improved the checkbox style for form builder Build & Design screens
+* Enhancement: Improved the Consent block by removing "Link Text" option when "Show terms in form" display type is selected
+* Enhancement: Improved the File Upload field interactivity to limit the button scope
+* Fix: Resolved an issue with the drag and drop block placement in the form builder
+* Fix: Resolved an issue where the Give Goal and Multi-Form Goal blocks and shortcodes were displaying the wrong donation amount
+* Fix: Resolved an issue when exporting donations that use Razorpay gateway
+* Fix: Resolved an issue in the form builder where recurring donations descriptions were not always matching frequency selection
+* Fix: Resolved an issue with custom donor columns in csv exports and revive filter give_export_donors_get_default_columns
+* Security: Resolved various security issues related to user permissions
+
+= 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
+* 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/BetaFeatures/ServiceProvider.php b/src/BetaFeatures/ServiceProvider.php
index 6d63d14c47..bacd4e9e24 100644
--- a/src/BetaFeatures/ServiceProvider.php
+++ b/src/BetaFeatures/ServiceProvider.php
@@ -40,7 +40,7 @@ public function boot(): void
});
add_action('give_admin_field_beta_features', function(){
- echo sprintf('', __('Beta features are a way to get early access to new features. These features are functional but will be updated frequently. Updates may include changes to the feature settings, admin screens, design and database.', 'give'));
+ echo sprintf('BETA %s
', __('Beta features are a way to get early access to new features. These features are functional but will be updated frequently. Updates may include changes to the feature settings, admin screens, design and database.', 'give'));
});
add_action('give_admin_field_beta_features_feedback_link', function () {
diff --git a/src/Controller/Form.php b/src/Controller/Form.php
index 3a14c9b566..2ec7290601 100644
--- a/src/Controller/Form.php
+++ b/src/Controller/Form.php
@@ -62,6 +62,7 @@ public function loadTemplateOnFrontend()
/**
* Load receipt view.
*
+ * @since 3.16.0 add action give_donation_confirmation_page_enqueue_scripts
* @since 2.7.0
*/
public function loadReceiptView()
@@ -71,6 +72,8 @@ public function loadReceiptView()
return;
}
+ do_action('give_donation_confirmation_page_enqueue_scripts');
+
// Handle success page.
if (FormUtils::isViewingFormReceipt() && ! FormUtils::isLegacyForm()) {
/* @var Template $formTemplate */
@@ -113,7 +116,6 @@ public function loadReceiptView()
include $formTemplate->getReceiptView();
exit();
}
-
// Render receipt on success page in iframe.
add_filter('the_content', [$this, 'showReceiptInIframeOnSuccessPage'], 1);
}
@@ -164,6 +166,7 @@ public function setFailedTransactionError()
/**
* Handle receipt shortcode on success page
*
+ * @since 3.16.0 add filter give_donation_confirmation_success_page_shortcode_view
* @since 2.7.0
*
* @param string $content
@@ -173,9 +176,10 @@ public function setFailedTransactionError()
public function showReceiptInIframeOnSuccessPage($content)
{
$receiptShortcode = ShortcodeUtils::getReceiptShortcodeFromConfirmationPage();
- $content = str_replace($receiptShortcode, give_form_shortcode([]), $content);
- return $content;
+ $view = apply_filters('give_donation_confirmation_success_page_shortcode_view', give_form_shortcode([]));
+
+ return str_replace($receiptShortcode, $view, $content);
}
/**
diff --git a/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php
new file mode 100644
index 0000000000..c4d45963eb
--- /dev/null
+++ b/src/DonationForms/Actions/AddHoneyPotFieldToDonationForms.php
@@ -0,0 +1,44 @@
+all();
+ $lastSection = $form->count() ? $formNodes[$form->count() - 1] : null;
+
+ if ($lastSection && is_null($form->getNodeByName($honeypotFieldName))) {
+ $field = Honeypot::make($honeypotFieldName)
+ ->label($this->generateLabelFromFieldName($honeypotFieldName))
+ ->scope('honeypot')
+ ->showInAdmin(false)
+ ->showInReceipt(false)
+ ->rules(new HoneyPotRule());
+
+ $lastSection->append($field);
+ }
+ }
+
+ /**
+ * @since 3.17.0
+ */
+ private function generateLabelFromFieldName(string $honeypotFieldName): string
+ {
+ return ucwords(trim(implode(" ", preg_split("/(?=[A-Z])/", $honeypotFieldName))));
+ }
+}
diff --git a/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php b/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php
index 34c18dd32a..67a11112aa 100644
--- a/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php
+++ b/src/DonationForms/Actions/ConvertDonationFormBlocksToFieldsApi.php
@@ -261,6 +261,8 @@ protected function createNodeFromBlockWithUniqueAttributes(BlockModel $block, in
}
/**
+ * @since 3.17.0 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/DonationForms/Actions/DispatchDonateControllerDonationCreatedListeners.php b/src/DonationForms/Actions/DispatchDonateControllerDonationCreatedListeners.php
index 7e1f26ffda..34fe4b18b9 100644
--- a/src/DonationForms/Actions/DispatchDonateControllerDonationCreatedListeners.php
+++ b/src/DonationForms/Actions/DispatchDonateControllerDonationCreatedListeners.php
@@ -25,4 +25,4 @@ public function __invoke(DonateControllerData $formData, Donation $donation, ?Su
(new AddRedirectUrlsToGatewayData())($formData, $donation);
(new UpdateDonationLevelId())($formData->getDonationForm(), $donation);
}
-}
\ No newline at end of file
+}
diff --git a/src/DonationForms/Actions/PrintFormMetaTags.php b/src/DonationForms/Actions/PrintFormMetaTags.php
new file mode 100644
index 0000000000..877496ef51
--- /dev/null
+++ b/src/DonationForms/Actions/PrintFormMetaTags.php
@@ -0,0 +1,32 @@
+post_type) &&
+ $post->post_type === 'give_forms'
+ && Utils::isV3Form($post->ID)
+ ) {
+ /** @var $form $form */
+ $form = DonationForm::find($post->ID);
+
+ // og:image
+ if ($form && !empty($form->settings->designSettingsImageUrl)) {
+ printf(' ', esc_url($form->settings->designSettingsImageUrl));
+ }
+ }
+ }
+}
diff --git a/src/DonationForms/Actions/ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe.php b/src/DonationForms/Actions/ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe.php
new file mode 100644
index 0000000000..f3a81bce06
--- /dev/null
+++ b/src/DonationForms/Actions/ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe.php
@@ -0,0 +1,25 @@
+receiptId) {
+ $viewUrl = (new GenerateDonationConfirmationReceiptViewRouteUrl())($data->receiptId);
+ return "";
+ }
+
+ return $view;
+ }
+}
diff --git a/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php b/src/DonationForms/AsyncData/Actions/GetAsyncFormDataForListView.php
new file mode 100644
index 0000000000..adbede9c82
--- /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);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ private function isAsyncProgressBar(): bool
+ {
+ return AdminFormListViewOptions::isGoalColumnAsync() || FormGridViewOptions::isProgressBarAmountRaisedAsync();
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ private function isAsyncDonationCount(): bool
+ {
+ return AdminFormListViewOptions::isDonationColumnAsync() || FormGridViewOptions::isProgressBarDonationsCountAsync();
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ 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..339bcc3e0e
--- /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;
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ 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..6a83c90201
--- /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,
+ ]
+ );
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ 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..c17521d3d8
--- /dev/null
+++ b/src/DonationForms/AsyncData/AdminFormListView/AdminFormListView.php
@@ -0,0 +1,69 @@
+ [$formId], 'statusList' => ['any']]))->getDonationCount();
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public static function getFormRevenueValue($formId): int
+ {
+ return (new DonationQuery())->form($formId)->sumIntendedAmount();
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ 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..d060c14d68
--- /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.
+ *
+ * @since 3.16.0
+ */
+ 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.
+ *
+ * @since 3.16.0
+ */
+ 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.
+ *
+ * @since 3.16.0
+ */
+ 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.
+ *
+ * @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
+ 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.
+ *
+ * @since 3.16.0
+ */
+ 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.
+ *
+ * @since 3.16.0
+ */
+ 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.
+ *
+ * @since 3.16.0
+ */
+ 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.
+ *
+ * @since 3.16.0
+ */
+ 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/Blocks/DonationFormBlock/Controllers/BlockRenderController.php b/src/DonationForms/Blocks/DonationFormBlock/Controllers/BlockRenderController.php
index 81462124ff..4279f412fd 100644
--- a/src/DonationForms/Blocks/DonationFormBlock/Controllers/BlockRenderController.php
+++ b/src/DonationForms/Blocks/DonationFormBlock/Controllers/BlockRenderController.php
@@ -104,14 +104,6 @@ private function getFormViewUrl(DonationForm $donationForm): string
*/
protected function loadEmbedScript()
{
- (new EnqueueScript(
- 'givewp-donation-form-embed',
- 'build/donationFormEmbed.js',
- GIVE_PLUGIN_DIR,
- GIVE_PLUGIN_URL,
- 'give'
- ))->loadInFooter()->enqueue();
-
(new EnqueueScript(
'givewp-donation-form-embed-app',
'build/donationFormBlockApp.js',
diff --git a/src/DonationForms/Blocks/DonationFormBlock/resources/app/Components/ModalForm.tsx b/src/DonationForms/Blocks/DonationFormBlock/resources/app/Components/ModalForm.tsx
index 397c1bf908..22d7e0026d 100644
--- a/src/DonationForms/Blocks/DonationFormBlock/resources/app/Components/ModalForm.tsx
+++ b/src/DonationForms/Blocks/DonationFormBlock/resources/app/Components/ModalForm.tsx
@@ -42,6 +42,7 @@ export default function ModalForm({dataSrc, embedId, openFormButton, isFormRedir
id={embedId}
src={dataSrcUrl}
checkOrigin={false}
+ heightCalculationMethod={'taggedElement'}
style={{
minWidth: '100%',
border: 'none',
diff --git a/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx b/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx
index 3ae0e8b8cc..27f11daff9 100644
--- a/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx
+++ b/src/DonationForms/Blocks/DonationFormBlock/resources/app/index.tsx
@@ -73,6 +73,7 @@ function DonationFormBlockApp({
id={embedId}
src={dataSrc}
checkOrigin={false}
+ heightCalculationMethod={'taggedElement'}
style={{
width: '1px',
minWidth: '100%',
@@ -92,28 +93,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/DataTransferObjects/DonateControllerData.php b/src/DonationForms/DataTransferObjects/DonateControllerData.php
index 62e88fd3de..d5c18c8503 100644
--- a/src/DonationForms/DataTransferObjects/DonateControllerData.php
+++ b/src/DonationForms/DataTransferObjects/DonateControllerData.php
@@ -187,10 +187,17 @@ public function toInitialSubscriptionDonation(int $donorId, int $subscriptionId)
}
/**
+ * @since 3.16.0 Added "givewp_donation_confirmation_page_redirect_enabled" filter
* @since 3.0.0
*/
public function getSuccessUrl(Donation $donation): string
{
+ $form = $this->getDonationForm();
+
+ if (apply_filters('givewp_donation_confirmation_page_redirect_enabled', $form->settings->enableReceiptConfirmationPage, $donation->formId)) {
+ return $this->getDonationConfirmationPageFromSettings($donation);
+ }
+
return $this->isEmbed ?
$this->getDonationConfirmationReceiptUrl($donation) :
$this->getDonationConfirmationReceiptViewRouteUrl($donation);
@@ -222,6 +229,22 @@ public function getDonationConfirmationReceiptUrl(Donation $donation): string
return (new GenerateDonationConfirmationReceiptUrl())($donation, $this->originUrl, $this->embedId);
}
+ /**
+ * @since 3.16.0
+ */
+ public function getDonationConfirmationPageFromSettings(Donation $donation): string
+ {
+ $settings = give_get_settings();
+
+ $page = isset($settings['success_page'])
+ ? get_permalink(absint($settings['success_page']))
+ : get_bloginfo('url');
+
+ $page = apply_filters('givewp_donation_confirmation_page_redirect_permalink', $page, $donation->formId);
+
+ return esc_url_raw(add_query_arg(['receipt-id' => $donation->purchaseKey], $page));
+ }
+
/**
* @since 3.0.0
*/
diff --git a/src/DonationForms/DataTransferObjects/DonateFormRouteData.php b/src/DonationForms/DataTransferObjects/DonateFormRouteData.php
index 8473b11ee8..0e602e11c1 100644
--- a/src/DonationForms/DataTransferObjects/DonateFormRouteData.php
+++ b/src/DonationForms/DataTransferObjects/DonateFormRouteData.php
@@ -3,6 +3,7 @@
namespace Give\DonationForms\DataTransferObjects;
use Give\DonationForms\Exceptions\DonationFormFieldErrorsException;
+use Give\DonationForms\Exceptions\DonationFormForbidden;
use Give\DonationForms\Models\DonationForm;
use Give\Framework\FieldsAPI\Actions\CreateValidatorFromForm;
use Give\Framework\FieldsAPI\Exceptions\NameCollisionException;
@@ -62,9 +63,10 @@ public static function fromRequest(array $requestData): self
* compares the request against the individual fields,
* their types and validation rules.
*
+ * @since 3.14.0 Added form status validation
* @since 3.0.0
*
- * @throws DonationFormFieldErrorsException|NameCollisionException
+ * @throws DonationFormFieldErrorsException|NameCollisionException|DonationFormForbidden
*/
public function validated(): DonateControllerData
{
@@ -74,8 +76,8 @@ public function validated(): DonateControllerData
/** @var DonationForm $form */
$form = DonationForm::find($this->formId);
- if ( ! $form) {
- $this->throwDonationFormFieldErrorsException(['formId' => 'Invalid Form ID, Form not found']);
+ if (!$form || !$this->isValidForm($form)) {
+ throw new DonationFormForbidden();
}
$validator = (new CreateValidatorFromForm())($form->schema(), $request);
@@ -134,4 +136,20 @@ public function toArray(): array
{
return get_object_vars($this);
}
+
+ /**
+ * @since 3.14.0
+ */
+ private function isValidForm(DonationForm $form): bool
+ {
+ if ($form->status->isTrash()) {
+ return false;
+ }
+
+ if (!$form->status->isPublished() && !current_user_can('edit_give_forms')) {
+ return false;
+ }
+
+ return true;
+ }
}
diff --git a/src/DonationForms/DonationFormsAdminPage.php b/src/DonationForms/DonationFormsAdminPage.php
new file mode 100644
index 0000000000..7571be524e
--- /dev/null
+++ b/src/DonationForms/DonationFormsAdminPage.php
@@ -0,0 +1,20 @@
+join(function (JoinQueryBuilder $builder) use ($key, $alias) {
$builder
@@ -58,7 +58,7 @@ public function form($formId)
* An opinionated where method for the multiple donation form IDs meta field.
* @since 3.12.0
*/
- public function forms(array $formIds)
+ public function forms(array $formIds): DonationQuery
{
$this->joinMeta('_give_payment_form_id', 'formId');
$this->whereIn('formId.meta_value', $formIds);
@@ -69,7 +69,7 @@ public function forms(array $formIds)
* An opinionated whereBetween method for the completed date meta field.
* @since 3.12.0
*/
- public function between($startDate, $endDate)
+ public function between($startDate, $endDate): DonationQuery
{
// If the dates are empty or invalid, they will fallback to January 1st, 1970.
// For the start date, this is exactly what we need, but for the end date, we should set it as the current date so that we have a correct date range.
@@ -83,17 +83,71 @@ public function between($startDate, $endDate)
return $this;
}
+ /**
+ * @since 3.14.0
+ */
+ public function includeOnlyValidStatuses(): DonationQuery
+ {
+ $this->whereIn('donation.post_status', ['publish', 'give_subscription']);
+
+ return $this;
+ }
+
+ /**
+ * @since 3.14.0
+ */
+ public function includeOnlyCurrentMode(): DonationQuery
+ {
+ $this->joinMeta('_give_payment_mode', 'paymentMode');
+ $this->where('paymentMode.meta_value', give_is_test_mode() ? 'test' : 'live');
+
+ return $this;
+ }
+
/**
* Returns a calculated sum of the intended amounts (without recovered fees) for the donations.
+ *
+ * @since 3.14.0 Use the NULLIF function to prevent zero values that can generate a wrong final result and use $this->includeOnlyValidStatuses() and $this->includeOnlyCurrentMode()
* @since 3.12.0
* @return int|float
*/
- public function sumIntendedAmount()
+ public function sumIntendedAmount($includeOnlyValidStatuses = true, $includeOnlyCurrentMode = true)
{
+ if ($includeOnlyValidStatuses) {
+ $this->includeOnlyValidStatuses();
+ }
+
+ if ($includeOnlyCurrentMode) {
+ $this->includeOnlyCurrentMode();
+ }
+
$this->joinMeta('_give_payment_total', 'amount');
$this->joinMeta('_give_fee_donation_amount', 'intendedAmount');
return $this->sum(
- 'COALESCE(intendedAmount.meta_value, amount.meta_value)'
+ 'COALESCE(NULLIF(intendedAmount.meta_value,0), NULLIF(amount.meta_value,0), 0)'
+ );
+ }
+
+ /**
+ * Returns a calculated sum of the amounts (with recovered fees) for the donations.
+ *
+ * @since 3.14.0
+ * @return int|float
+ */
+ public function sumAmount($includeOnlyValidStatuses = true, $includeOnlyCurrentMode = true)
+ {
+ if ($includeOnlyValidStatuses) {
+ $this->includeOnlyValidStatuses();
+ }
+
+ if ($includeOnlyCurrentMode) {
+ $this->includeOnlyCurrentMode();
+ }
+
+ $this->joinMeta('_give_payment_total', 'amount');
+
+ return $this->sum(
+ 'amount.meta_value'
);
}
diff --git a/src/DonationForms/Exceptions/DonationFormForbidden.php b/src/DonationForms/Exceptions/DonationFormForbidden.php
new file mode 100644
index 0000000000..672cdd6866
--- /dev/null
+++ b/src/DonationForms/Exceptions/DonationFormForbidden.php
@@ -0,0 +1,17 @@
+getDonationConfirmationReceiptViewRouteUrl($donation);
+ $redirectUrl = $formData->getDonationConfirmationPageFromSettings($donation);
+ $form = $formData->getDonationForm();
+
+ if (apply_filters('givewp_donation_confirmation_page_redirect_enabled', $form->settings->enableReceiptConfirmationPage, $donation->formId)) {
+ $filteredUrl = $redirectUrl;
+ }
add_filter('give_get_success_page_uri', static function ($url) use ($filteredUrl) {
return $filteredUrl;
diff --git a/src/DonationForms/Properties/FormSettings.php b/src/DonationForms/Properties/FormSettings.php
index 7fa6ec5420..c8ffb80f9f 100644
--- a/src/DonationForms/Properties/FormSettings.php
+++ b/src/DonationForms/Properties/FormSettings.php
@@ -13,9 +13,10 @@
use Give\Framework\Support\Contracts\Jsonable;
/**
+ * @since 3.16.0 Added $enableReceiptConfirmationPage property
* @since 3.12.0 Add goalProgressType
- * @since 3.2.0 Remove addSlashesRecursive method
- * @since 3.0.0
+ * @since 3.2.0 Remove addSlashesRecursive method
+ * @since 3.0.0
*/
class FormSettings implements Arrayable, Jsonable
{
@@ -261,11 +262,15 @@ class FormSettings implements Arrayable, Jsonable
* @var array
*/
public $currencySwitcherSettings;
-
/**
- * @since 3.7.0 Added formExcerpt
+ * @since 3.16.0
+ * @var bool
+ */
+ public $enableReceiptConfirmationPage;
/**
+ * @since 3.16.0 Added $enableReceiptConfirmationPage
+ * @since 3.7.0 Added formExcerpt
* @since 3.11.0 Sanitize customCSS property
* @since 3.2.0 Added registrationNotification
* @since 3.0.0
@@ -313,6 +318,9 @@ public static function fromArray(array $array): self
'{first_name}, your contribution means a lot and will be put to good use in making a difference. We’ve sent your donation receipt to {email}.',
'give'
);
+
+ $self->enableReceiptConfirmationPage = $array['enableReceiptConfirmationPage'] ?? false;
+
$self->formStatus = ! empty($array['formStatus']) ? new DonationFormStatus(
$array['formStatus']
) : DonationFormStatus::DRAFT();
diff --git a/src/DonationForms/Routes/DonateRoute.php b/src/DonationForms/Routes/DonateRoute.php
index 9f6e74bcb7..300ba74b9b 100644
--- a/src/DonationForms/Routes/DonateRoute.php
+++ b/src/DonationForms/Routes/DonateRoute.php
@@ -3,10 +3,13 @@
namespace Give\DonationForms\Routes;
+use Exception;
use Give\DonationForms\Controllers\DonateController;
+use Give\DonationForms\DataTransferObjects\DonateControllerData;
use Give\DonationForms\DataTransferObjects\DonateFormRouteData;
use Give\DonationForms\DataTransferObjects\DonateRouteData;
use Give\DonationForms\Exceptions\DonationFormFieldErrorsException;
+use Give\DonationForms\Exceptions\DonationFormForbidden;
use Give\DonationForms\ValueObjects\DonationFormErrorTypes;
use Give\Framework\PaymentGateways\Exceptions\PaymentGatewayException;
use Give\Framework\PaymentGateways\Traits\HandleHttpResponses;
@@ -53,6 +56,17 @@ public function __invoke(array $request)
try {
$data = $formData->validated();
+
+ /**
+ * Allow for additional validation of the donation form data.
+ * The donation flow can be interrupted by throwing an Exception.
+ *
+ * @since 3.15.0
+ *
+ * @param DonateControllerData $data
+ */
+ do_action('givewp_donate_form_data_validated', $data);
+
$this->donateController->donate($data, $data->getGateway());
} catch (DonationFormFieldErrorsException $exception) {
$type = DonationFormErrorTypes::VALIDATION;
@@ -62,7 +76,9 @@ public function __invoke(array $request)
$type = DonationFormErrorTypes::GATEWAY;
$this->logError($type, $exception->getMessage(), $formData);
$this->sendJsonError($type, new WP_Error($type, $exception->getMessage()));
- } catch (\Exception $exception) {
+ } catch (DonationFormForbidden $exception) {
+ wp_die($exception->getMessage(), 403);
+ } catch (Exception $exception) {
$type = DonationFormErrorTypes::UNKNOWN;
$this->logError($type, $exception->getMessage(), $formData);
$this->sendJsonError($type, new WP_Error($type, $exception->getMessage()));
diff --git a/src/DonationForms/Rules/HoneyPotRule.php b/src/DonationForms/Rules/HoneyPotRule.php
new file mode 100644
index 0000000000..38cb3a2127
--- /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 2f26728781..80f81a0384 100644
--- a/src/DonationForms/ServiceProvider.php
+++ b/src/DonationForms/ServiceProvider.php
@@ -3,10 +3,19 @@
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;
+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;
@@ -25,7 +34,13 @@
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\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;
@@ -33,7 +48,6 @@
use Give\Log\Log;
use Give\ServiceProviders\ServiceProvider as ServiceProviderInterface;
-
class ServiceProvider implements ServiceProviderInterface
{
@@ -68,6 +82,8 @@ public function boot()
$this->registerSingleFormPage();
$this->registerShortcodes();
$this->registerPostStatus();
+ $this->registerAddFormSubmenuLink();
+ $this->registerHoneyPotField();
Hooks::addAction('givewp_donation_form_created', StoreBackwardsCompatibleFormMeta::class);
Hooks::addAction('givewp_donation_form_updated', StoreBackwardsCompatibleFormMeta::class);
@@ -79,6 +95,135 @@ public function boot()
RemoveDuplicateMeta::class,
UpdateDonationLevelsSchema::class,
]);
+
+ /**
+ * @since 3.16.0
+ * 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
+ );
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ private function registerAddFormSubmenuLink()
+ {
+ Hooks::addAction('admin_menu', DonationFormsAdminPage::class, 'addFormSubmenuLink', 999);
}
/**
@@ -168,7 +313,7 @@ private function registerFormDesigns()
} catch (Exception $e) {
Log::error('Error registering form designs', [
'message' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
+ 'trace' => $e->getTraceAsString(),
]);
}
});
@@ -188,6 +333,17 @@ protected function registerSingleFormPage()
protected function registerShortcodes()
{
Hooks::addFilter('givewp_form_shortcode_output', GiveFormShortcode::class, '__invoke', 10, 2);
+ Hooks::addFilter('give_donation_confirmation_success_page_shortcode_view', ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe::class);
+ Hooks::addFilter('give_receipt_shortcode_output', ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe::class);
+ add_action('give_donation_confirmation_page_enqueue_scripts', function() {
+ wp_enqueue_script(
+ 'givewp-donation-form-embed',
+ GIVE_PLUGIN_URL . 'build/donationFormEmbed.js',
+ [],
+ GIVE_VERSION,
+ true
+ );
+ });
}
/**
@@ -199,4 +355,33 @@ protected function registerPostStatus()
register_post_status(DonationFormStatus::UPGRADED);
});
}
+
+ /**
+ * @since 3.16.2
+ * @throws EmptyNameException
+ */
+ private function registerHoneyPotField(): void
+ {
+ add_action('givewp_donation_form_schema', function (DonationFormModel $form, int $formId) {
+ /**
+ * 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
+ *
+ * @since 3.17.0
+ */
+ $honeypotFieldName = (string)apply_filters('givewp_donation_forms_honeypot_field_name', 'donationBirthday', $formId);
+
+ (new AddHoneyPotFieldToDonationForms())($form, $honeypotFieldName);
+ }
+ }, 10, 2);
+ }
}
diff --git a/src/DonationForms/V2/DonationFormsAdminPage.php b/src/DonationForms/V2/DonationFormsAdminPage.php
index 9319d5d22c..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;
@@ -102,10 +103,10 @@ public function loadScripts()
'table' => give(DonationFormsListTable::class)->toArray(),
'adminUrl' => $this->adminUrl,
'pluginUrl' => GIVE_PLUGIN_URL,
- 'showBanner' => !get_user_meta(get_current_user_id(), 'givewp-show-onboarding-banner', true),
'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')
@@ -316,6 +317,7 @@ public static function getUrl(): string
/**
* Get an array of supported addons
*
+ * @since 3.14.0 Added support for Razorpay
* @since 3.4.2 Added support for Gift Aid
* @since 3.3.0 Add support to the Funds and Designations addon
* @since 3.0.0
@@ -339,20 +341,16 @@ public function getSupportedAddons(): array
'Donation Upsells for WooCommerce' => class_exists('Give_WooCommerce'),
'Constant Contact' => class_exists('Give_Constant_Contact'),
'MailChimp' => class_exists('Give_MailChimp'),
- // 'Manual Donations' => class_exists('Give_Manual_Donations'),
+ 'Manual Donations' => class_exists('Give_Manual_Donations'),
'Funds' => defined('GIVE_FUNDS_ADDON_NAME'),
'Peer-to-Peer' => defined('GIVE_P2P_NAME'),
'Gift Aid' => class_exists('Give_Gift_Aid'),
- // 'Text-to-Give' => defined('GIVE_TEXT_TO_GIVE_ADDON_NAME'),
- // 'Donation Block for Stripe' => defined('DONATION_BLOCK_FILE'),
+ 'Text-to-Give' => defined('GIVE_TEXT_TO_GIVE_ADDON_NAME'),
'Double the Donation' => defined('GIVE_DTD_NAME'),
- // 'Simple Social Shout' => class_exists('SIMPLE_SOCIAL_SHARE_4_GIVEWP'),
- // 'Receipt Attachments' => defined('GIVERA_VERSION'),
'Per Form Gateways' => class_exists('Give_Per_Form_Gateways'),
- // 'Per Form Confirmations' => class_exists('Per_Form_Confirmations_4_GIVEWP'),
- // 'Form Countdown' => class_exists('Give_Form_Countdown'),
'ConvertKit' => defined('GIVE_CONVERTKIT_VERSION'),
'ActiveCampaign' => class_exists('Give_ActiveCampaign'),
+ 'Razorpay' => class_exists('Give_Razorpay_Gateway'),
];
$output = [];
diff --git a/src/DonationForms/V2/Endpoints/Endpoint.php b/src/DonationForms/V2/Endpoints/Endpoint.php
index 979e0f5063..761e9bcd1a 100644
--- a/src/DonationForms/V2/Endpoints/Endpoint.php
+++ b/src/DonationForms/V2/Endpoints/Endpoint.php
@@ -26,6 +26,16 @@ public function validateInt($value)
return filter_var($value, FILTER_VALIDATE_INT);
}
+ /**
+ * @since 3.14.0
+ * @param string $id
+ * @return bool
+ */
+ public function validatePostType(string $id)
+ {
+ return get_post_type($id) === 'give_forms';
+ }
+
/**
* Check user permissions
* @return bool|WP_Error
diff --git a/src/DonationForms/V2/Endpoints/FormActions.php b/src/DonationForms/V2/Endpoints/FormActions.php
index 208138e5ac..8b368ee209 100644
--- a/src/DonationForms/V2/Endpoints/FormActions.php
+++ b/src/DonationForms/V2/Endpoints/FormActions.php
@@ -7,6 +7,7 @@
use WP_REST_Response;
/**
+ * @since 3.14.0 updated to validate form id is a donation form post type
* @since 2.19.0
*/
class FormActions extends Endpoint
@@ -47,7 +48,7 @@ public function registerRoute()
'required' => true,
'validate_callback' => function ($ids) {
foreach ($this->splitString($ids) as $id) {
- if ( ! $this->validateInt($id)) {
+ if ( ! $this->validateInt($id) || !$this->validatePostType($id)) {
return false;
}
}
diff --git a/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php b/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php
index 9d9fe0cbc8..6e9bb85578 100644
--- a/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php
+++ b/src/DonationForms/V2/ListTable/Columns/DonationCountColumn.php
@@ -38,6 +38,8 @@ public function getLabel(): string
}
/**
+ * @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
*
* @inheritDoc
@@ -60,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..b5fe554e60 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
}
/**
+ * @since 3.16.0 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 9c44c6b231..2fc13da635 100644
--- a/src/DonationForms/V2/ListTable/Columns/GoalColumn.php
+++ b/src/DonationForms/V2/ListTable/Columns/GoalColumn.php
@@ -35,6 +35,8 @@ public function getLabel(): string
}
/**
+ * @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
*
* @inheritDoc
@@ -78,7 +80,8 @@ class="goalProgress"
$goal['goal']
),
sprintf(
- ($goal['progress'] >= 100 ? ' %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/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/components/DonationFormsListTable.tsx b/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx
index 48315e5ff3..38993e202f 100644
--- a/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx
+++ b/src/DonationForms/V2/resources/components/DonationFormsListTable.tsx
@@ -9,7 +9,6 @@ import Select from '@givewp/components/ListTable/Select';
import {Interweave} from 'interweave';
import InterweaveSSR from '@givewp/components/ListTable/InterweaveSSR';
import BlankSlate from '@givewp/components/ListTable/BlankSlate';
-import FormBuilderButton from './Onboarding/Components/FormBuilderButton';
import {CubeIcon} from '@givewp/components/AdminUI/Icons';
declare global {
@@ -20,14 +19,14 @@ declare global {
tooltipActionUrl: string;
migrationApiRoot: string;
apiRoot: string;
- authors: Array<{ id: string | number; name: string }>;
- table: { columns: Array };
+ authors: Array<{id: string | number; name: string}>;
+ table: {columns: Array};
pluginUrl: string;
- showBanner: boolean;
showUpgradedTooltip: boolean;
isMigrated: boolean;
supportedAddons: Array;
supportedGateways: Array;
+ isOptionBasedFormEditorEnabled: boolean;
};
GiveNextGen?: {
@@ -84,15 +83,13 @@ const donationFormsFilters: Array = [
const columnFilters: Array = [
{
column: 'title',
- filter: item => {
+ filter: (item) => {
if (item?.v3form) {
return (
-
- {__('Uses the Visual Form Builder', 'give')}
-
+
{__('Uses the Visual Form Builder', 'give')}
@@ -110,13 +107,18 @@ const columnFilters: Array = [
- {__('The name of this form is already associated with an upgraded form. You can safely delete this form', 'give')}.
+ {__(
+ 'The name of this form is already associated with an upgraded form. You can safely delete this form',
+ 'give'
+ )}
+ .
{
e.currentTarget.parentElement.remove();
fetch(window.GiveDonationForms.tooltipActionUrl, {method: 'POST'});
- }}>
+ }}
+ >
{__('Got it', 'give')}
@@ -128,7 +130,7 @@ const columnFilters: Array
= [
return ;
},
- }
+ },
];
const donationFormsBulkActions: Array = [
@@ -238,11 +240,9 @@ const ListTableBlankSlate = (
);
export default function DonationFormsListTable() {
-
const [state, setState] = useState({
- showBanner: Boolean(window.GiveDonationForms.showBanner),
- showFeatureNoticeDialog: false
- })
+ showFeatureNoticeDialog: false,
+ });
return (
@@ -258,20 +258,20 @@ export default function DonationFormsListTable() {
columnFilters={columnFilters}
banner={Onboarding}
>
-
- setState(prev => ({
- ...prev,
- showFeatureNoticeDialog: true
- }))}
- />
-
-
+ {window.GiveDonationForms.isOptionBasedFormEditorEnabled && (
+
+ {__('Switch to Legacy View', 'give')}
+
+ )}
+
{__('Add Form', 'give')}
-
- {__('Switch to Legacy View')}
-
);
diff --git a/src/DonationForms/V2/resources/components/DonationFormsRowActions.tsx b/src/DonationForms/V2/resources/components/DonationFormsRowActions.tsx
index a57bcb5ca4..6ba994d34b 100644
--- a/src/DonationForms/V2/resources/components/DonationFormsRowActions.tsx
+++ b/src/DonationForms/V2/resources/components/DonationFormsRowActions.tsx
@@ -6,6 +6,7 @@ import {useContext} from 'react';
import {ShowConfirmModalContext} from '@givewp/components/ListTable/ListTablePage';
import {Interweave} from 'interweave';
import {OnboardingContext} from './Onboarding';
+import {UpgradeModalContent} from "./Migration";
const donationFormsApi = new ListTableApi(window.GiveDonationForms);
@@ -49,6 +50,18 @@ export function DonationFormsRowActions({data, item, removeRow, addRow, setUpdat
showConfirmModal(__('Trash', 'give'), confirmTrashForm, deleteForm, 'danger');
};
+ const confirmUpgradeModal = (event) => {
+ showConfirmModal(
+ __('Upgrade', 'give'),
+ UpgradeModalContent,
+ async (selected) => {
+ const response = await donationFormsApi.fetchWithArgs("/migrate/" + item.id, {}, 'POST');
+ await mutate(parameters);
+ return response;
+ }
+ );
+ };
+
return (
<>
{parameters.status === 'trash' ? (
@@ -86,6 +99,12 @@ export function DonationFormsRowActions({data, item, removeRow, addRow, setUpdat
displayText={__('Duplicate', 'give')}
hiddenText={item?.name}
/>
+ {!item.v3form && ( )}
>
)}
>
diff --git a/src/DonationForms/V2/resources/components/Migration/index.tsx b/src/DonationForms/V2/resources/components/Migration/index.tsx
new file mode 100644
index 0000000000..92d37fff7d
--- /dev/null
+++ b/src/DonationForms/V2/resources/components/Migration/index.tsx
@@ -0,0 +1,48 @@
+import {__, sprintf} from "@wordpress/i18n";
+import {createInterpolateElement} from "@wordpress/element";
+import {CheckVerified} from "@givewp/components/AdminUI/Icons";
+
+/**
+ * @since 3.16.0
+ */
+export const UpgradeModalContent = () => {
+
+ const {supportedAddons, supportedGateways} = window.GiveDonationForms;
+
+ return
+ {createInterpolateElement(
+ sprintf(__('GiveWP 3.0 introduces an enhanced forms experience powered by the new Visual Donation Form Builder. The team is still working on add-on and gateway compatibility. If you need to use an add-on or gateway that isn\'t listed, use the "%sAdd form%s" option for now.', 'give'), '', ' '),
+ {
+ b: ,
+ }
+ )}
+
+ {supportedAddons.length > 0 && (
+
+ )}
+
+ {supportedGateways.length > 0 && (
+
+ )}
+
+
+}
+
+const SupportedItemsList = ({title, items}) => {
+ return (
+ <>
+ {title}
+
+ {items.map((item) => (
+
+ {item}
+
+ ))}
+
+ >
+ )
+}
diff --git a/src/DonationForms/V2/resources/components/Onboarding/Components/FormBuilderButton.tsx b/src/DonationForms/V2/resources/components/Onboarding/Components/FormBuilderButton.tsx
index 2819620eef..d032fa0165 100644
--- a/src/DonationForms/V2/resources/components/Onboarding/Components/FormBuilderButton.tsx
+++ b/src/DonationForms/V2/resources/components/Onboarding/Components/FormBuilderButton.tsx
@@ -8,7 +8,7 @@ export default function FormBuilderButton({onClick}) {
className={styles.tryNewFormBuilderButton}
onClick={onClick}
>
- {__('Try the new form builder', 'give')}
+ {__('Use the new visual form builder', 'give')}
)
}
diff --git a/src/DonationForms/V2/resources/components/Onboarding/index.tsx b/src/DonationForms/V2/resources/components/Onboarding/index.tsx
index 3c77f8fd42..8050132ba8 100644
--- a/src/DonationForms/V2/resources/components/Onboarding/index.tsx
+++ b/src/DonationForms/V2/resources/components/Onboarding/index.tsx
@@ -1,11 +1,9 @@
import {createContext, useContext} from 'react';
-import Banner from './Components/Banner';
import {FeatureNoticeDialog} from './Dialogs';
export const OnboardingContext = createContext([]);
export interface OnboardingStateProps {
- showBanner: boolean;
showFeatureNoticeDialog: boolean;
}
@@ -14,8 +12,6 @@ export default function Onboarding() {
return (
<>
- {state.showBanner && }
-
{state.showFeatureNoticeDialog && (
- ,
- 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/app/fields/FieldNode.tsx b/src/DonationForms/resources/app/fields/FieldNode.tsx
index abd3830d87..e28f2e8eff 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'];
+
+/**
+ * @since 3.16.2 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/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 && (
{
+ /* @ts-ignore */
+ window.parent.document.getElementById(window.parentIFrame?.getId())?.scrollIntoView()
+ }, [currentStep]);
+
const stepElements = steps?.map(({id, element}) => {
const shouldRenderElement = currentStep >= id;
const isFirstStep = id === 0;
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/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/DonationForms/resources/registrars/templates/fields/Consent/ConsentModal.tsx b/src/DonationForms/resources/registrars/templates/fields/Consent/ConsentModal.tsx
index bac500ee3e..461884087c 100644
--- a/src/DonationForms/resources/registrars/templates/fields/Consent/ConsentModal.tsx
+++ b/src/DonationForms/resources/registrars/templates/fields/Consent/ConsentModal.tsx
@@ -1,25 +1,14 @@
import {__} from '@wordpress/i18n';
-import {createPortal} from 'react-dom';
import {Markup} from 'interweave';
import {Button} from '@wordpress/components';
+import createIframePortal from './createIframePortal';
import './styles.scss';
export default function ConsentModal({setShowModal, modalHeading, modalAcceptanceText, agreementText, acceptTerms}) {
- const scrollModalIntoView = (element) => {
- if (element) {
- element.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'nearest'});
- }
- };
-
- return createPortal(
+ return createIframePortal(
-
{
- element && scrollModalIntoView(element);
- }}
- >
+
{modalHeading}
@@ -30,10 +19,12 @@ export default function ConsentModal({setShowModal, modalHeading, modalAcceptanc
setShowModal(false)}>
{__('Cancel', 'give')}
- {modalAcceptanceText}
+
+ {modalAcceptanceText}
+
,
- document.body
+ window.top.document.body
);
}
diff --git a/src/DonationForms/resources/registrars/templates/fields/Consent/createIframePortal.tsx b/src/DonationForms/resources/registrars/templates/fields/Consent/createIframePortal.tsx
new file mode 100644
index 0000000000..4144064e60
--- /dev/null
+++ b/src/DonationForms/resources/registrars/templates/fields/Consent/createIframePortal.tsx
@@ -0,0 +1,92 @@
+import {createPortal, render} from 'react-dom';
+import {useEffect, useRef} from 'react';
+
+import './styles.scss';
+
+/**
+ * @since 3.14.0
+ * Creates a portal to the Top Level document, rendering children elements within an iframe.
+ */
+export default function createIframePortal(children, targetElement = window.top.document.body) {
+ const iframeRef = useRef
(null);
+
+ useEffect(() => {
+ const iframe = iframeRef.current;
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+
+ if (iframe) {
+ // Clear existing content.
+ iframeDoc.head.innerHTML = '';
+ iframeDoc.body.innerHTML = '';
+
+ async function renderContent() {
+ try {
+ await fetchStylesheets(iframeDoc);
+ render(children, iframeDoc.body);
+ } catch (error) {
+ console.error('Error loading stylesheets:', error);
+ }
+ }
+
+ renderContent();
+ }
+ }, []);
+
+ return createPortal(
+ ,
+ targetElement
+ );
+}
+
+/**
+ * @since 3.14.0
+ * Fetches stylesheets from the originating document and injects them into the new iframe document's head.
+ * This allows user provided styles to be applied to the iframe content.
+ * Returns a promise that resolves when all stylesheets are loaded.
+ */
+export async function fetchStylesheets(iframeDoc: Document) {
+ const styleSheets = Array.from(document.styleSheets);
+
+ // Promisify the loading of each stylesheet
+ const loadStylesheet = (styleSheet) => {
+ return new Promise((resolve, reject) => {
+ try {
+ if (styleSheet.href) {
+ // For external stylesheets
+ const newLink = document.createElement('link');
+ newLink.rel = 'stylesheet';
+ newLink.href = styleSheet.href;
+ newLink.onload = () => resolve();
+ newLink.onerror = reject;
+ iframeDoc.head.appendChild(newLink);
+ } else if (styleSheet.cssRules) {
+ // For tags
+ const newStyle = document.createElement('style');
+ Array.from(styleSheet.cssRules).forEach((rule: {cssText: string}) => {
+ newStyle.appendChild(document.createTextNode(rule.cssText));
+ });
+ iframeDoc.head.appendChild(newStyle);
+ resolve();
+ }
+ } catch (error) {
+ reject(error);
+ }
+ });
+ };
+
+ const promises = styleSheets.map(loadStylesheet);
+ return await Promise.all(promises);
+}
diff --git a/src/DonationForms/resources/registrars/templates/fields/Consent/styles.scss b/src/DonationForms/resources/registrars/templates/fields/Consent/styles.scss
index 481c404fd5..ba3c992cbb 100644
--- a/src/DonationForms/resources/registrars/templates/fields/Consent/styles.scss
+++ b/src/DonationForms/resources/registrars/templates/fields/Consent/styles.scss
@@ -7,7 +7,7 @@
}
&-modal {
- position: absolute;
+ position: fixed;
top: 0;
left: 0;
bottom: 0;
@@ -17,9 +17,9 @@
justify-content: center;
background: transparent;
backdrop-filter: blur(2px);
- z-index: 999;
&-content {
+ max-width: 48rem;
background: var(--givep-shades-white, #fff);
padding: 2.5rem 3.5rem;
width: calc(min(100%, 51.5rem) + 2rem);
@@ -41,8 +41,7 @@
display: flex;
gap: 1rem;
- > button {
- margin: 0;
+ button:first-child {
background: transparent;
color: var(--givewp-primary-color);
border: 1px solid var(--givewp-primary-color);
@@ -66,4 +65,3 @@
}
}
}
-
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 (
-
-
+ <>
+
+
+
{description && }
-
+ >
);
}
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..ebadddfa63
--- /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';
+
+/**
+ * @since 3.16.2
+ */
+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 (
+
+
+
+ {description && }
+
+
+
+
+
+ );
+}
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 ? (
-
{
- const loginUrl = getRedirectUrl(new URL(loginRedirectUrl));
-
- window.top.location.assign(loginUrl);
- }}
- >
- {loginNotice}
-
+
{loginNotice}
) : (
{loginNotice}
)}
@@ -147,24 +144,17 @@ const LoginForm = ({children, success, lostPasswordUrl, nodeName}) => {
};
return (
-
-
{children}
+
+
{children}
{!!errorMessage &&
}
-
@@ -181,11 +171,8 @@ const LoginForm = ({children, success, lostPasswordUrl, nodeName}) => {
const LoginNotice = ({children, onClick}) => {
return (
-
+
{children}
- )
-}
+ );
+};
diff --git a/src/DonationForms/resources/registrars/templates/index.ts b/src/DonationForms/resources/registrars/templates/index.ts
index 1074914ebf..328f29dfd9 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';
+/**
+ * @since 3.16.2 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/registrars/templates/layouts/HeaderDescription.tsx b/src/DonationForms/resources/registrars/templates/layouts/HeaderDescription.tsx
index 3f6a9cd991..37d99f151a 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';
/**
+ * @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) {
- return {text}
;
+ 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/DonationForms/resources/styles/_base-overrides.scss b/src/DonationForms/resources/styles/_base-overrides.scss
index a032f4fda7..dfc9f8f4b8 100644
--- a/src/DonationForms/resources/styles/_base-overrides.scss
+++ b/src/DonationForms/resources/styles/_base-overrides.scss
@@ -81,9 +81,17 @@ input[type="file"] {
input[type="text"]:not([name="amount"], [name="amount-custom"]),
input[type="password"],
input[type="email"],
+input[type="checkbox"],
textarea {
+ 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;
+ }
}
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/_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;
diff --git a/src/DonationForms/resources/styles/components/_goal.scss b/src/DonationForms/resources/styles/components/_goal.scss
index cddd198e2f..49b54ebab9 100644
--- a/src/DonationForms/resources/styles/components/_goal.scss
+++ b/src/DonationForms/resources/styles/components/_goal.scss
@@ -11,6 +11,10 @@
margin: 0;
padding: 0;
list-style: none;
+
+ @media (max-width: 400px) {
+ flex-wrap: wrap; /* Enable wrapping on screens 400px or narrower */
+ }
}
&__list-item {
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/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/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 {
diff --git a/src/DonationSpam/Akismet/API.php b/src/DonationSpam/Akismet/API.php
new file mode 100644
index 0000000000..ac6c794955
--- /dev/null
+++ b/src/DonationSpam/Akismet/API.php
@@ -0,0 +1,21 @@
+toHttpQuery(), 'comment-check');
+ }
+}
diff --git a/src/DonationSpam/Akismet/Actions/ValidateDonation.php b/src/DonationSpam/Akismet/Actions/ValidateDonation.php
new file mode 100644
index 0000000000..db4757f0d9
--- /dev/null
+++ b/src/DonationSpam/Akismet/Actions/ValidateDonation.php
@@ -0,0 +1,62 @@
+akismet = $akismet;
+ $this->whitelist = $whitelist;
+ }
+
+ /**
+ * @since 3.15.0
+ *
+ * @param DonateControllerData $data
+ *
+ * @throws SpamDonationException
+ */
+ public function __invoke(DonateControllerData $data): void
+ {
+ if(!$this->whitelist->validate($data->email)) {
+
+ $args = CommentCheckArgs::make($data);
+ $response = $this->akismet->commentCheck($args);
+ $spam = 'true' === $response[1];
+
+ if($spam) {
+ $message = "This donor's email ($data->firstName $data->lastName - $data->email) has been flagged as SPAM";
+ if(!give_akismet_is_email_logged($data->email)) {
+ Log::spam($message, (array) new SpamContext($args, $response));
+ }
+ throw new SpamDonationException($message);
+ }
+ }
+ }
+}
diff --git a/src/DonationSpam/Akismet/DataTransferObjects/CommentCheckArgs.php b/src/DonationSpam/Akismet/DataTransferObjects/CommentCheckArgs.php
new file mode 100644
index 0000000000..719d6e6bef
--- /dev/null
+++ b/src/DonationSpam/Akismet/DataTransferObjects/CommentCheckArgs.php
@@ -0,0 +1,60 @@
+comment_type = 'contact-form';
+ $self->comment_content = $data->comment;
+ $self->comment_author = $data->firstName;
+ $self->comment_author_email = $data->email;
+
+ $self->blog = get_option('home');
+ $self->blog_lang = get_locale();
+ $self->blog_charset = get_option('blog_charset');
+
+ $self->user_ip = @$_SERVER['REMOTE_ADDR'];
+ $self->user_agent = @$_SERVER['HTTP_USER_AGENT'];
+ $self->referrer = @$_SERVER['HTTP_REFERER'];
+
+ // Append additional server variables.
+ foreach ( $_SERVER as $key => $value ) {
+ if ( ! in_array( $key, [ 'HTTP_COOKIE', 'HTTP_COOKIE2', 'PHP_AUTH_PW' ], true ) ) {
+ $self->$key = $value;
+ }
+ }
+
+ return $self;
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ public function toHttpQuery(): string
+ {
+ return http_build_query(get_object_vars($this));
+ }
+}
diff --git a/src/DonationSpam/Akismet/DataTransferObjects/SpamContext.php b/src/DonationSpam/Akismet/DataTransferObjects/SpamContext.php
new file mode 100644
index 0000000000..fb0101d4be
--- /dev/null
+++ b/src/DonationSpam/Akismet/DataTransferObjects/SpamContext.php
@@ -0,0 +1,54 @@
+args = $args;
+ $this->response = $response;
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ public function __serialize(): array
+ {
+ return [
+ 'donor_email' => $this->args->comment_author_email,
+ 'filter' => 'akismet',
+ 'message' => $this->formatMessage(),
+ ];
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ public function formatMessage(): string
+ {
+ return sprintf(
+ '%1$s
%2$s %3$s %4$s ',
+ __( 'Request', 'give' ),
+ print_r( $this->args, true ),
+ __( 'Response', 'give' ),
+ print_r( $this->response, true )
+ );
+ }
+}
diff --git a/src/DonationSpam/EmailAddressWhiteList.php b/src/DonationSpam/EmailAddressWhiteList.php
new file mode 100644
index 0000000000..13b61c5ab6
--- /dev/null
+++ b/src/DonationSpam/EmailAddressWhiteList.php
@@ -0,0 +1,31 @@
+whitelistEmails = $whitelistEmails;
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ public function validate($email): bool
+ {
+ return in_array($email, $this->whitelistEmails, true);
+ }
+}
diff --git a/src/DonationSpam/Exceptions/SpamDonationException.php b/src/DonationSpam/Exceptions/SpamDonationException.php
new file mode 100644
index 0000000000..2facf98d68
--- /dev/null
+++ b/src/DonationSpam/Exceptions/SpamDonationException.php
@@ -0,0 +1,10 @@
+singleton(EmailAddressWhiteList::class, function () {
+ return new EmailAddressWhiteList(
+ (array) apply_filters( 'give_akismet_whitelist_emails', give_akismet_get_whitelisted_emails() )
+ );
+ });
+ }
+
+ /**
+ * @since 3.15.0
+ * @inheritDoc
+ */
+ public function boot(): void
+ {
+ if($this->isAkismetEnabledAndConfigured()) {
+ Hooks::addAction('givewp_donate_form_data_validated', Akismet\Actions\ValidateDonation::class);
+ }
+ }
+
+ /**
+ * @since 3.15.0
+ * @return bool
+ */
+ public function isAkismetEnabledAndConfigured(): bool
+ {
+ return
+ give_check_akismet_key()
+ && give_is_setting_enabled(
+ give_get_option( 'akismet_spam_protection', 'enabled')
+ );
+ }
+}
diff --git a/src/Donations/DonationsAdminPage.php b/src/Donations/DonationsAdminPage.php
index 6672ae7711..5a6bc78e3c 100644
--- a/src/Donations/DonationsAdminPage.php
+++ b/src/Donations/DonationsAdminPage.php
@@ -137,7 +137,7 @@ private function getForms()
return array_merge([
[
'value' => '0',
- 'text' => 'Any',
+ 'text' => __('Any', 'give'),
],
], $options);
}
diff --git a/src/Donations/Migrations/UnserializeTitlePrefix.php b/src/Donations/Migrations/UnserializeTitlePrefix.php
new file mode 100644
index 0000000000..860f0f6856
--- /dev/null
+++ b/src/Donations/Migrations/UnserializeTitlePrefix.php
@@ -0,0 +1,59 @@
+where('meta_key', '_give_donor_billing_title_prefix')->getAll();
+
+ foreach ($items as $item) {
+ if (Utils::isSerialized($item->meta_value)) {
+ $unserializedTitlePrefix = Utils::safeUnserialize($item->meta_value);
+
+ DB::table('give_donationmeta')
+ ->where('donation_id', $item->donation_id)
+ ->where('meta_key', '_give_donor_billing_title_prefix')
+ ->update([
+ 'meta_value' => $unserializedTitlePrefix,
+ ]);
+ }
+ }
+ }
+
+
+ /**
+ * @since 3.17.2
+ */
+ public static function id()
+ {
+ return 'donation-meta-unserialize-title-prefix';
+ }
+
+ /**
+ * @since 3.17.2
+ */
+ public static function title()
+ {
+ return 'Unserialize data in the _give_donor_billing_title_prefix meta value';
+ }
+
+ /**
+ * @since 3.17.2
+ */
+ public static function timestamp()
+ {
+ return strtotime('2024-23-10');
+ }
+}
diff --git a/src/Donations/Repositories/DonationRepository.php b/src/Donations/Repositories/DonationRepository.php
index ec47cf7b81..f7057583a4 100644
--- a/src/Donations/Repositories/DonationRepository.php
+++ b/src/Donations/Repositories/DonationRepository.php
@@ -75,6 +75,14 @@ public function getByGatewayTransactionId($gatewayTransactionId)
return $this->queryByGatewayTransactionId($gatewayTransactionId)->get();
}
+ /**
+ * @since 3.16.0
+ */
+ public function getTotalDonationCountByGatewayTransactionId($gatewayTransactionId): int
+ {
+ return $this->queryByGatewayTransactionId($gatewayTransactionId)->count();
+ }
+
/**
* @since 2.21.0
*/
diff --git a/src/Donations/ServiceProvider.php b/src/Donations/ServiceProvider.php
index cbc4d49f4b..c7b826baff 100644
--- a/src/Donations/ServiceProvider.php
+++ b/src/Donations/ServiceProvider.php
@@ -16,6 +16,7 @@
use Give\Donations\Migrations\AddMissingDonorIdToDonationComments;
use Give\Donations\Migrations\MoveDonationCommentToDonationMetaTable;
use Give\Donations\Migrations\SetAutomaticFormattingOption;
+use Give\Donations\Migrations\UnserializeTitlePrefix;
use Give\Donations\Models\Donation;
use Give\Donations\Repositories\DonationNotesRepository;
use Give\Donations\Repositories\DonationRepository;
@@ -53,6 +54,7 @@ public function boot()
AddMissingDonorIdToDonationComments::class,
SetAutomaticFormattingOption::class,
MoveDonationCommentToDonationMetaTable::class,
+ UnserializeTitlePrefix::class,
]);
}
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/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/App.php b/src/DonorDashboards/App.php
index 8d5a605b7b..259abe087b 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
*
+ * @since 3.19.0 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/Helpers.php b/src/DonorDashboards/Helpers.php
index 21ed0f2f58..a50c8c548b 100644
--- a/src/DonorDashboards/Helpers.php
+++ b/src/DonorDashboards/Helpers.php
@@ -2,6 +2,9 @@
namespace Give\DonorDashboards;
+use Give\Donors\Models\Donor;
+use WP_User;
+
/**
* @since 2.10.0
*/
@@ -38,11 +41,17 @@ public static function getCurrentDonorId()
/**
* Retrieve donor logged in status
*
+ * @since 3.15.0 added additional user role check
+ * @since 3.14.0 Add user capability and user role check
* @since 2.20.2
*/
public static function isDonorLoggedIn(): bool
{
- return is_user_logged_in() || (
+ /** @var WP_User $user */
+ $user = wp_get_current_user();
+ $allowedRoles = ['administrator', 'give_donor', 'give_subscriber'];
+
+ return (is_user_logged_in() && !empty(array_intersect($allowedRoles, $user->roles))) || (
give_is_setting_enabled( give_get_option( 'email_access' ) ) &&
Give()->email_access->is_valid_token(Give()->email_access->get_token())
);
diff --git a/src/DonorDashboards/Pipeline/Stages/UpdateDonorAvatar.php b/src/DonorDashboards/Pipeline/Stages/UpdateDonorAvatar.php
index 328b948b35..17a77a30c9 100644
--- a/src/DonorDashboards/Pipeline/Stages/UpdateDonorAvatar.php
+++ b/src/DonorDashboards/Pipeline/Stages/UpdateDonorAvatar.php
@@ -2,7 +2,11 @@
namespace Give\DonorDashboards\Pipeline\Stages;
+use Give\Log\Log;
+use WP_REST_Response;
+
/**
+ * @since 3.14.2 added security measures to updateAvatarInMetaDB
* @since 2.10.0
*/
class UpdateDonorAvatar implements Stage
@@ -23,14 +27,32 @@ public function __invoke($payload)
protected function updateAvatarInMetaDB()
{
- $attributeMetaMap = [
- 'avatarId' => '_give_donor_avatar_id',
- ];
-
- foreach ($attributeMetaMap as $attribute => $metaKey) {
- if (key_exists($attribute, $this->data)) {
- $this->donor->update_meta($metaKey, $this->data[$attribute]);
- }
+ if (!array_key_exists('avatarId', $this->data)) {
+ return false;
+ }
+
+ $avatarId = $this->data['avatarId'];
+
+ if (give()->donorDashboard->getAvatarId() && !give()->donorDashboard->avatarBelongsToCurrentUser($avatarId)) {
+ Log::error(
+ 'Avatar update permission denied.',
+ [
+ 'donorId' => give()->donorDashboard->getId(),
+ 'avatarId' => give()->donorDashboard->getAvatarId()
+ ]
+ );
+
+ return new WP_REST_Response(
+ [
+ 'status' => 401,
+ 'response' => 'unauthorized',
+ 'body_response' => [
+ 'message' => __('Avatar update permission denied.', 'give'),
+ ],
+ ]
+ );
}
+
+ return $this->donor->update_meta('_give_donor_avatar_id', $avatarId);
}
}
diff --git a/src/DonorDashboards/Profile.php b/src/DonorDashboards/Profile.php
index bad476ec46..d0c2969819 100644
--- a/src/DonorDashboards/Profile.php
+++ b/src/DonorDashboards/Profile.php
@@ -188,4 +188,12 @@ public function getCountry()
return give_get_country();
}
}
+
+ /**
+ * @since 3.14.2
+ */
+ public function avatarBelongsToCurrentUser(?int $avatarId = null): bool
+ {
+ return (int)get_post_field("post_author", $avatarId ?? $this->getAvatarId()) === get_current_user_id();
+ }
}
diff --git a/src/DonorDashboards/Tabs/EditProfileTab/AvatarRoute.php b/src/DonorDashboards/Tabs/EditProfileTab/AvatarRoute.php
index bdf4e7b7cb..3c02ff2804 100644
--- a/src/DonorDashboards/Tabs/EditProfileTab/AvatarRoute.php
+++ b/src/DonorDashboards/Tabs/EditProfileTab/AvatarRoute.php
@@ -7,6 +7,7 @@
use WP_REST_Response;
/**
+ * @since 3.14.2 added security measure avatarBelongsToCurrentUser to handleRequest
* @since 2.10.3
*/
class AvatarRoute extends RouteAbstract
@@ -35,7 +36,7 @@ public function args()
*/
public function handleRequest(WP_REST_Request $request)
{
- if ( ! (is_array($_POST) && is_array($_FILES))) {
+ if (!(is_array($_POST) && is_array($_FILES))) {
return new WP_REST_Response(
[
'status' => 400,
@@ -49,10 +50,23 @@ public function handleRequest(WP_REST_Request $request)
// Delete existing Donor profile avatar attachment
if (give()->donorDashboard->getAvatarId()) {
+ if (!give()->donorDashboard->avatarBelongsToCurrentUser()) {
+ return new WP_REST_Response(
+ [
+ 'status' => 401,
+ 'response' => 'unauthorized',
+ 'body_response' => [
+ 'message' => __('Permission denied.', 'give'),
+ ],
+ ]
+ );
+ }
+
wp_delete_attachment(give()->donorDashboard->getAvatarId(), true);
}
- if ( ! function_exists('wp_handle_upload')) {
+
+ if (!function_exists('wp_handle_upload')) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
}
@@ -105,5 +119,4 @@ public function handleRequest(WP_REST_Request $request)
]
);
}
-
}
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 (
onClick() : null}
type={type}
{...rest}
>
- {children}
+ {children}
{icon && }
);
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..f85bc162e8
--- /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";
+
+/**
+ * @since 3.17.0 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}
+
+ {__('Okay', 'give')}
+
+
+ );
+}
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()} />
-
+ >
);
};
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/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}
)}
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..778fc0cb86 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;
}
@@ -22,6 +22,10 @@ const SelectControl = ({label, value, isLoading, onChange, options, placeholder,
const selectedOptionValue = options !== null ? options.filter((option) => option.value === value) : null;
const selectStyles = {
+ menu: (provided) => ({
+ ...provided,
+ zIndex: '9999',
+ }),
control: (provided) => ({
...provided,
fontSize: '14px',
@@ -107,14 +111,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/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()} />
-
- );
-};
-
-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')}
+
+
+
+ {__('Nevermind', 'give')}
+
+
+ {__('Yes, cancel', '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..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
@@ -99,15 +99,12 @@ const AmountControl = ({currency, onChange, value, options, min, max}) => {
return (
-
-
-
-
{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 cf0f2ec23f..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
@@ -9,8 +9,27 @@ $errorColor: #c91f1f;
color: $errorColor;
}
+.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;
+ 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;
@@ -30,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;
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 (
-
-
-
-
-
-
- {updated ? (
-
- {__('Updated', 'give')}
-
- ) : (
-
- {__('Update Subscription', 'give')}{' '}
-
-
- )}
-
-
-
-
- );
-};
-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..3df618ea66
--- /dev/null
+++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/index.tsx
@@ -0,0 +1,184 @@
+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 './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);
+
+/**
+ * @since 3.19.0 Add support for hiding amount controls via filter
+ */
+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 || 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);
+
+ // 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 (
+
+ {showAmountControls && (
+
+ )}
+ {showPaymentMethodControls && (
+
+ )}
+
+ {loading &&
}
+
+
+ {showPausingControls && (
+ <>
+
+
+
+ {subscriptionStatus === 'active' ? (
+
+ {__('Pause Subscription', 'give')}
+
+ ) : (
+
+ {__('Resume Subscription', 'give')}
+
+ )}
+ >
+ )}
+
+
+ {updated ? (
+
+ {__('Updated', 'give')}
+
+ ) : (
+
+ {__('Update Subscription', 'give')}
+
+
+ )}
+
+
+ {isCancelModalOpen && (
+
setIsCancelModalOpen(!isCancelModalOpen)}
+ id={id}
+ />
+ )}
+ setIsCancelModalOpen(true)}
+ >
+ {__('Cancel Subscription', 'give')}
+
+
+ );
+};
+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 (
+
+ {__('Duration of pause', 'give')}
+
+
setPauseDuration(parseInt(e.target.value))}
+ >
+
+ {__('How long would you like to pause your subscription?', 'give')}
+
+ {durationOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+
+ {__('Pause Subscription', 'give')}
+
+
+ );
+}
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/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/index.js b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/index.js
index 85c383aa4a..9ac1754d74 100644
--- a/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/index.js
+++ b/src/DonorDashboards/resources/js/app/components/subscription-manager/payment-method-control/index.js
@@ -1,3 +1,5 @@
+import React from 'react';
+
import AuthorizeControl from './authorize-control';
import SquareControl from './square-control';
import StripeControl from './stripe-control';
@@ -5,6 +7,9 @@ import CardControl from './card-control';
import './style.scss';
+/**
+ * @since 3.19.0 Add controller for Blink payment method.
+ */
const PaymentMethodControl = (props) => {
switch (props.gateway.id) {
case 'stripe':
@@ -23,6 +28,11 @@ const PaymentMethodControl = (props) => {
case 'paypalpro': {
return ;
}
+ case 'blink': {
+ // Donor Dashboard currently loads its own version of React so we need to pass it to the component
+ const Element = wp.hooks.applyFilters('give_donor_dashboard_blink_payment_method_control', null, props);
+ return Element && ;
+ }
default: {
return null;
}
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/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..3fe85d031b
--- /dev/null
+++ b/src/DonorDashboards/resources/js/app/components/subscription-row/index.tsx
@@ -0,0 +1,88 @@
+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;
+
+ const gatewayCanUpdateSubscription = gateway.can_update || gateway.can_update_payment_method;
+
+ 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')}
+
+
+ {gatewayCanUpdateSubscription && (
+
+
+ {__('Manage Subscription', 'give')}
+
+
+ )}
+ {gateway.can_cancel && !gatewayCanUpdateSubscription && (
+ <>
+ {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/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/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/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/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/Donors/DonorsAdminPage.php b/src/Donors/DonorsAdminPage.php
index c1593c575e..5e46d7f209 100644
--- a/src/Donors/DonorsAdminPage.php
+++ b/src/Donors/DonorsAdminPage.php
@@ -104,7 +104,7 @@ public function getForms()
return array_merge([
[
'value' => '0',
- 'text' => 'Any',
+ 'text' => __('Any', 'give'),
],
], $options);
}
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/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/EventTickets/Routes/UpdateEvent.php b/src/EventTickets/Routes/UpdateEvent.php
index bcd4dc9355..d4a33da727 100644
--- a/src/EventTickets/Routes/UpdateEvent.php
+++ b/src/EventTickets/Routes/UpdateEvent.php
@@ -10,6 +10,7 @@
/**
* @since 3.6.0
+ * @since 3.14.0 add permission callback check
*/
class UpdateEvent implements RestRoute
{
@@ -28,7 +29,9 @@ public function registerRoute()
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'handleRequest'],
- 'permission_callback' => '__return_true',
+ 'permission_callback' => function () {
+ return current_user_can('edit_posts');
+ },
],
'args' => [
'event_id' => [
@@ -82,7 +85,7 @@ public function handleRequest(WP_REST_Request $request)
{
$event = Event::find($request->get_param('event_id'));
- foreach(['title', 'description', 'startDateTime', 'endDateTime'] as $param) {
+ foreach (['title', 'description', 'startDateTime', 'endDateTime'] as $param) {
if ($request->has_param($param)) {
$event->setAttribute($param, $request->get_param($param));
}
diff --git a/src/EventTickets/Routes/UpdateEventTicketType.php b/src/EventTickets/Routes/UpdateEventTicketType.php
index 53f254e2a9..dcea73838b 100644
--- a/src/EventTickets/Routes/UpdateEventTicketType.php
+++ b/src/EventTickets/Routes/UpdateEventTicketType.php
@@ -12,6 +12,7 @@
/**
* @since 3.6.0
+ * @since 3.14.0 add permission callback check
*/
class UpdateEventTicketType implements RestRoute
{
@@ -30,7 +31,9 @@ public function registerRoute()
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'handleRequest'],
- 'permission_callback' => '__return_true',
+ 'permission_callback' => function () {
+ return current_user_can('edit_posts');
+ },
],
'args' => [
'ticket_type_id' => [
diff --git a/src/Exports/DonorsExport.php b/src/Exports/DonorsExport.php
index 67fff3a8b9..2bf9b45d04 100644
--- a/src/Exports/DonorsExport.php
+++ b/src/Exports/DonorsExport.php
@@ -157,31 +157,49 @@ protected function filterExportData(array $exportData): array
}
/**
+ * @since 3.14.0
+ */
+ protected function filterColumnData(array $defaultColumns): array
+ {
+ /**
+ * @since 3.14.0
+ *
+ * @param array $defaultColumns
+ */
+ return apply_filters('give_export_donors_get_default_columns', $defaultColumns );
+ }
+
+ /**
+ * @since 3.14.0 allow cols to be filtered.
* @since 3.12.1 Include donor_phone_number col.
* @since 2.29.0 Include donor created col
* @since 2.21.2
*/
public function csv_cols(): array
{
- return $this->flattenAddressColumn(
- array_intersect_key([
- 'full_name' => __('Name', 'give'),
- 'email' => __('Email', 'give'),
- 'address' => [
- 'address_line1' => __('Address', 'give'),
- 'address_line2' => __('Address 2', 'give'),
- 'address_city' => __('City', 'give'),
- 'address_state' => __('State', 'give'),
- 'address_zip' => __('Zip', 'give'),
- 'address_country' => __('Country', 'give'),
- ],
- 'userid' => __('User ID', 'give'),
- 'donor_created_date' => __('Donor Created', 'give'),
- 'donor_phone_number' => __('Donor Phone Number', 'give'),
- 'donations' => __('Number of donations', 'give'),
- 'donation_sum' => __('Total Donated', 'give'),
- ], $this->postedData['give_export_columns'])
+ $defaultColumns = [
+ 'full_name' => __('Name', 'give'),
+ 'email' => __('Email', 'give'),
+ 'userid' => __('User ID', 'give'),
+ 'donor_created_date' => __('Donor Created', 'give'),
+ 'donor_phone_number' => __('Donor Phone Number', 'give'),
+ 'donations' => __('Number of donations', 'give'),
+ 'donation_sum' => __('Total Donated', 'give'),
+ 'address' => [
+ 'address_line1' => __('Address', 'give'),
+ 'address_line2' => __('Address 2', 'give'),
+ 'address_city' => __('City', 'give'),
+ 'address_state' => __('State', 'give'),
+ 'address_zip' => __('Zip', 'give'),
+ 'address_country' => __('Country', 'give'),
+ ],
+ ];
+
+ $defaultColumns = $this->flattenAddressColumn(
+ array_intersect_key($defaultColumns, $this->postedData['give_export_columns'])
);
+
+ return $this->filterColumnData($defaultColumns);
}
/**
diff --git a/src/FeatureFlags/FeatureFlags.php b/src/FeatureFlags/FeatureFlags.php
new file mode 100644
index 0000000000..45aa47a8b6
--- /dev/null
+++ b/src/FeatureFlags/FeatureFlags.php
@@ -0,0 +1,14 @@
+
+
+
+
+
%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')
+ );
+ }
+
+ /**
+ * @since 3.18.0
+ */
+ 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..e7afcb15a9
--- /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..fdfd432671
--- /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;
+ }
+
+ /**
+ * @since 3.18.0
+ */
+ 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;
+ }
+
+ /**
+ * @since 3.18.0
+ */
+ final public function maybeSetNewDefaultSection($currentSection)
+ {
+ if (OptionBasedFormEditor::isEnabled()) {
+ return $currentSection;
+ }
+
+ $newDefaultSection = $this->getNewDefaultSection();
+
+ return ! empty($newDefaultSection) && $newDefaultSection != $currentSection ? $newDefaultSection : $currentSection;
+ }
+
+ /**
+ * @since 3.18.0
+ */
+ private function isOptionDisabled($option): bool
+ {
+ return $option && in_array($option, $this->getDisabledOptionIds());
+ }
+
+ /**
+ * @since 3.18.0
+ */
+ 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..8d88292662
--- /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..ab2c15509b
--- /dev/null
+++ b/src/FeatureFlags/OptionBasedFormEditor/Settings/DefaultOptions.php
@@ -0,0 +1,29 @@
+settings->designSettingsImageUrl;
+ }
+
return get_the_post_thumbnail_url($formId, 'full');
}
diff --git a/src/FormBuilder/Controllers/FormBuilderResourceController.php b/src/FormBuilder/Controllers/FormBuilderResourceController.php
index 8ddba0438d..60d87dd2e7 100644
--- a/src/FormBuilder/Controllers/FormBuilderResourceController.php
+++ b/src/FormBuilder/Controllers/FormBuilderResourceController.php
@@ -94,7 +94,11 @@ public function update(WP_REST_Request $request)
$form->status = $updatedSettings->formStatus;
$form->save();
- do_action('givewp_form_builder_updated', $form);
+ /**
+ * @since 3.16.0 Add the request as an additional parameter.
+ * @since 3.0.0
+ */
+ do_action('givewp_form_builder_updated', $form, $request);
return rest_ensure_response([
'settings' => $form->settings->toJson(),
diff --git a/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php b/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php
index 7904d9ffdb..b61ace3ead 100644
--- a/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php
+++ b/src/FormBuilder/Routes/RegisterFormBuilderPageRoute.php
@@ -157,6 +157,14 @@ public function renderPage()
'isDismissed' => get_user_meta(get_current_user_id(), 'givewp-goal-notice-dismissed', true),
]);
+ /**
+ * @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'),
+ 'isDismissed' => get_user_meta(get_current_user_id(), 'givewp-additional-payment-gateways-notice-dismissed', true),
+ ]);
+
View::render('FormBuilder.admin-form-builder');
}
diff --git a/src/FormBuilder/ServiceProvider.php b/src/FormBuilder/ServiceProvider.php
index 507d26d29d..8d78b43b88 100644
--- a/src/FormBuilder/ServiceProvider.php
+++ b/src/FormBuilder/ServiceProvider.php
@@ -88,5 +88,12 @@ protected function setupOnboardingTour()
add_action('wp_ajax_givewp_goal_hide_notice', static function () {
add_user_meta(get_current_user_id(), 'givewp-goal-notice-dismissed', time(), true);
});
+
+ /**
+ * @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/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/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}) => {
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
/>
)}
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..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
@@ -2,6 +2,17 @@
.components-checkbox-control__label {
font-size: 1rem;
font-weight: 500;
+ line-height: 24px;
+ color: var(--givewp-gray-100);
+ }
+
+ .components-base-control__help {
+ font-size: 0.875rem;
+ 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;
+ }
}
}
diff --git a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/billing-address/Edit.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/billing-address/Edit.tsx
index 017ca77577..d79b3d0435 100644
--- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/billing-address/Edit.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/billing-address/Edit.tsx
@@ -142,7 +142,7 @@ export default function Edit({
/>
-
+
-
+
-
+
-
+
-
+
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,66 +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 && (
)}
-
-
- {/* 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 ? 'true' : 'false'}
+ 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 && (
+
+
-
+ )}
setAttributes({firstNameLabel: value})}
+ onChange={(value) => setAttributes({ firstNameLabel: value })}
/>
setAttributes({firstNamePlaceholder: value})}
+ onChange={(value) => setAttributes({ firstNamePlaceholder: value })}
/>
@@ -185,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/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')}
/>
- {__('Log In', 'give')}
- {__('Reset Password', 'give')}
+
+ {__('Log In', 'give')}
+
+
+ {__('Forgot your password?', 'give')} {__('Reset', 'give')}
+
)}
{!required && (
-
+
: undefined}
+ icon={
+ !!loginRedirect ? (
+
+ ) : undefined
+ }
// iconPosition={'right' as 'left' | 'right'} // The icon position does not seem to be working.
- style={{flexDirection: 'row-reverse'}}
+ style={{
+ flexDirection: 'row-reverse',
+ fontSize: '14px',
+ height: 'auto',
+ lineHeight: '1.5',
+ padding: '0',
+ }}
>
{loginNotice}
@@ -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/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/Edit.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/Edit.tsx
index e2d7080e5c..3974a68fa0 100644
--- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/Edit.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/payment-gateways/Edit.tsx
@@ -2,6 +2,10 @@ import {ReactNode} from 'react';
import {BlockEditProps} from '@wordpress/blocks';
import {getFormBuilderWindowData} from '@givewp/form-builder/common/getWindowData';
import {applyFilters} from '@wordpress/hooks';
+import {InspectorControls} from "@wordpress/block-editor";
+import {__} from "@wordpress/i18n";
+import {Icon} from '@wordpress/components';
+import {external} from "@wordpress/icons";
const GatewayItem = ({label, icon}: {label: string; icon: ReactNode}) => {
return (
@@ -62,6 +66,20 @@ export default function Edit(props: BlockEditProps
) {
/>
))}
+
+
+
);
}
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/blocks/fields/terms-and-conditions/Edit.tsx b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/terms-and-conditions/Edit.tsx
index ee4835b344..b727e699fe 100644
--- a/src/FormBuilder/resources/js/form-builder/src/blocks/fields/terms-and-conditions/Edit.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/blocks/fields/terms-and-conditions/Edit.tsx
@@ -103,7 +103,7 @@ export default function Edit({
/>
- {isLinkDisplay && (
+ {(isLinkDisplay || isModalDisplay) && (
Acceptance of any contribution, gift or grant is at the discretion of the GiveWP. The GiveWP will not accept any gift unless it can be used or expended consistently with the purpose and mission of the GiveWP. No irrevocable gift, whether outright or life-income in character, will be accepted if under any reasonable set of circumstances the gift would jeopardize the donor’s financial security.The GiveWP will refrain from providing advice about the tax or other treatment of gifts and will encourage donors to seek guidance from their own professional advisers to assist them in the process of making their donation.',
+ 'Acceptance of any contribution, gift or grant is at the discretion of the GiveWP.
' +
+ 'The GiveWP will not accept any gift unless it can be used or expended consistently with the purpose and mission of the GiveWP.
' +
+ 'No irrevocable gift, whether outright or life-income in character, will be accepted if under any reasonable set of circumstances the gift would jeopardize the donor’s financial security.
' +
+ 'The GiveWP will refrain from providing advice about the tax or other treatment of gifts and will encourage donors to seek guidance from their own professional advisers to assist them in the process of making their donation.
',
'give'
),
},
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={[]}
/>
)}
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%;
+}
diff --git a/src/FormBuilder/resources/js/form-builder/src/components/EmbedForm/index.tsx b/src/FormBuilder/resources/js/form-builder/src/components/EmbedForm/index.tsx
index b86663f06a..9d5334bb4c 100644
--- a/src/FormBuilder/resources/js/form-builder/src/components/EmbedForm/index.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/components/EmbedForm/index.tsx
@@ -86,8 +86,8 @@ export default function EmbedFormModal({handleClose}: EmbedFormModalProps) {
}, []);
const postOptions = [
- {label: 'Page', value: 'page'},
- {label: 'Post', value: 'post'},
+ {label: __('Page', 'give'), value: 'page'},
+ {label: __('Post', 'give'), value: 'post'},
];
const displayStyles = [
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',
};
diff --git a/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/schema-steps.tsx b/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/schema-steps.tsx
index d1618d8df3..7f6720bd66 100644
--- a/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/schema-steps.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/components/onboarding/steps/schema-steps.tsx
@@ -4,7 +4,7 @@ import Placement from '@givewp/form-builder/components/onboarding/steps/types/pl
const schemaSteps = [
{
id: 'schema-canvas',
- attachTo: {element: '#form-blocks-canvas', on: 'right-start' as Placement},
+ attachTo: {element: '.interface-interface-skeleton__content', on: 'right-start' as Placement},
title: __('Canvas', 'give'),
text: __('Add, reorder, and edit blocks and sections here to make up your form.', 'give'),
},
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..b0e3725bb7
--- /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'
+
+/**
+ * @since 3.16.2
+ */
+const InspectorNotice = ({title, description, helpText, helpUrl, onDismiss}) => {
+
+ return (
+
+ )
+}
+
+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/src/FormBuilder/resources/js/form-builder/src/containers/BlockEditorInterfaceSkeletonContainer.tsx b/src/FormBuilder/resources/js/form-builder/src/containers/BlockEditorInterfaceSkeletonContainer.tsx
index 59c2763c26..831106c845 100644
--- a/src/FormBuilder/resources/js/form-builder/src/containers/BlockEditorInterfaceSkeletonContainer.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/containers/BlockEditorInterfaceSkeletonContainer.tsx
@@ -14,6 +14,7 @@ import {Button} from '@wordpress/components';
import {listView, plus} from '@wordpress/icons';
import {useEditorState} from '@givewp/form-builder/stores/editor-state';
import EditorMode from '@givewp/form-builder/types/editorMode';
+import {__} from "@wordpress/i18n";
export default function BlockEditorInterfaceSkeletonContainer() {
const {mode} = useEditorState();
@@ -69,6 +70,8 @@ const SchemaEditorSkeleton = () => {
isPressed={'add' === selectedSecondarySidebar}
icon={plus}
variant="primary"
+ title={__('Toggle block inserter', 'give')}
+ label={__('Toggle block inserter', 'give')}
/>
{
onClick={() => toggleSelectedSecondarySidebar('list')}
isPressed={'list' === selectedSecondarySidebar}
icon={listView}
+ title={__('Form Field Overview', 'give')}
+ label={__('Form Field Overview', 'give')}
/>
>
);
diff --git a/src/FormBuilder/resources/js/form-builder/src/containers/HeaderContainer.tsx b/src/FormBuilder/resources/js/form-builder/src/containers/HeaderContainer.tsx
index 60010804ea..5278a1146e 100644
--- a/src/FormBuilder/resources/js/form-builder/src/containers/HeaderContainer.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/containers/HeaderContainer.tsx
@@ -208,10 +208,12 @@ const HeaderContainer = ({SecondarySidebarButtons = null, showSidebar, toggleSho
isPressed={showEmbedModal}
onClick={() => setShowEmbedModal(!showEmbedModal)}
label={__('Embed form', 'give')}
+ title={__('Embed form', 'give')}
/>
{isPublished && (
{mode !== EditorMode.settings && (
-
+
)}
>
)}
@@ -249,6 +257,8 @@ const HeaderContainer = ({SecondarySidebarButtons = null, showSidebar, toggleSho
}
onToggle();
}}
+ label={__('Options', 'give')}
+ title={__('Options', 'give')}
/>
);
}}
diff --git a/src/FormBuilder/resources/js/form-builder/src/promos/recurring-donations.tsx b/src/FormBuilder/resources/js/form-builder/src/promos/recurring-donations.tsx
index c78823c6db..52bc464d8a 100644
--- a/src/FormBuilder/resources/js/form-builder/src/promos/recurring-donations.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/promos/recurring-donations.tsx
@@ -19,8 +19,8 @@ const RecurringDonationsPromo = () => {
{__('Provide donors the option of making flexible recurring donations.', 'give')}
- Upgrade your plan
- Read more
+ {__('Upgrade your plan', 'give')}
+ {__('Read more', 'give')}
}
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..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
@@ -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';
+/**
+ * @since 3.16.2 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 && (
-
)}
diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/design/style-controls/color/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/design/style-controls/color/index.tsx
index 0e0775d742..1dbe89c0b0 100644
--- a/src/FormBuilder/resources/js/form-builder/src/settings/design/style-controls/color/index.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/settings/design/style-controls/color/index.tsx
@@ -1,7 +1,7 @@
import {__} from '@wordpress/i18n';
import {setFormSettings, useFormState} from '@givewp/form-builder/stores/form-state';
import useDonationFormPubSub from '@givewp/forms/app/utilities/useDonationFormPubSub';
-import {PanelColorSettings, SETTINGS_DEFAULTS} from '@wordpress/block-editor';
+import {PanelColorSettings} from '@wordpress/block-editor';
import {PanelBody} from '@wordpress/components';
export default function Color({dispatch}) {
@@ -11,6 +11,20 @@ export default function Color({dispatch}) {
const {publishColors} = useDonationFormPubSub();
+ const defaultColors = [
+ {name: 'Black', slug: 'black', color: '#000000'},
+ {name: 'Dark Blue', slug: 'dark-blue', color: '#1E1AE2'},
+ {name: 'Give Primary Default', slug: 'give-primary-default', color: '#69b86b'},
+ {name: 'Red', slug: 'red', color: '#BD3D36'},
+ {name: 'Orange', slug: 'orange', color: '#EB712E'},
+ {name: 'Gray', slug: 'gray', color: '#5F7385'},
+ {name: 'Light Blue', slug: 'light-blue', color: '#4492DD'},
+ {name: 'Light Green', slug: 'light-green', color: '#63CC8A'},
+ {name: 'Purple', slug: 'purple', color: '#9058D8'},
+ {name: 'Teal', slug: 'teal', color: '#2BBAB1'},
+ {name: 'Yellow', slug: 'yellow', color: '#F1BB40'},
+ ];
+
return (
diff --git a/src/FormBuilder/resources/js/form-builder/src/settings/group-donation-confirmation/index.tsx b/src/FormBuilder/resources/js/form-builder/src/settings/group-donation-confirmation/index.tsx
index a837ca725c..7091f3b502 100644
--- a/src/FormBuilder/resources/js/form-builder/src/settings/group-donation-confirmation/index.tsx
+++ b/src/FormBuilder/resources/js/form-builder/src/settings/group-donation-confirmation/index.tsx
@@ -1,17 +1,19 @@
import {__} from '@wordpress/i18n';
-import {PanelRow} from '@wordpress/components';
+import {PanelRow, ToggleControl} from '@wordpress/components';
import {SettingsSection} from '@givewp/form-builder-library';
import DonationConfirmation from './donation-confirmation';
import {getFormBuilderWindowData} from '@givewp/form-builder/common/getWindowData';
import TemplateTags from '@givewp/form-builder/components/settings/TemplateTags';
+import {createInterpolateElement} from '@wordpress/element';
const {donationConfirmationTemplateTags} = getFormBuilderWindowData();
/**
+ * @since 3.16.0 Added setting for enableReceiptConfirmationPage
* @since 3.3.0
*/
export default function FormDonationConfirmationSettingsGroup({settings, setSettings}) {
- const {receiptHeading, receiptDescription} = settings;
+ const {receiptHeading, receiptDescription, enableReceiptConfirmationPage} = settings;
return (
<>
@@ -46,6 +48,21 @@ export default function FormDonationConfirmationSettingsGroup({settings, setSett
+
+
+ setSettings({enableReceiptConfirmationPage: !enableReceiptConfirmationPage})}
+ help={createInterpolateElement(
+ __( 'When enabled, donors are redirected to a separate page to view their donation confirmation rather than viewing it on the donation form page. This can be useful for event and conversion tracking tools like Google Analytics. Learn how to customize the confirmation page. ', 'give' ),
+ {
+ a: ,
+ }
+ )}
+ />
+
+
>
);
}
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 && (
div {
@@ -206,8 +213,13 @@
gap: 2rem;
.block-editor-block-list__block {
+ max-width: 720px;
+ padding: 0 0.5rem;
+
&:not([contenteditable]):focus::after {
box-shadow: none;
+ content: none;
+ outline-width: 0;
}
&.is-highlighted {
@@ -296,3 +308,6 @@
}
}
+.givewp-form-settings__editor .interface-interface-skeleton {
+ width: 100%;
+}
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;
+}
diff --git a/src/FormBuilder/resources/js/form-builder/src/styles/_form-builder.scss b/src/FormBuilder/resources/js/form-builder/src/styles/_form-builder.scss
index 14104e00b4..1d3a2e7e0e 100644
--- a/src/FormBuilder/resources/js/form-builder/src/styles/_form-builder.scss
+++ b/src/FormBuilder/resources/js/form-builder/src/styles/_form-builder.scss
@@ -42,15 +42,11 @@
border: 1px solid lightgray;
bottom: 0;
//color: slategray;
- height: calc(100vh - 60px); // Account for header height above sidebar
+ height: calc(100vh - 65px); // Account for header height above sidebar
//overflow: hidden; // The Inspector Popout needs to be visible.
- position: fixed !important; // !important override added when migrated to WordPress - not sure why this is necessary...
- top: 60px; // Account for header @todo move this to layout, not in the component.
overflow-y: scroll;
&.givewp-next-gen-sidebar-primary {
- right: 0;
-
.block-editor-block-inspector {
> h2 {
border-bottom: 1px solid #e0e0e0;
@@ -70,10 +66,6 @@
}
}
- &.givewp-next-gen-sidebar-secondary {
- left: 0;
- }
-
&__inner {
padding: 1rem;
}
@@ -249,7 +241,7 @@
}
}
- &_visibility{
+ &_visibility {
font-size: 0.75rem;
line-height: 1.4;
margin-top: -8px;
@@ -309,9 +301,37 @@
fill: #000;
stroke: #fff;
}
- };
+ }
+;
}
.iti {
width: 100%;
}
+
+/**
+ * WordPress 6.6 compatibility
+ */
+.givewp-next-gen-sidebar-secondary {
+ // Hide the Blocks, Patterns, Media tabs
+ .block-editor-inserter__tablist-and-close-button {
+ display: none;
+ }
+}
+
+
+/**
+ * WordPress 6.7 compatibility
+ */
+.givewp-next-gen-sidebar-secondary {
+ // Hide the Blocks, Patterns, Media tabs (css name change)
+ .block-editor-tabbed-sidebar__tablist-and-close-button {
+ display: none;
+ }
+
+ // WP adds a fixed width to the tabbed sidebar, we need to override it
+ .block-editor-inserter__main-area .block-editor-tabbed-sidebar {
+ width: 100%;
+ }
+}
+
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);
diff --git a/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts b/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts
index 4171e7a1ff..4ce09a992d 100644
--- a/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts
+++ b/src/FormBuilder/resources/js/form-builder/src/types/formSettings.ts
@@ -2,6 +2,7 @@ import {FormStatus} from '@givewp/form-builder/types/formStatus';
import {EmailTemplateOption} from '@givewp/form-builder/types/emailTemplateOption';
/**
+ * @since 3.16.0 Added enableReceiptConfirmationPage
* @since 3.7.0 Added formExcerpt
* @since 3.0.0
*/
@@ -56,4 +57,5 @@ export type FormSettings = {
designSettingsImageOpacity: string;
designSettingsImageColor: string;
formExcerpt: string;
+ enableReceiptConfirmationPage: boolean;
};
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);
}
}
diff --git a/src/FormMigration/FormMetaDecorator.php b/src/FormMigration/FormMetaDecorator.php
index 960cb825b0..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);
}
@@ -881,7 +881,7 @@ public function isConvertKitEnabled(): bool
$isGloballyEnabled = $this->getMeta('_give_convertkit_override_option') === 'default' &&
give_is_setting_enabled(give_get_option('give_convertkit_show_subscribe_checkbox'));
- return $isFormEnabled ? $isGloballyEnabled : $isFormDisabled;
+ return ! ($isFormDisabled || ( ! $isGloballyEnabled && ! $isFormEnabled));
}
/**
@@ -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,10 +1016,18 @@ public function getCurrencySwitcherDefaultCurrency(): string
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function getCurrencySwitcherSupportedCurrencies(): array
{
return (array)$this->getMeta('cs_supported_currency', []);
}
+
+ /**
+ * @since 3.14.0
+ */
+ public function isRazorpayPerFormSettingsEnabled(): bool
+ {
+ return give_is_setting_enabled($this->getMeta('razorpay_per_form_account_options'));
+ }
}
diff --git a/src/FormMigration/ServiceProvider.php b/src/FormMigration/ServiceProvider.php
index 34e2596524..094587ac38 100644
--- a/src/FormMigration/ServiceProvider.php
+++ b/src/FormMigration/ServiceProvider.php
@@ -37,6 +37,7 @@ public function register()
Steps\FormFields\CompanyDonations::class,
Steps\DonationGoal::class,
Steps\TermsAndConditions::class,
+ Steps\FormTaxonomies::class,
Steps\FormGrid::class,
Steps\FormFieldManager::class,
Steps\OfflineDonations::class,
@@ -56,6 +57,7 @@ public function register()
Steps\ActiveCampaign::class,
Steps\DoubleTheDonation::class,
Steps\CurrencySwitcher::class,
+ Steps\RazorpayPerFormSettings::class,
]);
});
}
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/FormMigration/Steps/FormTaxonomies.php b/src/FormMigration/Steps/FormTaxonomies.php
new file mode 100644
index 0000000000..8014121680
--- /dev/null
+++ b/src/FormMigration/Steps/FormTaxonomies.php
@@ -0,0 +1,34 @@
+migrateTaxonomy('give_forms_tag');
+ }
+
+ if (taxonomy_exists('give_forms_category')) {
+ $this->migrateTaxonomy('give_forms_category');
+ }
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function migrateTaxonomy($taxonomy): void
+ {
+ $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/src/FormMigration/Steps/RazorpayPerFormSettings.php b/src/FormMigration/Steps/RazorpayPerFormSettings.php
new file mode 100644
index 0000000000..01372b080d
--- /dev/null
+++ b/src/FormMigration/Steps/RazorpayPerFormSettings.php
@@ -0,0 +1,56 @@
+formV2->isRazorpayPerFormSettingsEnabled();
+ }
+
+ /**
+ * @since 3.14.0
+ */
+ public function process()
+ {
+ $oldFormId = $this->formV2->id;
+
+ $paymentGatewaysBlock = $this->fieldBlocks->findByName('givewp/payment-gateways');
+
+ $paymentGatewaysBlock->setAttribute('razorpayUseGlobalSettings',
+ $this->getMetaValue($oldFormId, 'razorpay_per_form_account_options', 'global') === 'global');
+
+ $paymentGatewaysBlock->setAttribute('razorpayLiveKeyId',
+ $this->getMetaValue($oldFormId, 'razorpay_per_form_live_merchant_key_id', ''));
+ $paymentGatewaysBlock->setAttribute('razorpayLiveSecretKey',
+ $this->getMetaValue($oldFormId, 'razorpay_per_form_live_merchant_secret_key', ''));
+
+ $paymentGatewaysBlock->setAttribute('razorpayTestKeyId',
+ $this->getMetaValue($oldFormId, 'razorpay_per_form_test_merchant_key_id', ''));
+ $paymentGatewaysBlock->setAttribute('razorpayTestSecretKey',
+ $this->getMetaValue($oldFormId, 'razorpay_per_form_test_merchant_secret_key', ''));
+ }
+
+ /**
+ * @since 3.14.0
+ */
+ private function getMetaValue(int $formId, string $metaKey, $defaultValue)
+ {
+ $metaValue = give()->form_meta->get_meta($formId, $metaKey, true);
+
+ if ( ! $metaValue) {
+ return $defaultValue;
+ }
+
+ return $metaValue;
+ }
+}
diff --git a/src/FormTaxonomies/Actions/EnqueueFormBuilderAssets.php b/src/FormTaxonomies/Actions/EnqueueFormBuilderAssets.php
new file mode 100644
index 0000000000..05d78341ef
--- /dev/null
+++ b/src/FormTaxonomies/Actions/EnqueueFormBuilderAssets.php
@@ -0,0 +1,60 @@
+viewModel = $viewModel;
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function __invoke()
+ {
+ if($this->viewModel->isFormTagsEnabled() || $this->viewModel->isFormCategoriesEnabled()) {
+
+ $scriptAsset = ScriptAsset::get(GIVE_PLUGIN_DIR . 'build/formTaxonomySettings.asset.php');
+
+ wp_enqueue_script(
+ 'givewp-builder-taxonomy-settings',
+ GIVE_PLUGIN_URL . 'build/formTaxonomySettings.js',
+ $scriptAsset['dependencies'],
+ $scriptAsset['version'],
+ true
+ );
+
+ Language::setScriptTranslations('givewp-builder-taxonomy-settings');
+
+ wp_enqueue_style(
+ 'givewp-builder-taxonomy-settings',
+ GIVE_PLUGIN_URL . 'build/style-formTaxonomySettings.css'
+ );
+
+ wp_add_inline_script('givewp-builder-taxonomy-settings','var giveTaxonomySettings =' . json_encode([
+ 'formTagsEnabled' => $this->viewModel->isFormTagsEnabled(),
+ 'formCategoriesEnabled' => $this->viewModel->isFormCategoriesEnabled(),
+ 'formTagsSelected' => $this->viewModel->getSelectedFormTags(),
+ 'formCategoriesAvailable' => $this->viewModel->getFormCategories(),
+ 'formCategoriesSelected' => $this->viewModel->getSelectedFormCategories(),
+ ]));
+ }
+ }
+}
diff --git a/src/FormTaxonomies/Actions/UpdateFormTaxonomies.php b/src/FormTaxonomies/Actions/UpdateFormTaxonomies.php
new file mode 100644
index 0000000000..acd9f48904
--- /dev/null
+++ b/src/FormTaxonomies/Actions/UpdateFormTaxonomies.php
@@ -0,0 +1,38 @@
+get_param('settings'), true);
+
+ if(isset($formBuilderSettings['formTags'])) {
+ $formTags = $this->validateTermIds(array_column($formBuilderSettings['formTags'], 'id'));
+ wp_set_object_terms($form->id, $formTags, 'give_forms_tag');
+ }
+
+ if(isset($formBuilderSettings['formCategories'])) {
+ $formCategories = $this->validateTermIds($formBuilderSettings['formCategories']);
+ wp_set_object_terms($form->id, $formCategories, 'give_forms_category');
+ }
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function validateTermIds(array $termsIds): array
+ {
+ return array_unique( array_map( 'intval', $termsIds ) );
+ }
+}
diff --git a/src/FormTaxonomies/ServiceProvider.php b/src/FormTaxonomies/ServiceProvider.php
new file mode 100644
index 0000000000..880998ba5a
--- /dev/null
+++ b/src/FormTaxonomies/ServiceProvider.php
@@ -0,0 +1,35 @@
+bind(Actions\EnqueueFormBuilderAssets::class, function() {
+ $formId = absint($_GET['donationFormID'] ?? 0);
+ return new Actions\EnqueueFormBuilderAssets(
+ new ViewModels\FormTaxonomyViewModel($formId, give_get_settings())
+ );
+ });
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function boot()
+ {
+ Hooks::addAction('givewp_form_builder_updated', Actions\UpdateFormTaxonomies::class, '__invoke', 10, 2);
+ Hooks::addAction('givewp_form_builder_enqueue_scripts', Actions\EnqueueFormBuilderAssets::class);
+ }
+}
diff --git a/src/FormTaxonomies/ViewModels/FormTaxonomyViewModel.php b/src/FormTaxonomies/ViewModels/FormTaxonomyViewModel.php
new file mode 100644
index 0000000000..bebd2a56d4
--- /dev/null
+++ b/src/FormTaxonomies/ViewModels/FormTaxonomyViewModel.php
@@ -0,0 +1,104 @@
+formId = $formId;
+ $this->settings = $settings;
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function isFormTagsEnabled(): bool
+ {
+ return give_is_setting_enabled($this->settings['tags']);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function isFormCategoriesEnabled(): bool
+ {
+ return give_is_setting_enabled($this->settings['categories']);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function getSelectedFormTags(): array
+ {
+ if(!$this->isFormTagsEnabled()) {
+ return [];
+ }
+
+ $terms = wp_get_post_terms($this->formId, 'give_forms_tag');
+
+ return array_map(function ($term) {
+ return [
+ 'id' => $term->term_id,
+ 'value' => $term->name,
+ ];
+ }, $terms) ?? [];
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function getFormCategories(): array
+ {
+ if(!$this->isFormCategoriesEnabled()) {
+ return [];
+ }
+
+ $terms = get_terms([
+ 'taxonomy' => 'give_forms_category',
+ 'hide_empty' => false,
+ ]);
+
+ return array_map(function ($term) {
+ return [
+ 'id' => $term->term_id,
+ 'name' => $term->name,
+ 'parent' => $term->parent,
+ ];
+ }, $terms) ?? [];
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function getSelectedFormCategories(): array
+ {
+ if(!$this->isFormCategoriesEnabled()) {
+ return [];
+ }
+
+ $terms = wp_get_post_terms($this->formId, 'give_forms_category');
+
+ return array_map(function ($term) {
+ return $term->term_id;
+ }, $terms) ?? [];
+ }
+}
diff --git a/src/FormTaxonomies/resources/form-builder/form-categories.tsx b/src/FormTaxonomies/resources/form-builder/form-categories.tsx
new file mode 100644
index 0000000000..92abdaa048
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/form-categories.tsx
@@ -0,0 +1,63 @@
+import {getAvailableFormCategories, getInitialFormCategories} from "./windowData";
+import {buildTermsTree} from "./utils/terms";
+import {CheckboxControl} from "@wordpress/components";
+import {decodeEntities} from "@wordpress/html-entities";
+import {useMemo} from "react";
+
+/**
+ * @since 3.16.0
+ */
+const FormCategorySetting = ({settings, setSettings}) => {
+ const {
+ formCategories = getInitialFormCategories(),
+ } = settings;
+
+ const categoryTree = useMemo(() => buildTermsTree(getAvailableFormCategories()), [])
+
+ /**
+ * @since 3.16.0
+ */
+ const onChange = (categoryId ) => {
+ setSettings({formCategories: formCategories.includes( categoryId )
+ ? formCategories.filter( ( id ) => id !== categoryId )
+ : [ ...formCategories, categoryId ]
+ })
+ };
+
+ return (
+
+ {renderTerms(categoryTree, formCategories, onChange)}
+
+ );
+}
+
+/**
+ * @since 3.16.0
+ */
+const renderTerms = (availableTerms, selectedTerms, onChange) => {
+ return availableTerms.map((term) => {
+ return (
+
+
{
+ const termId = parseInt( term.id, 10 );
+ onChange( termId );
+ } }
+ label={ decodeEntities( term.name ) }
+ />
+ { !! term.children.length && (
+
+ { renderTerms( term.children, selectedTerms, onChange ) }
+
+ ) }
+
+ );
+ } );
+};
+
+export default FormCategorySetting;
diff --git a/src/FormTaxonomies/resources/form-builder/form-tags.tsx b/src/FormTaxonomies/resources/form-builder/form-tags.tsx
new file mode 100644
index 0000000000..e7829bc3f0
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/form-tags.tsx
@@ -0,0 +1,68 @@
+import {__} from "@wordpress/i18n";
+import {debounce} from "@wordpress/compose";
+import apiFetch from '@wordpress/api-fetch';
+import {FormTokenField} from "@wordpress/components";
+import {getInitialFormTags} from "./windowData";
+import {useState} from "react";
+
+/**
+ * @since 3.16.0
+ */
+const FormTagSetting = ({settings, setSettings}) => {
+ const {formTags = getInitialFormTags()} = settings;
+ const setFormTags = (tags) => setSettings({formTags: tags})
+ const [searchResults, setSearchResults] = useState([])
+
+ /**
+ * @since 3.16.0
+ */
+ const searchTags = (search) => apiFetch({path: '/wp/v2/give_forms_tag?search=' + search}).then(setSearchResults)
+
+ /**
+ * @since 3.16.0
+ */
+ const resolveFormTags = ( tags ) => {
+ const [newTag, isNewAndUnique] = validateNewAndUnique(tags, formTags);
+ isNewAndUnique
+ ? findOrCreateTag(newTag, (id) => setFormTags([...formTags, {id, value: newTag}]))
+ : setFormTags(tags)
+ }
+
+ return tag.name)}
+ disabled={!formTags}
+ >
+}
+
+/**
+ * @since 3.16.0
+ */
+const findOrCreateTag = (name, callback) => {
+ apiFetch( {path: '/wp/v2/give_forms_tag', method: 'POST', data: { name }} )
+ .then((response: any) => callback(response.id))
+ .catch((error) => {
+ if ( error.code !== 'term_exists' ) {
+ throw error;
+ }
+ callback(error.data.term_id)
+ })
+}
+
+/**
+ * @since 3.16.0
+ */
+const validateNewAndUnique = (tags, previousTags): [string|null, boolean] => {
+ // @note New terms are simple string inputs, as opposed to resolved objects ({id, value}).
+ const newTag = tags.find( ( term ) => typeof term === "string" )
+ const isUnique = !previousTags.some( ( tag ) => newTag === tag.value.toLowerCase() )
+ return [
+ newTag,
+ (newTag && isUnique)
+ ]
+}
+
+export default FormTagSetting;
diff --git a/src/FormTaxonomies/resources/form-builder/index.tsx b/src/FormTaxonomies/resources/form-builder/index.tsx
new file mode 100644
index 0000000000..3a20739437
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/index.tsx
@@ -0,0 +1,5 @@
+import {addFilter} from "@wordpress/hooks";
+import withTaxonomySettingsRoute from "./taxonomy-settings";
+import './style.scss';
+
+addFilter('givewp_form_builder_settings_additional_routes', 'givewp/form-taxonomies', withTaxonomySettingsRoute);
diff --git a/src/FormTaxonomies/resources/form-builder/style.scss b/src/FormTaxonomies/resources/form-builder/style.scss
new file mode 100644
index 0000000000..25bdc97e13
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/style.scss
@@ -0,0 +1,6 @@
+#give-form-settings__form-taxonomies {
+ .components-form-token-field__input {
+ border: 0 !important; // Remove input border to match Block Editor.
+ padding: 8px; // Remove input padding to match Block Editor.
+ }
+}
diff --git a/src/FormTaxonomies/resources/form-builder/taxonomy-settings.tsx b/src/FormTaxonomies/resources/form-builder/taxonomy-settings.tsx
new file mode 100644
index 0000000000..b6c5fae8db
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/taxonomy-settings.tsx
@@ -0,0 +1,60 @@
+import {SettingsSection} from "@givewp/form-builder-library";
+import {__} from "@wordpress/i18n";
+import {PanelRow} from "@wordpress/components";
+import FormTagSetting from "./form-tags";
+import FormCategorySetting from "./form-categories";
+import getWindowData, {isFormCategoriesEnabled, isFormTagsEnabled} from "./windowData";
+
+/**
+ * @since 3.16.0
+ */
+const TaxonomySettings = ({settings, setSettings}) => {
+
+ return (
+
+ )
+}
+
+/**
+ * @since 3.16.0
+ */
+export default function withTaxonomySettingsRoute (routes) {
+
+ const isFormTagsEnabled = getWindowData().formTagsEnabled;
+ const isFormCategoriesEnabled = getWindowData().formCategoriesEnabled;
+
+ /**
+ * @since 3.16.0
+ */
+ const getDynamicLabel = () => {
+ return isFormTagsEnabled && isFormCategoriesEnabled ? __('Tags and Categories', '')
+ : isFormTagsEnabled ? __('Form Tags', '')
+ : isFormCategoriesEnabled ? __('Form Categories', '') : '';
+ }
+
+ return [
+ ...routes,
+ {
+ name: getDynamicLabel(),
+ path: 'give-form-tags',
+ element: TaxonomySettings,
+ },
+ ];
+}
diff --git a/src/FormTaxonomies/resources/form-builder/utils/terms.ts b/src/FormTaxonomies/resources/form-builder/utils/terms.ts
new file mode 100644
index 0000000000..6cd84a6d0e
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/utils/terms.ts
@@ -0,0 +1,91 @@
+/**
+ * WordPress dependencies
+ */
+import { decodeEntities } from '@wordpress/html-entities';
+
+/**
+ * Returns terms in a tree form.
+ *
+ * @since 3.16.0
+ *
+ * @param {Array} flatTerms Array of terms in flat format.
+ *
+ * @return {Array} Array of terms in tree format.
+ */
+export function buildTermsTree( flatTerms ) {
+ const flatTermsWithParentAndChildren = flatTerms.map( ( term ) => {
+ return {
+ children: [],
+ parent: null,
+ ...term,
+ };
+ } );
+
+ // All terms should have a `parent` because we're about to index them by it.
+ if (
+ flatTermsWithParentAndChildren.some( ( { parent } ) => parent === null )
+ ) {
+ return flatTermsWithParentAndChildren;
+ }
+
+ const termsByParent = flatTermsWithParentAndChildren.reduce(
+ ( acc, term ) => {
+ const { parent } = term;
+ if ( ! acc[ parent ] ) {
+ acc[ parent ] = [];
+ }
+ acc[ parent ].push( term );
+ return acc;
+ },
+ {}
+ );
+
+ const fillWithChildren = ( terms ) => {
+ return terms.map( ( term ) => {
+ const children = termsByParent[ term.id ];
+ return {
+ ...term,
+ children:
+ children && children.length
+ ? fillWithChildren( children )
+ : [],
+ };
+ } );
+ };
+
+ return fillWithChildren( termsByParent[ '0' ] || [] );
+}
+
+export const unescapeString = ( arg ) => {
+ return decodeEntities( arg );
+};
+
+/**
+ * Returns a term object with name unescaped.
+ *
+ * @since 3.16.0
+ *
+ * @param {Object} term The term object to unescape.
+ *
+ * @return {Object} Term object with name property unescaped.
+ */
+export const unescapeTerm = ( term ) => {
+ return {
+ ...term,
+ name: unescapeString( term.name ),
+ };
+};
+
+/**
+ * Returns an array of term objects with names unescaped.
+ * The unescape of each term is performed using the unescapeTerm function.
+ *
+ * @since 3.16.0
+ *
+ * @param {Object[]} terms Array of term objects to unescape.
+ *
+ * @return {Object[]} Array of term objects unescaped.
+ */
+export const unescapeTerms = ( terms ) => {
+ return ( terms ?? [] ).map( unescapeTerm );
+};
diff --git a/src/FormTaxonomies/resources/form-builder/windowData.ts b/src/FormTaxonomies/resources/form-builder/windowData.ts
new file mode 100644
index 0000000000..fb32747629
--- /dev/null
+++ b/src/FormTaxonomies/resources/form-builder/windowData.ts
@@ -0,0 +1,73 @@
+/**
+ * @since 3.16.0
+ */
+type FormTagToken = {
+ id: number;
+ value: string;
+};
+
+/**
+ * @since 3.16.0
+ */
+type CategoryTerm = {
+ id: number;
+ name: string;
+ parent: number;
+};
+
+/**
+ * @since 3.16.0
+ */
+interface TaxonomySettings {
+ formTagsEnabled: boolean;
+ formTagsSelected: FormTagToken[];
+ formCategoriesEnabled: boolean;
+ formCategoriesSelected: number[];
+ formCategoriesAvailable: CategoryTerm[];
+}
+
+declare const window: {
+ giveTaxonomySettings: TaxonomySettings;
+} & Window;
+
+/**
+ * @since 3.16.0
+ */
+export default function getWindowData(): TaxonomySettings {
+ return window.giveTaxonomySettings;
+}
+
+/**
+ * @since 3.16.0
+ */
+export function isFormTagsEnabled(): boolean {
+ return window.giveTaxonomySettings.formTagsEnabled;
+}
+
+/**
+ * @since 3.16.0
+ */
+export function isFormCategoriesEnabled(): boolean {
+ return window.giveTaxonomySettings.formCategoriesEnabled;
+}
+
+/**
+ * @since 3.16.0
+ */
+export function getInitialFormTags(): FormTagToken[] {
+ return window.giveTaxonomySettings.formTagsSelected;
+}
+
+/**
+ * @since 3.16.0
+ */
+export function getInitialFormCategories(): any[] {
+ return window.giveTaxonomySettings.formCategoriesSelected;
+}
+
+/**
+ * @since 3.16.0
+ */
+export function getAvailableFormCategories(): any[] {
+ return window.giveTaxonomySettings.formCategoriesAvailable;
+}
diff --git a/src/Framework/Blocks/BlockCollection.php b/src/Framework/Blocks/BlockCollection.php
index fc206c6bbd..dc56d918cc 100644
--- a/src/Framework/Blocks/BlockCollection.php
+++ b/src/Framework/Blocks/BlockCollection.php
@@ -209,9 +209,18 @@ public function append(BlockModel $block): BlockCollection
return $this;
}
+ /**
+ * @since 3.15.0 returns the block collection if block does not exist.
+ * @since 3.0.0
+ */
public function remove($blockName) {
$blockCollection = $this->findByNameRecursive($blockName, 0, 'parent');
$innerBlocks = $blockCollection->blocks;
+
+ if(!$innerBlocks){
+ return $this;
+ }
+
$blockIndex = array_search($blockName, array_column($innerBlocks, 'name'));
array_splice($innerBlocks, $blockIndex, 1);
$blockCollection->blocks = $innerBlocks;
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'))
);
}
diff --git a/src/Framework/FieldsAPI/Date.php b/src/Framework/FieldsAPI/Date.php
index 4ab19e79ab..0257fb6367 100644
--- a/src/Framework/FieldsAPI/Date.php
+++ b/src/Framework/FieldsAPI/Date.php
@@ -3,7 +3,7 @@
namespace Give\Framework\FieldsAPI;
/**
- * @unlreased add date format attribute
+ * @since 3.0.0 add date format attribute
* @since 2.32.0 added description
* @since 2.12.0
*/
diff --git a/src/Framework/FieldsAPI/Honeypot.php b/src/Framework/FieldsAPI/Honeypot.php
new file mode 100644
index 0000000000..156c278019
--- /dev/null
+++ b/src/Framework/FieldsAPI/Honeypot.php
@@ -0,0 +1,13 @@
+gateways;
}
-}
\ No newline at end of file
+}
diff --git a/src/Framework/ListTable/ListTable.php b/src/Framework/ListTable/ListTable.php
index dce00aad31..2579eed329 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);
+ /**
+ * @since 3.16.0
+ */
+ do_action("givewp_list_table_cell_value_{$column::getId()}_before", $column, $model, $locale);
+
+ /**
+ * @since 3.16.0
+ */
+ $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/Framework/Migrations/Controllers/ManualMigration.php b/src/Framework/Migrations/Controllers/ManualMigration.php
index 1693d95e6c..2daa91f1c6 100644
--- a/src/Framework/Migrations/Controllers/ManualMigration.php
+++ b/src/Framework/Migrations/Controllers/ManualMigration.php
@@ -36,16 +36,17 @@ public function __construct(MigrationsRegister $migrationsRegister)
}
/**
+ * @since 3.19.0 sanitize params
* @since 2.9.2
*/
public function __invoke()
{
if ( ! empty($_GET['give-run-migration'])) {
- $migrationToRun = $_GET['give-run-migration'];
+ $migrationToRun = give_clean($_GET['give-run-migration']);
}
if ( ! empty($_GET['give-clear-update'])) {
- $migrationToClear = $_GET['give-clear-update'];
+ $migrationToClear = give_clean($_GET['give-clear-update']);
}
$hasMigration = isset($migrationToRun) || isset($migrationToClear);
diff --git a/src/Framework/Models/Factories/ModelFactory.php b/src/Framework/Models/Factories/ModelFactory.php
index c59db6d596..684dfa28d5 100644
--- a/src/Framework/Models/Factories/ModelFactory.php
+++ b/src/Framework/Models/Factories/ModelFactory.php
@@ -3,7 +3,7 @@
namespace Give\Framework\Models\Factories;
use Exception;
-use Give\Vendors\Faker\Generator;
+use Faker\Generator;
use Give\Framework\Database\DB;
/**
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/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php b/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php
new file mode 100644
index 0000000000..827a9ada86
--- /dev/null
+++ b/src/Framework/PaymentGateways/Contracts/Subscription/SubscriptionPausable.php
@@ -0,0 +1,32 @@
+subscriptionModule->cancelSubscription($subscription);
}
+ /**
+ * @inheritDoc
+ *
+ * @since 3.17.0
+ */
+ 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
+ *
+ * @since 3.17.0
+ */
+ 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
+ *
+ * @since 3.17.0
+ */
+ 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..9fb4314d1a 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;
}
+ /**
+ * @since 3.17.0
+ */
+ public function canPauseSubscription(): bool
+ {
+ return $this instanceof SubscriptionPausable;
+ }
+
/**
* @inheritDoc
*/
diff --git a/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php b/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php
index 180b3e575f..44a45a2b05 100644
--- a/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php
+++ b/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php
@@ -15,6 +15,7 @@
class SubscriptionRenewalDonationCreated
{
/**
+ * @since 3.16.0 Add log messages and a defensive approach to prevent duplicated renewals
* @since 3.6.0
*/
public function __invoke(
@@ -25,6 +26,48 @@ public function __invoke(
$subscription = give()->subscriptions->getByGatewaySubscriptionId($gatewaySubscriptionId);
if ( ! $subscription) {
+ PaymentGatewayLog::error(
+ sprintf('The renewal was not created for the gateway transaction ID %s because no subscription with the gateway subscription %s was found.',
+ $gatewayTransactionId, $gatewaySubscriptionId),
+ [
+ 'Gateway Subscription ID' => $gatewaySubscriptionId,
+ 'Gateway Transaction ID' => $gatewayTransactionId,
+ 'Message' => $message,
+ ]
+ );
+
+ return;
+ }
+
+ if ($subscription->initialDonation()->gatewayTransactionId === $gatewayTransactionId) {
+ PaymentGatewayLog::error(
+ sprintf('The renewal was not created for the gateway transaction ID %s because the initial donation of the subscription %s is already using the informed gateway transaction ID %s.',
+ $gatewayTransactionId, $subscription->id, $gatewaySubscriptionId),
+ [
+ 'Gateway Subscription ID' => $gatewaySubscriptionId,
+ 'Gateway Transaction ID' => $gatewayTransactionId,
+ 'Message' => $message,
+ 'Subscription' => $subscription->toArray(),
+ ]
+ );
+
+ return;
+ }
+
+ $donation = give()->donations->getByGatewayTransactionId($gatewayTransactionId);
+
+ if ($donation) {
+ PaymentGatewayLog::error(
+ sprintf('The renewal was not created for the gateway transaction ID %s because the donation %s is already using the informed gateway transaction ID %s.',
+ $gatewayTransactionId, $donation->id, $gatewaySubscriptionId),
+ [
+ 'Gateway Subscription ID' => $gatewaySubscriptionId,
+ 'Gateway Transaction ID' => $gatewayTransactionId,
+ 'Message' => $message,
+ 'Donation' => $donation->toArray(),
+ ]
+ );
+
return;
}
diff --git a/src/Framework/Support/ValueObjects/BaseEnum.php b/src/Framework/Support/ValueObjects/BaseEnum.php
new file mode 100644
index 0000000000..ea3f6f8725
--- /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/Helpers/Utils.php b/src/Helpers/Utils.php
index 9470ab5389..b2c46fe9a2 100644
--- a/src/Helpers/Utils.php
+++ b/src/Helpers/Utils.php
@@ -111,4 +111,96 @@ public static function isPluginActive($plugin)
return is_plugin_active($plugin);
}
+
+ /**
+ * @since 3.17.2
+ */
+ public static function removeBackslashes($data)
+ {
+ /**
+ * The stripslashes_deep() method removes only the first backslash occurrence from
+ * a given string, so we are using the ltrim() method to make sure we are removing
+ * all other occurrences. We need to remove these backslashes from the beginner of
+ * the input because attackers can use them to bypass the is_serialized() check.
+ */
+ $data = stripslashes_deep($data);
+ $data = is_string($data) ? ltrim($data, '\\') : $data;
+
+ return $data;
+ }
+
+ /**
+ * The regular expression attempts to capture the basic structure of a serialized array
+ * or object. This is more robust than the is_serialized() function but still not perfect.
+ *
+ * @since 3.17.2
+ */
+ public static function containsSerializedDataRegex($data): bool
+ {
+ if ( ! is_string($data)) {
+ return false;
+ }
+
+ $pattern = '/(a:\d+:\{.*\})|(O:\d+:"[^"]+":\{.*\})/';
+
+ return preg_match($pattern, $data) === 1;
+ }
+
+ /**
+ * @since 3.17.2
+ */
+ public static function isSerialized($data): bool
+ {
+ $data = self::removeBackslashes($data);
+
+ if (is_serialized($data) || self::containsSerializedDataRegex($data)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.17.2
+ */
+ public static function safeUnserialize($data)
+ {
+ $data = self::removeBackslashes($data);
+
+ /**
+ * We are setting the allowed_classes to false as a default to
+ * prevent the injection of objects that can run unwished code.
+ *
+ * From PHP docs:
+ * allowed_classes - Either an array of class names which should be accepted, false to accept no classes, or
+ * true to accept all classes. If this option is defined and unserialize() encounters an object of a class
+ * that isn't to be accepted, then the object will be instantiated as __PHP_Incomplete_Class instead. Omitting
+ * this option is the same as defining it as true: PHP will attempt to instantiate objects of any class.
+ */
+ $unserializedData = @unserialize(trim($data), ['allowed_classes' => false]);
+
+ /*
+ * In case the passed string is not unserializeable, false is returned.
+ *
+ * @see https://www.php.net/manual/en/function.unserialize.php
+ */
+
+ return ! $unserializedData && ! self::containsSerializedDataRegex($data) ? $data : $unserializedData;
+ }
+
+ /**
+ * 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 self::isSerialized($data)
+ ? self::safeUnserialize($data)
+ : $data;
+ }
}
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/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/MultiFormGoals/ProgressBar/Model.php b/src/MultiFormGoals/ProgressBar/Model.php
index 289b113374..ea024438e2 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;
+/**
+ * @since 3.16.0 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
*
+ * @since 3.16.0 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
*
+ * @since 3.16.0 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/MultiFormGoals/ProgressBar/Query.php b/src/MultiFormGoals/ProgressBar/Query.php
index 19f306fbdc..2da71de268 100644
--- a/src/MultiFormGoals/ProgressBar/Query.php
+++ b/src/MultiFormGoals/ProgressBar/Query.php
@@ -29,12 +29,13 @@ public function __construct($formIDs)
}
/**
+ * @since 3.14.0 Consider the donation mode (test or live) instead of querying both modes together
* @return string
*/
public function getSQL()
{
global $wpdb;
-
+ $mode = give_is_test_mode() ? 'test' : 'live';
$sql = "
SELECT
sum( revenue.amount ) as total,
@@ -42,10 +43,14 @@ public function getSQL()
FROM {$wpdb->posts} as payment
JOIN {$wpdb->give_revenue} as revenue
ON revenue.donation_id = payment.ID
+ JOIN {$wpdb->paymentmeta} paymentMode
+ ON payment.ID = paymentMode.donation_id AND paymentMode.meta_key = '_give_payment_mode'
WHERE
payment.post_type = 'give_payment'
AND
payment.post_status IN ( 'publish', 'give_subscription' )
+ AND
+ paymentMode.meta_value = '{$mode}'
";
if ( ! empty($this->formIDs)) {
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 = ``;
}
- 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/Onboarding/BlockFactory.php b/src/Onboarding/BlockFactory.php
new file mode 100644
index 0000000000..7bb299a051
--- /dev/null
+++ b/src/Onboarding/BlockFactory.php
@@ -0,0 +1,99 @@
+ 'givewp/company',
+ 'attributes' => array_merge([
+ 'label' => __('Company Name', 'give'),
+ 'isRequired' => false,
+ ], $attributes),
+ ]);
+ }
+
+ /**
+ *
+ * @since 3.15.0
+ *
+ * @param array $attributes
+ *
+ * @return BlockModel
+ */
+ public static function termsAndConditions(array $attributes = []): BlockModel
+ {
+ return BlockModel::make([
+ 'name' => 'givewp/terms-and-conditions',
+ 'attributes' => array_merge([
+ 'useGlobalSettings' => false,
+ 'checkboxLabel' => __('I agree to the Terms and conditions.', 'give'),
+ 'displayType' => 'showFormTerms',
+ 'linkText' => __('Show terms', 'give'),
+ 'linkUrl' => '',
+ 'agreementText' => __(
+ 'Acceptance of any contribution, gift or grant is at the discretion of the GiveWP. The GiveWP will not accept any gift unless it can be used or expended consistently with the purpose and mission of the GiveWP. No irrevocable gift, whether outright or life-income in character, will be accepted if under any reasonable set of circumstances the gift would jeopardize the donor’s financial security. The GiveWP will refrain from providing advice about the tax or other treatment of gifts and will encourage donors to seek guidance from their own professional advisers to assist them in the process of making their donation.',
+ 'give'
+ ),
+ 'modalHeading' => __('Do you consent to the following', 'give'),
+ 'modalAcceptanceText' => __('Accept', 'give'),
+ ], $attributes),
+ ]);
+ }
+
+ /**
+ *
+ * @since 3.15.0
+ *
+ * @param array $attributes
+ *
+ * @return BlockModel
+ */
+ public static function donorComments(array $attributes = []): BlockModel
+ {
+ return BlockModel::make([
+ 'name' => 'givewp/donor-comments',
+ 'attributes' => array_merge([
+ 'label' => __('Comment', 'give'),
+ 'description' => __('Would you like to add a comment to this donation?', 'give'),
+ ], $attributes),
+ ]);
+ }
+
+ /**
+ *
+ * @since 3.15.0
+ *
+ * @param array $attributes
+ *
+ * @return BlockModel
+ */
+ public static function anonymousDonations(array $attributes = []): BlockModel
+ {
+ return BlockModel::make([
+ 'name' => 'givewp/anonymous',
+ 'attributes' => array_merge([
+ 'label' => __('Make this an anonymous donation.', 'give'),
+ 'description' => __(
+ 'Would you like to prevent your name, image, and comment from being displayed publicly?',
+ 'give'
+ ),
+ ], $attributes),
+ ]);
+ }
+}
diff --git a/src/Onboarding/FormRepository.php b/src/Onboarding/FormRepository.php
index 47d0c26257..51639a61da 100644
--- a/src/Onboarding/FormRepository.php
+++ b/src/Onboarding/FormRepository.php
@@ -2,6 +2,12 @@
namespace Give\Onboarding;
+use Give\DonationForms\Models\DonationForm;
+use Give\DonationForms\Properties\FormSettings;
+use Give\DonationForms\ValueObjects\DonationFormStatus;
+use Give\FormBuilder\Actions\GenerateDefaultDonationFormBlockCollection;
+use Give\Log\Log;
+
/**
* @since 2.8.0
*/
@@ -17,8 +23,8 @@ class FormRepository
/**
* @since 2.8.0
*
- * @param SettingsRepository $settingsRepository
- *
+ * @param SettingsRepositoryFactory $settingsRepositoryFactory
+ * @param DefaultFormFactory $defaultFormFactory
*/
public function __construct(
SettingsRepositoryFactory $settingsRepositoryFactory,
@@ -66,17 +72,30 @@ protected function isFormAvailable($formID)
}
/**
+ * @since 3.15.0 Create the default v3 form.
* @since 2.8.0
* @return int Form ID
*
*/
protected function makeAndPersist()
{
- $formID = $this->defaultFormFactory->make();
+ $form = new DonationForm([
+ 'title' => __('GiveWP Donation Form', 'give'),
+ 'status' => DonationFormStatus::PUBLISHED(),
+ 'settings' => FormSettings::fromArray([
+ 'designId' => 'multi-step',
+ 'designSettingsImageUrl' => GIVE_PLUGIN_URL . '/assets/dist/images/admin/onboarding/header-image.jpg',
+ 'designSettingsImageStyle' => 'above',
+ 'designSettingsImageAlt' => 'GiveWP Onboarding Donation Form',
+ ]),
+ 'blocks' => (new GenerateDefaultDonationFormBlockCollection())(),
+ ]);
+
+ $form->save();
- $this->settingsRepository->set('form_id', $formID);
+ $this->settingsRepository->set('form_id', $form->id);
$this->settingsRepository->save();
- return $formID;
+ return $form->id;
}
}
diff --git a/src/Onboarding/Routes/FeaturesRoute.php b/src/Onboarding/Routes/FeaturesRoute.php
index 0083a1a31e..101ba887ae 100644
--- a/src/Onboarding/Routes/FeaturesRoute.php
+++ b/src/Onboarding/Routes/FeaturesRoute.php
@@ -3,6 +3,10 @@
namespace Give\Onboarding\Routes;
use Give\API\RestRoute;
+use Give\DonationForms\Models\DonationForm;
+use Give\Framework\Exceptions\Primitives\Exception;
+use Give\Onboarding\BlockFactory;
+use Give\Onboarding\SettingsRepository;
use Give\Onboarding\SettingsRepositoryFactory;
use WP_REST_Request;
@@ -23,83 +27,32 @@ class FeaturesRoute implements RestRoute
/**
* @since 2.8.0
*
- * @param SettingsRepository $settingsRepository
- *
+ * @param SettingsRepositoryFactory $settingsRepositoryFactory
*/
public function __construct(SettingsRepositoryFactory $settingsRepositoryFactory)
{
$this->settingsRepository = $settingsRepositoryFactory->make('give_onboarding');
}
- /**
- * @since 2.8.0
- *
- * @param WP_REST_Request $request
- *
- * @return array
- *
- */
- public function handleRequest(WP_REST_Request $request)
- {
- $features = json_decode($request->get_param('value'));
-
- $formID = $this->settingsRepository->get('form_id');
-
- update_post_meta($formID, '_give_goal_option', in_array('donation-goal', $features) ? 'enabled' : 'disabled');
- update_post_meta(
- $formID,
- '_give_donor_comment',
- in_array('donation-comments', $features) ? 'enabled' : 'disabled'
- );
- update_post_meta(
- $formID,
- '_give_terms_option',
- in_array('terms-conditions', $features) ? 'enabled' : 'disabled'
- );
- update_post_meta(
- $formID,
- '_give_customize_offline_donations',
- in_array('offline-donations', $features) ? 'enabled' : 'disabled'
- );
- update_post_meta(
- $formID,
- '_give_anonymous_donation',
- in_array('anonymous-donations', $features) ? 'enabled' : 'disabled'
- );
- update_post_meta(
- $formID,
- '_give_company_field',
- in_array('company-donations', $features) ? 'optional' : 'disabled'
- ); // Note: The company field has two values for enabled, "required" and "optional".
-
- return [
- 'data' => [
- 'setting' => 'features',
- 'value' => $features,
- 'formID' => $formID,
- ],
- ];
- }
-
/**
* @inheritDoc
*/
- public function registerRoute()
+ public function registerRoute(): void
{
register_rest_route(
'give-api/v2',
$this->endpoint,
[
[
- 'methods' => 'POST',
- 'callback' => [$this, 'handleRequest'],
+ 'methods' => 'POST',
+ 'callback' => [$this, 'handleRequest'],
'permission_callback' => function () {
return current_user_can('manage_options');
},
- 'args' => [
+ 'args' => [
'value' => [
- 'type' => 'string',
- 'required' => true,
+ 'type' => 'string',
+ 'required' => true,
// 'validate_callback' => [ $this, 'validateSetting' ],
'sanitize_callback' => 'sanitize_text_field',
],
@@ -115,25 +68,116 @@ public function registerRoute()
* @return array
*
*/
- public function getSchema()
+ public function getSchema(): array
{
return [
// This tells the spec of JSON Schema we are using which is draft 4.
- '$schema' => 'http://json-schema.org/draft-04/schema#',
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
// The title property marks the identity of the resource.
- 'title' => 'onboarding',
- 'type' => 'object',
+ 'title' => 'onboarding',
+ 'type' => 'object',
// In JSON Schema you can specify object properties in the properties attribute.
'properties' => [
'setting' => [
'description' => esc_html__('The reference name for the setting being updated.', 'give'),
- 'type' => 'string',
+ 'type' => 'string',
],
- 'value' => [
+ 'value' => [
'description' => esc_html__('The value of the setting being updated.', 'give'),
- 'type' => 'string',
+ 'type' => 'string',
],
],
];
}
+
+ /**
+ * @since 3.15.0 Handle v3 form features.
+ * @since 2.8.0
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return array
+ *
+ * @throws Exception
+ */
+ public function handleRequest(WP_REST_Request $request)
+ {
+ $features = json_decode($request->get_param('value'));
+
+ $formID = $this->settingsRepository->get('form_id');
+
+ $this->handleFormFeatures($formID, $features);
+
+ return [
+ 'data' => [
+ 'setting' => 'features',
+ 'value' => $features,
+ 'formID' => $formID,
+ ],
+ ];
+ }
+
+ /**
+ * @since 3.15.0 Update the v3 form features based on Wizard settings.
+ *
+ * @param $formID
+ * @param $features
+ *
+ * @return void
+ * @throws Exception
+ */
+ public function handleFormFeatures($formID, $features): void
+ {
+ $donationForm = DonationForm::find($formID);
+
+ if (!$donationForm) {
+ return;
+ }
+
+ // Donation Goal
+ $donationForm->settings->enableDonationGoal = in_array('donation-goal', $features, true);
+
+ // Offline Donations
+ $gateways = give_get_option('gateways_v3', []);
+ if(in_array('offline-donations', $features, true)) {
+ $gateways['offline'] = 1;
+ } else {
+ unset($gateways['offline']);
+ }
+ give_update_option('gateways_v3', $gateways);
+
+ // Donation Comment
+ $commentBlockExists = $donationForm->blocks->findByName('givewp/donor-comments');
+ if (!in_array('donation-comments', $features, true) ) {
+ $donationForm->blocks->remove('givewp/donor-comments');
+ } elseif (!$commentBlockExists) {
+ $donationForm->blocks->insertAfter('givewp/email', BlockFactory::donorComments());
+ }
+
+ // Terms and Conditions
+ $termsBlockExists = $donationForm->blocks->findByName('givewp/terms-and-conditions');
+ if (!in_array('terms-conditions', $features, true)) {
+ $donationForm->blocks->remove('givewp/terms-and-conditions');
+ } elseif (!$termsBlockExists) {
+ $donationForm->blocks->insertBefore('givewp/payment-gateways', BlockFactory::termsAndConditions());
+ }
+
+ // Anonymous Donations
+ $anonymousBlockExists = $donationForm->blocks->findByName('givewp/anonymous');
+ if (!in_array('anonymous-donations', $features, true)) {
+ $donationForm->blocks->remove('givewp/anonymous');
+ } elseif (!$anonymousBlockExists) {
+ $donationForm->blocks->insertAfter('givewp/email', BlockFactory::anonymousDonations());
+ }
+
+ // Company Donations
+ $companyBlockExists = $donationForm->blocks->findByName('givewp/company');
+ if (!in_array('company-donations', $features, true)) {
+ $donationForm->blocks->remove('givewp/company');
+ } elseif (!$companyBlockExists) {
+ $donationForm->blocks->insertAfter('givewp/email', BlockFactory::company());
+ }
+
+ $donationForm->save();
+ }
}
diff --git a/src/Onboarding/Setup/Page.php b/src/Onboarding/Setup/Page.php
index 5342551ea4..3ed4503439 100644
--- a/src/Onboarding/Setup/Page.php
+++ b/src/Onboarding/Setup/Page.php
@@ -92,6 +92,25 @@ public function enqueue_scripts()
GIVE_VERSION,
$in_footer = true
);
+
+ wp_enqueue_script(
+ 'give-admin-add-ons-script',
+ GIVE_PLUGIN_URL . 'assets/dist/js/admin-add-ons.js',
+ ['jquery'],
+ GIVE_VERSION,
+ $in_footer = true
+ );
+
+ $localized_data = [
+ 'notices' => [
+ 'invalid_license' => __( 'Sorry, you entered an invalid key.', 'give' ),
+ 'download_file' => __( 'Success! You have activated your license key and are receiving updates and priority support. Click here to download your add-on.', 'give' ),
+ 'addon_activated' => __( '{pluginName} add-on activated successfully.', 'give' ),
+ 'addon_activation_error' => __( 'The add-on did not activate successfully.', 'give' ),
+ ],
+ ];
+
+ wp_localize_script( 'give-admin-add-ons-script', 'give_addon_var', $localized_data );
}
/**
diff --git a/src/Onboarding/Setup/templates/action-button.html b/src/Onboarding/Setup/templates/action-button.html
new file mode 100644
index 0000000000..599d26e0bb
--- /dev/null
+++ b/src/Onboarding/Setup/templates/action-button.html
@@ -0,0 +1 @@
+{{ text }}
diff --git a/src/Onboarding/Setup/templates/action-link.html b/src/Onboarding/Setup/templates/action-link.html
index 0a8e9d53e2..3836434d1f 100644
--- a/src/Onboarding/Setup/templates/action-link.html
+++ b/src/Onboarding/Setup/templates/action-link.html
@@ -1 +1 @@
-{{ screenReaderText }}
\ No newline at end of file
+{{ label }}
diff --git a/src/Onboarding/Setup/templates/activate-license.html b/src/Onboarding/Setup/templates/activate-license.html
new file mode 100644
index 0000000000..3eb4573868
--- /dev/null
+++ b/src/Onboarding/Setup/templates/activate-license.html
@@ -0,0 +1,50 @@
+{{ text }} {{ label }}
+
+
+
+
diff --git a/src/Onboarding/Setup/templates/badge.html b/src/Onboarding/Setup/templates/badge.html
new file mode 100644
index 0000000000..f5a14f5c27
--- /dev/null
+++ b/src/Onboarding/Setup/templates/badge.html
@@ -0,0 +1 @@
+{{ text }}
diff --git a/src/Onboarding/Setup/templates/index.html.php b/src/Onboarding/Setup/templates/index.html.php
index bb1c61bb8f..89022ce801 100644
--- a/src/Onboarding/Setup/templates/index.html.php
+++ b/src/Onboarding/Setup/templates/index.html.php
@@ -1,3 +1,20 @@
+
@@ -27,34 +44,38 @@
isFormConfigured()) {
+ $form = DonationForm::find((int)$settings['form_id']);
+
+ $customizeFormURL = $form && $form->id ? admin_url('post.php?action=edit&post=' . $form->id) : admin_url('edit.php?post_type=give_forms&page=give-forms');
+ }
+
echo $this->render_template(
'section',
[
+ 'class' => !$this->isFormConfigured() ? 'current-step' : '',
'title' => sprintf('%s 1: %s', __('Step', 'give'), __('Create your first donation form', 'give')),
- 'badge' => '5 Minutes ',
- 'contents' => $this->render_template(
- 'row-item',
- [
- 'testId' => 'setup-configuration',
- 'class' => ($this->isFormConfigured(
- )) ? 'setup-item-configuration setup-item-completed' : 'setup-item-configuration',
- 'icon' => ($this->isFormConfigured())
- ? $this->image('check-circle.min.png')
- : $this->image('configuration@2x.min.png'),
- 'icon_alt' => esc_html__('First-Time Configuration', 'give'),
- 'title' => esc_html__('First-Time Configuration', 'give'),
- 'description' => esc_html__(
- 'Every fundraising campaign begins with a donation form. Click here to create your first donation form in minutes. Once created you can use it anywhere on your website.',
- 'give'
- ),
- 'action' => $this->render_template(
- 'action-link',
- [
- 'href' => admin_url('?page=give-onboarding-wizard'),
- 'screenReaderText' => 'Configure GiveWP',
- ]
- ),
- ]
+ 'badge' => ($this->isFormConfigured()
+ ? $this->render_template('badge', [
+ 'class' => 'completed',
+ 'text' => esc_html__('Completed', 'give'),
+ ])
+ : $this->render_template('badge', [
+ 'class' => 'not-completed',
+ 'text' => esc_html__('Not Completed', 'give'),
+ ])
+ ),
+ 'button' => ($this->isFormConfigured()
+ ? $this->render_template('action-button', [
+ 'href' => esc_url($customizeFormURL),
+ 'text' => esc_html__('Customize form', 'give'),
+ 'target' => '_blank',
+ ])
+ : $this->render_template('action-button', [
+ 'href' => esc_url(admin_url('?page=give-onboarding-wizard')),
+ 'text' => esc_html__('Configure GiveWP', 'give'),
+ 'target' => '_blank',
+ ])
),
]
);
@@ -65,9 +86,52 @@
echo $this->render_template(
'section',
[
+ 'class' => ($this->isFormConfigured() && !($this->isStripeSetup() || $this->isPayPalSetup())) ? 'current-step' : '',
'title' => sprintf('%s 2: %s', __('Step', 'give'), __('Connect a payment gateway', 'give')),
+ 'badge' => (($this->isStripeSetup() || $this->isPayPalSetup())
+ ? $this->render_template('badge', [
+ 'class' => 'completed',
+ 'text' => esc_html__('Completed', 'give'),
+ ])
+ : $this->render_template('badge', [
+ 'class' => 'not-completed',
+ 'text' => esc_html__('Not Completed', 'give'),
+ ])
+ ),
'contents' => [
- ! $this->isStripeSetup() ? $this->render_template(
+ $this->render_template(
+ 'row-item',
+ [
+ 'testId' => 'stripe',
+ 'class' => ($this->isStripeSetup()) ? 'stripe setup-item-completed' : 'stripe',
+ 'icon' => ($this->isStripeSetup())
+ ? $this->image('check-circle.min.png')
+ : $this->image('stripe@2x.min.png'),
+ 'icon_alt' => esc_html__('Stripe', 'give'),
+ 'title' => esc_html__('Connect to Stripe', 'give'),
+ 'description' => esc_html__(
+ 'Stripe is one of the most popular payment gateways, and for good reason! Receive one-time and Recurring Donations (add-on) using many of the most popular payment methods. Note: the FREE version of Stripe includes an additional 2% fee for processing one-time donations. Remove the fee by installing and activating the premium Stripe add-on.',
+ 'give'
+ ),
+ 'action' => ($this->isStripeSetup()) ? sprintf(
+ ' Stripe Settings ',
+ esc_url(add_query_arg(
+ [
+ 'post_type' => 'give_forms',
+ 'page' => 'give-settings',
+ 'tab' => 'gateways',
+ 'section' => 'stripe-settings',
+ ],
+ admin_url('edit.php')
+ ))
+ )
+ : sprintf(
+ ' Connect to Stripe ',
+ $this->stripeConnectURL()
+ ),
+ ]
+ ),
+ $this->render_template(
'row-item',
[
'testId' => 'paypal',
@@ -99,48 +163,16 @@
)
),
]
- ) : '',
- ! $this->isPayPalSetup() ? $this->render_template(
- 'row-item',
- [
- 'testId' => 'stripe',
- 'class' => ($this->isStripeSetup()) ? 'stripe setup-item-completed' : 'stripe',
- 'icon' => ($this->isStripeSetup())
- ? $this->image('check-circle.min.png')
- : $this->image('stripe@2x.min.png'),
- 'icon_alt' => esc_html__('Stripe', 'give'),
- 'title' => esc_html__('Connect to Stripe', 'give'),
- 'description' => esc_html__(
- 'Stripe is one of the most popular payment gateways, and for good reason! Receive one-time and Recurring Donations (add-on) using many of the most popular payment methods. Note: the FREE version of Stripe includes an additional 2% fee for processing one-time donations. Remove the fee by installing and activating the premium Stripe add-on.',
- 'give'
- ),
- 'action' => ($this->isStripeSetup()) ? sprintf(
- ' Stripe Settings ',
- esc_url(add_query_arg(
- [
- 'post_type' => 'give_forms',
- 'page' => 'give-settings',
- 'tab' => 'gateways',
- 'section' => 'stripe-settings',
- ],
- admin_url('edit.php')
- ))
- )
- : sprintf(
- ' Connect to Stripe ',
- $this->stripeConnectURL()
- ),
- ]
- ) : '',
+ ),
],
'footer' => $this->render_template(
'footer',
[
'contents' => sprintf(
- __(
- 'Want to use a different gateway? GiveWP has support for many others including Authorize.net, Square, Razorpay and more! %s',
- 'give'
- ),
+ ' %s %s
%s',
+ $this->image('payment-gateway.svg'),
+ __('Explore other payment gateways:', 'give'),
+ __('GiveWP has support for many others including Authorize.net, Square, Razorpay and more!', 'give'),
sprintf(
'
%s ',
'http://docs.givewp.com/payment-gateways', // UTM included.
@@ -155,15 +187,61 @@
render_template(
'section',
[
- 'title' => sprintf('%s 3: %s', __('Step', 'give'), __('Level up your fundraising', 'give')),
+ 'title' => sprintf('%s 3: %s', __('Step', 'give'), __('Get more from your fundraising campaign with add-ons', 'give')),
+ 'badge' => $this->render_template('badge', [
+ 'class' => 'optional',
+ 'text' => esc_html__('Optional', 'give'),
+ ]),
'contents' => [
- ! empty($settings['addons']) ? $this->render_template(
+ (! empty($settings['addons'] || $needsActivation)) ? $this->render_template(
'sub-header',
[
- 'text' => 'Based on your selections, Give recommends the following add-ons to support your fundraising.',
+ 'text' => sprintf(
+ '%s%s',
+ (! empty($settings['addons']) ? esc_html__('Based on your selections, Give recommends the following add-ons to support your fundraising.', 'give') . ' ' : ''),
+ ($needsActivation ? $this->render_template('activate-license',
+ [
+ 'text' => esc_html__('Already have an add-on license?', 'give'),
+ 'label' => esc_html__('Activate your license', 'give'),
+ 'href' => esc_url(admin_url('edit.php?post_type=give_forms&page=give-settings&tab=licenses')),
+ 'title' => esc_html__('Activate an Add-on License', 'give'),
+ 'description' => sprintf(
+ __('Enter your license key below to unlock your GiveWP add-ons. You can access your licenses anytime from the
My Account section on the GiveWP website. ', 'give'),
+ Give_License::get_account_url()
+ ),
+ 'nonce' => wp_nonce_field('give-license-activator-nonce', 'give_license_activator_nonce', true, false),
+ 'form-label' => esc_html__('License key', 'give'),
+ 'form-placeholder' => esc_html__('Enter your license key', 'give'),
+ 'form-submit-activate' => esc_html__('Activate License', 'give'),
+ 'form-submit-activating' => esc_html__('Verifying License...', 'give'),
+ 'form-submit-value' => esc_html__('Activate License', 'give'),
+ ]
+ ) : '')
+ )
]
) : '',
in_array('recurring-donations', $settings['addons']) ? $this->render_template(
@@ -174,7 +252,7 @@
'icon_alt' => __('Recurring Donations', 'give'),
'title' => __('Recurring Donations', 'give'),
'description' => __(
- 'The Recurring Donations add-on for GiveWP brings you more dependable payments by allowing your donors to give regularly at different time intervals. Let your donors choose how often they give and how much. Manage your subscriptions, view specialized reports, and connect more strategically with your recurring donors.',
+ 'Raise funds reliably through subscriptions based donations. Let your donors choose how often they give and how much. Manage your subscriptions, view specialized reports, and strengthen relationships with your recurring donors.',
'give'
),
'action' => $this->render_template(
@@ -182,7 +260,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-recurring', // UTM included.
- 'screenReaderText' => __('Learn more about Recurring Donations', 'give'),
+ 'label' => __('Get Recurring Donations', 'give'),
]
),
]
@@ -195,7 +273,7 @@
'icon_alt' => __('Fee Recovery', 'give'),
'title' => __('Fee Recovery', 'give'),
'description' => __(
- 'Credit Card processing fees can take away a big chunk of your donations. This means less money goes to your cause. Why not ask your donors to further help your cause by asking them to take care of the payment processing fees? That’s where the Fee Recovery add-on comes into play.',
+ 'Maximize your donations by allowing donors to cover payment processing fees, ensuring more funds go directly to your cause.',
'give'
),
'action' => $this->render_template(
@@ -203,7 +281,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-fee-recovery', // UTM included.
- 'screenReaderText' => __('Learn more about Fee Recovery', 'give'),
+ 'label' => __('Get Fee Recovery', 'give'),
]
),
]
@@ -224,7 +302,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-pdf-receipts', // UTM included.
- 'screenReaderText' => __('Learn more about PDF Receipts', 'give'),
+ 'label' => __('Get PDF Receipts', 'give'),
]
),
]
@@ -245,7 +323,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-ffm', // UTM included.
- 'screenReaderText' => __('Learn more about Form Field Manager', 'give'),
+ 'label' => __('Get Form Field Manager', 'give'),
]
),
]
@@ -258,7 +336,7 @@
'icon_alt' => __('Currency Switcher', 'give'),
'title' => __('Currency Switcher', 'give'),
'description' => __(
- 'Allow your donors to switch to their currency of choice and increase your overall giving with the GiveWP Currency Switcher add-on. Select from an extensive list of currencies, set the currency based on your donor\'s location, pull from live exchange rates and more!',
+ 'Let donors choose from your selected currencies, increasing global donations with live exchange rates and extensive currency options.',
'give'
),
'action' => $this->render_template(
@@ -266,7 +344,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-currency-switcher', // UTM included.
- 'screenReaderText' => __('Learn more about Currency Switcher', 'give'),
+ 'label' => __('Get Currency Switcher', 'give'),
]
),
]
@@ -287,7 +365,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-tributes', // UTM included.
- 'screenReaderText' => __('Learn more about Tributes', 'give'),
+ 'label' => __('Get Tributes', 'give'),
]
),
]
@@ -300,7 +378,7 @@
'icon_alt' => esc_html__('Add-ons', 'give'),
'title' => esc_html__('GiveWP Add-ons', 'give'),
'description' => esc_html__(
- 'Make your fundraising even more effective with powerful add-ons like Recurring Donations, Fee Recovery, Google Analytics Donation Tracking, MailChimp, and much more. View our growing library of 35+ add-ons and extend your fundraising now.',
+ 'Boost your fundraising efforts with powerful add-ons like Recurring Donations, Fee Recovery, Google Analytics, Mailchimp, and more. Explore our extensive library of 35+ add-ons to enhance your fundraising now.',
'give'
),
'action' => $this->render_template(
@@ -308,7 +386,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/setup-addons', // UTM included.
- 'screenReaderText' => __('View Add-ons for GiveWP', 'give'),
+ 'label' => esc_html__('View all premium add-ons', 'give'),
]
),
]
@@ -332,7 +410,7 @@
'icon_alt' => esc_html__('GiveWP Getting Started Guide', 'give'),
'title' => esc_html__('GiveWP Getting Started Guide', 'give'),
'description' => esc_html__(
- 'Start off on the right foot by learning the basics of the plugin and how to get the most out of it to further your online fundraising efforts.',
+ 'Learn the basics and advanced tips to optimize your fundraising with GiveWP.',
'give'
),
'action' => $this->render_template(
@@ -340,7 +418,7 @@
[
'target' => '_blank',
'href' => 'http://docs.givewp.com/getting-started', // UTM included.
- 'screenReaderText' => __('Learn more about GiveWP', 'give'),
+ 'label' => __('Get started', 'give'),
]
),
]
diff --git a/src/Onboarding/Setup/templates/section.html b/src/Onboarding/Setup/templates/section.html
index 5507d71e8b..ee18a93f25 100644
--- a/src/Onboarding/Setup/templates/section.html
+++ b/src/Onboarding/Setup/templates/section.html
@@ -1,10 +1,11 @@
-
+
{{ title }}
{{ badge }}
+ {{ button }}
{{ contents }}
{{ footer }}
-
\ No newline at end of file
+
diff --git a/src/Onboarding/Wizard/Page.php b/src/Onboarding/Wizard/Page.php
index 8ae8c0dd01..70895d037f 100644
--- a/src/Onboarding/Wizard/Page.php
+++ b/src/Onboarding/Wizard/Page.php
@@ -62,10 +62,11 @@ public function __construct(
* Register Onboarding Wizard as an admin page route
*
* @since 2.8.0
+ * @since 3.14.0 change capability to manage_give_settings
**/
public function add_page()
{
- add_submenu_page('', '', '', 'manage_options', $this->slug);
+ add_submenu_page('', '', '', 'manage_give_settings', $this->slug);
}
/**
@@ -74,10 +75,11 @@ public function add_page()
* If the current page query matches the onboarding wizard's slug, method renders the onboarding wizard.
*
* @since 2.8.0
+ * @since 3.14.0 add user capability check
**/
public function setup_wizard()
{
- if (empty($_GET['page']) || $this->slug !== $_GET['page']) { // WPCS: CSRF ok, input var ok.
+ if (empty($_GET['page']) || $this->slug !== $_GET['page'] || ! current_user_can('manage_give_settings')) { // WPCS: CSRF ok, input var ok.
return;
} else {
$this->render_page();
@@ -124,6 +126,7 @@ public function enqueue_scripts()
wp_enqueue_style('givewp-admin-fonts');
$formID = $this->formRepository->getDefaultFormID();
+ $formPreviewUrl = home_url('/?givewp-route=donation-form-view&form-id=');
$featureGoal = get_post_meta($formID, '_give_goal_option', true);
$featureComments = get_post_meta($formID, '_give_donor_comment', true);
$featureTerms = get_post_meta($formID, '_give_terms_option', true);
@@ -140,7 +143,7 @@ public function enqueue_scripts()
'setupUrl' => SetupPage::getSetupPageEnabledOrDisabled() === SetupPage::ENABLED ?
admin_url('edit.php?post_type=give_forms&page=give-setup') :
DonationFormsAdminPage::getUrl(),
- 'formPreviewUrl' => admin_url('?page=give-form-preview'),
+ 'formPreviewUrl' => $formPreviewUrl,
'localeCurrency' => $this->localeCollection->pluck('currency_code'),
'currencies' => FormatList::fromKeyValue(give_get_currencies_list()),
'currencySelected' => $currency,
diff --git a/src/PaymentGateways/DataTransferObjects/GiveInsertPaymentData.php b/src/PaymentGateways/DataTransferObjects/GiveInsertPaymentData.php
index 18b8a969ea..5ac3f6baf7 100644
--- a/src/PaymentGateways/DataTransferObjects/GiveInsertPaymentData.php
+++ b/src/PaymentGateways/DataTransferObjects/GiveInsertPaymentData.php
@@ -116,7 +116,7 @@ public function toArray()
*
* Check legacy code give_get_donation_form_user:1212
*
- * @unlreased
+ * @since 2.24.0
*
* @return array|bool
*/
diff --git a/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php b/src/PaymentGateways/Gateways/Offline/Actions/EnqueueOfflineFormBuilderScripts.php
index ffc3b83032..684f1b0a95 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.
*
+ * @since 3.16.2 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'
),
diff --git a/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx b/src/PaymentGateways/Gateways/PayPalCommerce/payPalCommerceGateway.tsx
index 78925299e5..d421306f3d 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));
}
},
+
+ /**
+ * @since 3.17.1 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 (
diff --git a/src/PaymentGateways/Gateways/ServiceProvider.php b/src/PaymentGateways/Gateways/ServiceProvider.php
index 522e33a2cb..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()
{
diff --git a/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php b/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php
index 81c751c833..8c233e75ff 100644
--- a/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php
+++ b/src/PaymentGateways/Gateways/Stripe/LegacyStripeAdapter.php
@@ -86,16 +86,16 @@ public function addDonationDetails()
$account = 'connect' === $accountDetail['type'] ?
"{$accountDetail['account_name']} ({$accountId})" :
give_stripe_convert_slug_to_title($accountId);
- ?>
+?>
+ esc_html_e('Stripe Account:', 'give'); ?>
- {__('Donation amount must be greater than zero to proceed.', 'give')}>;
+ }
+
return (
diff --git a/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php b/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php
index b7331c7406..7614bfe7aa 100644
--- a/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php
+++ b/src/PaymentGateways/PayPalCommerce/AdminSettingFields.php
@@ -13,6 +13,7 @@
* Class AdminSettingFields
* @package Give\PaymentGateways\PayPalCommerce
*
+ * @since 3.16.0 added nonce to disconnect button
* @since 2.9.0
*/
class AdminSettingFields
@@ -166,7 +167,7 @@ public function introductionSection()
-
@@ -431,9 +432,7 @@ class="button-wrap connection-setting
-
+
@@ -472,7 +471,9 @@ class="button-wrap disconnection-setting
+ data-mode="mode; ?>"
+ data-nonce=""
+ >
diff --git a/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php b/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php
index d7cfde3d05..cb58c0c7bc 100644
--- a/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php
+++ b/src/PaymentGateways/PayPalCommerce/AjaxRequestHandler.php
@@ -184,13 +184,16 @@ public function onGetPartnerUrlAjaxRequestHandler()
/**
* give_paypal_commerce_disconnect_account ajax request handler.
*
- * @unreleased Add new $keepWebhooks option
+ * @since 3.16.0 added security nonce check
+ * @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
*/
public function removePayPalAccount()
{
+ check_ajax_referer( 'give_paypal_commerce_disconnect_account');
+
if (! current_user_can('manage_give_settings')) {
wp_send_json_error(['error' => esc_html__('You are not allowed to perform this action.', 'give')]);
}
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..dc1be766b0 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,60 +243,7 @@ public static function getDataByPricingPlan(array $data): string
}
/**
- * @unreleased
- *
- * 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
- * all visible banners.
- */
- public function alternateVisibleBanners(): array
- {
- $visibleBanners = $this->getVisibleBanners();
- $bannerCount = count($visibleBanners);
-
- if ($bannerCount > 0) {
- $currentIndex = $_SESSION['banner_index'] ?? 0;
-
- $selectedBanner = $visibleBanners[$currentIndex];
-
- $currentIndex = ($currentIndex + 1) % $bannerCount;
-
- $_SESSION['banner_index'] = $currentIndex;
-
- if( !$selectedBanner){
- $this->destroySession();
- return $visibleBanners;
- }
-
- return [$selectedBanner];
- }
-
- return $visibleBanners;
- }
-
- /**
- * @unreleased
- */
- public function startSession(): void
- {
- if (!session_id()) {
- session_start();
- }
- }
-
- /**
- * @unreleased
- */
- public function destroySession(): void
- {
- if (session_id()) {
- session_destroy();
- }
- }
-
-
- /**
- * @unreleased
+ * @since 3.13.0
*/
public static function getBasicLicenseSlugs(): array
{
@@ -335,7 +282,7 @@ public static function getBasicLicenseSlugs(): array
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public static function getPlusLicenseSlugs(): array
{
@@ -360,7 +307,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
deleted file mode 100644
index 12b04f3c6c..0000000000
--- a/src/Promotions/InPluginUpsells/StellarSaleBanners.php
+++ /dev/null
@@ -1,143 +0,0 @@
- 'bfgt2024-give',
- 'mainHeader' => self::getDataByPricingPlan([
- 'Pro' => __('Make it stellar.', 'give'),
- 'default' => __('Make it yours.', 'give'),
- ]),
- 'subHeader' => self::getDataByPricingPlan([
- 'Basic' => __('Save 40% on the GiveWP Plus Plan.', 'give'),
- 'Plus' => __('Save 40% on the GiveWP Pro Plan.', 'give'),
- 'Pro' => __('Save 40% on all StellarWP products.', 'give'),
- 'default' => __('Save 40% on the GiveWP Plus Plan.', 'give'),
- ]),
- 'actionText' => __('Shop Now', 'give'),
- 'actionURL' => self::getDataByPricingPlan([
- 'Basic' => 'https://go.givewp.com/plusplan',
- 'Plus' => 'https://go.givewp.com/pro',
- 'Pro' => 'https://go.givewp.com/stellarsale',
- 'default' => 'https://go.givewp.com/plusplan',
- ]),
- 'secondaryActionText' => __('View all StellarWP Deals', 'give'),
- 'secondaryActionURL' => 'https://go.givewp.com/stellarsale',
- 'content' => self::getDataByPricingPlan([
- 'Pro' => sprintf(__('Take %s off all brands during the annual Stellar Sale. Now through July 30.', 'give'),
- '
40% '),
- 'default' => sprintf(__('Take %s off all StellarWP brands during the annual Stellar Sale. Now through July 30.', 'give'),
- '
40% '),
- ]),
- 'startDate' => '2024-07-23 00:00',
- 'endDate' => '2024-07-30 23:59',
- ],
- ];
-
- foreach($this->getAddonBanners() as $addonBanner){
- $banners[] = $addonBanner;
- }
-
- return $banners;
- }
-
- /**
- * @unreleased
- */
- public function getP2PBanners(): array
- {
- return [
- [
- 'id' => 'bfgt2024-p2p',
- 'mainHeader' => __('Make it yours.', 'give'),
- 'subHeader' => __('Save 40% on Peer-to-Peer Fundraising.', 'give'),
- 'actionText' => __('Shop Now', 'give'),
- 'actionURL' => self::getDataByPricingPlan([
- 'Basic' => 'https://go.givewp.com/p2p',
- 'Plus' => 'https://go.givewp.com/p2ppro',
- 'default' => 'https://go.givewp.com/p2p',
- ]),
- 'secondaryActionText' => __('View all StellarWP Deals', 'give'),
- 'secondaryActionURL' => 'https://go.givewp.com/stellarsale',
- 'content' => self::getDataByPricingPlan([
- 'Basic' => __('Open up your donation forms to your supporters during the annual Stellar Sale. Now through July 30.', 'give'),
- 'Plus' => __('Upgrade to the Pro Plan and get Peer-to-Peer Fundraising during the annual Stellar Sale. Now through July 30.', 'give'),
- 'Pro' => __('Upgrade to the Pro Plan and get Peer-to-Peer Fundraising during the annual Stellar Sale. Now through July 30.', 'give'),
- 'default' => __('Open up your donation forms to your supporters during the annual Stellar Sale. Now through July 30.', 'give'),
- ]),
- 'startDate' => '2024-07-23 00:00',
- 'endDate' => '2024-07-30 23:59',
- ],
- ];
- }
-
- /**
- * @unreleased
- */
- public function getAddonBanners(): array
- {
- if(self::getUserPricingPlan() === 'Pro') {
- return [];
- }
-
- $addonBanners = [];
-
- if(!defined('GIVE_P2P_VERSION')) {
- $addonBanners = $this->getP2PBanners();
- }
-
- return $addonBanners;
- }
-
- /**
- * @unreleased
- */
- public function loadScripts(): void
- {
- wp_enqueue_style(
- 'give-in-plugin-upsells-stellar-sales-banner',
- GIVE_PLUGIN_URL . 'assets/dist/css/admin-stellarwp-sales-banner.css',
- [],
- GIVE_VERSION
- );
-
- wp_enqueue_style('givewp-admin-fonts');
- }
-
- /**
- * @unreleased
- */
- public function render(): void
- {
- $banners = $this->alternateVisibleBanners();
-
- if (!empty($banners)) {
- include __DIR__ . '/resources/views/stellarwp-sale-banner.php';
- }
- }
-
- /**
- * @unreleased
- */
- public static function isShowing(): bool
- {
- $saleBanners = new self();
- $page = $_GET['page'] ?? [];
- $validPages = ['give-donors', 'give-payment-history', 'give-reports'];
-
- return isset($_GET['post_type']) && $_GET['post_type'] === 'give_forms' &&
- in_array($page, $validPages, true) &&
- !empty($saleBanners->getBanners());
- }
-}
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 @@
, root);
+ createRoot(root).render(
);
}
diff --git a/src/ServiceProviders/LegacyServiceProvider.php b/src/ServiceProviders/LegacyServiceProvider.php
index 1d46bb3935..42ff0e4889 100644
--- a/src/ServiceProviders/LegacyServiceProvider.php
+++ b/src/ServiceProviders/LegacyServiceProvider.php
@@ -38,7 +38,7 @@ public function boot()
/**
* Load all the legacy class files since they don't have auto-loading
*
- * @unrleased remove WP_Background_Process & WP_Async_Request in favor of namespaced versions.
+ * @since 3.1.2 remove WP_Background_Process & WP_Async_Request in favor of namespaced versions.
* @since 3.0.0 remove the manual (Test Donations) gateway from loading in favor of the new Test Donations gateway
* @since 2.8.0
*/
diff --git a/src/Session/SessionDonation/SessionObjects/Donation.php b/src/Session/SessionDonation/SessionObjects/Donation.php
index 9d28a34050..40d88a55ca 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.
+ *
+ * @since 3.18.0
+ * @var FormEntry
+ */
+ public $formEntry;
+
+ /**
+ * Donor information.
+ *
+ * @since 3.18.0
+ * @var DonorInfo
+ */
+ public $donorInfo;
+
+ /**
+ * Card information.
+ *
+ * @since 3.18.0
+ * @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..5a1f0f29a4 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.
+ *
+ * @since 3.18.0
+ * @var FormEntry
+ */
+ public $formEntry;
+
+ /**
+ * Donor information.
+ *
+ * @since 3.18.0
+ * @var DonorInfo
+ */
+ public $donorInfo;
+
+ /**
+ * Card information.
+ *
+ * @since 3.18.0
+ * @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.
*
diff --git a/src/Settings/Security/Actions/RegisterPage.php b/src/Settings/Security/Actions/RegisterPage.php
new file mode 100644
index 0000000000..378bca8601
--- /dev/null
+++ b/src/Settings/Security/Actions/RegisterPage.php
@@ -0,0 +1,21 @@
+getSettings();
+ }
+
+ /**
+ * @since 3.17.0
+ */
+ protected function getSettings(): array
+ {
+ return [
+ [
+ 'id' => 'give_title_settings_security_1',
+ 'type' => 'title',
+ ],
+ $this->getHoneypotSettings(),
+ [
+ 'id' => 'give_title_settings_security_1',
+ 'type' => 'sectionend',
+ ],
+ ];
+ }
+
+ /**
+ * @since 3.17.1 enable by default
+ * @since 3.17.0
+ */
+ 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' => 'enabled',
+ '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..9147d1b82c
--- /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..776aa5e27f
--- /dev/null
+++ b/src/Settings/ServiceProvider.php
@@ -0,0 +1,42 @@
+registerSecuritySettings();
+ }
+
+ /**
+ * @since 3.17.0
+ */
+ 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/src/Subscriptions/Repositories/SubscriptionRepository.php b/src/Subscriptions/Repositories/SubscriptionRepository.php
index e83df8f344..0c99851181 100644
--- a/src/Subscriptions/Repositories/SubscriptionRepository.php
+++ b/src/Subscriptions/Repositories/SubscriptionRepository.php
@@ -186,6 +186,7 @@ public function insert(Subscription $subscription)
}
/**
+ * @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
@@ -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/SubscriptionsAdminPage.php b/src/Subscriptions/SubscriptionsAdminPage.php
index 7709278132..ecb0dfdaac 100644
--- a/src/Subscriptions/SubscriptionsAdminPage.php
+++ b/src/Subscriptions/SubscriptionsAdminPage.php
@@ -79,7 +79,7 @@ private function getForms()
return array_merge([
[
'value' => '0',
- 'text' => 'Any',
+ 'text' => __('Any', 'give'),
]
], $options);
}
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/Subscriptions/ValueObjects/SubscriptionStatus.php b/src/Subscriptions/ValueObjects/SubscriptionStatus.php
index b71dab27da..8ead12566d 100644
--- a/src/Subscriptions/ValueObjects/SubscriptionStatus.php
+++ b/src/Subscriptions/ValueObjects/SubscriptionStatus.php
@@ -5,6 +5,7 @@
use Give\Framework\Support\ValueObjects\Enum;
/**
+ * @since 3.17.0 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';
/**
+ * @since 3.17.0 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/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(
+
+
+
+ );
+}
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}
>
- {__('Switch to Legacy View')}
+ {__('Switch to Legacy View', 'give')}
);
diff --git a/src/TestData/Framework/Factory.php b/src/TestData/Framework/Factory.php
index 79c0e15751..4a0fe27fdf 100644
--- a/src/TestData/Framework/Factory.php
+++ b/src/TestData/Framework/Factory.php
@@ -2,7 +2,7 @@
namespace Give\TestData\Framework;
-use Give\Vendors\Faker\Generator;
+use Faker\Generator;
abstract class Factory implements FactoryContract
{
diff --git a/src/TestData/Framework/Provider/RandomProvider.php b/src/TestData/Framework/Provider/RandomProvider.php
index 6667a6bdfe..5d1cfe4842 100644
--- a/src/TestData/Framework/Provider/RandomProvider.php
+++ b/src/TestData/Framework/Provider/RandomProvider.php
@@ -3,7 +3,7 @@
namespace Give\TestData\Framework\Provider;
-use Give\Vendors\Faker\Generator;
+use Faker\Generator;
abstract class RandomProvider implements ProviderContract
{
diff --git a/src/TestData/ServiceProvider.php b/src/TestData/ServiceProvider.php
index caf006bccb..7205064d89 100644
--- a/src/TestData/ServiceProvider.php
+++ b/src/TestData/ServiceProvider.php
@@ -2,8 +2,9 @@
namespace Give\TestData;
-use Give\Vendors\Faker\Factory as FakerFactory;
-use Give\Vendors\Faker\Generator as FakerGenerator;
+use Composer\InstalledVersions;
+use Faker\Factory as FakerFactory;
+use Faker\Generator as FakerGenerator;
use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
use Give\ServiceProviders\ServiceProvider as GiveServiceProvider;
use Give\TestData\Commands\DonationSeedCommand;
@@ -25,6 +26,10 @@ class ServiceProvider implements GiveServiceProvider
*/
public function register()
{
+ if ( ! $this->isFakerInstalled()) {
+ return;
+ }
+
// Instead of passing around an instance, bind a singleton to the container.
give()->singleton(
FakerGenerator::class,
@@ -39,6 +44,10 @@ function () {
*/
public function boot()
{
+ if ( ! $this->isFakerInstalled()) {
+ return;
+ }
+
// Add CLI commands
if (defined('WP_CLI') && WP_CLI) {
$this->addCommands();
@@ -88,4 +97,18 @@ private function addCommands()
WP_CLI::add_command('give test-donation-form', give()->make(FormSeedCommand::class));
WP_CLI::add_command('give test-logs', give()->make(LogsSeedCommand::class));
}
+
+ /**
+ * Helper function used to check if Faker library is installed
+ *
+ * @see https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * @since 3.17.2
+ *
+ * @return bool
+ */
+ private function isFakerInstalled(): bool
+ {
+ return InstalledVersions::isInstalled('fakerphp/faker');
+ }
}
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/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/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;
}
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 = ({
)}
-
+
{title}
diff --git a/src/Views/Components/ListTable/ListTablePage/ListTablePage.module.scss b/src/Views/Components/ListTable/ListTablePage/ListTablePage.module.scss
index 7f0e417789..5731c9e450 100644
--- a/src/Views/Components/ListTable/ListTablePage/ListTablePage.module.scss
+++ b/src/Views/Components/ListTable/ListTablePage/ListTablePage.module.scss
@@ -120,6 +120,22 @@
justify-content: flex-end;
}
+.button:is(:global(.button)) {
+ border-radius: 0.125rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ line-height: 1.25rem;
+ padding: 0.5rem 1rem;
+}
+
+.buttonSecondary:is(:global(.button)) {
+ background-color: #fff;
+
+ &:hover {
+ background-color: #f6f7f7;
+ }
+}
+
.addFormButton {
$depth: 0px 1px 0px rgba(0, 0, 0, 0.25);
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;
diff --git a/src/Views/Form/Templates/Classic/Classic.php b/src/Views/Form/Templates/Classic/Classic.php
index 95554d0d28..8892f25abc 100644
--- a/src/Views/Form/Templates/Classic/Classic.php
+++ b/src/Views/Form/Templates/Classic/Classic.php
@@ -2,11 +2,13 @@
namespace Give\Views\Form\Templates\Classic;
+use Give\DonationForms\DonationQuery;
use Give\Form\Template;
use Give\Form\Template\Hookable;
use Give\Form\Template\Scriptable;
use Give\Helpers\Form\Template as FormTemplateUtils;
use Give\Helpers\Form\Template\Utils\Frontend;
+use Give\MultiFormGoals\ProgressBar\Model as ProgressBarModal;
use Give\Receipt\DonationReceipt;
use Give_Donate_Form;
use InvalidArgumentException;
@@ -103,7 +105,7 @@ public function loadHooks()
];
foreach ($sections as $section) {
- list ($start, $end) = array_pad($section[ 'hooks' ], 2, null);
+ [$start, $end] = array_pad($section['hooks'], 2, null);
add_action($start, function () use ($section) {
printf('', $section[ 'class' ]);
@@ -289,10 +291,13 @@ public function getReceiptDetails($donationId)
return $receipt;
}
+ /**
+ * @since 3.14.0 Use sumIntendedAmount() and getDonationCount() methods to retrieve the proper values for the raised amount and donations count
+ */
public function getFormGoalStats(Give_Donate_Form $form)
{
$goalStats = give_goal_progress_stats($form->get_ID());
- $raisedRaw = $form->get_earnings();
+ $raisedRaw = (new DonationQuery())->form($form->ID)->sumIntendedAmount();
// Setup default raised value
$raised = give_currency_filter(
@@ -306,7 +311,7 @@ public function getFormGoalStats(Give_Donate_Form $form)
);
// Setup default count value
- $count = $form->get_sales();
+ $count = (new ProgressBarModal(['ids' => [$form->get_ID()]]))->getDonationCount();
// Setup default count label
$countLabel = _n('donation', 'donations', $count, 'give');
diff --git a/src/Views/Form/Templates/Sequoia/sections/income-stats.php b/src/Views/Form/Templates/Sequoia/sections/income-stats.php
index f4f09841f5..78396ac557 100644
--- a/src/Views/Form/Templates/Sequoia/sections/income-stats.php
+++ b/src/Views/Form/Templates/Sequoia/sections/income-stats.php
@@ -3,6 +3,13 @@
/**
* @var int $formId
*/
+
+use Give\DonationForms\DonationQuery;
+use Give\MultiFormGoals\ProgressBar\Model as ProgressBarModal;
+
+/**
+ * @since 3.14.0 Use sumIntendedAmount() and getDonationCount() methods to retrieve the proper values for the raised amount and donations count
+ */
if ($form->has_goal()) : ?>
get_earnings(),
+ (new DonationQuery())->form($formId)->sumIntendedAmount(),
[
'sanitize' => false,
'decimal' => false,
@@ -19,7 +26,7 @@
);
// Setup default count value
- $count = $form->get_sales();
+ $count = (new ProgressBarModal(['ids' => [$formId]]))->getDonationCount();;
// Setup default count label
$countLabel = _n('donation', 'donations', $count, 'give');
diff --git a/templates/shortcode-donor-wall.php b/templates/shortcode-donor-wall.php
index b54a5a4c37..7b360a17f8 100644
--- a/templates/shortcode-donor-wall.php
+++ b/templates/shortcode-donor-wall.php
@@ -38,6 +38,17 @@
";
+ } 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 "
+
+
+
+ ";
+
} elseif ($donation['_give_payment_donor_email'] && give_validate_gravatar(
$donation['_give_payment_donor_email']
)) {
diff --git a/templates/shortcode-form-grid.php b/templates/shortcode-form-grid.php
index f9711f398b..76cf35a0bb 100644
--- a/templates/shortcode-form-grid.php
+++ b/templates/shortcode-form-grid.php
@@ -3,10 +3,10 @@
* This template is used to display the donation grid with [donation_grid]
*/
-// Exit if accessed directly.
use Give\Helpers\Form\Template;
use Give\Helpers\Form\Utils as FormUtils;
+// Exit if accessed directly.
if (!defined('ABSPATH')) {
exit;
}
@@ -14,6 +14,7 @@
/**
* List of changes
*
+ * @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.
*/
@@ -236,12 +237,19 @@ 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;
+
+ /**
+ * @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(
'give_goal_shortcode_stats',
[
@@ -401,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)
);
@@ -451,8 +459,7 @@ static function ($term) use ($tag_text_color, $tag_bg_color) {
- get_sales(), ['decimal' => false]) ?>
+ get_sales(), $form_id) ?>
__('Subscribe to our newsletter?'),
- 'give_activecampaign_lists' => ['1', '2'],
- 'give_activecampaign_tags' => ['tag 1', 'tag 2'],
+ // Arrange
+ $options = [
+ 'give_activecampaign_globally_enabled' => 'on',
+ 'give_activecampaign_label' => __('Subscribe to our newsletter?'),
+ 'give_activecampaign_lists' => ['1', '2'],
+ 'give_activecampaign_tags' => ['tag 1', 'tag 2'],
'give_activecampaign_checkbox_default' => true,
];
+ foreach ($options as $key => $value) {
+ give_update_option($key, $value);
+ }
+ $meta = ['activecampaign_per_form_options' => 'global'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $mailchimp = new ActiveCampaign($payload);
-
- $mailchimp->process();
-
- $block = $payload->formV3->blocks->findByName('give-activecampaign/activecampaign');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ActiveCampaign::class);
- $this->assertSame($meta['give_activecampaign_label'], $block->getAttribute('label'));
- $this->assertSame($meta['give_activecampaign_lists'], $block->getAttribute('selectedLists'));
- $this->assertSame($meta['give_activecampaign_tags'], $block->getAttribute('selectedTags'));
+ // Assert
+ $block = $v3Form->blocks->findByName('give-activecampaign/activecampaign');
+ $this->assertSame($options['give_activecampaign_label'], $block->getAttribute('label'));
+ $this->assertSame($options['give_activecampaign_lists'], $block->getAttribute('selectedLists'));
+ $this->assertSame($options['give_activecampaign_tags'], $block->getAttribute('selectedTags'));
$this->assertTrue(true, $block->getAttribute('defaultChecked'));
}
/**
- * @since 3.10.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateActiveCampaignBlockAttributesFromGlobalSettings(): void
+ public function testFormConfiguredToUseGlobalActiveCampaignSettingsIsMigratedWithoutActiveCampaignBlockWhenNotGloballyEnabled()
{
- $meta = [
- 'give_activecampaign_label' => __('Subscribe to our newsletter?'),
- 'give_activecampaign_lists' => ['1', '2'],
- 'give_activecampaign_tags' => ['tag 1', 'tag 2'],
- 'give_activecampaign_checkbox_default' => true,
- ];
-
- foreach ($meta as $key => $value) {
- give_update_option($key, $value);
- }
-
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
+ // Arrange
+ give_update_option('give_activecampaign_globally_enabled', 'off');
+ $meta = ['activecampaign_per_form_options' => 'global'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $payload = FormMigrationPayload::fromFormV2($formV2);
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ActiveCampaign::class);
- $mailchimp = new ActiveCampaign($payload);
-
- $mailchimp->process();
-
- $block = $payload->formV3->blocks->findByName('give-activecampaign/activecampaign');
-
- $this->assertSame($meta['give_activecampaign_label'], $block->getAttribute('label'));
- $this->assertSame($meta['give_activecampaign_lists'], $block->getAttribute('selectedLists'));
- $this->assertSame($meta['give_activecampaign_tags'], $block->getAttribute('selectedTags'));
- $this->assertTrue(true, $block->getAttribute('defaultChecked'));
+ // Assert
+ $block = $v3Form->blocks->findByName('give-activecampaign/activecampaign');
+ $this->assertNull($block);
}
/**
- * @since 3.10.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateActiveCampaignBlockAttributesWhenNoMeta(): void
+ public function testFormConfiguredToDisableActiveCampaignIsMigratedWithoutActiveCampaignBlock()
{
- $formV2 = $this->createSimpleDonationForm();
+ // Arrange
+ $meta = ['activecampaign_per_form_options' => 'disabled'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $payload = FormMigrationPayload::fromFormV2($formV2);
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ActiveCampaign::class);
- $mailchimp = new ActiveCampaign($payload);
+ // Assert
+ $block = $v3Form->blocks->findByName('give-activecampaign/activecampaign');
+ $this->assertNull($block);
+ }
- $mailchimp->process();
+ /**
+ * @since 3.16.0
+ */
+ public function testFormConfiguredToUseCustomizedActiveCampaignSettingsIsMigrated()
+ {
+ // Arrange
+ $meta = [
+ 'activecampaign_per_form_options' => 'customized',
+ 'give_activecampaign_label' => __('Subscribe to our newsletter?'),
+ 'give_activecampaign_lists' => ['1', '2'],
+ 'give_activecampaign_tags' => ['tag 1', 'tag 2'],
+ 'give_activecampaign_checkbox_default' => true,
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $block = $payload->formV3->blocks->findByName('give-activecampaign/activecampaign');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ActiveCampaign::class);
- $this->assertSame(__('Subscribe to our newsletter?'), $block->getAttribute('label'));
- $this->assertSame([], $block->getAttribute('selectedLists'));
- $this->assertNull(null, $block->getAttribute('selectedTags'));
+ // Assert
+ $block = $v3Form->blocks->findByName('give-activecampaign/activecampaign');
+ $this->assertSame($meta['give_activecampaign_label'], $block->getAttribute('label'));
+ $this->assertSame($meta['give_activecampaign_lists'], $block->getAttribute('selectedLists'));
+ $this->assertSame($meta['give_activecampaign_tags'], $block->getAttribute('selectedTags'));
$this->assertTrue(true, $block->getAttribute('defaultChecked'));
}
}
diff --git a/tests/Feature/FormMigration/Steps/TestConstantContact.php b/tests/Feature/FormMigration/Steps/TestConstantContact.php
index 52369f1d0c..ab133a52d5 100644
--- a/tests/Feature/FormMigration/Steps/TestConstantContact.php
+++ b/tests/Feature/FormMigration/Steps/TestConstantContact.php
@@ -2,94 +2,91 @@
namespace Give\Tests\Feature\FormMigration\Steps;
-use Give\FormMigration\DataTransferObjects\FormMigrationPayload;
use Give\FormMigration\Steps\ConstantContact;
use Give\Tests\TestCase;
use Give\Tests\TestTraits\RefreshDatabase;
use Give\Tests\Unit\DonationForms\TestTraits\LegacyDonationFormAdapter;
+use Give\Tests\Unit\FormMigration\TestTraits\FormMigrationProcessor;
/**
+ * @since 3.16.0 Update to use FormMigrationProcessor trait
* @since 3.7.0
- *
- * @covers \Give\FormMigration\Steps\DonationGoal
*/
class TestConstantContact extends TestCase
{
- use RefreshDatabase, LegacyDonationFormAdapter;
+ use FormMigrationProcessor;
+ use LegacyDonationFormAdapter;
+ use RefreshDatabase;
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.7.0
*/
- public function testProcessShouldUpdateConstantContactBlockAttributesWithV2FormMeta(): void
+ public function testFormMigratesUsingGlobalSettingsWhenGloballyEnabled(): void
{
- $meta = [
- '_give_constant_contact_custom_label' => 'Subscribe to our newsletter?',
- '_give_constant_contact_checked_default' => 'on',
- '_give_constant_contact' => ['1928414891'],
+ // Arrange
+ $options = [
+ '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);
+ }
+ $meta = ['_give_constant_contact_disabled' => 'false'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $constantContact = new ConstantContact($payload);
-
- $constantContact->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/constantcontact');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConstantContact::class);
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/constantcontact');
$this->assertTrue(true, $block->getAttribute('checked' === 'on'));
- $this->assertSame($meta['_give_constant_contact_custom_label'], $block->getAttribute('label'));
- $this->assertSame($meta['_give_constant_contact'], $block->getAttribute('selectedEmailLists'));
+ $this->assertSame($options['givewp_constant_contact_label'], $block->getAttribute('label'));
+ $this->assertSame($options['givewp_constant_contact_list'], $block->getAttribute('selectedEmailLists'));
}
/**
- * @since 3.7.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateConstantContactBlockAttributesWithGlobalSettings(): void
+ public function testFormConfiguredToDisableConstantContactIsMigratedWithoutConstantContactBlock()
{
- $meta = [
- 'give_constant_contact_label' => 'Subscribe to our newsletter?',
- 'give_constant_contact_checked_default' => 'on',
- 'give_constant_contact_list' => ['1928414891'],
- ];
-
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
+ // Arrange
+ give_update_option('givewp_constant_contact_show_checkout_signup', 'on');
+ $meta = ['_give_constant_contact_disable' => 'true'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $payload = FormMigrationPayload::fromFormV2($formV2);
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConstantContact::class);
- foreach ($meta as $key => $value) {
- give_update_option($key, $value);
- }
-
- $constantContact = new ConstantContact($payload);
-
- $constantContact->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/constantcontact');
-
- $this->assertTrue(true, $block->getAttribute('checked' === 'on'));
- $this->assertSame($meta['give_constant_contact_label'], $block->getAttribute('label'));
- $this->assertSame($meta['give_constant_contact_list'], $block->getAttribute('selectedEmailLists'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/constantcontact');
+ $this->assertNull($block);
}
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.7.0
*/
- public function testProcessShouldUpdateConstantContactBlockAttributesWhenNoMeta(): void
+ public function testFormConfiguredToUseCustomizedConstantContactSettingsIsMigrated(): void
{
- $formV2 = $this->createSimpleDonationForm();
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $constantContact = new ConstantContact($payload);
-
- $constantContact->process();
+ // Arrange
+ $meta = [
+ '_give_constant_contact_enable' => 'true',
+ '_give_constant_contact_custom_label' => 'Subscribe to our newsletter?',
+ '_give_constant_contact_checked_default' => 'on',
+ '_give_constant_contact' => ['1928414891'],
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $block = $payload->formV3->blocks->findByName('givewp/constantcontact');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConstantContact::class);
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/constantcontact');
$this->assertTrue(true, $block->getAttribute('checked' === 'on'));
- $this->assertSame('Subscribe to our newsletter?', $block->getAttribute('label'));
- $this->assertNull(null, $block->getAttribute('selectedEmailLists'));
+ $this->assertSame($meta['_give_constant_contact_custom_label'], $block->getAttribute('label'));
+ $this->assertSame($meta['_give_constant_contact'], $block->getAttribute('selectedEmailLists'));
}
}
diff --git a/tests/Feature/FormMigration/Steps/TestConvertKit.php b/tests/Feature/FormMigration/Steps/TestConvertKit.php
index 9c51256eb2..9832d66858 100644
--- a/tests/Feature/FormMigration/Steps/TestConvertKit.php
+++ b/tests/Feature/FormMigration/Steps/TestConvertKit.php
@@ -4,94 +4,108 @@
namespace Give\Tests\Feature\FormMigration\Steps;
-use Give\FormMigration\DataTransferObjects\FormMigrationPayload;
use Give\FormMigration\Steps\ConvertKit;
use Give\Tests\TestCase;
use Give\Tests\TestTraits\RefreshDatabase;
use Give\Tests\Unit\DonationForms\TestTraits\LegacyDonationFormAdapter;
+use Give\Tests\Unit\FormMigration\TestTraits\FormMigrationProcessor;
class TestConvertKit extends TestCase
{
- use RefreshDatabase, LegacyDonationFormAdapter;
+ use FormMigrationProcessor;
+ use LegacyDonationFormAdapter;
+ use RefreshDatabase;
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.11.0
*/
- public function testProcessShouldUpdateConvertkitBlockAttributesFromV2FormMeta(): void
+ public function testFormConfiguredToUseGlobalConvertKitSettingsMigratesUsingGlobalSettingsWhenGloballyEnabled()
{
- $meta = [
- '_give_convertkit_custom_label' => __('Subscribe to newsletter?' , 'give'),
- '_give_convertkit' => '6352843',
- '_give_convertkit_tags' => ['4619079', '4619080'],
- '_give_convertkit_checked_default' => true,
+ // Arrange
+ $options = [
+ 'give_convertkit_show_subscribe_checkbox' => 'enabled',
+ 'give_convertkit_label' => __('Subscribe to newsletter?', 'give'),
+ 'give_convertkit_list' => '6352843',
+ '_give_convertkit_tags' => ['4619079', '4619080'],
+ 'give_convertkit_checked_default' => true,
];
+ foreach ($options as $key => $value) {
+ give_update_option($key, $value);
+ }
+ $meta = ['_give_convertkit_override_option' => 'default'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $convertkit = new ConvertKit($payload);
-
- $convertkit->process();
-
- $block = $payload->formV3->blocks->findByName('givewp-convertkit/convertkit');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConvertKit::class);
- $this->assertSame($meta['_give_convertkit_custom_label'], $block->getAttribute('label'));
- $this->assertSame($meta['_give_convertkit_tags'], $block->getAttribute('tagSubscribers'));
- $this->assertSame($meta['_give_convertkit'], $block->getAttribute('selectedForm'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-convertkit/convertkit');
+ $this->assertSame($options['give_convertkit_label'], $block->getAttribute('label'));
+ $this->assertSame($options['give_convertkit_list'], $block->getAttribute('selectedForm'));
+ $this->assertSame($options['_give_convertkit_tags'], $block->getAttribute('tagSubscribers'));
$this->assertTrue(true, $block->getAttribute('defaultChecked'));
}
/**
- * @since 3.11.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateConvertkitBlockAttributesFromGlobalSettings(): void
+ public function testFormConfiguredToUseGlobalConvertKitSettingsIsMigratedWithoutConvertKitBlockWhenNotGloballyEnabled()
{
- $meta = [
- 'give_convertkit_label' => __('Subscribe to newsletter?', 'give'),
- 'give_convertkit_list' => '6352843',
- '_give_convertkit_tags' => ['4619079', '4619080'],
- 'give_convertkit_checked_default' => true,
- ];
+ // Arrange
+ give_update_option('give_convertkit_show_subscribe_checkbox', 'disabled');
+ $meta = ['_give_convertkit_override_option' => 'default'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- foreach ($meta as $key => $value) {
- give_update_option($key, $value);
- }
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConvertKit::class);
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $convertkit = new ConvertKit($payload);
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-convertkit/convertkit');
+ $this->assertNull($block);
+ }
- $convertkit->process();
+ /**
+ * @since 3.16.0
+ */
+ public function testFormConfiguredToDisableConvertKitIsMigratedWithoutConvertKitBlock()
+ {
+ // Arrange
+ $meta = ['_give_convertkit_override_option' => 'disabled'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $block = $payload->formV3->blocks->findByName('givewp-convertkit/convertkit');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConvertKit::class);
- $this->assertSame($meta['give_convertkit_label'], $block->getAttribute('label'));
- $this->assertSame($meta['give_convertkit_list'], $block->getAttribute('selectedForm'));
- $this->assertSame($meta['_give_convertkit_tags'], $block->getAttribute('tagSubscribers'));
- $this->assertTrue(true, $block->getAttribute('defaultChecked'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-convertkit/convertkit');
+ $this->assertNull($block);
}
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.11.0
*/
- public function testProcessShouldUpdateConvertkitBlockAttributesWhenNoMeta(): void
+ public function testFormConfiguredToUseCustomizedConvertKitSettingsIsMigrated()
{
- $formV2 = $this->createSimpleDonationForm();
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $convertkit = new ConvertKit($payload);
-
- $convertkit->process();
+ // Arrange
+ $meta = [
+ '_give_convertkit_override_option' => 'customize',
+ '_give_convertkit_custom_label' => __('Subscribe to newsletter?' , 'give'),
+ '_give_convertkit' => '6352843',
+ '_give_convertkit_tags' => ['4619079', '4619080'],
+ '_give_convertkit_checked_default' => true,
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $block = $payload->formV3->blocks->findByName('givewp-convertkit/convertkit');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, ConvertKit::class);
- $this->assertSame(__('Subscribe to newsletter?', 'give'), $block->getAttribute('label'));
- $this->assertSame('', $block->getAttribute('selectedForm'));
- $this->assertNull(null, $block->getAttribute('tagSubscribers'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-convertkit/convertkit');
+ $this->assertSame($meta['_give_convertkit_custom_label'], $block->getAttribute('label'));
+ $this->assertSame($meta['_give_convertkit_tags'], $block->getAttribute('tagSubscribers'));
+ $this->assertSame($meta['_give_convertkit'], $block->getAttribute('selectedForm'));
$this->assertTrue(true, $block->getAttribute('defaultChecked'));
}
}
diff --git a/tests/Feature/FormMigration/Steps/TestCurrencySwitcher.php b/tests/Feature/FormMigration/Steps/TestCurrencySwitcher.php
index 0c3cf497a9..8483475e92 100644
--- a/tests/Feature/FormMigration/Steps/TestCurrencySwitcher.php
+++ b/tests/Feature/FormMigration/Steps/TestCurrencySwitcher.php
@@ -17,7 +17,7 @@ class TestCurrencySwitcher extends TestCase
use LegacyDonationFormAdapter;
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testFormWithoutCurrencySwitcherSettingsMigratesUsingGlobalSettings(): void
{
@@ -35,7 +35,7 @@ public function testFormWithoutCurrencySwitcherSettingsMigratesUsingGlobalSettin
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testFormConfiguredToUseGlobalCurrencySwitcherSettingsIsMigrated(): void
{
@@ -56,7 +56,7 @@ public function testFormConfiguredToUseGlobalCurrencySwitcherSettingsIsMigrated(
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testFormConfiguredToDisableCurrencySwitcherIsMigrated(): void
{
@@ -77,7 +77,7 @@ public function testFormConfiguredToDisableCurrencySwitcherIsMigrated(): void
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testFormConfiguredToUseLocalCurrencySwitcherIsMigrated(): void
{
@@ -111,7 +111,7 @@ public function testFormConfiguredToUseLocalCurrencySwitcherIsMigrated(): void
* Sets up and returns a v2 donation form configured with the
* given attributes being set to the Currency Switcher settings.
*
- * @unreleased
+ * @since 3.13.0
*
* @throws Exception
*/
@@ -127,7 +127,7 @@ private function setUpDonationForm(array $attributes = []): V2DonationForm
}
/**
- * @unreleased
+ * @since 3.13.0
*/
private function migrateForm(V2DonationForm $form): DonationForm
{
diff --git a/tests/Feature/FormMigration/Steps/TestDonationGoal.php b/tests/Feature/FormMigration/Steps/TestDonationGoal.php
index 5628134dd9..12d9794487 100644
--- a/tests/Feature/FormMigration/Steps/TestDonationGoal.php
+++ b/tests/Feature/FormMigration/Steps/TestDonationGoal.php
@@ -2,26 +2,31 @@
namespace Give\Tests\Feature\FormMigration\Steps;
-use Give\FormMigration\DataTransferObjects\FormMigrationPayload;
use Give\FormMigration\Steps\DonationGoal;
use Give\Tests\TestCase;
use Give\Tests\TestTraits\RefreshDatabase;
use Give\Tests\Unit\DonationForms\TestTraits\LegacyDonationFormAdapter;
+use Give\Tests\Unit\FormMigration\TestTraits\FormMigrationProcessor;
/**
+ * @since 3.16.0 Update to use FormMigrationProcessor trait
* @since 3.4.0
*
* @covers \Give\FormMigration\Steps\DonationGoal
*/
class TestDonationGoal extends TestCase
{
- use RefreshDatabase, LegacyDonationFormAdapter;
+ use FormMigrationProcessor;
+ use LegacyDonationFormAdapter;
+ use RefreshDatabase;
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.4.0
*/
public function testProcessShouldUpdateDonationFormDonationGoalSettings(): void
{
+ // Arrange
$meta = [
'_give_goal_option' => 'enabled',
'_give_goal_setting' => 'enabled',
@@ -30,17 +35,13 @@ public function testProcessShouldUpdateDonationFormDonationGoalSettings(): void
'_give_close_form_when_goal_achieved' => 'enabled',
'_give_form_goal_achieved_message' => __( 'Thank you to all our donors, we have met our fundraising goal.', 'give' ),
];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $donationGoal = new DonationGoal($payload);
-
- $donationGoal->process();
-
- $settings = $payload->formV3->settings;
+ // Act
+ $v3Form = $this->migrateForm($v2Form, DonationGoal::class);
+ // Assert
+ $settings = $v3Form->settings;
$this->assertTrue(true, $settings->enableDonationGoal);
$this->assertTrue($settings->goalType->isAmount());
$this->assertSame((string)$meta['_give_set_goal'], $settings->goalAmount);
diff --git a/tests/Feature/FormMigration/Steps/TestDonationOptions.php b/tests/Feature/FormMigration/Steps/TestDonationOptions.php
index 3c4d0240b4..8ab4ce1274 100644
--- a/tests/Feature/FormMigration/Steps/TestDonationOptions.php
+++ b/tests/Feature/FormMigration/Steps/TestDonationOptions.php
@@ -2,11 +2,11 @@
namespace Give\Tests\Feature\FormMigration\Steps;
-use Give\FormMigration\DataTransferObjects\FormMigrationPayload;
use Give\FormMigration\Steps\DonationOptions;
use Give\Tests\TestCase;
use Give\Tests\TestTraits\RefreshDatabase;
use Give\Tests\Unit\DonationForms\TestTraits\LegacyDonationFormAdapter;
+use Give\Tests\Unit\FormMigration\TestTraits\FormMigrationProcessor;
/**
* @since 3.4.0
@@ -14,13 +14,17 @@
* @covers \Give\FormMigration\Steps\DonationOptions
*/
class TestDonationOptions extends TestCase {
- use RefreshDatabase, LegacyDonationFormAdapter;
+ use FormMigrationProcessor;
+ use LegacyDonationFormAdapter;
+ use RefreshDatabase;
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.4.0
*/
public function testProcessShouldUpdateDonationAmountBlockAttributes(): void
{
+ // Arrange
$meta = [
'_give_price_option' => 'set',
'_give_set_price' => '100',
@@ -28,17 +32,13 @@ public function testProcessShouldUpdateDonationAmountBlockAttributes(): void
'_give_custom_amount_range_minimum' => '1',
'_give_custom_amount_range_maximum' => '1000',
];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $donationOptions = new DonationOptions($payload);
-
- $donationOptions->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/donation-amount');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, DonationOptions::class);
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/donation-amount');
$this->assertSame($meta['_give_price_option'], $block->getAttribute('priceOption'));
$this->assertSame($meta['_give_set_price'], $block->getAttribute('setPrice'));
$this->assertTrue($block->getAttribute('customAmount'));
@@ -47,27 +47,25 @@ public function testProcessShouldUpdateDonationAmountBlockAttributes(): void
}
/**
+ * @since 3.16.0 Update test to use FormMigrationProcessor::migrateForm method
* @since 3.12.0 Updated test to include donation levels with descriptions
* @since 3.4.0
*/
public function testProcessShouldUpdateDonationAmountBlockAttributesWithDonationLevels(): void
{
+ //Arrange
$meta = [
'_give_custom_amount' => 'enabled',
'_give_custom_amount_range_minimum' => '1',
'_give_custom_amount_range_maximum' => '1000',
];
+ $v2Form = $this->createMultiLevelDonationForm(['meta' => $meta]);
- $formV2 = $this->createMultiLevelDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $donationOptions = new DonationOptions($payload);
-
- $donationOptions->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/donation-amount');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, DonationOptions::class);
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/donation-amount');
$expectedLevels = [
[
'value' => 10.00,
diff --git a/tests/Feature/FormMigration/Steps/TestDoubleTheDonation.php b/tests/Feature/FormMigration/Steps/TestDoubleTheDonation.php
index 89f94a1b72..7a9df4f7ce 100644
--- a/tests/Feature/FormMigration/Steps/TestDoubleTheDonation.php
+++ b/tests/Feature/FormMigration/Steps/TestDoubleTheDonation.php
@@ -2,41 +2,43 @@
namespace Give\Tests\Feature\FormMigration\Steps;
-use Give\FormMigration\DataTransferObjects\FormMigrationPayload;
use Give\FormMigration\Steps\DoubleTheDonation;
use Give\Tests\TestCase;
use Give\Tests\TestTraits\RefreshDatabase;
use Give\Tests\Unit\DonationForms\TestTraits\LegacyDonationFormAdapter;
+use Give\Tests\Unit\FormMigration\TestTraits\FormMigrationProcessor;
/**
+ * @since 3.16.0 Update to use FormMigrationProcessor trait
* @since 3.8.0
*
* @covers \Give\FormMigration\Steps\DoubleTheDonation
*/
class TestDoubleTheDonation extends TestCase
{
- use RefreshDatabase, LegacyDonationFormAdapter;
+ use FormMigrationProcessor;
+ use LegacyDonationFormAdapter;
+ use RefreshDatabase;
public function testProcessShouldUpdateDoubleTheDonationBlockAttributes(): void
{
+ // Arrange
$meta = [
'give_dtd_label' => 'DTD Label',
+ 'dtd_enable_disable' => 'enabled',
];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+ // Act
+ $v3Form = $this->migrateForm($v2Form, DoubleTheDonation::class);
+
+ // Assert
$company = [
'company_id' => '',
'company_name' => '',
'entered_text' => '',
];
-
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $dtd = new DoubleTheDonation($payload);
- $dtd->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/dtd');
-
+ $block = $v3Form->blocks->findByName('givewp/dtd');
$this->assertSame($meta['give_dtd_label'], $block->getAttribute('label'));
$this->assertEqualsIgnoringCase($company, $block->getAttribute('company'));
}
diff --git a/tests/Feature/FormMigration/Steps/TestEmailSettings.php b/tests/Feature/FormMigration/Steps/TestEmailSettings.php
new file mode 100644
index 0000000000..5e2e904fa8
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestEmailSettings.php
@@ -0,0 +1,91 @@
+ 'enabled',
+ '_give_email_template' => 'default',
+ '_give_email_logo' => 'logo.png',
+ '_give_from_name' => 'Charity Org',
+ '_give_from_email' => 'email@example.org',
+ ];
+
+ $notifications = Give_Email_Notifications::get_instance()->get_email_notifications();
+ foreach ($notifications as $notification) {
+ add_filter("give_{$notification->config['id']}_get_recipients", [$this, 'getNotificationRecipients'], 1, 3);
+
+ $prefix = '_give_' . $notification->config['id'];
+ $notificationMeta = [
+ $prefix . '_notification' => 'enabled',
+ $prefix . '_email_subject' => $notification->config['label'],
+ $prefix . '_email_header' => 'Header for: ' . $notification->config['label'],
+ $prefix . '_email_message' => 'Message for: ' . $notification->config['label'],
+ $prefix . '_email_content_type' => 'text/html',
+ ];
+
+ if ($notification->config['has_recipient_field']) {
+ $notificationMeta[$prefix . '_recipient'] = [['email' => 'donor@charity.org']];
+ }
+ $meta = array_merge($meta, $notificationMeta);
+ }
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, EmailSettings::class);
+
+ // Assert
+ $this->assertSame($meta['_give_email_options'], $v3Form->settings->emailOptionsStatus);
+ $this->assertSame($meta['_give_email_template'], $v3Form->settings->emailTemplate);
+ $this->assertSame($meta['_give_email_logo'], $v3Form->settings->emailLogo);
+ $this->assertSame($meta['_give_from_name'], $v3Form->settings->emailFromName);
+ $this->assertSame($meta['_give_from_email'], $v3Form->settings->emailFromEmail);
+
+ foreach ($notifications as $notification) {
+ $configId = $notification->config['id'];
+ $this->assertSame('enabled', $v3Form->settings->emailTemplateOptions[$configId]['status']);
+ $this->assertSame($notification->config['label'], $v3Form->settings->emailTemplateOptions[$configId]['email_subject']);
+ $this->assertSame('Header for: ' . $notification->config['label'], $v3Form->settings->emailTemplateOptions[$configId]['email_header']);
+ $this->assertSame('Message for: ' . $notification->config['label'], $v3Form->settings->emailTemplateOptions[$configId]['email_message']);
+ $this->assertSame('text/html', $v3Form->settings->emailTemplateOptions[$configId]['email_content_type']);
+
+ if ($notification->config['has_recipient_field']) {
+ $this->assertSame(['donor@charity.org'],
+ $v3Form->settings->emailTemplateOptions[$configId]['recipient']);
+ }
+
+ remove_filter("give_{$notification->config['id']}_get_recipients", [$this, 'getNotificationRecipients'], 1);
+ }
+ }
+
+ public function getNotificationRecipients($recipientEmail, $instance, $formId)
+ {
+ return Give_Email_Notification_Util::get_value(
+ $instance,
+ Give_Email_Setting_Field::get_prefix( $instance, $formId ) . 'recipient',
+ $formId
+ );
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFeeRecovery.php b/tests/Feature/FormMigration/Steps/TestFeeRecovery.php
new file mode 100644
index 0000000000..eb378184f2
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFeeRecovery.php
@@ -0,0 +1,114 @@
+ 'enabled',
+ 'give_fee_configuration' => 'all_gateways',
+ 'give_fee_percentage' => 5,
+ 'give_fee_base_amount' => 0.50,
+ 'give_fee_maximum_fee_amount' => 20.00,
+ 'give_fee_breakdown' => 'enabled',
+ 'give_fee_mode' => 'donor_opt_in',
+ 'give_fee_checkbox_label' => 'Fee Recovery checkbox label',
+ 'give_fee_explanation' => 'Message for fee recovery',
+ ];
+ foreach ($options as $key => $value) {
+ give_update_option($key, $value);
+ }
+ $meta = ['_form_give_fee_recovery' => 'global'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FeeRecovery::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-fee-recovery/fee-recovery');
+ $this->assertSame(true, $block->getAttribute('useGlobalSettings'));
+ $this->assertSame(true, $block->getAttribute('feeSupportForAllGateways'));
+ $this->assertSame([], $block->getAttribute('perGatewaySettings'));
+ $this->assertSame(5.0, $block->getAttribute('feePercentage'));
+ $this->assertSame(0.5, $block->getAttribute('feeBaseAmount'));
+ $this->assertSame(20.0, $block->getAttribute('maxFeeAmount'));
+ $this->assertSame(true, $block->getAttribute('includeInDonationSummary'));
+ $this->assertSame(true, $block->getAttribute('donorOptIn'));
+ $this->assertSame('Fee Recovery checkbox label', $block->getAttribute('feeCheckboxLabel'));
+ $this->assertSame('Message for fee recovery', $block->getAttribute('feeMessage'));
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testFeeRecoveryProcessWithPerFormSettings(): void
+ {
+ // Arrange
+ $meta = [
+ '_form_give_fee_recovery' => 'enabled',
+ '_form_give_fee_configuration' => 'all_gateways',
+ '_form_give_fee_percentage' => 5,
+ '_form_give_fee_base_amount' => 0.50,
+ '_form_give_fee_maximum_fee_amount' => 20.00,
+ '_form_breakdown' => 'enabled',
+ '_form_give_fee_mode' => 'donor_opt_in',
+ '_form_give_fee_checkbox_label' => 'Fee Recovery checkbox label',
+ '_form_give_fee_explanation' => 'Message for fee recovery',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FeeRecovery::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-fee-recovery/fee-recovery');
+ $this->assertSame(false, $block->getAttribute('useGlobalSettings'));
+ $this->assertSame(true, $block->getAttribute('feeSupportForAllGateways'));
+ $this->assertSame(5.0, $block->getAttribute('feePercentage'));
+ $this->assertSame(0.5, $block->getAttribute('feeBaseAmount'));
+ $this->assertSame(20.0, $block->getAttribute('maxFeeAmount'));
+ $this->assertSame(true, $block->getAttribute('includeInDonationSummary'));
+ $this->assertSame(true, $block->getAttribute('donorOptIn'));
+ $this->assertSame('Fee Recovery checkbox label', $block->getAttribute('feeCheckboxLabel'));
+ $this->assertSame('Message for fee recovery', $block->getAttribute('feeMessage'));
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testFeeRecoveryProcessWithGlobalSettingsDisabled(): void
+ {
+ // Arrange
+ give_update_option('give_fee_recovery', 'disabled');
+ $meta = ['_form_give_fee_recovery' => 'global'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FeeRecovery::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp-fee-recovery/fee-recovery');
+ $this->assertNull($block);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFormExcerpt.php b/tests/Feature/FormMigration/Steps/TestFormExcerpt.php
new file mode 100644
index 0000000000..b994a3a55b
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFormExcerpt.php
@@ -0,0 +1,39 @@
+ 'This is a test excerpt',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['form' => $form]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormExcerpt::class);
+
+ // Assert
+ $this->assertSame($form['post_excerpt'], $v3Form->settings->formExcerpt);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFormFeaturedImage.php b/tests/Feature/FormMigration/Steps/TestFormFeaturedImage.php
new file mode 100644
index 0000000000..0d1e3d0970
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFormFeaturedImage.php
@@ -0,0 +1,106 @@
+ 'sequoia',
+ '_give_sequoia_form_template_settings' => [
+ 'introduction' => [
+ 'image' => 'https://example.com/image.jpg',
+ ],
+ ],
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFeaturedImage::class);
+
+ // Assert
+ $this->assertSame('https://example.com/image.jpg', $v3Form->settings->designSettingsImageUrl);
+ $this->assertSame('center', $v3Form->settings->designSettingsImageStyle);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testClassicTemplateHeaderBackgroundImageIsMigratedCorrectly(): void
+ {
+ // Arrange
+ $meta = [
+ '_give_form_template' => 'classic',
+ '_give_classic_form_template_settings' => [
+ 'visual_appearance' => [
+ 'header_background_image' => 'https://example.com/image.jpg',
+ ],
+ ],
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFeaturedImage::class);
+
+ // Assert
+ $this->assertSame('https://example.com/image.jpg', $v3Form->settings->designSettingsImageUrl);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testFallbackImageIsMigratedWhenFeaturedImageIsMissing(): void
+ {
+ // Arrange
+ $v2Form = $this->createSimpleDonationForm();
+ update_post_meta($v2Form->id, '_thumbnail_id', '1');
+ add_filter('wp_get_attachment_image_src', function ($image, $attachmentId) {
+ if ($attachmentId === 1) {
+ return ['https://example.com/image.jpg', 100, 100, false];
+ }
+
+ return $image;
+ }, 10, 2);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFeaturedImage::class);
+
+ // Assert
+ $this->assertSame('https://example.com/image.jpg', $v3Form->settings->designSettingsImageUrl);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testNoImageIsMigratedWhenNoImageExists ()
+ {
+ // Arrange
+ $v2Form = $this->createSimpleDonationForm();
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFeaturedImage::class);
+
+ // Assert
+ $this->assertEmpty($v3Form->settings->designSettingsImageUrl);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFormFields.php b/tests/Feature/FormMigration/Steps/TestFormFields.php
new file mode 100644
index 0000000000..103fc82a17
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFormFields.php
@@ -0,0 +1,88 @@
+ ['Mr.', 'Mrs.', 'Ms.', 'Dr.'],
+ ];
+ foreach ($options as $key => $value) {
+ give_update_option($key, $value);
+ }
+ $meta = [
+ '_give_name_title_prefix' => 'required',
+ '_give_title_prefixes' => ['Mr.', 'Mrs.', 'Ms.', 'Dr.'],
+ '_give_last_name_field_required' => 'required',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFields::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/donor-name');
+ $this->assertTrue($block->getAttribute('showHonorific'));
+ $this->assertEquals($options['title_prefixes'], $block->getAttribute('honorifics'));
+ $this->assertTrue($block->getAttribute('requireLastName'));
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testDonorCommentsFormFieldProcess()
+ {
+ // Arrange
+ $meta = [
+ '_give_donor_comment' => 'enabled',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFields::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/donor-comments');
+ $this->assertNotNull($block);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testAnonymousDonationsFormFieldProcess()
+ {
+ // Arrange
+ $meta = [
+ '_give_anonymous_donation' => 'enabled',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormFields::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/anonymous');
+ $this->assertNotNull($block);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFormGrid.php b/tests/Feature/FormMigration/Steps/TestFormGrid.php
new file mode 100644
index 0000000000..f662f65ffb
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFormGrid.php
@@ -0,0 +1,45 @@
+ 'custom',
+ '_give_form_grid_redirect_url' => 'https://example.com',
+ '_give_form_grid_donate_button_text' => 'Donate Now',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormGrid::class);
+
+ // Assert
+ $this->assertTrue($v3Form->settings->formGridCustomize);
+ $this->assertEquals('https://example.com', $v3Form->settings->formGridRedirectUrl);
+ $this->assertEquals('Donate Now', $v3Form->settings->formGridDonateButtonText);
+ $this->assertTrue($v3Form->settings->formGridHideDocumentationLink);
+
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFormMeta.php b/tests/Feature/FormMigration/Steps/TestFormMeta.php
new file mode 100644
index 0000000000..619fe363bd
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFormMeta.php
@@ -0,0 +1,43 @@
+ true,
+ '_string_legacy_meta' => 'string',
+ '_array_legacy_meta' => ['key' => 'value'],
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormMeta::class);
+
+ // Assert
+ $this->assertTrue((bool) give_get_meta($v3Form->id, '_boolean_legacy_meta', true));
+ $this->assertSame('string', give_get_meta($v3Form->id, '_string_legacy_meta', true));
+ $this->assertSame(['key' => 'value'], give_get_meta($v3Form->id, '_array_legacy_meta', true));
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestFormTitle.php b/tests/Feature/FormMigration/Steps/TestFormTitle.php
new file mode 100644
index 0000000000..7eae07a3fe
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestFormTitle.php
@@ -0,0 +1,40 @@
+ 'Form Title',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['form' => $form]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, FormTitle::class);
+
+ // Assert
+ $this->assertSame($form['post_title'], $v3Form->title);
+ $this->assertSame($form['post_title'], $v3Form->settings->formTitle);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestMailchimp.php b/tests/Feature/FormMigration/Steps/TestMailchimp.php
index 7c9e9be78c..02bcfd4b0a 100644
--- a/tests/Feature/FormMigration/Steps/TestMailchimp.php
+++ b/tests/Feature/FormMigration/Steps/TestMailchimp.php
@@ -4,106 +4,115 @@
namespace Give\Tests\Feature\FormMigration\Steps;
-use Give\FormMigration\DataTransferObjects\FormMigrationPayload;
use Give\FormMigration\Steps\Mailchimp;
use Give\Tests\TestCase;
use Give\Tests\TestTraits\RefreshDatabase;
use Give\Tests\Unit\DonationForms\TestTraits\LegacyDonationFormAdapter;
+use Give\Tests\Unit\FormMigration\TestTraits\FormMigrationProcessor;
class TestMailchimp extends TestCase
{
- use RefreshDatabase, LegacyDonationFormAdapter;
+ use FormMigrationProcessor;
+ use LegacyDonationFormAdapter;
+ use RefreshDatabase;
/**
- * @since 3.7.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateMailchimpBlockAttributesFromV2FormMeta(): void
+ public function testMailchimpSettingsAreMigratedWhenGloballyEnabledAndNotDisabledForSpecificFormUsingGlobalSettings(): void
{
- $meta = [
- '_give_mailchimp_custom_label' => __('Subscribe to newsletter?'),
- '_give_mailchimp_tags' => ['Animal-Rescue-Campaign', 'Housing-And-Shelter-Campaign'],
- '_give_mailchimp' => ['de73f3f82f'],
- '_give_mailchimp_checked_default' => true,
- '_give_mailchimp_send_donation_data' => true,
- '_give_mailchimp_send_ffm' => true,
+ // Arrange
+ $options = [
+ 'give_mailchimp_show_checkout_signup' => 'on',
+ 'give_mailchimp_label' => __('Subscribe to newsletter?'),
+ 'give_mailchimp_list' => ['de73f3f82f'],
+ 'give_mailchimp_checked_default' => true,
+ 'give_mailchimp_double_opt_in' => true,
+ 'give_mailchimp_donation_data' => true,
+ 'give_mailchimp_ffm_pass_field' => true,
];
+ foreach ($options as $key => $value) {
+ give_update_option($key, $value);
+ }
+ $v2Form = $this->createSimpleDonationForm();
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
-
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $mailchimp = new Mailchimp($payload);
-
- $mailchimp->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/mailchimp');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, Mailchimp::class);
- $this->assertSame($meta['_give_mailchimp_custom_label'], $block->getAttribute('label'));
- $this->assertSame($meta['_give_mailchimp_tags'], $block->getAttribute('subscriberTags'));
- $this->assertSame($meta['_give_mailchimp'], $block->getAttribute('defaultAudiences'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/mailchimp');
+ $this->assertSame($options['give_mailchimp_label'], $block->getAttribute('label'));
+ $this->assertSame($options['give_mailchimp_list'], $block->getAttribute('defaultAudiences'));
+ $this->assertNull(null, $block->getAttribute('subscriberTags'));
$this->assertTrue(true, $block->getAttribute('checked'));
+ $this->assertTrue(true, $block->getAttribute('doubleOptIn'));
$this->assertTrue(true, $block->getAttribute('sendDonationData'));
$this->assertTrue(true, $block->getAttribute('sendFFMData'));
}
/**
- * @since 3.7.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateMailchimpBlockAttributesFromGlobalSettings(): void
+ public function testMailchimpSettingsAreMigratedWhenGloballyEnabledAndNotDisabledForSpecificFormUsingFormSettings(): void
{
+ // Arrange
+ give_update_option('give_mailchimp_show_checkout_signup', 'on');
$meta = [
- 'give_mailchimp_label' => __('Subscribe to newsletter?'),
- 'give_mailchimp_list' => ['de73f3f82f'],
- 'give_mailchimp_checked_default' => true,
- 'give_mailchimp_double_opt_in' => true,
- 'give_mailchimp_donation_data' => true,
- 'give_mailchimp_ffm_pass_field' => true,
+ '_give_mailchimp_custom_label' => __('Subscribe to newsletter?'),
+ '_give_mailchimp_tags' => ['Animal-Rescue-Campaign', 'Housing-And-Shelter-Campaign'],
+ '_give_mailchimp' => ['de73f3f82f'],
+ '_give_mailchimp_checked_default' => true,
+ '_give_mailchimp_send_donation_data' => true,
+ '_give_mailchimp_send_ffm' => true,
];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- foreach ($meta as $key => $value) {
- give_update_option($key, $value);
- }
-
- $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
+ // Act
+ $v3Form = $this->migrateForm($v2Form, Mailchimp::class);
- $payload = FormMigrationPayload::fromFormV2($formV2);
-
- $mailchimp = new Mailchimp($payload);
-
- $mailchimp->process();
-
- $block = $payload->formV3->blocks->findByName('givewp/mailchimp');
-
- $this->assertSame($meta['give_mailchimp_label'], $block->getAttribute('label'));
- $this->assertSame($meta['give_mailchimp_list'], $block->getAttribute('defaultAudiences'));
- $this->assertNull(null, $block->getAttribute('subscriberTags'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/mailchimp');
+ $this->assertSame($meta['_give_mailchimp_custom_label'], $block->getAttribute('label'));
+ $this->assertSame($meta['_give_mailchimp_tags'], $block->getAttribute('subscriberTags'));
+ $this->assertSame($meta['_give_mailchimp'], $block->getAttribute('defaultAudiences'));
$this->assertTrue(true, $block->getAttribute('checked'));
- $this->assertTrue(true, $block->getAttribute('doubleOptIn'));
$this->assertTrue(true, $block->getAttribute('sendDonationData'));
$this->assertTrue(true, $block->getAttribute('sendFFMData'));
}
/**
- * @since 3.7.0
+ * @since 3.16.0
*/
- public function testProcessShouldUpdateMailchimpBlockAttributesWhenNoMeta(): void
+ public function testMailchimpSettingsAreNotMigratedWhenNotGloballyEnabledOrEnabledPerForm()
{
- $formV2 = $this->createSimpleDonationForm();
+ // Arrange
+ give_update_option('give_mailchimp_show_checkout_signup', 'off');
+ $meta = ['_give_mailchimp_enable' => 'false'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $payload = FormMigrationPayload::fromFormV2($formV2);
+ // Act
+ $v3Form = $this->migrateForm($v2Form, Mailchimp::class);
- $mailchimp = new Mailchimp($payload);
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/mailchimp');
+ $this->assertNull($block);
+ }
- $mailchimp->process();
+ /**
+ * @since 3.16.0
+ */
+ public function testMailchimpSettingsAreNotMigratedWhenGloballyEnabledButDisabledForSpecificForm()
+ {
+ // Arrange
+ give_update_option('give_mailchimp_show_checkout_signup', 'off');
+ $meta = ['_give_mailchimp_disable' => 'false'];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
- $block = $payload->formV3->blocks->findByName('givewp/mailchimp');
+ // Act
+ $v3Form = $this->migrateForm($v2Form, Mailchimp::class);
- $this->assertSame(__('Subscribe to newsletter?'), $block->getAttribute('label'));
- $this->assertNull(null, $block->getAttribute('subscriberTags'));
- $this->assertSame([''], $block->getAttribute('defaultAudiences'));
- $this->assertTrue(true, $block->getAttribute('checked'));
- $this->assertTrue(true, $block->getAttribute('doubleOptIn'));
- $this->assertTrue(true, $block->getAttribute('sendDonationData'));
- $this->assertTrue(true, $block->getAttribute('sendFFMData'));
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/mailchimp');
+ $this->assertNull($block);
}
}
diff --git a/tests/Feature/FormMigration/Steps/TestMigrateMeta.php b/tests/Feature/FormMigration/Steps/TestMigrateMeta.php
new file mode 100644
index 0000000000..2237593e0c
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestMigrateMeta.php
@@ -0,0 +1,36 @@
+createSimpleDonationForm();
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, MigrateMeta::class);
+
+ // Assert
+ $this->assertSame($v2Form->id, (int) give_get_meta($v3Form->id, 'migratedFormId', true));
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestOfflineDonations.php b/tests/Feature/FormMigration/Steps/TestOfflineDonations.php
new file mode 100644
index 0000000000..85b707cbda
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestOfflineDonations.php
@@ -0,0 +1,61 @@
+ 'custom',
+ '_give_offline_donation_enable_billing_fields_single' => 'enabled',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, OfflineDonations::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/billing-address');
+ $this->assertNotNull($block);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testOfflineDonationsProcessMigratesNotes(): void
+ {
+ // Arrange
+ $instructions = 'Please send a check to 123 Main St.';
+ $meta = [
+ '_give_customize_offline_donations' => 'custom',
+ '_give_offline_checkout_notes' => $instructions,
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, OfflineDonations::class);
+
+ // Assert
+ $this->assertEquals($instructions, $v3Form->settings->offlineDonationInstructions);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestPaymentGateways.php b/tests/Feature/FormMigration/Steps/TestPaymentGateways.php
new file mode 100644
index 0000000000..22f820ce03
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestPaymentGateways.php
@@ -0,0 +1,47 @@
+ 'enabled',
+ '_give_stripe_default_account' => 'acct_1',
+ '_give_customize_offline_donations' => 'enabled',
+ '_give_offline_checkout_notes' => 'Offline checkout notes',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, PaymentGateways::class);
+
+ // Assert
+ $block = $v3Form->blocks->findByName('givewp/payment-gateways');
+ $this->assertFalse($block->getAttribute('stripeUseGlobalDefault'));
+ $this->assertSame('acct_1', $block->getAttribute('accountId'));
+ $this->assertTrue($block->getAttribute('offlineEnabled'));
+ $this->assertFalse($block->getAttribute('offlineUseGlobalInstructions'));
+ $this->assertSame('Offline checkout notes', $block->getAttribute('offlineDonationInstructions'));
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestPdfSettings.php b/tests/Feature/FormMigration/Steps/TestPdfSettings.php
new file mode 100644
index 0000000000..fbca731b03
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestPdfSettings.php
@@ -0,0 +1,73 @@
+ 'enabled',
+ 'give_pdf_generation_method' => 'auto',
+ 'give_pdf_colorpicker' => '#FF5733',
+ 'give_pdf_templates' => 'custom_template',
+ 'give_pdf_logo_upload' => 'logo.png',
+ 'give_pdf_company_name' => 'My Company',
+ 'give_pdf_address_line1' => '123 Main St',
+ 'give_pdf_address_line2' => 'Apt 4B',
+ 'give_pdf_address_city_state_zip' => 'New York, NY 10001',
+ 'give_pdf_url' => 'https://example.com',
+ 'give_pdf_email_address' => 'info@example.com',
+ 'give_pdf_header_message' => 'Thank you for your donation!',
+ 'give_pdf_footer_message' => 'Footer message here.',
+ 'give_pdf_additional_notes' => 'Additional notes...',
+ 'give_pdf_receipt_template' => 'custom_template',
+ 'give_pdf_receipt_template_name' => 'Custom Template Name',
+ 'give_pdf_builder_page_size' => 'A4',
+ 'give_pdf_builder' => 'custom_builder',
+ ];
+ $v2Form = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ // Act
+ $v3Form = $this->migrateForm($v2Form, PdfSettings::class);
+
+ // Assert
+ $this->assertEquals($meta['give_pdf_receipts_enable_disable'], $v3Form->settings->pdfSettings['enable']);
+ $this->assertEquals($meta['give_pdf_generation_method'], $v3Form->settings->pdfSettings['generationMethod']);
+ $this->assertEquals($meta['give_pdf_colorpicker'], $v3Form->settings->pdfSettings['colorPicker']);
+ $this->assertEquals($meta['give_pdf_templates'], $v3Form->settings->pdfSettings['templateId']);
+ $this->assertEquals($meta['give_pdf_logo_upload'], $v3Form->settings->pdfSettings['logoUpload']);
+ $this->assertEquals($meta['give_pdf_company_name'], $v3Form->settings->pdfSettings['name']);
+ $this->assertEquals($meta['give_pdf_address_line1'], $v3Form->settings->pdfSettings['addressLine1']);
+ $this->assertEquals($meta['give_pdf_address_line2'], $v3Form->settings->pdfSettings['addressLine2']);
+ $this->assertEquals($meta['give_pdf_address_city_state_zip'], $v3Form->settings->pdfSettings['cityStateZip']);
+ $this->assertEquals($meta['give_pdf_url'], $v3Form->settings->pdfSettings['displayWebsiteUrl']);
+ $this->assertEquals($meta['give_pdf_email_address'], $v3Form->settings->pdfSettings['emailAddress']);
+ $this->assertEquals($meta['give_pdf_header_message'], $v3Form->settings->pdfSettings['headerMessage']);
+ $this->assertEquals($meta['give_pdf_footer_message'], $v3Form->settings->pdfSettings['footerMessage']);
+ $this->assertEquals($meta['give_pdf_additional_notes'], $v3Form->settings->pdfSettings['additionalNotes']);
+ $this->assertEquals($meta['give_pdf_receipt_template'], $v3Form->settings->pdfSettings['customTemplateId']);
+ $this->assertEquals($meta['give_pdf_receipt_template_name'], $v3Form->settings->pdfSettings['customTemplateName']);
+ $this->assertEquals($meta['give_pdf_builder_page_size'], $v3Form->settings->pdfSettings['customPageSize']);
+ $this->assertEquals($meta['give_pdf_builder'], $v3Form->settings->pdfSettings['customPdfBuilder']);
+ }
+}
diff --git a/tests/Feature/FormMigration/Steps/TestRazorpayPerFormSettings.php b/tests/Feature/FormMigration/Steps/TestRazorpayPerFormSettings.php
new file mode 100644
index 0000000000..97e2934e41
--- /dev/null
+++ b/tests/Feature/FormMigration/Steps/TestRazorpayPerFormSettings.php
@@ -0,0 +1,82 @@
+ 'global',
+ ];
+ $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
+ $payload = FormMigrationPayload::fromFormV2($formV2);
+ $razorpayPerFormSettings = new RazorpayPerFormSettings($payload);
+
+ $this->assertNotTrue($razorpayPerFormSettings->canHandle());
+ }
+
+ /**
+ * @since 3.14.0
+ */
+ public function testCanHandleShouldReturnTrue()
+ {
+ $meta = [
+ 'razorpay_per_form_account_options' => 'enabled',
+ ];
+ $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
+ $payload = FormMigrationPayload::fromFormV2($formV2);
+ $razorpayPerFormSettings = new RazorpayPerFormSettings($payload);
+
+ $this->assertTrue($razorpayPerFormSettings->canHandle());
+ }
+
+ /**
+ * @since 3.14.0
+ */
+ public function testProcessShouldUpdatePaymentGatewaysBlockAttributes(): void
+ {
+ $liveKeyId = 'live_12304567890';
+ $liveSecretKey = 'live_0123456789';
+ $testKeyId = 'test_12304567890';
+ $testSecretKey = 'test_0123456789';
+
+ $meta = [
+ 'razorpay_per_form_account_options' => 'enabled',
+ 'razorpay_per_form_live_merchant_key_id' => $liveKeyId,
+ 'razorpay_per_form_live_merchant_secret_key' => $liveSecretKey,
+ 'razorpay_per_form_test_merchant_key_id' => $testKeyId,
+ 'razorpay_per_form_test_merchant_secret_key' => $testSecretKey,
+ ];
+
+ $formV2 = $this->createSimpleDonationForm(['meta' => $meta]);
+
+ $payload = FormMigrationPayload::fromFormV2($formV2);
+
+ $razorpayPerFormSettings = new RazorpayPerFormSettings($payload);
+ $razorpayPerFormSettings->process();
+
+ $paymentGatewaysBlock = $payload->formV3->blocks->findByName('givewp/payment-gateways');
+
+ $this->assertSame(false, $paymentGatewaysBlock->getAttribute('razorpayUseGlobalSettings'));
+ $this->assertSame($liveKeyId, $paymentGatewaysBlock->getAttribute('razorpayLiveKeyId'));
+ $this->assertSame($liveSecretKey, $paymentGatewaysBlock->getAttribute('razorpayLiveSecretKey'));
+ $this->assertSame($testKeyId, $paymentGatewaysBlock->getAttribute('razorpayTestKeyId'));
+ $this->assertSame($testSecretKey, $paymentGatewaysBlock->getAttribute('razorpayTestSecretKey'));
+ }
+}
+
diff --git a/tests/Feature/FormMigration/TestFormMetaDecorator.php b/tests/Feature/FormMigration/TestFormMetaDecorator.php
index 8fb2ccc352..4bd5c340e2 100644
--- a/tests/Feature/FormMigration/TestFormMetaDecorator.php
+++ b/tests/Feature/FormMigration/TestFormMetaDecorator.php
@@ -333,7 +333,7 @@ public function testIsDoubleTheDonationLabelSetShouldReturnTrue(): void
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testGetCurrencySwitcherStatus(): void
{
@@ -356,7 +356,7 @@ public function testGetCurrencySwitcherStatus(): void
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testGetCurrencySwitcherMessage(): void
{
@@ -385,7 +385,7 @@ public function testGetCurrencySwitcherMessage(): void
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testGetCurrencySwitcherDefaultCurrency(): void
{
@@ -408,7 +408,7 @@ public function testGetCurrencySwitcherDefaultCurrency(): void
}
/**
- * @unreleased
+ * @since 3.13.0
*/
public function testGetCurrencySwitcherSupportedCurrencies(): void
{
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));
+ }
+}
diff --git a/tests/Unit/DataTransferObjects/DonateFormDataTest.php b/tests/Unit/DataTransferObjects/DonateFormDataTest.php
index 6e325cef5a..f8616e0b52 100644
--- a/tests/Unit/DataTransferObjects/DonateFormDataTest.php
+++ b/tests/Unit/DataTransferObjects/DonateFormDataTest.php
@@ -26,6 +26,7 @@ class DonateFormDataTest extends TestCase
use RefreshDatabase;
/**
+ * @since 3.17.0 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 c7466ef474..8e8133328b 100644
--- a/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php
+++ b/tests/Unit/DataTransferObjects/DonateFormRouteDataTest.php
@@ -2,9 +2,13 @@
namespace TestsNextGen\Unit\DataTransferObjects;
+use Exception;
use Give\DonationForms\DataTransferObjects\DonateControllerData;
use Give\DonationForms\DataTransferObjects\DonateFormRouteData;
+use Give\DonationForms\Exceptions\DonationFormFieldErrorsException;
+use Give\DonationForms\Exceptions\DonationFormForbidden;
use Give\DonationForms\Models\DonationForm;
+use Give\DonationForms\ValueObjects\DonationFormStatus;
use Give\Donations\ValueObjects\DonationType;
use Give\Framework\Blocks\BlockCollection;
use Give\Framework\Blocks\BlockModel;
@@ -19,6 +23,7 @@ class DonateFormRouteDataTest extends TestCase
{
/**
+ * @since 3.17.0 updated to ignore honeypot field
* @since 3.0.0
*/
public function testValidatedShouldReturnValidatedData()
@@ -34,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' => ''],
@@ -92,6 +99,7 @@ public function testValidatedShouldReturnValidatedData()
}
/**
+ * @since 3.17.0 updated to ignore honeypot field
* @since 3.0.0
*/
public function testValidatedShouldReturnValidatedDataWithSubscriptionData()
@@ -107,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' => ''],
@@ -226,4 +236,154 @@ public function testValidatedShouldReturnValidatedDataWithSubscriptionData()
$this->assertEquals($validData, $data);
}
+
+ /**
+ * @since 3.14.0
+ * @dataProvider donationFormStatusProvider
+ * @throws Exception
+ */
+ public function testValidatedShouldThrowExceptionDonationFormForbidden(DonationFormStatus $formStatus): void
+ {
+ $this->expectException(DonationFormForbidden::class);
+
+ /** @var DonationForm $form */
+ $form = DonationForm::factory()->create(['status' => $formStatus]);
+
+ add_filter('give_get_option_gateways', static function ($gateways) {
+ return array_merge($gateways, [TestGateway::id() => true]);
+ });
+
+ add_filter('give_default_gateway', static function () {
+ return TestGateway::id();
+ });
+
+ $form->save();
+
+ $form = DonationForm::find($form->id);
+
+ $data = new DonateControllerData();
+
+ $data->gatewayId = TestGateway::id();
+ $data->amount = 100;
+ $data->currency = "USD";
+ $data->firstName = "Bill";
+ $data->lastName = "Murray";
+ $data->email = "billmurray@givewp.com";
+ $data->formId = $form->id;
+ $data->formTitle = $form->title;
+ $data->company = null;
+ $data->wpUserId = 0;
+ $data->honorific = null;
+ $data->donationType = DonationType::SINGLE();
+ $data->subscriptionFrequency = null;
+ $data->subscriptionPeriod = null;
+ $data->subscriptionInstallments = null;
+ $data->country = null;
+ $data->address1 = null;
+ $data->address2 = null;
+ $data->city = null;
+ $data->state = null;
+ $data->zip = null;
+
+ $request = array_merge(get_object_vars($data), [
+ 'donationType' => $data->donationType->getValue(),
+ ]);
+
+ $formData = DonateFormRouteData::fromRequest($request);
+
+ $formData->validated();
+ }
+
+ /**
+ * @since 3.14.0
+ * @throws Exception
+ */
+ public function testValidatedShouldThrowExceptionDonationFormFieldErrorsException(): void
+ {
+
+ $this->expectException(DonationFormFieldErrorsException::class);
+
+ /** @var DonationForm $form */
+ $form = DonationForm::factory()->create();
+
+ add_filter('give_get_option_gateways', static function ($gateways) {
+ return array_merge($gateways, [TestGateway::id() => true]);
+ });
+
+ add_filter('give_default_gateway', static function () {
+ return TestGateway::id();
+ });
+
+ $customFieldBlockModel = BlockModel::make([
+ 'name' => 'givewp/section',
+ 'attributes' => ['title' => '', 'description' => ''],
+ 'innerBlocks' => [
+ [
+ 'name' => 'givewp/text',
+ 'attributes' => [
+ 'fieldName' => 'text_block_meta',
+ 'title' => 'Custom Text Field',
+ 'description' => '',
+ 'isRequired' => true
+ ],
+ ]
+ ]
+ ]);
+
+ $form->blocks = BlockCollection::make(
+ array_merge([$customFieldBlockModel], $form->blocks->getBlocks())
+ );
+
+ $form->save();
+
+ $data = new DonateControllerData();
+
+ $data->gatewayId = TestGateway::id();
+ $data->amount = 100;
+ $data->currency = "USD";
+ $data->firstName = "Bill";
+ $data->lastName = "Murray";
+ $data->email = "billmurray@givewp.com";
+ $data->formId = $form->id;
+ $data->formTitle = $form->title;
+ $data->company = null;
+ $data->wpUserId = 0;
+ $data->honorific = null;
+ $data->text_block_meta = null;
+ $data->donationType = DonationType::SINGLE();
+ $data->subscriptionFrequency = null;
+ $data->subscriptionPeriod = null;
+ $data->subscriptionInstallments = null;
+ $data->country = null;
+ $data->address1 = null;
+ $data->address2 = null;
+ $data->city = null;
+ $data->state = null;
+ $data->zip = null;
+
+ $request = array_merge(get_object_vars($data), [
+ 'text_block_meta' => null,
+ 'donationType' => $data->donationType->getValue(),
+ ]);
+
+ $formData = DonateFormRouteData::fromRequest($request);
+
+ $formData->validated();
+ }
+
+ /**
+ * @since 3.14.0
+ */
+ public function donationFormStatusProvider(): array
+ {
+ $notAllowed = [];
+ $allowed = [DonationFormStatus::PUBLISHED()->getValue()];
+ foreach (DonationFormStatus::values() as $status) {
+ if (!in_array($status->getValue(), $allowed, true)) {
+ $notAllowed[] = [$status];
+ }
+ }
+
+ return $notAllowed;
+ }
}
diff --git a/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php
new file mode 100644
index 0000000000..6deb2fcc46
--- /dev/null
+++ b/tests/Unit/DonationForms/Actions/TestAddHoneyPotFieldToDonationForms.php
@@ -0,0 +1,47 @@
+append(Section::make('section-1'), Section::make('section-2'), Section::make('section-3'));
+ $action = new AddHoneyPotFieldToDonationForms();
+ $action($formNode, $fieldName);
+
+ /** @var Section $lastSection */
+ $lastSection = $formNode->getNodeByName('section-3');
+
+ /** @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/Actions/TestReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe.php b/tests/Unit/DonationForms/Actions/TestReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe.php
new file mode 100644
index 0000000000..78ce13d217
--- /dev/null
+++ b/tests/Unit/DonationForms/Actions/TestReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe.php
@@ -0,0 +1,59 @@
+assertEquals($view, $result);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testShouldNotReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframeIfInvalidReceiptId(): void
+ {
+ $view = 'originalView';
+ $_GET['receipt_id'] = 1234;
+
+ $result = (new ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe())($view);
+
+ $this->assertEquals($view, $result);
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testShouldReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe(): void
+ {
+ /** @var Donation $donation */
+ $donation = Donation::factory()->create();
+ $view = 'originalView';
+ $receiptId = $donation->purchaseKey;
+ $_GET['receipt-id'] = $receiptId;
+
+ $result = (new ReplaceGiveReceiptShortcodeViewWithDonationConfirmationIframe())($view);
+ $replacedViewUrl = (new GenerateDonationConfirmationReceiptViewRouteUrl())($receiptId);
+
+ $this->assertEquals("", $result);
+ }
+}
diff --git a/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php b/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php
index 143f7e6443..79ea8175cf 100644
--- a/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php
+++ b/tests/Unit/DonationForms/Repositories/TestDonationFormRepository.php
@@ -252,6 +252,7 @@ public function testIsLegacyFormShouldReturnFalseIfNotLegacy()
/**
+ * @since 3.17.0 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);
diff --git a/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php b/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php
new file mode 100644
index 0000000000..c4db7135f4
--- /dev/null
+++ b/tests/Unit/DonationForms/Rules/TestHoneyPotRule.php
@@ -0,0 +1,54 @@
+expectException(SpamDonationException::class);
+ }
+
+ $rule = new HoneyPotRule();
+
+ self::assertValidationRulePassed($rule, $value);
+ }
+
+ /**
+ * @since 3.16.2
+ *
+ * @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..c1cf3e3f72
--- /dev/null
+++ b/tests/Unit/DonationForms/TestTraits/HasValidationRules.php
@@ -0,0 +1,54 @@
+ 'true'];
+ protected $notSpamResponse = [1 => 'false'];
+
+ /**
+ * @since 3.15.0
+ */
+ public function testValidatesNotSpamDonation()
+ {
+ $data = new DonateControllerData();
+
+ /** @var API|PHPUnit_Framework_MockObject_MockObject */
+ $akismet = $this->mockAkismetAPI();
+ $akismet->method('commentCheck')->willReturn($this->notSpamResponse);
+
+ $action = new ValidateDonation(
+ $akismet,
+ new EmailAddressWhiteList()
+ );
+
+ $action->__invoke($data);
+
+ $this->assertTrue(true); // Assert no exception thrown.
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ public function testThrowsSpamDonationException()
+ {
+ $data = new DonateControllerData();
+
+ /** @var API|PHPUnit_Framework_MockObject_MockObject */
+ $akismet = $this->mockAkismetAPI();
+ $akismet->method('commentCheck')->willReturn($this->spamResponse);
+
+ $action = new ValidateDonation(
+ $akismet,
+ new EmailAddressWhiteList()
+ );
+
+ $this->expectException(SpamDonationException::class);
+
+ $action->__invoke($data);
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ protected function mockAkismetAPI()
+ {
+ return $this->createMock(API::class, function(PHPUnit_Framework_MockObject_MockBuilder $mockBuilder) {
+ $mockBuilder->setMethods(['commentCheck']);
+ return $mockBuilder->getMock();
+ });
+ }
+}
diff --git a/tests/Unit/DonationSpam/EmailAddressWhiteListTest.php b/tests/Unit/DonationSpam/EmailAddressWhiteListTest.php
new file mode 100644
index 0000000000..cc22374f10
--- /dev/null
+++ b/tests/Unit/DonationSpam/EmailAddressWhiteListTest.php
@@ -0,0 +1,30 @@
+assertTrue($validator->validate('admin@wordpress.test'));
+ }
+
+ /**
+ * @since 3.15.0
+ */
+ public function testDoesNotValidateNonWhitelistedEmailAddress()
+ {
+ $validator = new EmailAddressWhiteList(['admin@wordpress.test']);
+ $this->assertFalse($validator->validate('subscriber@wordpress.test'));
+ }
+}
diff --git a/tests/Unit/DonationSpam/ServiceProviderTest.php b/tests/Unit/DonationSpam/ServiceProviderTest.php
new file mode 100644
index 0000000000..4b832bd536
--- /dev/null
+++ b/tests/Unit/DonationSpam/ServiceProviderTest.php
@@ -0,0 +1,25 @@
+validate('name@email.test');
+
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/Unit/DonorDashboards/Repositories/TestProfile.php b/tests/Unit/DonorDashboards/Repositories/TestProfile.php
new file mode 100644
index 0000000000..e0388276a4
--- /dev/null
+++ b/tests/Unit/DonorDashboards/Repositories/TestProfile.php
@@ -0,0 +1,104 @@
+user->create_and_get();
+
+ /** @var Donor $donor */
+ $donor = Donor::factory()->create([
+ 'userId' => $user->ID,
+ ]);
+
+ wp_set_current_user($donor->userId);
+
+ $attachment = self::factory()->attachment->create_and_get([
+ 'post_author' => $donor->userId,
+ 'post_title' => 'test',
+ 'post_content' => 'test',
+ 'post_status' => 'inherit',
+ 'post_mime_type' => 'image/jpeg',
+ ]);
+
+ give()->donor_meta->update_meta($donor->id, '_give_donor_avatar_id', $attachment->ID);
+
+ $profileRepository = new Profile();
+
+ $this->assertTrue($profileRepository->avatarBelongsToCurrentUser());
+ }
+
+ /**
+ * @since 3.14.2
+ */
+ public function testAvatarBelongsToCurrentUserShouldReturnTrueWithAvatarParam(): void
+ {
+ $user = self::factory()->user->create_and_get();
+
+ /** @var Donor $donor */
+ $donor = Donor::factory()->create([
+ 'userId' => $user->ID,
+ ]);
+
+ wp_set_current_user($donor->userId);
+
+ $attachment = self::factory()->attachment->create_and_get([
+ 'post_author' => $donor->userId,
+ 'post_title' => 'test',
+ 'post_content' => 'test',
+ 'post_status' => 'inherit',
+ 'post_mime_type' => 'image/jpeg',
+ ]);
+
+ give()->donor_meta->update_meta($donor->id, '_give_donor_avatar_id', $attachment->ID);
+
+ $profileRepository = new Profile();
+
+ $this->assertTrue($profileRepository->avatarBelongsToCurrentUser($attachment->ID));
+ }
+
+ /**
+ * @since 3.14.2
+ */
+ public function testAvatarBelongsToCurrentUserShouldReturnFalse(): void
+ {
+ $user = self::factory()->user->create_and_get();
+
+ /** @var Donor $donor */
+ $donor = Donor::factory()->create([
+ 'userId' => $user->ID,
+ ]);
+
+ wp_set_current_user($donor->userId);
+
+ $attachment = self::factory()->attachment->create_and_get([
+ 'post_author' => $donor->userId + 1, // Different user
+ 'post_title' => 'test',
+ 'post_content' => 'test',
+ 'post_status' => 'inherit',
+ 'post_mime_type' => 'image/jpeg',
+ ]);
+
+ give()->donor_meta->update_meta($donor->id, '_give_donor_avatar_id', $attachment->ID);
+
+
+ $profileRepository = new Profile();
+
+ $this->assertFalse($profileRepository->avatarBelongsToCurrentUser());
+ }
+}
diff --git a/tests/Unit/FormBuilder/ServiceProviderTest.php b/tests/Unit/FormBuilder/ServiceProviderTest.php
new file mode 100644
index 0000000000..cb295dfb17
--- /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)
+ );
+ }
+}
diff --git a/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php b/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php
new file mode 100644
index 0000000000..7679288852
--- /dev/null
+++ b/tests/Unit/FormMigration/Steps/FormTaxonomiesTest.php
@@ -0,0 +1,151 @@
+createSimpleDonationForm();
+ $donationFormV3 = DonationForm::factory()->create();
+
+ give_update_option('tags', 'enabled');
+ give_setup_taxonomies();
+
+ $tag = wp_create_term('aye', 'give_forms_tag');
+ wp_set_post_terms($donationFormV2->id, [$tag['term_id']], 'give_forms_tag');
+
+ $step = new FormTaxonomies(
+ new FormMigrationPayload($donationFormV2, $donationFormV3)
+ );
+ $step->process();
+
+ $this->assertContains(
+ $tag['term_id'],
+ wp_list_pluck(get_the_terms($donationFormV3->id, 'give_forms_tag'), 'term_id')
+ );
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testDoesNotMigrateFormTagsWhenTagsDisabled()
+ {
+ $donationFormV2 = $this->createSimpleDonationForm();
+ $donationFormV3 = DonationForm::factory()->create();
+
+ give_update_option('tags', 'enabled');
+ give_setup_taxonomies();
+
+ $tag = wp_create_term('aye', 'give_forms_tag');
+ wp_set_post_terms($donationFormV2->id, [$tag['term_id']], 'give_forms_tag');
+
+ give_update_option('tags', 'disabled');
+ unregister_taxonomy('give_forms_tag');
+
+ $step = new FormTaxonomies(
+ new FormMigrationPayload($donationFormV2, $donationFormV3)
+ );
+ $step->process();
+
+ $this->assertNotContains(
+ $tag['term_id'],
+ wp_list_pluck(get_the_terms($donationFormV3->id, 'give_forms_tag'), 'term_id')
+ );
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testMigratesFormCategories()
+ {
+ $donationFormV2 = $this->createSimpleDonationForm();
+ $donationFormV3 = DonationForm::factory()->create();
+
+ give_update_option('categories', 'enabled');
+ give_setup_taxonomies();
+
+ $category = wp_create_term('bee', 'give_forms_category');
+ wp_set_post_terms($donationFormV2->id, [$category['term_id']], 'give_forms_category');
+
+ give_update_option('categories', 'disabled');
+ unregister_taxonomy('give_forms_tag');
+
+ $step = new FormTaxonomies(
+ new FormMigrationPayload($donationFormV2, $donationFormV3)
+ );
+ $step->process();
+
+ $this->assertContains(
+ $category['term_id'],
+ wp_list_pluck(get_the_terms($donationFormV3->id, 'give_forms_category'), 'term_id')
+ );
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testDoesNotMigrateFormCategoriesWhenCategoriesDisabled()
+ {
+ $donationFormV2 = $this->createSimpleDonationForm();
+ $donationFormV3 = DonationForm::factory()->create();
+
+ give_update_option('categories', 'enabled');
+ give_setup_taxonomies();
+
+ $category = wp_create_term('bee', 'give_forms_category');
+ wp_set_post_terms($donationFormV2->id, [$category['term_id']], 'give_forms_category');
+
+ $step = new FormTaxonomies(
+ new FormMigrationPayload($donationFormV2, $donationFormV3)
+ );
+ $step->process();
+
+ $this->assertContains(
+ $category['term_id'],
+ 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);
+ }
+}
diff --git a/tests/Unit/FormMigration/TestTraits/FormMigrationProcessor.php b/tests/Unit/FormMigration/TestTraits/FormMigrationProcessor.php
new file mode 100644
index 0000000000..68c8e6fb3b
--- /dev/null
+++ b/tests/Unit/FormMigration/TestTraits/FormMigrationProcessor.php
@@ -0,0 +1,29 @@
+formV3->save();
+
+ return $payload->formV3;
+ }
+}
diff --git a/tests/Unit/FormTaxonomies/Actions/UpdateFormTaxonomiesTest.php b/tests/Unit/FormTaxonomies/Actions/UpdateFormTaxonomiesTest.php
new file mode 100644
index 0000000000..6df022fe49
--- /dev/null
+++ b/tests/Unit/FormTaxonomies/Actions/UpdateFormTaxonomiesTest.php
@@ -0,0 +1,62 @@
+create();
+ $request = new \WP_REST_Request();
+
+ $tag = wp_create_term('aye', 'give_forms_tag');
+ $request->set_param('settings', json_encode([
+ 'formTags' => [['id' => $tag['term_id']]],
+ ]));
+
+ (new UpdateFormTaxonomies)($form, $request);
+
+ $terms = wp_get_post_terms($form->id, 'give_forms_tag');
+ $this->assertEquals([$tag['term_id']], wp_list_pluck($terms, 'term_id'));
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testUpdatesFormCategory()
+ {
+ give_update_option('categories', 'enabled');
+ give_setup_taxonomies();
+
+ $form = DonationForm::factory()->create();
+ $request = new \WP_REST_Request();
+
+ $category = wp_create_term('aye', 'give_forms_category');
+ $request->set_param('settings', json_encode([
+ 'formCategories' => [$category['term_id']],
+ ]));
+
+ (new UpdateFormTaxonomies)($form, $request);
+
+ $terms = wp_get_post_terms($form->id, 'give_forms_category');
+ $this->assertEquals([$category['term_id']], wp_list_pluck($terms, 'term_id'));
+ }
+}
diff --git a/tests/Unit/FormTaxonomies/ViewModels/FormTaxonomyViewModelTest.php b/tests/Unit/FormTaxonomies/ViewModels/FormTaxonomyViewModelTest.php
new file mode 100644
index 0000000000..4805e83a30
--- /dev/null
+++ b/tests/Unit/FormTaxonomies/ViewModels/FormTaxonomyViewModelTest.php
@@ -0,0 +1,83 @@
+createSimpleDonationForm();
+
+ give_update_option('tags', 'enabled');
+ give_setup_taxonomies();
+
+ $viewModel = new FormTaxonomyViewModel($form->id, give_get_settings());
+
+ $this->assertTrue($viewModel->isFormTagsEnabled());
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testIsFormCategoriesEnabled()
+ {
+ $form = $this->createSimpleDonationForm();
+
+ give_update_option('categories', 'enabled');
+ give_setup_taxonomies();
+
+ $viewModel = new FormTaxonomyViewModel($form->id, give_get_settings());
+
+ $this->assertTrue($viewModel->isFormCategoriesEnabled());
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testGetSelectedFormTags()
+ {
+ $form = $this->createSimpleDonationForm();
+
+ give_update_option('tags', 'enabled');
+ give_setup_taxonomies();
+
+ $tag = wp_create_term('aye', 'give_forms_tag');
+ wp_set_post_terms($form->id, [$tag['term_id']], 'give_forms_tag');
+
+ $viewModel = new FormTaxonomyViewModel($form->id, give_get_settings());
+
+ $this->assertEquals([['id' => $tag['term_id'], 'value' => 'aye']], $viewModel->getSelectedFormTags());
+ }
+
+ /**
+ * @since 3.16.0
+ */
+ public function testGetSelectedFormCategories()
+ {
+ $form = $this->createSimpleDonationForm();
+
+ give_update_option('categories', 'enabled');
+ give_setup_taxonomies();
+
+ $category = wp_create_term('aye', 'give_forms_category');
+ wp_set_post_terms($form->id, [$category['term_id']], 'give_forms_category');
+
+ $viewModel = new FormTaxonomyViewModel($form->id, give_get_settings());
+
+ $this->assertEquals([$category['term_id']], $viewModel->getSelectedFormCategories());
+ }
+}
diff --git a/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php b/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php
index b0de74412a..3b353e2234 100644
--- a/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php
+++ b/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php
@@ -44,4 +44,52 @@ public function testShouldCreateRenewalDonation()
$this->assertEquals($subscription->id, $renewalDonation->subscriptionId);
}
+
+ /**
+ * @since 3.16.0
+ *
+ * @throws Exception
+ */
+ public function testShouldNotCreateRenewalDonationWithFirstGatewayTransactionId()
+ {
+ $subscription = Subscription::factory()->createWithDonation();
+ $donation = $subscription->initialDonation();
+
+ $firstGatewayTransactionId = 'first-gateway-transaction-id';
+
+ $donation->status = DonationStatus::COMPLETE();
+ $donation->gatewayTransactionId = $firstGatewayTransactionId;
+ $donation->save();
+
+ give(SubscriptionRenewalDonationCreated::class)($subscription->gatewaySubscriptionId,
+ $firstGatewayTransactionId);
+
+ $totalDonations = give()->donations->getTotalDonationCountByGatewayTransactionId($firstGatewayTransactionId);
+
+ $this->assertEquals(1, $totalDonations);
+ }
+
+ /**
+ * @since 3.16.0
+ *
+ * @throws Exception
+ */
+ public function testShouldNotCreateRenewalDonationWithDuplicatedGatewayTransactionId()
+ {
+ $subscription = Subscription::factory()->createWithDonation();
+
+ $duplicatedGatewayTransactionId = 'duplicated-gateway-transaction-id';
+
+ // #1 Renewal Donation
+ give(SubscriptionRenewalDonationCreated::class)($subscription->gatewaySubscriptionId,
+ $duplicatedGatewayTransactionId);
+
+ // #2 Renewal Donation - This one should not be created
+ give(SubscriptionRenewalDonationCreated::class)($subscription->gatewaySubscriptionId,
+ $duplicatedGatewayTransactionId);
+
+ $totalDonations = give()->donations->getTotalDonationCountByGatewayTransactionId($duplicatedGatewayTransactionId);
+
+ $this->assertEquals(1, $totalDonations);
+ }
}
diff --git a/tests/Unit/Helpers/UtilsTest.php b/tests/Unit/Helpers/UtilsTest.php
new file mode 100644
index 0000000000..d0fbe9cc54
--- /dev/null
+++ b/tests/Unit/Helpers/UtilsTest.php
@@ -0,0 +1,104 @@
+assertTrue(strpos($stringWithoutBackslashes, '\\') === false);
+
+ $stringWithoutBackslashes = Utils::removeBackslashes('\\\\ double-backslash-bypass.');
+ $this->assertTrue(strpos($stringWithoutBackslashes, '\\') === false);
+
+ $stringWithoutBackslashes = Utils::removeBackslashes('\\\\\\\\\\\\ multiple-backslash-bypass.');
+ $this->assertTrue(strpos($stringWithoutBackslashes, '\\') === false);
+ }
+
+ /**
+ * @since 3.17.2
+ */
+ public function testContainsSerializedDataRegex()
+ {
+ $stringWithSerializedDataRegex = 'Lorem ipsum dolor sit amet, {a:2:{i:0;s:5:\"hello\";i:1;s:5:\"world\";}} consectetur adipiscing elit.';
+ $this->assertTrue(Utils::containsSerializedDataRegex($stringWithSerializedDataRegex));
+
+ $stringWithoutSerializedDataRegex = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
+ $this->assertNotTrue(Utils::containsSerializedDataRegex($stringWithoutSerializedDataRegex));
+ }
+
+ /**
+ * @since 3.17.2
+ *
+ * @dataProvider serializedDataProvider
+ */
+ public function testIsSerialized($data, bool $expected)
+ {
+ if ($expected) {
+ $this->assertTrue(Utils::isSerialized($data));
+ } else {
+ $this->assertFalse(Utils::isSerialized($data));
+ }
+ }
+
+ /**
+ * @since 3.17.2
+ *
+ * @dataProvider serializedDataProvider
+ */
+ public function testSafeUnserialize($data, bool $expected)
+ {
+ $unserializedData = Utils::safeUnserialize($data);
+ if ($expected) {
+ $this->assertNotEquals($unserializedData, $data);
+ } else {
+ $this->assertEquals($unserializedData, $data);
+ }
+ }
+
+ /**
+ * @since 3.17.2
+ *
+ * @dataProvider serializedDataProvider
+ */
+ public function testMaybeSafeUnserialize($data, bool $expected)
+ {
+ $unserializedData = Utils::maybeSafeUnserialize($data);
+ if ($expected) {
+ $this->assertNotEquals($unserializedData, $data);
+ } else {
+ $this->assertEquals($unserializedData, $data);
+ }
+ }
+
+ /**
+ * @since 3.17.2
+ */
+ public function serializedDataProvider(): array
+ {
+ return [
+ [serialize('bar'), true],
+ ['\\' . serialize('backslash-bypass'), true],
+ ['\\\\' . serialize('double-backslash-bypass'), true],
+ [
+ // String with serialized data hidden in the middle of the content
+ 'Lorem ipsum dolor sit amet, {a:2:{i:0;s:5:\"hello\";i:1;s:5:\"world\";}} consectetur adipiscing elit.',
+ true,
+ ],
+ ['foo', false],
+ [serialize('qux'), true],
+ ['bar', false],
+ ['foo bar', false],
+ ];
+ }
+}
diff --git a/tests/Unit/PaymentGateways/Stripe/StripePaymentElementGateway/Actions/UpdateStripeFormBuilderSettingsMetaTest.php b/tests/Unit/PaymentGateways/Stripe/StripePaymentElementGateway/Actions/UpdateStripeFormBuilderSettingsMetaTest.php
index ec8e623bc3..1e1beed535 100644
--- a/tests/Unit/PaymentGateways/Stripe/StripePaymentElementGateway/Actions/UpdateStripeFormBuilderSettingsMetaTest.php
+++ b/tests/Unit/PaymentGateways/Stripe/StripePaymentElementGateway/Actions/UpdateStripeFormBuilderSettingsMetaTest.php
@@ -4,7 +4,7 @@
use Closure;
use Exception;
-use Give\Vendors\Faker\Factory;
+use Faker\Factory;
use Give\DonationForms\Models\DonationForm;
use Give\PaymentGateways\Gateways\Stripe\StripePaymentElementGateway\Actions\UpdateStripeFormBuilderSettingsMeta;
use Give\PaymentGateways\Gateways\Stripe\StripePaymentElementGateway\StripePaymentElementGateway;
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(),
],
diff --git a/tests/includes/legacy/tests-functions.php b/tests/includes/legacy/tests-functions.php
index c6162a8cf2..bcdd8960d1 100644
--- a/tests/includes/legacy/tests-functions.php
+++ b/tests/includes/legacy/tests-functions.php
@@ -86,4 +86,42 @@ 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 );
}
+
+ /**
+ * @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
+ {
+ if ($expected) {
+ $this->assertTrue(give_donation_form_has_serialized_fields($fields));
+ } else {
+ $this->assertFalse(give_donation_form_has_serialized_fields($fields));
+ }
+ }
+
+ /**
+ * @since 3.17.2 Add string with serialized data hidden in the middle of the content
+ * @since 3.16.4
+ */
+ 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',
+ // String with serialized data hidden in the middle of the content
+ 'baz' => 'Lorem ipsum dolor sit amet, {a:2:{i:0;s:5:\"hello\";i:1;s:5:\"world\";}} consectetur adipiscing elit.',
+ ],
+ true,
+ ],
+ [['foo' => 'bar'], false],
+ [['foo' => 'bar', 'baz' => serialize('qux')], true],
+ [['foo' => 'bar', 'baz' => 'qux'], false],
+ [['foo' => 'bar', 'baz' => 1], false],
+ ];
+ }
}
diff --git a/tests/includes/legacy/tests-give.php b/tests/includes/legacy/tests-give.php
index c270de5229..ffe4f40cbb 100644
--- a/tests/includes/legacy/tests-give.php
+++ b/tests/includes/legacy/tests-give.php
@@ -32,7 +32,7 @@ public function test_constants() {
// Plugin Root File
$path = str_replace( 'tests/unit-tests/', '', plugin_dir_path( $filePath ) );
- $this->assertSame( GIVE_PLUGIN_FILE, $path . 'give.php' );
+ $this->assertSame(GIVE_PLUGIN_FILE, untrailingslashit($path) . DIRECTORY_SEPARATOR . 'give.php');
}
/**
diff --git a/tests/includes/legacy/tests-post-types.php b/tests/includes/legacy/tests-post-types.php
index d584ab0e6f..8f4e660613 100644
--- a/tests/includes/legacy/tests-post-types.php
+++ b/tests/includes/legacy/tests-post-types.php
@@ -29,8 +29,8 @@ public function test_give_post_type_labels() {
$this->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 );
diff --git a/webpack.mix.js b/webpack.mix.js
index b2eaf51d5e..8c7be78756 100644
--- a/webpack.mix.js
+++ b/webpack.mix.js
@@ -18,7 +18,11 @@ mix.setPublicPath('assets/dist')
.sass('src/Views/Form/Templates/Classic/resources/css/form.scss', 'css/give-classic-template.css')
.sass('src/MultiFormGoals/resources/css/common.scss', 'css/multi-form-goal-block.css')
.sass('src/DonationSummary/resources/css/summary.scss', 'css/give-donation-summary.css')
- .sass('src/Promotions/InPluginUpsells/resources/css/stellarwp-sales-banner.scss', 'css/admin-stellarwp-sales-banner.css')
+ .sass(
+ 'src/Promotions/InPluginUpsells/resources/css/stellarwp-sales-banner.scss',
+ 'css/admin-stellarwp-sales-banner.css'
+ )
+ .sass('src/DonationForms/AsyncData/resources/loadAsyncData.scss', 'css/give-donation-forms-load-async-data.css')
.js('assets/src/js/frontend/give.js', 'js/')
.js('assets/src/js/frontend/give-stripe.js', 'js/')
@@ -42,16 +46,15 @@ mix.setPublicPath('assets/dist')
.js('src/MigrationLog/Admin/index.js', 'js/give-migrations-list-table-app.js')
.js('src/DonationSummary/resources/js/summary.js', 'js/give-donation-summary.js')
.js('src/Promotions/InPluginUpsells/resources/js/addons-admin-page.js', 'js/admin-upsell-addons-page.js')
+ .js('src/DonationForms/AsyncData/resources/loadAsyncData.js', 'js/give-donation-forms-load-async-data.js')
.ts('src/DonationForms/V2/resources/admin-donation-forms.tsx', 'js/give-admin-donation-forms.js')
.ts('src/DonationForms/V2/resources/edit-v2form.tsx', 'js/give-edit-v2form.js')
.ts('src/DonationForms/V2/resources/add-v2form.tsx', 'js/give-add-v2form.js')
- .ts('src/Donors/resources/admin-donors.tsx', 'js/give-admin-donors.js').
- ts('src/Donations/resources/index.tsx', 'js/give-admin-donations.js').
- ts('src/EventTickets/resources/admin/events-list-table.tsx',
- 'js/give-admin-event-tickets.js').
- ts('src/EventTickets/resources/admin/event-details.tsx',
- 'js/give-admin-event-tickets-details.js')
+ .ts('src/Donors/resources/admin-donors.tsx', 'js/give-admin-donors.js')
+ .ts('src/Donations/resources/index.tsx', 'js/give-admin-donations.js')
+ .ts('src/EventTickets/resources/admin/events-list-table.tsx', 'js/give-admin-event-tickets.js')
+ .ts('src/EventTickets/resources/admin/event-details.tsx', 'js/give-admin-event-tickets-details.js')
.ts('src/Subscriptions/resources/admin-subscriptions.tsx', 'js/give-admin-subscriptions.js')
.js('src/Promotions/InPluginUpsells/resources/js/sale-banner.js', 'js/admin-upsell-sale-banner.js')
.ts('src/Promotions/InPluginUpsells/resources/js/donation-options.ts', 'js/donation-options.js')
diff --git a/wordpress-scripts-webpack.config.js b/wordpress-scripts-webpack.config.js
index 0e633be4a6..bc837daf69 100644
--- a/wordpress-scripts-webpack.config.js
+++ b/wordpress-scripts-webpack.config.js
@@ -58,6 +58,7 @@ module.exports = {
baseFormDesignCss: srcPath('DonationForms/resources/styles/base.scss'),
formBuilderApp: srcPath('FormBuilder/resources/js/form-builder/src/index.tsx'),
formBuilderRegistrars: srcPath('FormBuilder/resources/js/registrars/index.ts'),
+ formTaxonomySettings: srcPath('FormTaxonomies/resources/form-builder/index.tsx'),
adminBlocks: path.resolve(process.cwd(), 'blocks', 'load.js'),
},
};