diff --git a/js/src/components/audience-country-select.js b/js/src/components/audience-country-select.js
deleted file mode 100644
index 02a39ef4bc..0000000000
--- a/js/src/components/audience-country-select.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Internal dependencies
- */
-import SupportedCountrySelect from '.~/components/supported-country-select';
-import AppSpinner from '.~/components/app-spinner';
-import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
-
-/**
- * @typedef {import('.~/data/actions').CountryCode} CountryCode
- */
-
-/**
- * Returns a SupportedCountrySelect component with list of countries grouped by continents.
- * And SupportedCountrySelect will be rendered via TreeSelectControl component.
- *
- * This component is for selecting countries under the merchant selected targeting audiences.
- *
- * @param {Object} props React props.
- * @param {Array} [props.additionalCountryCodes] Additional countries that are not in the target audience countries and need to be selectable.
- * @param {Object} props.restProps Props to be forwarded to SupportedCountrySelect.
- */
-const AudienceCountrySelect = ( { additionalCountryCodes, ...restProps } ) => {
- let { data: countryCodes } = useTargetAudienceFinalCountryCodes();
-
- if ( ! countryCodes ) {
- return ;
- }
-
- if ( additionalCountryCodes ) {
- countryCodes = Array.from(
- new Set( countryCodes.concat( additionalCountryCodes ) )
- );
- }
-
- return (
-
- );
-};
-
-export default AudienceCountrySelect;
diff --git a/js/src/components/paid-ads/ads-campaign.js b/js/src/components/paid-ads/ads-campaign.js
deleted file mode 100644
index c65ca371da..0000000000
--- a/js/src/components/paid-ads/ads-campaign.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { createInterpolateElement } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import StepContent from '.~/components/stepper/step-content';
-import StepContentHeader from '.~/components/stepper/step-content-header';
-import StepContentFooter from '.~/components/stepper/step-content-footer';
-import StepContentActions from '.~/components/stepper/step-content-actions';
-import AppDocumentationLink from '.~/components/app-documentation-link';
-import AppButton from '.~/components/app-button';
-import { useAdaptiveFormContext } from '.~/components/adaptive-form';
-import AudienceSection from './audience-section';
-import BudgetSection from './budget-section';
-import { CampaignPreviewCard } from './campaign-preview';
-import PaidAdsFaqsPanel from './faqs-panel';
-
-/**
- * @typedef {import('.~/data/actions').Campaign} Campaign
- */
-
-/**
- * Renders the container of the form content for campaign management.
- *
- * Please note that this component relies on an CampaignAssetsForm's context and custom adapter,
- * so it expects a `CampaignAssetsForm` to existing in its parents.
- *
- * @fires gla_documentation_link_click with `{ context: 'create-ads' | 'edit-ads' | 'setup-ads', link_id: 'see-what-ads-look-like', href: 'https://support.google.com/google-ads/answer/6275294' }`
- *
- * @param {Object} props React props.
- * @param {Campaign} [props.campaign] Campaign data to be edited. If not provided, this component will show campaign creation UI.
- * @param {() => void} props.onContinue Callback called once continue button is clicked.
- * @param {'create-ads'|'edit-ads'|'setup-ads'} props.trackingContext A context indicating which page this component is used on. This will be the value of `context` in the track event properties.
- */
-export default function AdsCampaign( {
- campaign,
- onContinue,
- trackingContext,
-} ) {
- const isCreation = ! campaign;
- const formContext = useAdaptiveFormContext();
- const { isValidForm } = formContext;
-
- const disabledBudgetSection = ! formContext.values.countryCodes.length;
- const helperText = isCreation
- ? __(
- 'You can only choose from countries you’ve selected during product listings configuration.',
- 'google-listings-and-ads'
- )
- : __(
- 'Once a campaign has been created, you cannot change the target country(s).',
- 'google-listings-and-ads'
- );
-
- return (
-
- See what your ads will look like.',
- 'google-listings-and-ads'
- ),
- {
- link: (
-
- ),
- }
- ) }
- />
-
-
-
-
-
-
-
-
- { __( 'Continue', 'google-listings-and-ads' ) }
-
-
-
-
-
- );
-}
diff --git a/js/src/components/paid-ads/ads-campaign/ads-campaign.js b/js/src/components/paid-ads/ads-campaign/ads-campaign.js
new file mode 100644
index 0000000000..0ff2497eac
--- /dev/null
+++ b/js/src/components/paid-ads/ads-campaign/ads-campaign.js
@@ -0,0 +1,189 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ createInterpolateElement,
+ useState,
+ useEffect,
+} from '@wordpress/element';
+import { noop } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { useAdaptiveFormContext } from '.~/components/adaptive-form';
+import StepContent from '.~/components/stepper/step-content';
+import StepContentHeader from '.~/components/stepper/step-content-header';
+import StepContentActions from '.~/components/stepper/step-content-actions';
+import StepContentFooter from '.~/components/stepper/step-content-footer';
+import AppDocumentationLink from '.~/components/app-documentation-link';
+import AppButton from '.~/components/app-button';
+import PaidAdsFaqsPanel from './faqs-panel';
+import PaidAdsFeaturesSection from './paid-ads-features-section';
+import PaidAdsSetupSections from './paid-ads-setup-sections';
+import SkipButton from './skip-button';
+import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
+import { ACTION_SKIP, ACTION_COMPLETE } from './constants';
+
+/**
+ * @typedef {import('.~/data/actions').Campaign} Campaign
+ */
+
+/**
+ * Clicking on the "Complete setup" button to complete the onboarding flow with paid ads.
+ *
+ * @event gla_onboarding_complete_with_paid_ads_button_click
+ * @property {number} budget The budget for the campaign
+ * @property {string} audiences The targeted audiences for the campaign
+ */
+
+/**
+ * Renders the container of the form content for campaign management.
+ *
+ * Please note that this component relies on an CampaignAssetsForm's context and custom adapter,
+ * so it expects a `CampaignAssetsForm` to existing in its parents.
+ *
+ * @fires gla_documentation_link_click with `{ context: 'create-ads' | 'edit-ads' | 'setup-ads', link_id: 'see-what-ads-look-like', href: 'https://support.google.com/google-ads/answer/6275294' }`
+ * @fires gla_onboarding_complete_with_paid_ads_button_click
+ * @param {Object} props React props.
+ * @param {Campaign} [props.campaign] Campaign data to be edited. If not provided, this component will show campaign creation UI.
+ * @param {string} props.headerTitle The title of the step.
+ * @param {() => void} props.onContinue Callback called once continue button is clicked.
+ * @param {() => void} [props.onSkip] Callback called once skip button is clicked.
+ * @param {boolean} [props.hasError=false] Whether there's an error to reset the completing state.
+ * @param {boolean} [props.isOnboardingFlow=false] Whether this component is used in onboarding flow.
+ * @param {'create-ads'|'edit-ads'|'setup-ads'} props.trackingContext A context indicating which page this component is used on. This will be the value of `context` in the track event properties.
+ */
+export default function AdsCampaign( {
+ campaign,
+ headerTitle,
+ onContinue,
+ onSkip = noop,
+ hasError = false,
+ isOnboardingFlow = false,
+ trackingContext,
+} ) {
+ const formContext = useAdaptiveFormContext();
+ const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
+ const { isValidForm, setValue } = formContext;
+ const [ completing, setCompleting ] = useState( null );
+ const [ paidAds, setPaidAds ] = useState( {} );
+
+ useEffect( () => {
+ if ( hasError ) {
+ setCompleting( null );
+ }
+ }, [ hasError ] );
+
+ const handleOnStatesReceived = ( paidAdsValues ) => {
+ setPaidAds( paidAdsValues );
+
+ const { amount } = paidAdsValues;
+ setValue( 'amount', amount );
+ };
+
+ const handleSkipCreateAds = () => {
+ setCompleting( ACTION_SKIP );
+
+ onSkip( paidAds );
+ };
+
+ const handleCompleteClick = ( event ) => {
+ setCompleting( event.target.dataset.action );
+
+ onContinue( paidAds );
+ };
+
+ // The status check of Google Ads account connection is included in `paidAds.isReady`,
+ // because when there is no connected account, it will disable the budget section and set the `amount` to `undefined`.
+ const disabledComplete =
+ completing === ACTION_SKIP || ! paidAds.isReady || ! isValidForm;
+
+ let continueButtonProps = {
+ text: __( 'Continue', 'google-listings-and-ads' ),
+ };
+
+ if ( isOnboardingFlow ) {
+ continueButtonProps = {
+ 'data-action': ACTION_COMPLETE,
+ text: __( 'Complete setup', 'google-listings-and-ads' ),
+ eventName: 'gla_onboarding_complete_with_paid_ads_button_click',
+ eventProps: {
+ budget: paidAds.amount,
+ audiences: countryCodes?.join( ',' ),
+ },
+ };
+ }
+
+ let description = createInterpolateElement(
+ __(
+ 'Paid Performance Max campaigns are automatically optimized for you by Google. See what your ads will look like.',
+ 'google-listings-and-ads'
+ ),
+ {
+ link: (
+
+ ),
+ }
+ );
+
+ if ( isOnboardingFlow ) {
+ description = __(
+ 'You’re ready to set up a Performance Max campaign to drive more sales with ads. Your products will be included in the campaign after they’re approved.',
+ 'google-listings-and-ads'
+ );
+ }
+
+ return (
+
+
+
+ { isOnboardingFlow && }
+
+
+
+
+
+ { isOnboardingFlow && (
+
+ ) }
+
+
+
+
+
+
+
+ );
+}
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/clientSession.js b/js/src/components/paid-ads/ads-campaign/clientSession.js
similarity index 100%
rename from js/src/setup-mc/setup-stepper/setup-paid-ads/clientSession.js
rename to js/src/components/paid-ads/ads-campaign/clientSession.js
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/constants.js b/js/src/components/paid-ads/ads-campaign/constants.js
similarity index 100%
rename from js/src/setup-mc/setup-stepper/setup-paid-ads/constants.js
rename to js/src/components/paid-ads/ads-campaign/constants.js
diff --git a/js/src/components/paid-ads/faqs-panel.js b/js/src/components/paid-ads/ads-campaign/faqs-panel.js
similarity index 100%
rename from js/src/components/paid-ads/faqs-panel.js
rename to js/src/components/paid-ads/ads-campaign/faqs-panel.js
diff --git a/js/src/components/paid-ads/ads-campaign/index.js b/js/src/components/paid-ads/ads-campaign/index.js
new file mode 100644
index 0000000000..19cf67b1b5
--- /dev/null
+++ b/js/src/components/paid-ads/ads-campaign/index.js
@@ -0,0 +1 @@
+export { default } from './ads-campaign';
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js b/js/src/components/paid-ads/ads-campaign/paid-ads-features-section.js
similarity index 100%
rename from js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js
rename to js/src/components/paid-ads/ads-campaign/paid-ads-features-section.js
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.scss b/js/src/components/paid-ads/ads-campaign/paid-ads-features-section.scss
similarity index 100%
rename from js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.scss
rename to js/src/components/paid-ads/ads-campaign/paid-ads-features-section.scss
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js b/js/src/components/paid-ads/ads-campaign/paid-ads-setup-sections.js
similarity index 81%
rename from js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js
rename to js/src/components/paid-ads/ads-campaign/paid-ads-setup-sections.js
index 7072eab023..c7a967ada1 100644
--- a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js
+++ b/js/src/components/paid-ads/ads-campaign/paid-ads-setup-sections.js
@@ -14,13 +14,20 @@ import SpinnerCard from '.~/components/spinner-card';
import Section from '.~/wcdl/section';
import validateCampaign from '.~/components/paid-ads/validateCampaign';
import clientSession from './clientSession';
+import CampaignPreviewCard from '.~/components/paid-ads/campaign-preview/campaign-preview-card';
import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants';
+/**
+ *
+ * @typedef {import('.~/data/actions').Campaign} Campaign
+ */
+
/**
* @typedef {import('.~/data/actions').CountryCode} CountryCode
*/
/**
+ *
* @typedef {Object} PaidAdsData
* @property {number|undefined} amount Daily average cost of the paid ads campaign.
* @property {boolean} isValid Whether the campaign data are valid values.
@@ -54,11 +61,18 @@ function resolveInitialPaidAds( paidAds ) {
* @param {Object} props React props.
* @param {(onStatesReceived: PaidAdsData)=>void} props.onStatesReceived Callback to receive the data for setting up paid ads when initial and also when the budget and billing are updated.
* @param {Array|undefined} props.countryCodes Country codes for the campaign.
+ * @param {Campaign} [props.campaign] Campaign data to be edited. If not provided, this component will show campaign creation UI.
+ * @param {boolean} [props.showCampaignPreviewCard=false] Whether to show the campaign preview card.
+ * @param {boolean} [props.loadCampaignFromClientSession=false] Whether to load the campaign data from the client session.
*/
export default function PaidAdsSetupSections( {
onStatesReceived,
countryCodes,
+ campaign,
+ loadCampaignFromClientSession,
+ showCampaignPreviewCard = false,
} ) {
+ const isCreation = ! campaign;
const { billingStatus } = useGoogleAdsAccountBillingStatus();
const onStatesReceivedRef = useRef();
@@ -66,10 +80,16 @@ export default function PaidAdsSetupSections( {
const [ paidAds, setPaidAds ] = useState( () => {
// Resolve the starting paid ads data with the campaign data stored in the client session.
- const startingPaidAds = {
+ let startingPaidAds = {
...defaultPaidAds,
- ...clientSession.getCampaign(),
};
+
+ if ( loadCampaignFromClientSession ) {
+ startingPaidAds = {
+ ...startingPaidAds,
+ ...clientSession.getCampaign(),
+ };
+ }
return resolveInitialPaidAds( startingPaidAds );
} );
@@ -108,7 +128,7 @@ export default function PaidAdsSetupSections( {
}
const initialValues = {
- amount: paidAds.amount,
+ amount: isCreation ? paidAds.amount : campaign.amount,
};
return (
@@ -126,6 +146,7 @@ export default function PaidAdsSetupSections( {
countryCodes={ countryCodes }
>
+ { showCampaignPreviewCard && }
);
} }
diff --git a/js/src/components/paid-ads/ads-campaign/skip-button.js b/js/src/components/paid-ads/ads-campaign/skip-button.js
new file mode 100644
index 0000000000..85a6048d23
--- /dev/null
+++ b/js/src/components/paid-ads/ads-campaign/skip-button.js
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import { useState } from '@wordpress/element';
+import { noop } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
+import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus';
+import AppButton from '.~/components/app-button';
+import SkipPaidAdsConfirmationModal from './skip-paid-ads-confirmation-modal';
+import { recordGlaEvent } from '.~/utils/tracks';
+import { ACTION_COMPLETE, ACTION_SKIP } from './constants';
+
+/**
+ * Clicking on the skip paid ads button to complete the onboarding flow.
+ * The 'unknown' value of properties may means:
+ * - the final status has not yet been resolved when recording this event
+ * - the status is not available, for example, the billing status is unknown if Google Ads account is not yet connected
+ *
+ * @event gla_onboarding_complete_button_click
+ * @property {string} google_ads_account_status The connection status of merchant's Google Ads addcount, e.g. 'connected', 'disconnected', 'incomplete'
+ * @property {string} billing_method_status The status of billing method of merchant's Google Ads addcount e.g. 'unknown', 'pending', 'approved', 'cancelled'
+ * @property {string} campaign_form_validation Whether the entered paid campaign form data are valid, e.g. 'unknown', 'valid', 'invalid'
+ */
+
+export default function SkipButton( {
+ text,
+ paidAds,
+ onSkipCreatePaidAds = noop,
+ completing,
+} ) {
+ const [
+ showSkipPaidAdsConfirmationModal,
+ setShowSkipPaidAdsConfirmationModal,
+ ] = useState( false );
+ const { googleAdsAccount, hasGoogleAdsConnection } = useGoogleAdsAccount();
+ const { billingStatus } = useGoogleAdsAccountBillingStatus();
+
+ const handleOnSkipClick = () => {
+ setShowSkipPaidAdsConfirmationModal( true );
+ };
+
+ const handleCancelSkipPaidAdsClick = () => {
+ setShowSkipPaidAdsConfirmationModal( false );
+ };
+
+ const handleSkipCreatePaidAds = () => {
+ setShowSkipPaidAdsConfirmationModal( false );
+
+ const eventProps = {
+ google_ads_account_status: googleAdsAccount?.status,
+ billing_method_status: billingStatus?.status || 'unknown',
+ campaign_form_validation: paidAds?.isValid ? 'valid' : 'invalid',
+ };
+ recordGlaEvent( 'gla_onboarding_complete_button_click', eventProps );
+
+ onSkipCreatePaidAds();
+ };
+
+ const disabledSkip =
+ completing === ACTION_COMPLETE || ! hasGoogleAdsConnection;
+
+ return (
+ <>
+
+
+ { showSkipPaidAdsConfirmationModal && (
+
+ ) }
+ >
+ );
+}
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/skip-paid-ads-confirmation-modal.js b/js/src/components/paid-ads/ads-campaign/skip-paid-ads-confirmation-modal.js
similarity index 100%
rename from js/src/setup-mc/setup-stepper/setup-paid-ads/skip-paid-ads-confirmation-modal.js
rename to js/src/components/paid-ads/ads-campaign/skip-paid-ads-confirmation-modal.js
diff --git a/js/src/components/paid-ads/audience-section.js b/js/src/components/paid-ads/audience-section.js
deleted file mode 100644
index f5ce860dc4..0000000000
--- a/js/src/components/paid-ads/audience-section.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { SelectControl } from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import useCountryKeyNameMap from '.~/hooks/useCountryKeyNameMap';
-import Section from '.~/wcdl/section';
-import AudienceCountrySelect from '.~/components/audience-country-select';
-import './audience-section.scss';
-
-function toCountryOptions( countryCodes, countryNameMap ) {
- return countryCodes.map( ( code ) => ( {
- label: countryNameMap[ code ],
- value: code,
- } ) );
-}
-
-/**
- * Renders and UI with country(s) selector.
- *
- * @param {Object} props React props.
- * @param {Object} props.formProps Form props forwarded from `Form` component.
- * @param {boolean} [props.multiple=true] Whether the selector is multi-selected.
- * @param {boolean} [props.disabled=false] Whether the selector is disabled.
- * @param {JSX.Element} [props.countrySelectHelperText] Helper text to be displayed under the selector.
- */
-const AudienceSection = ( props ) => {
- const {
- formProps: { getInputProps },
- multiple = true,
- disabled = false,
- countrySelectHelperText,
- } = props;
-
- const countryNameMap = useCountryKeyNameMap();
- const inputProps = getInputProps( 'countryCodes' );
-
- const selector = multiple ? (
-
- ) : (
-
- );
-
- return (
-
- { __(
- 'Choose where you want your product ads to appear',
- 'google-listings-and-ads'
- ) }
-
- }
- >
-
- { selector }
-
-
- );
-};
-
-export default AudienceSection;
diff --git a/js/src/components/paid-ads/audience-section.scss b/js/src/components/paid-ads/audience-section.scss
deleted file mode 100644
index 565a4bc70f..0000000000
--- a/js/src/components/paid-ads/audience-section.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.gla-audience-section {
- // Adjust imported from @wordpress/components.
- // Repeat selector to make it higher priority.
- .components-input-control__container.components-input-control__container {
- .components-select-control__input {
- padding-left: $grid-unit-20;
- }
- }
-
- // Adjust help text of imported from @wordpress/components.
- .components-base-control__help {
- margin: 0;
- font-style: italic;
- }
-}
diff --git a/js/src/components/paid-ads/audienceSection.test.js b/js/src/components/paid-ads/audienceSection.test.js
deleted file mode 100644
index a7eb440a84..0000000000
--- a/js/src/components/paid-ads/audienceSection.test.js
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * External dependencies
- */
-import '@testing-library/jest-dom';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-/**
- * Internal dependencies
- */
-import AudienceSection from '.~/components/paid-ads/audience-section';
-
-jest.mock( '.~/hooks/useAppSelectDispatch' );
-jest.mock( '.~/hooks/useCountryKeyNameMap' );
-
-jest.mock( '.~/hooks/useTargetAudienceFinalCountryCodes', () =>
- jest.fn( () => ( { data: [ 'GB', 'US', 'ES' ] } ) )
-);
-
-describe( 'AudienceSection with multiple countries selector', () => {
- let defaultProps;
- let onChange;
-
- beforeEach( () => {
- onChange = jest.fn();
- defaultProps = {
- formProps: {
- getInputProps: () => ( { onChange } ),
- },
- };
- } );
-
- test( 'If Audience section is disabled the country field should be disabled', async () => {
- const user = userEvent.setup();
-
- render( );
-
- const dropdown = await screen.findByRole( 'combobox' );
- expect( dropdown ).toBeDisabled();
-
- //Test that input is not editable
- expect( dropdown ).toHaveValue( '' );
- await user.type( dropdown, 'spa' );
- expect( dropdown ).toHaveValue( '' );
-
- const options = screen.queryAllByRole( 'checkbox' );
- expect( options.length ).toBe( 0 );
- expect( onChange ).toHaveBeenCalledTimes( 0 );
- } );
-
- test( 'If Audience section is enable the country field should be enable & editable', async () => {
- const user = userEvent.setup();
-
- render( );
-
- const dropdown = await screen.findByRole( 'combobox' );
- expect( dropdown ).not.toBeDisabled();
-
- //Test that input is editable
- expect( dropdown ).toHaveValue( '' );
- await user.type( dropdown, 'spa' );
- expect( dropdown ).toHaveValue( 'spa' );
-
- const options = await screen.findAllByRole( 'checkbox' );
- expect( options.length ).toBeGreaterThan( 0 );
-
- const firstOption = options[ 0 ];
- await user.click( firstOption );
- expect( onChange ).toHaveBeenCalledTimes( 1 );
- } );
-} );
-
-describe( 'AudienceSection with single country selector', () => {
- let defaultProps;
- let onChange;
-
- beforeEach( () => {
- onChange = jest.fn();
- defaultProps = {
- multiple: false,
- formProps: {
- getInputProps: () => ( {
- value: [ 'US', 'ES', 'GB' ],
- selected: [ 'ES' ],
- onChange,
- } ),
- },
- };
- } );
-
- test( 'When AudienceSection is disabled, the country field should be disabled', () => {
- render( );
- const dropdown = screen.queryByRole( 'combobox' );
-
- expect( dropdown ).toBeDisabled();
- } );
-
- test( 'When AudienceSection is enable, the country field should be enable', () => {
- render( );
- const dropdown = screen.queryByRole( 'combobox' );
-
- expect( dropdown ).not.toBeDisabled();
- } );
-
- test( 'When selecting another option, the country field should trigger `onChange` callback', async () => {
- const user = userEvent.setup();
-
- render( );
-
- const dropdown = screen.queryByRole( 'combobox' );
- await user.selectOptions( dropdown, 'GB' );
-
- expect( onChange ).toHaveBeenCalledTimes( 1 );
- } );
-
- test( 'The country field should have the given options', () => {
- render( );
- const options = screen.queryAllByRole( 'option' );
-
- expect( options.length ).toBe( 3 );
- } );
-
- test( 'The country field should select the option by given value', () => {
- render( );
- const option = screen.queryByRole( 'option', { selected: true } );
-
- expect( option.value ).toBe( 'ES' );
- } );
-} );
diff --git a/js/src/components/paid-ads/billing-card/billing-card.js b/js/src/components/paid-ads/billing-card/billing-card.js
index 78f84d066b..f05ec0ca7f 100644
--- a/js/src/components/paid-ads/billing-card/billing-card.js
+++ b/js/src/components/paid-ads/billing-card/billing-card.js
@@ -2,6 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
import Gridiconcheckmark from 'gridicons/dist/checkmark';
import { Flex, FlexBlock } from '@wordpress/components';
@@ -15,21 +16,33 @@ import fallbackBillingUrl from './fallbackBillingUrl';
import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants';
import './billing-card.scss';
-const { APPROVED } = GOOGLE_ADS_BILLING_STATUS;
+const { APPROVED, PENDING } = GOOGLE_ADS_BILLING_STATUS;
/**
* Renders a success notice card or a setup card according to the billing status
* of the connected Google Ads account.
*/
export default function BillingCard() {
+ const [ showNotice, setShowNotice ] = useState( false );
const { billingStatus, hasFinishedResolution } =
useGoogleAdsAccountBillingStatus();
+ useEffect( () => {
+ // only show notice if we are in pending state before
+ if ( billingStatus.status === PENDING && ! showNotice ) {
+ setShowNotice( true );
+ }
+ }, [ billingStatus, showNotice ] );
+
if ( ! hasFinishedResolution ) {
return ;
}
if ( billingStatus.status === APPROVED ) {
+ if ( ! showNotice ) {
+ return null;
+ }
+
return (
diff --git a/js/src/pages/create-paid-ads-campaign/index.js b/js/src/pages/create-paid-ads-campaign/index.js
index e076243e6a..534c75bcb7 100644
--- a/js/src/pages/create-paid-ads-campaign/index.js
+++ b/js/src/pages/create-paid-ads-campaign/index.js
@@ -145,6 +145,10 @@ const CreatePaidAdsCampaign = () => {
),
content: (
handleContinueClick( STEP.ASSET_GROUP )
diff --git a/js/src/pages/edit-paid-ads-campaign/index.js b/js/src/pages/edit-paid-ads-campaign/index.js
index 1d23f45666..676d1d39c7 100644
--- a/js/src/pages/edit-paid-ads-campaign/index.js
+++ b/js/src/pages/edit-paid-ads-campaign/index.js
@@ -200,6 +200,10 @@ const EditPaidAdsCampaign = () => {
onContinue={ () =>
handleContinueClick( STEP.ASSET_GROUP )
}
+ headerTitle={ __(
+ 'Edit your paid campaign',
+ 'google-listings-and-ads'
+ ) }
/>
),
onClick: handleStepperClick,
diff --git a/js/src/setup-ads/ads-stepper/index.js b/js/src/setup-ads/ads-stepper/index.js
index 3979e0dcba..60b5e95534 100644
--- a/js/src/setup-ads/ads-stepper/index.js
+++ b/js/src/setup-ads/ads-stepper/index.js
@@ -91,6 +91,10 @@ const AdsStepper = ( { formProps } ) => {
),
content: (
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads.js
new file mode 100644
index 0000000000..1fc492a495
--- /dev/null
+++ b/js/src/setup-mc/setup-stepper/setup-paid-ads.js
@@ -0,0 +1,89 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import apiFetch from '@wordpress/api-fetch';
+import { useState } from '@wordpress/element';
+import { noop } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import useAdminUrl from '.~/hooks/useAdminUrl';
+import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
+import useAdsSetupCompleteCallback from '.~/hooks/useAdsSetupCompleteCallback';
+import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
+import AdsCampaign from '.~/components/paid-ads/ads-campaign';
+import CampaignAssetsForm from '.~/components/paid-ads/campaign-assets-form';
+import { getProductFeedUrl } from '.~/utils/urls';
+import { API_NAMESPACE } from '.~/data/constants';
+import { GUIDE_NAMES } from '.~/constants';
+
+/**
+ * Renders the onboarding step for setting up the paid ads (Google Ads account and paid campaign)
+ * or skipping it, and then completing the onboarding flow.
+ */
+export default function SetupPaidAds() {
+ const adminUrl = useAdminUrl();
+ const [ hasError, setHasError ] = useState( false );
+ const { createNotice } = useDispatchCoreNotices();
+ const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
+ const [ handleSetupComplete ] = useAdsSetupCompleteCallback();
+
+ const finishOnboardingSetup = async ( onBeforeFinish = noop ) => {
+ try {
+ await onBeforeFinish();
+ await apiFetch( {
+ path: `${ API_NAMESPACE }/mc/settings/sync`,
+ method: 'POST',
+ } );
+ } catch ( e ) {
+ setHasError( true );
+
+ createNotice(
+ 'error',
+ __(
+ 'Unable to complete your setup.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+
+ // Force reload WC admin page to initiate the relevant dependencies of the Dashboard page.
+ const query = { guide: GUIDE_NAMES.SUBMISSION_SUCCESS };
+ window.location.href = adminUrl + getProductFeedUrl( query );
+ };
+
+ const handleSkipCreatePaidAds = async () => {
+ await finishOnboardingSetup();
+ };
+
+ const handleCompleteClick = async ( paidAdsData ) => {
+ const onBeforeFinish = handleSetupComplete.bind(
+ null,
+ paidAdsData.amount,
+ countryCodes
+ );
+
+ await finishOnboardingSetup( onBeforeFinish );
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/index.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/index.js
deleted file mode 100644
index b949d024c1..0000000000
--- a/js/src/setup-mc/setup-stepper/setup-paid-ads/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './setup-paid-ads';
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js
deleted file mode 100644
index 76b6381fb9..0000000000
--- a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js
+++ /dev/null
@@ -1,216 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import apiFetch from '@wordpress/api-fetch';
-import { select } from '@wordpress/data';
-import { useState } from '@wordpress/element';
-import { Flex } from '@wordpress/components';
-import { noop } from 'lodash';
-
-/**
- * Internal dependencies
- */
-import useAdminUrl from '.~/hooks/useAdminUrl';
-import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
-import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
-import useAdsSetupCompleteCallback from '.~/hooks/useAdsSetupCompleteCallback';
-import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
-import StepContent from '.~/components/stepper/step-content';
-import StepContentHeader from '.~/components/stepper/step-content-header';
-import StepContentFooter from '.~/components/stepper/step-content-footer';
-import StepContentActions from '.~/components/stepper/step-content-actions';
-import AppButton from '.~/components/app-button';
-import PaidAdsFaqsPanel from '.~/components/paid-ads/faqs-panel';
-import PaidAdsFeaturesSection from './paid-ads-features-section';
-import PaidAdsSetupSections from './paid-ads-setup-sections';
-import SkipPaidAdsConfirmationModal from './skip-paid-ads-confirmation-modal';
-import { getProductFeedUrl } from '.~/utils/urls';
-import { API_NAMESPACE, STORE_KEY } from '.~/data/constants';
-import { GUIDE_NAMES } from '.~/constants';
-import { ACTION_COMPLETE, ACTION_SKIP } from './constants';
-import { recordGlaEvent } from '.~/utils/tracks';
-
-/**
- * Clicking on the "Create a paid ad campaign" button to open the paid ads setup in the onboarding flow.
- *
- * @event gla_onboarding_open_paid_ads_setup_button_click
- */
-
-/**
- * Clicking on the "Complete setup" button to complete the onboarding flow with paid ads.
- *
- * @event gla_onboarding_complete_with_paid_ads_button_click
- * @property {number} budget The budget for the campaign
- * @property {string} audiences The targeted audiences for the campaign
- */
-
-/**
- * Clicking on the skip paid ads button to complete the onboarding flow.
- * The 'unknown' value of properties may means:
- * - the final status has not yet been resolved when recording this event
- * - the status is not available, for example, the billing status is unknown if Google Ads account is not yet connected
- *
- * @event gla_onboarding_complete_button_click
- * @property {string} google_ads_account_status The connection status of merchant's Google Ads addcount, e.g. 'connected', 'disconnected', 'incomplete'
- * @property {string} billing_method_status The status of billing method of merchant's Google Ads addcount e.g. 'unknown', 'pending', 'approved', 'cancelled'
- * @property {string} campaign_form_validation Whether the entered paid campaign form data are valid, e.g. 'unknown', 'valid', 'invalid'
- */
-
-/**
- * Renders the onboarding step for setting up the paid ads (Google Ads account and paid campaign)
- * or skipping it, and then completing the onboarding flow.
- *
- * @fires gla_onboarding_open_paid_ads_setup_button_click
- * @fires gla_onboarding_complete_with_paid_ads_button_click
- * @fires gla_onboarding_complete_button_click
- */
-export default function SetupPaidAds() {
- const adminUrl = useAdminUrl();
- const { createNotice } = useDispatchCoreNotices();
- const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
- const { googleAdsAccount, hasGoogleAdsConnection } = useGoogleAdsAccount();
- const [ handleSetupComplete ] = useAdsSetupCompleteCallback();
- const [ paidAds, setPaidAds ] = useState( {} );
- const [ completing, setCompleting ] = useState( null );
- const [
- showSkipPaidAdsConfirmationModal,
- setShowSkipPaidAdsConfirmationModal,
- ] = useState( false );
-
- const finishOnboardingSetup = async ( event, onBeforeFinish = noop ) => {
- setCompleting( event.target.dataset.action );
-
- try {
- await onBeforeFinish();
- await apiFetch( {
- path: `${ API_NAMESPACE }/mc/settings/sync`,
- method: 'POST',
- } );
- } catch ( e ) {
- setCompleting( null );
-
- createNotice(
- 'error',
- __(
- 'Unable to complete your setup.',
- 'google-listings-and-ads'
- )
- );
- }
-
- // Force reload WC admin page to initiate the relevant dependencies of the Dashboard page.
- const query = { guide: GUIDE_NAMES.SUBMISSION_SUCCESS };
- window.location.href = adminUrl + getProductFeedUrl( query );
- };
-
- const handleCompleteClick = async ( event ) => {
- const onBeforeFinish = handleSetupComplete.bind(
- null,
- paidAds.amount,
- countryCodes
- );
- await finishOnboardingSetup( event, onBeforeFinish );
- };
-
- const handleSkipCreatePaidAds = async ( event ) => {
- const selector = select( STORE_KEY );
- const billing = selector.getGoogleAdsAccountBillingStatus();
-
- setShowSkipPaidAdsConfirmationModal( false );
-
- const eventProps = {
- google_ads_account_status: googleAdsAccount?.status,
- billing_method_status: billing?.status || 'unknown',
- campaign_form_validation: paidAds.isValid ? 'valid' : 'invalid',
- };
-
- recordGlaEvent( 'gla_onboarding_complete_button_click', eventProps );
-
- await finishOnboardingSetup( event );
- };
-
- const handleShowSkipPaidAdsConfirmationModal = () => {
- setShowSkipPaidAdsConfirmationModal( true );
- };
-
- const handleCancelSkipPaidAdsClick = () => {
- setShowSkipPaidAdsConfirmationModal( false );
- };
-
- // The status check of Google Ads account connection is included in `paidAds.isReady`,
- // because when there is no connected account, it will disable the budget section and set the `amount` to `undefined`.
- const disabledComplete = completing === ACTION_SKIP || ! paidAds.isReady;
-
- function createSkipButton( text ) {
- const disabledSkip =
- completing === ACTION_COMPLETE || ! hasGoogleAdsConnection;
-
- return (
-
- );
- }
-
- return (
-
-
-
-
-
- { showSkipPaidAdsConfirmationModal && (
-
- ) }
-
-
-
-
- { createSkipButton(
- __(
- 'Skip paid ads creation',
- 'google-listings-and-ads'
- )
- ) }
-
-
-
-
-
-
- );
-}
diff --git a/package-lock.json b/package-lock.json
index 646f4851a0..7aa1b22f18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "google-listings-and-ads",
- "version": "2.8.4",
+ "version": "2.8.5",
"license": "GPL-3.0-or-later",
"dependencies": {
"@woocommerce/components": "^12.3.0",
diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js
index 805f77e798..297cd79d0d 100644
--- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js
+++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js
@@ -11,8 +11,6 @@ import SetupAdsAccountsPage from '../../utils/pages/setup-ads/setup-ads-accounts
import SetupBudgetPage from '../../utils/pages/setup-ads/setup-budget';
import { LOAD_STATE } from '../../utils/constants';
import {
- getCountryInputSearchBoxContainer,
- getCountryTagsFromInputSearchBoxContainer,
getFAQPanelTitle,
getFAQPanelRow,
checkFAQExpandable,
@@ -282,6 +280,9 @@ test.describe( 'Set up Ads account', () => {
test.describe( 'Create your paid campaign', () => {
test.beforeAll( async () => {
setupBudgetPage = new SetupBudgetPage( page );
+ await setupBudgetPage.fulfillBillingStatusRequest( {
+ status: 'pending',
+ } );
} );
test( 'Continue to create paid campaign', async () => {
@@ -294,10 +295,6 @@ test.describe( 'Set up Ads account', () => {
} )
).toBeVisible();
- await expect(
- page.getByRole( 'heading', { name: 'Ads audience' } )
- ).toBeVisible();
-
await expect(
page.getByRole( 'heading', { name: 'Set your budget' } )
).toBeVisible();
@@ -355,17 +352,6 @@ test.describe( 'Set up Ads account', () => {
} );
} );
- test( 'Audience should be United States', async () => {
- const countrySearchBoxContainer =
- getCountryInputSearchBoxContainer( page );
- const countryTags =
- getCountryTagsFromInputSearchBoxContainer( page );
- await expect( countryTags ).toHaveCount( 1 );
- await expect( countrySearchBoxContainer ).toContainText(
- 'United States'
- );
- } );
-
test( 'Set the budget', async () => {
budget = '0';
await setupBudgetPage.fillBudget( budget );
@@ -376,10 +362,6 @@ test.describe( 'Set up Ads account', () => {
budget = '1';
await setupBudgetPage.fillBudget( budget );
-
- await expect(
- page.getByRole( 'button', { name: 'Continue' } )
- ).toBeEnabled();
} );
test( 'Budget Recommendation', async () => {
@@ -391,13 +373,7 @@ test.describe( 'Set up Ads account', () => {
test.describe( 'Set up billing', () => {
test.describe( 'Billing status is not approved', () => {
- test.beforeAll( async () => {
- await setupBudgetPage.fulfillBillingStatusRequest( {
- status: 'pending',
- } );
- } );
test( 'It should say that the billing is not setup', async () => {
- await page.getByRole( 'button', { name: 'Continue' } ).click();
await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED );
await expect(
@@ -409,99 +385,52 @@ test.describe( 'Set up Ads account', () => {
await expect(
page.getByText(
- 'In order to launch your paid campaign, your billing information is required. You will be billed directly by Google and only pay when someone clicks on your ad.'
+ 'You do not have billing information set up in your Google Ads account. Once you have set up billing, you can start running ads.'
)
).toBeVisible();
-
- await expect(
- page.getByRole( 'link', {
- name: 'click here instead',
- } )
- ).toBeVisible();
} );
// eslint-disable-next-line jest/expect-expect
test( 'should open a popup when clicking set up billing button', async () => {
await checkBillingAdsPopup( page );
} );
- } );
- test.describe( 'Billing status is approved', async () => {
- test.beforeAll( async () => {
+ test( 'should see billing has been set up successfully when billing status API returns approved', async () => {
+ const newPagePromise = page.waitForEvent( 'popup' );
+ await setupBudgetPage.clickSetUpBillingLink();
+ const newPage = await newPagePromise;
+ await newPage.waitForLoadState();
+
await setupBudgetPage.fulfillBillingStatusRequest( {
- status: 'approved',
+ status: 'pending',
} );
- await setupAdsAccounts.mockAdsAccountsResponse( {
- id: ADS_ACCOUNTS[ 1 ],
- billing_url: null,
+ await newPage.close();
+ await setupBudgetPage.focusBudget();
+ await setupBudgetPage.fulfillBillingStatusRequest( {
+ status: 'approved',
} );
-
- // Simulate a bit of delay when creating the Ads campaign so we have enough time to test the content in the page before the redirect.
- await page.route(
- /\/wc\/gla\/ads\/campaigns\b/,
- async ( route ) => {
- await new Promise( ( f ) => setTimeout( f, 500 ) );
- await route.continue();
- }
- );
- } );
- test( 'It should say that the billing is setup', async () => {
- //Every 30s the page will check if the billing status is approved and it will trigger the campaign creation.
await setupBudgetPage.awaitForBillingStatusRequest();
- await setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion(
- budget,
- [ 'US' ]
- );
-
- await expect(
- page.getByText(
- 'Great! You already have billing information saved for this'
- )
- ).toBeVisible();
- //It should redirect to the dashboard page
- await page.waitForURL(
- '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success',
- {
- timeout: 30000,
- waitUntil: LOAD_STATE.DOM_CONTENT_LOADED,
- }
+ const billingSetupSuccessSection =
+ setupBudgetPage.getBillingSetupSuccessSection();
+ await expect( billingSetupSuccessSection ).toContainText(
+ 'Billing method for Google Ads added successfully'
);
} );
-
- test( 'It should show the campaign creation success message', async () => {
- await expect(
- page.getByRole( 'heading', {
- name: "You've set up a paid Performance Max Campaign!",
- } )
- ).toBeVisible();
-
- await expect(
- page.getByRole( 'button', {
- name: 'Create another campaign',
- } )
- ).toBeEnabled();
-
- await expect(
- page.getByRole( 'button', {
- name: 'Got It',
- } )
- ).toBeEnabled();
-
- await page
- .getByRole( 'button', {
- name: 'Got It',
- } )
- .click();
- } );
} );
} );
test.describe( 'Create Ads with billing data already setup', () => {
+ test.beforeAll( async () => {
+ await setupBudgetPage.fulfillBillingStatusRequest( {
+ status: 'approved',
+ } );
+ } );
+
test( 'Launch paid campaign should be enabled', async () => {
- //Click Add paid Campaign
- await adsConnectionButton.click();
+ //Reload the page
+ await page.reload();
await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED );
//Step 1 - Accounts are already set up.
@@ -509,7 +438,14 @@ test.describe( 'Set up Ads account', () => {
await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED );
//Step 2 - Fill the budget
+ await setupBudgetPage.fulfillBillingStatusRequest( {
+ status: 'approved',
+ } );
await setupBudgetPage.fillBudget( '1' );
+
+ await expect(
+ page.getByRole( 'button', { name: 'Continue' } )
+ ).toBeEnabled();
await page.getByRole( 'button', { name: 'Continue' } ).click();
await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED );
@@ -534,5 +470,31 @@ test.describe( 'Set up Ads account', () => {
.click();
await campaignCreation;
} );
+
+ test( 'It should show the campaign creation success message', async () => {
+ await expect(
+ page.getByRole( 'heading', {
+ name: "You've set up a paid Performance Max Campaign!",
+ } )
+ ).toBeVisible();
+
+ await expect(
+ page.getByRole( 'button', {
+ name: 'Create another campaign',
+ } )
+ ).toBeEnabled();
+
+ await expect(
+ page.getByRole( 'button', {
+ name: 'Got It',
+ } )
+ ).toBeEnabled();
+
+ await page
+ .getByRole( 'button', {
+ name: 'Got It',
+ } )
+ .click();
+ } );
} );
} );
diff --git a/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js b/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js
index 92c3fb3206..adc31ea614 100644
--- a/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js
+++ b/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js
@@ -247,12 +247,21 @@ test.describe( 'Complete your campaign', () => {
} );
test( 'should see billing has been set up successfully when billing status API returns approved', async () => {
+ await setupBudgetPage.mockAdsAccountsResponse( {
+ id: 12345,
+ billing_url: null,
+ } );
await setupBudgetPage.fulfillBillingStatusRequest( {
- status: 'approved',
+ status: 'pending',
} );
await newPage.close();
- await page.reload();
+ // return focus to the page.
+ await setupBudgetPage.focusBudget();
+ await setupBudgetPage.fulfillBillingStatusRequest( {
+ status: 'approved',
+ } );
+ await setupBudgetPage.awaitForBillingStatusRequest();
const billingSetupSuccessSection =
setupBudgetPage.getBillingSetupSuccessSection();
diff --git a/tests/e2e/utils/page.js b/tests/e2e/utils/page.js
index 54912b00f8..51713bd110 100644
--- a/tests/e2e/utils/page.js
+++ b/tests/e2e/utils/page.js
@@ -54,19 +54,6 @@ export function getCountryInputSearchBoxContainer( page ) {
);
}
-/**
- * Get country tags from input search box container.
- *
- * @param {import('@playwright/test').Page} page The current page.
- *
- * @return {import('@playwright/test').Locator} Get country tags from input search box container.
- */
-export function getCountryTagsFromInputSearchBoxContainer( page ) {
- return getCountryInputSearchBoxContainer( page ).locator(
- '.woocommerce-tag'
- );
-}
-
/**
* Get country input search box.
*
diff --git a/tests/e2e/utils/pages/setup-ads/setup-budget.js b/tests/e2e/utils/pages/setup-ads/setup-budget.js
index a94232393c..afb0875017 100644
--- a/tests/e2e/utils/pages/setup-ads/setup-budget.js
+++ b/tests/e2e/utils/pages/setup-ads/setup-budget.js
@@ -87,6 +87,16 @@ export default class SetupBudget extends MockRequests {
await input.fill( budget );
}
+ /**
+ * Focus the budget input.
+ *
+ * @return {Promise}
+ */
+ async focusBudget() {
+ const input = this.getBudgetInput();
+ await input.focus();
+ }
+
/**
* Click set up billing button.
*