From 4881f349605e79c44fe735a15e150ec15ea7f550 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Wed, 3 Aug 2022 12:09:20 +0800 Subject: [PATCH 001/140] Move SetupFreeListings component of editing free listings to the shared directory --- .../free-listings}/setup-free-listings/form-content.js | 0 .../free-listings}/setup-free-listings/index.js | 0 js/src/edit-free-campaign/index.js | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename js/src/{edit-free-campaign => components/free-listings}/setup-free-listings/form-content.js (100%) rename js/src/{edit-free-campaign => components/free-listings}/setup-free-listings/index.js (100%) diff --git a/js/src/edit-free-campaign/setup-free-listings/form-content.js b/js/src/components/free-listings/setup-free-listings/form-content.js similarity index 100% rename from js/src/edit-free-campaign/setup-free-listings/form-content.js rename to js/src/components/free-listings/setup-free-listings/form-content.js diff --git a/js/src/edit-free-campaign/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js similarity index 100% rename from js/src/edit-free-campaign/setup-free-listings/index.js rename to js/src/components/free-listings/setup-free-listings/index.js diff --git a/js/src/edit-free-campaign/index.js b/js/src/edit-free-campaign/index.js index bce445b831..97ce8414c0 100644 --- a/js/src/edit-free-campaign/index.js +++ b/js/src/edit-free-campaign/index.js @@ -14,10 +14,10 @@ import { isEqual } from 'lodash'; import { useAppDispatch } from '.~/data'; import TopBar from '.~/components/stepper/top-bar'; import ChooseAudience from '.~/components/free-listings/choose-audience'; +import SetupFreeListings from '.~/components/free-listings/setup-free-listings'; import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; import useSettings from '.~/components/free-listings/configure-product-listings/useSettings'; import useApiFetchCallback from '.~/hooks/useApiFetchCallback'; -import SetupFreeListings from './setup-free-listings'; import useLayout from '.~/hooks/useLayout'; import useNavigateAwayPromptEffect from '.~/hooks/useNavigateAwayPromptEffect'; import useShippingRates from '.~/hooks/useShippingRates'; From 93249e65336667bc7372bb66973587440323ca92 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Wed, 3 Aug 2022 16:58:46 +0800 Subject: [PATCH 002/140] Change the shared ChooseAudience to ChooseAudienceSection and merge it into SetupFreeListings for the editing free listings page --- .../choose-audience-section.js} | 46 ++---- .../choose-audience-section.scss | 15 ++ .../choose-audience-section/index.js | 1 + .../free-listings/choose-audience/index.js | 90 ------------ .../free-listings/choose-audience/index.scss | 13 -- .../setup-free-listings/form-content.js | 2 + .../setup-free-listings/index.js | 60 ++++++-- js/src/edit-free-campaign/index.js | 138 ++---------------- .../choose-audience/form-content.js | 1 - .../setup-stepper/choose-audience/index.js | 1 - 10 files changed, 96 insertions(+), 271 deletions(-) rename js/src/components/free-listings/{choose-audience/form-content.js => choose-audience-section/choose-audience-section.js} (76%) create mode 100644 js/src/components/free-listings/choose-audience-section/choose-audience-section.scss create mode 100644 js/src/components/free-listings/choose-audience-section/index.js delete mode 100644 js/src/components/free-listings/choose-audience/index.js delete mode 100644 js/src/components/free-listings/choose-audience/index.scss diff --git a/js/src/components/free-listings/choose-audience/form-content.js b/js/src/components/free-listings/choose-audience-section/choose-audience-section.js similarity index 76% rename from js/src/components/free-listings/choose-audience/form-content.js rename to js/src/components/free-listings/choose-audience-section/choose-audience-section.js index 4919c0edcd..cf9701f26d 100644 --- a/js/src/components/free-listings/choose-audience/form-content.js +++ b/js/src/components/free-listings/choose-audience-section/choose-audience-section.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Button, RadioControl } from '@wordpress/components'; +import { RadioControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; @@ -13,29 +13,28 @@ import AppDocumentationLink from '.~/components/app-documentation-link'; import Section from '.~/wcdl/section'; import Subsection from '.~/wcdl/subsection'; import RadioHelperText from '.~/wcdl/radio-helper-text'; -import StepContentFooter from '.~/components/stepper/step-content-footer'; import SupportedCountrySelect from '.~/components/supported-country-select'; import VerticalGapLayout from '.~/components/vertical-gap-layout'; -import '.~/components/free-listings/choose-audience/index.scss'; +import './choose-audience-section.scss'; /** - * Form to choose audience. + * Section form to choose audience. * * To be used in onboarding and further editing. * Does not provide any save strategy, this is to be bound externaly. - * Copied from {@link .~/setup-mc/setup-stepper/choose-audience/form-content.js}. * - * @param {Object} props + * @param {Object} props React props. + * @param {Object} props.formProps Form props forwarded from `Form` component. * @fires gla_documentation_link_click with `{ context: 'setup-mc-audience', link_id: 'site-language', href: 'https://support.google.com/merchants/answer/160637' }` */ -const FormContent = ( props ) => { - const { formProps } = props; - const { values, isValidForm, getInputProps, handleSubmit } = formProps; +const ChooseAudienceSection = ( { formProps } ) => { + const { values, getInputProps } = formProps; const { locale, language } = values; return ( <>
@@ -52,7 +51,7 @@ const FormContent = ( props ) => { { __( 'Language', 'google-listings-and-ads' ) } - + { createInterpolateElement( __( 'Listings can only be displayed in your site language. Read more', @@ -83,7 +82,7 @@ const FormContent = ( props ) => { { __( 'Location', 'google-listings-and-ads' ) } - + { __( 'Your store should already have the appropriate shipping and tax rates (if required) for potential customers in your selected location(s).', 'google-listings-and-ads' @@ -99,18 +98,14 @@ const FormContent = ( props ) => { ) } value="selected" > -
- -
-
- { __( + + /> {
- - - ); }; -export default FormContent; +export default ChooseAudienceSection; diff --git a/js/src/components/free-listings/choose-audience-section/choose-audience-section.scss b/js/src/components/free-listings/choose-audience-section/choose-audience-section.scss new file mode 100644 index 0000000000..542b48cd42 --- /dev/null +++ b/js/src/components/free-listings/choose-audience-section/choose-audience-section.scss @@ -0,0 +1,15 @@ +.gla-choose-audience-section { + &__language-helper, + .wcdl-radio-helper-text { + font-style: normal; + color: $gray-700; + } + + .wcdl-subsection-helper-text { + margin-bottom: calc(var(--main-gap) / 3 * 2); + } + + .woocommerce-tree-select-control__help { + margin-top: $grid-unit-10; + } +} diff --git a/js/src/components/free-listings/choose-audience-section/index.js b/js/src/components/free-listings/choose-audience-section/index.js new file mode 100644 index 0000000000..0ecccdce11 --- /dev/null +++ b/js/src/components/free-listings/choose-audience-section/index.js @@ -0,0 +1 @@ +export { default } from './choose-audience-section'; diff --git a/js/src/components/free-listings/choose-audience/index.js b/js/src/components/free-listings/choose-audience/index.js deleted file mode 100644 index 56ebcd12f0..0000000000 --- a/js/src/components/free-listings/choose-audience/index.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Form } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import AppSpinner from '.~/components/app-spinner'; -import StepContent from '.~/components/stepper/step-content'; -import StepContentHeader from '.~/components/stepper/step-content-header'; -import FormContent from './form-content'; -import '.~/components/free-listings/choose-audience/index.scss'; - -/** - * Step with a form to choose audience. - * - * To be used in onboarding and further editing. - * Does not provide any save strategy, this is to be bound externaly. - * Copied from {@link .~/setup-mc/setup-stepper/choose-audience/index.js}. - * - * @param {Object} props - * @param {string} [props.initialData] Target audience data, if not given AppSinner will be rendered. - * @param {(change: {name, value}, values: Object) => void} props.onChange Callback called with form data once form data is changed. Forwarded from {@link Form.Props.onChange}. - * @param {function(Object)} props.onContinue Callback called with form data once continue button is clicked. - */ -export default function ChooseAudience( { - initialData, - onChange = () => {}, - onContinue = () => {}, -} ) { - if ( ! initialData ) { - return ; - } - - const handleValidate = ( values ) => { - const errors = {}; - - if ( ! values.location ) { - errors.location = __( - 'Please select a location option.', - 'google-listings-and-ads' - ); - } - - if ( values.location === 'selected' && values.countries.length === 0 ) { - errors.countries = __( - 'Please select at least one country.', - 'google-listings-and-ads' - ); - } - - return errors; - }; - - return ( -
- - - { initialData && ( -
- { ( formProps ) => { - return ; - } } - - ) } -
-
- ); -} diff --git a/js/src/components/free-listings/choose-audience/index.scss b/js/src/components/free-listings/choose-audience/index.scss deleted file mode 100644 index ca57e12e4f..0000000000 --- a/js/src/components/free-listings/choose-audience/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -.gla-choose-audience { - .input { - margin-bottom: calc(var(--main-gap) / 3); - } - - .helper-text { - margin-bottom: calc(var(--main-gap) / 3 * 2); - } - - .cannot-find-country { - font-style: italic; - } -} diff --git a/js/src/components/free-listings/setup-free-listings/form-content.js b/js/src/components/free-listings/setup-free-listings/form-content.js index 9c876d6dd5..cc1e8ff657 100644 --- a/js/src/components/free-listings/setup-free-listings/form-content.js +++ b/js/src/components/free-listings/setup-free-listings/form-content.js @@ -10,6 +10,7 @@ import StepContent from '.~/components/stepper/step-content'; import StepContentFooter from '.~/components/stepper/step-content-footer'; import TaxRate from '.~/components/free-listings/configure-product-listings/tax-rate'; import useDisplayTaxRate from '.~/components/free-listings/configure-product-listings/useDisplayTaxRate'; +import ChooseAudienceSection from '.~/components/free-listings/choose-audience-section'; import ShippingRateSection from '.~/components/shipping-rate-section'; import ShippingTimeSection from '.~/components/free-listings/configure-product-listings/shipping-time-section'; import AppButton from '.~/components/app-button'; @@ -44,6 +45,7 @@ const FormContent = ( { return ( + { * without any save strategy, this is to be bound externaly. * * @param {Object} props - * @param {Array} props.countries List of available countries to be forwarded to FormContent. + * @param {TargetAudienceData} props.targetAudience Target audience value data to be initialed the form, if not given AppSpinner will be rendered. + * @param {(targetAudience: TargetAudienceData) => Array} props.resolveFinalCountries Callback for this component to resolve the given `targetAudience` to the final list of countries. + * @param {(targetAudience: TargetAudienceData) => void} [props.onTargetAudienceChange] Callback called with new data once target audience data is changed. Forwarded from and {@link Form.Props.onChange}. * @param {Object} props.settings Settings data, if not given AppSpinner will be rendered. - * @param {(change: {name, value}, values: Object) => void} props.onSettingsChange Callback called with new data once form data is changed. Forwarded from and {@link Form.Props.onChange}. + * @param {(change: {name, value}, values: Object) => void} [props.onSettingsChange] Callback called with new data once form data is changed. Forwarded from and {@link Form.Props.onChange}. * @param {Array} props.shippingRates Shipping rates data, if not given AppSpinner will be rendered. - * @param {(newValue: Object) => void} props.onShippingRatesChange Callback called with new data once shipping rates are changed. Forwarded from {@link Form.Props.onChange}. + * @param {(newValue: Object) => void} [props.onShippingRatesChange] Callback called with new data once shipping rates are changed. Forwarded from {@link Form.Props.onChange}. * @param {Array} props.shippingTimes Shipping times data, if not given AppSpinner will be rendered. - * @param {(newValue: Object) => void} props.onShippingTimesChange Callback called with new data once shipping times are changed. Forwarded from {@link Form.Props.onChange}. - * @param {function(Object)} props.onContinue Callback called with form data once continue button is clicked. Could be async. While it's being resolved the form would turn into a saving state. + * @param {(newValue: Object) => void} [props.onShippingTimesChange] Callback called with new data once shipping times are changed. Forwarded from {@link Form.Props.onChange}. + * @param {() => void} [props.onContinue] Callback called once continue button is clicked. Could be async. While it's being resolved the form would turn into a saving state. * @param {string} [props.submitLabel] Submit button label, to be forwarded to `FormContent`. */ const SetupFreeListings = ( { - countries, + targetAudience, + resolveFinalCountries, + onTargetAudienceChange = noop, settings, - onSettingsChange = () => {}, + onSettingsChange = noop, shippingRates, - onShippingRatesChange = () => {}, + onShippingRatesChange = noop, shippingTimes, - onShippingTimesChange = () => {}, - onContinue = () => {}, + onShippingTimesChange = noop, + onContinue = noop, submitLabel, } ) => { const [ saving, setSaving ] = useState( false ); + const formRef = useRef(); - if ( ! settings || ! shippingRates || ! shippingTimes || ! countries ) { + if ( ! ( targetAudience && settings && shippingRates && shippingTimes ) ) { return ; } const handleValidate = ( values ) => { + const countries = resolveFinalCountries( values ); const { shipping_country_times: shippingTimesData } = values; return checkErrors( values, shippingTimesData, countries ); @@ -108,6 +116,22 @@ const SetupFreeListings = ( { onShippingTimesChange( values.shipping_country_times ); } else if ( settingsFieldNames.includes( change.name ) ) { onSettingsChange( change, getSettings( values ) ); + } else if ( targetAudienceFields.includes( change.name ) ) { + onTargetAudienceChange( pick( values, targetAudienceFields ) ); + + // Only keep shipping data with selected countries. + [ 'shipping_country_rates', 'shipping_country_times' ].forEach( + ( field ) => { + const countries = resolveFinalCountries( values ); + const currentValues = values[ field ]; + const nextValues = currentValues.filter( ( el ) => + countries.includes( el.country || el.countryCode ) + ); + if ( nextValues.length !== currentValues.length ) { + formRef.current.setValue( field, nextValues ); + } + } + ); } }; @@ -115,7 +139,13 @@ const SetupFreeListings = ( {
{ ( formProps ) => { + const countries = resolveFinalCountries( formProps.values ); + return ( { savedTargetAudience ); const [ settings, updateSettings ] = useState( savedSettings ); - const [ forceUnblockedNavigation, setForceUnblockedNavigation ] = useState( - false - ); const { hasFinishedResolution: hfrShippingRates, @@ -155,53 +133,15 @@ const EditFreeCampaign = () => { 'You have unsaved campaign data. Are you sure you want to leave?', 'google-listings-and-ads' ), - didAnythingChanged && ! forceUnblockedNavigation, - isNotOurStep + didAnythingChanged ); - const { pageStep = '1' } = getQuery(); const dashboardURL = getNewPath( // Clear the step we were at, but perserve programId to be able to highlight the program. { pageStep: undefined, subpath: undefined }, '/google/dashboard' ); - /** - * Update shipping rates and times after users are done - * with the changes in Choose Audience step. - * - * Shipping rates and shipping times that do not have - * a corresponding country in target audience will be removed. - */ - const updateShippingAfterChooseAudienceStep = () => { - const finalCountries = getFinalCountries( targetAudience ); - - const newShippingRates = loadedShippingRates.filter( - ( shippingRate ) => { - return finalCountries.includes( shippingRate.country ); - } - ); - updateShippingRates( newShippingRates ); - - const newShippingTimes = loadedShippingTimes.filter( - ( shippingTime ) => { - return finalCountries.includes( shippingTime.countryCode ); - } - ); - updateShippingTimes( newShippingTimes ); - }; - - const handleChooseAudienceChange = ( change, newTargetAudience ) => { - updateTargetAudience( newTargetAudience ); - }; - - const handleChooseAudienceContinue = () => { - setForceUnblockedNavigation( true ); - updateShippingAfterChooseAudienceStep(); - getHistory().push( getNewPath( { pageStep: '2' } ) ); - setForceUnblockedNavigation( false ); - }; - const handleSetupFreeListingsContinue = async () => { // TODO: Disable the form so the user won't be able to input any changes, which could be disregarded. try { @@ -234,18 +174,6 @@ const EditFreeCampaign = () => { } }; - const handleStepClick = ( key ) => { - /** - * When users move from Step 1 Choose Audience to Step 2 Configure listings, - * we update shipping rates and shipping times based on the changes in Choose Audience. - */ - if ( key === '2' ) { - updateShippingAfterChooseAudienceStep(); - } - - getHistory().push( getNewPath( { pageStep: key } ) ); - }; - return ( <> { } backHref={ dashboardURL } /> - - ), - onClick: handleStepClick, - }, - { - key: '2', - label: __( - 'Configure your product listings', - 'google-listings-and-ads' - ), - content: ( - { - updateSettings( newSettings ); - } } - shippingRates={ loadedShippingRates } - onShippingRatesChange={ updateShippingRates } - shippingTimes={ loadedShippingTimes } - onShippingTimesChange={ updateShippingTimes } - onContinue={ handleSetupFreeListingsContinue } - submitLabel={ __( - 'Save changes', - 'google-listings-and-ads' - ) } - /> - ), - onClick: handleStepClick, - }, - ] } + { + updateSettings( newSettings ); + } } + shippingRates={ loadedShippingRates } + onShippingRatesChange={ updateShippingRates } + shippingTimes={ loadedShippingTimes } + onShippingTimesChange={ updateShippingTimes } + onContinue={ handleSetupFreeListingsContinue } + submitLabel={ __( 'Save changes', 'google-listings-and-ads' ) } /> ); diff --git a/js/src/setup-mc/setup-stepper/choose-audience/form-content.js b/js/src/setup-mc/setup-stepper/choose-audience/form-content.js index 5407b724e1..a32c40f219 100644 --- a/js/src/setup-mc/setup-stepper/choose-audience/form-content.js +++ b/js/src/setup-mc/setup-stepper/choose-audience/form-content.js @@ -18,7 +18,6 @@ import SupportedCountrySelect from '.~/components/supported-country-select'; import VerticalGapLayout from '.~/components/vertical-gap-layout'; import useAutoSaveTargetAudienceEffect from './useAutoSaveTargetAudienceEffect'; import useAutoClearShippingEffect from './useAutoClearShippingEffect'; -import '.~/components/free-listings/choose-audience/index.scss'; /** * Form to choose audience. diff --git a/js/src/setup-mc/setup-stepper/choose-audience/index.js b/js/src/setup-mc/setup-stepper/choose-audience/index.js index 153818910f..c888922698 100644 --- a/js/src/setup-mc/setup-stepper/choose-audience/index.js +++ b/js/src/setup-mc/setup-stepper/choose-audience/index.js @@ -12,7 +12,6 @@ import StepContent from '.~/components/stepper/step-content'; import StepContentHeader from '.~/components/stepper/step-content-header'; import FormContent from './form-content'; import useTargetAudienceWithSuggestions from './useTargetAudienceWithSuggestions'; -import '.~/components/free-listings/choose-audience/index.scss'; /** * Step with a form to choose audience. From 79911f806a723749bb37a92c66239949eb210d6f Mon Sep 17 00:00:00 2001 From: Eason Su Date: Wed, 3 Aug 2022 17:43:24 +0800 Subject: [PATCH 003/140] Add validations for merged audience values in the shared SetupFreeListings --- .../__snapshots__/checkErrors.test.js.snap | 6 ++ .../configure-product-listings/checkErrors.js | 16 ++++++ .../checkErrors.test.js | 55 +++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/js/src/components/free-listings/configure-product-listings/__snapshots__/checkErrors.test.js.snap b/js/src/components/free-listings/configure-product-listings/__snapshots__/checkErrors.test.js.snap index 77fd9edc26..cd095af7b7 100644 --- a/js/src/components/free-listings/configure-product-listings/__snapshots__/checkErrors.test.js.snap +++ b/js/src/components/free-listings/configure-product-listings/__snapshots__/checkErrors.test.js.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`checkErrors Audience When the audience countries array is empty and the value of audience location option is 'selected', should not pass 1`] = `"Please select at least one country."`; + +exports[`checkErrors Audience When the audience location option is an invalid value or missing, should not pass 1`] = `"Please select a location option."`; + +exports[`checkErrors Audience When the audience location option is an invalid value or missing, should not pass 2`] = `"Please select a location option."`; + exports[`checkErrors For tax rate, if selected country codes include 'US' When the tax rate option is an invalid value or missing, should not pass 1`] = `"Please specify tax rate option."`; exports[`checkErrors For tax rate, if selected country codes include 'US' When the tax rate option is an invalid value or missing, should not pass 2`] = `"Please specify tax rate option."`; diff --git a/js/src/components/free-listings/configure-product-listings/checkErrors.js b/js/src/components/free-listings/configure-product-listings/checkErrors.js index 37ca703036..d06e7d3f45 100644 --- a/js/src/components/free-listings/configure-product-listings/checkErrors.js +++ b/js/src/components/free-listings/configure-product-listings/checkErrors.js @@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n'; */ import isNonFreeFlatShippingRate from '.~/utils/isNonFreeFlatShippingRate'; +const validlocationSet = new Set( [ 'all', 'selected' ] ); const validShippingRateSet = new Set( [ 'automatic', 'flat', 'manual' ] ); const validShippingTimeSet = new Set( [ 'flat', 'manual' ] ); const validTaxRateSet = new Set( [ 'destination', 'manual' ] ); @@ -15,6 +16,21 @@ const validTaxRateSet = new Set( [ 'destination', 'manual' ] ); const checkErrors = ( values, shippingTimes, finalCountryCodes ) => { const errors = {}; + // Check audience. + if ( ! validlocationSet.has( values.location ) ) { + errors.location = __( + 'Please select a location option.', + 'google-listings-and-ads' + ); + } + + if ( values.location === 'selected' && values.countries.length === 0 ) { + errors.countries = __( + 'Please select at least one country.', + 'google-listings-and-ads' + ); + } + /** * Check shipping rates. */ diff --git a/js/src/components/free-listings/configure-product-listings/checkErrors.test.js b/js/src/components/free-listings/configure-product-listings/checkErrors.test.js index c9a90d8f03..baebd0ba1b 100644 --- a/js/src/components/free-listings/configure-product-listings/checkErrors.test.js +++ b/js/src/components/free-listings/configure-product-listings/checkErrors.test.js @@ -38,6 +38,8 @@ const defaultFormValues = { describe( 'checkErrors', () => { it( 'When all checks are passed, should return an empty object', () => { const values = { + location: 'selected', + countries: [ 'US', 'JP' ], shipping_rate: 'flat', shipping_time: 'flat', tax_rate: 'manual', @@ -59,6 +61,59 @@ describe( 'checkErrors', () => { expect( errors ).toHaveProperty( 'shipping_time' ); } ); + describe( 'Audience', () => { + it( 'When the audience location option is an invalid value or missing, should not pass', () => { + // Not set yet + let errors = checkErrors( {}, [], [] ); + + expect( errors ).toHaveProperty( 'location' ); + expect( errors.location ).toMatchSnapshot(); + + // Invalid value + errors = checkErrors( { location: true }, [], [] ); + + expect( errors ).toHaveProperty( 'location' ); + expect( errors.location ).toMatchSnapshot(); + } ); + + it( 'When the audience location option is a valid value, should pass', () => { + // Selected all countries + let errors = checkErrors( { location: 'all' }, [], [] ); + + expect( errors ).not.toHaveProperty( 'location' ); + + // Selected "selected countries only" + const values = { + location: 'selected', + countries: [], + }; + errors = checkErrors( values, [], [] ); + + expect( errors ).not.toHaveProperty( 'location' ); + } ); + + it( `When the audience countries array is empty and the value of audience location option is 'selected', should not pass`, () => { + const values = { + location: 'selected', + countries: [], + }; + const errors = checkErrors( values, [], [] ); + + expect( errors ).toHaveProperty( 'countries' ); + expect( errors.countries ).toMatchSnapshot(); + } ); + + it( `When the audience countries array is not empty and the value of audience location option is 'selected', should pass`, () => { + const values = { + location: 'selected', + countries: [ '' ], + }; + const errors = checkErrors( values, [], [] ); + + expect( errors ).not.toHaveProperty( 'countries' ); + } ); + } ); + describe( 'Shipping rates', () => { let automaticShipping; let flatShipping; From 55e2c0909aa016148b6311b7b3a0873f94bfec8c Mon Sep 17 00:00:00 2001 From: Eason Su Date: Wed, 3 Aug 2022 18:17:28 +0800 Subject: [PATCH 004/140] Replace SetupFreeListings with the shared one for the onboarding flow --- .../setup-free-listings/index.js | 4 +- .../setup-stepper/saved-setup-stepper.js | 90 +++++++++++++------ .../setup-mc/setup-stepper/stepNameKeyMap.js | 8 +- .../useTargetAudienceWithSuggestions.js | 0 4 files changed, 71 insertions(+), 31 deletions(-) rename js/src/setup-mc/setup-stepper/{choose-audience => }/useTargetAudienceWithSuggestions.js (100%) diff --git a/js/src/components/free-listings/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js index 52a52daed3..39f8be0d77 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -147,8 +147,8 @@ const SetupFreeListings = ( { location: targetAudience.location, countries: targetAudience.countries || [], // These are the fields for settings. - shipping_rate: settings.shipping_rate, - shipping_time: settings.shipping_time, + shipping_rate: settings.shipping_rate || 'automatic', + shipping_time: settings.shipping_time || 'flat', tax_rate: settings.tax_rate, website_live: settings.website_live, checkout_process_secure: settings.checkout_process_secure, diff --git a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js index 828884d897..118c1755d5 100644 --- a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js +++ b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js @@ -3,15 +3,22 @@ */ import { Stepper } from '@woocommerce/components'; import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ +import { useAppDispatch } from '.~/data'; +import useTargetAudienceWithSuggestions from './useTargetAudienceWithSuggestions'; +import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; +import useSettings from '.~/components/free-listings/configure-product-listings/useSettings'; +import useShippingRates from '.~/hooks/useShippingRates'; +import useShippingTimes from '.~/hooks/useShippingTimes'; +import useSaveShippingRates from '.~/hooks/useSaveShippingRates'; +import useSaveShippingTimes from '.~/hooks/useSaveShippingTimes'; import SetupAccounts from './setup-accounts'; -import SetupFreeListings from './setup-free-listings'; -import ChooseAudience from './choose-audience'; +import SetupFreeListings from '.~/components/free-listings/setup-free-listings'; import StoreRequirements from './store-requirements'; import './index.scss'; import stepNameKeyMap from './stepNameKeyMap'; @@ -25,6 +32,36 @@ import stepNameKeyMap from './stepNameKeyMap'; const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { const [ step, setStep ] = useState( savedStep ); + const { settings } = useSettings(); + const { data: suggestedAudience } = useTargetAudienceWithSuggestions(); + const { + targetAudience, + getFinalCountries, + } = useTargetAudienceFinalCountryCodes(); + const { + hasFinishedResolution: hasResolvedShippingRates, + data: shippingRates, + } = useShippingRates(); + const { + hasFinishedResolution: hasResolvedShippingTimes, + data: shippingTimes, + } = useShippingTimes(); + + const { saveTargetAudience, saveSettings } = useAppDispatch(); + const { saveShippingRates } = useSaveShippingRates(); + const { saveShippingTimes } = useSaveShippingTimes(); + + // Auto-save the suggested audience data as the initial values to fall back with the original implementation. + // Ref: https://github.com/woocommerce/google-listings-and-ads/blob/2.0.2/js/src/setup-mc/setup-stepper/choose-audience/form-content.js#L37 + useEffect( () => { + if ( + targetAudience?.location === null && + suggestedAudience?.location + ) { + saveTargetAudience( suggestedAudience ); + } + }, [ targetAudience, suggestedAudience, saveTargetAudience ] ); + const handleSetupAccountsContinue = () => { recordEvent( 'gla_setup_mc', { target: 'step1_continue', @@ -34,18 +71,9 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { onRefetchSavedStep(); }; - const handleChooseAudienceContinue = () => { - recordEvent( 'gla_setup_mc', { - target: 'step2_continue', - trigger: 'click', - } ); - setStep( stepNameKeyMap.shipping_and_taxes ); - onRefetchSavedStep(); - }; - const handleSetupListingsContinue = () => { recordEvent( 'gla_setup_mc', { - target: 'step3_continue', + target: 'step2_continue', trigger: 'click', } ); setStep( stepNameKeyMap.store_requirements ); @@ -58,6 +86,14 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { } }; + const handleSettingsChange = ( change, newSettings ) => { + saveSettings( newSettings ); + }; + + const initShippingRates = hasResolvedShippingRates ? shippingRates : null; + const initShippingTimes = hasResolvedShippingTimes ? shippingTimes : null; + const initTargetAudience = targetAudience?.location ? targetAudience : null; + return ( {} } ) => { { key: stepNameKeyMap.target_audience, label: __( - 'Choose your audience', - 'google-listings-and-ads' - ), - content: ( - - ), - onClick: handleStepClick, - }, - { - key: stepNameKeyMap.shipping_and_taxes, - label: __( - 'Configure your product listings', + 'Configure product listings', 'google-listings-and-ads' ), content: ( ), onClick: handleStepClick, diff --git a/js/src/setup-mc/setup-stepper/stepNameKeyMap.js b/js/src/setup-mc/setup-stepper/stepNameKeyMap.js index 5be111f9be..75498b68e6 100644 --- a/js/src/setup-mc/setup-stepper/stepNameKeyMap.js +++ b/js/src/setup-mc/setup-stepper/stepNameKeyMap.js @@ -1,8 +1,12 @@ +// TODO: +// Two keys have the same value '2' temporarily before the overall changes of +// onboarding steps are completed. They would be mapped/changed to new step keys +// and values in the subsequent PR. const stepNameKeyMap = { accounts: '1', target_audience: '2', - shipping_and_taxes: '3', - store_requirements: '4', + shipping_and_taxes: '2', + store_requirements: '3', }; export default stepNameKeyMap; diff --git a/js/src/setup-mc/setup-stepper/choose-audience/useTargetAudienceWithSuggestions.js b/js/src/setup-mc/setup-stepper/useTargetAudienceWithSuggestions.js similarity index 100% rename from js/src/setup-mc/setup-stepper/choose-audience/useTargetAudienceWithSuggestions.js rename to js/src/setup-mc/setup-stepper/useTargetAudienceWithSuggestions.js From 193ab56f6ef1bfcb93153fa107d4969242d9a121 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Wed, 3 Aug 2022 19:15:45 +0800 Subject: [PATCH 005/140] Catch errors when saving data in the SavedSetupStepper of onboarding flow --- .../setup-free-listings/index.js | 4 +- js/src/edit-free-campaign/index.js | 4 +- .../setup-stepper/saved-setup-stepper.js | 42 +++++++++++++++---- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/js/src/components/free-listings/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js index 39f8be0d77..f04f7979d8 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -68,7 +68,7 @@ const getSettings = ( values ) => { * @param {(targetAudience: TargetAudienceData) => Array} props.resolveFinalCountries Callback for this component to resolve the given `targetAudience` to the final list of countries. * @param {(targetAudience: TargetAudienceData) => void} [props.onTargetAudienceChange] Callback called with new data once target audience data is changed. Forwarded from and {@link Form.Props.onChange}. * @param {Object} props.settings Settings data, if not given AppSpinner will be rendered. - * @param {(change: {name, value}, values: Object) => void} [props.onSettingsChange] Callback called with new data once form data is changed. Forwarded from and {@link Form.Props.onChange}. + * @param {(newValue: Object) => void} [props.onSettingsChange] Callback called with new data once form data is changed. Forwarded from and {@link Form.Props.onChange}. * @param {Array} props.shippingRates Shipping rates data, if not given AppSpinner will be rendered. * @param {(newValue: Object) => void} [props.onShippingRatesChange] Callback called with new data once shipping rates are changed. Forwarded from {@link Form.Props.onChange}. * @param {Array} props.shippingTimes Shipping times data, if not given AppSpinner will be rendered. @@ -115,7 +115,7 @@ const SetupFreeListings = ( { } else if ( change.name === 'shipping_country_times' ) { onShippingTimesChange( values.shipping_country_times ); } else if ( settingsFieldNames.includes( change.name ) ) { - onSettingsChange( change, getSettings( values ) ); + onSettingsChange( getSettings( values ) ); } else if ( targetAudienceFields.includes( change.name ) ) { onTargetAudienceChange( pick( values, targetAudienceFields ) ); diff --git a/js/src/edit-free-campaign/index.js b/js/src/edit-free-campaign/index.js index 86ff7ff96a..c9a167ed90 100644 --- a/js/src/edit-free-campaign/index.js +++ b/js/src/edit-free-campaign/index.js @@ -188,9 +188,7 @@ const EditFreeCampaign = () => { resolveFinalCountries={ getFinalCountries } onTargetAudienceChange={ updateTargetAudience } settings={ settings } - onSettingsChange={ ( change, newSettings ) => { - updateSettings( newSettings ); - } } + onSettingsChange={ updateSettings } shippingRates={ loadedShippingRates } onShippingRatesChange={ updateShippingRates } shippingTimes={ loadedShippingTimes } diff --git a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js index 118c1755d5..78598f551a 100644 --- a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js +++ b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js @@ -17,6 +17,7 @@ import useShippingRates from '.~/hooks/useShippingRates'; import useShippingTimes from '.~/hooks/useShippingTimes'; import useSaveShippingRates from '.~/hooks/useSaveShippingRates'; import useSaveShippingTimes from '.~/hooks/useSaveShippingTimes'; +import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; import SetupAccounts from './setup-accounts'; import SetupFreeListings from '.~/components/free-listings/setup-free-listings'; import StoreRequirements from './store-requirements'; @@ -50,6 +51,7 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { const { saveTargetAudience, saveSettings } = useAppDispatch(); const { saveShippingRates } = useSaveShippingRates(); const { saveShippingTimes } = useSaveShippingTimes(); + const { createNotice } = useDispatchCoreNotices(); // Auto-save the suggested audience data as the initial values to fall back with the original implementation. // Ref: https://github.com/woocommerce/google-listings-and-ads/blob/2.0.2/js/src/setup-mc/setup-stepper/choose-audience/form-content.js#L37 @@ -86,9 +88,9 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { } }; - const handleSettingsChange = ( change, newSettings ) => { - saveSettings( newSettings ); - }; + function handleFormChange( errorMessage, newValue ) { + this( newValue ).catch( () => createNotice( 'error', errorMessage ) ); + } const initShippingRates = hasResolvedShippingRates ? shippingRates : null; const initShippingTimes = hasResolvedShippingTimes ? shippingTimes : null; @@ -121,14 +123,38 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { content: ( Date: Thu, 4 Aug 2022 12:27:49 +0800 Subject: [PATCH 006/140] Remove unused files --- .../setup-free-listings/form-content.js | 2 - .../setup-free-listings/index.js | 3 - js/src/hooks/useDebouncedCallbackEffect.js | 54 ------ .../choose-audience/form-content.js | 151 ---------------- .../setup-stepper/choose-audience/index.js | 88 ---------- .../useAutoClearShippingEffect.js | 71 -------- .../useAutoSaveTargetAudienceEffect.js | 61 ------- .../useAutoSaveTargetAudienceEffect.test.js | 79 --------- .../setup-free-listings/form-content.js | 53 ------ .../setup-free-listings/index.js | 164 ------------------ .../shipping-time-section.js | 55 ------ .../add-time-button/add-time-modal/index.js | 98 ----------- .../useGetRemainingCountryCodes.js | 40 ----- .../add-time-button/index.js | 41 ----- .../countriesTimeInput.test.js | 75 -------- .../countries-time-input-form/index.js | 40 ----- .../edit-time-button/edit-time-modal/index.js | 118 ------------- .../edit-time-button/index.js | 45 ----- .../edit-time-button/index.scss | 7 - .../countries-time-input/index.js | 44 ----- .../countries-time-input/index.scss | 7 - .../getCountriesTimeArray.js | 55 ------ .../shipping-time-setup/index.js | 62 ------- .../shipping-time-setup/index.scss | 15 -- .../useAutoSaveSettingsEffect.js | 49 ------ .../useAutoSaveSettingsEffect.test.js | 88 ---------- .../useAutoSaveShippingRatesEffect.js | 35 ---- .../setup-free-listings/useSaveSuggestions.js | 45 ----- .../useShippingRatesSuggestions.js | 69 -------- .../useShippingRatesSuggestions.test.js | 90 ---------- .../useShippingRatesWithSavedSuggestions.js | 105 ----------- ...eShippingRatesWithSavedSuggestions.test.js | 147 ---------------- 32 files changed, 2056 deletions(-) delete mode 100644 js/src/hooks/useDebouncedCallbackEffect.js delete mode 100644 js/src/setup-mc/setup-stepper/choose-audience/form-content.js delete mode 100644 js/src/setup-mc/setup-stepper/choose-audience/index.js delete mode 100644 js/src/setup-mc/setup-stepper/choose-audience/useAutoClearShippingEffect.js delete mode 100644 js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.js delete mode 100644 js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.test.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/form-content.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time-section.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/useGetRemainingCountryCodes.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/countriesTimeInput.test.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/edit-time-modal/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.scss delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.scss delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/getCountriesTimeArray.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.scss delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.test.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveShippingRatesEffect.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useSaveSuggestions.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.test.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.test.js diff --git a/js/src/components/free-listings/setup-free-listings/form-content.js b/js/src/components/free-listings/setup-free-listings/form-content.js index cc1e8ff657..ae101df6ef 100644 --- a/js/src/components/free-listings/setup-free-listings/form-content.js +++ b/js/src/components/free-listings/setup-free-listings/form-content.js @@ -22,8 +22,6 @@ import ConditionalSection from '.~/components/conditional-section'; /** * Form to configure free listigns. - * Copied from {@link .~/setup-mc/setup-stepper/setup-free-listings/form-content.js}, - * without auto-save functionality. * * @param {Object} props React props. * @param {Array} props.countries List of available countries to be forwarded to ShippingRateSection and ShippingTimeSection. diff --git a/js/src/components/free-listings/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js index f04f7979d8..38b6176e06 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -60,9 +60,6 @@ const getSettings = ( values ) => { /** * Setup step to configure free listings. * - * Copied from {@link .~/setup-mc/setup-stepper/setup-free-listings/index.js}, - * without any save strategy, this is to be bound externaly. - * * @param {Object} props * @param {TargetAudienceData} props.targetAudience Target audience value data to be initialed the form, if not given AppSpinner will be rendered. * @param {(targetAudience: TargetAudienceData) => Array} props.resolveFinalCountries Callback for this component to resolve the given `targetAudience` to the final list of countries. diff --git a/js/src/hooks/useDebouncedCallbackEffect.js b/js/src/hooks/useDebouncedCallbackEffect.js deleted file mode 100644 index d192244129..0000000000 --- a/js/src/hooks/useDebouncedCallbackEffect.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useRef } from '@wordpress/element'; -import { useDebouncedCallback } from 'use-debounce'; - -/** - * Internal dependencies - */ -import useIsEqualRefValue from './useIsEqualRefValue'; - -const defaultOptions = { - wait: 500, - callOnFirstRender: false, -}; - -/** - * Call function with debounce delay. - * - * By default, it does not call on first render, since the first render is the loading of value from API. - * Pass an options object with `callOnFirstRender` set to `true` to call on first render. - * - * @param {Object} value Value to be passed to func. - * @param {Function} func Function to be debounced. - * @param {Object} [options] Options object. - * @param {number} [options.wait] Number of milliseconds to wait before calling the function. Default is 500. - * @param {boolean} [options.callOnFirstRender] Boolean indicating whether to call the function on first render. Default is false. - */ -const useDebouncedCallbackEffect = ( - value = {}, - func = () => {}, - options = defaultOptions -) => { - const { wait, callOnFirstRender } = { - ...defaultOptions, - ...options, - }; - const valueRefValue = useIsEqualRefValue( value ); - const debouncedCallback = useDebouncedCallback( func, wait ); - const ref = useRef( false ); - - useEffect( () => { - // whether to call on first render. - if ( ! callOnFirstRender && ! ref.current ) { - ref.current = true; - return; - } - - // call the debounced callback. - debouncedCallback.callback( valueRefValue ); - }, [ callOnFirstRender, debouncedCallback, valueRefValue ] ); -}; - -export default useDebouncedCallbackEffect; diff --git a/js/src/setup-mc/setup-stepper/choose-audience/form-content.js b/js/src/setup-mc/setup-stepper/choose-audience/form-content.js deleted file mode 100644 index a32c40f219..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/form-content.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * External dependencies - */ -import { Button, RadioControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { createInterpolateElement, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import AppRadioContentControl from '.~/components/app-radio-content-control'; -import AppDocumentationLink from '.~/components/app-documentation-link'; -import Section from '.~/wcdl/section'; -import Subsection from '.~/wcdl/subsection'; -import RadioHelperText from '.~/wcdl/radio-helper-text'; -import StepContentFooter from '.~/components/stepper/step-content-footer'; -import SupportedCountrySelect from '.~/components/supported-country-select'; -import VerticalGapLayout from '.~/components/vertical-gap-layout'; -import useAutoSaveTargetAudienceEffect from './useAutoSaveTargetAudienceEffect'; -import useAutoClearShippingEffect from './useAutoClearShippingEffect'; - -/** - * Form to choose audience. - * Auto-saves. - * - * @see .~/components/free-listings/choose-audience/form-content - * @param {Object} props - * @fires gla_documentation_link_click with `{ context: 'setup-mc-audience', link_id: 'site-language', href: 'https://support.google.com/merchants/answer/160637' }` - */ -const FormContent = ( props ) => { - const { formProps } = props; - const { values, isValidForm, getInputProps, handleSubmit } = formProps; - const { locale, language } = values; - const [ isAutoSaved, setAutoSaved ] = useState( true ); - - useAutoSaveTargetAudienceEffect( values, setAutoSaved ); - useAutoClearShippingEffect( values.location, values.countries ); - - return ( - <> -
- { __( - 'Where do you want to sell your products?', - 'google-listings-and-ads' - ) } -

- } - > - - - - - { __( 'Language', 'google-listings-and-ads' ) } - - - { createInterpolateElement( - __( - 'Listings can only be displayed in your site language. Read more', - 'google-listings-and-ads' - ), - { - link: ( - - ), - } - ) } - - - - - - { __( 'Location', 'google-listings-and-ads' ) } - - - { __( - 'Your store should already have the appropriate shipping and tax rates (if required) for potential customers in your selected location(s).', - 'google-listings-and-ads' - ) } - - - -
- -
-
- { __( - 'Can’t find a country? Only supported countries can be selected.', - 'google-listings-and-ads' - ) } -
-
- - - { __( - 'Your listings will be shown in all supported countries.', - 'google-listings-and-ads' - ) } - - -
-
-
-
-
- - - - - ); -}; - -export default FormContent; diff --git a/js/src/setup-mc/setup-stepper/choose-audience/index.js b/js/src/setup-mc/setup-stepper/choose-audience/index.js deleted file mode 100644 index c888922698..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/index.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Form } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import AppSpinner from '.~/components/app-spinner'; -import StepContent from '.~/components/stepper/step-content'; -import StepContentHeader from '.~/components/stepper/step-content-header'; -import FormContent from './form-content'; -import useTargetAudienceWithSuggestions from './useTargetAudienceWithSuggestions'; - -/** - * Step with a form to choose audience. - * Auto-saves. - * - * @see .~/components/free-listings/choose-audience - * @param {Object} props - * @param {function(Object)} props.onContinue Callback called with form data once continue button is clicked. - */ -const ChooseAudience = ( props ) => { - const { onContinue = () => {} } = props; - const { loading, data } = useTargetAudienceWithSuggestions(); - - if ( loading ) { - return ; - } - - const handleValidate = ( values ) => { - const errors = {}; - - if ( ! values.location ) { - errors.location = __( - 'Please select a location option.', - 'google-listings-and-ads' - ); - } - - if ( values.location === 'selected' && values.countries.length === 0 ) { - errors.countries = __( - 'Please select at least one country.', - 'google-listings-and-ads' - ); - } - - return errors; - }; - - const handleSubmitCallback = () => { - onContinue(); - }; - - return ( -
- - - - { ( formProps ) => { - return ; - } } - - -
- ); -}; - -export default ChooseAudience; diff --git a/js/src/setup-mc/setup-stepper/choose-audience/useAutoClearShippingEffect.js b/js/src/setup-mc/setup-stepper/choose-audience/useAutoClearShippingEffect.js deleted file mode 100644 index 5f92bc377e..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/useAutoClearShippingEffect.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useRef } from '@wordpress/element'; -import { useDebouncedCallback } from 'use-debounce'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { useAppDispatch } from '.~/data'; -import useShippingTimes from '.~/hooks/useShippingTimes'; -import useSaveShippingRates from '.~/hooks/useSaveShippingRates'; -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; - -const wait = 500; - -const useAutoClearShippingEffect = ( location, countries ) => { - const { data: shippingTimes } = useShippingTimes(); - const { saveShippingRates } = useSaveShippingRates(); - const { deleteShippingTimes } = useAppDispatch(); - const { createNotice } = useDispatchCoreNotices(); - - const debouncedDelete = useDebouncedCallback( async () => { - try { - saveShippingRates( [] ); - - if ( shippingTimes.length ) { - const countryCodes = shippingTimes.map( - ( el ) => el.countryCode - ); - deleteShippingTimes( countryCodes ); - } - } catch ( error ) { - createNotice( - 'error', - __( - 'Something went wrong while trying to clear your shipping data. Please try again later.', - 'google-listings-and-ads' - ) - ); - } - }, wait ); - - const locationRef = useRef( null ); - const countriesRef = useRef( null ); - - useEffect( () => { - if ( locationRef.current === null && countriesRef.current === null ) { - locationRef.current = location; - countriesRef.current = countries; - return; - } - - if ( - ( locationRef.current === 'all' && location === 'all' ) || - ( locationRef.current === 'selected' && - location === 'selected' && - countriesRef.current.length === countries.length ) - ) { - return; - } - - locationRef.current = location; - countriesRef.current = countries; - - debouncedDelete.callback(); - }, [ debouncedDelete, location, countries ] ); -}; - -export default useAutoClearShippingEffect; diff --git a/js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.js b/js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.js deleted file mode 100644 index 9a6a824064..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { noop } from 'lodash'; - -/** - * Internal dependencies - */ -import { useAppDispatch } from '.~/data'; -import useDebouncedCallbackEffect from '.~/hooks/useDebouncedCallbackEffect'; -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; - -/** - * @typedef { import(".~/data/actions").TargetAudienceData } TargetAudienceData - */ - -/** - * Automatically save settings value upon value change with a debounce delay. - * - * This will save the target audience data on first render, - * because the data might be coming from the target audience suggestion API - * which has not been saved before yet. - * - * @param {TargetAudienceData} targetAudience Target audience value object to be saved. - * @param {(autoSaveResult: boolean) => void} autoSaveCallback Callback function when the autosave is called - */ -const useAutoSaveTargetAudienceEffect = ( - targetAudience, - autoSaveCallback = noop -) => { - const { saveTargetAudience } = useAppDispatch(); - const { createNotice } = useDispatchCoreNotices(); - - /** - * A `saveTargetAudienceCallback` callback that catches error and throws the error notice. - * - * @param {TargetAudienceData} value Target audience value object to be saved. - */ - const saveTargetAudienceCallback = async ( value ) => { - try { - await saveTargetAudience( value ); - autoSaveCallback( true ); - } catch ( error ) { - createNotice( - 'error', - __( - 'There was an error saving target audience data.', - 'google-listings-and-ads' - ) - ); - autoSaveCallback( false ); - } - }; - - useDebouncedCallbackEffect( targetAudience, saveTargetAudienceCallback, { - callOnFirstRender: true, - } ); -}; - -export default useAutoSaveTargetAudienceEffect; diff --git a/js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.test.js b/js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.test.js deleted file mode 100644 index e967425992..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/useAutoSaveTargetAudienceEffect.test.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * External dependencies - */ -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import useAutoSaveTargetAudienceEffect from './useAutoSaveTargetAudienceEffect'; - -const mockSaveTargetAudience = jest.fn().mockName( 'saveTargetAudience' ); -const mockAutoSaveCallback = jest.fn().mockName( 'autoSaveCallbak' ); -const mockCreateNotice = jest.fn().mockName( 'createNotice' ); - -jest.mock( '.~/data', () => ( { - useAppDispatch: () => ( { - saveTargetAudience: mockSaveTargetAudience, - } ), -} ) ); - -jest.mock( '.~/hooks/useDispatchCoreNotices', () => () => ( { - createNotice: mockCreateNotice, -} ) ); - -describe( 'useAutoSaveTargetAudienceEffect', () => { - const values = { - countries: [ 'ES', 'IT', 'FR' ], - language: 'English', - locale: 'en_US', - location: 'selected', - }; - - afterEach( () => { - jest.clearAllMocks(); - } ); - - test( 'Autosaving without errors', async () => { - renderHook( () => - useAutoSaveTargetAudienceEffect( values, mockAutoSaveCallback ) - ); - - await waitFor( () => { - expect( mockSaveTargetAudience ).toHaveBeenCalledTimes( 1 ); - expect( mockSaveTargetAudience ).toHaveBeenCalledWith( values ); - - expect( mockAutoSaveCallback ).toHaveBeenCalledTimes( 1 ); - expect( mockAutoSaveCallback ).toHaveBeenCalledWith( true ); - - //No errors should be displayed - expect( mockCreateNotice ).toHaveBeenCalledTimes( 0 ); - } ); - } ); - - test( 'Autosaving with errors', async () => { - mockSaveTargetAudience.mockImplementation( () => { - throw new Error( 'New error!' ); - } ); - - renderHook( () => - useAutoSaveTargetAudienceEffect( values, mockAutoSaveCallback ) - ); - - await waitFor( () => { - expect( mockSaveTargetAudience ).toHaveBeenCalledTimes( 1 ); - expect( mockSaveTargetAudience ).toHaveBeenCalledWith( values ); - - expect( mockAutoSaveCallback ).toHaveBeenCalledTimes( 1 ); - expect( mockAutoSaveCallback ).toHaveBeenCalledWith( false ); - - //Errors should be displayed - expect( mockCreateNotice ).toHaveBeenCalledTimes( 1 ); - expect( mockCreateNotice ).toHaveBeenCalledWith( - 'error', - 'There was an error saving target audience data.' - ); - } ); - } ); -} ); diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/form-content.js b/js/src/setup-mc/setup-stepper/setup-free-listings/form-content.js deleted file mode 100644 index 465ec9b130..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/form-content.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Internal dependencies - */ -import StepContent from '.~/components/stepper/step-content'; -import StepContentFooter from '.~/components/stepper/step-content-footer'; -import TaxRate from '.~/components/free-listings/configure-product-listings/tax-rate'; -import useAutoSaveSettingsEffect from './useAutoSaveSettingsEffect'; -import useDisplayTaxRate from '.~/components/free-listings/configure-product-listings/useDisplayTaxRate'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import ConditionalSection from '.~/components/conditional-section'; -import ShippingRateSection from '.~/components/shipping-rate-section'; -import ShippingTimeSection from './shipping-time-section'; -import useAutoSaveShippingRatesEffect from './useAutoSaveShippingRatesEffect'; - -/** - * Form to configure free listings. - * Auto-saves changes. - * - * @see /js/src/edit-free-campaign/setup-free-listings/form-content.js - * @param {Object} props - */ -const FormContent = ( props ) => { - const { formProps, submitButton } = props; - const { values } = formProps; - const { - shipping_country_rates: shippingRatesValue, - ...settingsValue - } = values; - const { data: audienceCountries } = useTargetAudienceFinalCountryCodes(); - const shouldDisplayTaxRate = useDisplayTaxRate( audienceCountries ); - const shouldDisplayShippingTime = values.shipping_time === 'flat'; - - useAutoSaveSettingsEffect( settingsValue ); - useAutoSaveShippingRatesEffect( shippingRatesValue ); - - return ( - - - { shouldDisplayShippingTime && ( - - ) } - - - - { submitButton } - - ); -}; - -export default FormContent; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/index.js deleted file mode 100644 index 063cae5203..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/index.js +++ /dev/null @@ -1,164 +0,0 @@ -/** - * External dependencies - */ -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { Form } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import AppSpinner from '.~/components/app-spinner'; -import Hero from '.~/components/free-listings/configure-product-listings/hero'; -import useSettings from '.~/components/free-listings/configure-product-listings/useSettings'; -import checkErrors from '.~/components/free-listings/configure-product-listings/checkErrors'; -import FormContent from './form-content'; -import AppButton from '.~/components/app-button'; -import useIsMounted from '.~/hooks/useIsMounted'; -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; -import useShippingTimes from '.~/hooks/useShippingTimes'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import getOfferFreeShippingInitialValue from '.~/utils/getOfferFreeShippingInitialValue'; -import useShippingRatesWithSavedSuggestions from './useShippingRatesWithSavedSuggestions'; -import { useAppDispatch } from '.~/data'; -import useSaveShippingRates from '.~/hooks/useSaveShippingRates'; - -/** - * Setup step to configure free listings. - * Auto-saves changes. - * - * @param {Object} props React props. - * @param {function(Object)} props.onContinue Callback called with form data once continue button is clicked. Could be async. While it's being resolved the form would turn into a saving state. - * @see /js/src/edit-free-campaign/setup-free-listings/index.js - */ -const SetupFreeListings = ( props ) => { - const { onContinue = () => {} } = props; - const { settings } = useSettings(); - const { - loading: loadingShippingRates, - data: dataShippingRates, - } = useShippingRatesWithSavedSuggestions(); - const { data: shippingTimesData } = useShippingTimes(); - const { - data: finalCountryCodesData, - } = useTargetAudienceFinalCountryCodes(); - const { saveSettings } = useAppDispatch(); - const { saveShippingRates } = useSaveShippingRates(); - const [ saving, setSaving ] = useState( false ); - const { createNotice } = useDispatchCoreNotices(); - const isMounted = useIsMounted(); - - if ( - ! settings || - loadingShippingRates || - ! shippingTimesData || - ! finalCountryCodesData - ) { - return ; - } - - /** - * Validation handler. - * - * We just return empty object here, - * because we call `checkErrors` in the rendering below, - * to accommodate for shippingRatesData and shippingTimesData from wp-data store. - * Apparently when shippingRates and shippingTimes are changed, - * the handleValidate function here does not get called. - * - * When we have shipping rates and shipping times as part of form values, - * then we can move `checkErrors` from inside rendering into this handleValidate function. - */ - const handleValidate = () => { - return {}; - }; - - const handleSubmitCallback = async ( values ) => { - /** - * Even though we already have auto-save effects in the FormContent, - * we are saving the form values here again to be sure, - * because the auto-save may not be fired - * when users were having focus on the text input fields - * and then immediately click on the Continue button. - */ - const { - shipping_country_rates: shippingRatesValue, - ...settingsValue - } = values; - - setSaving( true ); - - try { - await Promise.all( [ - saveSettings( settingsValue ), - saveShippingRates( shippingRatesValue ), - ] ); - onContinue(); - } catch ( error ) { - if ( isMounted() ) { - setSaving( false ); - } - - createNotice( - 'error', - __( - 'There is a problem in saving your settings and shipping rates. Please try again later.', - 'google-listings-and-ads' - ) - ); - } - }; - - return ( -
- -
- { ( formProps ) => { - const { values, handleSubmit } = formProps; - - const errors = checkErrors( - values, - shippingTimesData, - finalCountryCodesData - ); - - const isContinueDisabled = - Object.keys( errors ).length >= 1; - - return ( - - { __( - 'Continue', - 'google-listings-and-ads' - ) } - - } - /> - ); - } } - -
- ); -}; - -export default SetupFreeListings; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time-section.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time-section.js deleted file mode 100644 index 42be0920b1..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time-section.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Section from '.~/wcdl/section'; -import AppDocumentationLink from '.~/components/app-documentation-link'; -import ShippingTimeSetup from './shipping-time/shipping-time-setup'; - -/* - * @fires gla_documentation_link_click with `{ context: 'setup-mc-shipping', link_id: 'shipping-read-more', href: 'https://support.google.com/merchants/answer/7050921' }` - */ -const ShippingTimeSection = ( { formProps } ) => { - return ( -
-

- { __( - 'Your shipping times will be shown to potential customers on Google.', - 'google-listings-and-ads' - ) } -

-

- - { __( 'Read more', 'google-listings-and-ads' ) } - -

-
- } - > - - - - { __( - 'Estimated shipping times', - 'google-listings-and-ads' - ) } - - - - - - ); -}; - -export default ShippingTimeSection; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/index.js deleted file mode 100644 index 5661f797fc..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/index.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * External dependencies - */ -import { useState } from '@wordpress/element'; -import { Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { Form } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import AppModal from '.~/components/app-modal'; -import AppInputNumberControl from '.~/components/app-input-number-control'; -import VerticalGapLayout from '.~/components/vertical-gap-layout'; -import AudienceCountrySelect from '.~/components/audience-country-select'; -import { useAppDispatch } from '.~/data'; -import validateShippingTimeGroup from '.~/utils/validateShippingTimeGroup'; -import useGetRemainingCountryCodes from './useGetRemainingCountryCodes'; - -const AddTimeModal = ( props ) => { - const { onRequestClose } = props; - const { upsertShippingTimes } = useAppDispatch(); - const remainingCountryCodes = useGetRemainingCountryCodes(); - const [ dropdownVisible, setDropdownVisible ] = useState( false ); - - const handleSubmitCallback = ( values ) => { - upsertShippingTimes( { - countryCodes: values.countries, - time: values.time, - } ); - - onRequestClose(); - }; - - return ( -
- { ( formProps ) => { - const { getInputProps, isValidForm, handleSubmit } = formProps; - - return ( - - { __( 'Save', 'google-listings-and-ads' ) } - , - ] } - onRequestClose={ onRequestClose } - > - - - - - - ); - } } -
- ); -}; - -export default AddTimeModal; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/useGetRemainingCountryCodes.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/useGetRemainingCountryCodes.js deleted file mode 100644 index 3ba097201e..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/add-time-modal/useGetRemainingCountryCodes.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * External dependencies - */ -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_KEY } from '.~/data'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; - -/** - * Get the country codes that do not have any shipping time setup yet. - * This is done by comparing the selected country codes in Step 2 Choose Audience page - * and the shipping time setup in Step 3. - * - * @return {Array} array of country codes that do not have any shipping time setup yet. - */ -const useGetRemainingCountryCodes = () => { - const { data: selectedCountryCodes } = useTargetAudienceFinalCountryCodes(); - - const actual = useSelect( ( select ) => { - return select( STORE_KEY ) - .getShippingTimes() - .map( ( el ) => el.countryCode ); - }, [] ); - - if ( ! selectedCountryCodes ) { - return []; - } - - const actualSet = new Set( actual ); - const remaining = selectedCountryCodes.filter( - ( el ) => ! actualSet.has( el ) - ); - - return remaining; -}; - -export default useGetRemainingCountryCodes; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/index.js deleted file mode 100644 index d97edce7e7..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/add-time-button/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * External dependencies - */ -import { Button } from '@wordpress/components'; -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import GridiconPlusSmall from 'gridicons/dist/plus-small'; - -/** - * Internal dependencies - */ -import AddTimeModal from './add-time-modal'; - -const AddTimeButton = () => { - const [ isOpen, setOpen ] = useState( false ); - - const handleClick = () => { - setOpen( true ); - }; - - const handleModalRequestClose = () => { - setOpen( false ); - }; - - return ( - <> - - { isOpen && ( - - ) } - - ); -}; - -export default AddTimeButton; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/countriesTimeInput.test.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/countriesTimeInput.test.js deleted file mode 100644 index 42a4196662..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/countriesTimeInput.test.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import '@testing-library/jest-dom'; -import { render, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import CountriesTimeInputForm from './'; - -jest.mock( '.~/hooks/useStoreCurrency' ); - -jest.mock( '.~/hooks/useCountryKeyNameMap' ); - -jest.mock( '.~/hooks/useTargetAudienceFinalCountryCodes', () => () => ( { - data: { selectedCountryCodes: [ 'ES' ] }, -} ) ); - -const mockupsertShippingTimes = jest.fn(); - -jest.mock( '.~/data', () => ( { - useAppDispatch: () => ( { - upsertShippingTimes: mockupsertShippingTimes, - } ), -} ) ); - -describe( 'CountriesTimeInput', () => { - const defaultProps = { - savedValue: { - countries: [ 'ES' ], - time: '1', - }, - }; - - afterEach( () => { - jest.clearAllMocks(); - } ); - - test( 'Check if the saved time value is set in the input', () => { - render( ); - - const input = screen.getByRole( 'textbox' ); - expect( input ).toHaveValue( defaultProps.savedValue.time ); - } ); - - test( 'Check if the new value is updated without using the saved value & upsertShippingTimes is called', () => { - render( ); - - const input = screen.getByRole( 'textbox' ); - expect( input ).toHaveValue( defaultProps.savedValue.time ); - - userEvent.clear( input ); - userEvent.type( input, '2' ); - - fireEvent.blur( input ); - - expect( input ).toHaveValue( '2' ); - expect( mockupsertShippingTimes ).toHaveBeenCalledTimes( 1 ); - } ); - - test( 'Check when the saved time value is null and it has not been edited & upsertShippingTimes is called', () => { - render( - - ); - - const input = screen.getByRole( 'textbox' ); - fireEvent.blur( input ); - expect( input ).toHaveValue( '0' ); - expect( mockupsertShippingTimes ).toHaveBeenCalledTimes( 1 ); - } ); -} ); diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/index.js deleted file mode 100644 index a0e3891c48..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input-form/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * External dependencies - */ -import { useState, useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { useAppDispatch } from '.~/data'; -import CountriesTimeInput from '../countries-time-input'; -import useIsEqualRefValue from '.~/hooks/useIsEqualRefValue'; - -const CountriesTimeInputForm = ( props ) => { - const savedValue = useIsEqualRefValue( props.savedValue ); - const [ value, setValue ] = useState( savedValue ); - const { upsertShippingTimes } = useAppDispatch(); - - useEffect( () => { - setValue( savedValue ); - }, [ savedValue ] ); - - const handleBlur = ( event, numberValue ) => { - const { countries, time } = value; - - if ( time === numberValue ) { - return; - } - - setValue( { - countries, - time: numberValue, - } ); - - upsertShippingTimes( { countryCodes: countries, time: numberValue } ); - }; - - return ; -}; - -export default CountriesTimeInputForm; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/edit-time-modal/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/edit-time-modal/index.js deleted file mode 100644 index fd033ea6ab..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/edit-time-modal/index.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * External dependencies - */ -import { useState } from '@wordpress/element'; -import { Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { Form } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import AppModal from '.~/components/app-modal'; -import AppInputNumberControl from '.~/components/app-input-number-control'; -import VerticalGapLayout from '.~/components/vertical-gap-layout'; -import AudienceCountrySelect from '.~/components/audience-country-select'; -import { useAppDispatch } from '.~/data'; -import validateShippingTimeGroup from '.~/utils/validateShippingTimeGroup'; - -const EditTimeModal = ( props ) => { - const { time: groupedTime, onRequestClose } = props; - const { upsertShippingTimes, deleteShippingTimes } = useAppDispatch(); - const [ dropdownVisible, setDropdownVisible ] = useState( false ); - - const handleDeleteClick = () => { - deleteShippingTimes( groupedTime.countries ); - - onRequestClose(); - }; - - const handleSubmitCallback = ( values ) => { - upsertShippingTimes( { - countryCodes: values.countries, - time: values.time, - } ); - - const valuesCountrySet = new Set( values.countries ); - const deletedCountryCodes = groupedTime.countries.filter( - ( el ) => ! valuesCountrySet.has( el ) - ); - if ( deletedCountryCodes.length ) { - deleteShippingTimes( deletedCountryCodes ); - } - - onRequestClose(); - }; - - return ( -
- { ( formProps ) => { - const { getInputProps, isValidForm, handleSubmit } = formProps; - - return ( - - { __( 'Delete', 'google-listings-and-ads' ) } - , - , - ] } - onRequestClose={ onRequestClose } - > - - - - - - ); - } } -
- ); -}; - -export default EditTimeModal; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.js deleted file mode 100644 index b08af45c0d..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * External dependencies - */ -import { Button } from '@wordpress/components'; -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import EditTimeModal from './edit-time-modal'; -import './index.scss'; - -const EditTimeButton = ( props ) => { - const { time } = props; - const [ isOpen, setOpen ] = useState( false ); - - const handleClick = () => { - setOpen( true ); - }; - - const handleModalRequestClose = () => { - setOpen( false ); - }; - - return ( - <> - - { isOpen && ( - - ) } - - ); -}; - -export default EditTimeButton; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.scss b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.scss deleted file mode 100644 index fc84ba6563..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/edit-time-button/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -.gla-edit-time-button { - &.components-button.is-tertiary { - height: fit-content; - line-height: 1.4em; - padding: 0; - } -} diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.js deleted file mode 100644 index cb5cb49931..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import AppInputNumberControl from '.~/components/app-input-number-control'; -import AppSpinner from '.~/components/app-spinner'; -import ShippingTimeInputControlLabelText from '.~/components/shipping-time-input-control-label-text'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import EditTimeButton from './edit-time-button'; -import './index.scss'; - -const CountriesTimeInput = ( props ) => { - const { value, onBlur } = props; - const { countries, time } = value; - const { data: selectedCountryCodes } = useTargetAudienceFinalCountryCodes(); - - if ( ! selectedCountryCodes ) { - return ; - } - - return ( -
- - - -
- } - suffix={ __( 'days', 'google-listings-and-ads' ) } - value={ time } - onBlur={ onBlur } - /> - - ); -}; - -export default CountriesTimeInput; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.scss b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.scss deleted file mode 100644 index 02c95cced4..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/countries-time-input/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -.gla-countries-time-input { - .label { - display: flex; - justify-content: space-between; - gap: $grid-unit-05; - } -} diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/getCountriesTimeArray.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/getCountriesTimeArray.js deleted file mode 100644 index ef4c0e44b6..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/getCountriesTimeArray.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Groups shipping times based on time. - * - * Usage example: - * - * ```js - * const shippingTimes = [ - * { - * countryCode: 'US', - * time: 10 - * }, - * { - * countryCode: 'AU', - * time: 10 - * }, - * { - * countryCode: 'CN', - * time: 15 - * }, - * ] - * - * const result = getCountriesTimeArray( shippingTimes ); - * - * // result: - * // [ - * // { - * // countries: ['US', 'AU'], - * // time: 10 - * // }, - * // { - * // countries: ['CN'], - * // time: 15 - * // }, - * ] - * ``` - * - * @param {Array} shippingTimes Array of shipping times in the format of `{ countryCode, time }`. - */ -const getCountriesTimeArray = ( shippingTimes ) => { - const timeGroupMap = new Map(); - - shippingTimes.forEach( ( shippingTime ) => { - const { countryCode, time } = shippingTime; - const group = timeGroupMap.get( time ) || { - countries: [], - time, - }; - group.countries.push( countryCode ); - timeGroupMap.set( time, group ); - } ); - - return Array.from( timeGroupMap.values() ); -}; - -export default getCountriesTimeArray; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.js b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.js deleted file mode 100644 index c8771d07bd..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Internal dependencies - */ -import AppSpinner from '.~/components/app-spinner'; -import useShippingTimes from '.~/hooks/useShippingTimes'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import VerticalGapLayout from '.~/components/vertical-gap-layout'; -import AddTimeButton from './add-time-button'; -import getCountriesTimeArray from './getCountriesTimeArray'; -import CountriesTimeInputForm from './countries-time-input-form'; -import './index.scss'; - -const ShippingTimeSetup = () => { - const { data: shippingTimes } = useShippingTimes(); - const { data: selectedCountryCodes } = useTargetAudienceFinalCountryCodes(); - - if ( ! selectedCountryCodes ) { - return ; - } - - const expectedCountryCount = selectedCountryCodes.length; - const actualCountryCount = shippingTimes.length; - const remainingCount = expectedCountryCount - actualCountryCount; - - const countriesTimeArray = getCountriesTimeArray( shippingTimes ); - - // Prefill to-be-added time. - if ( countriesTimeArray.length === 0 ) { - countriesTimeArray.push( { - countries: selectedCountryCodes, - time: null, - } ); - } - - return ( -
- -
- - { countriesTimeArray.map( ( el ) => { - return ( -
- -
- ); - } ) } - { actualCountryCount >= 1 && remainingCount >= 1 && ( -
- -
- ) } -
-
-
-
- ); -}; - -export default ShippingTimeSetup; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.scss b/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.scss deleted file mode 100644 index 1c7518dd10..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/shipping-time/shipping-time-setup/index.scss +++ /dev/null @@ -1,15 +0,0 @@ -.gla-shipping-time-setup { - margin-top: calc(var(--main-gap) / 2); - - .countries-time { - margin-bottom: calc(var(--main-gap) / 2); - - .countries-time-input-form { - max-width: $gla-width-medium; - } - } - - .add-time-button { - align-self: flex-start; - } -} diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.js deleted file mode 100644 index 4f76102224..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { useAppDispatch } from '.~/data'; -import useDebouncedCallbackEffect from '.~/hooks/useDebouncedCallbackEffect'; -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; - -/** - * @typedef { import(".~/data/actions").SettingsData } SettingsData - */ - -/** - * Automatically save settings value upon value change with a debounce delay. - * It does not save on first render since the first render is the loading of value from API. - * - * @param {SettingsData} settings Settings value object to be saved. - */ -const useAutoSaveSettingsEffect = ( settings ) => { - const { saveSettings } = useAppDispatch(); - const { createNotice } = useDispatchCoreNotices(); - - /** - * A `saveSettingsCallback` callback that catches error and throws the error notice. - * - * @param {SettingsData} value Target audience value object to be saved. - */ - const saveSettingsCallback = async ( value ) => { - try { - await saveSettings( value ); - } catch ( error ) { - createNotice( - 'error', - __( - 'There was an error trying to save settings. Please try again later.', - 'google-listings-and-ads' - ) - ); - } - }; - - useDebouncedCallbackEffect( settings, saveSettingsCallback ); -}; - -export default useAutoSaveSettingsEffect; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.test.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.test.js deleted file mode 100644 index 8b1dbf227d..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveSettingsEffect.test.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * External dependencies - */ -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import useAutoSaveSettingsEffect from './useAutoSaveSettingsEffect'; - -const mockSaveSettings = jest.fn().mockName( 'saveSettings' ); -const mockCreateNotice = jest.fn().mockName( 'createNotice' ); - -jest.mock( '.~/data', () => ( { - useAppDispatch: () => ( { - saveSettings: mockSaveSettings, - } ), -} ) ); - -jest.mock( '.~/hooks/useDispatchCoreNotices', () => () => ( { - createNotice: mockCreateNotice, -} ) ); - -describe( 'useAutoSaveSettingsEffect', () => { - const initialSettings = { - shipping_rate: null, - tax_rate: null, - shipping_time: null, - }; - - const newSettings = { - shipping_rate: 'automatic', - tax_rate: null, - shipping_time: 'flat', - }; - - afterEach( () => { - jest.clearAllMocks(); - } ); - - test( 'Autosaving without errors', async () => { - const { rerender } = renderHook( - ( settings ) => useAutoSaveSettingsEffect( settings ), - { initialProps: initialSettings } - ); - - //Should not be call in the first render - await waitFor( () => { - expect( mockSaveSettings ).toHaveBeenCalledTimes( 0 ); - expect( mockCreateNotice ).toHaveBeenCalledTimes( 0 ); - } ); - - rerender( newSettings ); - - await waitFor( () => { - expect( mockSaveSettings ).toHaveBeenCalledTimes( 1 ); - expect( mockSaveSettings ).toHaveBeenCalledWith( newSettings ); - //No errors should be displayed - expect( mockCreateNotice ).toHaveBeenCalledTimes( 0 ); - } ); - } ); - - test( 'Autosaving with errors', async () => { - mockSaveSettings.mockImplementation( () => { - throw new Error( 'New error!' ); - } ); - - const { rerender } = renderHook( - ( settings ) => useAutoSaveSettingsEffect( settings ), - { initialProps: initialSettings } - ); - - rerender( newSettings ); - - await waitFor( () => { - expect( mockSaveSettings ).toHaveBeenCalledTimes( 1 ); - expect( mockSaveSettings ).toHaveBeenCalledWith( newSettings ); - - //Errors should be displayed - expect( mockCreateNotice ).toHaveBeenCalledTimes( 1 ); - expect( mockCreateNotice ).toHaveBeenCalledWith( - 'error', - 'There was an error trying to save settings. Please try again later.' - ); - } ); - } ); -} ); diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveShippingRatesEffect.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveShippingRatesEffect.js deleted file mode 100644 index 7858a08cc6..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useAutoSaveShippingRatesEffect.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Internal dependencies - */ -import useIsEqualRefValue from '.~/hooks/useIsEqualRefValue'; -import useDebouncedCallbackEffect from '.~/hooks/useDebouncedCallbackEffect'; -import useSaveShippingRates from '.~/hooks/useSaveShippingRates'; - -/** - * @typedef { import(".~/data/actions").ShippingRate } ShippingRate - */ - -const useAutoSaveShippingRatesEffect = ( shippingRates ) => { - const { saveShippingRates } = useSaveShippingRates(); - const shippingRatesRefValue = useIsEqualRefValue( shippingRates ); - - /** - * A `saveShippingRates` callback that catches error and do nothing. - * We don't want to show error messages for this auto save feature, - * and want it to fail silently in the background. - * - * @param {Array} value Shipping rates. - */ - const saveShippingRatesCallback = async ( value ) => { - try { - await saveShippingRates( value ); - } catch ( error ) {} - }; - - useDebouncedCallbackEffect( - shippingRatesRefValue, - saveShippingRatesCallback - ); -}; - -export default useAutoSaveShippingRatesEffect; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useSaveSuggestions.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useSaveSuggestions.js deleted file mode 100644 index a839d70a1f..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useSaveSuggestions.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * External dependencies - */ -import { useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; -import useSaveShippingRates from '.~/hooks/useSaveShippingRates'; - -/** - * A hook that returns a `saveSuggestions` callback. - * - * If there is an error during saving suggestions, - * it will display an error notice in the UI. - * - * @return {Function} `saveSuggestions` function to save suggestions as shipping rates. - */ -const useSaveSuggestions = () => { - const { createNotice } = useDispatchCoreNotices(); - const { saveShippingRates } = useSaveShippingRates(); - - const saveSuggestions = useCallback( - async ( suggestions ) => { - try { - await saveShippingRates( suggestions ); - } catch ( error ) { - createNotice( - 'error', - __( - `Unable to use your WooCommerce shipping settings as shipping rates in Google. You may have to enter shipping rates manually.`, - 'google-listings-and-ads' - ) - ); - } - }, - [ createNotice, saveShippingRates ] - ); - - return saveSuggestions; -}; - -export default useSaveSuggestions; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.js deleted file mode 100644 index ecf23d7dde..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * External dependencies - */ -import { addQueryArgs } from '@wordpress/url'; - -/** - * Internal dependencies - */ -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import useApiFetchEffect from '.~/hooks/useApiFetchEffect'; -import { API_NAMESPACE } from '.~/data/constants'; -import { SHIPPING_RATE_METHOD } from '.~/constants'; - -/** - * @typedef {Object} ShippingRatesSuggestionsResult - * @property {boolean} loading Whether loading is in progress. - * @property {Array?} data Shipping rates suggestions. - */ - -/** - * Get the shipping rates suggestions. - * - * This depends on the `useTargetAudienceFinalCountryCodes` hook, - * i.e. the target audience countres specified in Setup MC Step 2. - * - * This will only return shipping rates suggestions with FLAT_RATE method. - * Other methods (e.g. free shipping) are filtered out because - * they are not well supported in the API and UI yet. - * - * @return {ShippingRatesSuggestionsResult} Result object with `loading` and `data`. - */ -const useShippingRatesSuggestions = () => { - const { - loading: loadingFinalCountryCodes, - data: dataFinalCountryCodes, - } = useTargetAudienceFinalCountryCodes(); - - /** - * The API will only be called when `dataFinalCountryCodes` is truthy. - */ - const { - loading: loadingSuggestions, - data: dataSuggestions, - } = useApiFetchEffect( - dataFinalCountryCodes && { - path: addQueryArgs( - `${ API_NAMESPACE }/mc/shipping/rates/suggestions`, - { country_codes: dataFinalCountryCodes } - ), - } - ); - - /** - * Shipping rate suggestions data with only FLAT_RATE method. - * - * Other methods (e.g. free shipping) are filtered out because - * they are not well supported in the API and UI yet. - */ - const data = dataSuggestions?.filter( - ( el ) => el.method === SHIPPING_RATE_METHOD.FLAT_RATE - ); - - return { - loading: loadingFinalCountryCodes || loadingSuggestions, - data, - }; -}; - -export default useShippingRatesSuggestions; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.test.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.test.js deleted file mode 100644 index 66f9eef6a0..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesSuggestions.test.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * External dependencies - */ -import { renderHook } from '@testing-library/react-hooks'; - -/** - * Internal dependencies - */ -import useShippingRatesSuggestions from './useShippingRatesSuggestions'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import useApiFetchEffect from '.~/hooks/useApiFetchEffect'; - -jest.mock( '.~/hooks/useTargetAudienceFinalCountryCodes', () => jest.fn() ); -jest.mock( '.~/hooks/useApiFetchEffect', () => jest.fn() ); - -describe( 'useShippingRatesSuggestions', () => { - it( 'should return loading true when it is still loading target audience final country codes', () => { - useTargetAudienceFinalCountryCodes.mockReturnValue( { - loading: true, - } ); - useApiFetchEffect.mockReturnValue( { - loading: true, - } ); - - const { result } = renderHook( () => useShippingRatesSuggestions() ); - - expect( result.current.loading ).toBe( true ); - expect( result.current.data ).toBe( undefined ); - } ); - - it( 'should return loading true when it is still loading shipping rates suggestions', () => { - useTargetAudienceFinalCountryCodes.mockReturnValue( { - loading: false, - } ); - useApiFetchEffect.mockReturnValue( { - loading: true, - } ); - - const { result } = renderHook( () => useShippingRatesSuggestions() ); - - expect( result.current.loading ).toBe( true ); - expect( result.current.data ).toBe( undefined ); - } ); - - it( 'should return loading false with data when target audience final country codes and shipping rates suggestions are both loaded', () => { - useTargetAudienceFinalCountryCodes.mockReturnValue( { - loading: false, - data: [ 'GB', 'US', 'ES' ], - } ); - useApiFetchEffect.mockReturnValue( { - loading: false, - data: [ - { - country: 'GB', - method: 'flat_rate', - currency: 'US', - rate: 12, - options: {}, - }, - { - country: 'US', - method: 'flat_rate', - currency: 'US', - rate: 10, - options: {}, - }, - ], - } ); - - const { result } = renderHook( () => useShippingRatesSuggestions() ); - - expect( result.current.loading ).toBe( false ); - expect( result.current.data ).toStrictEqual( [ - { - country: 'GB', - method: 'flat_rate', - currency: 'US', - rate: 12, - options: {}, - }, - { - country: 'US', - method: 'flat_rate', - currency: 'US', - rate: 10, - options: {}, - }, - ] ); - } ); -} ); diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.js deleted file mode 100644 index 89ba9c5ca8..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * External dependencies - */ -import { useState, useRef, useCallback } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import useShippingRates from '.~/hooks/useShippingRates'; -import useShippingRatesSuggestions from './useShippingRatesSuggestions'; -import useSaveSuggestions from './useSaveSuggestions'; -import useCallbackOnceEffect from '.~/hooks/useCallbackOnceEffect'; - -/** - * @typedef {Object} ShippingRatesWithSavedSuggestionsResult - * @property {boolean} loading Whether loading is in progress. - * @property {Array?} data Shipping rates. - */ - -/** - * Check existing shipping rates, and if it is empty, get shipping rates suggestions - * and save the suggestions as shipping rates. - * - * @return {ShippingRatesWithSavedSuggestionsResult} Result object with `loading` and `data`. - */ -const useShippingRatesWithSavedSuggestions = () => { - const { - hasFinishedResolution: hfrShippingRates, - data: dataShippingRates, - } = useShippingRates(); - const { - loading: loadingSuggestions, - data: dataSuggestions, - } = useShippingRatesSuggestions(); - - /** - * `isInitialShippingRatesEmptyRef` is used to indicate - * whether the initial loaded shipping rates - * has a pre-saved value or not. - * - * If it does not have a pre-saved value, - * shipping rates should be an empty array, - * and we should save the suggestions as shipping rates. - * - * If it does have a pre-saved value, - * then we should not save the suggestions, - * even when users manually deleted all the pre-saved shipping rates value. - * The exception is when users deleted all the pre-saved value - * and then reload the page, - * then the suggestions would be saved as shipping rates as per above logic. - */ - const isInitialShippingRatesEmptyRef = useRef( undefined ); - if ( - hfrShippingRates && - isInitialShippingRatesEmptyRef.current === undefined - ) { - isInitialShippingRatesEmptyRef.current = dataShippingRates.length === 0; - } - - /** - * Boolean to indicate we should save suggestions, - * when the initial shipping rates is empty - * and we have suggestions data. - */ - const shouldSaveSuggestions = - isInitialShippingRatesEmptyRef.current && dataSuggestions; - - /** - * `saveSuggestionsFinished` is used to indicate whether saving has finished. - * This is only used when we have no pre-saved initial shipping rates value - * and we call `saveSuggestions`. It is initially set to `false`, - * and will be set to `true` after the suggestions are saved. - */ - const [ saveSuggestionsFinished, setSaveSuggestionsFinished ] = useState( - false - ); - const saveSuggestions = useSaveSuggestions(); - const callSaveSuggestions = useCallback( - async ( suggestions ) => { - await saveSuggestions( suggestions ); - setSaveSuggestionsFinished( true ); - }, - [ saveSuggestions ] - ); - - /** - * Call save suggestions with dataSuggestions for one time only - * when shouldSaveSuggestions is true. - */ - useCallbackOnceEffect( - shouldSaveSuggestions, - callSaveSuggestions, - dataSuggestions - ); - - return { - loading: - loadingSuggestions || - ! hfrShippingRates || - ( shouldSaveSuggestions && ! saveSuggestionsFinished ), - data: dataShippingRates, - }; -}; - -export default useShippingRatesWithSavedSuggestions; diff --git a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.test.js b/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.test.js deleted file mode 100644 index c9a2482144..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-free-listings/useShippingRatesWithSavedSuggestions.test.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * External dependencies - */ -import { renderHook } from '@testing-library/react-hooks'; - -/** - * Internal dependencies - */ -import useShippingRatesWithSavedSuggestions from './useShippingRatesWithSavedSuggestions'; -import useShippingRates from '.~/hooks/useShippingRates'; -import useShippingRatesSuggestions from './useShippingRatesSuggestions'; -import useSaveSuggestions from './useSaveSuggestions'; - -jest.mock( '.~/hooks/useShippingRates', () => jest.fn() ); -jest.mock( './useShippingRatesSuggestions', () => jest.fn() ); -jest.mock( './useSaveSuggestions', () => jest.fn() ); - -const shippingRatesData = [ - { - country: 'Malaysia', - country_code: 'MY', - currency: 'USD', - rate: '20', - }, -]; - -const shippingRatesSuggestionsData = [ - { - country: 'Malaysia', - country_code: 'MY', - currency: 'USD', - rate: 20, - }, -]; - -describe( 'useShippingRatesWithSavedSuggestions', () => { - it( 'should save suggestions as shipping rates when initial shipping rates is empty', async () => { - useShippingRates - .mockReturnValueOnce( { - hasFinishedResolution: false, - data: undefined, - } ) - .mockReturnValue( { - hasFinishedResolution: true, - data: [], - } ); - useShippingRatesSuggestions - .mockReturnValueOnce( { - loading: true, - data: undefined, - } ) - .mockReturnValueOnce( { - loading: true, - data: undefined, - } ) - .mockReturnValue( { - loading: false, - data: shippingRatesSuggestionsData, - } ); - const mockSaveSuggestions = jest.fn(); - useSaveSuggestions.mockReturnValue( mockSaveSuggestions ); - - const { result, rerender, waitForNextUpdate } = renderHook( () => - useShippingRatesWithSavedSuggestions() - ); - - /** - * Shipping rates and suggestions are loading. - */ - expect( result.current.loading ).toBe( true ); - expect( result.current.data ).toBe( undefined ); - expect( mockSaveSuggestions ).toHaveBeenCalledTimes( 0 ); - - /** - * Shipping rates are loaded; suggestions are loading. - */ - rerender(); - expect( result.current.loading ).toBe( true ); - expect( result.current.data ).toStrictEqual( [] ); - expect( mockSaveSuggestions ).toHaveBeenCalledTimes( 0 ); - - /** - * Shipping rates and suggestions are loaded, - * and saveSuggestions is called. - */ - rerender(); - await waitForNextUpdate(); - expect( result.current.loading ).toBe( false ); - expect( result.current.data ).toStrictEqual( [] ); - expect( mockSaveSuggestions ).toHaveBeenCalledTimes( 1 ); - } ); - - it( 'should not save suggestions as shipping rates when there is an initial shipping rates', async () => { - useShippingRates - .mockReturnValueOnce( { - hasFinishedResolution: false, - data: undefined, - } ) - .mockReturnValue( { - hasFinishedResolution: true, - data: shippingRatesData, - } ); - useShippingRatesSuggestions - .mockReturnValueOnce( { - loading: true, - data: undefined, - } ) - .mockReturnValueOnce( { - loading: true, - data: undefined, - } ) - .mockReturnValue( { - loading: false, - data: shippingRatesSuggestionsData, - } ); - const mockSaveSuggestions = jest.fn(); - useSaveSuggestions.mockReturnValue( mockSaveSuggestions ); - - const { result, rerender } = renderHook( () => - useShippingRatesWithSavedSuggestions() - ); - - /** - * Shipping rates and suggestions are loading. - */ - expect( result.current.loading ).toBe( true ); - expect( result.current.data ).toBe( undefined ); - expect( mockSaveSuggestions ).toHaveBeenCalledTimes( 0 ); - - /** - * Shipping rates are loaded; suggestions are loading. - */ - rerender(); - expect( result.current.loading ).toBe( true ); - expect( result.current.data ).toStrictEqual( shippingRatesData ); - expect( mockSaveSuggestions ).toHaveBeenCalledTimes( 0 ); - - /** - * Shipping rates and suggestions are loaded, - * and saveSuggestions is not called. - */ - rerender(); - expect( result.current.loading ).toBe( false ); - expect( result.current.data ).toStrictEqual( shippingRatesData ); - expect( mockSaveSuggestions ).toHaveBeenCalledTimes( 0 ); - } ); -} ); From fdf10dcde4fc4d949a4e4e895dc5f06e714a1b42 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 4 Aug 2022 16:04:53 +0800 Subject: [PATCH 007/140] Remove an unused npm package use-debounce --- .eslintrc.js | 6 ------ package-lock.json | 5 ----- package.json | 3 +-- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d90f5208e5..55e5f33d8d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,12 +10,6 @@ const webpackResolver = { * Ref: https://webpack.js.org/configuration/resolve/#resolveextensions */ extensions: [ '.js' ], - /** - * Make eslint be able to resolve the exports config of `use-debounce`. - * The `exports` config of package.json doesn't work before the current eslint support it. - * Ref: https://github.com/xnimorz/use-debounce/blob/5.2.0/package.json#L8-L14 - */ - conditionNames: [ 'import', 'require' ], }, }, }; diff --git a/package-lock.json b/package-lock.json index 76da8cb348..dfed8d4b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40269,11 +40269,6 @@ "ts-essentials": "^2.0.3" } }, - "use-debounce": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-5.2.1.tgz", - "integrity": "sha512-BQG5uEypYHd/ASF6imzYR8tJHh5qGn28oZG/5iVAbljV6MUrfyT4jzxA8co+L+WLCT1U8VBwzzvlb3CHmUDpEA==" - }, "use-enhanced-state": { "version": "0.0.13", "resolved": "https://registry.npmjs.org/use-enhanced-state/-/use-enhanced-state-0.0.13.tgz", diff --git a/package.json b/package.json index 8ec2e02af0..1cac3aa39c 100644 --- a/package.json +++ b/package.json @@ -99,8 +99,7 @@ "libphonenumber-js": "^1.9.22", "lodash": "^4.17.20", "prop-types": "^15.7.2", - "rememo": "^3.0.0", - "use-debounce": "^5.2.0" + "rememo": "^3.0.0" }, "config": { "wp_org_slug": "google-listings-and-ads", From 159bd2da14a5c73c7262e4b8295b2b6d9982f1f8 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 5 Aug 2022 14:35:46 +0800 Subject: [PATCH 008/140] Add the CountryCode type back to SetupFreeListings --- js/src/components/free-listings/setup-free-listings/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/src/components/free-listings/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js index 38b6176e06..ec9e39cfac 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -18,6 +18,7 @@ import FormContent from './form-content'; * @typedef {import('.~/data/actions').TargetAudienceData } TargetAudienceData * @typedef {import('.~/data/actions').ShippingRate} ShippingRateFromServerSide * @typedef {import('.~/data/actions').ShippingTime} ShippingTime + * @typedef {import('.~/data/actions').CountryCode} CountryCode */ const targetAudienceFields = [ 'locale', 'language', 'location', 'countries' ]; @@ -62,7 +63,7 @@ const getSettings = ( values ) => { * * @param {Object} props * @param {TargetAudienceData} props.targetAudience Target audience value data to be initialed the form, if not given AppSpinner will be rendered. - * @param {(targetAudience: TargetAudienceData) => Array} props.resolveFinalCountries Callback for this component to resolve the given `targetAudience` to the final list of countries. + * @param {(targetAudience: TargetAudienceData) => Array} props.resolveFinalCountries Callback for this component to resolve the given `targetAudience` to the final list of countries. * @param {(targetAudience: TargetAudienceData) => void} [props.onTargetAudienceChange] Callback called with new data once target audience data is changed. Forwarded from and {@link Form.Props.onChange}. * @param {Object} props.settings Settings data, if not given AppSpinner will be rendered. * @param {(newValue: Object) => void} [props.onSettingsChange] Callback called with new data once form data is changed. Forwarded from and {@link Form.Props.onChange}. From 43a2f76deda2218b5943be595bb6a1d0b1c5ee47 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Mon, 8 Aug 2022 16:47:51 +0800 Subject: [PATCH 009/140] Add auto-saving for MC settings to fall back with the original implementation --- .../free-listings/setup-free-listings/index.js | 4 ++-- .../setup-mc/setup-stepper/saved-setup-stepper.js | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/js/src/components/free-listings/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js index ec9e39cfac..542566a2be 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -145,8 +145,8 @@ const SetupFreeListings = ( { location: targetAudience.location, countries: targetAudience.countries || [], // These are the fields for settings. - shipping_rate: settings.shipping_rate || 'automatic', - shipping_time: settings.shipping_time || 'flat', + shipping_rate: settings.shipping_rate, + shipping_time: settings.shipping_time, tax_rate: settings.tax_rate, website_live: settings.website_live, checkout_process_secure: settings.checkout_process_secure, diff --git a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js index 78598f551a..4e902b2a7a 100644 --- a/js/src/setup-mc/setup-stepper/saved-setup-stepper.js +++ b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js @@ -64,6 +64,18 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { } }, [ targetAudience, suggestedAudience, saveTargetAudience ] ); + // Auto-save the default values for shipping options to fall back with the original implementation. + // Ref: https://github.com/woocommerce/google-listings-and-ads/blob/2.0.2/js/src/setup-mc/setup-stepper/setup-free-listings/form-content.js#L33 + useEffect( () => { + if ( settings?.shipping_rate === null ) { + saveSettings( { + ...settings, + shipping_rate: 'automatic', + shipping_time: 'flat', + } ); + } + }, [ settings, saveSettings ] ); + const handleSetupAccountsContinue = () => { recordEvent( 'gla_setup_mc', { target: 'step1_continue', @@ -95,6 +107,7 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { const initShippingRates = hasResolvedShippingRates ? shippingRates : null; const initShippingTimes = hasResolvedShippingTimes ? shippingTimes : null; const initTargetAudience = targetAudience?.location ? targetAudience : null; + const initSettings = settings?.shipping_rate ? settings : null; return ( {} } ) => { content: ( Date: Fri, 5 Aug 2022 12:57:43 +0800 Subject: [PATCH 010/140] Adjust layouts and copywriting for the Get Started page --- .../get-started-with-video-card/index.js | 29 +++++------- .../get-started-with-video-card/index.scss | 45 +++++-------------- 2 files changed, 21 insertions(+), 53 deletions(-) diff --git a/js/src/get-started-page/get-started-with-video-card/index.js b/js/src/get-started-page/get-started-with-video-card/index.js index 5778aefd0f..b28491fc48 100644 --- a/js/src/get-started-page/get-started-with-video-card/index.js +++ b/js/src/get-started-page/get-started-with-video-card/index.js @@ -3,7 +3,6 @@ */ import { Card, - CardHeader, CardBody, FlexBlock, Tip, @@ -19,7 +18,6 @@ import { glaData } from '.~/constants'; import AppButton from '.~/components/app-button'; import AppDocumentationLink from '.~/components/app-documentation-link'; import WistiaVideo from '.~/components/wistia-video'; -import googleLogoURL from '.~/images/google-logo.svg'; import { getSetupMCUrl } from '.~/utils/urls'; import './index.scss'; @@ -32,20 +30,6 @@ const GetStartedWithVideoCard = () => { return ( - -

- { __( - 'The official extension for WooCommerce, built in collaboration with', - 'google-listings-and-ads' - ) } -

- { -
{ /> + + { __( + 'The official extension for WooCommerce, built in collaboration with Google', + 'google-listings-and-ads' + ) } + { __( - 'Reach new customers across Google with free product listings and paid ads', + 'Reach millions of shoppers with product listings on Google', 'google-listings-and-ads' ) } @@ -68,7 +61,7 @@ const GetStartedWithVideoCard = () => { className="gla-get-started-with-video-card__description" > { __( - 'Sync your products directly to Google, manage your product feed, and create Google Ad campaigns–without leaving your WooCommerce dashboard. The official extension, built in collaboration with Google.', + 'Sync your products directly to Google, manage your product feed, and create Google Ad campaigns — all without leaving your WooCommerce dashboard.', 'google-listings-and-ads' ) } diff --git a/js/src/get-started-page/get-started-with-video-card/index.scss b/js/src/get-started-page/get-started-with-video-card/index.scss index f19558b8bd..7aed7d05e4 100644 --- a/js/src/get-started-page/get-started-with-video-card/index.scss +++ b/js/src/get-started-page/get-started-with-video-card/index.scss @@ -1,35 +1,6 @@ .gla-get-started-with-video-card { flex-direction: column; - .components-flex { - // Adjust imported from @wordpress/components. - &.components-card__header { - justify-content: flex-end; - padding: $grid-unit * 2.5; - - & > p { - margin: 0 1em; - line-height: $gla-line-height-smaller; - font-size: $helptext-font-size; - color: $gray-600; - } - - @media (max-width: $break-small) { - justify-content: center; - text-align: center; - padding: $grid-unit-20; - - & > p::after { - content: " Google"; - } - - & > img { - display: none; - } - } - } - } - .motivation-video { min-width: 100%; margin: 0; @@ -42,7 +13,7 @@ flex-direction: column; align-items: center; text-align: center; - margin: calc(var(--main-gap) * 1.67) calc(var(--main-gap) * 4.67); + margin: calc(var(--main-gap) * 1.67) 0 var(--main-gap); padding: 0; @media (max-width: $break-small) { @@ -50,16 +21,20 @@ } } + & &__caption { + margin-bottom: $grid-unit-20; + color: $gray-600; + } + // Use more specific rules to make it higher priority to override component. & &__title { - padding: 0 $grid-unit-30; - margin-bottom: $grid-unit-15; + margin-bottom: $grid-unit-20; } // Use more specific rules to make it higher priority to override component. & &__description { - margin-bottom: $grid-unit-40; - padding: 0 $grid-unit-10; + max-width: $gla-width-large; + margin-bottom: $grid-unit-30; } // Use more specific rules to make it higher priority to override component. @@ -76,7 +51,7 @@ & &__hint { font-size: $helptext-font-size; color: $gray-400; - margin-bottom: $grid-unit-40; + margin-bottom: $grid-unit-30; } // Use more specific rules to make it higher priority to override component. From 0980e14484e58e74117ee2e009aa4dbb82491e88 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 5 Aug 2022 14:49:21 +0800 Subject: [PATCH 011/140] Adjust layouts for StepContentHeader --- js/src/components/stepper/step-content-header/index.js | 4 +++- js/src/components/stepper/step-content-header/index.scss | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/js/src/components/stepper/step-content-header/index.js b/js/src/components/stepper/step-content-header/index.js index d2cd762dc4..81dfe0b7c8 100644 --- a/js/src/components/stepper/step-content-header/index.js +++ b/js/src/components/stepper/step-content-header/index.js @@ -9,7 +9,9 @@ const StepContentHeader = ( props ) => { return (

{ title }

-
{ description }
+
+ { description } +
); }; diff --git a/js/src/components/stepper/step-content-header/index.scss b/js/src/components/stepper/step-content-header/index.scss index 058e8c4bcb..b69de053ac 100644 --- a/js/src/components/stepper/step-content-header/index.scss +++ b/js/src/components/stepper/step-content-header/index.scss @@ -2,7 +2,7 @@ display: flex; flex-direction: column; align-items: center; - max-width: 487px; + max-width: $gla-width-large; text-align: center; margin: auto; gap: $grid-unit-15; @@ -10,13 +10,10 @@ h1 { font-size: $gla-font-large; - font-weight: 400; padding: 0; } - .description { - font-size: $default-font-size; - font-weight: 400; - margin: 0; + &__description { + line-height: $gla-line-height-smaller; } } From d5510e55eee66efe30709299964214291720adcc Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 5 Aug 2022 12:54:41 +0800 Subject: [PATCH 012/140] Change the logos of Google MC and Ads account cards --- .../account-card/google-ads-logo.svg | 5 ++++ .../google-merchant-center-logo.svg | 30 +++++++++++++++++++ js/src/components/account-card/index.js | 24 +++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 js/src/components/account-card/google-ads-logo.svg create mode 100644 js/src/components/account-card/google-merchant-center-logo.svg diff --git a/js/src/components/account-card/google-ads-logo.svg b/js/src/components/account-card/google-ads-logo.svg new file mode 100644 index 0000000000..8a0eb0dfb4 --- /dev/null +++ b/js/src/components/account-card/google-ads-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/js/src/components/account-card/google-merchant-center-logo.svg b/js/src/components/account-card/google-merchant-center-logo.svg new file mode 100644 index 0000000000..45d22c5a6b --- /dev/null +++ b/js/src/components/account-card/google-merchant-center-logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/src/components/account-card/index.js b/js/src/components/account-card/index.js index 07f7003a60..c5aae22944 100644 --- a/js/src/components/account-card/index.js +++ b/js/src/components/account-card/index.js @@ -13,6 +13,8 @@ import { Icon, store as storeIcon } from '@wordpress/icons'; import Section from '.~/wcdl/section'; import Subsection from '.~/wcdl/subsection'; import googleLogoURL from './gogole-g-logo.svg'; +import googleMCLogoURL from './google-merchant-center-logo.svg'; +import googleAdsLogoURL from './google-ads-logo.svg'; import wpLogoURL from './wp-logo.svg'; import './index.scss'; @@ -40,6 +42,24 @@ const googleLogo = ( /> ); +const googleMCLogo = ( + { +); + +const googleAdsLogo = ( + { +); + const wpLogo = ( Date: Fri, 5 Aug 2022 14:47:48 +0800 Subject: [PATCH 013/140] Adjust layouts and copywriting for the accounts setup step --- .../create-account-card.js | 2 +- .../google-mc-account-card/disabled-card.js | 2 +- .../setup-accounts/google-mc-account/index.js | 48 ------------------- .../google-mc-account/index.scss | 44 ----------------- .../setup-stepper/setup-accounts/index.js | 6 +-- 5 files changed, 5 insertions(+), 97 deletions(-) delete mode 100644 js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.js delete mode 100644 js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.scss diff --git a/js/src/components/google-mc-account-card/create-account-card.js b/js/src/components/google-mc-account-card/create-account-card.js index b1de08d296..b3c4fb5d70 100644 --- a/js/src/components/google-mc-account-card/create-account-card.js +++ b/js/src/components/google-mc-account-card/create-account-card.js @@ -20,7 +20,7 @@ const CreateAccountCard = ( props ) => { isSecondary onCreateAccount={ onCreateAccount } > - { __( 'Create Account', 'google-listings-and-ads' ) } + { __( 'Create account', 'google-listings-and-ads' ) } } /> diff --git a/js/src/components/google-mc-account-card/disabled-card.js b/js/src/components/google-mc-account-card/disabled-card.js index 4b0be4ef02..708bfb5a85 100644 --- a/js/src/components/google-mc-account-card/disabled-card.js +++ b/js/src/components/google-mc-account-card/disabled-card.js @@ -16,7 +16,7 @@ const DisabledCard = () => { appearance={ APPEARANCE.GOOGLE_MERCHANT_CENTER } indicator={ } /> diff --git a/js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.js b/js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.js deleted file mode 100644 index 76b33182b3..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Tip } from '@wordpress/components'; -import { createInterpolateElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import Section from '.~/wcdl/section'; -import AppTooltip from '.~/components/app-tooltip'; -import GoogleMCAccountCard from '.~/components/google-mc-account-card'; -import './index.scss'; - -const GoogleMCAccount = () => { - return ( -
-
- { createInterpolateElement( - __( - 'Google’s Shopping tab has an average of over 50% increase in clicks and over 100% increase in impressions across free listings and ads. Source', - 'google-listings-and-ads' - ), - { - strong: , - tooltip: ( - - ), - } - ) } - - } - > - -
-
- ); -}; - -export default GoogleMCAccount; diff --git a/js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.scss b/js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.scss deleted file mode 100644 index b5baf3acad..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-accounts/google-mc-account/index.scss +++ /dev/null @@ -1,44 +0,0 @@ -.gla-google-mc-account { - .wcdl-section { - header { - box-shadow: inset 0 1px 0 $gray-300; - padding-top: var(--main-gap); - - .components-tip { - svg { - /* - * Original values below are: - * align-self: center; - * fill: $alert-yellow; - * - * See: https://github.com/WordPress/gutenberg/blob/ba5f6d061934625027beab9d2bf600ae42a63be9/packages/components/src/tip/style.scss. - */ - align-self: flex-start; - fill: currentcolor; - } - - .app-tooltip__children-container { - /* - * Make the "Source" text color blue. - */ - color: #007cba; - - .components-popover { - &__content { - /* - * Specify these so that the text wraps around within a box with 250px width. - * Without these, the popover text will show up in one long horizontal line. - * Original values below are: - * width: (not specified); - * white-space: nowrap; - */ - width: $gla-width-small; - white-space: normal; - text-align: left; - } - } - } - } - } - } -} diff --git a/js/src/setup-mc/setup-stepper/setup-accounts/index.js b/js/src/setup-mc/setup-stepper/setup-accounts/index.js index eee3267245..b0d0c8dc34 100644 --- a/js/src/setup-mc/setup-stepper/setup-accounts/index.js +++ b/js/src/setup-mc/setup-stepper/setup-accounts/index.js @@ -18,7 +18,7 @@ import Section from '.~/wcdl/section'; import VerticalGapLayout from '.~/components/vertical-gap-layout'; import WPComAccountCard from '.~/components/wpcom-account-card'; import GoogleAccountCard from '.~/components/google-account-card'; -import GoogleMCAccount from './google-mc-account'; +import GoogleMCAccountCard from '.~/components/google-mc-account-card'; import Faqs from './faqs'; const SetupAccounts = ( props ) => { @@ -57,7 +57,7 @@ const SetupAccounts = ( props ) => { 'google-listings-and-ads' ) } description={ __( - 'Connect your WordPress.com account, Google account, and Google Merchant Center account to use Google Listings & Ads.', + 'Connect the accounts required to use Google Listings & Ads.', 'google-listings-and-ads' ) } /> @@ -71,9 +71,9 @@ const SetupAccounts = ( props ) => { + - +
); From 6518b794df74702876adbf830d7ffeef5efe0fac Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 26 Aug 2022 17:53:42 +0800 Subject: [PATCH 052/140] Tweak UI for the AudienceSection component --- js/src/components/paid-ads/audience-section.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/js/src/components/paid-ads/audience-section.js b/js/src/components/paid-ads/audience-section.js index b4e8e7d751..a9c763abac 100644 --- a/js/src/components/paid-ads/audience-section.js +++ b/js/src/components/paid-ads/audience-section.js @@ -32,7 +32,10 @@ const AudienceSection = ( props ) => { formProps: { getInputProps }, multiple = true, disabled = false, - countrySelectHelperText, + countrySelectHelperText = __( + 'You can only choose from countries you’ve selected during product listings configuration.', + 'google-listings-and-ads' + ), } = props; const countryNameMap = useCountryKeyNameMap(); @@ -40,7 +43,7 @@ const AudienceSection = ( props ) => { const selector = multiple ? ( { return (
{ __( - 'Choose where do you want your product ads to appear.', + 'Choose where you want your product ads to appear', 'google-listings-and-ads' ) }

From 0947ec30e44e9cda66a1f9d5ead325b1cf4eaf79 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 26 Aug 2022 17:54:46 +0800 Subject: [PATCH 053/140] Tweak UI for the BudgetSection-related components --- js/src/components/account-card/index.scss | 1 + .../budget-recommendation/index.js | 16 +++++----- .../budget-recommendation/index.scss | 31 +++++++++++++------ .../paid-ads/budget-section/index.js | 28 +++-------------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/js/src/components/account-card/index.scss b/js/src/components/account-card/index.scss index bb5855ce96..583373a056 100644 --- a/js/src/components/account-card/index.scss +++ b/js/src/components/account-card/index.scss @@ -23,6 +23,7 @@ &__description { display: flex; flex-direction: column; + align-items: flex-start; gap: 1em; color: $gray-900; diff --git a/js/src/components/paid-ads/budget-section/budget-recommendation/index.js b/js/src/components/paid-ads/budget-section/budget-recommendation/index.js index dd49291eb7..450cf48273 100644 --- a/js/src/components/paid-ads/budget-section/budget-recommendation/index.js +++ b/js/src/components/paid-ads/budget-section/budget-recommendation/index.js @@ -3,7 +3,8 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; -import GridiconInfoOutline from 'gridicons/dist/info-outline'; +import { Tip } from '@wordpress/components'; +import GridiconNoticeOutline from 'gridicons/dist/notice-outline'; /** * Internal dependencies @@ -30,16 +31,16 @@ function getHighestBudget( recommendations ) { } function toRecommendationRange( isMultiple, ...values ) { - const conversionMap = { strong: }; + const conversionMap = { strong: , em: , br:
}; const template = isMultiple ? // translators: it's a range of recommended budget amount. 1: the low value of the range, 2: the high value of the range, 3: the currency of amount. __( - 'Most merchants targeting similar countries set a daily budget of %1$f to %2$f %3$s for approximately 10 conversions a week.', + 'Google will optimize your ads to maximize performance across the country/s you select.
Tip: Most merchants targeting similar countries set a daily budget of %1$f to %2$f %3$s', 'google-listings-and-ads' ) : // translators: it's a range of recommended budget amount. 1: the low value of the range, 2: the high value of the range, 3: the currency of amount, 4: a country name selected by the merchant. __( - 'Most merchants targeting %4$s set a daily budget of %1$f to %2$f %3$s for approximately 10 conversions a week.', + 'Google will optimize your ads to maximize performance across the country/s you select.
Tip: Most merchants targeting %4$s set a daily budget of %1$f to %2$f %3$s', 'google-listings-and-ads' ); @@ -78,13 +79,9 @@ const BudgetRecommendation = ( props ) => { return (
-
- -
{ recommendationRange }
-
{ showLowerBudgetNotice && (
- +
{ __( 'With a budget lower than your competitor range, your campaign may not get noticeable results.', @@ -93,6 +90,7 @@ const BudgetRecommendation = ( props ) => {
) } + { recommendationRange }
); }; diff --git a/js/src/components/paid-ads/budget-section/budget-recommendation/index.scss b/js/src/components/paid-ads/budget-section/budget-recommendation/index.scss index d4151b5e73..cf6aae7e72 100644 --- a/js/src/components/paid-ads/budget-section/budget-recommendation/index.scss +++ b/js/src/components/paid-ads/budget-section/budget-recommendation/index.scss @@ -1,27 +1,38 @@ .gla-budget-recommendation { - font-style: italic; - - &__recommendation, &__low-budget { display: flex; + align-items: center; gap: calc(var(--main-gap) / 3); + margin-bottom: calc(var(--main-gap) / 2); + font-style: italic; > svg { - flex: 1 0 auto; + flex: 0 0 auto; } } - &__recommendation { - margin-bottom: calc(var(--main-gap) / 2); + .components-tip { + padding: $grid-unit-15 $grid-unit-20; + background-color: #f0f6fc; + + > p { + line-height: $gla-line-height-medium; + font-size: inherit; + } - svg { - fill: $gray-600; + > svg { + align-self: initial; + margin: $grid-unit-05 $grid-unit-05 * 2.5 0 0; } } + .components-tip, &__low-budget { - svg { - fill: $alert-yellow; + font-size: $gla-font-smaller; + color: $black; + + > svg { + fill: $gray-900; } } } diff --git a/js/src/components/paid-ads/budget-section/index.js b/js/src/components/paid-ads/budget-section/index.js index 56a3e1e207..917f0cb8bf 100644 --- a/js/src/components/paid-ads/budget-section/index.js +++ b/js/src/components/paid-ads/budget-section/index.js @@ -53,29 +53,11 @@ const BudgetSection = ( props ) => {
-

- { __( - 'Enter a daily average cost that works best for your business and the results that you want. You can change your budget or cancel your ad at any time.', - 'google-listings-and-ads' - ) } -

-

- { __( - 'You will be billed directly by Google Ads.', - 'google-listings-and-ads' - ) } -

-

- { __( - 'Google will optimize your ads to maximize performance across your selected country(s).', - 'google-listings-and-ads' - ) } -

- - } + title={ __( 'Set your budget', 'google-listings-and-ads' ) } + description={ __( + 'With Performance Max campaigns, you can set your own budget and Google’s Smart Bidding technology will serve the most appropriate ad, with the optimal bid, to maximize campaign performance.', + 'google-listings-and-ads' + ) } > From e38a58ad98ec37fdabff136b0a9fe4235a001f7a Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 26 Aug 2022 17:57:30 +0800 Subject: [PATCH 054/140] Fix that the validation of campaign `amount` should only allow a number type (excluding NaN) --- .../__snapshots__/validateForm.test.js.snap | 15 ++++ js/src/utils/paid-ads/validateForm.js | 2 +- js/src/utils/paid-ads/validateForm.test.js | 84 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 js/src/utils/paid-ads/__snapshots__/validateForm.test.js.snap create mode 100644 js/src/utils/paid-ads/validateForm.test.js diff --git a/js/src/utils/paid-ads/__snapshots__/validateForm.test.js.snap b/js/src/utils/paid-ads/__snapshots__/validateForm.test.js.snap new file mode 100644 index 0000000000..e1149b730b --- /dev/null +++ b/js/src/utils/paid-ads/__snapshots__/validateForm.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validateForm When the amount is not a number, should not pass 1`] = `"Please make sure daily average cost is greater than 0."`; + +exports[`validateForm When the amount is not a number, should not pass 2`] = `"Please make sure daily average cost is greater than 0."`; + +exports[`validateForm When the amount is not a number, should not pass 3`] = `"Please make sure daily average cost is greater than 0."`; + +exports[`validateForm When the amount is not a number, should not pass 4`] = `"Please make sure daily average cost is greater than 0."`; + +exports[`validateForm When the amount is ≤ 0, should not pass 1`] = `"Please make sure daily average cost is greater than 0."`; + +exports[`validateForm When the amount is ≤ 0, should not pass 2`] = `"Please make sure daily average cost is greater than 0."`; + +exports[`validateForm When the country codes array is empty, should not pass 1`] = `"Please select at least one country for your ads campaign."`; diff --git a/js/src/utils/paid-ads/validateForm.js b/js/src/utils/paid-ads/validateForm.js index 722b1cecba..3735ea9ffd 100644 --- a/js/src/utils/paid-ads/validateForm.js +++ b/js/src/utils/paid-ads/validateForm.js @@ -25,7 +25,7 @@ const validateForm = ( values ) => { ); } - if ( values.amount <= 0 ) { + if ( ! Number.isFinite( values.amount ) || values.amount <= 0 ) { errors.amount = __( 'Please make sure daily average cost is greater than 0.', 'google-listings-and-ads' diff --git a/js/src/utils/paid-ads/validateForm.test.js b/js/src/utils/paid-ads/validateForm.test.js new file mode 100644 index 0000000000..4989bf7fda --- /dev/null +++ b/js/src/utils/paid-ads/validateForm.test.js @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import validateForm from './validateForm'; + +/** + * `validateForm` function returns an object, and if any checks are not passed, + * set properties respectively with an error message to indicate it. + */ +describe( 'validateForm', () => { + let values; + + beforeEach( () => { + // Initial values + values = { countryCodes: [], amount: 0 }; + } ); + + it( 'When all checks are passed, should return an empty object', () => { + const errors = validateForm( { + countryCodes: [ 'US' ], + amount: 1, + } ); + + expect( errors ).toStrictEqual( {} ); + } ); + + it( 'should indicate multiple unpassed checks by setting properties in the returned object', () => { + const errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'countryCodes' ); + expect( errors ).toHaveProperty( 'amount' ); + } ); + + it( 'When the country codes array is empty, should not pass', () => { + const errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'countryCodes' ); + expect( errors.countryCodes ).toMatchSnapshot(); + } ); + + it( 'When the amount is not a number, should not pass', () => { + let errors; + + values.amount = ''; + errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'amount' ); + expect( errors.amount ).toMatchSnapshot(); + + values.amount = undefined; + errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'amount' ); + expect( errors.amount ).toMatchSnapshot(); + + values.amount = new Date(); + errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'amount' ); + expect( errors.amount ).toMatchSnapshot(); + + values.amount = NaN; + errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'amount' ); + expect( errors.amount ).toMatchSnapshot(); + } ); + + it( 'When the amount is ≤ 0, should not pass', () => { + let errors; + + values.amount = 0; + errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'amount' ); + expect( errors.amount ).toMatchSnapshot(); + + values.amount = -0.01; + errors = validateForm( values ); + + expect( errors ).toHaveProperty( 'amount' ); + expect( errors.amount ).toMatchSnapshot(); + } ); +} ); From 3e0debdc862d734ea3d46ceb7782285474344ea2 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Fri, 26 Aug 2022 18:09:11 +0800 Subject: [PATCH 055/140] Add the ads audience and budget sections to step 4 of the onboarding flow for paid campaign creation --- .../setup-paid-ads/setup-paid-ads.js | 67 ++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js index e7cd7ca2d1..ecadc75682 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js @@ -5,12 +5,15 @@ import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { useState } from '@wordpress/element'; import { Flex } from '@wordpress/components'; +import { Form } from '@woocommerce/components'; /** * Internal dependencies */ import useAdminUrl from '.~/hooks/useAdminUrl'; import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; +import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; +import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; import StepContent from '.~/components/stepper/step-content'; import StepContentHeader from '.~/components/stepper/step-content-header'; import StepContentFooter from '.~/components/stepper/step-content-footer'; @@ -19,19 +22,64 @@ import AppButton from '.~/components/app-button'; import ProductFeedStatusSection from './product-feed-status-section'; import PaidAdsFeaturesSection from './paid-ads-features-section'; import GoogleAdsAccountSection from './google-ads-account-section'; +import AudienceSection from '.~/components/paid-ads/audience-section'; +import BudgetSection from '.~/components/paid-ads/budget-section'; +import validateForm from '.~/utils/paid-ads/validateForm'; import { getProductFeedUrl } from '.~/utils/urls'; import { GUIDE_NAMES } from '.~/constants'; import { API_NAMESPACE } from '.~/data/constants'; -function PaidAdsSectionsGroup() { - // TODO: Add audience and budget sections. - return ; +function PaidAdsSectionsGroup( { onCampaignChange } ) { + const { googleAdsAccount } = useGoogleAdsAccount(); + const { data: targetAudience } = useTargetAudienceFinalCountryCodes(); + + if ( ! targetAudience ) { + return ; + } + + const handleChange = ( _, values, isValid ) => { + onCampaignChange( { ...values, isValid } ); + }; + + return ( +
+ { ( formProps ) => { + const { countryCodes } = formProps.values; + const disabledAudience = + googleAdsAccount?.status !== 'connected'; + const disabledBudget = + disabledAudience || countryCodes.length === 0; + + return ( + <> + + + + + ); + } } + + ); } export default function SetupPaidAds() { const adminUrl = useAdminUrl(); const { createNotice } = useDispatchCoreNotices(); const [ showPaidAdsSetup, setShowPaidAdsSetup ] = useState( false ); + const [ campaign, setCampaign ] = useState( {} ); const [ completing, setCompleting ] = useState( null ); const finishFreeListingsSetup = async ( event ) => { @@ -64,11 +112,10 @@ export default function SetupPaidAds() { await finishFreeListingsSetup( event ); }; - // TODO: Add more disabled conditions to check - // - Google Ads account connection - // - Campaign data - // - Billing setup - const disabledComplete = completing === 'skip-ads'; + // The status check of Google Ads account connection is included in `campaign.isValid`, + // because when there is no connected account, it will disable the budget section and set the `amount` to `undefined`. + // TODO: Add a condition to check Billing setup + const disabledComplete = completing === 'skip-ads' || ! campaign.isValid; function createSkipButton( text ) { return ( @@ -113,7 +160,9 @@ export default function SetupPaidAds() { /> } /> - { showPaidAdsSetup && } + { showPaidAdsSetup && ( + + ) }
diff --git a/js/src/components/paid-ads/campaign-preview/components/search-bar.js b/js/src/components/paid-ads/campaign-preview/components/search-bar.js index 78905fcdbc..0284e1902d 100644 --- a/js/src/components/paid-ads/campaign-preview/components/search-bar.js +++ b/js/src/components/paid-ads/campaign-preview/components/search-bar.js @@ -26,9 +26,9 @@ export default function SearchBar( { hideMenu = false } ) { className="gla-ads-mockup__search-bar-menu" hidden={ hideMenu } > - - - + + + ); diff --git a/js/src/components/paid-ads/campaign-preview/mockup-display.js b/js/src/components/paid-ads/campaign-preview/mockup-display.js index 8ad7d4cafe..afdb0e8b13 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-display.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-display.js @@ -25,10 +25,10 @@ export default function MockupDisplay( { product } ) { return (
- - - - + + + +
@@ -45,7 +45,7 @@ export default function MockupDisplay( { product } ) {
- +
diff --git a/js/src/components/paid-ads/campaign-preview/mockup-gmail.js b/js/src/components/paid-ads/campaign-preview/mockup-gmail.js index 07a8876231..c963ae2e4d 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-gmail.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-gmail.js @@ -14,9 +14,9 @@ import gmailLogoURL from './images/gmail-logo.svg'; function MailItem() { return (
- - - + + +
); } diff --git a/js/src/components/paid-ads/campaign-preview/mockup-search.js b/js/src/components/paid-ads/campaign-preview/mockup-search.js index 6fce4ffede..0632784cb5 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-search.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-search.js @@ -35,18 +35,18 @@ export default function MockupSearch( { product } ) {
- - - - - + + + + +
{ product.shopUrl } - +
@@ -62,8 +62,8 @@ export default function MockupSearch( { product } ) {
- - + + @@ -72,7 +72,7 @@ export default function MockupSearch( { product } ) {
- +
diff --git a/js/src/components/paid-ads/campaign-preview/mockup-shopping.js b/js/src/components/paid-ads/campaign-preview/mockup-shopping.js index f1c592b233..17d6a7cc4f 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-shopping.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-shopping.js @@ -25,8 +25,8 @@ export default function MockupShopping( { product } ) { return (
- - + +
- +
- +
diff --git a/js/src/components/paid-ads/campaign-preview/mockup-youtube.js b/js/src/components/paid-ads/campaign-preview/mockup-youtube.js index cc5c5f6a06..95418c5f7e 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-youtube.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-youtube.js @@ -46,8 +46,8 @@ export default function MockupYouTube( { product } ) { { product.title } - - + + { product.shopName } From 631e8fe5f5f680ea82ee2c427a6d51617817669a Mon Sep 17 00:00:00 2001 From: Eason Su Date: Tue, 30 Aug 2022 12:53:03 +0800 Subject: [PATCH 060/140] Merge smaller and larger props as a single size prop for ScaledText. Address https://github.com/woocommerce/google-listings-and-ads/pull/1650#discussion_r957320406 --- .../campaign-preview/components/product-banner.js | 2 +- .../campaign-preview/components/scaled-text.js | 12 +++++------- .../paid-ads/campaign-preview/mockup-search.js | 2 +- .../paid-ads/campaign-preview/mockup-shopping.js | 4 ++-- .../paid-ads/campaign-preview/mockup-youtube.js | 6 +++--- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/js/src/components/paid-ads/campaign-preview/components/product-banner.js b/js/src/components/paid-ads/campaign-preview/components/product-banner.js index 172daa0b4a..a6bd76c683 100644 --- a/js/src/components/paid-ads/campaign-preview/components/product-banner.js +++ b/js/src/components/paid-ads/campaign-preview/components/product-banner.js @@ -19,7 +19,7 @@ export default function ProductBanner( { product } ) { return (
- + { product.shopName } diff --git a/js/src/components/paid-ads/campaign-preview/components/scaled-text.js b/js/src/components/paid-ads/campaign-preview/components/scaled-text.js index 88953abc3b..7b84602031 100644 --- a/js/src/components/paid-ads/campaign-preview/components/scaled-text.js +++ b/js/src/components/paid-ads/campaign-preview/components/scaled-text.js @@ -4,6 +4,7 @@ import classnames from 'classnames'; const styleName = { + default: false, // The default style is pre-defined doesn't and need to set another class name. adBadge: 'gla-ads-mockup__scaled-text--ad-badge', smaller: 'gla-ads-mockup__scaled-text--smaller', larger: 'gla-ads-mockup__scaled-text--larger', @@ -17,24 +18,21 @@ const styleName = { * * @param {Object} props React props. * @param {boolean} [props.adBadge=false] Whether to show the 'AD' badge. - * @param {boolean} [props.smaller] Whether to draw a smaller text size. - * @param {boolean} [props.larger] Whether to draw a larger text size. + * @param {'smaller'|'default'|'larger'} [props.size='default'] Text size. * @param {'blue'|'gray-700'|'gray-800'} [props.color='gray-700'] Text color. * @param {JSX.Element} [props.children] Content to be rendered. */ export default function ScaledText( { adBadge = false, - smaller, - larger, + size = 'default', color = 'gray-700', children, } ) { const className = classnames( 'gla-ads-mockup__scaled-text', styleName[ color ], - adBadge && styleName.adBadge, - smaller && styleName.smaller, - larger && styleName.larger + styleName[ size ], + adBadge && styleName.adBadge ); return
; } diff --git a/js/src/components/paid-ads/campaign-preview/mockup-search.js b/js/src/components/paid-ads/campaign-preview/mockup-search.js index 0632784cb5..bdf2b04d2b 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-search.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-search.js @@ -43,7 +43,7 @@ export default function MockupSearch( { product } ) {
- + { product.shopUrl } diff --git a/js/src/components/paid-ads/campaign-preview/mockup-shopping.js b/js/src/components/paid-ads/campaign-preview/mockup-shopping.js index 17d6a7cc4f..07c1cdd467 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-shopping.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-shopping.js @@ -43,11 +43,11 @@ export default function MockupShopping( { product } ) {
- + { product.title } { product.price } - { product.shopName } + { product.shopName }
diff --git a/js/src/components/paid-ads/campaign-preview/mockup-youtube.js b/js/src/components/paid-ads/campaign-preview/mockup-youtube.js index 95418c5f7e..3f2eefe555 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-youtube.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-youtube.js @@ -36,19 +36,19 @@ export default function MockupYouTube( { product } ) {
- + { __( 'LEARN MORE', 'google-listings-and-ads' ) }
- + { product.title } - + { product.shopName }
From c08364ff753c5a2c50096e5b2254fd0c9ded0032 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Tue, 30 Aug 2022 16:50:19 +0800 Subject: [PATCH 061/140] Two values were missed to be replaced with constants in the previous commit. Ref: 3e0debdc862d734ea3d46ceb7782285474344ea2 --- .../setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js index 3ea57f7b3c..a2ff45e66f 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js @@ -128,7 +128,7 @@ export default function SetupPaidAds() { return ( Date: Mon, 5 Sep 2022 12:15:40 +0800 Subject: [PATCH 062/140] Use constants for the status of Google Ads account. Address https://github.com/woocommerce/google-listings-and-ads/pull/1655#discussion_r960356644 --- js/src/components/different-currency-notice.js | 3 ++- .../google-ads-account-card/google-ads-account-card.js | 3 ++- js/src/constants.js | 7 +++++++ .../summary-section/paid-campaign-promotion-card.js | 4 +++- js/src/settings/linked-accounts.js | 5 ++++- .../setup-stepper/setup-paid-ads/setup-paid-ads.js | 7 ++++--- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/js/src/components/different-currency-notice.js b/js/src/components/different-currency-notice.js index 9bb32e9a76..9a6b5ab891 100644 --- a/js/src/components/different-currency-notice.js +++ b/js/src/components/different-currency-notice.js @@ -11,6 +11,7 @@ import { createInterpolateElement } from '@wordpress/element'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import useStoreCurrency from '.~/hooks/useStoreCurrency'; import AppDocumentationLink from '.~/components/app-documentation-link'; +import { GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; /** * Shows warning {@link Notice} @@ -31,7 +32,7 @@ const DifferentCurrencyNotice = ( { context } ) => { // Do not render if data is not available, account not connected, or the same currencies are used. if ( ! googleAdsAccount || - googleAdsAccount.status !== 'connected' || + googleAdsAccount.status !== GOOGLE_ADS_ACCOUNT_STATUS.CONNECTED || googleAdsAccount.currency === storeCurrency ) { return null; diff --git a/js/src/components/google-ads-account-card/google-ads-account-card.js b/js/src/components/google-ads-account-card/google-ads-account-card.js index bb481ed8e4..9e6dba446c 100644 --- a/js/src/components/google-ads-account-card/google-ads-account-card.js +++ b/js/src/components/google-ads-account-card/google-ads-account-card.js @@ -7,6 +7,7 @@ import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import ConnectedGoogleAdsAccountCard from './connected-google-ads-account-card'; import NonConnected from './non-connected'; import AuthorizeAds from './authorize-ads'; +import { GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; export default function GoogleAdsAccountCard() { const { google, scope } = useGoogleAccount(); @@ -20,7 +21,7 @@ export default function GoogleAdsAccountCard() { return ; } - if ( googleAdsAccount.status === 'disconnected' ) { + if ( googleAdsAccount.status === GOOGLE_ADS_ACCOUNT_STATUS.DISCONNECTED ) { return ; } diff --git a/js/src/constants.js b/js/src/constants.js index 75b779f620..dcb0e1f57b 100644 --- a/js/src/constants.js +++ b/js/src/constants.js @@ -50,3 +50,10 @@ export const ISSUE_TYPE_PRODUCT = 'product'; export const ISSUE_TYPE_ACCOUNT = 'account'; export const REQUEST_REVIEW = 'request-review'; export const ISSUE_TABLE_PER_PAGE = 5; + +// Account status related +export const GOOGLE_ADS_ACCOUNT_STATUS = { + CONNECTED: 'connected', + DISCONNECTED: 'disconnected', + INCOMPLETE: 'incomplete', +}; diff --git a/js/src/dashboard/summary-section/paid-campaign-promotion-card.js b/js/src/dashboard/summary-section/paid-campaign-promotion-card.js index 499e7df51b..39296f3a36 100644 --- a/js/src/dashboard/summary-section/paid-campaign-promotion-card.js +++ b/js/src/dashboard/summary-section/paid-campaign-promotion-card.js @@ -9,10 +9,12 @@ import { Spinner } from '@woocommerce/components'; */ import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import AddPaidCampaignButton from '.~/components/paid-ads/add-paid-campaign-button'; +import { GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; const PromotionContent = ( { adsAccount } ) => { const showFreeCredit = - adsAccount.sub_account || adsAccount.status === 'disconnected'; + adsAccount.sub_account || + adsAccount.status === GOOGLE_ADS_ACCOUNT_STATUS.DISCONNECTED; return ( <> diff --git a/js/src/settings/linked-accounts.js b/js/src/settings/linked-accounts.js index f7dbbf94a5..689554eeac 100644 --- a/js/src/settings/linked-accounts.js +++ b/js/src/settings/linked-accounts.js @@ -24,6 +24,9 @@ import { ConnectedGoogleAdsAccountCard } from '.~/components/google-ads-account- import Section from '.~/wcdl/section'; import LinkedAccountsSectionWrapper from './linked-accounts-section-wrapper'; import DisconnectModal, { ALL_ACCOUNTS, ADS_ACCOUNT } from './disconnect-modal'; +import { GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; + +const { CONNECTED, INCOMPLETE } = GOOGLE_ADS_ACCOUNT_STATUS; /** * Accounts are disconnected from the Setting page @@ -48,7 +51,7 @@ export default function LinkedAccounts() { googleMCAccount && googleAdsAccount ); - const hasAdsAccount = [ 'connected', 'incomplete' ].includes( + const hasAdsAccount = [ CONNECTED, INCOMPLETE ].includes( googleAdsAccount?.status ); diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js index a2ff45e66f..bff0b43f32 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js @@ -26,9 +26,11 @@ import AudienceSection from '.~/components/paid-ads/audience-section'; import BudgetSection from '.~/components/paid-ads/budget-section'; import validateForm from '.~/utils/paid-ads/validateForm'; import { getProductFeedUrl } from '.~/utils/urls'; -import { GUIDE_NAMES } from '.~/constants'; +import { GUIDE_NAMES, GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; import { API_NAMESPACE } from '.~/data/constants'; +const { CONNECTED } = GOOGLE_ADS_ACCOUNT_STATUS; + function PaidAdsSectionsGroup( { onCampaignChange } ) { const { googleAdsAccount } = useGoogleAdsAccount(); const { data: targetAudience } = useTargetAudienceFinalCountryCodes(); @@ -52,8 +54,7 @@ function PaidAdsSectionsGroup( { onCampaignChange } ) { > { ( formProps ) => { const { countryCodes } = formProps.values; - const disabledAudience = - googleAdsAccount?.status !== 'connected'; + const disabledAudience = googleAdsAccount?.status !== CONNECTED; const disabledBudget = disabledAudience || countryCodes.length === 0; From 5b6522ca10ad75556e6b2de7c120cc712aab70b1 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Wed, 14 Sep 2022 18:53:05 +0800 Subject: [PATCH 063/140] Fix a compatibility problem with WC 6.9 about the breaking change of the exposed interfaces of Form component --- .../setup-free-listings/index.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/js/src/components/free-listings/setup-free-listings/index.js b/js/src/components/free-listings/setup-free-listings/index.js index 542566a2be..cab01f6760 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -88,7 +88,7 @@ const SetupFreeListings = ( { submitLabel, } ) => { const [ saving, setSaving ] = useState( false ); - const formRef = useRef(); + const formPropsDelegateeRef = useRef( [] ); if ( ! ( targetAudience && settings && shippingRates && shippingTimes ) ) { return ; @@ -126,7 +126,9 @@ const SetupFreeListings = ( { countries.includes( el.country || el.countryCode ) ); if ( nextValues.length !== currentValues.length ) { - formRef.current.setValue( field, nextValues ); + formPropsDelegateeRef.current.push( ( formProps ) => + formProps.setValue( field, nextValues ) + ); } } ); @@ -137,7 +139,6 @@ const SetupFreeListings = ( {
{ const countries = resolveFinalCountries( formProps.values ); + // Since WC 6.9, the original Form is re-implemented as Functional component from + // Class component, but the exposed interfaces are completely changed. Given that + // there is no longer a regular interface for updating Form values externally, this + // is a hack to delegate the access to `formProps` for external use. + // + // And, only one delegate can be consumed at a time in this render prop to ensure + // the updating states will always be the latest when calling. + // + // See: + // - https://github.com/woocommerce/woocommerce/blob/6.8.2/packages/js/components/src/form/index.js#L42-L46 + // - https://github.com/woocommerce/woocommerce/blob/6.9.0/packages/js/components/src/form/form.tsx#L125-L127 + // - https://github.com/woocommerce/woocommerce/blob/6.9.0/packages/js/components/src/form/form.tsx#L134-L138 + if ( formPropsDelegateeRef.current.length ) { + formPropsDelegateeRef.current.pop()( formProps ); + } + return ( Date: Thu, 15 Sep 2022 18:08:13 +0800 Subject: [PATCH 064/140] Add Google Ads account status to constants.js --- js/src/constants.js | 7 +++++++ js/src/setup-ads/ads-stepper/setup-billing/index.js | 10 +++++++--- .../setup-card/useAutoCheckBillingStatusEffect.js | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/js/src/constants.js b/js/src/constants.js index dcb0e1f57b..f104a16a17 100644 --- a/js/src/constants.js +++ b/js/src/constants.js @@ -57,3 +57,10 @@ export const GOOGLE_ADS_ACCOUNT_STATUS = { DISCONNECTED: 'disconnected', INCOMPLETE: 'incomplete', }; + +export const GOOGLE_ADS_BILLING_STATUS = { + UNKNOWN: 'unknown', + PENDING: 'pending', + APPROVED: 'approved', + CANCELLED: 'cancelled', +}; diff --git a/js/src/setup-ads/ads-stepper/setup-billing/index.js b/js/src/setup-ads/ads-stepper/setup-billing/index.js index 928b13bd3c..1dbdf325e4 100644 --- a/js/src/setup-ads/ads-stepper/setup-billing/index.js +++ b/js/src/setup-ads/ads-stepper/setup-billing/index.js @@ -16,6 +16,7 @@ import BillingSavedCard from './billing-saved-card'; import StepContentFooter from '.~/components/stepper/step-content-footer'; import AppButton from '.~/components/app-button'; import fallbackBillingUrl from './fallbackBillingUrl'; +import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; const SetupBilling = ( props ) => { const { @@ -28,12 +29,15 @@ const SetupBilling = ( props ) => { return ; } + const isApproved = + billingStatus.status === GOOGLE_ADS_BILLING_STATUS.APPROVED; + return ( { 'google-listings-and-ads' ) } > - { billingStatus.status === 'approved' ? ( + { isApproved ? ( ) : ( { /> ) }
- { billingStatus.status === 'approved' && ( + { isApproved && ( {} ) => { path: '/wc/gla/ads/billing-status', } ); - if ( billingStatus.status !== 'approved' ) { + if ( billingStatus.status !== GOOGLE_ADS_BILLING_STATUS.APPROVED ) { return; } From 1f07354244277f6a12549a614894d4c40ee1db8d Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 15 Sep 2022 18:09:23 +0800 Subject: [PATCH 065/140] Invalidate the store state of billing status when receiving Google Ads account data --- js/src/data/resolvers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/src/data/resolvers.js b/js/src/data/resolvers.js index f052d9c134..fc907d5011 100644 --- a/js/src/data/resolvers.js +++ b/js/src/data/resolvers.js @@ -110,6 +110,10 @@ export function* getGoogleAdsAccountBillingStatus() { yield fetchGoogleAdsAccountBillingStatus(); } +getGoogleAdsAccountBillingStatus.shouldInvalidate = ( action ) => { + return action.type === TYPES.RECEIVE_ACCOUNTS_GOOGLE_ADS; +}; + export function* getExistingGoogleAdsAccounts() { yield fetchExistingGoogleAdsAccounts(); } From 67ee30c11865d34aa65d485a1b5e77de26780d18 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 15 Sep 2022 18:44:55 +0800 Subject: [PATCH 066/140] Move files to the shared components directory --- .../paid-ads/billing-card/billing-setup-card.js} | 6 +++--- .../paid-ads/billing-card/billing-setup-card.scss} | 0 .../paid-ads/billing-card}/fallbackBillingUrl.js | 0 js/src/components/paid-ads/billing-card/index.js | 2 ++ .../billing-card}/useAutoCheckBillingStatusEffect.js | 0 .../useAdsSetupCompleteCallback.js} | 2 +- js/src/setup-ads/ads-stepper/setup-billing/index.js | 8 +++++--- js/src/setup-ads/setup-ads-form.js | 4 ++-- 8 files changed, 13 insertions(+), 9 deletions(-) rename js/src/{setup-ads/ads-stepper/setup-billing/setup-card/index.js => components/paid-ads/billing-card/billing-setup-card.js} (95%) rename js/src/{setup-ads/ads-stepper/setup-billing/setup-card/index.scss => components/paid-ads/billing-card/billing-setup-card.scss} (100%) rename js/src/{setup-ads/ads-stepper/setup-billing => components/paid-ads/billing-card}/fallbackBillingUrl.js (100%) create mode 100644 js/src/components/paid-ads/billing-card/index.js rename js/src/{setup-ads/ads-stepper/setup-billing/setup-card => components/paid-ads/billing-card}/useAutoCheckBillingStatusEffect.js (100%) rename js/src/{setup-ads/useSetupCompleteCallback.js => hooks/useAdsSetupCompleteCallback.js} (95%) diff --git a/js/src/setup-ads/ads-stepper/setup-billing/setup-card/index.js b/js/src/components/paid-ads/billing-card/billing-setup-card.js similarity index 95% rename from js/src/setup-ads/ads-stepper/setup-billing/setup-card/index.js rename to js/src/components/paid-ads/billing-card/billing-setup-card.js index 09fbc62faa..4f8e09a0fc 100644 --- a/js/src/setup-ads/ads-stepper/setup-billing/setup-card/index.js +++ b/js/src/components/paid-ads/billing-card/billing-setup-card.js @@ -11,9 +11,9 @@ import AppSpinner from '.~/components/app-spinner'; import TitleButtonLayout from '.~/components/title-button-layout'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import Section from '.~/wcdl/section'; -import './index.scss'; import AppButton from '.~/components/app-button'; import useAutoCheckBillingStatusEffect from './useAutoCheckBillingStatusEffect'; +import './billing-setup-card.scss'; /** * "Set up billing" button for Google Ads account is clicked. @@ -31,7 +31,7 @@ import useAutoCheckBillingStatusEffect from './useAutoCheckBillingStatusEffect'; * @param {Function} props.onSetupComplete Callback function when setup is completed * @return {JSX.Element} Card filled with content or `AppSpinner`. */ -const SetupCard = ( { billingUrl, onSetupComplete } ) => { +const BillingSetupCard = ( { billingUrl, onSetupComplete } ) => { const { googleAdsAccount } = useGoogleAdsAccount(); useAutoCheckBillingStatusEffect( onSetupComplete ); @@ -78,4 +78,4 @@ const SetupCard = ( { billingUrl, onSetupComplete } ) => { ); }; -export default SetupCard; +export default BillingSetupCard; diff --git a/js/src/setup-ads/ads-stepper/setup-billing/setup-card/index.scss b/js/src/components/paid-ads/billing-card/billing-setup-card.scss similarity index 100% rename from js/src/setup-ads/ads-stepper/setup-billing/setup-card/index.scss rename to js/src/components/paid-ads/billing-card/billing-setup-card.scss diff --git a/js/src/setup-ads/ads-stepper/setup-billing/fallbackBillingUrl.js b/js/src/components/paid-ads/billing-card/fallbackBillingUrl.js similarity index 100% rename from js/src/setup-ads/ads-stepper/setup-billing/fallbackBillingUrl.js rename to js/src/components/paid-ads/billing-card/fallbackBillingUrl.js diff --git a/js/src/components/paid-ads/billing-card/index.js b/js/src/components/paid-ads/billing-card/index.js new file mode 100644 index 0000000000..ae4834a8a9 --- /dev/null +++ b/js/src/components/paid-ads/billing-card/index.js @@ -0,0 +1,2 @@ +export { default as BillingSetupCard } from './billing-setup-card'; +export { default as fallbackBillingUrl } from './fallbackBillingUrl'; diff --git a/js/src/setup-ads/ads-stepper/setup-billing/setup-card/useAutoCheckBillingStatusEffect.js b/js/src/components/paid-ads/billing-card/useAutoCheckBillingStatusEffect.js similarity index 100% rename from js/src/setup-ads/ads-stepper/setup-billing/setup-card/useAutoCheckBillingStatusEffect.js rename to js/src/components/paid-ads/billing-card/useAutoCheckBillingStatusEffect.js diff --git a/js/src/setup-ads/useSetupCompleteCallback.js b/js/src/hooks/useAdsSetupCompleteCallback.js similarity index 95% rename from js/src/setup-ads/useSetupCompleteCallback.js rename to js/src/hooks/useAdsSetupCompleteCallback.js index 61cc12b66d..5bcb4c7792 100644 --- a/js/src/setup-ads/useSetupCompleteCallback.js +++ b/js/src/hooks/useAdsSetupCompleteCallback.js @@ -11,7 +11,7 @@ import apiFetch from '@wordpress/api-fetch'; import { useAppDispatch } from '.~/data'; import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; -export default function useSetupCompleteCallback() { +export default function useAdsSetupCompleteCallback() { const { createAdsCampaign } = useAppDispatch(); const { createNotice } = useDispatchCoreNotices(); const [ loading, setLoading ] = useState( false ); diff --git a/js/src/setup-ads/ads-stepper/setup-billing/index.js b/js/src/setup-ads/ads-stepper/setup-billing/index.js index 1dbdf325e4..1dc6b234ed 100644 --- a/js/src/setup-ads/ads-stepper/setup-billing/index.js +++ b/js/src/setup-ads/ads-stepper/setup-billing/index.js @@ -11,11 +11,13 @@ import StepContentHeader from '.~/components/stepper/step-content-header'; import AppSpinner from '.~/components/app-spinner'; import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; import Section from '.~/wcdl/section'; -import SetupCard from './setup-card'; +import { + BillingSetupCard, + fallbackBillingUrl, +} from '.~/components/paid-ads/billing-card'; import BillingSavedCard from './billing-saved-card'; import StepContentFooter from '.~/components/stepper/step-content-footer'; import AppButton from '.~/components/app-button'; -import fallbackBillingUrl from './fallbackBillingUrl'; import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; const SetupBilling = ( props ) => { @@ -57,7 +59,7 @@ const SetupBilling = ( props ) => { { isApproved ? ( ) : ( - { const [ didFormChanged, setFormChanged ] = useState( false ); const [ isSubmitted, setSubmitted ] = useState( false ); - const [ handleSetupComplete, isSubmitting ] = useSetupCompleteCallback(); + const [ handleSetupComplete, isSubmitting ] = useAdsSetupCompleteCallback(); const adminUrl = useAdminUrl(); const { data: targetAudience } = useTargetAudienceFinalCountryCodes(); From 289caddade0bc845be81fa383e2efca599ca1710 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 15 Sep 2022 18:49:58 +0800 Subject: [PATCH 067/140] Add the hasFinishedResolution state to useGoogleAdsAccountBillingStatus hook --- js/src/hooks/useGoogleAdsAccountBillingStatus.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/js/src/hooks/useGoogleAdsAccountBillingStatus.js b/js/src/hooks/useGoogleAdsAccountBillingStatus.js index 4472cb6eb1..b3f055561a 100644 --- a/js/src/hooks/useGoogleAdsAccountBillingStatus.js +++ b/js/src/hooks/useGoogleAdsAccountBillingStatus.js @@ -8,14 +8,18 @@ import { useSelect } from '@wordpress/data'; */ import { STORE_KEY } from '.~/data/constants'; +const selectorName = 'getGoogleAdsAccountBillingStatus'; + const useGoogleAdsAccountBillingStatus = () => { return useSelect( ( select ) => { - const billingStatus = select( - STORE_KEY - ).getGoogleAdsAccountBillingStatus(); + const selector = select( STORE_KEY ); return { - billingStatus, + billingStatus: selector[ selectorName ](), + hasFinishedResolution: selector.hasFinishedResolution( + selectorName, + [] + ), }; }, [] ); }; From 3eca055003c25882fa6125d17f98d86d1024b87d Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 15 Sep 2022 18:57:00 +0800 Subject: [PATCH 068/140] Implement the BillingCard component --- .../paid-ads/billing-card/billing-card.js | 53 +++++++++++++++++++ .../paid-ads/billing-card/billing-card.scss | 13 +++++ .../components/paid-ads/billing-card/index.js | 1 + 3 files changed, 67 insertions(+) create mode 100644 js/src/components/paid-ads/billing-card/billing-card.js create mode 100644 js/src/components/paid-ads/billing-card/billing-card.scss diff --git a/js/src/components/paid-ads/billing-card/billing-card.js b/js/src/components/paid-ads/billing-card/billing-card.js new file mode 100644 index 0000000000..d441a37def --- /dev/null +++ b/js/src/components/paid-ads/billing-card/billing-card.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import Gridiconcheckmark from 'gridicons/dist/checkmark'; +import { Flex, FlexBlock } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; +import SpinnerCard from '.~/components/spinner-card'; +import BillingSetupCard from './billing-setup-card'; +import fallbackBillingUrl from './fallbackBillingUrl'; +import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; +import './billing-card.scss'; + +const { APPROVED } = GOOGLE_ADS_BILLING_STATUS; + +/** + * Renders a success notice card or a setup card according to the billing status + * of the connected Google Ads account. + */ +export default function BillingCard() { + const { + billingStatus, + hasFinishedResolution, + } = useGoogleAdsAccountBillingStatus(); + + if ( ! hasFinishedResolution ) { + return ; + } + + if ( billingStatus.status === APPROVED ) { + return ( + + + + { __( + 'Billing method for Google Ads added successfully', + 'google-listings-and-ads' + ) } + + + ); + } + + return ( + + ); +} diff --git a/js/src/components/paid-ads/billing-card/billing-card.scss b/js/src/components/paid-ads/billing-card/billing-card.scss new file mode 100644 index 0000000000..6f4f978424 --- /dev/null +++ b/js/src/components/paid-ads/billing-card/billing-card.scss @@ -0,0 +1,13 @@ +.gla-google-ads-billing-card { + &__success-status { + padding: $grid-unit-15; + line-height: 2em; + background-color: #eff9f1; + color: $gray-900; + + .gridicon { + fill: $gla-color-green; + margin: 0 $grid-unit-15 0 $grid-unit-05; + } + } +} diff --git a/js/src/components/paid-ads/billing-card/index.js b/js/src/components/paid-ads/billing-card/index.js index ae4834a8a9..06954d0a52 100644 --- a/js/src/components/paid-ads/billing-card/index.js +++ b/js/src/components/paid-ads/billing-card/index.js @@ -1,2 +1,3 @@ +export { default } from './billing-card'; export { default as BillingSetupCard } from './billing-setup-card'; export { default as fallbackBillingUrl } from './fallbackBillingUrl'; From 388b02be4b5fbf443f1ee8f7996a65cbf8557107 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 15 Sep 2022 19:03:33 +0800 Subject: [PATCH 069/140] Adjust the billing setup UI to the new design and open the setup URL via a popup window --- .../billing-card/billing-setup-card.js | 91 ++++++++++++------- .../billing-card/billing-setup-card.scss | 12 ++- .../useAutoCheckBillingStatusEffect.js | 3 +- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/js/src/components/paid-ads/billing-card/billing-setup-card.js b/js/src/components/paid-ads/billing-card/billing-setup-card.js index 4f8e09a0fc..93bc53baa4 100644 --- a/js/src/components/paid-ads/billing-card/billing-setup-card.js +++ b/js/src/components/paid-ads/billing-card/billing-setup-card.js @@ -2,19 +2,37 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { createInterpolateElement } from '@wordpress/element'; +import { ExternalLink } from '@wordpress/components'; /** * Internal dependencies */ -import toAccountText from '.~/utils/toAccountText'; import AppSpinner from '.~/components/app-spinner'; -import TitleButtonLayout from '.~/components/title-button-layout'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import Section from '.~/wcdl/section'; import AppButton from '.~/components/app-button'; import useAutoCheckBillingStatusEffect from './useAutoCheckBillingStatusEffect'; import './billing-setup-card.scss'; +/** + * Returns a string of window.open()'s features that aligns with the center of the current window. + * + * @param {Window} defaultView The window object. + * @param {number} windowWidth Expected window width. + * @param {number} windowHeight Expected window height. + * @return {string} Centered alignment window features for calling with window.open(). + */ +function getWindowFeatures( defaultView, windowWidth, windowHeight ) { + const { innerWidth, innerHeight, screenX, screenY, screen } = defaultView; + const width = Math.min( windowWidth, screen.availWidth ); + const height = Math.min( windowHeight, screen.availHeight ); + const left = ( innerWidth - width ) / 2 + screenX; + const top = ( innerHeight - height ) / 2 + screenY; + + return `popup=1,left=${ left },top=${ top },width=${ width },height=${ height }`; +} + /** * "Set up billing" button for Google Ads account is clicked. * @@ -39,42 +57,45 @@ const BillingSetupCard = ( { billingUrl, onSetupComplete } ) => { return ; } + const handleClick = ( e ) => { + const { defaultView } = e.target.ownerDocument; + const features = getWindowFeatures( defaultView, 600, 800 ); + + defaultView.open( billingUrl, '_blank', features ); + }; + return ( -
- - -
- -
-
-
- { __( - 'You do not have billing information set up in your Google Ads account. Once you have completed your billing setup, your campaign will launch automatically.', - 'google-listings-and-ads' - ) } -
- - { __( - 'Set up billing', + + +
+ { __( + 'You do not have billing information set up in your Google Ads account. Once you have set up billing, you can start running ads.', + 'google-listings-and-ads' + ) } +
+ { createInterpolateElement( + __( + 'You will be directed to Google Ads for this step. In case your browser is unable to open the pop-up, click here instead.', 'google-listings-and-ads' - ) } - + ), + { link: } + ) }
- - -
+
+ + { __( 'Set up billing', 'google-listings-and-ads' ) } + +
+
); }; diff --git a/js/src/components/paid-ads/billing-card/billing-setup-card.scss b/js/src/components/paid-ads/billing-card/billing-setup-card.scss index 5c820a417b..59ed0f1dc2 100644 --- a/js/src/components/paid-ads/billing-card/billing-setup-card.scss +++ b/js/src/components/paid-ads/billing-card/billing-setup-card.scss @@ -1,14 +1,18 @@ .gla-google-ads-billing-setup-card { - &__account-number { - margin-bottom: calc(var(--main-gap) / 2); + .components-card__body { + display: flex; + gap: $grid-unit-20; } &__description { display: flex; - gap: calc(var(--main-gap) * 2); + flex-direction: column; + gap: $grid-unit-20; + color: $black; - &__text { + &__helper { font-style: italic; + color: $gray-700; } } } diff --git a/js/src/components/paid-ads/billing-card/useAutoCheckBillingStatusEffect.js b/js/src/components/paid-ads/billing-card/useAutoCheckBillingStatusEffect.js index 683f4f9a3c..145f9d0526 100644 --- a/js/src/components/paid-ads/billing-card/useAutoCheckBillingStatusEffect.js +++ b/js/src/components/paid-ads/billing-card/useAutoCheckBillingStatusEffect.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { useCallback } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; +import { noop } from 'lodash'; /** * Internal dependencies @@ -23,7 +24,7 @@ const completeGoogleAdsAccountSetup = () => { } ); }; -const useAutoCheckBillingStatusEffect = ( onStatusApproved = () => {} ) => { +const useAutoCheckBillingStatusEffect = ( onStatusApproved = noop ) => { const { createNotice } = useDispatchCoreNotices(); const { receiveGoogleAdsAccountBillingStatus } = useAppDispatch(); From fd030a8273c0c34803a189eb50c5752c065b3212 Mon Sep 17 00:00:00 2001 From: Eason Su Date: Thu, 15 Sep 2022 19:26:05 +0800 Subject: [PATCH 070/140] Add the billing card to the last step of onboarding flow --- .../paid-ads/budget-section/index.js | 11 ++- .../paid-ads/budget-section/index.scss | 6 ++ .../setup-paid-ads/setup-paid-ads.js | 69 ++++++++++++++----- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/js/src/components/paid-ads/budget-section/index.js b/js/src/components/paid-ads/budget-section/index.js index 0786d91194..043ed97feb 100644 --- a/js/src/components/paid-ads/budget-section/index.js +++ b/js/src/components/paid-ads/budget-section/index.js @@ -26,12 +26,10 @@ const nonInteractableProps = { * @param {Object} props React props. * @param {Object} props.formProps Form props forwarded from `Form` component. * @param {boolean} [props.disabled=false] Whether display the Card in disabled style. + * @param {JSX.Element} [props.children] Extra content to be rendered under the card of budget inputs. */ -const BudgetSection = ( props ) => { - const { - formProps: { getInputProps, setValue, values }, - disabled = false, - } = props; +const BudgetSection = ( { formProps, disabled = false, children } ) => { + const { getInputProps, setValue, values } = formProps; const { countryCodes, amount } = values; const { googleAdsAccount } = useGoogleAdsAccount(); const monthlyMaxEstimated = getMonthlyMaxEstimated( amount ); @@ -80,7 +78,7 @@ const BudgetSection = ( props ) => { { ) } + { children }
); diff --git a/js/src/components/paid-ads/budget-section/index.scss b/js/src/components/paid-ads/budget-section/index.scss index 1062e0df3c..cfd59320b0 100644 --- a/js/src/components/paid-ads/budget-section/index.scss +++ b/js/src/components/paid-ads/budget-section/index.scss @@ -1,4 +1,10 @@ .gla-budget-section { + .wcdl-section__body { + display: flex; + flex-direction: column; + gap: $grid-unit-20; + } + &__card-body { display: flex; flex-direction: column; diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js index bff0b43f32..4b21e729ca 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; -import { useState } from '@wordpress/element'; +import { useState, useRef, useEffect } from '@wordpress/element'; import { Flex } from '@wordpress/components'; import { Form } from '@woocommerce/components'; @@ -14,6 +14,7 @@ import useAdminUrl from '.~/hooks/useAdminUrl'; import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; +import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; import StepContent from '.~/components/stepper/step-content'; import StepContentHeader from '.~/components/stepper/step-content-header'; import StepContentFooter from '.~/components/stepper/step-content-footer'; @@ -24,24 +25,50 @@ import PaidAdsFeaturesSection from './paid-ads-features-section'; import GoogleAdsAccountSection from './google-ads-account-section'; import AudienceSection from '.~/components/paid-ads/audience-section'; import BudgetSection from '.~/components/paid-ads/budget-section'; +import BillingCard from '.~/components/paid-ads/billing-card'; import validateForm from '.~/utils/paid-ads/validateForm'; import { getProductFeedUrl } from '.~/utils/urls'; -import { GUIDE_NAMES, GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; import { API_NAMESPACE } from '.~/data/constants'; +import { + GUIDE_NAMES, + GOOGLE_ADS_ACCOUNT_STATUS, + GOOGLE_ADS_BILLING_STATUS, +} from '.~/constants'; -const { CONNECTED } = GOOGLE_ADS_ACCOUNT_STATUS; - -function PaidAdsSectionsGroup( { onCampaignChange } ) { +function PaidAdsSectionsGroup( { onStatesReceived } ) { const { googleAdsAccount } = useGoogleAdsAccount(); const { data: targetAudience } = useTargetAudienceFinalCountryCodes(); + const { billingStatus } = useGoogleAdsAccountBillingStatus(); - if ( ! targetAudience ) { - return ; - } + const onStatesReceivedRef = useRef(); + onStatesReceivedRef.current = onStatesReceived; - const handleChange = ( _, values, isValid ) => { - onCampaignChange( { ...values, isValid } ); + const initialValues = { + amount: 0, + countryCodes: targetAudience, }; + const [ campaign, setCampaign ] = useState( { + ...initialValues, + isValid: ! Object.keys( validateForm( initialValues ) ).length, + isReady: false, + } ); + + const isBillingCompleted = + billingStatus?.status === GOOGLE_ADS_BILLING_STATUS.APPROVED; + + // Watch the `isBillingCompleted` in order to ensure the parent component can + // continue the setup from a middle state. For example, refresh the current page + // after the billing setup is finished. + useEffect( () => { + onStatesReceivedRef.current( { + ...campaign, + isReady: campaign.isValid && isBillingCompleted, + } ); + }, [ campaign, isBillingCompleted ] ); + + if ( ! targetAudience || ! billingStatus ) { + return ; + } return ( { + setCampaign( { ...values, isValid } ); + } } validate={ validateForm } > { ( formProps ) => { const { countryCodes } = formProps.values; - const disabledAudience = googleAdsAccount?.status !== CONNECTED; + const disabledAudience = ! [ + GOOGLE_ADS_ACCOUNT_STATUS.CONNECTED, + GOOGLE_ADS_ACCOUNT_STATUS.INCOMPLETE, + ].includes( googleAdsAccount?.status ); const disabledBudget = disabledAudience || countryCodes.length === 0; @@ -68,7 +100,9 @@ function PaidAdsSectionsGroup( { onCampaignChange } ) { + > + { ! disabledBudget && } + ); } } @@ -87,7 +121,7 @@ export default function SetupPaidAds() { const adminUrl = useAdminUrl(); const { createNotice } = useDispatchCoreNotices(); const [ showPaidAdsSetup, setShowPaidAdsSetup ] = useState( false ); - const [ campaign, setCampaign ] = useState( {} ); + const [ paidAds, setPaidAds ] = useState( {} ); const [ completing, setCompleting ] = useState( null ); const finishFreeListingsSetup = async ( event ) => { @@ -120,10 +154,9 @@ export default function SetupPaidAds() { await finishFreeListingsSetup( event ); }; - // The status check of Google Ads account connection is included in `campaign.isValid`, + // The status check of Google Ads account connection is included in `paidAds.completing`, // because when there is no connected account, it will disable the budget section and set the `amount` to `undefined`. - // TODO: Add a condition to check Billing setup - const disabledComplete = completing === ACTION_SKIP || ! campaign.isValid; + const disabledComplete = completing === ACTION_SKIP || ! paidAds.isReady; function createSkipButton( text ) { return ( @@ -169,7 +202,7 @@ export default function SetupPaidAds() { } /> { showPaidAdsSetup && ( - + ) }