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/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/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; 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 94% 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 index 9c876d6dd5..ae101df6ef 100644 --- a/js/src/edit-free-campaign/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'; @@ -21,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. @@ -44,6 +43,7 @@ const FormContent = ( { return ( + { /** * 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 {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 {(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 {(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 ); @@ -107,7 +113,23 @@ 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 ) ); + + // 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 +137,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, - }, - ] } + ); 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 5407b724e1..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/form-content.js +++ /dev/null @@ -1,152 +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'; -import '.~/components/free-listings/choose-audience/index.scss'; - -/** - * 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 153818910f..0000000000 --- a/js/src/setup-mc/setup-stepper/choose-audience/index.js +++ /dev/null @@ -1,89 +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'; -import '.~/components/free-listings/choose-audience/index.scss'; - -/** - * 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/saved-setup-stepper.js b/js/src/setup-mc/setup-stepper/saved-setup-stepper.js index 828884d897..937c10ed9a 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,23 @@ */ 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 useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; 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 +33,49 @@ 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(); + 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 + useEffect( () => { + if ( + targetAudience?.location === null && + suggestedAudience?.location + ) { + saveTargetAudience( suggestedAudience ); + } + }, [ 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', @@ -34,18 +85,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 +100,26 @@ const SavedSetupStepper = ( { savedStep, onRefetchSavedStep = () => {} } ) => { } }; + /** + * Handles form change callback and callback's errors via binding an actual callback function and an error message. + * + * `this` should be an async callback function that handles the form change. + * For example: + * `handleFormChange.bind( saveSettings, __( 'Oops!', 'google-listings-and-ads' ) )` + * + * @this {(newValue: *) => Promise} + * @param {string} errorMessage Message when the error occurs. + * @param {*} newValue The new values will be called with the bound callback function. + */ + function handleFormChange( errorMessage, newValue ) { + this( newValue ).catch( () => createNotice( 'error', errorMessage ) ); + } + + const initShippingRates = hasResolvedShippingRates ? shippingRates : null; + const initShippingTimes = hasResolvedShippingTimes ? shippingTimes : null; + const initTargetAudience = targetAudience?.location ? targetAudience : null; + const initSettings = settings?.shipping_rate ? settings : 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/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 ); - } ); -} ); 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 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",