diff --git a/js/src/components/account-card/index.js b/js/src/components/account-card/index.js index 7b2433366a..3330f33249 100644 --- a/js/src/components/account-card/index.js +++ b/js/src/components/account-card/index.js @@ -121,7 +121,7 @@ const appearanceDict = { // The `center` is the default alignment, and no need to append any additional class name. const alignStyleName = { center: false, - top: `gla-account-card__styled--align-top`, + top: 'gla-account-card__styled--align-top', }; const indicatorAlignStyleName = { diff --git a/js/src/components/google-combo-account-card/connect-ads/confirm-create-modal.js b/js/src/components/google-combo-account-card/connect-ads/confirm-create-modal.js new file mode 100644 index 0000000000..c03656f0a2 --- /dev/null +++ b/js/src/components/google-combo-account-card/connect-ads/confirm-create-modal.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AppModal from '.~/components/app-modal'; +import AppButton from '.~/components/app-button'; +import WarningIcon from '.~/components/warning-icon'; +import './confirm-create-modal.scss'; + +/** + * Google Ads account creation confirmation modal. + * This modal is shown when the user tries to create a new Google Ads account. + * + * @param {Object} props Component props. + * @param {Function} props.onContinue Callback to continue with account creation. + * @param {Function} props.onRequestClose Callback to close the modal. + * @return {JSX.Element} Confirmation modal. + */ +const ConfirmCreateModal = ( { onContinue, onRequestClose } ) => { + return ( + + { __( + 'Yes, I want a new account', + 'google-listings-and-ads' + ) } + , + + { __( 'Cancel', 'google-listings-and-ads' ) } + , + ] } + onRequestClose={ onRequestClose } + > +

+ + + { __( + 'Are you sure you want to create a new Google Ads account?', + 'google-listings-and-ads' + ) } + +

+

+ { __( + 'You already have another Ads account associated with this Google account.', + 'google-listings-and-ads' + ) } +

+

+ { __( + 'If you create a new Google Ads account, you will need to accept an invite to the account before it can be used.', + 'google-listings-and-ads' + ) } +

+
+ ); +}; + +export default ConfirmCreateModal; diff --git a/js/src/components/google-combo-account-card/connect-ads/confirm-create-modal.scss b/js/src/components/google-combo-account-card/connect-ads/confirm-create-modal.scss new file mode 100644 index 0000000000..fc15d2235d --- /dev/null +++ b/js/src/components/google-combo-account-card/connect-ads/confirm-create-modal.scss @@ -0,0 +1,8 @@ +.gla-ads-warning-modal { + + .gla-ads-warning-modal__warning-text { + display: flex; + align-items: center; + gap: calc(var(--main-gap) / 3); + } +} diff --git a/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js b/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js index 9405e43857..6c2cd912c7 100644 --- a/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js +++ b/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js @@ -14,17 +14,21 @@ import DisconnectAccount from '.~/components/google-ads-account-card/disconnect- * * @param {Object} props Props. * @param {boolean} props.isConnected Whether the account is connected. + * @param {Function} props.onCreateNewClick Callback when clicking on the button to create a new account. * @param {Object} props.restProps Rest props. Passed to AppButton. * @return {JSX.Element} Footer component. */ -const ConnectAdsFooter = ( { isConnected, ...restProps } ) => { - // If the account is connected, show the disconnect button. +const ConnectAdsFooter = ( { + isConnected, + onCreateNewClick, + ...restProps +} ) => { if ( isConnected ) { return ; } return ( - + { __( 'Or, create a new Google Ads account', 'google-listings-and-ads' diff --git a/js/src/components/google-combo-account-card/connect-ads/connect-ads.js b/js/src/components/google-combo-account-card/connect-ads/connect-ads.js index ec672fbc5b..5d61561cd1 100644 --- a/js/src/components/google-combo-account-card/connect-ads/connect-ads.js +++ b/js/src/components/google-combo-account-card/connect-ads/connect-ads.js @@ -1,122 +1,54 @@ /** * External dependencies */ -import { useEffect, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import useApiFetchCallback from '.~/hooks/useApiFetchCallback'; -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; -import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; -import { useAppDispatch } from '.~/data'; -import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady'; import AccountCard from '.~/components/account-card'; -import AdsAccountSelectControl from '.~/components/ads-account-select-control'; -import ConnectedIconLabel from '.~/components/connected-icon-label'; -import ConnectAdsFooter from './connect-ads-footer'; -import LoadingLabel from '.~/components/loading-label'; -import ConnectButton from '.~/components/google-ads-account-card/connect-ads/connect-button'; +import ConfirmCreateModal from './confirm-create-modal'; +import ConnectExistingAccount from './connect-existing-account'; +import UpsertingAccount from './upserting-account'; /** * ConnectAds component renders an account card to connect to an existing Google Ads account. * + * @param {Object} props Component props. + * @param {Function} props.onRequestCreate A callback to fire when creating a new account. + * @param {string|null} props.upsertingAction The action the user is performing. Possible values are 'create', 'update', or null. * @return {JSX.Element} {@link AccountCard} filled with content. */ -const ConnectAds = () => { - const [ value, setValue ] = useState(); - const [ isLoading, setLoading ] = useState( false ); - const { refetchGoogleAdsAccount } = useGoogleAdsAccount(); - const { createNotice } = useDispatchCoreNotices(); - const { fetchGoogleAdsAccountStatus } = useAppDispatch(); - const isConnected = useGoogleAdsAccountReady(); - const { googleAdsAccount, hasFinishedResolution } = useGoogleAdsAccount(); - const [ connectGoogleAdsAccount ] = useApiFetchCallback( { - path: '/wc/gla/ads/accounts', - method: 'POST', - data: { id: value }, - } ); +const ConnectAds = ( { onRequestCreate, upsertingAction } ) => { + const [ showCreateNewModal, setShowCreateNewModal ] = useState( false ); - useEffect( () => { - if ( isConnected ) { - setValue( googleAdsAccount.id ); - } - }, [ googleAdsAccount, isConnected ] ); + if ( upsertingAction ) { + return ; + } - const handleConnectClick = async () => { - if ( ! value ) { - return; - } - - setLoading( true ); - try { - await connectGoogleAdsAccount(); - await fetchGoogleAdsAccountStatus(); - await refetchGoogleAdsAccount(); - setLoading( false ); - } catch ( error ) { - setLoading( false ); - createNotice( - 'error', - __( - 'Unable to connect your Google Ads account. Please try again later.', - 'google-listings-and-ads' - ) - ); - } + const handleCreateClick = () => { + setShowCreateNewModal( true ); }; - const getIndicator = () => { - if ( ! hasFinishedResolution ) { - return ; - } - - if ( isLoading ) { - return ( - - ); - } - - if ( isConnected ) { - return ; - } + const handleRequestClose = () => { + setShowCreateNewModal( false ); + }; - return ( - - ); + const handleContinue = () => { + onRequestCreate(); + handleRequestClose(); }; return ( - + + { showCreateNewModal && ( + - } - actions={ - - } - /> + ) } + ); }; diff --git a/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js b/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js new file mode 100644 index 0000000000..a42679f6ff --- /dev/null +++ b/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AccountCard from '.~/components/account-card'; +import ConnectAdsFooter from './connect-ads-footer'; +import LoadingLabel from '.~/components/loading-label'; +import useApiFetchCallback from '.~/hooks/useApiFetchCallback'; +import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; +import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; +import { useAppDispatch } from '.~/data'; +import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady'; +import AdsAccountSelectControl from '.~/components/ads-account-select-control'; +import ConnectedIconLabel from '.~/components/connected-icon-label'; +import ConnectButton from '.~/components/google-ads-account-card/connect-ads/connect-button'; + +/** + * Renders an account card to connect to an existing Google Ads account. + * + * @param {Object} props Component props. + * @param {Function} props.onCreateClick Callback when clicking on the button to create a new account + */ +const ConnectExistingAccount = ( { onCreateClick } ) => { + const [ value, setValue ] = useState(); + const [ isLoading, setLoading ] = useState( false ); + const { createNotice } = useDispatchCoreNotices(); + const { fetchGoogleAdsAccountStatus } = useAppDispatch(); + const isConnected = useGoogleAdsAccountReady(); + const { googleAdsAccount, hasFinishedResolution, refetchGoogleAdsAccount } = + useGoogleAdsAccount(); + const [ connectGoogleAdsAccount ] = useApiFetchCallback( { + path: '/wc/gla/ads/accounts', + method: 'POST', + data: { id: value }, + } ); + + useEffect( () => { + if ( isConnected ) { + setValue( googleAdsAccount.id ); + } + }, [ googleAdsAccount, isConnected ] ); + + const handleConnectClick = async () => { + if ( ! value ) { + return; + } + + setLoading( true ); + try { + await connectGoogleAdsAccount(); + await fetchGoogleAdsAccountStatus(); + await refetchGoogleAdsAccount(); + } catch ( error ) { + createNotice( + 'error', + __( + 'Unable to connect your Google Ads account. Please try again later.', + 'google-listings-and-ads' + ) + ); + } finally { + setLoading( false ); + } + }; + + const getIndicator = () => { + if ( ! hasFinishedResolution ) { + return ; + } + + if ( isLoading ) { + return ( + + ); + } + + if ( isConnected ) { + return ; + } + + return ( + + ); + }; + + return ( + + } + actions={ + + } + /> + ); +}; + +export default ConnectExistingAccount; diff --git a/js/src/components/google-combo-account-card/connect-ads/upserting-account.js b/js/src/components/google-combo-account-card/connect-ads/upserting-account.js new file mode 100644 index 0000000000..61e22ca118 --- /dev/null +++ b/js/src/components/google-combo-account-card/connect-ads/upserting-account.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AccountCard from '.~/components/account-card'; +import LoadingLabel from '.~/components/loading-label'; + +/** + * Renders indication that the user is in the process of creating or connecting a Google Ads account. + * + * @param {Object} props Component props. + * @param {string} props.upsertingAction The action the user is performing. + */ +const UpsertingAccount = ( { upsertingAction } ) => { + const isConnecting = upsertingAction === 'update'; + + let title = __( + 'Creating a new Google Ads account', + 'google-listings-and-ads' + ); + let indicatorLabel = __( 'Creating…', 'google-listings-and-ads' ); + + if ( isConnecting ) { + title = __( + 'Connecting your Google Ads account', + 'google-listings-and-ads' + ); + indicatorLabel = __( 'Connecting…', 'google-listings-and-ads' ); + } + + return ( + } + /> + ); +}; + +export default UpsertingAccount; diff --git a/js/src/components/google-combo-account-card/connected-google-combo-account-card.js b/js/src/components/google-combo-account-card/connected-google-combo-account-card.js index d92e9dc06c..e43b153f16 100644 --- a/js/src/components/google-combo-account-card/connected-google-combo-account-card.js +++ b/js/src/components/google-combo-account-card/connected-google-combo-account-card.js @@ -48,7 +48,7 @@ const ConnectedGoogleComboAccountCard = () => { const { invalidateResolution } = useAppDispatch(); const { googleAdsAccount } = useGoogleAdsAccount(); const { hasAccess, step } = useGoogleAdsAccountStatus(); - const [ upsertAdsAccount, { loading } ] = useUpsertAdsAccount(); + const [ upsertAdsAccount, { action, loading } ] = useUpsertAdsAccount(); const finalizeAdsAccountCreation = hasAccess === true && step === 'conversion_action'; @@ -83,8 +83,9 @@ const ConnectedGoogleComboAccountCard = () => { const hasExistingGoogleAdsAccounts = existingGoogleAdsAccounts?.length > 0; const showConnectAds = - ( editMode && hasExistingGoogleAdsAccounts ) || - ( ! isConnected && hasExistingGoogleAdsAccounts ); + ( ( editMode && hasExistingGoogleAdsAccounts ) || + ( ! isConnected && hasExistingGoogleAdsAccounts ) ) && + ! shouldClaimGoogleAdsAccount; // Show the spinner if there's an account creation in progress and account should not be claimed. // If we are not showing the ConnectMC screen, for e.g when we are creating the first account, @@ -116,7 +117,12 @@ const ConnectedGoogleComboAccountCard = () => { expandedDetail /> - { showConnectAds && } + { showConnectAds && ( + + ) } { showConnectMC && ( { await setUpAccountsPage.mockJetpackConnected(); await setUpAccountsPage.mockGoogleConnected(); - await setUpAccountsPage.fulfillAdsAccounts( [ { id: 1 } ] ); - await setUpAccountsPage.mockMCHasAccounts(); - await setUpAccountsPage.mockAdsAccountIncomplete(); - await setUpAccountsPage.mockMCConnected(); - const once = setUpAccountsPage.fulfillTimes( 1 ); await once.mockAdsHasNoAccounts(); @@ -591,24 +586,108 @@ test.describe( 'Set up accounts', () => { ).toBeEnabled(); } ); - test( 'should display the correct Google Ads ID when connected', async () => { + test( 'should send an API request to connect existing Google Ads account', async () => { + const adsAccountsResponse = + setUpAccountsPage.registerAdsAccountsResponse(); + + const googleAdsAccountCard = + setUpAccountsPage.getGoogleAdsAccountCard(); + await setUpAccountsPage.mockAdsStatusClaimed(); + + const adsAccountDropdown = + googleAdsAccountCard.locator( 'select' ); + await adsAccountDropdown.selectOption( '222222' ); + await googleAdsAccountCard + .getByRole( 'button', { name: 'Connect' } ) + .click(); + + await setUpAccountsPage.mockAdsAccountConnected( 222222 ); + await adsAccountsResponse; + const googleAccountCard = setUpAccountsPage.getGoogleAccountCard(); + await expect( + googleAccountCard.getByText( 'Google Ads ID: 222222' ) + ).toBeVisible(); + } ); + } ); + + test.describe( 'When new Google Ads account is created', () => { + test.beforeAll( async () => { + await setUpAccountsPage.mockAdsAccountDisconnected(); + + await setUpAccountsPage.goto(); + } ); + + test( 'should see the Create new Google Ads account link', async () => { const googleAdsAccountCard = setUpAccountsPage.getGoogleAdsAccountCard(); - const once = setUpAccountsPage.fulfillTimes( 1 ); - await once.fulfillAdsAccounts( [ { id: 12345 } ] ); - await once.mockAdsAccountConnected(); - await once.mockAdsStatusClaimed(); + await expect( + googleAdsAccountCard.getByText( + 'Or, create a new Google Ads account', + { exact: true } + ) + ).toBeVisible(); + } ); + + test( 'clicking the "Create new Google Ads account" link should open the modal', async () => { + const googleAdsAccountCard = + setUpAccountsPage.getGoogleAdsAccountCard(); await googleAdsAccountCard - .getByRole( 'button', { name: 'Connect' } ) + .getByText( 'Or, create a new Google Ads account' ) .click(); - await expect( googleAccountCard ).toContainText( - 'Google Ads ID: 12345' + await expect( setUpAccountsPage.getModal() ).toBeVisible(); + await expect( setUpAccountsPage.getModalHeader() ).toHaveText( + 'Create Google Ads Account' ); + + // "Yes, I want a new account" button should be disabled and secondary. + const yesButton = setUpAccountsPage.getModalSecondaryButton(); + const cancelButton = setUpAccountsPage.getModalPrimaryButton(); + await expect( yesButton ).toHaveText( + 'Yes, I want a new account' + ); + + await expect( cancelButton ).toHaveText( 'Cancel' ); + + // Click the cancel button to close the modal. + await cancelButton.click(); + await expect( setUpAccountsPage.getModal() ).not.toBeVisible(); + } ); + + test( 'clicking the "Yes, I want a new account" button should create a new Google Ads account', async () => { + const googleAccountCard = + setUpAccountsPage.getGoogleAccountCard(); + const googleAdsAccountCard = + setUpAccountsPage.getGoogleAdsAccountCard(); + + await setUpAccountsPage.fulfillAdsAccounts( [ + { + id: 111111, + }, + ] ); + + await setUpAccountsPage.mockAdsStatusNotClaimed(); + await setUpAccountsPage.mockAdsAccountIncomplete(); + + await googleAdsAccountCard + .getByText( 'Or, create a new Google Ads account' ) + .click(); + + await expect( setUpAccountsPage.getModal() ).toBeVisible(); + + const yesButton = setUpAccountsPage.getModalSecondaryButton(); + await yesButton.click(); + + await expect( setUpAccountsPage.getModal() ).not.toBeVisible(); + + // Google Ads ID should be displayed. + await expect( + googleAccountCard.getByText( 'Google Ads ID: 12345' ) + ).toBeVisible(); } ); } ); } ); diff --git a/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js b/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js index 5fc10005e1..dba65c7e04 100644 --- a/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js +++ b/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js @@ -380,4 +380,17 @@ export default class SetUpAccountsPage extends MockRequests { getTermsCheckbox() { return this.page.getByLabel( /I accept the terms and conditions/ ); } + + /** + * Register the response when connecting an Ads account + * + * @return {Promise} The response. + */ + registerAdsAccountsResponse() { + return this.page.waitForResponse( + ( response ) => + response.url().includes( '/gla/ads/accounts' ) && + response.status() === 200 + ); + } }