diff --git a/js/src/components/conditional-section.js b/js/src/components/conditional-section.js deleted file mode 100644 index 57a9bc7ccd..0000000000 --- a/js/src/components/conditional-section.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Internal dependencies - */ -import Section from '~/components/section'; -import AppSpinner from '~/components/app-spinner'; - -/** - * Component to conditionally render a section. - * If `show` is set to - * - `true` returns given children - * - `false` returns null - * - `null` - unspecified, render section preloader - * - * @param {Object} props React props - * @param {boolean | null} props.show Flag to indicate whether the element should be shown. - * @param {Array} props.children Content to be rendered. - * @return {Array | null} Children, preloader section, or null. - */ -const ConditionalSection = ( { show, children } ) => { - switch ( show ) { - case true: - return children; - case false: - return null; - default: - return ( -
- -
- ); - } -}; - -export default ConditionalSection; 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 5138c253eb..32f5668b0c 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 @@ -6,14 +6,6 @@ exports[`checkErrors Audience When the audience location option is an invalid va 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 store country code or 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 store country code or 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."`; - -exports[`checkErrors For tax rate, if store country code or selected country codes include 'US' When the tax rate option is an invalid value or missing, should not pass 3`] = `"Please specify tax rate option."`; - -exports[`checkErrors For tax rate, if store country code or selected country codes include 'US' When the tax rate option is an invalid value or missing, should not pass 4`] = `"Please specify tax rate option."`; - exports[`checkErrors Offer free shipping With flat shipping rate option When there are some non-free shipping rates, and offer free shipping is checked, and there is no minimum order amount for non-free shipping rates, should not pass 1`] = `"Please enter minimum order for free shipping."`; exports[`checkErrors Shipping rates For flat type When there are any selected countries with shipping rates not set, should not pass 1`] = `"Please specify estimated shipping rates for all the countries, and the rate cannot be less than 0."`; 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 20b9e663ee..200a59a5b7 100644 --- a/js/src/components/free-listings/configure-product-listings/checkErrors.js +++ b/js/src/components/free-listings/configure-product-listings/checkErrors.js @@ -6,15 +6,8 @@ import { __ } from '@wordpress/i18n'; 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' ] ); -const checkErrors = ( - values, - shippingTimes, - finalCountryCodes, - storeCountryCode, - hideTaxRates = false -) => { +const checkErrors = ( values, shippingTimes, finalCountryCodes ) => { const errors = {}; // Check audience. @@ -102,20 +95,6 @@ const checkErrors = ( ); } - /** - * Check tax rate (required for U.S. only). - */ - if ( - ! hideTaxRates && - ( storeCountryCode === 'US' || finalCountryCodes.includes( 'US' ) ) && - ! validTaxRateSet.has( values.tax_rate ) - ) { - errors.tax_rate = __( - 'Please specify tax rate option.', - 'google-listings-and-ads' - ); - } - return errors; }; 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 52e37e4151..5e2debb65e 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 @@ -41,7 +41,6 @@ describe( 'checkErrors', () => { countries: [ 'US', 'JP' ], shipping_rate: 'flat', shipping_time: 'flat', - tax_rate: 'manual', shipping_country_rates: toRates( [ 'US', 10 ], [ 'JP', 30, 88 ] ), offer_free_shipping: true, }; @@ -436,72 +435,4 @@ describe( 'checkErrors', () => { } ); } ); } ); - - describe( `For tax rate, if store country code or selected country codes include 'US'`, () => { - let codes; - - beforeEach( () => { - codes = [ 'US' ]; - } ); - - it( `When the tax rate option is an invalid value or missing, should not pass`, () => { - // Not set yet - let errors = checkErrors( defaultFormValues, [], codes ); - - expect( errors ).toHaveProperty( 'tax_rate' ); - expect( errors.tax_rate ).toMatchSnapshot(); - - errors = checkErrors( defaultFormValues, [], [], 'US' ); - - expect( errors ).toHaveProperty( 'tax_rate' ); - expect( errors.tax_rate ).toMatchSnapshot(); - - // Invalid value - errors = checkErrors( - { ...defaultFormValues, tax_rate: true }, - [], - codes - ); - - expect( errors ).toHaveProperty( 'tax_rate' ); - expect( errors.tax_rate ).toMatchSnapshot(); - - // Invalid value - errors = checkErrors( - { ...defaultFormValues, tax_rate: 'invalid' }, - [], - codes - ); - - expect( errors ).toHaveProperty( 'tax_rate' ); - expect( errors.tax_rate ).toMatchSnapshot(); - } ); - - it( 'When the tax rate option is a valid value, should pass', () => { - // Selected destination - const destinationTaxRate = { - ...defaultFormValues, - tax_rate: 'destination', - }; - - let errors = checkErrors( destinationTaxRate, [], codes ); - - expect( errors ).not.toHaveProperty( 'tax_rate' ); - - errors = checkErrors( destinationTaxRate, [], [], 'US' ); - - expect( errors ).not.toHaveProperty( 'tax_rate' ); - - // Selected manual - const manualTaxRate = { ...defaultFormValues, tax_rate: 'manual' }; - - errors = checkErrors( manualTaxRate, [], codes ); - - expect( errors ).not.toHaveProperty( 'tax_rate' ); - - errors = checkErrors( destinationTaxRate, [], [], 'US' ); - - expect( errors ).not.toHaveProperty( 'tax_rate' ); - } ); - } ); } ); 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 7000c62255..04afbe9f97 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 @@ -1,76 +1,33 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ import { useAdaptiveFormContext } from '~/components/adaptive-form'; -import StepContent from '~/components/stepper/step-content'; -import StepContentActions from '~/components/stepper/step-content-actions'; -import StepContentFooter from '~/components/stepper/step-content-footer'; -import TaxRate from '~/pages/settings/setup-tax-rate/tax-rate'; -import useDisplayTaxRate from '~/pages/settings/setup-tax-rate/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'; -import ConditionalSection from '~/components/conditional-section'; import OrderValueConditionSection from '~/components/order-value-condition-section'; import isNonFreeShippingRate from '~/utils/isNonFreeShippingRate'; /** * Form to configure free listigns. - * - * @param {Object} props React props. - * @param {string} [props.submitLabel="Complete setup"] Submit button label. - * @param {boolean} [props.hideTaxRates] Whether to hide tax rate section. */ -const FormContent = ( { - submitLabel = __( 'Complete setup', 'google-listings-and-ads' ), - hideTaxRates, -} ) => { - const { values, isValidForm, handleSubmit, adapter } = - useAdaptiveFormContext(); - const displayTaxRate = useDisplayTaxRate( adapter.audienceCountries ); - const shouldDisplayTaxRate = ! hideTaxRates && displayTaxRate; +const FormContent = () => { + const { values } = useAdaptiveFormContext(); + const shouldDisplayShippingTime = values.shipping_time === 'flat'; const shouldDisplayOrderValueCondition = values.shipping_rate === 'flat' && values.shipping_country_rates.some( isNonFreeShippingRate ); - const handleSubmitClick = ( event ) => { - if ( shouldDisplayTaxRate !== null && isValidForm ) { - return handleSubmit( event ); - } - - adapter.showValidation(); - }; - return ( - + <> { shouldDisplayOrderValueCondition && ( ) } { shouldDisplayShippingTime && } - - - - - - - { submitLabel } - - - - + ); }; 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 8b08c1779a..81585e4473 100644 --- a/js/src/components/free-listings/setup-free-listings/index.js +++ b/js/src/components/free-listings/setup-free-listings/index.js @@ -2,15 +2,15 @@ * External dependencies */ import { useRef } from '@wordpress/element'; +import { createSlotFill } from '@wordpress/components'; import { Form } from '@woocommerce/components'; import { pick, noop } from 'lodash'; /** * Internal dependencies */ -import useStoreCountry from '~/hooks/useStoreCountry'; import AppSpinner from '~/components/app-spinner'; -import Hero from '~/components/free-listings/configure-product-listings/hero'; +import AppButton from '~/components/app-button'; import AdaptiveForm from '~/components/adaptive-form'; import ValidationErrors from '~/components/validation-errors'; import checkErrors from '~/components/free-listings/configure-product-listings/checkErrors'; @@ -32,7 +32,7 @@ const targetAudienceFields = [ 'locale', 'language', 'location', 'countries' ]; * * If we are adding a new settings field, it should be added into this array. */ -const settingsFieldNames = [ 'shipping_rate', 'shipping_time', 'tax_rate' ]; +const settingsFieldNames = [ 'shipping_rate', 'shipping_time' ]; /** * Get settings object from Form values. @@ -53,9 +53,16 @@ const getSettings = ( values ) => { return pick( values, settingsFieldNames ); }; +const alwaysTrue = () => true; + +const { Fill, Slot } = createSlotFill( 'gla/SetupFreeListings/SubmitButton' ); + /** * Setup step to configure free listings. * + * Note that this component requires to specify the location where it wants to + * render its submit button via ``. + * * @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. @@ -66,10 +73,9 @@ const getSettings = ( values ) => { * @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 {() => boolean | Promise} [props.onRequestSubmit] Callback called before the form is submitted. If it returns false, the form will not be submitted. * @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`. - * @param {JSX.Element} props.headerTitle Title in the header block of this setup. - * @param {boolean} [props.hideTaxRates=false] Whether to hide tax rate section, to be forwarded to `FormContent`. + * @param {string} props.submitLabel Submit button label. */ const SetupFreeListings = ( { targetAudience, @@ -81,13 +87,11 @@ const SetupFreeListings = ( { onShippingRatesChange = noop, shippingTimes, onShippingTimesChange = noop, + onRequestSubmit = alwaysTrue, onContinue = noop, submitLabel, - headerTitle, - hideTaxRates = false, } ) => { const formRef = useRef(); - const { code: storeCountryCode } = useStoreCountry(); if ( ! ( targetAudience && settings && shippingRates && shippingTimes ) ) { return ; @@ -97,13 +101,7 @@ const SetupFreeListings = ( { const countries = resolveFinalCountries( values ); const { shipping_country_times: shippingTimesData } = values; - return checkErrors( - values, - shippingTimesData, - countries, - storeCountryCode, - hideTaxRates - ); + return checkErrors( values, shippingTimesData, countries ); }; const handleChange = ( change, values ) => { @@ -208,7 +206,6 @@ const SetupFreeListings = ( { return (
- - + { ( formContext ) => { + const { isValidForm, handleSubmit, adapter } = formContext; + const handleSubmitClick = async ( event ) => { + if ( isValidForm ) { + if ( ! ( await onRequestSubmit() ) ) { + return; + } + return handleSubmit( event ); + } + + adapter.showValidation(); + }; + + return ( + <> + + + + { submitLabel } + + + + ); + } }
); }; +SetupFreeListings.SubmitButton = Slot; + export default SetupFreeListings; diff --git a/js/src/components/main-tab-nav/main-tab-nav.js b/js/src/components/main-tab-nav/main-tab-nav.js index fcdc0c05e6..efe45df049 100644 --- a/js/src/components/main-tab-nav/main-tab-nav.js +++ b/js/src/components/main-tab-nav/main-tab-nav.js @@ -11,6 +11,7 @@ import { glaData } from '~/constants'; import AppTabNav from '~/components/app-tab-nav'; import useMenuEffect from '~/hooks/useMenuEffect'; import GtinMigrationBanner from '~/components/gtin-migration-banner'; +import { getShippingUrl } from '~/utils/urls'; let tabs = [ { @@ -38,6 +39,11 @@ let tabs = [ title: __( 'Settings', 'google-listings-and-ads' ), href: getNewPath( {}, '/google/settings', {} ), }, + { + key: 'shipping', + title: __( 'Shipping', 'google-listings-and-ads' ), + href: getShippingUrl(), + }, ]; // Hide reports tab. diff --git a/js/src/components/order-value-condition-section/minimum-order-card/minimum-order-card.test.js b/js/src/components/order-value-condition-section/minimum-order-card/minimum-order-card.test.js index 5813e59b94..7b316c0d99 100644 --- a/js/src/components/order-value-condition-section/minimum-order-card/minimum-order-card.test.js +++ b/js/src/components/order-value-condition-section/minimum-order-card/minimum-order-card.test.js @@ -43,7 +43,6 @@ const adaptiveFormContextDefaultValues = { shipping_country_times: [], shipping_rate: true, shipping_time: true, - tax_rate: null, }; const adaptiveFormInputPropsDefaultValues = { diff --git a/js/src/components/shipping-rate-section/shipping-rate-section.test.js b/js/src/components/shipping-rate-section/shipping-rate-section.test.js index 053fa09fb0..9d54c2cc1b 100644 --- a/js/src/components/shipping-rate-section/shipping-rate-section.test.js +++ b/js/src/components/shipping-rate-section/shipping-rate-section.test.js @@ -40,7 +40,6 @@ jest.mock( '~/components/adaptive-form', () => ( { shipping_country_times: [], shipping_rate: 'flat', shipping_time: 'flat', - tax_rate: null, }, }; } ), diff --git a/js/src/index.js b/js/src/index.js index 35211b1466..1e10bfd92e 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -50,6 +50,10 @@ const Settings = lazy( () => import( /* webpackChunkName: "settings" */ './pages/settings' ) ); +const Shipping = lazy( () => + import( /* webpackChunkName: "shipping" */ './pages/shipping' ) +); + export const pagePaths = new Set(); const woocommerceTranslation = @@ -72,9 +76,6 @@ addFilter( container: GetStartedPage, path: '/google/start', wpOpenMenu: 'toplevel_page_woocommerce-marketing', - navArgs: { - id: 'google-start', - }, }, { breadcrumbs: [ @@ -100,9 +101,6 @@ addFilter( container: Dashboard, path: '/google/dashboard', wpOpenMenu: 'toplevel_page_woocommerce-marketing', - navArgs: { - id: 'google-dashboard', - }, }, { breadcrumbs: [ @@ -112,9 +110,6 @@ addFilter( container: Reports, path: '/google/reports', wpOpenMenu: 'toplevel_page_woocommerce-marketing', - navArgs: { - id: 'google-reports', - }, }, { breadcrumbs: [ @@ -124,9 +119,6 @@ addFilter( container: ProductFeed, path: '/google/product-feed', wpOpenMenu: 'toplevel_page_woocommerce-marketing', - navArgs: { - id: 'google-product-feed', - }, }, { breadcrumbs: [ @@ -136,9 +128,6 @@ addFilter( container: AttributeMapping, path: '/google/attribute-mapping', wpOpenMenu: 'toplevel_page_woocommerce-marketing', - navArgs: { - id: 'google-attribute-mapping', - }, }, { breadcrumbs: [ @@ -148,9 +137,15 @@ addFilter( container: Settings, path: '/google/settings', wpOpenMenu: 'toplevel_page_woocommerce-marketing', - navArgs: { - id: 'google-settings', - }, + }, + { + breadcrumbs: [ + ...initialBreadcrumbs, + __( 'Shipping', 'google-listings-and-ads' ), + ], + container: Shipping, + path: '/google/shipping', + wpOpenMenu: 'toplevel_page_woocommerce-marketing', }, ]; diff --git a/js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js b/js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js index 8673354a42..63416ea0a9 100644 --- a/js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js +++ b/js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js @@ -7,10 +7,9 @@ import { getHistory } from '@woocommerce/navigation'; /** * Internal dependencies */ -import { FREE_LISTINGS_PROGRAM_ID } from '~/constants'; import AppButton from '~/components/app-button'; import AppModal from '~/components/app-modal'; -import { getEditFreeListingsUrl, getEditCampaignUrl } from '~/utils/urls'; +import { getEditCampaignUrl } from '~/utils/urls'; import { recordGlaEvent } from '~/utils/tracks'; import './edit-program-prompt-modal.scss'; @@ -19,7 +18,7 @@ import './edit-program-prompt-modal.scss'; * * @event gla_dashboard_edit_program_click * @property {string} programId program id - * @property {string} url url (free or paid) + * @property {string} url URL for editing the program (paid campaign) */ /** @@ -35,10 +34,7 @@ const EditProgramPromptModal = ( { programId, onRequestClose } ) => { }; const handleContinueEditClick = () => { - const url = - programId === FREE_LISTINGS_PROGRAM_ID - ? getEditFreeListingsUrl() - : getEditCampaignUrl( programId ); + const url = getEditCampaignUrl( programId ); getHistory().push( url ); diff --git a/js/src/pages/dashboard/all-programs-table-card/index.js b/js/src/pages/dashboard/all-programs-table-card/index.js index 26c8bb1104..039acd0052 100644 --- a/js/src/pages/dashboard/all-programs-table-card/index.js +++ b/js/src/pages/dashboard/all-programs-table-card/index.js @@ -159,16 +159,14 @@ const AllProgramsTableCard = ( props ) => { ), }, { - display: ( + display: el.id !== FREE_LISTINGS_PROGRAM_ID && (
- { el.id !== FREE_LISTINGS_PROGRAM_ID && ( - - ) } +
), }, diff --git a/js/src/pages/dashboard/all-programs-table-card/index.test.js b/js/src/pages/dashboard/all-programs-table-card/index.test.js index 81b0fbcb3a..e3e29ed76a 100644 --- a/js/src/pages/dashboard/all-programs-table-card/index.test.js +++ b/js/src/pages/dashboard/all-programs-table-card/index.test.js @@ -102,13 +102,15 @@ describe( 'AllProgramsTableCard', () => { expect( checkbox ).toBeDisabled(); } ); - it( 'Should render the free listings row without the remove button', () => { + it( 'Should render the free listings row without the edit and remove buttons', () => { render( ); const row = screen.getByRole( 'row', { name: /free listings/i } ); - const button = getRemoveButton( row ); + const editButton = getEditButton( row ); + const removeButton = getRemoveButton( row ); - expect( button ).not.toBeInTheDocument(); + expect( editButton ).not.toBeInTheDocument(); + expect( removeButton ).not.toBeInTheDocument(); } ); it( 'Should render the free listings row with a free daily budget text', () => { @@ -168,17 +170,17 @@ describe( 'AllProgramsTableCard', () => { expect( button2 ).toBeEnabled(); } ); - it( 'Should render the free listings and PMax campaign rows with edit buttons', () => { - mockCampaigns( pmaxCampaign ); + it( 'Should render the edit button for both enabled and disabled PMax campaign rows', () => { + mockCampaigns( pmaxCampaign, pmaxCampaignDisabled ); render( ); - const freeRow = screen.getByRole( 'row', { name: /free listings/i } ); - const pmaxRow = screen.getByRole( 'row', { name: /campaign/i } ); - const freeButton = getEditButton( freeRow ); - const pmaxButton = getEditButton( pmaxRow ); + const rows = screen.getAllByRole( 'row', { name: /campaign/i } ); + const button1 = getEditButton( rows[ 0 ] ); + const button2 = getEditButton( rows[ 1 ] ); - expect( freeButton ).toBeEnabled(); - expect( pmaxButton ).toBeEnabled(); + expect( rows ).toHaveLength( 2 ); + expect( button1 ).toBeEnabled(); + expect( button2 ).toBeEnabled(); } ); it( 'Should render non-PMax campaign with an disabled edit button', () => { @@ -195,13 +197,10 @@ describe( 'AllProgramsTableCard', () => { mockCampaigns( shoppingCampaign, pmaxCampaign ); render( ); - const rows = screen.getAllByRole( 'row', { - name: /free listings|campaign/i, - } ); - const [ freeRow, shoppingRow, pmaxRow ] = rows; + const rows = screen.getAllByRole( 'row', { name: /campaign/i } ); + const [ shoppingRow, pmaxRow ] = rows; const className = 'gla-campaign-edit-button'; - expect( getEditButton( freeRow ) ).not.toHaveClass( className ); expect( getEditButton( shoppingRow ) ).not.toHaveClass( className ); expect( getEditButton( pmaxRow ) ).toHaveClass( className ); } ); diff --git a/js/src/pages/dashboard/index.js b/js/src/pages/dashboard/index.js index c543a3953d..a5bd52bbc5 100644 --- a/js/src/pages/dashboard/index.js +++ b/js/src/pages/dashboard/index.js @@ -20,7 +20,6 @@ import AllProgramsTableCard from './all-programs-table-card'; import { glaData, GUIDE_NAMES } from '~/constants'; import { subpaths, getCreateCampaignUrl } from '~/utils/urls'; import isWCTracksEnabled from '~/utils/isWCTracksEnabled'; -import EditFreeListings from '~/pages/edit-free-listings'; import EditPaidAdsCampaign from '~/pages/edit-paid-ads-campaign'; import CreatePaidAdsCampaign from '~/pages/create-paid-ads-campaign'; import { CTA_CREATE_ANOTHER_CAMPAIGN, CTA_CONFIRM } from './constants'; @@ -59,8 +58,6 @@ const Dashboard = () => { const query = getQuery(); switch ( query.subpath ) { - case subpaths.editFreeListings: - return ; case subpaths.editCampaign: return ; case subpaths.createCampaign: diff --git a/js/src/components/free-listings/configure-product-listings/hero/index.js b/js/src/pages/onboarding/setup-stepper/hero/index.js similarity index 86% rename from js/src/components/free-listings/configure-product-listings/hero/index.js rename to js/src/pages/onboarding/setup-stepper/hero/index.js index ba86bea573..bd1d65cb09 100644 --- a/js/src/components/free-listings/configure-product-listings/hero/index.js +++ b/js/src/pages/onboarding/setup-stepper/hero/index.js @@ -12,16 +12,16 @@ import './index.scss'; /** * Hero element for free listing configuration. - * - * @param {Object} props React props. - * @param {JSX.Element} props.headerTitle Title in the header block. */ -const Hero = ( { headerTitle } ) => { +const Hero = () => { return (

diff --git a/js/src/components/free-listings/configure-product-listings/hero/index.scss b/js/src/pages/onboarding/setup-stepper/hero/index.scss similarity index 100% rename from js/src/components/free-listings/configure-product-listings/hero/index.scss rename to js/src/pages/onboarding/setup-stepper/hero/index.scss diff --git a/js/src/pages/onboarding/setup-stepper/saved-setup-stepper.js b/js/src/pages/onboarding/setup-stepper/saved-setup-stepper.js index 2aed34243a..0e3ade978c 100644 --- a/js/src/pages/onboarding/setup-stepper/saved-setup-stepper.js +++ b/js/src/pages/onboarding/setup-stepper/saved-setup-stepper.js @@ -19,7 +19,7 @@ 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 SetupListings from './setup-listings'; import SetupPaidAds from './setup-paid-ads'; import stepNameKeyMap from './stepNameKeyMap'; import { @@ -157,15 +157,10 @@ const SavedSetupStepper = ( { savedStep } ) => { 'google-listings-and-ads' ), content: ( - { return null; } ); + SetupFreeListings.SubmitButton = () => + jest.fn().mockName( 'SetupFreeListings.SubmitButton' ); + SetupPaidAds.mockReturnValue( null ); } ); diff --git a/js/src/pages/onboarding/setup-stepper/setup-listings.js b/js/src/pages/onboarding/setup-stepper/setup-listings.js new file mode 100644 index 0000000000..031e40ad15 --- /dev/null +++ b/js/src/pages/onboarding/setup-stepper/setup-listings.js @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import StepContent from '~/components/stepper/step-content'; +import StepContentActions from '~/components/stepper/step-content-actions'; +import StepContentFooter from '~/components/stepper/step-content-footer'; +import SetupFreeListings from '~/components/free-listings/setup-free-listings'; +import Hero from './hero'; + +/** + * Renders the onboarding step for setting up the product listings. + * + * @param {Object} props React props to be forwarded to `SetupFreeListings`. + */ +export default function SetupListings( props ) { + return ( + <> + + + + + + + + + + + ); +} diff --git a/js/src/pages/settings/index.js b/js/src/pages/settings/index.js index 39338c24ab..2058a8b610 100644 --- a/js/src/pages/settings/index.js +++ b/js/src/pages/settings/index.js @@ -13,6 +13,7 @@ import useGoogleAccount from '~/hooks/useGoogleAccount'; import useUpdateRestAPIAuthorizeStatusByUrlQuery from '~/hooks/useUpdateRestAPIAuthorizeStatusByUrlQuery'; import { subpaths, getReconnectAccountUrl } from '~/utils/urls'; import { ContactInformationPreview } from '~/components/contact-information'; +import SetupTaxRate from './setup-tax-rate'; import LinkedAccounts from './linked-accounts'; import ReconnectWPComAccount from './reconnect-wpcom-account'; import ReconnectGoogleAccount from './reconnect-google-account'; @@ -65,6 +66,7 @@ const Settings = () => { +

); diff --git a/js/src/pages/settings/setup-tax-rate/index.js b/js/src/pages/settings/setup-tax-rate/index.js new file mode 100644 index 0000000000..541e3d049a --- /dev/null +++ b/js/src/pages/settings/setup-tax-rate/index.js @@ -0,0 +1 @@ +export { default } from './setup-tax-rate'; diff --git a/js/src/pages/settings/setup-tax-rate/setup-tax-rate.js b/js/src/pages/settings/setup-tax-rate/setup-tax-rate.js new file mode 100644 index 0000000000..fda0022392 --- /dev/null +++ b/js/src/pages/settings/setup-tax-rate/setup-tax-rate.js @@ -0,0 +1,128 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Flex } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import Section from '~/components/section'; +import AppSpinner from '~/components/app-spinner'; +import AppButton from '~/components/app-button'; +import AdaptiveForm from '~/components/adaptive-form'; +import TaxRate from './tax-rate'; +import useSettings from '~/hooks/useSettings'; +import useDisplayTaxRate from './useDisplayTaxRate'; +import useTargetAudienceFinalCountryCodes from '~/hooks/useTargetAudienceFinalCountryCodes'; +import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; +import { handleApiError } from '~/utils/handleError'; + +const validTaxRateSet = new Set( [ 'destination', 'manual' ] ); + +/** + * Renders the tax rate setup if the current target audience requires it. + * + * This component won't display the validation error message on UI, + * because it should be obvious to the user that they have to select + * one of the radio options to continue the submission. + */ +export default function SetupTaxRate() { + const { settings, saveSettings, syncSettings } = useSettings(); + const { data: audienceCountries } = useTargetAudienceFinalCountryCodes(); + const shouldDisplayTaxRate = useDisplayTaxRate( audienceCountries ); + const { createNotice } = useDispatchCoreNotices(); + + if ( ! shouldDisplayTaxRate || ! settings?.hasOwnProperty( 'tax_rate' ) ) { + if ( shouldDisplayTaxRate === false ) { + return null; + } + + return ( +
+ +
+ ); + } + + const handleValidate = ( values ) => { + const errors = {}; + + if ( ! validTaxRateSet.has( values.tax_rate ) ) { + errors.tax_rate = __( + 'Please specify tax rate option.', + 'google-listings-and-ads' + ); + } + + return errors; + }; + + const handleSubmit = async ( values ) => { + const nextSettings = { + ...settings, + tax_rate: values.tax_rate, + }; + + return saveSettings( nextSettings ) + .then( syncSettings, ( error ) => { + handleApiError( + error, + __( + 'There was an error saving tax rate.', + 'google-listings-and-ads' + ) + ); + } ) + .catch( ( error ) => { + handleApiError( + error, + __( + 'There was an error synchronizing tax rate to Google Merchant Center.', + 'google-listings-and-ads' + ) + ); + } ) + .then( () => { + createNotice( + 'success', + __( + 'Your change to tax rate has been saved and will be synced to your Google Merchant Center.', + 'google-listings-and-ads' + ) + ); + } ); + }; + + return ( + + { ( formContext ) => { + const { values, isValidForm } = formContext; + const taxRate = values.tax_rate; + const disabled = ! isValidForm || taxRate === settings.tax_rate; + + return ( + + + + { __( + 'Save tax rate', + 'google-listings-and-ads' + ) } + + + + ); + } } + + ); +} diff --git a/js/src/pages/settings/setup-tax-rate/tax-rate.js b/js/src/pages/settings/setup-tax-rate/tax-rate.js index 1d2fa86a6e..6474ea9d61 100644 --- a/js/src/pages/settings/setup-tax-rate/tax-rate.js +++ b/js/src/pages/settings/setup-tax-rate/tax-rate.js @@ -19,10 +19,16 @@ import VerticalGapLayout from '~/components/vertical-gap-layout'; * @fires gla_documentation_link_click with `{ context: 'setup-mc-tax-rate', link_id: 'tax-rate-manual', href: 'https://www.google.com/retail/solutions/merchant-center/' }` */ -const TaxRate = () => { +/** + * Renders the options of tax rate. + * + * @param {Object} props React props. + * @param {JSX.Element} [props.children] Children to be rendered below the card. + */ +const TaxRate = ( { children } ) => { const { getInputProps, - adapter: { renderRequestedValidation }, + adapter: { isSubmitting }, } = useAdaptiveFormContext(); return ( @@ -62,6 +68,7 @@ const TaxRate = () => { ) } value="destination" collapsible + disabled={ isSubmitting } > { __( @@ -78,6 +85,7 @@ const TaxRate = () => { ) } value="manual" collapsible + disabled={ isSubmitting } > { createInterpolateElement( @@ -98,9 +106,9 @@ const TaxRate = () => { - { renderRequestedValidation( 'tax_rate' ) } + { children } ); }; diff --git a/js/src/pages/shipping/confirm-save-modal.js b/js/src/pages/shipping/confirm-save-modal.js new file mode 100644 index 0000000000..7343be36f7 --- /dev/null +++ b/js/src/pages/shipping/confirm-save-modal.js @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AppButton from '~/components/app-button'; +import AppModal from '~/components/app-modal'; +import styles from './confirm-save-modal.module.scss'; + +/** + * Renders a modal to confirm before saving changes. + * + * @param {Object} props React props. + * @param {Function} props.onContinue Callback when the continue button is clicked. + * @param {Function} props.onRequestClose Callback when requesting to close the modal. + */ +export default function ConfirmSaveModal( { onContinue, onRequestClose } ) { + return ( + + { __( `Don't save`, 'google-listings-and-ads' ) } + , + + { __( 'Continue to save', 'google-listings-and-ads' ) } + , + ] } + onRequestClose={ onRequestClose } + > +

+ { __( + 'Results typically improve with time.', + 'google-listings-and-ads' + ) } +

+

+ { __( + 'Changes will result in the loss of any optimisations learned over time.', + 'google-listings-and-ads' + ) } +

+

+ { __( + 'We recommend allowing your listings to run for at least 14 days after set up without changing them for optimal performance.', + 'google-listings-and-ads' + ) } +

+
+ ); +} diff --git a/js/src/pages/shipping/confirm-save-modal.module.scss b/js/src/pages/shipping/confirm-save-modal.module.scss new file mode 100644 index 0000000000..7e2372bc7b --- /dev/null +++ b/js/src/pages/shipping/confirm-save-modal.module.scss @@ -0,0 +1,5 @@ +.confirmationModal { + @include break-small { + max-width: $gla-width-medium-large; + } +} diff --git a/js/src/pages/edit-free-listings/hasUnsavedShippingRates.js b/js/src/pages/shipping/hasUnsavedShippingRates.js similarity index 100% rename from js/src/pages/edit-free-listings/hasUnsavedShippingRates.js rename to js/src/pages/shipping/hasUnsavedShippingRates.js diff --git a/js/src/pages/edit-free-listings/hasUnsavedShippingRates.test.js b/js/src/pages/shipping/hasUnsavedShippingRates.test.js similarity index 100% rename from js/src/pages/edit-free-listings/hasUnsavedShippingRates.test.js rename to js/src/pages/shipping/hasUnsavedShippingRates.test.js diff --git a/js/src/pages/edit-free-listings/index.js b/js/src/pages/shipping/index.js similarity index 79% rename from js/src/pages/edit-free-listings/index.js rename to js/src/pages/shipping/index.js index 83c103c9e4..3b5e0dbbaa 100644 --- a/js/src/pages/edit-free-listings/index.js +++ b/js/src/pages/shipping/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { useEffect, useState } from '@wordpress/element'; -import { getNewPath } from '@woocommerce/navigation'; +import { Flex } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { isEqual } from 'lodash'; @@ -10,16 +10,15 @@ import { isEqual } from 'lodash'; * Internal dependencies */ import { useAppDispatch } from '~/data'; -import TopBar from '~/components/stepper/top-bar'; +import MainTabNav from '~/components/main-tab-nav'; import SetupFreeListings from '~/components/free-listings/setup-free-listings'; +import ConfirmSaveModal from './confirm-save-modal'; import useTargetAudienceFinalCountryCodes from '~/hooks/useTargetAudienceFinalCountryCodes'; import useSettings from '~/hooks/useSettings'; -import useLayout from '~/hooks/useLayout'; import useNavigateAwayPromptEffect from '~/hooks/useNavigateAwayPromptEffect'; import useShippingRates from '~/hooks/useShippingRates'; import useShippingTimes from '~/hooks/useShippingTimes'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; -import HelpIconButton from '~/components/help-icon-button'; import hasUnsavedShippingRates from './hasUnsavedShippingRates'; import useSaveShippingRates from '~/hooks/useSaveShippingRates'; import useSaveShippingTimes from '~/hooks/useSaveShippingTimes'; @@ -27,24 +26,22 @@ import createErrorMessageForRejectedPromises from '~/utils/createErrorMessageFor import { recordGlaEvent } from '~/utils/tracks'; /** - * Saving changes to the free listings. + * Saving changes of audience and/or shipping settings to the free listings. * * @event gla_free_campaign_edited */ /** - * Page Component to edit free listings. - * Provides two steps: - * - Choose your audience - * - Configure your free listings - * Given the user is editing an existing campaign, both steps should be available. - * The displayed step is driven by `pageStep` URL parameter, to make it easier to permalink and navigate back and forth. + * Page component to edit audience and shipping settings. + * + * Note that: + * - This page used to be called "Edit free listings" page. + * - Although it's presented on UI as "Shipping" page, + * it actually contains other Merchant Center settings. * * @fires gla_free_campaign_edited */ -const EditFreeListings = () => { - useLayout( 'full-content' ); - +export default function Shipping() { const { targetAudience: savedTargetAudience, getFinalCountries } = useTargetAudienceFinalCountryCodes(); @@ -76,6 +73,8 @@ const EditFreeListings = () => { const [ shippingTimes, updateShippingTimes ] = useState( savedShippingTimes ); + const [ resolveConfirmation, setResolveConfirmation ] = useState( null ); + // TODO: Consider making it less repetitive. useEffect( () => updateSettings( savedSettings ), [ savedSettings ] ); useEffect( @@ -119,20 +118,23 @@ const EditFreeListings = () => { didRatesChanged || didTimesChanged; - // Confirm leaving the page, if there are any changes and the user is navigating away from our stepper. + // Confirm leaving the page, if there are any changes and the user is navigating away from this page. useNavigateAwayPromptEffect( __( - 'You have unsaved campaign data. Are you sure you want to leave?', + 'You have unsaved changes. Are you sure you want to leave?', 'google-listings-and-ads' ), didAnythingChanged ); - 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' - ); + const handleRequestSubmit = () => { + return new Promise( ( resolve ) => { + setResolveConfirmation( () => ( shouldContinue ) => { + resolve( shouldContinue ); + setResolveConfirmation( null ); + } ); + } ); + }; const handleSetupFreeListingsContinue = async () => { // TODO: Disable the form so the user won't be able to input any changes, which could be disregarded. @@ -163,7 +165,7 @@ const EditFreeListings = () => { createNotice( 'success', __( - 'Your changes to your Free Listings have been saved and will be synced to your Google Merchant Center account.', + 'Your changes have been saved and will be synced to your Google Merchant Center account.', 'google-listings-and-ads' ) ); @@ -188,18 +190,8 @@ const EditFreeListings = () => { return ( <> - - } - backHref={ dashboardURL } - /> + { onShippingRatesChange={ updateShippingRates } shippingTimes={ initialTimes } onShippingTimesChange={ updateShippingTimes } + onRequestSubmit={ handleRequestSubmit } onContinue={ handleSetupFreeListingsContinue } submitLabel={ __( 'Save changes', 'google-listings-and-ads' ) } /> + + + + { resolveConfirmation && ( + resolveConfirmation( true ) } + onRequestClose={ () => resolveConfirmation( false ) } + /> + ) } ); -}; - -export default EditFreeListings; +} diff --git a/js/src/utils/urls.js b/js/src/utils/urls.js index cf343fcfb9..3723ee03bf 100644 --- a/js/src/utils/urls.js +++ b/js/src/utils/urls.js @@ -19,10 +19,10 @@ export const pagePaths = { reports: '/google/reports', productFeed: '/google/product-feed', settings: '/google/settings', + shipping: '/google/shipping', }; export const subpaths = { - editFreeListings: '/free-listings/edit', editCampaign: '/campaigns/edit', createCampaign: '/campaigns/create', editStoreAddress: '/edit-store-address', @@ -36,10 +36,6 @@ const dashboardPath = pagePaths.dashboard; const settingsPath = pagePaths.settings; const reportsPath = pagePaths.reports; -export const getEditFreeListingsUrl = () => { - return getNewPath( { subpath: subpaths.editFreeListings }, dashboardPath ); -}; - /** * Gets the path to the campaign editing page with given query parameters. * @@ -84,6 +80,10 @@ export const getSettingsUrl = () => { return getNewPath( null, settingsPath, null ); }; +export const getShippingUrl = () => { + return getNewPath( null, pagePaths.shipping, null ); +}; + export const geReportsUrl = () => { return getNewPath( null, reportsPath, null ); }; diff --git a/src/Internal/DependencyManagement/CoreServiceProvider.php b/src/Internal/DependencyManagement/CoreServiceProvider.php index f17032ba79..a0b1bff53a 100644 --- a/src/Internal/DependencyManagement/CoreServiceProvider.php +++ b/src/Internal/DependencyManagement/CoreServiceProvider.php @@ -66,6 +66,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Menu\Settings; use Automattic\WooCommerce\GoogleListingsAndAds\Menu\SetupAds; use Automattic\WooCommerce\GoogleListingsAndAds\Menu\SetupMerchantCenter; +use Automattic\WooCommerce\GoogleListingsAndAds\Menu\Shipping; use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\AccountService; use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\AccountService as MerchantAccountService; use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\ContactInformation; @@ -174,6 +175,7 @@ class CoreServiceProvider extends AbstractServiceProvider { RESTControllers::class => true, Service::class => true, Settings::class => true, + Shipping::class => true, SetupAds::class => true, SetupMerchantCenter::class => true, SetupCampaignNote::class => true, @@ -311,6 +313,7 @@ function ( ...$arguments ) { $this->conditionally_share_with_tags( ProductFeed::class ); $this->conditionally_share_with_tags( AttributeMapping::class ); $this->conditionally_share_with_tags( Settings::class ); + $this->conditionally_share_with_tags( Shipping::class ); $this->share_with_tags( TrackerSnapshot::class ); $this->conditionally_share_with_tags( EventTracking::class, ContainerInterface::class ); $this->conditionally_share_with_tags( RESTControllers::class, ContainerInterface::class ); diff --git a/src/Menu/AttributeMapping.php b/src/Menu/AttributeMapping.php index 1b7a770a84..7bdd0cde63 100644 --- a/src/Menu/AttributeMapping.php +++ b/src/Menu/AttributeMapping.php @@ -22,14 +22,10 @@ public function register(): void { function () { wc_admin_register_page( [ - 'title' => __( 'Attribute Mapping', 'google-listings-and-ads' ), - 'parent' => 'google-listings-and-ads-category', - 'path' => '/google/attribute-mapping', - 'id' => 'google-attribute-mapping', - 'nav_args' => [ - 'order' => 40, - 'parent' => 'google-listings-and-ads-category', - ], + 'title' => __( 'Attribute Mapping', 'google-listings-and-ads' ), + 'parent' => 'google-listings-and-ads-category', + 'path' => '/google/attribute-mapping', + 'id' => 'google-attribute-mapping', ] ); } diff --git a/src/Menu/ProductFeed.php b/src/Menu/ProductFeed.php index 9d0b45b64e..9460c82833 100644 --- a/src/Menu/ProductFeed.php +++ b/src/Menu/ProductFeed.php @@ -22,14 +22,10 @@ public function register(): void { function () { wc_admin_register_page( [ - 'title' => __( 'Product Feed', 'google-listings-and-ads' ), - 'parent' => 'google-listings-and-ads-category', - 'path' => '/google/product-feed', - 'id' => 'google-product-feed', - 'nav_args' => [ - 'order' => 30, - 'parent' => 'google-listings-and-ads-category', - ], + 'title' => __( 'Product Feed', 'google-listings-and-ads' ), + 'parent' => 'google-listings-and-ads-category', + 'path' => '/google/product-feed', + 'id' => 'google-product-feed', ] ); } diff --git a/src/Menu/Reports.php b/src/Menu/Reports.php index 1de143d36f..d8b44d0212 100644 --- a/src/Menu/Reports.php +++ b/src/Menu/Reports.php @@ -22,14 +22,10 @@ public function register(): void { function () { wc_admin_register_page( [ - 'title' => __( 'Reports', 'google-listings-and-ads' ), - 'parent' => 'google-listings-and-ads-category', - 'path' => '/google/reports', - 'id' => 'google-reports', - 'nav_args' => [ - 'order' => 20, - 'parent' => 'google-listings-and-ads-category', - ], + 'title' => __( 'Reports', 'google-listings-and-ads' ), + 'parent' => 'google-listings-and-ads-category', + 'path' => '/google/reports', + 'id' => 'google-reports', ] ); } diff --git a/src/Menu/Settings.php b/src/Menu/Settings.php index 1701fad9ce..8bf406e745 100644 --- a/src/Menu/Settings.php +++ b/src/Menu/Settings.php @@ -22,14 +22,10 @@ public function register(): void { function () { wc_admin_register_page( [ - 'title' => __( 'Settings', 'google-listings-and-ads' ), - 'parent' => 'google-listings-and-ads-category', - 'path' => '/google/settings', - 'id' => 'google-settings', - 'nav_args' => [ - 'order' => 50, - 'parent' => 'google-listings-and-ads-category', - ], + 'title' => __( 'Settings', 'google-listings-and-ads' ), + 'parent' => 'google-listings-and-ads-category', + 'path' => '/google/settings', + 'id' => 'google-settings', ] ); } diff --git a/src/Menu/Shipping.php b/src/Menu/Shipping.php new file mode 100644 index 0000000000..ca7a2d83f7 --- /dev/null +++ b/src/Menu/Shipping.php @@ -0,0 +1,34 @@ + 'google-shipping', + 'parent' => 'google-listings-and-ads-category', + 'title' => __( 'Shipping', 'google-listings-and-ads' ), + 'path' => '/google/shipping', + ] + ); + } + ); + } +} diff --git a/src/Tracking/README.md b/src/Tracking/README.md index 8f4aa605e8..e949ef0d9f 100644 --- a/src/Tracking/README.md +++ b/src/Tracking/README.md @@ -279,15 +279,15 @@ Triggered when the save button in contact information page is clicked. #### Emitters - [`EditStoreAddress`](../../js/src/pages/settings/edit-store-address.js#L41) -### [`gla_dashboard_edit_program_click`](../../js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js#L17) +### [`gla_dashboard_edit_program_click`](../../js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js#L16) Triggered when "continue" to edit program button is clicked. #### Properties | name | type | description | | ---- | ---- | ----------- | `programId` | `string` | program id -`url` | `string` | url (free or paid) +`url` | `string` | URL for editing the program (paid campaign) #### Emitters -- [`EditProgramPromptModal`](../../js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js#L32) when "Continue to edit" is clicked. +- [`EditProgramPromptModal`](../../js/src/pages/dashboard/all-programs-table-card/edit-program-button/edit-program-prompt-modal.js#L31) when "Continue to edit" is clicked. ### [`gla_datepicker_update`](../../js/src/utils/tracks.js#L135) Triggered when datepicker (date ranger picker) is updated, @@ -305,14 +305,14 @@ Triggered when datepicker (date ranger picker) is updated, - [`ProductsReportFilters`](../../js/src/pages/reports/products/products-report-filters.js#L41) - [`ProgramsReportFilters`](../../js/src/pages/reports/programs/programs-report-filters.js#L43) -### [`gla_disconnected_accounts`](../../js/src/pages/settings/linked-accounts.js#L32) +### [`gla_disconnected_accounts`](../../js/src/pages/settings/linked-accounts.js#L31) Accounts are disconnected from the Setting page #### Properties | name | type | description | | ---- | ---- | ----------- | `context` | `string` | (`all-accounts`\|`ads-account`) - indicate which accounts have been disconnected. #### Emitters -- [`exports`](../../js/src/pages/settings/linked-accounts.js#L42) +- [`exports`](../../js/src/pages/settings/linked-accounts.js#L41) ### [`gla_documentation_link_click`](../../js/src/components/app-documentation-link/index.js#L6) When a documentation link is clicked. @@ -349,7 +349,7 @@ When a documentation link is clicked. - [`FreeAdCredit`](../../js/src/pages/dashboard/summary-section/paid-features/free-ad-credit.js#L19) with `{ context: 'dashboard', link_id: 'free-ad-credit-terms', href: 'https://www.google.com/ads/coupons/terms/' }` - [`GetStartedCard`](../../js/src/pages/get-started/get-started-card/index.js#L23) with `{ context: 'get-started', linkId: 'wp-terms-of-service', href: 'https://wordpress.com/tos/' }`. - [`GetStartedWithHeroCard`](../../js/src/pages/get-started/get-started-with-hero-card/index.js#L23) with `{ context: 'get-started-with-hero', linkId: 'wp-terms-of-service', href: 'https://wordpress.com/tos/' }`. -- [`GoogleMCDisclaimer`](../../js/src/pages/onboarding/setup-stepper/setup-accounts/index.js#L37) +- [`GoogleMCDisclaimer`](../../js/src/pages/onboarding/setup-stepper/setup-accounts/index.js#L36) - with `{ context: 'setup-mc-accounts', link_id: 'comparison-shopping-services', href: 'https://support.google.com/merchants/topic/9080307' }` - with `{ context: 'setup-mc-accounts', link_id: 'comparison-shopping-partners-find-a-partner', href: 'https://comparisonshoppingpartners.withgoogle.com/find_a_partner/' }` - [`IssuesTableDataModal`](../../js/src/pages/product-feed/issues-table-card/issues-table-data-modal.js#L21) with { context: 'issues-data-table-modal' } @@ -360,9 +360,6 @@ When a documentation link is clicked. - with `{ context: 'setup-mc-shipping', link_id: 'shipping-read-more', href: 'https://support.google.com/merchants/answer/7050921' }` - with `{ context: 'setup-mc-shipping', link_id: 'shipping-manual', href: 'https://www.google.com/retail/solutions/merchant-center/' }` - [`ShippingTimeSection`](../../js/src/components/free-listings/configure-product-listings/shipping-time-section.js#L17) with `{ context: 'setup-mc-shipping', link_id: 'shipping-read-more', href: 'https://support.google.com/merchants/answer/7050921' }` -- [`TaxRate`](../../js/src/components/free-listings/configure-product-listings/tax-rate.js#L22) - - with `{ context: 'setup-mc-tax-rate', link_id: 'tax-rate-read-more', href: 'https://support.google.com/merchants/answer/160162' }` - - with `{ context: 'setup-mc-tax-rate', link_id: 'tax-rate-manual', href: 'https://www.google.com/retail/solutions/merchant-center/' }` - [`TermsModal`](../../js/src/components/google-ads-account-card/terms-modal/index.js#L36) - with `{ context: 'setup-ads', link_id: 'shopping-ads-policies', href: 'https://support.google.com/merchants/answer/6149970' }` - with `{ context: 'setup-ads', link_id: 'google-ads-terms-of-service', href: 'https://support.google.com/adspolicy/answer/54818' }` @@ -471,10 +468,10 @@ Clicking on the link to view free ad credit value by country. #### Emitters - [`FreeAdCredit`](../../js/src/components/free-ad-credit/index.js#L27) with `{ context: 'setup-ads' }`. -### [`gla_free_campaign_edited`](../../js/src/pages/edit-free-listings/index.js#L30) -Saving changes to the free listings. +### [`gla_free_campaign_edited`](../../js/src/pages/shipping/index.js#L28) +Saving changes of audience and/or shipping settings to the free listings. #### Emitters -- [`EditFreeListings`](../../js/src/pages/edit-free-listings/index.js#L46) +- [`exports`](../../js/src/pages/shipping/index.js#L44) ### [`gla_google_account_connect_button_click`](../../js/src/utils/tracks.js#L175) Clicking on the button to connect Google account. @@ -590,7 +587,7 @@ A modal is closed. `action` | `string` | Indicates the modal is closed by what action (e.g. `maybe-later`\|`dismiss` \| `create-another-campaign`) - `maybe-later` is used when the "Maybe later" button on the modal is clicked - `dismiss` is used when the modal is dismissed by clicking on "X" icon, overlay, generic "Cancel" button, or pressing ESC - `create-another-campaign` is used when the button "Create another campaign" is clicked - `create-paid-campaign` is used when the button "Create paid campaign" is clicked - `confirm` is used when the button "Confirm", "Save" or similar generic "Accept" button is clicked #### Emitters - [`AttributeMappingTable`](../../js/src/pages/attribute-mapping/attribute-mapping-table.js#L59) When any of the modals is closed -- [`Dashboard`](../../js/src/pages/dashboard/index.js#L34) when CES modal is closed. +- [`Dashboard`](../../js/src/pages/dashboard/index.js#L33) when CES modal is closed. - [`ReviewRequest`](../../js/src/pages/product-feed/review-request/index.js#L31) with `action: 'request-review-success' | 'maybe-later' | 'dismiss', context: REQUEST_REVIEW` - [`SubmissionSuccessGuide`](../../js/src/pages/product-feed/submission-success-guide/index.js#L157) with `action: 'create-paid-campaign' | 'maybe-later' | 'view-product-feed' | 'dismiss'` @@ -627,7 +624,7 @@ Clicking on the skip paid ads button to complete the onboarding flow. `billing_method_status` | `string` | The status of billing method of merchant's Google Ads addcount e.g. 'unknown', 'pending', 'approved', 'cancelled' `campaign_form_validation` | `string` | Whether the entered paid campaign form data are valid, e.g. 'unknown', 'valid', 'invalid' -### [`gla_onboarding_complete_with_paid_ads_button_click`](../../js/src/pages/onboarding/setup-stepper/setup-paid-ads.js#L29) +### [`gla_onboarding_complete_with_paid_ads_button_click`](../../js/src/pages/onboarding/setup-stepper/setup-paid-ads.js#L28) Clicking on the "Complete setup" button to complete the onboarding flow with paid ads. #### Properties | name | type | description | @@ -635,7 +632,7 @@ Clicking on the "Complete setup" button to complete the onboarding flow with pai `budget` | `number` | The budget for the campaign `audiences` | `string` | The targeted audiences for the campaign #### Emitters -- [`exports`](../../js/src/pages/onboarding/setup-stepper/setup-paid-ads.js#L42) +- [`exports`](../../js/src/pages/onboarding/setup-stepper/setup-paid-ads.js#L41) ### [`gla_open_ads_account_claim_invitation_button_click`](../../js/src/components/google-ads-account-card/claim-account-button.js#L15) Clicking on the button to open the invitation page for claiming the newly created Google Ads account. @@ -659,7 +656,7 @@ Triggered when moving to another step during creating/editing a campaign. - [`CreatePaidAdsCampaign`](../../js/src/pages/create-paid-ads-campaign/index.js#L48) - with `{ context: 'create-ads', triggered_by: 'step1-continue-button', action: 'go-to-step2' }`. - with `{ context: 'create-ads', triggered_by: 'stepper-step1-button', action: 'go-to-step1' }`. -- [`EditPaidAdsCampaign`](../../js/src/pages/edit-paid-ads-campaign/index.js#L57) +- [`EditPaidAdsCampaign`](../../js/src/pages/edit-paid-ads-campaign/index.js#L69) - with `{ context: 'edit-ads', triggered_by: 'step1-continue-button', action: 'go-to-step2' }`. - with `{ context: 'edit-ads', triggered_by: 'stepper-step1-button', action: 'go-to-step1' }`. diff --git a/tests/e2e/specs/dashboard/edit-free-listings.test.js b/tests/e2e/specs/dashboard/edit-free-listings.test.js deleted file mode 100644 index 2695856e5a..0000000000 --- a/tests/e2e/specs/dashboard/edit-free-listings.test.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * External dependencies - */ -import { expect, test } from '@playwright/test'; - -/** - * Internal dependencies - */ -import { clearOnboardedMerchant, setOnboardedMerchant } from '../../utils/api'; -import { LOAD_STATE } from '../../utils/constants'; -import { checkSnackBarMessage } from '../../utils/page'; -import DashboardPage from '../../utils/pages/dashboard'; -import EditFreeListingsPage from '../../utils/pages/edit-free-listings'; - -test.use( { storageState: process.env.ADMINSTATE } ); - -test.describe.configure( { mode: 'serial' } ); - -/** - * @type {import('../../utils/pages/dashboard.js').default} dashboardPage - */ -let dashboardPage = null; - -/** - * @type {import('../../utils/pages/edit-free-listings.js').default} editFreeListingsPage - */ -let editFreeListingsPage = null; - -/** - * @type {import('@playwright/test').Page} page - */ -let page = null; - -test.describe( 'Edit Free Listings', () => { - test.beforeAll( async ( { browser } ) => { - page = await browser.newPage(); - dashboardPage = new DashboardPage( page ); - editFreeListingsPage = new EditFreeListingsPage( page ); - - await editFreeListingsPage.fulfillSettings( { - shipping_rate: 'flat', - tax_rate: 'destination', - } ); - await setOnboardedMerchant(); - await dashboardPage.mockRequests(); - await dashboardPage.goto(); - } ); - - test.afterAll( async () => { - await clearOnboardedMerchant(); - await page.close(); - } ); - - test( 'Dashboard page contains Free Listings', async () => { - await expect( dashboardPage.freeListingRow ).toContainText( - 'Free listings' - ); - - await expect( dashboardPage.editFreeListingButton ).toBeEnabled(); - } ); - - test( 'Edit Free Listings should show modal', async () => { - await dashboardPage.clickEditFreeListings(); - - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - - const continueToEditButton = - await dashboardPage.getContinueToEditButton(); - const dontEditButton = await dashboardPage.getDontEditButton(); - await expect( continueToEditButton ).toBeEnabled(); - await expect( dontEditButton ).toBeEnabled(); - await dashboardPage.clickContinueToEditButton(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - } ); - - test( 'Check recommended shipping settings', async () => { - await editFreeListingsPage.checkRecommendShippingSettings(); - await editFreeListingsPage.fillCountriesShippingTimeInput( '5', '10' ); - await editFreeListingsPage.checkDestinationBasedTaxRates(); - const saveChangesButton = - await editFreeListingsPage.getSaveChangesButton(); - await expect( saveChangesButton ).toBeEnabled(); - } ); - - test( 'Save changes', async () => { - const awaitForRequests = editFreeListingsPage.registerSavingRequests(); - await editFreeListingsPage.mockSuccessfulSavingSettingsResponse(); - await editFreeListingsPage.clickSaveChanges(); - const requests = await awaitForRequests; - const settingsResponse = await ( - await requests[ 0 ].response() - ).json(); - - expect( settingsResponse.status ).toBe( 'success' ); - expect( settingsResponse.message ).toBe( - 'Merchant Center Settings successfully updated.' - ); - expect( settingsResponse.data.shipping_time ).toBe( 'flat' ); - expect( settingsResponse.data.tax_rate ).toBe( 'destination' ); - - await checkSnackBarMessage( - page, - 'Your changes to your Free Listings have been saved and will be synced to your Google Merchant Center account.' - ); - } ); -} ); diff --git a/tests/e2e/specs/settings/settings.test.js b/tests/e2e/specs/settings/settings.test.js new file mode 100644 index 0000000000..b94eaf854a --- /dev/null +++ b/tests/e2e/specs/settings/settings.test.js @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import { expect, test } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { clearOnboardedMerchant, setOnboardedMerchant } from '../../utils/api'; +import SettingsPage from '../../utils/pages/settings'; + +test.use( { storageState: process.env.ADMINSTATE } ); + +test.describe.configure( { mode: 'serial' } ); + +/** + * @type {import('../../utils/pages/settings.js').default} settingsPage + */ +let settingsPage = null; + +/** + * @type {import('@playwright/test').Page} page + */ +let page = null; + +test.describe( 'Settings', () => { + test.beforeAll( async ( { browser } ) => { + page = await browser.newPage(); + settingsPage = new SettingsPage( page ); + + await setOnboardedMerchant(); + await settingsPage.mockRequests(); + } ); + + test.afterAll( async () => { + await clearOnboardedMerchant(); + await page.close(); + } ); + + test.describe( 'Tax rate setup', () => { + test( 'Should not show the setup when selling in regions unrelated to the US', async () => { + // Mock the country where the store is located as outside of the US. + const once = settingsPage.fulfillTimes( 1 ); + await once.fulfillRequest( + // Having`(\w+%2C)*` is because multiple option queries may be consolidated into a single request. + /\/wc-admin\/options\?options=(\w+%2C)*woocommerce_default_country\b/, + { woocommerce_default_country: 'JP' } + ); + await settingsPage.mockTargetAudienceCountries( 'JP' ); + await settingsPage.goto(); + + await expect( + page.getByRole( 'heading', { name: 'Settings' } ) + ).toBeVisible(); + + await expect( + page.locator( '.woocommerce-spinner' ).first() + ).not.toBeVisible(); + + await expect( + page.getByText( 'Tax rate (required for U.S. only)' ) + ).not.toBeVisible(); + } ); + + test( 'Should show the setup when selling to the US and can update the setting', async () => { + await settingsPage.mockTargetAudienceCountries(); + await settingsPage.goto(); + + await expect( + page.getByText( 'Tax rate (required for U.S. only)' ) + ).toBeVisible(); + + const saveButton = page.getByRole( 'button', { + name: 'Save tax rate', + } ); + const saveSpinner = saveButton.locator( '.woocommerce-spinner' ); + const option = page.getByRole( 'radio', { checked: false } ); + const optionValue = option.getAttribute( 'value' ); + + // Save button will become clickable after selecting another option. + await expect( saveButton ).toBeDisabled(); + await option.check(); + await expect( saveButton ).toBeEnabled(); + + // Submit the change, and then the save button will go through loading state + // and stay disabled both during and after submission. + await saveButton.click(); + await expect( saveSpinner ).toBeVisible(); + await expect( saveButton ).toBeDisabled(); + await expect( saveSpinner ).not.toBeVisible(); + await expect( saveButton ).toBeDisabled(); + + // Reload to assert the setting has been actually saved. + await page.reload(); + await expect( + page.getByRole( 'radio', { checked: true } ) + ).toHaveAttribute( 'value', optionValue ); + } ); + } ); +} ); diff --git a/tests/e2e/specs/shipping.test.js b/tests/e2e/specs/shipping.test.js new file mode 100644 index 0000000000..447f1faba8 --- /dev/null +++ b/tests/e2e/specs/shipping.test.js @@ -0,0 +1,79 @@ +/** + * External dependencies + */ +import { expect, test } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { clearOnboardedMerchant, setOnboardedMerchant } from '../utils/api'; +import { checkSnackBarMessage } from '../utils/page'; +import ShippingPage from '../utils/pages/shipping'; + +test.use( { storageState: process.env.ADMINSTATE } ); + +test.describe.configure( { mode: 'serial' } ); + +/** + * @type {import('../utils/pages/shipping.js').default} shippingPage + */ +let shippingPage = null; + +/** + * @type {import('@playwright/test').Page} page + */ +let page = null; + +test.describe( 'Shipping', () => { + test.beforeAll( async ( { browser } ) => { + page = await browser.newPage(); + shippingPage = new ShippingPage( page ); + + await setOnboardedMerchant(); + await shippingPage.mockRequests(); + await shippingPage.goto(); + } ); + + test.afterAll( async () => { + await clearOnboardedMerchant(); + await page.close(); + } ); + + test( 'Should show confirmation modal before saving', async () => { + const modal = shippingPage.getConfirmationModal(); + + await shippingPage.clickSaveChanges(); + await expect( modal ).toBeVisible(); + + await shippingPage.getDontSaveButton().click(); + await expect( modal ).not.toBeVisible(); + } ); + + test( 'Check recommended shipping settings', async () => { + await shippingPage.checkRecommendShippingSettings(); + await shippingPage.fillCountriesShippingTimeInput( '5', '10' ); + const saveChangesButton = shippingPage.getSaveChangesButton(); + await expect( saveChangesButton ).toBeEnabled(); + } ); + + test( 'Save changes', async () => { + const awaitForRequests = shippingPage.registerSavingRequests(); + await shippingPage.clickSaveChanges(); + await shippingPage.getContinueSaveButton().click(); + const requests = await awaitForRequests; + const settingsResponse = await ( + await requests[ 0 ].response() + ).json(); + + expect( settingsResponse.status ).toBe( 'success' ); + expect( settingsResponse.message ).toBe( + 'Merchant Center Settings successfully updated.' + ); + expect( settingsResponse.data.shipping_time ).toBe( 'flat' ); + + await checkSnackBarMessage( + page, + 'Your changes have been saved and will be synced to your Google Merchant Center account.' + ); + } ); +} ); diff --git a/tests/e2e/utils/pages/dashboard.js b/tests/e2e/utils/pages/dashboard.js index d671fbaed9..80f1737fbb 100644 --- a/tests/e2e/utils/pages/dashboard.js +++ b/tests/e2e/utils/pages/dashboard.js @@ -14,12 +14,6 @@ export default class DashboardPage extends MockRequests { constructor( page ) { super( page ); this.page = page; - this.freeListingRow = this.page.locator( - '.gla-all-programs-table-card table tr:nth-child(2)' - ); - this.editFreeListingButton = this.freeListingRow.getByRole( 'button', { - name: 'Edit', - } ); this.googleAdsSummaryCard = this.page.locator( '.gla-dashboard__performance .gla-summary-card:nth-child(1)' ); @@ -112,47 +106,4 @@ export default class DashboardPage extends MockRequests { { waitUntil: LOAD_STATE.DOM_CONTENT_LOADED } ); } - - /** - * Click the edit free listings button. - * - * @return {Promise} - */ - async clickEditFreeListings() { - await this.editFreeListingButton.click(); - } - - /** - * Get the continue to edit button from the modal. - * - * @return {Promise} Get the continue to edit button from the modal. - */ - async getContinueToEditButton() { - return this.page.getByRole( 'button', { - name: 'Continue to edit', - exact: true, - } ); - } - - /** - * Get the don't edit button from the modal. - * - * @return {Promise} Get the don't edit button from the modal. - */ - async getDontEditButton() { - return this.page.getByRole( 'button', { - name: "Don't edit", - exact: true, - } ); - } - - /** - * Click the continue to edit button from the modal. - * - * @return {Promise} - */ - async clickContinueToEditButton() { - const continueToEditButton = await this.getContinueToEditButton(); - await continueToEditButton.click(); - } } diff --git a/tests/e2e/utils/pages/settings.js b/tests/e2e/utils/pages/settings.js index 4ae90c4385..f66acdec07 100644 --- a/tests/e2e/utils/pages/settings.js +++ b/tests/e2e/utils/pages/settings.js @@ -34,6 +34,35 @@ export default class SettingsPage extends MockRequests { ); } + /** + * Mock all requests related to external accounts such as Merchant Center, Google, etc. + * + * @return {Promise} + */ + async mockRequests() { + await this.mockJetpackConnected(); + await this.mockGoogleConnected(); + await this.mockMCConnected(); + await this.mockAdsAccountConnected(); + await this.mockContactInformation(); + await this.mockSuccessfulSettingsSyncRequest(); + } + + /** + * Mock the target audience request with the given countries. + * + * @param {Array} [countries=['US']] country codes to be mocked. + * @return {Promise} + */ + async mockTargetAudienceCountries( ...countries ) { + await this.fulfillTargetAudience( { + location: 'selected', + countries: countries.length ? countries : [ 'US' ], + locale: 'en_US', + language: 'English', + } ); + } + /** * Get the Grant Access Button. * diff --git a/tests/e2e/utils/pages/edit-free-listings.js b/tests/e2e/utils/pages/shipping.js similarity index 58% rename from tests/e2e/utils/pages/edit-free-listings.js rename to tests/e2e/utils/pages/shipping.js index 0c086cdeea..0f19e70d4a 100644 --- a/tests/e2e/utils/pages/edit-free-listings.js +++ b/tests/e2e/utils/pages/shipping.js @@ -1,9 +1,15 @@ +/** + * External dependencies + */ +import { Locator } from '@playwright/test'; + /** * Internal dependencies */ import MockRequests from '../mock-requests'; +import { LOAD_STATE } from '../constants'; -export default class EditFreeListingsPage extends MockRequests { +export default class ShippingPage extends MockRequests { /** * @param {import('@playwright/test').Page} page */ @@ -12,26 +18,90 @@ export default class EditFreeListingsPage extends MockRequests { this.page = page; } + /** + * Mock all requests related to external resources. + * + * @return {Promise} + */ + async mockRequests() { + await this.mockSuccessfulSettingsSyncRequest(); + + await this.fulfillSettings( { + shipping_rate: 'flat', + shipping_time: 'flat', + tax_rate: 'destination', + } ); + + await this.fulfillTargetAudience( { + location: 'selected', + countries: [ 'US' ], + locale: 'en_US', + language: 'English', + } ); + } + + /** + * Go to the Shipping page. + * + * @return {Promise} + */ + async goto() { + await this.page.goto( + '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fshipping', + { waitUntil: LOAD_STATE.DOM_CONTENT_LOADED } + ); + } + /** * Get Save Changes button. * - * @return {Promise} Get Save Changes button. + * @return {Locator} Get Save Changes button. */ - async getSaveChangesButton() { + getSaveChangesButton() { return this.page.getByRole( 'button', { name: 'Save changes', exact: true, } ); } + /** + * Get the "Don't save" button. + * + * @return {Locator} The button. + */ + getConfirmationModal() { + return this.page.getByRole( 'dialog', { name: 'Before you save…' } ); + } + + /** + * Get the "Don't save" button in the confirmation modal. + * + * @return {Locator} The button. + */ + getDontSaveButton() { + return this.getConfirmationModal().getByRole( 'button', { + name: `Don't save`, + } ); + } + + /** + * Get the "Continue to save" button in the confirmation modal. + * + * @return {Locator} The button. + */ + getContinueSaveButton() { + return this.getConfirmationModal().getByRole( 'button', { + name: 'Continue to save', + } ); + } + /** * Click the Save Changes button. * * @return {Promise} */ async clickSaveChanges() { - const saveChangesButton = await this.getSaveChangesButton(); - await saveChangesButton.click(); + await this.getSaveChangesButton().click(); } /** @@ -59,29 +129,6 @@ export default class EditFreeListingsPage extends MockRequests { await timesLocator.last().fill( max ); } - /** - * Check the destination based tax rates. - * - * @return {Promise} - */ - async checkDestinationBasedTaxRates() { - await this.page - .locator( 'text=My store uses destination-based tax rates.' ) - .check(); - } - - /** - * Mock the successful saving settings response. - * - * @return {Promise} - */ - async mockSuccessfulSavingSettingsResponse() { - await this.fulfillSettingsSync( { - status: 'success', - message: 'Successfully synchronized settings with Google.', - } ); - } - /** * Register the requests when the save button is clicked. *