diff --git a/js/src/components/paid-ads/budget-section/budget-recommendation/index.js b/js/src/components/paid-ads/budget-section/budget-recommendation/index.js
index 562ddcf272..573ecad293 100644
--- a/js/src/components/paid-ads/budget-section/budget-recommendation/index.js
+++ b/js/src/components/paid-ads/budget-section/budget-recommendation/index.js
@@ -10,36 +10,20 @@ import GridiconNoticeOutline from 'gridicons/dist/notice-outline';
* Internal dependencies
*/
import useCountryKeyNameMap from '.~/hooks/useCountryKeyNameMap';
-import useFetchBudgetRecommendationEffect from './useFetchBudgetRecommendationEffect';
+import useFetchBudgetRecommendation from '.~/hooks/useFetchBudgetRecommendation';
import './index.scss';
-/*
- * If a merchant selects more than one country, the budget recommendation
- * takes the highest country out from the selected countries.
- *
- * For example, a merchant selected Brunei (20 USD) and Croatia (15 USD),
- * then the budget recommendation should be (20 USD).
- */
-function getHighestBudget( recommendations ) {
- return recommendations.reduce( ( defender, challenger ) => {
- if ( challenger.daily_budget > defender.daily_budget ) {
- return challenger;
- }
- return defender;
- } );
-}
-
function toRecommendationRange( isMultiple, ...values ) {
const conversionMap = { strong: , em: , br: };
const template = isMultiple
? // translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount.
__(
- 'Google will optimize your ads to maximize performance. Tip: Most merchants who sell products in similar countries set a daily budget of %1$f %2$s',
+ 'We recommend running campaigns at least 1 month so it can learn to optimize for your business. Tip: Most merchants targeting similar countries set a daily budget of %1$f %2$s',
'google-listings-and-ads'
)
: // translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount 3: a country name selected by the merchant.
__(
- 'Google will optimize your ads to maximize performance. Tip: Most merchants targeting %3$s set a daily budget of %1$f %2$s',
+ 'We recommend running campaigns at least 1 month so it can learn to optimize for your business. Tip: Most merchants targeting %3$s set a daily budget of %1$f %2$s',
'google-listings-and-ads'
);
@@ -51,7 +35,9 @@ function toRecommendationRange( isMultiple, ...values ) {
const BudgetRecommendation = ( props ) => {
const { countryCodes, dailyAverageCost = Infinity } = props;
- const { data } = useFetchBudgetRecommendationEffect( countryCodes );
+ const { data, highestDailyBudgetCountryCode, highestDailyBudget } =
+ useFetchBudgetRecommendation( countryCodes );
+
const map = useCountryKeyNameMap();
if ( ! data ) {
@@ -59,18 +45,15 @@ const BudgetRecommendation = ( props ) => {
}
const { currency, recommendations } = data;
- const { daily_budget: dailyBudget, country } =
- getHighestBudget( recommendations );
-
- const countryName = map[ country ];
+ const countryName = map[ highestDailyBudgetCountryCode ];
const recommendationRange = toRecommendationRange(
recommendations.length > 1,
- dailyBudget,
+ highestDailyBudget,
currency,
countryName
);
- const showLowerBudgetNotice = dailyAverageCost < dailyBudget;
+ const showLowerBudgetNotice = dailyAverageCost < highestDailyBudget;
return (
diff --git a/js/src/components/paid-ads/budget-section/budget-recommendation/useFetchBudgetRecommendationEffect.js b/js/src/components/paid-ads/budget-section/budget-recommendation/useFetchBudgetRecommendationEffect.js
deleted file mode 100644
index 6d63fad522..0000000000
--- a/js/src/components/paid-ads/budget-section/budget-recommendation/useFetchBudgetRecommendationEffect.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * External dependencies
- */
-import { addQueryArgs } from '@wordpress/url';
-
-/**
- * Internal dependencies
- */
-import { API_NAMESPACE } from '.~/data/constants';
-import useApiFetchEffect from '.~/hooks/useApiFetchEffect';
-
-/**
- * @typedef { import(".~/data/actions").CountryCode } CountryCode
- */
-
-/**
- * Fetch the budget recommendation for a country in a side effect.
- *
- * @param {Array} countryCodes Country code array.
- * @return {Object} Budget recommendation.
- */
-const useFetchBudgetRecommendationEffect = ( countryCodes ) => {
- const url = `${ API_NAMESPACE }/ads/campaigns/budget-recommendation`;
- const query = { country_codes: countryCodes };
- const path = addQueryArgs( url, query );
- return useApiFetchEffect( { path } );
-};
-
-export default useFetchBudgetRecommendationEffect;
diff --git a/js/src/components/paid-ads/budget-section/index.js b/js/src/components/paid-ads/budget-section/index.js
index 87f2b940fc..121c74976a 100644
--- a/js/src/components/paid-ads/budget-section/index.js
+++ b/js/src/components/paid-ads/budget-section/index.js
@@ -2,7 +2,6 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
@@ -39,30 +38,13 @@ const BudgetSection = ( {
disabled = false,
children,
} ) => {
- const { getInputProps, setValue, values } = formProps;
+ const { getInputProps, values } = formProps;
const { amount } = values;
const { googleAdsAccount } = useGoogleAdsAccount();
const monthlyMaxEstimated = getMonthlyMaxEstimated( amount );
// Display the currency code that will be used by Google Ads, but still use the store's currency formatting settings.
const currency = googleAdsAccount?.currency;
- // Wrapping `useRef` is because since WC 6.9, the reference of `setValue` may be changed
- // after calling itself and further leads to an infinite re-rendering loop if used in a
- // `useEffect`.
- const setValueRef = useRef();
- setValueRef.current = setValue;
-
- /**
- * In addition to the initial value setting during initialization, when `disabled` changes
- * - from false to true, then clear filled amount to `undefined` for showing a blank .
- * - from true to false, then reset amount to the initial value passed from the consumer side.
- */
- const initialAmountRef = useRef( amount );
- useEffect( () => {
- const nextAmount = disabled ? undefined : initialAmountRef.current;
- setValueRef.current( 'amount', nextAmount );
- }, [ disabled ] );
-
return (
{
.end();
}
+ case TYPES.RECEIVE_ADS_BUDGET_RECOMMENDATIONS: {
+ const { countryCodesKey, currency, recommendations } = action;
+
+ return setIn(
+ state,
+ [ 'ads', 'budgetRecommendations', countryCodesKey ],
+ {
+ currency,
+ recommendations,
+ }
+ );
+ }
+
// Page will be reloaded after all accounts have been disconnected, so no need to mutate state.
case TYPES.DISCONNECT_ACCOUNTS_ALL:
default:
diff --git a/js/src/data/resolvers.js b/js/src/data/resolvers.js
index c29cb52bdd..25a1cd99fd 100644
--- a/js/src/data/resolvers.js
+++ b/js/src/data/resolvers.js
@@ -15,7 +15,7 @@ import {
} from '.~/constants';
import TYPES from './action-types';
import { API_NAMESPACE } from './constants';
-import { getReportKey } from './utils';
+import { getReportKey, getCountryCodesKey } from './utils';
import { handleApiError } from '.~/utils/handleError';
import { adaptAdsCampaign, adaptAssetGroup } from './adapters';
import { fetchWithHeaders, awaitPromise } from './controls';
@@ -48,6 +48,10 @@ import {
receiveTour,
} from './actions';
+/**
+ * @typedef {import('.~/data/actions').CountryCode} CountryCode
+ */
+
export function* getShippingRates() {
yield fetchShippingRates();
}
@@ -510,3 +514,54 @@ export function* getGoogleAdsAccountStatus() {
getGoogleAdsAccountStatus.shouldInvalidate = ( action ) => {
return action.type === TYPES.DISCONNECT_ACCOUNTS_GOOGLE_ADS;
};
+
+/**
+ * Fetch ad budget recommendations for the specified country codes.
+ *
+ * @param {Array} [countryCodes] An array of country codes for which to fetch budget recommendations.
+ */
+export function* getAdsBudgetRecommendations( countryCodes ) {
+ if ( ! countryCodes || ! countryCodes.length ) {
+ return;
+ }
+
+ const countryCodesKey = getCountryCodesKey( countryCodes );
+ const endpoint = `${ API_NAMESPACE }/ads/campaigns/budget-recommendation`;
+ const query = { country_codes: countryCodes };
+ const path = addQueryArgs( endpoint, query );
+
+ try {
+ const { data } = yield fetchWithHeaders( {
+ path,
+ } );
+
+ const { currency, recommendations } = data;
+
+ return {
+ type: TYPES.RECEIVE_ADS_BUDGET_RECOMMENDATIONS,
+ countryCodesKey,
+ currency,
+ recommendations,
+ };
+ } catch ( response ) {
+ // Intentionally silence the specific in case the no budget recommendations are found from the API.
+ if ( response.status === 404 ) {
+ return;
+ }
+
+ const bodyPromise = response?.json() || response?.text();
+ const error = yield awaitPromise( bodyPromise );
+
+ handleApiError(
+ error,
+ __(
+ 'There was an error getting the budget recommendation.',
+ 'google-listings-and-ads'
+ )
+ );
+ }
+}
+
+getAdsBudgetRecommendations.shouldInvalidate = ( action ) => {
+ return action.type === TYPES.DISCONNECT_ACCOUNTS_GOOGLE_ADS;
+};
diff --git a/js/src/data/selectors.js b/js/src/data/selectors.js
index 123b09f7d5..4240c3e0a9 100644
--- a/js/src/data/selectors.js
+++ b/js/src/data/selectors.js
@@ -8,7 +8,12 @@ import createSelector from 'rememo';
* Internal dependencies
*/
import { STORE_KEY } from './constants';
-import { getReportQuery, getReportKey, getPerformanceQuery } from './utils';
+import {
+ getReportQuery,
+ getReportKey,
+ getPerformanceQuery,
+ getCountryCodesKey,
+} from './utils';
/**
* @typedef {import('.~/data/actions').CountryCode} CountryCode
@@ -406,3 +411,16 @@ export const getTour = ( state, tourId ) => {
export const getGoogleAdsAccountStatus = ( state ) => {
return state.ads.accountStatus;
};
+
+/**
+ * Retrieves ad budget recommendations for provided country codes.
+ * If no recommendations are found, it returns `null`.
+ *
+ * @param {Object} state The state
+ * @param {Array} [countryCodes] - An array of country code strings to retrieve the budget recommendations for.
+ * @return {Object|null} The recommendations. It will be `null` if not yet fetched or fetched but doesn't exist.
+ */
+export const getAdsBudgetRecommendations = ( state, countryCodes = [] ) => {
+ const key = getCountryCodesKey( countryCodes );
+ return state.ads.budgetRecommendations[ key ] || null;
+};
diff --git a/js/src/data/test/reducer.test.js b/js/src/data/test/reducer.test.js
index b48277b36a..35416da5b3 100644
--- a/js/src/data/test/reducer.test.js
+++ b/js/src/data/test/reducer.test.js
@@ -72,6 +72,7 @@ describe( 'reducer', () => {
inviteLink: null,
step: null,
},
+ budgetRecommendations: {},
},
} );
@@ -865,6 +866,39 @@ describe( 'reducer', () => {
} );
} );
+ describe( 'Ads Budget Recommendations', () => {
+ const path = 'ads.budgetRecommendations';
+
+ it( 'should receive a budget recommendation', () => {
+ const recommendation = {
+ countryCodesKey: 'mu_sg',
+ currency: 'MUR',
+ recommendations: [
+ {
+ country: 'MU',
+ daily_budget: 15,
+ },
+ {
+ country: 'SG',
+ daily_budget: 10,
+ },
+ ],
+ };
+
+ const action = {
+ type: TYPES.RECEIVE_ADS_BUDGET_RECOMMENDATIONS,
+ ...recommendation,
+ };
+ const state = reducer( prepareState(), action );
+
+ state.assertConsistentRef();
+ expect( state ).toHaveProperty( `${ path }.mu_sg`, {
+ currency: recommendation.currency,
+ recommendations: recommendation.recommendations,
+ } );
+ } );
+ } );
+
describe( 'Remaining actions simply update the data payload to the specific path of state and return the updated state', () => {
// The readability is better than applying the formatting here.
/* eslint-disable prettier/prettier */
diff --git a/js/src/data/utils.js b/js/src/data/utils.js
index 2de2394f78..a42b7bd171 100644
--- a/js/src/data/utils.js
+++ b/js/src/data/utils.js
@@ -9,6 +9,10 @@ import { getCurrentDates } from '@woocommerce/date';
*/
import round from '.~/utils/round';
+/**
+ * @typedef { import(".~/data/actions").CountryCode } CountryCode
+ */
+
export const freeFields = [ 'clicks', 'impressions' ];
export const paidFields = [ 'sales', 'conversions', 'spend', ...freeFields ];
/**
@@ -190,6 +194,20 @@ export function mapReportFieldsToPerformance(
);
}
+/**
+ * Generates a unique key (slug) from an array of country codes.
+ *
+ * This function sorts the array of country codes alphabetically,
+ * joins them into a single string with underscore (`_`), and converts
+ * the result to lowercase.
+ *
+ * @param {Array} [countryCodes] - An array of country code strings.
+ * @return {string} A underscore-separated, lowercase string representing the sorted country codes.
+ */
+export function getCountryCodesKey( countryCodes = [] ) {
+ return [ ...countryCodes ].sort().join( '_' ).toLowerCase();
+}
+
/**
* Report fields fetched from report API.
*
diff --git a/js/src/hooks/useFetchBudgetRecommendation.js b/js/src/hooks/useFetchBudgetRecommendation.js
new file mode 100644
index 0000000000..bea96033ce
--- /dev/null
+++ b/js/src/hooks/useFetchBudgetRecommendation.js
@@ -0,0 +1,54 @@
+/**
+ * External dependencies
+ */
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { STORE_KEY } from '.~/data/constants';
+import getHighestBudget from '.~/utils/getHighestBudget';
+
+/**
+ * @typedef { import(".~/data/actions").CountryCode } CountryCode
+ */
+
+/**
+ * Fetch the highest budget recommendation for countries in a side effect.
+ *
+ * @param {Array} [countryCodes] An array of country codes. If empty, the fetch will not be triggered.
+ * @return {Object} Budget recommendation.
+ */
+const useFetchBudgetRecommendation = ( countryCodes ) => {
+ return useSelect(
+ ( select ) => {
+ const { getAdsBudgetRecommendations, hasFinishedResolution } =
+ select( STORE_KEY );
+
+ const data = getAdsBudgetRecommendations( countryCodes );
+ let highestDailyBudget = 0;
+ let highestDailyBudgetCountryCode;
+
+ if ( data ) {
+ const { recommendations } = data;
+ ( {
+ daily_budget: highestDailyBudget,
+ country: highestDailyBudgetCountryCode,
+ } = getHighestBudget( recommendations ) );
+ }
+
+ return {
+ data,
+ highestDailyBudget,
+ highestDailyBudgetCountryCode,
+ hasFinishedResolution: hasFinishedResolution(
+ 'getAdsBudgetRecommendations',
+ [ countryCodes ]
+ ),
+ };
+ },
+ [ countryCodes ]
+ );
+};
+
+export default useFetchBudgetRecommendation;
diff --git a/js/src/pages/create-paid-ads-campaign/index.js b/js/src/pages/create-paid-ads-campaign/index.js
index ae16722d76..3a2596af86 100644
--- a/js/src/pages/create-paid-ads-campaign/index.js
+++ b/js/src/pages/create-paid-ads-campaign/index.js
@@ -33,6 +33,7 @@ import {
recordStepperChangeEvent,
recordStepContinueEvent,
} from '.~/utils/tracks';
+import useFetchBudgetRecommendation from '.~/hooks/useFetchBudgetRecommendation';
const eventName = 'gla_paid_campaign_step';
const eventContext = 'create-ads';
@@ -52,6 +53,8 @@ const CreatePaidAdsCampaign = () => {
const { createAdsCampaign, updateCampaignAssetGroup } = useAppDispatch();
const { createNotice } = useDispatchCoreNotices();
const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
+ const { highestDailyBudget, hasFinishedResolution } =
+ useFetchBudgetRecommendation( countryCodes );
const handleStepperClick = ( nextStep ) => {
recordStepperChangeEvent(
@@ -114,7 +117,7 @@ const CreatePaidAdsCampaign = () => {
getHistory().push( getDashboardUrl( { campaign: 'saved' } ) );
};
- if ( ! countryCodes ) {
+ if ( ! countryCodes || ! hasFinishedResolution ) {
return null;
}
@@ -130,7 +133,7 @@ const CreatePaidAdsCampaign = () => {
/>
diff --git a/js/src/setup-ads/ads-stepper/index.js b/js/src/setup-ads/ads-stepper/index.js
index 51fe990ffb..5a7d70cdc3 100644
--- a/js/src/setup-ads/ads-stepper/index.js
+++ b/js/src/setup-ads/ads-stepper/index.js
@@ -19,12 +19,10 @@ import {
import SetupPaidAds from './setup-paid-ads';
/**
- * @param {Object} props React props
- * @param {boolean} props.isSubmitting When the form in the parent component, i.e SetupAdsForm, is currently being submitted via the useAdsSetupCompleteCallback hook.
* @fires gla_setup_ads with `{ triggered_by: 'step1-continue-button', action: 'go-to-step2' }`.
* @fires gla_setup_ads with `{ triggered_by: 'stepper-step1-button', action: 'go-to-step1'}`.
*/
-const AdsStepper = ( { isSubmitting } ) => {
+const AdsStepper = () => {
const [ step, setStep ] = useState( '1' );
useEventPropertiesFilter( FILTER_ONBOARDING, {
@@ -84,7 +82,7 @@ const AdsStepper = ( { isSubmitting } ) => {
'Create your paid campaign',
'google-listings-and-ads'
),
- content: ,
+ content: ,
onClick: handleStepClick,
},
] }
diff --git a/js/src/setup-ads/ads-stepper/setup-paid-ads.js b/js/src/setup-ads/ads-stepper/setup-paid-ads.js
index 6e38e09c92..905d51bc2d 100644
--- a/js/src/setup-ads/ads-stepper/setup-paid-ads.js
+++ b/js/src/setup-ads/ads-stepper/setup-paid-ads.js
@@ -2,6 +2,9 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
+import { isEqual } from 'lodash';
+import { useState, useEffect } from '@wordpress/element';
+import { getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
@@ -9,6 +12,14 @@ import { __ } from '@wordpress/i18n';
import AppButton from '.~/components/app-button';
import AdsCampaign from '.~/components/paid-ads/ads-campaign';
import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus';
+import useAdminUrl from '.~/hooks/useAdminUrl';
+import useNavigateAwayPromptEffect from '.~/hooks/useNavigateAwayPromptEffect';
+import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
+import useAdsSetupCompleteCallback from '.~/hooks/useAdsSetupCompleteCallback';
+import CampaignAssetsForm from '.~/components/paid-ads/campaign-assets-form';
+import { recordGlaEvent } from '.~/utils/tracks';
+import useFetchBudgetRecommendation from '.~/hooks/useFetchBudgetRecommendation';
+import AppSpinner from '.~/components/app-spinner';
import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants';
const { APPROVED } = GOOGLE_ADS_BILLING_STATUS;
@@ -16,35 +27,93 @@ const { APPROVED } = GOOGLE_ADS_BILLING_STATUS;
/**
* Renders the step to setup paid ads
*
- * @param {Object} props Component props.
- * @param {boolean} props.isSubmitting Indicates if the form is currently being submitted.
+ * @fires gla_launch_paid_campaign_button_click on submit
*/
-const SetupPaidAds = ( { isSubmitting } ) => {
+const SetupPaidAds = () => {
const { billingStatus } = useGoogleAdsAccountBillingStatus();
+ const [ didFormChanged, setFormChanged ] = useState( false );
+ const [ isSubmitted, setSubmitted ] = useState( false );
+ const [ handleSetupComplete, isSubmitting ] = useAdsSetupCompleteCallback();
+ const adminUrl = useAdminUrl();
+ const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
+ const { highestDailyBudget, hasFinishedResolution } =
+ useFetchBudgetRecommendation( countryCodes );
+
+ const initialValues = {
+ amount: highestDailyBudget,
+ };
+
+ useEffect( () => {
+ if ( isSubmitted ) {
+ // Force reload WC admin page to initiate the relevant dependencies of the Dashboard page.
+ const nextPath = getNewPath(
+ { guide: 'campaign-creation-success' },
+ '/google/dashboard'
+ );
+ window.location.href = adminUrl + nextPath;
+ }
+ }, [ isSubmitted, adminUrl ] );
+
+ const shouldPreventLeave = didFormChanged && ! isSubmitted;
+
+ useNavigateAwayPromptEffect(
+ __(
+ 'You have unsaved campaign data. Are you sure you want to leave?',
+ 'google-listings-and-ads'
+ ),
+ shouldPreventLeave
+ );
+
+ const handleSubmit = ( values ) => {
+ const { amount } = values;
+
+ recordGlaEvent( 'gla_launch_paid_campaign_button_click', {
+ audiences: countryCodes.join( ',' ),
+ budget: amount,
+ } );
+
+ handleSetupComplete( amount, countryCodes, () => {
+ setSubmitted( true );
+ } );
+ };
+
+ const handleChange = ( _, values ) => {
+ setFormChanged( ! isEqual( initialValues, values ) );
+ };
+
+ if ( ! countryCodes || ! hasFinishedResolution ) {
+ return ;
+ }
return (
- (
-
- ) }
- />
+
+ (
+
+ ) }
+ />
+
);
};
diff --git a/js/src/setup-ads/index.js b/js/src/setup-ads/index.js
index f191ada062..2f05026b64 100644
--- a/js/src/setup-ads/index.js
+++ b/js/src/setup-ads/index.js
@@ -2,12 +2,18 @@
* Internal dependencies
*/
import useLayout from '.~/hooks/useLayout';
-import SetupAdsForm from './setup-ads-form';
+import SetupAdsTopBar from './top-bar';
+import AdsStepper from './ads-stepper';
const SetupAds = () => {
useLayout( 'full-page' );
- return ;
+ return (
+ <>
+
+
+ >
+ );
};
export default SetupAds;
diff --git a/js/src/setup-ads/setup-ads-form.js b/js/src/setup-ads/setup-ads-form.js
deleted file mode 100644
index 74b298db71..0000000000
--- a/js/src/setup-ads/setup-ads-form.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * External dependencies
- */
-import { isEqual } from 'lodash';
-import { __ } from '@wordpress/i18n';
-import { useState, useEffect } from '@wordpress/element';
-import { getNewPath } from '@woocommerce/navigation';
-
-/**
- * Internal dependencies
- */
-import useAdminUrl from '.~/hooks/useAdminUrl';
-import useNavigateAwayPromptEffect from '.~/hooks/useNavigateAwayPromptEffect';
-import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
-import useAdsSetupCompleteCallback from '.~/hooks/useAdsSetupCompleteCallback';
-import CampaignAssetsForm from '.~/components/paid-ads/campaign-assets-form';
-import AdsStepper from './ads-stepper';
-import SetupAdsTopBar from './top-bar';
-import { recordGlaEvent } from '.~/utils/tracks';
-
-/**
- * @fires gla_launch_paid_campaign_button_click on submit
- */
-const SetupAdsForm = () => {
- const [ didFormChanged, setFormChanged ] = useState( false );
- const [ isSubmitted, setSubmitted ] = useState( false );
- const [ handleSetupComplete, isSubmitting ] = useAdsSetupCompleteCallback();
- const adminUrl = useAdminUrl();
- const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
-
- const initialValues = {
- amount: 0,
- };
-
- useEffect( () => {
- if ( isSubmitted ) {
- // Force reload WC admin page to initiate the relevant dependencies of the Dashboard page.
- const nextPath = getNewPath(
- { guide: 'campaign-creation-success' },
- '/google/dashboard'
- );
- window.location.href = adminUrl + nextPath;
- }
- }, [ isSubmitted, adminUrl ] );
-
- const shouldPreventLeave = didFormChanged && ! isSubmitted;
-
- useNavigateAwayPromptEffect(
- __(
- 'You have unsaved campaign data. Are you sure you want to leave?',
- 'google-listings-and-ads'
- ),
- shouldPreventLeave
- );
-
- const handleSubmit = ( values ) => {
- const { amount } = values;
-
- recordGlaEvent( 'gla_launch_paid_campaign_button_click', {
- audiences: countryCodes.join( ',' ),
- budget: amount,
- } );
-
- handleSetupComplete( amount, countryCodes, () => {
- setSubmitted( true );
- } );
- };
-
- const handleChange = ( _, values ) => {
- setFormChanged( ! isEqual( initialValues, values ) );
- };
-
- if ( ! countryCodes ) {
- return null;
- }
-
- return (
-
-
-
-
- );
-};
-
-export default SetupAdsForm;
diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads.js
index f5911f282d..2bb83b2e9f 100644
--- a/js/src/setup-mc/setup-stepper/setup-paid-ads.js
+++ b/js/src/setup-mc/setup-stepper/setup-paid-ads.js
@@ -23,6 +23,8 @@ import { GUIDE_NAMES, GOOGLE_ADS_BILLING_STATUS } from '.~/constants';
import { ACTION_COMPLETE, ACTION_SKIP } from './constants';
import SkipButton from './skip-button';
import clientSession from './clientSession';
+import useFetchBudgetRecommendation from '.~/hooks/useFetchBudgetRecommendation';
+import AppSpinner from '.~/components/app-spinner';
/**
* Clicking on the "Complete setup" button to complete the onboarding flow with paid ads.
@@ -42,6 +44,8 @@ export default function SetupPaidAds() {
const [ completing, setCompleting ] = useState( null );
const { createNotice } = useDispatchCoreNotices();
const { data: countryCodes } = useTargetAudienceFinalCountryCodes();
+ const { highestDailyBudget, hasFinishedResolution } =
+ useFetchBudgetRecommendation( countryCodes );
const [ handleSetupComplete ] = useAdsSetupCompleteCallback();
const { billingStatus } = useGoogleAdsAccountBillingStatus();
@@ -125,15 +129,21 @@ export default function SetupPaidAds() {
};
const paidAds = {
- amount: 0,
+ amount: highestDailyBudget,
...clientSession.getCampaign(),
};
+ if ( ! hasFinishedResolution || ! countryCodes ) {
+ return ;
+ }
+
return (
{
- clientSession.setCampaign( { ...values } );
+ if ( values.amount >= highestDailyBudget ) {
+ clientSession.setCampaign( values );
+ }
} }
>
{
+ if ( challenger.daily_budget > defender.daily_budget ) {
+ return challenger;
+ }
+ return defender;
+ } );
+}
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 987a7111ae..48ec190682 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
@@ -294,10 +294,6 @@ test.describe( 'Set up Ads account', () => {
page.getByRole( 'heading', { name: 'Set your budget' } )
).toBeVisible();
- await expect(
- setupBudgetPage.getLaunchPaidCampaignButton()
- ).toBeDisabled();
-
await expect(
page.getByRole( 'link', {
name: 'See what your ads will look like.',
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 adc31ea614..e41184030b 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
@@ -79,6 +79,24 @@ test.describe( 'Complete your campaign', () => {
[ 'GET' ]
),
+ completeCampaign.fulfillBudgetRecommendations( {
+ currency: 'USD',
+ recommendations: [
+ {
+ country: 'US',
+ daily_budget: 10,
+ },
+ {
+ country: 'TW',
+ daily_budget: 8,
+ },
+ {
+ country: 'GB',
+ daily_budget: 20,
+ },
+ ],
+ } ),
+
// The following mocks are requests will happen after completing the onboarding
completeCampaign.mockSuccessfulSettingsSyncRequest(),
@@ -195,6 +213,14 @@ test.describe( 'Complete your campaign', () => {
} );
test.describe( 'Set up budget', () => {
+ test( '"Daily average cost" input should have highest value set', async () => {
+ const dailyAverageCostInput =
+ setupBudgetPage.getBudgetInput();
+ await expect( dailyAverageCostInput ).toHaveValue(
+ '20.00'
+ );
+ } );
+
test( 'should see the low budget tip when the buget is set lower than the recommended value', async () => {
await setupBudgetPage.fillBudget( '1' );
const lowBudgetTip = setupBudgetPage.getLowerBudgetTip();
diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js
index ed0122e79f..4230c8c50d 100644
--- a/tests/e2e/utils/mock-requests.js
+++ b/tests/e2e/utils/mock-requests.js
@@ -365,6 +365,21 @@ export default class MockRequests {
);
}
+ /**
+ * Fulfill the budget recommendations request.
+ *
+ * @param {Object} payload
+ * @return {Promise}
+ */
+ async fulfillBudgetRecommendations( payload ) {
+ await this.fulfillRequest(
+ /\/wc\/gla\/ads\/campaigns\/budget-recommendation\b/,
+ payload,
+ 200,
+ [ 'GET' ]
+ );
+ }
+
/**
* Mock the request to connect Jetpack
*
diff --git a/tests/e2e/utils/pages/setup-ads/setup-budget.js b/tests/e2e/utils/pages/setup-ads/setup-budget.js
index bb164ff592..86eeab1af5 100644
--- a/tests/e2e/utils/pages/setup-ads/setup-budget.js
+++ b/tests/e2e/utils/pages/setup-ads/setup-budget.js
@@ -12,6 +12,26 @@ export default class SetupBudget extends MockRequests {
this.page = page;
}
+ /**
+ * Get budget recommendation tip section.
+ *
+ * @return {import('@playwright/test').Locator} The budget recommendation tip.
+ */
+ getBudgetRecommendationTip() {
+ return this.page.locator(
+ '.gla-budget-recommendation > .components-tip'
+ );
+ }
+
+ /**
+ * Get budget recommendation text row.
+ *
+ * @return {import('@playwright/test').Locator} The budget recommendation text row.
+ */
+ getBudgetRecommendationTextRow() {
+ return this.page.locator( '.components-tip p > em > strong' );
+ }
+
/**
* Get budget input.
*