Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Budget Setup Card. #2552

Open
wants to merge 21 commits into
base: feature/2459-campaign-creation-flow
Choose a base branch
from

Conversation

ankitrox
Copy link
Collaborator

@ankitrox ankitrox commented Aug 22, 2024

Changes proposed in this Pull Request:

Closes #2502 .

Replace this with a good description of your changes & reasoning.

Screenshots:

Screenshot 2024-08-22 at 9 40 27 PM

Detailed test instructions:

  1. Proceed with the onboarding flow and complete first 3 steps of the onboarding flow.
  2. On the 4th step, notice that Daily average cost input will have the pre-populated value with highest budget in the recommended budgets. For example:
  • If UAE (14 USD), Hong Kong (6 USD)and Vietnam (5 USD) are selected, the input should be populated with 14.00
  1. If you remove UAE from list of countries, the help text should get updated with the next highest value like Most merchants targeting similar countries set a daily budget of 6 USD (As next highest value is 6).

Additional details:

Changelog entry

Update - Change the campaign setup in the onboarding to use the recommended budget as the initial value and adjust its description.

@github-actions github-actions bot added the changelog: update Big changes to something that wasn't broken. label Aug 22, 2024
Copy link

codecov bot commented Aug 22, 2024

Codecov Report

Attention: Patch coverage is 2.77778% with 35 lines in your changes missing coverage. Please review.

Project coverage is 63.5%. Comparing base (5d5ed13) to head (ab2db99).
Report is 1 commits behind head on feature/2459-campaign-creation-flow.

Files with missing lines Patch % Lines
js/src/hooks/useBudgetRecommendationData.js 6.7% 13 Missing and 1 partial ⚠️
js/src/utils/getHighestBudget.js 0.0% 6 Missing and 2 partials ⚠️
js/src/components/paid-ads/ads-campaign.js 0.0% 2 Missing and 3 partials ⚠️
...-ads/budget-section/budget-recommendation/index.js 0.0% 3 Missing ⚠️
js/src/pages/create-paid-ads-campaign/index.js 0.0% 2 Missing and 1 partial ⚠️
js/src/components/paid-ads/budget-section/index.js 0.0% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                          Coverage Diff                          @@
##           feature/2459-campaign-creation-flow   #2552     +/-   ##
=====================================================================
- Coverage                                 63.8%   63.5%   -0.3%     
=====================================================================
  Files                                      326     329      +3     
  Lines                                     5089    5117     +28     
  Branches                                  1231    1238      +7     
=====================================================================
+ Hits                                      3245    3249      +4     
- Misses                                    1676    1695     +19     
- Partials                                   168     173      +5     
Flag Coverage Δ
js-unit-tests 63.5% <2.8%> (-0.3%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
js/src/hooks/useFetchBudgetRecommendationEffect.js 20.0% <ø> (ø)
js/src/components/paid-ads/budget-section/index.js 12.5% <0.0%> (+2.0%) ⬆️
...-ads/budget-section/budget-recommendation/index.js 6.2% <0.0%> (+2.2%) ⬆️
js/src/pages/create-paid-ads-campaign/index.js 10.0% <0.0%> (-0.8%) ⬇️
js/src/components/paid-ads/ads-campaign.js 0.0% <0.0%> (ø)
js/src/utils/getHighestBudget.js 0.0% <0.0%> (ø)
js/src/hooks/useBudgetRecommendationData.js 6.7% <6.7%> (ø)

... and 1 file with indirect coverage changes

@joemcgill joemcgill changed the base branch from develop to feature/2459-campaign-creation-flow August 22, 2024 14:29
Copy link
Collaborator

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, but issues I've noted a few issues where this could be improved.

@@ -266,16 +284,11 @@ test.describe( 'Complete your campaign', () => {
textContent
);

const responsePromise =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we no longer need to wait for this response after the country setting is changed? It looks like when the country values change, the text that reads "Tip: Most merchants targeting similar countries set a daily budget of %d USD" does change, so we should probably ensure that is still being tested.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I'm still unsure why we need to remove this. Could you clarify?

tests/e2e/utils/pages/setup-ads/setup-budget.js Outdated Show resolved Hide resolved
@ankitrox
Copy link
Collaborator Author

Thank you @joemcgill for your feedback.

I've addressed your comments and responded to one of your comments related to removing the response promise here.

Please let me know if there's anything else you want me to look into. Over to you for another round of review.

Thanks again!

Copy link
Collaborator

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I'm still seeing some unexpected behavior with these changes. If you go back to a previous step after after the budget recommendations have been displayed and then return to the last step, the budget recommendations are no longer showing. I've added a video to show what I'm seeing. Can you give this another look?

budget-card.mov

Could you also provide more context for this question about the e2e test changes?

@ankitrox
Copy link
Collaborator Author

@joemcgill
Thanks for the video. It helped me understand the issue well.

The issue was happening because PaidAdsSetupSections component was getting rendered before recommendationData is resolved.

There was also an issue of amount getting changed to dailyBudget even if user has entered some value previously which gets stored in sessionStorage. This has been fixed now.

Copy link
Collaborator

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ankitrox. This looks like it's working better now. I still have a question about one of the E2E changes that you made, which would be good to clarify, but I'm going to approve and send to @ankitguptaindia for QA.

@@ -266,16 +284,11 @@ test.describe( 'Complete your campaign', () => {
textContent
);

const responsePromise =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I'm still unsure why we need to remove this. Could you clarify?

@ankitrox
Copy link
Collaborator Author

ankitrox commented Sep 2, 2024

Hi @joemcgill

Apologies for missing the comment previously.

I have responded to the query here

@ankitguptaindia
Copy link
Member

QA/Test Report-

Testing Environment -

  • WordPress: 6.6.1
  • Theme active on store: Twenty Twenty-Four Version: 1.2
  • WooCommerce - Version 9.2.3
  • PHP: 8.3
  • Web Server: Nginx
  • Browser: Chrome - Version 127
  • OS: macOS Sonoma 14.6.1

Test Results -

Changes are working as expected. tested all related test scenarios and all are working well.

Functional Demo / Screencast -

Recording.812.mp4

Copy link
Member

@eason9487 eason9487 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some suggestions, questions, and issues that need to be resolved.

In addition, if I understand correctly, post-onboarding can still select audience countries when creating a campaign via the /wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&subpath=%2Fcampaigns%2Fcreate path.

However, there are now two issues with this page:

  • The recommended budget block is not shown
  • There is no API triggered to get the recommended budget after changing the audience countries.

image

@@ -93,6 +93,8 @@ const BudgetSection = ( { formProps, disabled = false, children } ) => {
<BudgetRecommendation
countryCodes={ countryCodes }
dailyAverageCost={ amount }
currency={ currency }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BudgetRecommendation component doesn't use the currency prop.

@@ -11,6 +11,7 @@ import { Form } from '@woocommerce/components';
import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes';
import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus';
import useFetchBudgetRecommendationEffect from '.~/components/paid-ads/budget-section/budget-recommendation/useFetchBudgetRecommendationEffect';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I understand is that the same logic of setting the default budget will then be applied to all ad creation forms, so moving the useFetchBudgetRecommendationEffect hook to the .~/hooks directory would be more appropriate.

@@ -78,6 +96,14 @@ export default function PaidAdsSetupSections( { onStatesReceived } ) {
const { hasGoogleAdsConnection } = useGoogleAdsAccount();
const { data: targetAudience } = useTargetAudienceFinalCountryCodes();
const { billingStatus } = useGoogleAdsAccountBillingStatus();
const { data: recommendationData } =
useFetchBudgetRecommendationEffect( targetAudience );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may cause API to fail with a 400 bad request error.

image

Steps:

  1. Proceed to onboarding step 4
  2. Refresh webpage

currentPaidAds = {
...currentPaidAds,
amount: sessionAmount || dailyBudget,
recommendationData,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder why recommendationData needs to be a part of the paidAds state?

@@ -143,6 +184,7 @@ export default function PaidAdsSetupSections( { onStatesReceived } ) {
const initialValues = {
countryCodes: paidAds.countryCodes,
amount: paidAds.amount,
recommendationData: paidAds.recommendationData,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recommendationData is not used as the form value, so passing it around this way is not ideal.

Comment on lines 61 to 63
recommendations.filter( ( recommendation ) =>
countryCodes.includes( recommendation.country )
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about in which case it needs this filter?

Comment on lines 42 to 56
/*
* 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;
} );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is completely the same as:

/*
* 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;
} );
}

Suggest considering whether it can be integrated into the useFetchBudgetRecommendationEffect hook or extracted as a util to eliminate duplication.

Comment on lines 156 to 169
if ( dailyBudget !== undefined ) {
// If the amount is already set in session, use that one.
const sessionData = clientSession.getCampaign();
const { amount: sessionAmount } = sessionData;

setPaidAds( ( currentPaidAds ) => {
currentPaidAds = {
...currentPaidAds,
amount: sessionAmount || dailyBudget,
recommendationData,
};
return resolveInitialPaidAds( currentPaidAds, targetAudience );
} );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is an amount restored from client session, the currentPaidAds.amount is already that value when the first time running this useEffect, so it may not need to read from client session again.

// Resolve the starting paid ads data with the campaign data stored in the client session.
const startingPaidAds = {
...defaultPaidAds,
...clientSession.getCampaign(),

Additionally, moving this logic into the resolveInitialPaidAds function would be better as it intends to resolve the initial values.

Comment on lines 16 to 18
* Get budget recommendation tip section.
*
* @return {import('@playwright/test').Locator} The budget recommendation text row.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This JSDoc description is not quite consistent.

@eason9487
Copy link
Member

The #2551 seems to have some code conflicts with this PR and even some of the logic might need to be rewritten afterward, so it's recommended to defer adjustment of this PR and rebase this PR onto #2551 after it's approved and merged.

Copy link
Collaborator

@asvinb asvinb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I left a comment about setting the initial values. Can you let me know what you think please?
Also there'll be additional work once #2551 is merged.

@@ -55,6 +57,14 @@ export default function AdsCampaign( {
'google-listings-and-ads'
);

// Set the amount from client session if it is available.
useEffect( () => {
Copy link
Collaborator

@asvinb asvinb Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox Wondering if we can set the amount in the initialValues prop for the forms directly instead of setting the initial form value in the child components?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I see AdsCampaign being used in 3 places:

  • js/src/pages/create-paid-ads-campaign/index.js - the initial values are set via CampaignAssetsForm
  • js/src/pages/edit-paid-ads-campaign/index.js - the initial values are set via CampaignAssetsForm. Here we don't need to worry about using grabbing amount from localStorage since it's for editing a campaign.
  • js/src/setup-ads/ads-stepper/index.js - The AdsStepper component is used by SetupAdsForm which also makes use of CampaignAssetsForm to set the initial values.

So that leaves only js/src/pages/create-paid-ads-campaign/index.js and js/src/setup-ads/ads-stepper/index.js where we can potentially set the amount to the value from localStorage. Maybe we can even create a hook for it to reduce the code duplication.

Let me know what you think.

Copy link
Collaborator

@asvinb asvinb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I left some more comments. Can you kindly let me know about the suggested approach about setting the default value for amount via the initialValues prop of CampaignAssetsForm.
Also you need to check when there's no recommended budget to not display the recommendation. This is what I can see on my side while testing:
image

);

const { country = '', daily_budget: dailyBudget } = getHighestBudget(
budgetData?.recommendations || []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox We already have a check here https://github.com/woocommerce/google-listings-and-ads/pull/2552/files#diff-3bdecec9939ad884905f627b3905c8b138f73d843890bb6d41799694c57f2ab8R10 in case recommendations is falsy. Shall we return an empty array in the getHighestBudget function instead of null?

>
<CampaignPreviewCard />
</BudgetSection>
{ ! loading && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox This behaviour is different from what we currently have, i.e having a spinner and loading state while the data is being loaded. Is there a particular reason why we need to wait for the recommendations?

formProps={ formContext }
disabled={ disabledBudgetSection }
countryCodes={ formContext.values.countryCodes }
country={ country }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox We are passing those new props country, dailyBudget and isMultiple to BudgetSection. However those are being then passed to BudgetRecommendation. Can't we get those values directly in BudgetRecommendation?

<CampaignPreviewCard />
</BudgetSection>
) }
{ loading && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox See my comment above.

return null;
}
const {
isMultiple,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the countryCodes prop was removed but it's still being passed to BudgetRecommendation in js/src/components/paid-ads/budget-section/index.js

* @param {JSX.Element} [props.children] Extra content to be rendered under the card of budget inputs.
*/
const BudgetSection = ( {
formProps,
country,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox See my previous comment about passing those props.

const { country = '', daily_budget: dailyBudget } = getHighestBudget(
budgetData?.recommendations || []
);
const multipleRecommendations = budgetData?.recommendations.length > 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox The code is duplicated here. Can we move it to a hook which can be reused elsewhere?

@ankitrox
Copy link
Collaborator Author

@asvinb I've created the new hook useBudgetRecommendationData, but kept the useEffectin same component because if we move up to SetupAdsForm component as well as for CreatePaidAdsCampaign, it will create the network error issue for useFetchBudgetRecommendationEffect because country codes can be undefined or blank and since these are hooks, we won't be able to call them conditionally.

Copy link
Collaborator

@asvinb asvinb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox I left some more comments. Can you check them out please?
Most importantly I think we can reduce a lot of the complexity around countryCodes. Right now, we need to fetch the budget recommendation data once the country code changes and the country codes are tied to the form. In #2535 we are planning to remove countryCodes altogether, instead relying on the country codes from useTargetAudienceFinalCountryCodes. Here is what I suggest:

  1. Remove the Audience field everywhere in the current PR (as opposed to removing them in Consolidate the ad creation step in the Ads Setup flow with the one used in Onboarding #2535)
  2. Have the initialValues for CampaignAssetsForm components set with the recommended budget or data from local storage.

The useBudgetRecommendationData won't need to accept any parameters since it can get the list of targeted countries via the useTargetAudienceFinalCountryCodes

What do you think @joemcgill ?

useEffect( () => {
if ( ! loading ) {
const {
country: budgetCountries = '',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useFetchBudgetRecommendationEffect( countryCodes );

const [ country, setCountry ] = useState( '' );
const [ dailyBudget, setDailyBudget ] = useState( 0 );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox Instead of defining all those state variables, can we not define only one state variable which is an object having all those properties?


setCountry( budgetCountries );
setDailyBudget( recommendedDailyBudget );
setMultipleRecommendations( data?.recommendations.length > 1 );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox Do we need to store that in a state variable? We can have a variable where data?.recommendations.length > 1 and return the variable.

const args = [ initialValues, values ].map(
( { countryCodes, ...v } ) => {
v.countrySet = new Set( countryCodes );
return v;
}
);

// Set the amount in session storage.
if ( isValid && _?.name === 'amount' ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitrox Why not use values.amount?

};

return (
<Form
initialValues={ initialValues }
onChange={ ( _, values, isValid ) => {
setPaidAds( { ...paidAds, ...values, isValid } );

if ( isValid ) {
clientSession.setCampaign( values );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog: update Big changes to something that wasn't broken.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Campaign Creation: Update Budget Setup Card
5 participants