diff --git a/js/src/components/paid-ads/ads-campaign.js b/js/src/components/paid-ads/ads-campaign/ads-campaign.js similarity index 80% rename from js/src/components/paid-ads/ads-campaign.js rename to js/src/components/paid-ads/ads-campaign/ads-campaign.js index c65ca371da..5800d7249b 100644 --- a/js/src/components/paid-ads/ads-campaign.js +++ b/js/src/components/paid-ads/ads-campaign/ads-campaign.js @@ -12,12 +12,11 @@ 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'; +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 @@ -26,24 +25,22 @@ import PaidAdsFaqsPanel from './faqs-panel'; /** * 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. + * Please note that this component relies on a CampaignAssetsForm's context and custom adapter, + * so it expects a `CampaignAssetsForm` to exist 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 {JSX.Element|Function} props.continueButton Continue button component. * @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, + continueButton, trackingContext, } ) { const isCreation = ! campaign; const formContext = useAdaptiveFormContext(); - const { isValidForm } = formContext; const disabledBudgetSection = ! formContext.values.countryCodes.length; const helperText = isCreation @@ -102,13 +99,11 @@ export default function AdsCampaign( { - - { __( 'Continue', 'google-listings-and-ads' ) } - + { typeof continueButton === 'function' + ? continueButton( { + formProps: formContext, + } ) + : continueButton } 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/components/paid-ads/continue-button.js b/js/src/components/paid-ads/continue-button.js new file mode 100644 index 0000000000..53788766f3 --- /dev/null +++ b/js/src/components/paid-ads/continue-button.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AppButton from '.~/components/app-button'; + +/** + * Renders Continue button on paid ad campaign create and edit page. + * + * @param {Object} props Props + * @param {Object} props.formProps Form props forwarded from `Form` component. + * @param {Function} props.onClick Function to handle the continue button click. + * @return {JSX.Element} The component. + */ +const ContinueButton = ( { formProps, onClick } ) => { + return ( + + ); +}; + +export default ContinueButton; diff --git a/js/src/components/title-button-layout/index.js b/js/src/components/title-button-layout/index.js deleted file mode 100644 index 8c71608853..0000000000 --- a/js/src/components/title-button-layout/index.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Internal dependencies - */ -import Subsection from '.~/wcdl/subsection'; -import ContentButtonLayout from '../content-button-layout'; -import './index.scss'; - -const TitleButtonLayout = ( props ) => { - const { title, button } = props; - - return ( - - { title } - { button } - - ); -}; - -export default TitleButtonLayout; diff --git a/js/src/components/title-button-layout/index.scss b/js/src/components/title-button-layout/index.scss deleted file mode 100644 index 88dfb1284d..0000000000 --- a/js/src/components/title-button-layout/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -.gla-title-button-layout { - .title { - margin-bottom: 0; - } -} diff --git a/js/src/pages/create-paid-ads-campaign/index.js b/js/src/pages/create-paid-ads-campaign/index.js index e076243e6a..c93a1cbf68 100644 --- a/js/src/pages/create-paid-ads-campaign/index.js +++ b/js/src/pages/create-paid-ads-campaign/index.js @@ -17,6 +17,7 @@ import { useAppDispatch } from '.~/data'; import { getDashboardUrl } from '.~/utils/urls'; import convertToAssetGroupUpdateBody from '.~/components/paid-ads/convertToAssetGroupUpdateBody'; import TopBar from '.~/components/stepper/top-bar'; +import ContinueButton from '.~/components/paid-ads/continue-button'; import HelpIconButton from '.~/components/help-icon-button'; import CampaignAssetsForm from '.~/components/paid-ads/campaign-assets-form'; import AdsCampaign from '.~/components/paid-ads/ads-campaign'; @@ -146,9 +147,16 @@ const CreatePaidAdsCampaign = () => { content: ( - handleContinueClick( STEP.ASSET_GROUP ) - } + continueButton={ ( props ) => ( + + handleContinueClick( + STEP.ASSET_GROUP + ) + } + /> + ) } /> ), onClick: handleStepperClick, diff --git a/js/src/pages/edit-paid-ads-campaign/index.js b/js/src/pages/edit-paid-ads-campaign/index.js index 1d23f45666..952c7f875b 100644 --- a/js/src/pages/edit-paid-ads-campaign/index.js +++ b/js/src/pages/edit-paid-ads-campaign/index.js @@ -19,6 +19,7 @@ import TopBar from '.~/components/stepper/top-bar'; import HelpIconButton from '.~/components/help-icon-button'; import CampaignAssetsForm from '.~/components/paid-ads/campaign-assets-form'; import AdsCampaign from '.~/components/paid-ads/ads-campaign'; +import ContinueButton from '.~/components/paid-ads/continue-button'; import AppSpinner from '.~/components/app-spinner'; import AssetGroup, { ACTION_SUBMIT_CAMPAIGN_AND_ASSETS, @@ -197,9 +198,16 @@ const EditPaidAdsCampaign = () => { - handleContinueClick( STEP.ASSET_GROUP ) - } + continueButton={ ( props ) => ( + + handleContinueClick( + STEP.ASSET_GROUP + ) + } + /> + ) } /> ), onClick: handleStepperClick, diff --git a/js/src/setup-ads/ads-stepper/index.js b/js/src/setup-ads/ads-stepper/index.js index 3979e0dcba..017ae3193e 100644 --- a/js/src/setup-ads/ads-stepper/index.js +++ b/js/src/setup-ads/ads-stepper/index.js @@ -9,8 +9,8 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import SetupAccounts from './setup-accounts'; +import AppButton from '.~/components/app-button'; import AdsCampaign from '.~/components/paid-ads/ads-campaign'; -import SetupBilling from './setup-billing'; import useEventPropertiesFilter from '.~/hooks/useEventPropertiesFilter'; import { recordStepperChangeEvent, @@ -22,12 +22,11 @@ import { /** * @param {Object} props React props * @param {Object} props.formProps Form props forwarded from `Form` component. - * @fires gla_setup_ads with `{ triggered_by: 'step1-continue-button' | 'step2-continue-button' , action: 'go-to-step2' | 'go-to-step3' }`. - * @fires gla_setup_ads with `{ triggered_by: 'stepper-step1-button' | 'stepper-step2-button', action: 'go-to-step1' | 'go-to-step2' }`. + * @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 = ( { formProps } ) => { const [ step, setStep ] = useState( '1' ); - useEventPropertiesFilter( FILTER_ONBOARDING, { context: CONTEXT_ADS_ONBOARDING, step, @@ -58,9 +57,9 @@ const AdsStepper = ( { formProps } ) => { continueStep( '2' ); }; - const handleCreateCampaignContinue = () => { - continueStep( '3' ); - }; + // @todo: Add check for billing status once billing setup is moved to step 2. + // For now, only disable based on the form being valid for testing purposes. + const isDisabledLaunch = ! formProps.isValidForm; return ( // This Stepper with this class name @@ -92,17 +91,22 @@ const AdsStepper = ( { formProps } ) => { content: ( + } /> ), onClick: handleStepClick, }, - { - key: '3', - label: __( 'Set up billing', 'google-listings-and-ads' ), - content: , - onClick: handleStepClick, - }, ] } /> ); diff --git a/js/src/setup-ads/ads-stepper/index.test.js b/js/src/setup-ads/ads-stepper/index.test.js index a286bf47b5..ebfad27dfc 100644 --- a/js/src/setup-ads/ads-stepper/index.test.js +++ b/js/src/setup-ads/ads-stepper/index.test.js @@ -8,7 +8,6 @@ jest.mock( './setup-accounts', () => jest.fn().mockName( 'SetupAccounts' ) ); jest.mock( '.~/components/paid-ads/ads-campaign', () => jest.fn().mockName( 'AdsCampaign' ) ); -jest.mock( './setup-billing', () => jest.fn().mockName( 'SetupBilling' ) ); /** * External dependencies @@ -23,7 +22,6 @@ import { recordEvent } from '@woocommerce/tracks'; import AdsStepper from './'; import SetupAccounts from './setup-accounts'; import AdsCampaign from '.~/components/paid-ads/ads-campaign'; -import SetupBilling from './setup-billing'; describe( 'AdsStepper', () => { let continueToStep2; @@ -39,8 +37,6 @@ describe( 'AdsStepper', () => { continueToStep3 = onContinue; return null; } ); - - SetupBilling.mockReturnValue( null ); } ); afterEach( () => { diff --git a/js/src/setup-ads/ads-stepper/setup-billing/billing-saved-card/index.js b/js/src/setup-ads/ads-stepper/setup-billing/billing-saved-card/index.js deleted file mode 100644 index 89f2bf01f2..0000000000 --- a/js/src/setup-ads/ads-stepper/setup-billing/billing-saved-card/index.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import GridiconCreditCard from 'gridicons/dist/credit-card'; -import { createInterpolateElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import toAccountText from '.~/utils/toAccountText'; -import AppSpinner from '.~/components/app-spinner'; -import TitleButtonLayout from '.~/components/title-button-layout'; -import TrackableLink from '.~/components/trackable-link'; -import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; -import Section from '.~/wcdl/section'; -import './index.scss'; - -/** - * Clicking on a Google Ads account text link. - * - * @event gla_google_ads_account_link_click - * @property {string} context Indicates which page / module the link is in - * @property {string} href Where the user is redirected - * @property {string} link_id A unique ID for the link within the page / module - */ - -/** - * @fires gla_google_ads_account_link_click with `{ context: 'setup-ads', link_id: 'google-ads-account' }` - */ -const BillingSavedCard = () => { - const { googleAdsAccount } = useGoogleAdsAccount(); - - if ( ! googleAdsAccount ) { - return ; - } - - return ( -
- - -
- -
-
- -
- { createInterpolateElement( - __( - 'Great! You already have billing information saved for this Google Ads account.', - 'google-listings-and-ads' - ), - { - link: ( - - ), - } - ) } -
-
-
-
-
- ); -}; - -export default BillingSavedCard; diff --git a/js/src/setup-ads/ads-stepper/setup-billing/billing-saved-card/index.scss b/js/src/setup-ads/ads-stepper/setup-billing/billing-saved-card/index.scss deleted file mode 100644 index 642ce39455..0000000000 --- a/js/src/setup-ads/ads-stepper/setup-billing/billing-saved-card/index.scss +++ /dev/null @@ -1,17 +0,0 @@ -.gla-google-ads-billing-saved-card { - &__account-number { - margin-bottom: calc(var(--main-gap) / 2); - } - - &__description { - display: flex; - gap: calc(var(--main-gap) / 3); - align-items: center; - font-style: italic; - - svg { - fill: $alert-green; - flex: 0 0 auto; - } - } -} diff --git a/js/src/setup-ads/ads-stepper/setup-billing/index.js b/js/src/setup-ads/ads-stepper/setup-billing/index.js deleted file mode 100644 index 6e610944d4..0000000000 --- a/js/src/setup-ads/ads-stepper/setup-billing/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import StepContent from '.~/components/stepper/step-content'; -import StepContentHeader from '.~/components/stepper/step-content-header'; -import AppSpinner from '.~/components/app-spinner'; -import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; -import Section from '.~/wcdl/section'; -import { - BillingSetupCard, - fallbackBillingUrl, -} from '.~/components/paid-ads/billing-card'; -import BillingSavedCard from './billing-saved-card'; -import StepContentActions from '.~/components/stepper/step-content-actions'; -import StepContentFooter from '.~/components/stepper/step-content-footer'; -import AppButton from '.~/components/app-button'; -import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; - -const SetupBilling = ( props ) => { - const { - formProps: { isSubmitting, handleSubmit }, - } = props; - - const { billingStatus } = useGoogleAdsAccountBillingStatus(); - - if ( ! billingStatus ) { - return ; - } - - const isApproved = - billingStatus.status === GOOGLE_ADS_BILLING_STATUS.APPROVED; - - return ( - - -
- { isApproved ? ( - - ) : ( - - ) } -
- { isApproved && ( - - - - { __( - 'Launch paid campaign', - 'google-listings-and-ads' - ) } - - - - ) } -
- ); -}; - -export default SetupBilling; 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..aa8218b58e 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 @@ -16,7 +16,6 @@ import { getFAQPanelTitle, getFAQPanelRow, checkFAQExpandable, - checkBillingAdsPopup, } from '../../utils/page'; const ADS_ACCOUNTS = [ @@ -67,6 +66,7 @@ test.describe( 'Set up Ads account', () => { page = await browser.newPage(); dashboardPage = new DashboardPage( page ); setupAdsAccounts = new SetupAdsAccountsPage( page ); + setupBudgetPage = new SetupBudgetPage( page ); await setOnboardedMerchant(); await setupAdsAccounts.mockAdsAccountsResponse( [] ); await dashboardPage.mockRequests(); @@ -281,10 +281,12 @@ test.describe( 'Set up Ads account', () => { test.describe( 'Create your paid campaign', () => { test.beforeAll( async () => { - setupBudgetPage = new SetupBudgetPage( page ); + await setupBudgetPage.fulfillBillingStatusRequest( { + status: 'approved', + } ); } ); - test( 'Continue to create paid campaign', async () => { + test( 'Continue to create paid ad campaign', async () => { await setupAdsAccounts.clickContinue(); await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); @@ -303,7 +305,7 @@ test.describe( 'Set up Ads account', () => { ).toBeVisible(); await expect( - page.getByRole( 'button', { name: 'Continue' } ) + setupBudgetPage.getLaunchPaidCampaignButton() ).toBeDisabled(); await expect( @@ -371,14 +373,14 @@ test.describe( 'Set up Ads account', () => { await setupBudgetPage.fillBudget( budget ); await expect( - page.getByRole( 'button', { name: 'Continue' } ) + setupBudgetPage.getLaunchPaidCampaignButton() ).toBeDisabled(); budget = '1'; await setupBudgetPage.fillBudget( budget ); await expect( - page.getByRole( 'button', { name: 'Continue' } ) + setupBudgetPage.getLaunchPaidCampaignButton() ).toBeEnabled(); } ); @@ -387,152 +389,51 @@ test.describe( 'Set up Ads account', () => { page.getByText( 'set a daily budget of 15 USD' ) ).toBeVisible(); } ); - } ); - 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( - page.getByRole( 'button', { - name: 'Set up billing', - exact: true, - } ) - ).toBeEnabled(); - - 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.' - ) - ).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 () => { - await setupBudgetPage.fulfillBillingStatusRequest( { - status: 'approved', - } ); - - await setupAdsAccounts.mockAdsAccountsResponse( { - id: ADS_ACCOUNTS[ 1 ], - billing_url: null, - } ); - - // 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, + test( 'It should show the campaign creation success message', async () => { + // Mock the campaign creation request. + const campaignCreation = + setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( + '1', [ '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, - } - ); - } ); - - 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 setupBudgetPage.getLaunchPaidCampaignButton().click(); - 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( 'Launch paid campaign should be enabled', async () => { - //Click Add paid Campaign - await adsConnectionButton.click(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - - //Step 1 - Accounts are already set up. - await setupAdsAccounts.clickContinue(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + await campaignCreation; - //Step 2 - Fill the budget - await setupBudgetPage.fillBudget( '1' ); - await page.getByRole( 'button', { name: 'Continue' } ).click(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + //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, + } + ); - //Step 3 - Billing is already setup await expect( - page.getByText( - 'Great! You already have billing information saved for this' - ) + page.getByRole( 'heading', { + name: "You've set up a paid Performance Max Campaign!", + } ) ).toBeVisible(); await expect( - page.getByRole( 'button', { name: 'Launch paid campaign' } ) + page.getByRole( 'button', { + name: 'Create another campaign', + } ) + ).toBeEnabled(); + + await expect( + page.getByRole( 'button', { + name: 'Got It', + } ) ).toBeEnabled(); - const campaignCreation = - setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( - '1', - [ 'US' ] - ); await page - .getByRole( 'button', { name: 'Launch paid campaign' } ) + .getByRole( 'button', { + name: 'Got It', + } ) .click(); - await campaignCreation; } ); } ); } ); diff --git a/tests/e2e/utils/pages/setup-ads/setup-budget.js b/tests/e2e/utils/pages/setup-ads/setup-budget.js index a94232393c..bf9bab0b71 100644 --- a/tests/e2e/utils/pages/setup-ads/setup-budget.js +++ b/tests/e2e/utils/pages/setup-ads/setup-budget.js @@ -75,6 +75,18 @@ export default class SetupBudget extends MockRequests { ); } + /** + * Get the Launch paid campaign button. + * + * @return {import('@playwright/test').Locator} Launch paid campaign button. + */ + getLaunchPaidCampaignButton() { + return this.page.getByRole( 'button', { + name: 'Launch paid campaign', + exact: true, + } ); + } + /** * Fill the budget. *