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 ( -
-

{ __( 'What else do you need to support your cause?', 'give' ) }

-

- { __( 'Take your fundraising to the next level with these premium add-ons.', 'give' ) } -

- dispatch( setAddons( value ) ) } > - - - { __( 'Recurring Donations', 'give' ) } - - - - { __( 'Donors Cover Fees', 'give' ) } - - - - { __( 'PDF Receipts', 'give' ) } - - - - { __( 'Custom Form Fields', 'give' ) } - - - - { __( 'Multiple Currencies', 'give' ) } - - - - { __( 'Dedicate Donations', 'give' ) } - - - -
- ); + return ( +
+

{__('What else do you need to support your cause?', 'give')}

+

{__('Take your fundraising to the next level with these premium add-ons.', 'give')}

+ dispatch(setAddons(value))}> + + +

{__('Recurring Donations', 'give')}

+

{__('Allow donors to make donations on a recurring basis.', 'give')}

+
+ + +

{__('Fee Recovery', 'give')}

+

{__('Enable donors to cover payment processing fees.', 'give')}

+
+ + +

{__('PDF Receipts', 'give')}

+

{__('Provide custom donation receipts in PDF format.', 'give')}

+
+ + +

{__('Custom Form Fields', 'give')}

+

{__('Add custom fields to your donation forms.', 'give')}

+
+ + +

{__('Currency Switcher', 'give')}

+

{__('Accept donations in your preferred currencies.', 'give')}

+
+ + +

{__('Tributes', 'give')}

+

{__('Allow donors to dedicate their donation to someone special.', 'give')}

+
+
+
+ + +
+
+ ); }; export default Addons; diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/addons/style.scss b/assets/src/js/admin/onboarding-wizard/app/steps/addons/style.scss index d669249e74..fa4426f9c9 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/addons/style.scss +++ b/assets/src/js/admin/onboarding-wizard/app/steps/addons/style.scss @@ -1,48 +1,61 @@ .give-obw-fundraising-needs { - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; - > h1 { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 600; - font-size: 24px; - line-height: 34px; - text-align: center; - color: #333; - margin: 0 0 8px 0; - } + > h1 { + font-weight: 600; + font-size: 24px; + line-height: 34px; + text-align: center; + color: #333; + margin: 0 0 8px; + } - > p { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 16px; - line-height: 22px; - text-align: center; - color: #333; - margin: 0 0 52px 0; - } + > p { + font-weight: 500; + font-size: 16px; + line-height: 22px; + text-align: center; + color: #333; + margin: 0; + } - .give-obw-card { - box-sizing: border-box; - height: 218px; - width: 242px; - align-items: center; - justify-content: center; + .give-obw-card { + align-items: center; + box-sizing: border-box; + width: 17.5rem; + min-height: 15rem; + position: relative; + padding-top: 7.25rem; - > strong { - font-family: Montserrat, Arial, Helvetica, sans-serif; - text-transform: uppercase; - font-weight: 500; - font-size: 18px; - line-height: 24px; - text-align: center; - letter-spacing: 3.5px; - color: #4a5568; - } - } + > svg { + position: absolute; + top: 2.25rem; + left: 50%; + translate: -50%; + } - > .give-obw-button { - margin-top: 52px; - } + > h2 { + color: #4a5568; + display: flex; + align-items: flex-end; + height: 2.66em; + font-size: 18px; + font-weight: 600; + line-height: 1.33; + margin: 0 0 0.25rem; + text-align: center; + text-transform: uppercase; + } + + > p { + color: #4a5568; + font-size: 14px; + font-weight: 400; + line-height: 1.57; + margin: 0; + text-align: center; + } + } } diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/index.js b/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/index.js index 98aa6bd358..f27df0f7ef 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/index.js +++ b/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/index.js @@ -1,51 +1,81 @@ // Import vendor dependencies -import { __ } from '@wordpress/i18n' +import {__} from '@wordpress/i18n'; + +// Import store dependencies +import {useStoreValue} from '../../store'; +import {setNewsletterSubscription} from '../../store/actions'; +import {subscribeToNewsletter} from '../../../utils'; // Import components import ContinueButton from '../../../components/continue-button'; +import PreviousButton from '../../../components/previous-button'; import DonationFormComponent from '../../../components/donation-form'; -import GradientChevronIcon from '../../../components/icons/gradient-chevron'; +import CheckboxInput from '../../../components/checkbox-input'; +import Bullet from '../../../components/icons/bullet'; // Import styles import './style.scss'; const DonationForm = () => { - return ( -
-
- -
-
-
-

{ __( 'Check out your first donation form!', 'give' ) }

-

- { __( 'This form is customized based on your responses.', 'give' ) } -

+ const [{configuration}, dispatch] = useStoreValue(); + const newsletterSubscription = configuration.newsletterSubscription; + + return ( +
+
+ +
+
+
+

{__('🎉 Congrats! Check out your first donation form.', 'give')}

+

{__('This form is customized based on your responses.', 'give')}

-

{ __( 'After setup you can:', 'give' ) }

-
    -
  • - - { __( 'Customize the text, color and image', 'give' ) } -
  • -
  • - - { __( 'Modify donation amounts and add a fundraising goal', 'give' ) } -
  • -
  • - - { __( 'Add or remove payment options', 'give' ) } -
  • -
  • - - { __( 'Extend with add-ons and more!', 'give' ) } -
  • -
- -
-
-
- ); +

{__('After setup you can:', 'give')}

+
    +
  • + + {__('Customize the text, color and image', 'give')} +
  • +
  • + + {__('Modify donation amounts and add a fundraising goal', 'give')} +
  • +
  • + + {__('Add or remove payment options', 'give')} +
  • +
  • + + {__('Extend functionality with add-ons and more', 'give')} +
  • +
+
+ dispatch(setNewsletterSubscription(e.target.checked))} + /> +
+
+ { + if (newsletterSubscription) { + subscribeToNewsletter(configuration); + } + }} + /> + +
+
+
+
+ ); }; export default DonationForm; diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/style.scss b/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/style.scss index 40d905f14c..d5d21d1adf 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/style.scss +++ b/assets/src/js/admin/onboarding-wizard/app/steps/donation-form/style.scss @@ -1,72 +1,87 @@ .give-obw-donation-form { - display: grid; - position: relative; - grid-template-columns: repeat(2, 1fr); - width: 1200px; - grid-gap: 62px; - align-items: center; + display: grid; + position: relative; + grid-template-columns: 578px 1fr; + width: 1256px; + grid-gap: 80px; + align-items: center; } .give-obw-donation-form__content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - align-self: flex-start; - height: calc(100vh - 190px); + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + align-self: flex-start; + height: calc(100vh - 190px); - .give-obw-donation-form__fixed { - position: fixed; - } + .give-obw-donation-form__fixed { + position: fixed; + max-width: 616px; + } - h1 { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 600; - font-size: 24px; - line-height: 34px; - color: #333; - margin: 0 0 8px 0; - } + h1 { + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 600; + font-size: 24px; + line-height: 34px; + color: #333; + margin: 0 0 8px; + } - p { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 16px; - line-height: 22px; - color: #333; - margin: 0 0 38px; - } + p { + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 500; + font-size: 16px; + line-height: 22px; + color: #333; + margin: 0 0 24px; + } - h2 { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 600; - font-size: 16px; - line-height: 22px; - color: #333; - margin: 0 0 4px 0; - } + h2 { + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 22px; + color: #333; + margin: 0 0 4px 0; + } - ul { - padding: 0; - margin: 12px 0 36px 0; - list-style: none; + ul { + padding: 0; + margin: 12px 0 36px 0; + list-style: none; - > li { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 16px; - line-height: 2; - color: #333; + > li { + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 500; + font-size: 16px; + line-height: 1.5; + color: #333; - padding: 0; - margin: 2px 0; + padding: 0; + margin: 8px 0 0; - display: flex; - align-items: center; + display: flex; + align-items: center; - svg { - margin: 0 8px 0 4px; - } - } - } + svg { + margin-right: 8px; + } + } + } + + .give-obw-newsletter-subscription-field { + margin-bottom: 2rem; + max-width: 590px; + + .give-obw-checkbox-input { + margin-left: 0; + margin-right: 0; + + &__help { + margin-bottom: 0; + } + } + } } diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/features/index.js b/assets/src/js/admin/onboarding-wizard/app/steps/features/index.js index 1dd6ee077d..1cb80b5f4c 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/features/index.js +++ b/assets/src/js/admin/onboarding-wizard/app/steps/features/index.js @@ -1,63 +1,69 @@ // Import vendor dependencies -import { __ } from '@wordpress/i18n' +import {__} from '@wordpress/i18n'; // Import store dependencies -import { useStoreValue } from '../../store'; -import { setFeatures } from '../../store/actions'; +import {useStoreValue} from '../../store'; +import {setFeatures} 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 OfflineDonationsIcon from '../../../components/icons/offline-donations'; import DonationGoalIcon from '../../../components/icons/donation-goal'; import DonationCommentsIcon from '../../../components/icons/donation-comments'; import TermsConditionsIcon from '../../../components/icons/terms-conditions'; import AnonymousDonationsIcon from '../../../components/icons/anonymous-donations'; -import CompanyDonationsIcon from '../../../components/icons/company-donations'; - -// Import styles +import CompanyDonationsIcon from '../../../components/icons/company-donations'; // Import styles import './style.scss'; const Features = () => { - const [ { configuration }, dispatch ] = useStoreValue(); - const features = configuration.features; + const [{configuration}, dispatch] = useStoreValue(); + const features = configuration.features; - return ( -
-

{ __( 'What do you need in your first donation form?', 'give' ) }

-

- { __( 'Don\'t worry, these settings can always be changed later.', 'give' ) } -

- dispatch( setFeatures( value ) ) } > - - - { __( 'Donation Goal', 'give' ) } - - - - { __( 'Donation Comments', 'give' ) } - - - - { __( 'Terms & Conditions', 'give' ) } - - - - { __( 'Offline Donations', 'give' ) } - - - - { __( 'Anonymous Donations', 'give' ) } - - - - { __( 'Company Donations', 'give' ) } - - - -
- ); + return ( +
+

{__('What do you need in your first donation form?', 'give')}

+

{__('Select the features you need. These can always be changed later.', 'give')}

+ dispatch(setFeatures(value))}> + + +

{__('Donation Goal', 'give')}

+

{__('Show the donation goal progress on the form.', 'give')}

+
+ + +

{__('Donation Comments', 'give')}

+

{__('Allow donors to add comments to their donations.', 'give')}

+
+ + +

{__('Terms & Conditions', 'give')}

+

{__('Require donors to accept terms and conditions.', 'give')}

+
+ + +

{__('Offline Donations', 'give')}

+

{__('Donors can choose to donate offline, via mail or in person.', 'give')}

+
+ + +

{__('Anonymous Donations', 'give')}

+

{__('Enable donors to give anonymously.', 'give')}

+
+ + +

{__('Company Donations', 'give')}

+

{__('Donors can donate via their company.', 'give')}

+
+
+
+ + +
+
+ ); }; export default Features; diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/features/style.scss b/assets/src/js/admin/onboarding-wizard/app/steps/features/style.scss index 051f9473c1..cd8a77ca16 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/features/style.scss +++ b/assets/src/js/admin/onboarding-wizard/app/steps/features/style.scss @@ -1,48 +1,52 @@ .give-obw-features { - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; - > h1 { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 600; - font-size: 24px; - line-height: 34px; - text-align: center; - color: #333; - margin: 0 0 8px 0; - } + > h1 { + font-weight: 600; + font-size: 24px; + line-height: 34px; + text-align: center; + color: #333; + margin: 0 0 8px 0; + } - > p { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 16px; - line-height: 22px; - text-align: center; - color: #333; - margin: 0 0 52px 0; - } + > p { + font-weight: 500; + font-size: 16px; + line-height: 22px; + text-align: center; + color: #333; + margin: 0 0 52px 0; + } - .give-obw-card { - box-sizing: border-box; - height: 218px; - width: 242px; - align-items: center; - justify-content: center; + .give-obw-card { + box-sizing: border-box; + width: 17.5rem; + align-items: center; + justify-content: center; - > strong { - font-family: Montserrat, Arial, Helvetica, sans-serif; - text-transform: uppercase; - font-weight: 500; - font-size: 18px; - line-height: 24px; - text-align: center; - letter-spacing: 3.5px; - color: #4a5568; - } - } + > h2 { + color: #4a5568; + display: flex; + align-items: flex-end; + height: 2em; + font-size: 18px; + font-weight: 600; + line-height: 1.33; + margin: 0 0 0.25rem; + text-align: center; + text-transform: uppercase; + } - > .give-obw-button { - margin-top: 52px; - } + > p { + color: #4a5568; + font-size: 14px; + font-weight: 400; + line-height: 1.57; + margin: 0; + text-align: center; + } + } } diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/introduction/index.js b/assets/src/js/admin/onboarding-wizard/app/steps/introduction/index.js index 04f88c7427..7bf99d7220 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/introduction/index.js +++ b/assets/src/js/admin/onboarding-wizard/app/steps/introduction/index.js @@ -1,11 +1,15 @@ // Import vendor dependencies -import { __ } from '@wordpress/i18n' +import {__} from '@wordpress/i18n'; + +// Import store dependencies +import {useStoreValue} from '../../store'; +import {setFormId} from '../../store/actions'; // Import styles import './style.scss'; // Import utilities -import { generateFormPreviewWithOnboardingAPI } from '../../../utils'; +import {generateFormPreviewWithOnboardingAPI} from '../../../utils'; import Card from '../../../components/card'; import GiveLogo from '../../../components/give-logo'; @@ -13,27 +17,38 @@ import ContinueButton from '../../../components/continue-button'; import DismissLink from '../../../components/dismiss-link'; const Introduction = () => { - const onStartSetup = () => { - generateFormPreviewWithOnboardingAPI(); - }; - - return ( -
- -
-

- { __( 'Welcome To', 'give' ) }{ __( 'GiveWP', 'give' ) } -

- -

- { __( 'You\'re only minutes away from accepting donations on your website! Use the Onboarding Wizard if this is your first time using GiveWP.', 'give' ) } -

- onStartSetup() } label={ __( 'Start Setup', 'give' ) } testId="intro-continue-button" /> -
-
- -
- ); + const [{}, dispatch] = useStoreValue(); + + const onStartSetup = async () => { + const formId = await generateFormPreviewWithOnboardingAPI(); + dispatch(setFormId(formId)); + }; + + return ( +
+ +
+

+ {__('Welcome To', 'give')} + {__('GiveWP', 'give')} +

+ +

+ {__( + 'Get started quickly with our Onboarding Wizard and begin accepting donations on your website in minutes.', + 'give' + )} +

+ onStartSetup()} + label={__('Start Setup', 'give')} + testId="intro-continue-button" + /> +
+
+ +
+ ); }; export default Introduction; diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/introduction/style.scss b/assets/src/js/admin/onboarding-wizard/app/steps/introduction/style.scss index a4558d7ad6..28c8e4930b 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/introduction/style.scss +++ b/assets/src/js/admin/onboarding-wizard/app/steps/introduction/style.scss @@ -1,38 +1,38 @@ .give-obw-introduction { - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; } .give-obw-introduction__content { - width: 480px; - padding: 100px 225px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + width: 480px; + padding: 100px 225px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; - > h1 { - text-transform: uppercase; - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 19px; - line-height: 24px; - letter-spacing: 6px; + > h1 { + text-transform: uppercase; + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 500; + font-size: 19px; + line-height: 24px; + letter-spacing: 6px; - color: #1da3b0; + color: #1da3b0; - margin: 0 0 30px 0; - } + margin: 0 0 30px 0; + } - > p { - font-family: 'Open Sans', Arial, Helvetica, sans-serif; - font-size: 16px; - line-height: 1.5; - font-weight: 600; - text-align: center; - color: #424242; + > p { + font-family: 'Open Sans', Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.5; + font-weight: 600; + text-align: center; + color: #424242; - margin: 40px 0 34px 0; - } + margin: 40px 0 34px 0; + } } diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/location/index.js b/assets/src/js/admin/onboarding-wizard/app/steps/location/index.js index 9a3ebcebb7..bb278abacf 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/location/index.js +++ b/assets/src/js/admin/onboarding-wizard/app/steps/location/index.js @@ -1,45 +1,65 @@ // Import vendor dependencies -import { __ } from '@wordpress/i18n' +import {__} from '@wordpress/i18n'; // Import store dependencies -import { useStoreValue } from '../../store'; -import { getLocaleCurrency } from '../../../utils'; -import { setCountry, setState, setCurrency, fetchStateList } from '../../store/actions'; - -// Import components +import {useStoreValue} from '../../store'; +import {getLocaleCurrency} from '../../../utils'; +import {fetchStateList, setCountry, setCurrency, setState} from '../../store/actions'; // Import components import Card from '../../../components/card'; import ContinueButton from '../../../components/continue-button'; +import PreviousButton from '../../../components/previous-button'; import SelectInput from '../../../components/select-input'; -import BackgroundImage from './background'; - -// Import styles +import BackgroundImage from './background'; // Import styles import './style.scss'; const Location = () => { - const [ { configuration, currenciesList, statesList, fetchingStatesList, countriesList }, dispatch ] = useStoreValue(); + const [{configuration, currenciesList, statesList, fetchingStatesList, countriesList}, dispatch] = useStoreValue(); - const country = configuration.country; - const state = configuration.state; - const currency = configuration.currency; + const country = configuration.country; + const state = configuration.state; + const currency = configuration.currency; - const onChangeCountry = ( value ) => { - dispatch( fetchStateList( value, dispatch ) ); - dispatch( setCountry( value ) ); - dispatch( setCurrency( getLocaleCurrency( value ) ) ); - }; + const onChangeCountry = (value) => { + dispatch(fetchStateList(value, dispatch)); + dispatch(setCountry(value)); + dispatch(setCurrency(getLocaleCurrency(value))); + }; - return ( -
- -

{ __( '🌎 Where are you fundraising?', 'give' ) }

- - - dispatch( setState( value ) ) } options={ statesList } isLoading={ fetchingStatesList } /> - dispatch( setCurrency( value ) ) } options={ currenciesList } /> - - -
- ); + return ( +
+ +

{__('🌎 Where are you fundraising?', 'give')}

+

{__('This information will be used to set up your donation form experience.', 'give')}

+ + + dispatch(setState(value))} + options={statesList} + isLoading={fetchingStatesList} + /> + dispatch(setCurrency(value))} + options={currenciesList} + /> + +
+ + +
+
+ ); }; export default Location; diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/location/style.scss b/assets/src/js/admin/onboarding-wizard/app/steps/location/style.scss index 3d90e0cd50..ccf2f106a7 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/location/style.scss +++ b/assets/src/js/admin/onboarding-wizard/app/steps/location/style.scss @@ -1,35 +1,48 @@ .give-obw-location { - position: relative; - display: flex; - flex-direction: column; - align-items: center; + position: relative; + display: flex; + flex-direction: column; + align-items: center; - > svg { - top: 50px; - position: absolute; - } + > svg { + top: 50px; + position: absolute; + } - > h1 { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 600; - font-size: 24px; - line-height: 34px; - color: #333; + > h1 { + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 600; + font-size: 24px; + line-height: 34px; + color: #333; + margin: 0 0 0.75rem; + } - margin: 0 0 52px 0; - } + > p { + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #333; + margin: 0 0 1.5rem + } - .give-obw-card { - padding: 40px; - margin-bottom: 85px; - } + .give-obw-card { + padding: 40px; + margin-bottom: 24px; + box-sizing: border-box; + width: 445px; + } - .give-obw-select-input { - width: 100%; - margin-bottom: 42px; + .give-obw-select-input { + width: 100%; + margin-bottom: 42px; - &:last-of-type { - margin-bottom: 0; - } - } + &:last-of-type { + margin-bottom: 0; + } + } + + .give-obw-footer { + width: 445px; + } } diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/index.js b/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/index.js index 56de6fd54f..6da00ce477 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/index.js +++ b/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/index.js @@ -1,75 +1,87 @@ // Import vendor dependencies -import { __ } from '@wordpress/i18n' +import {__} from '@wordpress/i18n'; // Import store dependencies -import { useStoreValue } from '../../store'; -import { setUserType, setCauseType } from '../../store/actions'; -import {getCauseTypes, saveSettingWithOnboardingAPI, subscribeToNewsletter} from '../../../utils'; +import {useStoreValue} from '../../store'; +import {setCauseType, setUsageTracking, setUserType} from '../../store/actions'; +import {getCauseTypes, saveSettingWithOnboardingAPI} from '../../../utils'; // Import components import CardInput from '../../../components/card-input'; import Card from '../../../components/card'; +import CheckboxInput from '../../../components/checkbox-input'; import SelectInput from '../../../components/select-input'; import ContinueButton from '../../../components/continue-button'; import IndividualIcon from '../../../components/icons/individual'; import OrganizationIcon from '../../../components/icons/organization'; import OtherIcon from '../../../components/icons/other'; -import SkipLink from '../../../components/skip-optin-link'; // Import styles import './style.scss'; const YourCause = () => { - const [{ configuration }, dispatch] = useStoreValue(); + const [{configuration}, dispatch] = useStoreValue(); - const userType = configuration.userType; - const causeType = configuration.causeType; + const userType = configuration.userType; + const causeType = configuration.causeType; + const usageTracking = configuration.usageTracking; - return ( -
-

{__( '👋 Hi there! Tell us a little about your Organization.', 'give' )}

-

{__( 'This information will be used to customize your experience to your fundraising needs.', 'give' )}

- dispatch( setUserType( values ) )} checkMultiple={false}> - - -

{__( 'I\'m fundraising as an', 'give' )}

- {__( 'Individual', 'give' )} -
- - -

{__( 'I\'m fundraising within an', 'give' )}

- {__( 'Organization', 'give' )} -
- - -

{__( 'My fundraising is', 'give' )}

- {__( 'Other', 'give' )} -
-
+ return ( +
+

{__('👋 Hi there! Tell us about your cause.', 'give')}

+

{__('This information will be used to customize your experience to your fundraising needs.', 'give')}

+ dispatch(setUserType(values))} checkMultiple={false}> + + +

{__("I'm fundraising as an", 'give')}

+ {__('Individual', 'give')} +
+ + +

{__("I'm fundraising within an", 'give')}

+ {__('Organization', 'give')} +
+ + +

{__('My fundraising is', 'give')}

+ {__('Other', 'give')} +
+
-
-

{__( 'What are you fundraising for?', 'give' )}

- {__( 'What type of cause is yours?', 'give' )} - dispatch( setCauseType( value ) )} options={getCauseTypes()} /> -
+
+

{__('What are you fundraising for?', 'give')}

+ {__('What type of cause is yours?', 'give')} + dispatch(setCauseType(value))} + options={getCauseTypes()} + /> +
-
-

{__( '🌱 Would you like to join the GiveWP Community?', 'give' )}

-

{__( 'By opting-in, you allow some basic data about how you use GiveWP to be used for us to improve the plugin for others. You also will receive emails from us with fundraising tips and more (which you can always unsubscribe from if you need to). If you skip this step, that\'s okay! GiveWP will still be set up for you no problem.', 'give' )}

-
+
+ dispatch(setUsageTracking(e.target.checked))} + /> +
- { - // Opt-in to usage tracking. - saveSettingWithOnboardingAPI('usage_tracking', 'enabled'); - - // Subscribe to ActiveCampaign. - subscribeToNewsletter( configuration ); - }} /> - - - -
- ); +
+ { + saveSettingWithOnboardingAPI('usage_tracking', usageTracking ? 'enabled' : 'disabled'); + }} + /> +
+
+ ); }; export default YourCause; diff --git a/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/style.scss b/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/style.scss index 2b58a476e0..ffc00cba52 100644 --- a/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/style.scss +++ b/assets/src/js/admin/onboarding-wizard/app/steps/your-cause/style.scss @@ -1,107 +1,93 @@ .give-obw-your-cause { - display: flex; - flex-direction: column; - align-items: center; - font-family: Montserrat, Arial, Helvetica, sans-serif; - - h1 { - font-weight: 600; - font-size: 24px; - line-height: 34px; - color: #333; - margin: 0; - } - - h2 { - font-weight: 600; - font-size: 18px; - line-height: 25px; - color: #333; - } - - > p { - font-weight: 400; - font-size: 16px; - line-height: 24px; - color: #333; - } - - .give-obw-text-field { - font-size: 14px; - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - color: #333; - line-height: 1.2; - width: 356px; - box-sizing: border-box; - margin: 10px 0 0; - border: 1px solid #b8b8b8; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - border-radius: 4px; - height: 50px; - padding: 0 18px; - &:focus { - box-shadow: 0 0 0 2px #7ec980, 0 0 0 3px #4fa651; - outline: none; - } - - } - - .give-obw-optin-field { - margin: 0 0 30px; - width: 356px; - - > h2 { - text-align: center; - margin: 0; - } - } - - .give-obw-community-field { - width: 750px; - text-align: center; - - p { - font-weight: 400; - font-size: 16px; - line-height: 24px; - color: #333; - } - } - - p.give-obw-email-notice { - font-size: 13px; - color: #a6a6a6; - font-weight: 400; - } - - - - .give-obw-button { - margin-top: 20px; - } - - .give-obw-card { - width: 250px; - height: 200px; - align-items: center; - - p { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 14px; - line-height: 20px; - color: #333; - } - - strong { - text-transform: uppercase; - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-weight: 500; - font-size: 19px; - line-height: 24px; - letter-spacing: 6px; - color: #4fa651; - } - } + display: flex; + flex-direction: column; + align-items: center; + font-family: Montserrat, Arial, Helvetica, sans-serif; + + h1 { + font-weight: 600; + font-size: 24px; + line-height: 1.42; + color: #333; + margin: 0 0 12px; + } + + h2 { + font-weight: 600; + font-size: 18px; + line-height: 25px; + color: #333; + } + + > p { + font-weight: 400; + font-size: 16px; + line-height: 1.5; + margin: 0; + color: #333; + } + + .give-obw-text-field { + font-size: 14px; + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 500; + color: #333; + line-height: 1.2; + width: 356px; + box-sizing: border-box; + margin: 10px 0 0; + border: 1px solid #b8b8b8; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border-radius: 4px; + height: 50px; + padding: 0 18px; + + &:focus { + box-shadow: 0 0 0 2px #7ec980, 0 0 0 3px #4fa651; + outline: none; + } + + } + + .give-obw-optin-field { + margin: 0 0 30px; + width: 356px; + + > h2 { + text-align: center; + margin: 0; + } + } + + .give-obw-usage-tracking-field { + margin-bottom: 1.5rem; + width: 956px; + } + + .give-obw-card { + box-sizing: border-box; + width: 292px; + height: 242px; + align-items: center; + padding: 39px 30px; + + p { + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 20px; + margin: 14px 0; + color: #333; + } + + strong { + text-transform: uppercase; + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-weight: 500; + font-size: 19px; + line-height: 24px; + letter-spacing: 6px; + color: #4fa651; + } + } } diff --git a/assets/src/js/admin/onboarding-wizard/app/store/actions.js b/assets/src/js/admin/onboarding-wizard/app/store/actions.js index d0d8f929f0..01b5b598e2 100644 --- a/assets/src/js/admin/onboarding-wizard/app/store/actions.js +++ b/assets/src/js/admin/onboarding-wizard/app/store/actions.js @@ -1,110 +1,139 @@ // Dispatch GO_TO_STEP action -export const goToStep = ( step ) => { - return { - type: 'GO_TO_STEP', - payload: { - step, - }, - }; +export const goToStep = (step) => { + return { + type: 'GO_TO_STEP', + payload: { + step, + }, + }; }; // Dispatch SET_USER_TYPE action -export const setUserType = ( type ) => { - return { - type: 'SET_USER_TYPE', - payload: { - type, - }, - }; +export const setUserType = (type) => { + return { + type: 'SET_USER_TYPE', + payload: { + type, + }, + }; }; // Dispatch SET_CAUSE_TYPE action -export const setCauseType = ( type ) => { - return { - type: 'SET_CAUSE_TYPE', - payload: { - type, - }, - }; +export const setCauseType = (type) => { + return { + type: 'SET_CAUSE_TYPE', + payload: { + type, + }, + }; +}; + +// Dispatch SET_USAGE_TRACKING action +export const setUsageTracking = (usageTracking) => { + return { + type: 'SET_USAGE_TRACKING', + payload: { + usageTracking, + }, + }; +}; + +// Dispatch SET_NEWSLETTER_SUBSCRIPTION action +export const setNewsletterSubscription = (newsletterSubscription) => { + return { + type: 'SET_NEWSLETTER_SUBSCRIPTION', + payload: { + newsletterSubscription, + }, + }; }; // Dispatch SET_COUNTRY action -export const setCountry = ( country ) => { - return { - type: 'SET_COUNTRY', - payload: { - country, - }, - }; +export const setCountry = (country) => { + return { + type: 'SET_COUNTRY', + payload: { + country, + }, + }; }; -export const fetchStateList = ( country, dispatch ) => { - return { - type: 'FETCH_STATE_LIST', - payload: { - country, - dispatch, - }, - }; +export const fetchStateList = (country, dispatch) => { + return { + type: 'FETCH_STATE_LIST', + payload: { + country, + dispatch, + }, + }; }; // Dispatch SET_FETCHING_STATES_LIST action -export const setFetchingStatesList = ( status ) => { - return { - type: 'SET_FETCHING_STATES_LIST', - payload: { - status, - }, - }; +export const setFetchingStatesList = (status) => { + return { + type: 'SET_FETCHING_STATES_LIST', + payload: { + status, + }, + }; }; // Dispatch SET_STATE_LIST action -export const setStateList = ( stateList ) => { - return { - type: 'SET_STATE_LIST', - payload: { - stateList, - }, - }; +export const setStateList = (stateList) => { + return { + type: 'SET_STATE_LIST', + payload: { + stateList, + }, + }; }; // Dispatch SET_STATE action -export const setState = ( state ) => { - return { - type: 'SET_STATE', - payload: { - state, - }, - }; +export const setState = (state) => { + return { + type: 'SET_STATE', + payload: { + state, + }, + }; }; // Dispatch SET_CURRENCY action -export const setCurrency = ( currency ) => { - return { - type: 'SET_CURRENCY', - payload: { - currency, - }, - }; +export const setCurrency = (currency) => { + return { + type: 'SET_CURRENCY', + payload: { + currency, + }, + }; }; // Dispatch SET_ADDONS action -export const setAddons = ( addons ) => { - return { - type: 'SET_ADDONS', - payload: { - addons, - }, - }; +export const setAddons = (addons) => { + return { + type: 'SET_ADDONS', + payload: { + addons, + }, + }; }; // Dispatch SET_FEATURES action -export const setFeatures = ( features ) => { - return { - type: 'SET_FEATURES', - payload: { - features, - }, - }; +export const setFeatures = (features) => { + return { + type: 'SET_FEATURES', + payload: { + features, + }, + }; }; +// Dispatch SET_FORM_ID action +export const setFormId = (formId) => { + return { + type: 'SET_FORM_ID', + payload: { + formId, + }, + }; +}; diff --git a/assets/src/js/admin/onboarding-wizard/app/store/reducer.js b/assets/src/js/admin/onboarding-wizard/app/store/reducer.js index f59da0e71d..ac92e6f20d 100644 --- a/assets/src/js/admin/onboarding-wizard/app/store/reducer.js +++ b/assets/src/js/admin/onboarding-wizard/app/store/reducer.js @@ -1,91 +1,119 @@ -import { - saveSettingWithOnboardingAPI, - fetchStatesListWithOnboardingAPI, -} from '../../utils'; +import {fetchStatesListWithOnboardingAPI, saveSettingWithOnboardingAPI} from '../../utils'; -import { observeAction } from './observers'; +import {observeAction} from './observers'; -export const reducer = ( state, action ) => { - observeAction( action ); +export const reducer = (state, action) => { + observeAction(action); - switch ( action.type ) { - case 'GO_TO_STEP': - return { - ...state, - currentStep: action.payload.step, - }; - case 'SET_USER_TYPE': - saveSettingWithOnboardingAPI( 'user_type', action.payload.type ); - return { - ...state, - configuration: { ...state.configuration, - userType: action.payload.type, - }, - }; - case 'SET_CAUSE_TYPE': - saveSettingWithOnboardingAPI( 'cause_type', action.payload.type ); - return { - ...state, - configuration: { ...state.configuration, - causeType: action.payload.type, - }, - }; - case 'SET_COUNTRY': - saveSettingWithOnboardingAPI( 'base_country', action.payload.country ); - return { - ...state, - configuration: { ...state.configuration, - country: action.payload.country, - }, - }; - case 'FETCH_STATE_LIST': - fetchStatesListWithOnboardingAPI( action.payload.country, action.payload.dispatch ); - return { - ...state, - }; - case 'SET_STATE_LIST': - return { - ...state, - statesList: action.payload.stateList, - }; - case 'SET_FETCHING_STATES_LIST': - return { - ...state, - fetchingStatesList: action.payload.status, - }; - case 'SET_STATE': - saveSettingWithOnboardingAPI( 'base_state', action.payload.state ); - return { - ...state, - configuration: { ...state.configuration, - state: action.payload.state, - }, - }; - case 'SET_CURRENCY': - saveSettingWithOnboardingAPI( 'currency', action.payload.currency ); - return { - ...state, - configuration: { ...state.configuration, - currency: action.payload.currency, - }, - }; - case 'SET_ADDONS': - saveSettingWithOnboardingAPI( 'addons', action.payload.addons ); - return { - ...state, - configuration: { ...state.configuration, - addons: action.payload.addons, - }, - }; - case 'SET_FEATURES': - saveSettingWithOnboardingAPI( 'features', action.payload.features ); - return { - ...state, - configuration: { ...state.configuration, - features: action.payload.features, - }, - }; - default: - return state; - } + switch (action.type) { + case 'GO_TO_STEP': + return { + ...state, + currentStep: action.payload.step, + }; + case 'SET_USER_TYPE': + saveSettingWithOnboardingAPI('user_type', action.payload.type); + return { + ...state, + configuration: { + ...state.configuration, + userType: action.payload.type, + }, + }; + case 'SET_CAUSE_TYPE': + saveSettingWithOnboardingAPI('cause_type', action.payload.type); + return { + ...state, + configuration: { + ...state.configuration, + causeType: action.payload.type, + }, + }; + case 'SET_USAGE_TRACKING': + return { + ...state, + configuration: { + ...state.configuration, + usageTracking: action.payload.usageTracking, + }, + }; + case 'SET_NEWSLETTER_SUBSCRIPTION': + return { + ...state, + configuration: { + ...state.configuration, + newsletterSubscription: action.payload.newsletterSubscription, + }, + }; + case 'SET_COUNTRY': + saveSettingWithOnboardingAPI('base_country', action.payload.country); + return { + ...state, + configuration: { + ...state.configuration, + country: action.payload.country, + }, + }; + case 'FETCH_STATE_LIST': + fetchStatesListWithOnboardingAPI(action.payload.country, action.payload.dispatch); + return { + ...state, + }; + case 'SET_STATE_LIST': + return { + ...state, + statesList: action.payload.stateList, + }; + case 'SET_FETCHING_STATES_LIST': + return { + ...state, + fetchingStatesList: action.payload.status, + }; + case 'SET_STATE': + saveSettingWithOnboardingAPI('base_state', action.payload.state); + return { + ...state, + configuration: { + ...state.configuration, + state: action.payload.state, + }, + }; + case 'SET_CURRENCY': + saveSettingWithOnboardingAPI('currency', action.payload.currency); + return { + ...state, + configuration: { + ...state.configuration, + currency: action.payload.currency, + }, + }; + case 'SET_ADDONS': + saveSettingWithOnboardingAPI('addons', action.payload.addons); + return { + ...state, + configuration: { + ...state.configuration, + addons: action.payload.addons, + }, + }; + case 'SET_FEATURES': + saveSettingWithOnboardingAPI('features', action.payload.features); + return { + ...state, + configuration: { + ...state.configuration, + features: action.payload.features, + }, + }; + case 'SET_FORM_ID': + return { + ...state, + configuration: { + ...state.configuration, + formId: action.payload.formId, + }, + }; + default: + return state; + } }; diff --git a/assets/src/js/admin/onboarding-wizard/components/button/index.js b/assets/src/js/admin/onboarding-wizard/components/button/index.js index b4681fdff0..dc6317bf6e 100644 --- a/assets/src/js/admin/onboarding-wizard/components/button/index.js +++ b/assets/src/js/admin/onboarding-wizard/components/button/index.js @@ -1,27 +1,30 @@ // Import vendor dependencies import PropTypes from 'prop-types'; +import cx from 'classnames'; // Import styles import './style.scss'; -const Button = ( { onClick, testId, children } ) => { - return ( - - ); +const Button = ({className, onClick, testId, children}) => { + return ( + + ); }; Button.propTypes = { - onClick: PropTypes.func, - testId: PropTypes.string, - children: PropTypes.node, + className: PropTypes.string, + onClick: PropTypes.func, + testId: PropTypes.string, + children: PropTypes.node, }; Button.defaultProps = { - onClick: null, - testId: null, - children: null, + className: null, + onClick: null, + testId: null, + children: null, }; export default Button; diff --git a/assets/src/js/admin/onboarding-wizard/components/button/style.scss b/assets/src/js/admin/onboarding-wizard/components/button/style.scss index 34e5e8a0e4..cec06a8b8c 100644 --- a/assets/src/js/admin/onboarding-wizard/components/button/style.scss +++ b/assets/src/js/admin/onboarding-wizard/components/button/style.scss @@ -1,31 +1,51 @@ .give-obw-button { - font-family: Montserrat, Arial, Helvetica, sans-serif; - font-style: normal; - font-weight: 500; - font-size: 21px; - line-height: 26px; - - display: flex; - position: relative; - align-items: center; - text-align: center; - letter-spacing: 1px; - text-transform: capitalize; - color: #fff; - - padding: 14px 34px; - background: #4fa651; - box-shadow: 0 2px 10px rgba(105, 184, 107, 0.6); - border-radius: 4px; - border: none; - - &:focus { - box-shadow: 0 0 0 2px #7ec980, 0 0 0 3px #4fa651; - outline: none; - } - - &:hover { - background-color: #4fa651; - filter: brightness(1.2); - } + font-family: Montserrat, Arial, Helvetica, sans-serif; + font-style: normal; + font-weight: 500; + font-size: 21px; + line-height: 26px; + + display: flex; + position: relative; + align-items: center; + text-align: center; + letter-spacing: 1px; + text-transform: capitalize; + color: #fff; + + padding: 14px 34px; + background: #4fa651; + box-shadow: 0 2px 10px rgba(105, 184, 107, 0.6); + border-radius: 4px; + border: none; + + &--reverse { + flex-direction: row-reverse; + } + + &--secondary.give-obw-button { + background: #fff; + box-shadow: none; + color: #4fa651; + outline: 1px solid #4fa651; + + &:focus { + outline: 1px solid #4fa651; + } + + &:hover { + background: #f2fff3; + filter: none; + } + } + + &:focus { + box-shadow: 0 0 0 2px #7ec980, 0 0 0 3px #4fa651; + outline: none; + } + + &:hover { + background-color: #4fa651; + filter: brightness(1.2); + } } diff --git a/assets/src/js/admin/onboarding-wizard/components/card-input/index.js b/assets/src/js/admin/onboarding-wizard/components/card-input/index.js index 6fc8a4c2cb..12ae53c4e3 100644 --- a/assets/src/js/admin/onboarding-wizard/components/card-input/index.js +++ b/assets/src/js/admin/onboarding-wizard/components/card-input/index.js @@ -9,66 +9,62 @@ import './style.scss'; import Card from '../card'; import Selected from './selected'; -const CardInput = ( { checkMultiple, values, onChange, children } ) => { - const handleChange = ( value ) => { - let newValues; - if ( checkMultiple === true ) { - newValues = values.includes( value ) ? values.filter( e => e !== value ) : values.concat( [ value ] ); - } else { - newValues = value; - } - onChange( newValues ); - }; +const CardInput = ({checkMultiple, values, onChange, children}) => { + const handleChange = (value) => { + let newValues; + if (checkMultiple === true) { + newValues = values.includes(value) ? values.filter((e) => e !== value) : values.concat([value]); + } else { + newValues = value; + } + onChange(newValues); + }; - const cards = children.map( ( card, index ) => { - const checked = values.includes( card.props.value ); - return ( -
- handleChange( evt.target.value ) } defaultChecked={ checked } /> -
- { !! checked && - - } + const cards = children.map((card, index) => { + const checked = values.includes(card.props.value); + return ( +
+ handleChange(evt.target.value)} + defaultChecked={checked} + /> +
+ {!!checked && } - -
-
- ); - } ); + +
+
+ ); + }); - return ( -
- { cards } -
- ); + return
{cards}
; }; CardInput.propTypes = { - checkMultiple: PropTypes.bool, - values: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.array - ]), - onChange: PropTypes.func, - children: function( props, propName, componentName ) { - const prop = props[ propName ]; + checkMultiple: PropTypes.bool, + values: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + onChange: PropTypes.func, + children: function (props, propName, componentName) { + const prop = props[propName]; - let error = null; - React.Children.forEach( prop, function( child ) { - if ( child.type !== Card && typeof child.props.value === undefined ) { - error = new Error( '`' + componentName + '` children should be of type `Card` with a `value` prop.' ); - } - } ); - return error; - }, + let error = null; + React.Children.forEach(prop, function (child) { + if (child.type !== Card && typeof child.props.value === undefined) { + error = new Error('`' + componentName + '` children should be of type `Card` with a `value` prop.'); + } + }); + return error; + }, }; CardInput.defaultProps = { - checkMultiple: true, - values: [], - onChange: null, - children: null, + checkMultiple: true, + values: [], + onChange: null, + children: null, }; export default CardInput; - diff --git a/assets/src/js/admin/onboarding-wizard/components/card-input/style.scss b/assets/src/js/admin/onboarding-wizard/components/card-input/style.scss index 1eda0775b3..908df2010f 100644 --- a/assets/src/js/admin/onboarding-wizard/components/card-input/style.scss +++ b/assets/src/js/admin/onboarding-wizard/components/card-input/style.scss @@ -1,48 +1,48 @@ .give-obw-card-input { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-gap: 40px; - border: none; - padding: 0; - margin: 30px 0 50px; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 40px; + border: none; + padding: 0; + margin: 1.5rem 0; } .give-obw-card-input__option { - position: relative; - transition: all 0.2s ease-out; - transform: scale(1) translateY(0); + position: relative; + transition: all 0.2s ease-out; + transform: scale(1) translateY(0); - label { - cursor: pointer; - } + label { + cursor: pointer; + } - .card-input-selected { - position: absolute; - top: 10px; - right: 10px; - z-index: 1; - } + .card-input-selected { + position: absolute; + top: 10px; + right: 10px; + z-index: 1; + } - &:hover { - transform: scale(1.02) translateY(-2px); + &:hover { + transform: scale(1.02) translateY(-2px); - .give-obw-card { - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - } - } + .give-obw-card { + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + } + } } input:active + .give-obw-card-input__option .give-obw-card, input:focus + .give-obw-card-input__option .give-obw-card { - // Using a border as a selected state with border-box box-sizing, - // creates a change in available content space within the box. - // Instead, a box-shadow maintains width and border-radius. - box-shadow: 0 0 0 2px #7ec980, 0 0 0 3px #4fa651, 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - transform: scale(1) translateY(0); + // Using a border as a selected state with border-box box-sizing, + // creates a change in available content space within the box. + // Instead, a box-shadow maintains width and border-radius. + box-shadow: 0 0 0 2px #7ec980, 0 0 0 3px #4fa651, 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + transform: scale(1) translateY(0); } input[type='checkbox'] { - position: absolute; - opacity: 0; + position: absolute; + opacity: 0; } diff --git a/assets/src/js/admin/onboarding-wizard/components/card/index.js b/assets/src/js/admin/onboarding-wizard/components/card/index.js index f501a796c4..0633360b67 100644 --- a/assets/src/js/admin/onboarding-wizard/components/card/index.js +++ b/assets/src/js/admin/onboarding-wizard/components/card/index.js @@ -4,20 +4,16 @@ import PropTypes from 'prop-types'; // Import styles import './style.scss'; -const Card = ( { children } ) => { - return ( -
- { children } -
- ); +const Card = ({children}) => { + return
{children}
; }; Card.propTypes = { - children: PropTypes.node, + children: PropTypes.node, }; Card.defaultProps = { - children: null, + children: null, }; export default Card; diff --git a/assets/src/js/admin/onboarding-wizard/components/card/style.scss b/assets/src/js/admin/onboarding-wizard/components/card/style.scss index 8c96693f44..2e17b3ad8a 100644 --- a/assets/src/js/admin/onboarding-wizard/components/card/style.scss +++ b/assets/src/js/admin/onboarding-wizard/components/card/style.scss @@ -1,18 +1,16 @@ .give-obw-card { - position: relative; - padding: 20px; - background: #fff; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - border: 1px solid #fff; - border-radius: 8px; + position: relative; + padding: 2.25rem 1.25rem 1.5rem; + background: #fff; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border: 1px solid #fff; + border-radius: 8px; + display: flex; + flex-direction: column; + justify-content: flex-end; + transition: all 0.2s ease-out; - display: flex; - flex-direction: column; - justify-content: center; - - transition: all 0.2s ease-out; - - > svg { - margin-bottom: 20px; - } + > svg { + flex: 1 + } } diff --git a/assets/src/js/admin/onboarding-wizard/components/checkbox-input/index.js b/assets/src/js/admin/onboarding-wizard/components/checkbox-input/index.js new file mode 100644 index 0000000000..a2333ee8ac --- /dev/null +++ b/assets/src/js/admin/onboarding-wizard/components/checkbox-input/index.js @@ -0,0 +1,47 @@ +// Import vendor dependencies +import PropTypes from 'prop-types'; + +// Import utilities +import {toKebabCase} from '../../utils'; + +// Import styles +import './style.scss'; + +const CheckboxInput = ({label, help, value, checked, testId, onChange}) => { + return ( +
+ {label && ( + + )} + {help &&

{help}

} + +
+ ); +}; + +CheckboxInput.propTypes = { + label: PropTypes.string, + help: PropTypes.string, + value: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChange: PropTypes.func, +}; + +CheckboxInput.defaultProps = { + label: null, + help: null, + value: null, + checked: false, + onChange: null, +}; + +export default CheckboxInput; diff --git a/assets/src/js/admin/onboarding-wizard/components/checkbox-input/style.scss b/assets/src/js/admin/onboarding-wizard/components/checkbox-input/style.scss new file mode 100644 index 0000000000..3bddfb5390 --- /dev/null +++ b/assets/src/js/admin/onboarding-wizard/components/checkbox-input/style.scss @@ -0,0 +1,49 @@ +/* stylelint-disable function-url-quotes */ + +.give-obw-checkbox-input { + margin: 0.5rem 1rem; + position: relative; + padding-left: 2.5rem; + + > .give-obw-checkbox-input__label { + color: #333; + font-size: 1.125rem; + font-weight: 600; + line-height: 1.56; + } + + > .give-obw-checkbox-input__help { + color: #0e0e0e; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + margin-top: 0.5rem; + } + + > .give-obw-checkbox-input__input { + appearance: none; + background-color: #fff; + border: solid 1px #4fa651; + border-radius: 0.25rem; + color: #4fa651; + cursor: pointer; + font-size: inherit; + height: 1.5rem; + left: 0; + margin: 0; + opacity: 1; + top: 0; + vertical-align: middle; + width: 1.5rem; + + &:checked { + --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2724%27 height=%2724%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27rgb%28255, 255, 255%29%27 stroke-width=%274%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpolyline points=%2720 6 9 17 4 12%27%3E%3C/polyline%3E%3C/svg%3E"); + + background-color: #4fa651; + background-image: var(--icon-checkbox); + background-position: center; + background-repeat: no-repeat; + background-size: 1em auto; + } + } +} diff --git a/assets/src/js/admin/onboarding-wizard/components/donation-form/index.js b/assets/src/js/admin/onboarding-wizard/components/donation-form/index.js index 3b472ec343..f7ae6687a6 100644 --- a/assets/src/js/admin/onboarding-wizard/components/donation-form/index.js +++ b/assets/src/js/admin/onboarding-wizard/components/donation-form/index.js @@ -1,9 +1,10 @@ // Import vendor dependencies -import { useState, useEffect } from 'react'; -import { __ } from '@wordpress/i18n' +import {useEffect, useRef, useState} from 'react'; +import {__} from '@wordpress/i18n'; +import IframeResizer from 'iframe-resizer-react'; // Import utilities -import { getWindowData } from '../../utils'; +import {getWindowData} from '../../utils'; // Import components import ConfigurationIcon from '../icons/configuration'; @@ -11,69 +12,53 @@ import ConfigurationIcon from '../icons/configuration'; // Import styles import './style.scss'; -const DonationForm = () => { - const formPreviewUrl = getWindowData( 'formPreviewUrl' ); - const [ iframeLoaded, setIframeLoaded ] = useState( false ); - const [ iframeHeight, setIframeHeight ] = useState( 749 ); +const DonationForm = ({formId}) => { + const formPreviewUrl = getWindowData('formPreviewUrl') + `${formId}`; + const [isLoading, setLoading] = useState(false); + const [previewHTML, setPreviewHTML] = useState(null); + const iframeRef = useRef(); - useEffect( () => { - window.addEventListener( 'message', receiveMessage, false ); - return () => { - window.removeEventListener( 'message', receiveMessage, false ); - }; - }, [] ); + useEffect(() => { + setLoading(true); + }, []); - const receiveMessage = ( event ) => { - switch ( event.data.action ) { - case 'resize': { - setIframeHeight( event.data.payload.height ); - break; - } - case 'loaded': { - onIframeLoaded(); - break; - } - default: { + return ( +
+ {isLoading && ( +
+ +

{__('Building Form Preview...', 'give')}

+
+ )} + { + iframe.iFrameResizer.resize(); + setLoading(false); + }} + /> - } - } - }; - - const iframeStyle = { - height: iframeHeight, - opacity: iframeLoaded === false ? '0' : '1', - }; - const messageStyle = { - height: iframeHeight, - opacity: iframeLoaded === false ? '1' : '0', - }; - - const onIframeLoaded = () => { - setIframeLoaded( true ); - hideInIframe( '#give_error_test_mode' ); - hideInIframe( '.social-sharing' ); - }; - - const hideInIframe = ( selector ) => { - const element = document.getElementById( 'donationFormPreview' ).contentDocument - .getElementById( 'iFrameResizer0' ).contentDocument - .querySelector( selector ); - if ( element ) { - element.style.display = 'none'; - } - }; - - return ( -
-
- -

- { __( 'Building Form Preview...', 'give' ) } -

-
- "; + } + + 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 ? '%2$s%3$s' : ''), + '%3$s%4$s', + apply_filters('givewp_list_table_goal_progress_achieved_opacity', $goal['progress'] >= 100 ? 1 : 0), GIVE_PLUGIN_URL . 'assets/dist/images/list-table/star-icon.svg', __('Goal achieved icon', 'give'), __('Goal achieved!', 'give') diff --git a/src/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 && ( + + )} + {__('Add Form', 'give')} -
); 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 - +
, - 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( + ", $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'), }, };