From 2c4f863eacf62283a337ea2e803296d2950479af Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 24 Sep 2024 21:58:23 +0400 Subject: [PATCH] Simplify hooks. --- .../paid-ads/campaign-assets-form.js | 34 ++++++-- .../components/paid-ads/validateCampaign.js | 2 +- .../hooks/useHighestBudgetRecommendation.js | 53 ----------- .../useValidateCampaignWithCountryCodes.js | 87 ++++++++++++++----- .../setup-paid-ads/paid-ads-setup-sections.js | 36 +++----- 5 files changed, 107 insertions(+), 105 deletions(-) delete mode 100644 js/src/hooks/useHighestBudgetRecommendation.js diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index 7a1befc0ed..01b6cbfe2e 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useState, useMemo } from '@wordpress/element'; +import { useState, useMemo, useEffect, useRef } from '@wordpress/element'; import { noop } from 'lodash'; /** @@ -74,21 +74,42 @@ export default function CampaignAssetsForm( { onChange = noop, ...adaptiveFormProps } ) { + const formRef = useRef(); const initialAssetGroup = useMemo( () => { return convertAssetEntityGroupToFormValues( assetEntityGroup ); }, [ assetEntityGroup ] ); - const [ baseAssetGroup, setBaseAssetGroup ] = useState( initialAssetGroup ); const [ hasImportedAssets, setHasImportedAssets ] = useState( false ); - const [ countryCodes, setCountryCodes ] = useState( [] ); - const { validateCampaignWithCountryCodes } = - useValidateCampaignWithCountryCodes( countryCodes ); + const { + validateCampaignWithCountryCodes, + dailyBudget, + refreshCountryCodes, + currencyCode, + } = useValidateCampaignWithCountryCodes(); + + // Grab the recommendations for the initial country codes. + useEffect( () => { + refreshCountryCodes( initialCampaign.countryCodes ); + }, [ initialCampaign.countryCodes, refreshCountryCodes ] ); const handleOnChange = ( _, values, arg ) => { - setCountryCodes( values.countryCodes ); onChange( _, values, arg ); + + // Whenever there's a change, update the country codes in the validation function. + refreshCountryCodes( values.countryCodes ); }; + useEffect( () => { + const { setValue } = formRef.current; + + // Trigger a form value change to refresh the validation function once again with the new budget values + // If the validation function and values do not change, then the validation will not be triggerred since the `validate` + // function uses useCallback and will not be re-created. + setValue( 'dailyBudget', dailyBudget ); + // Sometimes the currency code takes time to resolve and the budget data is already available. + setValue( 'currencyCode', currencyCode ); + }, [ dailyBudget, currencyCode ] ); + const extendAdapter = ( formContext ) => { const assetGroupErrors = validateAssetGroup( formContext.values ); const finalUrl = assetEntityGroup?.[ ASSET_GROUP_KEY.FINAL_URL ]; @@ -127,6 +148,7 @@ export default function CampaignAssetsForm( { return ( { if ( Number.isFinite( values.amount ) && - Number.isFinite( opts.dailyBudget ) + Number.isFinite( opts?.dailyBudget ) ) { const { amount } = values; const { dailyBudget, formatAmount } = opts; diff --git a/js/src/hooks/useHighestBudgetRecommendation.js b/js/src/hooks/useHighestBudgetRecommendation.js deleted file mode 100644 index 8c29612a1d..0000000000 --- a/js/src/hooks/useHighestBudgetRecommendation.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * External dependencies - */ -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_KEY } from '.~/data/constants'; -import getHighestBudget from '.~/utils/getHighestBudget'; -import useAdsCurrency from './useAdsCurrency'; - -/** - * @typedef { import(".~/data/actions").CountryCode } CountryCode - */ - -/** - * @typedef {Object} HighestBudgetRecommendationHook - * @property {number|undefined} dailyBudget The highest recommended daily budget. If no recommendations are available, this will be `undefined`. - * @property {boolean} hasFinishedResolution A boolean indicating whether the budget recommendation data has been fetched. - * @property {(amount: number) => string} formatAmount A function to format the budget amount according to the currency settings. - */ - -/** - * Fetch the highest budget recommendation for countries in a side effect. - * - * @param {Array} [countryCodes] An array of country codes. If empty, the dailyBudget will be undefined. - * @return {HighestBudgetRecommendationHook} An object containing the `dailyBudget` value, `formatAmount` function and a `hasFinishedResolution` state. - */ -const useHighestBudgetRecommendation = ( countryCodes ) => { - const { formatAmount } = useAdsCurrency(); - - return useSelect( - ( select ) => { - const { getAdsBudgetRecommendations, hasFinishedResolution } = - select( STORE_KEY ); - - const budgetData = getAdsBudgetRecommendations( countryCodes ); - const budget = getHighestBudget( budgetData?.recommendations ); - - return { - dailyBudget: budget?.daily_budget, - formatAmount, - hasFinishedResolution: hasFinishedResolution( - 'getAdsBudgetRecommendations' - ), - }; - }, - [ countryCodes, formatAmount ] - ); -}; - -export default useHighestBudgetRecommendation; diff --git a/js/src/hooks/useValidateCampaignWithCountryCodes.js b/js/src/hooks/useValidateCampaignWithCountryCodes.js index 69a80abf01..3d54223771 100644 --- a/js/src/hooks/useValidateCampaignWithCountryCodes.js +++ b/js/src/hooks/useValidateCampaignWithCountryCodes.js @@ -1,8 +1,16 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; + /** * Internal dependencies */ -import useHighestBudgetRecommendation from './useHighestBudgetRecommendation'; +import { STORE_KEY } from '.~/data/constants'; +import useAdsCurrency from './useAdsCurrency'; import validateCampaign from '.~/components/paid-ads/validateCampaign'; +import getHighestBudget from '.~/utils/getHighestBudget'; /** * @typedef {import('.~/components/types.js').CampaignFormValues} CampaignFormValues @@ -21,27 +29,66 @@ import validateCampaign from '.~/components/paid-ads/validateCampaign'; /** * Validate campaign form. Accepts the form values object and returns errors object. * - * @param {Array} [countryCodes] Country code array. If not provided, the validate function will not take into account budget recommendations. + * @param {Array} [initialCountryCodes] Country code array. If not provided, the validate function will not take into account budget recommendations. * @return {ValidateCampaignWithCountryCodesHook} An object containing the `validateCampaignWithCountryCodes` function and a `loading` state. */ -const useValidateCampaignWithCountryCodes = ( countryCodes ) => { - const { dailyBudget, formatAmount, hasFinishedResolution } = - useHighestBudgetRecommendation( countryCodes ); - - /** - * Validate campaign form. Accepts the form values object and returns errors object. - * - * @param {CampaignFormValues} values Campaign form values. - * @return {Object} An object containing any validation errors. If no errors, the object will be empty. - */ - const validateCampaignWithCountryCodes = ( values ) => { - return validateCampaign( values, { - dailyBudget, - formatAmount, - } ); - }; - - return { validateCampaignWithCountryCodes, hasFinishedResolution }; +const useValidateCampaignWithCountryCodes = ( initialCountryCodes ) => { + const { + formatAmount, + adsCurrencyConfig: { code }, + } = useAdsCurrency(); + const [ countryCodes, setCountryCodes ] = useState( [] ); + + useEffect( () => { + setCountryCodes( initialCountryCodes ); + }, [ initialCountryCodes ] ); + + return useSelect( + ( select ) => { + // If no country codes are provided, return the default validateCampaign function. + if ( ! countryCodes?.length ) { + return { + validateCampaignWithCountryCodes: validateCampaign, + dailyBudget: null, + formatAmount, + setCountryCodes, + currencyCode: code, + hasFinishedResolution: true, + }; + } + + const { getAdsBudgetRecommendations, hasFinishedResolution } = + select( STORE_KEY ); + const budgetData = getAdsBudgetRecommendations( countryCodes ); + const budget = getHighestBudget( budgetData?.recommendations ); + + /** + * Validate campaign form. Accepts the form values object and returns errors object. + * + * @param {CampaignFormValues} values Campaign form values. + * @return {Object} An object containing any validation errors. If no errors, the object will be empty. + */ + const validateCampaignWithCountryCodes = ( values ) => { + return validateCampaign( values, { + dailyBudget: budget?.daily_budget, + formatAmount, + } ); + }; + + return { + validateCampaignWithCountryCodes, + dailyBudget: budget?.daily_budget, + formatAmount, + refreshCountryCodes: setCountryCodes, + currencyCode: code, + hasFinishedResolution: + hasFinishedResolution( 'getAdsBudgetRecommendations', [ + countryCodes, + ] ) && code, + }; + }, + [ countryCodes, formatAmount, code ] + ); }; export default useValidateCampaignWithCountryCodes; diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js index ee41c83766..40f3111f11 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js @@ -45,38 +45,19 @@ export default function PaidAdsSetupSections( { onStatesReceived, countryCodes, } ) { - const { - hasFinishedResolution: hasResolvedValidateCampaignWithCountryCodes, - validateCampaignWithCountryCodes, - } = useValidateCampaignWithCountryCodes( countryCodes ); + const { validateCampaignWithCountryCodes, hasFinishedResolution } = + useValidateCampaignWithCountryCodes( countryCodes ); const { billingStatus } = useGoogleAdsAccountBillingStatus(); - const onStatesReceivedRef = useRef(); onStatesReceivedRef.current = onStatesReceived; - /** - * Resolve the initial paid ads data from the given paid ads data. - * Parts of the resolved data are used in the `initialValues` prop of `Form` component. - * - * @param {PaidAdsData} paidAds The paid ads data as the base to be resolved with other states. - * @return {PaidAdsData} The resolved paid ads data. - */ - function resolveInitialPaidAds( paidAds ) { - const nextPaidAds = { ...paidAds }; - nextPaidAds.isValid = ! Object.keys( - validateCampaignWithCountryCodes( nextPaidAds ) - ).length; - - return nextPaidAds; - } - const [ paidAds, setPaidAds ] = useState( () => { // Resolve the starting paid ads data with the campaign data stored in the client session. const startingPaidAds = { ...defaultPaidAds, ...clientSession.getCampaign(), }; - return resolveInitialPaidAds( startingPaidAds ); + return startingPaidAds; } ); const isBillingCompleted = @@ -97,15 +78,20 @@ export default function PaidAdsSetupSections( { For example, refresh page during onboarding flow after the billing setup is finished. */ useEffect( () => { + const isValid = ! Object.keys( + validateCampaignWithCountryCodes( paidAds ) + ).length; const nextPaidAds = { ...paidAds, - isReady: paidAds.isValid && isBillingCompleted, + isValid, + isReady: isValid && isBillingCompleted, }; + onStatesReceivedRef.current( nextPaidAds ); clientSession.setCampaign( nextPaidAds ); - }, [ paidAds, isBillingCompleted ] ); + }, [ paidAds, isBillingCompleted, validateCampaignWithCountryCodes ] ); - if ( ! billingStatus || ! hasResolvedValidateCampaignWithCountryCodes ) { + if ( ! billingStatus || ! hasFinishedResolution ) { return (