diff --git a/js/src/components/paid-ads/campaign-preview/mockup-search.js b/js/src/components/paid-ads/campaign-preview/mockup-search.js index bdf2b04d2b..fb1e013439 100644 --- a/js/src/components/paid-ads/campaign-preview/mockup-search.js +++ b/js/src/components/paid-ads/campaign-preview/mockup-search.js @@ -48,7 +48,7 @@ export default function MockupSearch( { product } ) { - +
diff --git a/js/src/hooks/useGoogleAdsAccount.js b/js/src/hooks/useGoogleAdsAccount.js index 9760ce87c8..16db5002f1 100644 --- a/js/src/hooks/useGoogleAdsAccount.js +++ b/js/src/hooks/useGoogleAdsAccount.js @@ -9,6 +9,7 @@ import { useCallback } from '@wordpress/element'; */ import { STORE_KEY } from '.~/data/constants'; import { useAppDispatch } from '.~/data'; +import { GOOGLE_ADS_ACCOUNT_STATUS } from '.~/constants'; import useGoogleAccount from './useGoogleAccount'; const googleAdsAccountSelector = 'getGoogleAdsAccount'; @@ -36,6 +37,15 @@ const useGoogleAdsAccount = () => { googleAdsAccountSelector ); + // The "incomplete" status means there is a connected account but billing is not yet set + // so it's considered as `true`. + // The main reason for not using a naming like `isGoogleAdsConnected` here is to make + // a slight distinction from the "connected" status. + const hasGoogleAdsConnection = [ + GOOGLE_ADS_ACCOUNT_STATUS.CONNECTED, + GOOGLE_ADS_ACCOUNT_STATUS.INCOMPLETE, + ].includes( acc?.status ); + return { googleAdsAccount: acc, isResolving: isResolvingGoogleAdsAccount, @@ -43,6 +53,7 @@ const useGoogleAdsAccount = () => { hasFinishedResolution: selector.hasFinishedResolution( googleAdsAccountSelector ), + hasGoogleAdsConnection, }; }, [ google, isResolving, refetchGoogleAdsAccount ] diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js index 14099026e6..fe3330df4c 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js @@ -15,7 +15,7 @@ import AppDocumentationLink from '.~/components/app-documentation-link'; import CampaignPreview from '.~/components/paid-ads/campaign-preview'; import './paid-ads-features-section.scss'; -function FeatureList() { +function FeatureList( { hideBudgetContent } ) { const featuresItems = [ { Icon: GridiconCheckmark, @@ -24,22 +24,27 @@ function FeatureList() { 'google-listings-and-ads' ), }, - { - Icon: GridiconCheckmark, - content: __( - 'Set a daily budget, and only pay when someone clicks.', - 'google-listings-and-ads' - ), - }, - { - Icon: GridiconGift, - content: __( - 'Claim $500 in ads credit when you spend your first $500 with Google Ads. Terms and conditions apply.', - 'google-listings-and-ads' - ), - }, ]; + if ( ! hideBudgetContent ) { + featuresItems.push( + { + Icon: GridiconCheckmark, + content: __( + 'Set a daily budget, and only pay when someone clicks.', + 'google-listings-and-ads' + ), + }, + { + Icon: GridiconGift, + content: __( + 'Claim $500 in ads credit when you spend your first $500 with Google Ads. Terms and conditions apply.', + 'google-listings-and-ads' + ), + } + ); + } + return (
{ featuresItems.map( ( { Icon, content }, idx ) => ( @@ -61,11 +66,13 @@ function FeatureList() { * for the next actions: skip or continue the paid ads setup. * * @param {Object} props React props. + * @param {boolean} props.hideBudgetContent Whether to hide the content about the ad budget. * @param {boolean} props.hideFooterButtons Whether to hide the buttons at the card footer. * @param {JSX.Element} props.skipButton Button to skip paid ads setup. * @param {JSX.Element} props.continueButton Button to continue paid ads setup. */ export default function PaidAdsFeaturesSection( { + hideBudgetContent, hideFooterButtons, skipButton, continueButton, @@ -120,7 +127,9 @@ export default function PaidAdsFeaturesSection( { 'google-listings-and-ads' ) }
- + { __( 'Source: Google Internal Data, July 2020', 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 f29fa2ff7a..86c3739bf7 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 @@ -11,16 +11,14 @@ import { Form } from '@woocommerce/components'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; -import GoogleAdsAccountSection from './google-ads-account-section'; import AudienceSection from '.~/components/paid-ads/audience-section'; import BudgetSection from '.~/components/paid-ads/budget-section'; import BillingCard from '.~/components/paid-ads/billing-card'; +import SpinnerCard from '.~/components/spinner-card'; +import Section from '.~/wcdl/section'; import validateCampaign from '.~/components/paid-ads/validateCampaign'; import clientSession from './clientSession'; -import { - GOOGLE_ADS_ACCOUNT_STATUS, - GOOGLE_ADS_BILLING_STATUS, -} from '.~/constants'; +import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; /** * @typedef { import(".~/data/actions").CountryCode } CountryCode @@ -77,7 +75,7 @@ function resolveInitialPaidAds( paidAds, targetAudience ) { * @param {(onStatesReceived: PaidAdsData)=>void} props.onStatesReceived Callback to receive the data for setting up paid ads when initial and also when the audience, budget, and billing are updated. */ export default function PaidAdsSetupSections( { onStatesReceived } ) { - const { googleAdsAccount } = useGoogleAdsAccount(); + const { hasGoogleAdsConnection } = useGoogleAdsAccount(); const { data: targetAudience } = useTargetAudienceFinalCountryCodes(); const { billingStatus } = useGoogleAdsAccountBillingStatus(); @@ -135,7 +133,11 @@ export default function PaidAdsSetupSections( { onStatesReceived } ) { }, [ targetAudience ] ); if ( ! targetAudience || ! billingStatus ) { - return ; + return ( +
+ +
+ ); } const initialValues = { @@ -153,16 +155,12 @@ export default function PaidAdsSetupSections( { onStatesReceived } ) { > { ( formProps ) => { const { countryCodes } = formProps.values; - const disabledAudience = ! [ - GOOGLE_ADS_ACCOUNT_STATUS.CONNECTED, - GOOGLE_ADS_ACCOUNT_STATUS.INCOMPLETE, - ].includes( googleAdsAccount?.status ); + const disabledAudience = ! hasGoogleAdsConnection; const disabledBudget = disabledAudience || countryCodes.length === 0; return ( <> - clientSession.getShowPaidAdsSetup( false ) @@ -123,31 +126,32 @@ export default function SetupPaidAds() { function createSkipButton( text ) { const eventProps = { opened_paid_ads_setup: 'no', - google_ads_account_status: 'unknown', + google_ads_account_status: googleAdsAccount?.status, billing_method_status: 'unknown', campaign_form_validation: 'unknown', }; if ( showPaidAdsSetup ) { const selector = select( STORE_KEY ); - const account = selector.getGoogleAdsAccount(); const billing = selector.getGoogleAdsAccountBillingStatus(); merge( eventProps, { opened_paid_ads_setup: 'yes', - google_ads_account_status: account?.status, billing_method_status: billing?.status, campaign_form_validation: paidAds.isValid ? 'valid' : 'invalid', } ); } + const disabledSkip = + completing === ACTION_COMPLETE || ! hasGoogleAdsConnection; + return ( + { test.describe( 'Create your paid campaign', () => { test.beforeAll( async () => { setupBudgetPage = new SetupBudgetPage( page ); - await setupBudgetPage.fulfillBudgetRecommendation( { - currency: 'USD', - recommendations: [ - { - country: 'US', - daily_budget_low: 5, - daily_budget_high: 15, - }, - ], - } ); } ); test( 'Continue to create paid campaign', async () => { @@ -360,7 +350,7 @@ test.describe( 'Set up Ads account', () => { test( 'Budget Recommendation', async () => { await expect( - page.getByText( 'set a daily budget of 5 to 15 USD' ) + page.getByText( 'set a daily budget of 15 USD' ) ).toBeVisible(); } ); } ); 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 c34ac1a1c4..4f9594f7f2 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 @@ -61,6 +61,12 @@ test.describe( 'Complete your campaign', () => { // Mock Merchant Center as connected completeCampaign.mockMCConnected(), + // Mock Google Ads as not yet connected. + setupAdsAccountPage.mockAdsAccountDisconnected(), + + // Mock there is no existing Google Ads account. + setupAdsAccountPage.mockAdsAccountsResponse( [] ), + // Mock MC step as paid_ads completeCampaign.mockMCSetup( 'incomplete', 'paid_ads' ), @@ -79,6 +85,40 @@ test.describe( 'Complete your campaign', () => { completeCampaign.fulfillSyncableProductsCountRequest( { count: 1024, } ), + + // The following mocks are requests will happen after completing the onboarding + completeCampaign.mockSuccessfulSettingsSyncRequest(), + + completeCampaign.fulfillProductStatisticsRequest( { + timestamp: 1695011644, + statistics: { + active: 0, + expiring: 0, + pending: 0, + disapproved: 0, + not_synced: 1137, + }, + scheduled_sync: 1, + } ), + + completeCampaign.fulfillAccountIssuesRequest( { + issues: [], + page: 1, + total: 0, + } ), + + completeCampaign.fulfillProductIssuesRequest( { + issues: [], + page: 1, + total: 0, + } ), + + completeCampaign.fulfillMCReview( { + cooldown: 0, + issues: [], + reviewEligibleRegions: [], + status: 'ONBOARDING', + } ), ] ); await completeCampaign.goto(); @@ -127,124 +167,63 @@ test.describe( 'Complete your campaign', () => { } ); } ); - test.describe( 'Click "Skip this step for now"', () => { - test.beforeAll( async () => { - await Promise.all( [ - // Mock settings sync request - completeCampaign.mockSuccessfulSettingsSyncRequest(), - - // Mock product statistics request - completeCampaign.fulfillProductStatisticsRequest( { - timestamp: 1695011644, - statistics: { - active: 0, - expiring: 0, - pending: 0, - disapproved: 0, - not_synced: 1137, - }, - scheduled_sync: 1, - } ), - ] ); - await completeCampaign.clickSkipStepButton(); - } ); - - test( 'should see the setup success modal', async () => { - const setupSuccessModal = page - .locator( '.components-modal__content' ) - .filter( { - hasText: - 'You’ve successfully set up Google Listings & Ads!', - } ); - await expect( setupSuccessModal ).toBeVisible(); - } ); - - test( 'should see the url contains product-feed', async () => { - expect( page.url() ).toMatch( /path=%2Fgoogle%2Fproduct-feed/ ); - } ); - } ); - - test.describe( 'Set up paid ads', () => { - test.beforeAll( async () => { - await Promise.all( [ - // Mock settings sync request - completeCampaign.mockSuccessfulSettingsSyncRequest(), - - // Mock product statistics request - completeCampaign.fulfillProductStatisticsRequest( { - timestamp: 1695011644, - statistics: { - active: 0, - expiring: 0, - pending: 0, - disapproved: 0, - not_synced: 1137, - }, - scheduled_sync: 1, - } ), - ] ); - await completeCampaign.goto(); - } ); - - test( 'should see the "Create a paid ad campaign" button is enabled', async () => { - const button = completeCampaign.getCreatePaidAdButton(); - await expect( button ).toBeVisible(); - await expect( button ).toBeEnabled(); - } ); - - test.describe( 'Click "Create a paid ad campaign" button', () => { - test.beforeAll( async () => { - await completeCampaign.clickCreatePaidAdButton(); - await setupAdsAccountPage.mockAdsAccountsResponse( [] ); + test.describe( 'Google Ads', () => { + test.describe( 'Google Ads section', () => { + test.beforeEach( async () => { + await completeCampaign.goto(); } ); - test( 'should see "Complete setup" button is disabled', async () => { - const completeSetupButton = - completeCampaign.getCompleteSetupButton(); - await expect( completeSetupButton ).toBeVisible(); - await expect( completeSetupButton ).toBeDisabled(); + test( 'should see Google Ads section', async () => { + const section = completeCampaign.getAdsAccountSection(); + await expect( section ).toBeVisible(); } ); - test( 'should see "Skip paid ads creation" button is enabled', async () => { - const skipPaidAdsCreationButton = - completeCampaign.getSkipPaidAdsCreationButton(); - await expect( skipPaidAdsCreationButton ).toBeVisible(); - await expect( skipPaidAdsCreationButton ).toBeEnabled(); - } ); - - test( 'should see "Google Ads" section is enabled', async () => { - const googleAdsSection = - completeCampaign.getAdsAccountSection(); - await expect( googleAdsSection ).toBeVisible(); + test( 'should only see non-budget related items and no footer buttons in the Features section when not yet connected', async () => { + const section = completeCampaign.getPaidAdsFeaturesSection(); + const items = section.locator( + '.gla-paid-ads-features-section__feature-list > div' + ); + await expect( items ).toHaveCount( 1 ); + await expect( items ).toContainText( 'Promote your products' ); - // Cannot use toBeEnabled() because
is not a native control element - // such as