diff --git a/js/src/components/google-combo-account-card/account-details.js b/js/src/components/google-combo-account-card/account-details.js index 6f3618b836..969a76ea03 100644 --- a/js/src/components/google-combo-account-card/account-details.js +++ b/js/src/components/google-combo-account-card/account-details.js @@ -17,13 +17,13 @@ import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount'; const AccountDetails = () => { const { google } = useGoogleAccount(); const { googleAdsAccount } = useGoogleAdsAccount(); - const { googleMCAccount } = useGoogleMCAccount(); + const { googleMCAccount, isReady: isGoogleMCReady } = useGoogleMCAccount(); return ( <>

{ google.email }

- { googleMCAccount?.id > 0 && + { isGoogleMCReady && sprintf( // Translators: %s is the Merchant Center ID __( 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 5c538c9751..ec672fbc5b 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 @@ -11,7 +11,6 @@ import useApiFetchCallback from '.~/hooks/useApiFetchCallback'; import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import { useAppDispatch } from '.~/data'; -import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts'; import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady'; import AccountCard from '.~/components/account-card'; import AdsAccountSelectControl from '.~/components/ads-account-select-control'; @@ -32,14 +31,7 @@ const ConnectAds = () => { const { createNotice } = useDispatchCoreNotices(); const { fetchGoogleAdsAccountStatus } = useAppDispatch(); const isConnected = useGoogleAdsAccountReady(); - const { - existingAccounts: accounts, - hasFinishedResolution: hasFinishedResolutionForExistingAdsAccount, - } = useExistingGoogleAdsAccounts(); - const { - googleAdsAccount, - hasFinishedResolution: hasFinishedResolutionForCurrentAccount, - } = useGoogleAdsAccount(); + const { googleAdsAccount, hasFinishedResolution } = useGoogleAdsAccount(); const [ connectGoogleAdsAccount ] = useApiFetchCallback( { path: '/wc/gla/ads/accounts', method: 'POST', @@ -75,16 +67,11 @@ const ConnectAds = () => { } }; - // If the accounts are still being fetched, we don't want to show the card. - if ( - ! hasFinishedResolutionForExistingAdsAccount || - ! hasFinishedResolutionForCurrentAccount || - ! accounts?.length - ) { - return null; - } - const getIndicator = () => { + if ( ! hasFinishedResolution ) { + return ; + } + if ( isLoading ) { return ( { appearance={ APPEARANCE.GOOGLE } disabled={ disabled } alignIcon="top" + className="gla-google-combo-service-account-card--google" description={ <>

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 1eaabea0d7..9f8a9651e8 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 @@ -15,6 +15,10 @@ import Indicator from './indicator'; import getAccountCreationTexts from './getAccountCreationTexts'; import SpinnerCard from '.~/components/spinner-card'; import useAutoCreateAdsMCAccounts from '.~/hooks/useAutoCreateAdsMCAccounts'; +import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount'; +import useExistingGoogleMCAccounts from '.~/hooks/useExistingGoogleMCAccounts'; +import useCreateMCAccount from '.~/hooks/useCreateMCAccount'; +import ConnectMC from '.~/components/google-mc-account-card/connect-mc'; import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady'; import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts'; import useGoogleAdsAccountStatus from '.~/hooks/useGoogleAdsAccountStatus'; @@ -32,7 +36,16 @@ const ConnectedGoogleComboAccountCard = () => { setShowConversionMeasurementNotice, ] = useState( false ); const initConnected = useRef( null ); - const { hasDetermined, creatingWhich } = useAutoCreateAdsMCAccounts(); + + // We use a single instance of the hook to create a MC (Merchant Center) account, + // ensuring consistent results across both the main component (ConnectedGoogleComboAccountCard) and its child component (ConnectMC). + // This approach is especially useful when an MC account is automatically created, and the URL needs to be reclaimed. + // The URL reclaim component is rendered within the ConnectMC component. + const [ createMCAccount, resultCreateMCAccount ] = useCreateMCAccount(); + const { data: existingGoogleMCAccounts } = useExistingGoogleMCAccounts(); + const { isReady: isGoogleMCReady } = useGoogleMCAccount(); + const { hasDetermined, creatingWhich } = + useAutoCreateAdsMCAccounts( createMCAccount ); const { text, subText } = getAccountCreationTexts( creatingWhich ); const { existingAccounts: existingGoogleAdsAccounts } = useExistingGoogleAdsAccounts(); @@ -83,6 +96,12 @@ const ConnectedGoogleComboAccountCard = () => { const shouldClaimGoogleAdsAccount = Boolean( googleAdsAccount?.id && hasAccess === false ); + + const hasExistingGoogleMCAccounts = existingGoogleMCAccounts?.length > 0; + const showConnectMC = + ( editMode && hasExistingGoogleMCAccounts ) || + ( ! isGoogleMCReady && hasExistingGoogleMCAccounts ); + const hasExistingGoogleAdsAccounts = existingGoogleAdsAccounts?.length > 0; const showConnectAds = ( editMode && hasExistingGoogleAdsAccounts ) || @@ -100,7 +119,7 @@ const ConnectedGoogleComboAccountCard = () => { } helper={ subText } indicator={ } @@ -114,6 +133,14 @@ const ConnectedGoogleComboAccountCard = () => { { showConnectAds && } + + { showConnectMC && ( + + ) } ); }; diff --git a/js/src/components/google-mc-account-card/connect-mc/actions.js b/js/src/components/google-mc-account-card/connect-mc/actions.js new file mode 100644 index 0000000000..30517e839c --- /dev/null +++ b/js/src/components/google-mc-account-card/connect-mc/actions.js @@ -0,0 +1,58 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import CreateAccountButton from '../create-account-button'; +import DisconnectAccountButton from '../disconnect-account-button'; + +/** + * Actions component. + * + * Conditionally renders either a button to disconnect the account if already + * connected, or a button to create a new Merchant Center account. + * + * @param {Object} props + * @param {boolean} props.isConnected Whether the Merchant Center account is connected. + * @param {Object} props.resultConnectMC The result of the connection request, used to handle loading state. + * @param {Object} props.resultCreateAccount The result of the create account request. + * @param {Function} props.onCreateAccount Callback function for creating a new Merchant Center account. + */ +const Actions = ( { + isConnected, + resultConnectMC, + resultCreateAccount, + onCreateAccount, +} ) => { + if ( isConnected ) { + const handleOnDisconnected = () => { + resultConnectMC.reset(); + resultCreateAccount.reset(); + }; + + return ( + + ); + } + + return ( + + { __( + 'Or, create a new Merchant Center account', + 'google-listings-and-ads' + ) } + + ); +}; + +export default Actions; diff --git a/js/src/components/google-mc-account-card/connect-mc/index.js b/js/src/components/google-mc-account-card/connect-mc/index.js index 658bc245f3..d8eb62d52e 100644 --- a/js/src/components/google-mc-account-card/connect-mc/index.js +++ b/js/src/components/google-mc-account-card/connect-mc/index.js @@ -1,26 +1,24 @@ /** * External dependencies */ -import { CardDivider } from '@wordpress/components'; -import { useState } from '@wordpress/element'; +import classnames from 'classnames'; +import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import MerchantCenterSelectControl from '.~/components/merchant-center-select-control'; +import MerchantCenterSelect from './merchant-center-select'; import AppButton from '.~/components/app-button'; -import Section from '.~/wcdl/section'; -import Subsection from '.~/wcdl/subsection'; -import ContentButtonLayout from '.~/components/content-button-layout'; +import useConnectMCAccount from '.~/hooks/useConnectMCAccount'; import SwitchUrlCard from '../switch-url-card'; import ReclaimUrlCard from '../reclaim-url-card'; -import AccountCard, { APPEARANCE } from '.~/components/account-card'; -import CreateAccountButton from '../create-account-button'; -import useConnectMCAccount from '../useConnectMCAccount'; -import useCreateMCAccount from '.~/hooks/useCreateMCAccount'; +import LoadingLabel from '.~/components/loading-label'; +import ConnectedIconLabel from '.~/components/connected-icon-label'; +import AccountCard from '.~/components/account-card'; +import Actions from './actions'; +import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount'; import CreatingCard from '../creating-card'; -import './index.scss'; /** * Clicking on the button to connect an existing Google Merchant Center account. @@ -37,101 +35,137 @@ import './index.scss'; */ /** + * ConnectMC component. + * + * This component renders Merchant Center connection card. + * It is using createAccount and resultCreateAccount from the parent component. * @fires gla_mc_account_connect_button_click + * @param {Object} props + * @param {Function} props.createAccount Callback function for creating a new Merchant Center account. + * @param {Object} props.resultCreateAccount The result of the create account request. + * @param {string} [props.className] Additional class name to be added to the card. */ -const ConnectMC = () => { +const ConnectMC = ( { createAccount, resultCreateAccount, className } ) => { const [ value, setValue ] = useState(); const [ handleConnectMC, resultConnectMC ] = useConnectMCAccount( value ); - const [ handleCreateAccount, resultCreateAccount ] = useCreateMCAccount(); + const { + googleMCAccount, + hasFinishedResolution, + isReady: isGoogleMCReady, + } = useGoogleMCAccount(); - if ( resultConnectMC.response?.status === 409 ) { - return ( - - ); - } + useEffect( () => { + if ( isGoogleMCReady ) { + setValue( googleMCAccount.id ); + } + }, [ googleMCAccount, isGoogleMCReady ] ); - if ( - resultConnectMC.response?.status === 403 || - resultCreateAccount.response?.status === 403 - ) { - return ( - { - resultConnectMC.reset(); - resultCreateAccount.reset(); - } } - /> - ); + if ( ! isGoogleMCReady ) { + if ( resultConnectMC.response?.status === 409 ) { + return ( + + ); + } + + if ( + resultConnectMC.response?.status === 403 || + resultCreateAccount.response?.status === 403 + ) { + return ( + { + resultConnectMC.reset(); + resultCreateAccount.reset(); + } } + /> + ); + } + + if ( + resultCreateAccount.loading || + resultCreateAccount.response?.status === 503 + ) { + return ( + + ); + } } - if ( - resultCreateAccount.loading || - resultCreateAccount.response?.status === 503 - ) { + const getIndicator = () => { + if ( ! hasFinishedResolution ) { + return ; + } + + if ( isGoogleMCReady ) { + return ; + } + + if ( resultConnectMC.loading ) { + return ( + + ); + } + return ( - + + { __( 'Connect', 'google-listings-and-ads' ) } + ); - } + }; return ( - - - - { __( - 'Connect to an existing account', - 'google-listings-and-ads' - ) } - - - - - { __( 'Connect', 'google-listings-and-ads' ) } - - - - - - { __( - 'Or, create a new Merchant Center account', - 'google-listings-and-ads' - ) } - - - + className={ classnames( 'gla-connect-mc-card', className ) } + title={ __( + 'Connect to existing Merchant Center account', + 'google-listings-and-ads' + ) } + helper={ __( + 'Required to sync products so they show on Google.', + 'google-listings-and-ads' + ) } + alignIndicator="toDetail" + indicator={ getIndicator() } + detail={ + + } + actions={ + + } + /> ); }; diff --git a/js/src/components/google-mc-account-card/connect-mc/index.scss b/js/src/components/google-mc-account-card/connect-mc/index.scss deleted file mode 100644 index 27b5c7cac2..0000000000 --- a/js/src/components/google-mc-account-card/connect-mc/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -.gla-connect-mc-card { - .app-select-control { - flex-grow: 1; - } -} diff --git a/js/src/components/google-mc-account-card/connect-mc/merchant-center-select.js b/js/src/components/google-mc-account-card/connect-mc/merchant-center-select.js new file mode 100644 index 0000000000..4c36a88b46 --- /dev/null +++ b/js/src/components/google-mc-account-card/connect-mc/merchant-center-select.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { getSetting } from '@woocommerce/settings'; // eslint-disable-line import/no-unresolved + +/** + * Internal dependencies + */ +import MerchantCenterSelectControl from '.~/components/merchant-center-select-control'; +import useExistingGoogleMCAccounts from '.~/hooks/useExistingGoogleMCAccounts'; +import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount'; +import AppSelectControl from '.~/components/app-select-control'; + +/** + * Renders the connected Merchant Center details, leveraging existing functionality + * and styles from AppSelectControl. + * + * Ideally, using MerchantCenterSelectControl only to display the list of MC accounts would be fine. However, during testing, + * we found that when a URL is reclaimed for Merchant Center (MC), the Google API does not + * return the newly reclaimed account immediately in the list provided by the useExistingGoogleMCAccounts hook, + * even though the data in the store is invalidated. In that case, thus we end up having an account ID + * which is not in the list of existing accounts. We then fake the connected select by manually providing + * the connected ID. + * + * @param {Object} props + * @param {boolean} props.isConnected Whether the Merchant Center account is connected. + */ +const MerchantCenterSelect = ( { isConnected, ...rest } ) => { + const { data: existingAccounts } = useExistingGoogleMCAccounts(); + const { googleMCAccount } = useGoogleMCAccount(); + + const accountIdExists = existingAccounts?.some( + ( existingAccount ) => existingAccount.id === googleMCAccount.id + ); + + // If the account ID is not in the list of existing accounts, fake the select options by displaying the connected account ID only. + if ( ! accountIdExists && isConnected ) { + const domain = new URL( getSetting( 'homeUrl' ) ).host; + + return ( + + ); + } + + return ( + + ); +}; + +export default MerchantCenterSelect; diff --git a/js/src/components/google-mc-account-card/create-account-card.js b/js/src/components/google-mc-account-card/create-account-card.js deleted file mode 100644 index b3c4fb5d70..0000000000 --- a/js/src/components/google-mc-account-card/create-account-card.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import CreateAccountButton from './create-account-button'; -import AccountCard, { APPEARANCE } from '.~/components/account-card'; - -const CreateAccountCard = ( props ) => { - const { onCreateAccount } = props; - - return ( - - { __( 'Create account', 'google-listings-and-ads' ) } - - } - /> - ); -}; - -export default CreateAccountCard; diff --git a/js/src/components/google-mc-account-card/create-account.js b/js/src/components/google-mc-account-card/create-account.js deleted file mode 100644 index 9bf5831ad0..0000000000 --- a/js/src/components/google-mc-account-card/create-account.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Internal dependencies - */ -import CreateAccountCard from './create-account-card'; -import CreatingCard from './creating-card'; -import ReclaimUrlCard from './reclaim-url-card'; -import useCreateMCAccount from '.~/hooks/useCreateMCAccount'; - -/** - * Create Account flow. - * - * During the account creation process, this will display the `CreatingCard`, - * and if there is a reclaim URL error, it will display the `ReclaimUrlCard`. - * - * @param {Object} props Props - * @param {Function} props.onSwitchAccount - * Called when users click on "Switch account" button in the ReclaimUrlCard, - * when there is a reclaim URL error during account creation process. - */ -const CreateAccount = ( props ) => { - const { onSwitchAccount } = props; - const [ handleCreateAccount, { loading, error, response } ] = - useCreateMCAccount(); - - if ( loading || ( response && response.status === 503 ) ) { - return ( - - ); - } - - if ( response && response.status === 403 ) { - return ( - - ); - } - - return ; -}; -export default CreateAccount; diff --git a/js/src/components/google-mc-account-card/disabled-card.js b/js/src/components/google-mc-account-card/disabled-card.js deleted file mode 100644 index 8f2545de22..0000000000 --- a/js/src/components/google-mc-account-card/disabled-card.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Internal dependencies - */ -import AccountCard, { APPEARANCE } from '.~/components/account-card'; - -const DisabledCard = () => { - return ( - - ); -}; - -export default DisabledCard; diff --git a/js/src/components/google-mc-account-card/disconnect-account-button.js b/js/src/components/google-mc-account-card/disconnect-account-button.js new file mode 100644 index 0000000000..423097ee70 --- /dev/null +++ b/js/src/components/google-mc-account-card/disconnect-account-button.js @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import AppButton from '.~/components/app-button'; +import { API_NAMESPACE } from '.~/data/constants'; +import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; +import useApiFetchCallback from '.~/hooks/useApiFetchCallback'; +import { useAppDispatch } from '.~/data'; + +/** + * Clicking on the "connect to a different Google Merchant Center account" button. + * + * @event gla_mc_account_connect_different_account_button_click + */ + +/** + * Renders a button to disconnect the current connected Google Merchant Center account. + * + * * @fires gla_mc_account_connect_different_account_button_click + */ +const DisconnectAccountButton = ( { onDisconnected = noop, ...restProps } ) => { + const { createNotice, removeNotice } = useDispatchCoreNotices(); + const { invalidateResolution } = useAppDispatch(); + + const [ + disconnectGoogleMCAccount, + { loading: isDisconnectingGoogleMCAccount }, + ] = useApiFetchCallback( { + path: `${ API_NAMESPACE }/mc/connection`, + method: 'DELETE', + } ); + + /** + * Event handler to switch GMC account. Upon click, it will: + * + * 1. Display a notice to indicate disconnection in progress, and advise users to wait. + * 2. Call API to disconnect the current connected GMC account. + * 3. Call API to refetch list of GMC accounts. + * Users may have just created a new account, + * and we want that new account to show up in the list. + * 4. Call API to refetch GMC account connection status. + * 5. If there is an error in the above API calls, display an error notice. + */ + const handleSwitch = async () => { + const { notice } = await createNotice( + 'info', + __( + 'Disconnecting your Google Merchant Center account, please wait…', + 'google-listings-and-ads' + ) + ); + + try { + await disconnectGoogleMCAccount(); + invalidateResolution( 'getExistingGoogleMCAccounts', [] ); + invalidateResolution( 'getGoogleMCAccount', [] ); + onDisconnected(); + } catch ( error ) { + createNotice( + 'error', + __( + 'Unable to disconnect your Google Merchant Center account. Please try again later.', + 'google-listings-and-ads' + ) + ); + } + + removeNotice( notice.id ); + }; + + return ( + + ); +}; + +export default DisconnectAccountButton; diff --git a/js/src/components/google-mc-account-card/google-mc-account-card.js b/js/src/components/google-mc-account-card/google-mc-account-card.js deleted file mode 100644 index 69429a4db0..0000000000 --- a/js/src/components/google-mc-account-card/google-mc-account-card.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Internal dependencies - */ -import SpinnerCard from '.~/components/spinner-card'; -import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount'; -import ConnectedGoogleMCAccountCard from './connected-google-mc-account-card'; -import DisabledCard from './disabled-card'; -import NonConnected from './non-connected'; - -const GoogleMCAccountCard = () => { - const { hasFinishedResolution, isPreconditionReady, googleMCAccount } = - useGoogleMCAccount(); - - if ( ! hasFinishedResolution ) { - return ; - } - - if ( ! isPreconditionReady ) { - return ; - } - - if ( - googleMCAccount.id === 0 || - ( googleMCAccount.status !== 'connected' && - googleMCAccount.step !== 'link_ads' ) - ) { - return ; - } - - return ( - - ); -}; - -export default GoogleMCAccountCard; diff --git a/js/src/components/google-mc-account-card/index.js b/js/src/components/google-mc-account-card/index.js index ebecad11fc..309d3d3d0e 100644 --- a/js/src/components/google-mc-account-card/index.js +++ b/js/src/components/google-mc-account-card/index.js @@ -1,2 +1 @@ -export { default } from './google-mc-account-card'; -export { default as ConnectedGoogleMCAccountCard } from './connected-google-mc-account-card'; +export { default as MerchantCenterAccountInfoCard } from './merchant-center-account-info-card'; diff --git a/js/src/components/google-mc-account-card/connected-google-mc-account-card.js b/js/src/components/google-mc-account-card/merchant-center-account-info-card.js similarity index 64% rename from js/src/components/google-mc-account-card/connected-google-mc-account-card.js rename to js/src/components/google-mc-account-card/merchant-center-account-info-card.js index d17820903f..c0070baf64 100644 --- a/js/src/components/google-mc-account-card/connected-google-mc-account-card.js +++ b/js/src/components/google-mc-account-card/merchant-center-account-info-card.js @@ -26,36 +26,20 @@ import DisconnectModal, { } from '.~/settings/disconnect-modal'; import { getSettingsUrl } from '.~/utils/urls'; -/** - * Clicking on the "connect to a different Google Merchant Center account" button. - * - * @event gla_mc_account_connect_different_account_button_click - */ - /** * Renders a Google Merchant Center account card UI with connected account information. - * It also provides a switch button that lets user connect with another account. * - * @fires gla_mc_account_connect_different_account_button_click * @param {Object} props React props. * @param {{ id: number }} props.googleMCAccount A data payload object containing the user's Google Merchant Center account ID. - * @param {boolean} [props.hideAccountSwitch=false] Indicate whether hide the account switch block at the card footer. * @param {boolean} [props.hideNotificationService=true] Indicate whether hide the enable Notification service block at the card footer. */ -const ConnectedGoogleMCAccountCard = ( { +const MerchantCenterAccountInfoCard = ( { googleMCAccount, - hideAccountSwitch = false, hideNotificationService = false, } ) => { const { createNotice, removeNotice } = useDispatchCoreNotices(); const { invalidateResolution } = useAppDispatch(); - const [ fetchGoogleMCDisconnect, { loading: loadingGoogleMCDisconnect } ] = - useApiFetchCallback( { - path: `${ API_NAMESPACE }/mc/connection`, - method: 'DELETE', - } ); - const [ fetchDisableNotifications, { loading: loadingDisableNotifications }, @@ -74,43 +58,6 @@ const ConnectedGoogleMCAccountCard = ( { const domain = new URL( getSetting( 'homeUrl' ) ).host; - /** - * Event handler to switch GMC account. Upon click, it will: - * - * 1. Display a notice to indicate disconnection in progress, and advise users to wait. - * 2. Call API to disconnect the current connected GMC account. - * 3. Call API to refetch list of GMC accounts. - * Users may have just created a new account, - * and we want that new account to show up in the list. - * 4. Call API to refetch GMC account connection status. - * 5. If there is an error in the above API calls, display an error notice. - */ - const handleSwitch = async () => { - const { notice } = await createNotice( - 'info', - __( - 'Disconnecting your Google Merchant Center account, please wait…', - 'google-listings-and-ads' - ) - ); - - try { - await fetchGoogleMCDisconnect(); - invalidateResolution( 'getExistingGoogleMCAccounts', [] ); - invalidateResolution( 'getGoogleMCAccount', [] ); - } catch ( error ) { - createNotice( - 'error', - __( - 'Unable to disconnect your Google Merchant Center account. Please try again later.', - 'google-listings-and-ads' - ) - ); - } - - removeNotice( notice.id ); - }; - const disableNotifications = async () => { const { notice } = await createNotice( 'info', @@ -150,8 +97,6 @@ const ConnectedGoogleMCAccountCard = ( { googleMCAccount.wpcom_rest_api_status !== GOOGLE_WPCOM_APP_CONNECTED_STATUS.APPROVED; - const showFooter = ! hideAccountSwitch || showDisconnectNotificationsButton; - return ( ) } - { showFooter && ( + { showDisconnectNotificationsButton && ( - { ! hideAccountSwitch && ( - - ) } - { showDisconnectNotificationsButton && ( - - ) } + ) } ); }; -export default ConnectedGoogleMCAccountCard; +export default MerchantCenterAccountInfoCard; diff --git a/js/src/components/google-mc-account-card/non-connected.js b/js/src/components/google-mc-account-card/non-connected.js deleted file mode 100644 index d6153360c2..0000000000 --- a/js/src/components/google-mc-account-card/non-connected.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Internal dependencies - */ -import useExistingGoogleMCAccounts from '.~/hooks/useExistingGoogleMCAccounts'; -import SpinnerCard from '.~/components/spinner-card'; -import ConnectMC from './connect-mc'; -import CreateAccount from './create-account'; - -const NonConnected = () => { - const { - data: existingAccounts, - hasFinishedResolution, - invalidateResolution, - } = useExistingGoogleMCAccounts(); - - if ( ! hasFinishedResolution ) { - return ; - } - - if ( existingAccounts.length > 0 ) { - return ; - } - - return ; -}; - -export default NonConnected; diff --git a/js/src/hooks/useAutoCreateAdsMCAccounts.js b/js/src/hooks/useAutoCreateAdsMCAccounts.js index e295f263bb..8bf00d4320 100644 --- a/js/src/hooks/useAutoCreateAdsMCAccounts.js +++ b/js/src/hooks/useAutoCreateAdsMCAccounts.js @@ -10,7 +10,6 @@ import useGoogleAdsAccount from './useGoogleAdsAccount'; import useExistingGoogleAdsAccounts from './useExistingGoogleAdsAccounts'; import useGoogleMCAccount from './useGoogleMCAccount'; import useExistingGoogleMCAccounts from './useExistingGoogleMCAccounts'; -import useCreateMCAccount from './useCreateMCAccount'; import useUpsertAdsAccount from '.~/hooks/useUpsertAdsAccount'; import { CREATING_ADS_ACCOUNT, @@ -65,18 +64,16 @@ const useShouldCreateMCAccount = () => { /** * useAutoCreateAdsMCAccounts hook. * Creates Google Ads and Merchant Center accounts if the user doesn't have any existing and connected accounts. - * + * @param {Function} createMCAccount Callback to create a Merchant Center account. * @return {AutoCreateAdsMCAccountsData} Object containing account creation data. */ -const useAutoCreateAdsMCAccounts = () => { +const useAutoCreateAdsMCAccounts = ( createMCAccount ) => { const lockedRef = useRef( false ); const [ creatingWhich, setCreatingWhich ] = useState( null ); const [ hasDetermined, setDetermined ] = useState( false ); const shouldCreateAds = useShouldCreateAdsAccount(); const shouldCreateMC = useShouldCreateMCAccount(); - - const [ handleCreateAccount ] = useCreateMCAccount(); const [ upsertAdsAccount ] = useUpsertAdsAccount(); useEffect( () => { @@ -108,10 +105,10 @@ const useAutoCreateAdsMCAccounts = () => { if ( which ) { const handleCreateAccountCallback = async () => { if ( which === CREATING_BOTH_ACCOUNTS ) { - await handleCreateAccount(); + await createMCAccount(); await upsertAdsAccount(); } else if ( which === CREATING_MC_ACCOUNT ) { - await handleCreateAccount(); + await createMCAccount(); } else if ( which === CREATING_ADS_ACCOUNT ) { await upsertAdsAccount(); } @@ -120,12 +117,7 @@ const useAutoCreateAdsMCAccounts = () => { handleCreateAccountCallback(); } - }, [ - handleCreateAccount, - shouldCreateAds, - shouldCreateMC, - upsertAdsAccount, - ] ); + }, [ createMCAccount, shouldCreateAds, shouldCreateMC, upsertAdsAccount ] ); return { hasDetermined, diff --git a/js/src/hooks/useAutoCreateAdsMCAccounts.test.js b/js/src/hooks/useAutoCreateAdsMCAccounts.test.js index 84c3ce5da5..961418a141 100644 --- a/js/src/hooks/useAutoCreateAdsMCAccounts.test.js +++ b/js/src/hooks/useAutoCreateAdsMCAccounts.test.js @@ -22,11 +22,11 @@ jest.mock( './useGoogleMCAccount' ); jest.mock( './useExistingGoogleMCAccounts' ); describe( 'useAutoCreateAdsMCAccounts hook', () => { - let handleCreateAccount; + let createMCAccount; let upsertAdsAccount; beforeEach( () => { - handleCreateAccount = jest.fn( () => Promise.resolve() ); + createMCAccount = jest.fn( () => Promise.resolve() ); upsertAdsAccount = jest.fn( () => Promise.resolve() ); useGoogleAdsAccount.mockReturnValue( { @@ -53,7 +53,7 @@ describe( 'useAutoCreateAdsMCAccounts hook', () => { describe( 'Automatic account creation', () => { beforeEach( () => { useCreateMCAccount.mockReturnValue( [ - handleCreateAccount, + createMCAccount, { response: undefined }, ] ); useUpsertAdsAccount.mockReturnValue( [ @@ -63,13 +63,15 @@ describe( 'useAutoCreateAdsMCAccounts hook', () => { } ); it( 'should create both accounts', async () => { - const { result } = renderHook( () => useAutoCreateAdsMCAccounts() ); + const { result } = renderHook( () => + useAutoCreateAdsMCAccounts( createMCAccount ) + ); await act( async () => { expect( result.current.creatingWhich ).toBe( 'both' ); } ); - expect( handleCreateAccount ).toHaveBeenCalledTimes( 1 ); + expect( createMCAccount ).toHaveBeenCalledTimes( 1 ); expect( upsertAdsAccount ).toHaveBeenCalledTimes( 1 ); } ); @@ -84,12 +86,14 @@ describe( 'useAutoCreateAdsMCAccounts hook', () => { ], } ); - const { result } = renderHook( () => useAutoCreateAdsMCAccounts() ); + const { result } = renderHook( () => + useAutoCreateAdsMCAccounts( createMCAccount ) + ); await act( async () => { expect( result.current.creatingWhich ).toBe( 'mc' ); } ); - expect( handleCreateAccount ).toHaveBeenCalledTimes( 1 ); + expect( createMCAccount ).toHaveBeenCalledTimes( 1 ); expect( upsertAdsAccount ).toHaveBeenCalledTimes( 0 ); } ); @@ -104,12 +108,14 @@ describe( 'useAutoCreateAdsMCAccounts hook', () => { ], } ); - const { result } = renderHook( () => useAutoCreateAdsMCAccounts() ); + const { result } = renderHook( () => + useAutoCreateAdsMCAccounts( createMCAccount ) + ); await act( async () => { expect( result.current.creatingWhich ).toBe( 'ads' ); } ); - expect( handleCreateAccount ).toHaveBeenCalledTimes( 0 ); + expect( createMCAccount ).toHaveBeenCalledTimes( 0 ); expect( upsertAdsAccount ).toHaveBeenCalledTimes( 1 ); } ); } ); @@ -138,10 +144,12 @@ describe( 'useAutoCreateAdsMCAccounts hook', () => { } ); it( 'should not create accounts if they already exist', () => { - const { result } = renderHook( () => useAutoCreateAdsMCAccounts() ); + const { result } = renderHook( () => + useAutoCreateAdsMCAccounts( createMCAccount ) + ); expect( result.current.creatingWhich ).toBe( null ); - expect( handleCreateAccount ).not.toHaveBeenCalled(); + expect( createMCAccount ).not.toHaveBeenCalled(); expect( upsertAdsAccount ).not.toHaveBeenCalled(); } ); } ); diff --git a/js/src/components/google-mc-account-card/useConnectMCAccount.js b/js/src/hooks/useConnectMCAccount.js similarity index 100% rename from js/src/components/google-mc-account-card/useConnectMCAccount.js rename to js/src/hooks/useConnectMCAccount.js diff --git a/js/src/settings/linked-accounts.js b/js/src/settings/linked-accounts.js index 8d69e90e3a..a09ddd2581 100644 --- a/js/src/settings/linked-accounts.js +++ b/js/src/settings/linked-accounts.js @@ -19,8 +19,8 @@ import SpinnerCard from '.~/components/spinner-card'; import VerticalGapLayout from '.~/components/vertical-gap-layout'; import { ConnectedWPComAccountCard } from '.~/components/wpcom-account-card'; import { ConnectedGoogleAccountCard } from '.~/components/google-account-card'; -import { ConnectedGoogleMCAccountCard } from '.~/components/google-mc-account-card'; import { ConnectedGoogleAdsAccountCard } from '.~/components/google-ads-account-card'; +import { MerchantCenterAccountInfoCard } from '.~/components/google-mc-account-card'; import Section from '.~/wcdl/section'; import LinkedAccountsSectionWrapper from './linked-accounts-section-wrapper'; import DisconnectModal, { ALL_ACCOUNTS, ADS_ACCOUNT } from './disconnect-modal'; @@ -93,9 +93,8 @@ export default function LinkedAccounts() { googleAccount={ google } hideAccountSwitch /> - { hasAdsAccount && ( { test.beforeAll( async ( { browser } ) => { page = await browser.newPage(); @@ -247,7 +262,7 @@ test.describe( 'Set up accounts', () => { await setUpAccountsPage.mockGoogleConnected(); await setUpAccountsPage.mockMCConnected(); await setUpAccountsPage.mockAdsAccountConnected(); - + await setUpAccountsPage.mockMCHasAccounts(); await setUpAccountsPage.goto(); } ); @@ -276,6 +291,262 @@ test.describe( 'Set up accounts', () => { } ); } ); + test.describe( 'Connect Merchant Center account', () => { + test.beforeAll( async () => { + await Promise.all( [ + // Mock Jetpack as connected. + setUpAccountsPage.mockJetpackConnected(), + + // Mock google as connected. + setUpAccountsPage.mockGoogleConnected( 'google@example.com' ), + + // Mock Google Ads as connected. + setUpAccountsPage.mockAdsAccountConnected(), + setUpAccountsPage.mockAdsStatusClaimed(), + + // Mock merchant center as not connected. + setUpAccountsPage.mockMCNotConnected(), + ] ); + } ); + + test.describe( 'Create Merchant Center Account', () => { + test.beforeAll( async () => { + await setUpAccountsPage.mockAdsAccountsResponse( ADS_ACCOUNTS ); + await setUpAccountsPage.mockMCHasAccounts(); + await setUpAccountsPage.goto(); + } ); + + test( 'should see their Google email, "Google Merchant Center" title & "Create account" button', async () => { + const googleDescriptionRow = + setUpAccountsPage.getGoogleDescriptionRow(); + await expect( googleDescriptionRow ).toContainText( + 'google@example.com' + ); + + const mcTitleRow = setUpAccountsPage.getMCTitleRow(); + await expect( mcTitleRow ).toContainText( + 'Connect to existing Merchant Center account' + ); + + const mcFooterButton = + setUpAccountsPage.getMCCardFooterButton(); + await expect( mcFooterButton ).toBeEnabled(); + + const continueButton = setUpAccountsPage.getContinueButton(); + await expect( continueButton ).toBeDisabled(); + } ); + + test( 'click "Create account" button should see the modal of confirmation of creating account', async () => { + // Click the create account button + const createAccountButton = + setUpAccountsPage.getMCCreateAccountButtonFromPage(); + await createAccountButton.click(); + + const modalHeader = setUpAccountsPage.getModalHeader(); + await expect( modalHeader ).toContainText( + 'Create Google Merchant Center Account' + ); + + const modalCheckbox = setUpAccountsPage.getModalCheckbox(); + await expect( modalCheckbox ).toBeEnabled(); + + const createAccountButtonFromModal = + setUpAccountsPage.getMCCreateAccountButtonFromModal(); + await expect( createAccountButtonFromModal ).toBeDisabled(); + + // Click the checkbox of accepting ToS, the create account button will be enabled. + await modalCheckbox.click(); + await expect( createAccountButtonFromModal ).toBeEnabled(); + } ); + + test.describe( 'click "Create account" button from the modal', () => { + test( 'should see Merchant Center account "Connected" when the website is not claimed', async () => { + await Promise.all( [ + // Mock Merchant Center create accounts + setUpAccountsPage.mockMCCreateAccountWebsiteNotClaimed(), + + // Mock Merchant Center as connected with ID 12345 + setUpAccountsPage.mockMCConnected( 12345 ), + ] ); + + const createAccountButtonFromModal = + setUpAccountsPage.getMCCreateAccountButtonFromModal(); + await createAccountButtonFromModal.click(); + const mcConnectedLabel = + setUpAccountsPage.getGoogleComboConnectedLabel(); + await expect( mcConnectedLabel ).toContainText( + 'Connected' + ); + + const mcDescriptionRow = + setUpAccountsPage.getMCDescriptionRow(); + await expect( mcDescriptionRow ).toContainText( + 'Merchant Center ID: 12345' + ); + } ); + + test.describe( 'when the website is already claimed', () => { + test( 'should see "Reclaim my URL" button, "Switch account" button, and site URL input', async ( { + baseURL, + } ) => { + const host = new URL( baseURL ).host; + + await Promise.all( [ + // Mock Merchant Center as not connected + setUpAccountsPage.mockMCNotConnected(), + ] ); + + await page.reload(); + + // Mock Merchant Center create accounts + await setUpAccountsPage.mockMCCreateAccountWebsiteClaimed( + 12345, + host + ); + + // Click "Create account" button from the page. + const createAccountButton = + setUpAccountsPage.getMCCreateAccountButtonFromPage(); + await createAccountButton.click(); + + // Check the checkbox to accept ToS. + const modalCheckbox = + setUpAccountsPage.getModalCheckbox(); + await modalCheckbox.click(); + + // Click "Create account" button from the modal. + const createAccountButtonFromModal = + setUpAccountsPage.getMCCreateAccountButtonFromModal(); + await createAccountButtonFromModal.click(); + + const reclaimButton = + setUpAccountsPage.getReclaimMyURLButton(); + await expect( reclaimButton ).toBeVisible(); + + const switchAccountButton = + setUpAccountsPage.getSwitchAccountButton(); + await expect( switchAccountButton ).toBeVisible(); + + const reclaimingURLInput = + setUpAccountsPage.getReclaimingURLInput(); + await expect( reclaimingURLInput ).toHaveValue( + baseURL + ); + + const continueButton = + setUpAccountsPage.getContinueButton(); + await expect( continueButton ).toBeDisabled(); + } ); + + test( 'click "Reclaim my URL" should send a claim overwrite request and see Merchant Center "Connected"', async () => { + await Promise.all( [ + // Mock Merchant Center accounts claim overwrite + setUpAccountsPage.mockMCAccountsClaimOverwrite( + 12345 + ), + + // Mock Merchant Center as connected with ID 12345 + setUpAccountsPage.mockMCConnected( 12345 ), + ] ); + + const reclaimButton = + setUpAccountsPage.getReclaimMyURLButton(); + await reclaimButton.click(); + + const mcConnectedLabel = + setUpAccountsPage.getGoogleComboConnectedLabel(); + await expect( mcConnectedLabel ).toContainText( + 'Connected' + ); + + const mcDescriptionRow = + setUpAccountsPage.getMCDescriptionRow(); + await expect( mcDescriptionRow ).toContainText( + 'Merchant Center ID: 12345' + ); + } ); + } ); + } ); + } ); + + test.describe( 'Connect Merchant Center account', () => { + test.beforeAll( async () => { + await Promise.all( [ + setUpAccountsPage.mockAdsAccountsResponse( ADS_ACCOUNTS ), + + // Mock merchant center as not connected. + setUpAccountsPage.mockMCNotConnected(), + + // Mock merchant center has accounts + setUpAccountsPage.mockMCHasAccounts(), + ] ); + + await setUpAccountsPage.goto(); + } ); + + test.describe( 'connect to an existing account', () => { + test( 'should see "Select an existing account" title', async () => { + const selectAccountTitle = + setUpAccountsPage.getSelectExistingMCAccountTitle(); + await expect( selectAccountTitle ).toContainText( + 'Connect to existing Merchant Center account' + ); + } ); + + test( 'should see "Or, create a new Merchant Center account" text', async () => { + const mcFooter = setUpAccountsPage.getMCCardFooter(); + await expect( mcFooter ).toContainText( + 'Or, create a new Merchant Center account' + ); + } ); + + test( 'should see "Connect" button', async () => { + const connectButton = setUpAccountsPage.getConnectButton(); + await expect( connectButton ).toBeEnabled(); + } ); + + test( 'should see "Continue" button is disabled', async () => { + const continueButton = + setUpAccountsPage.getContinueButton(); + await expect( continueButton ).toBeDisabled(); + } ); + + test( 'select MC Account 2 and click "Connect" button should see Merchant Center "Connected"', async () => { + await Promise.all( [ + // Mock Merchant Center create accounts + setUpAccountsPage.mockMCCreateAccountWebsiteNotClaimed(), + + // Mock Merchant Center as connected with ID 12345 + setUpAccountsPage.mockMCConnected( 23456 ), + ] ); + + // Select MC Account 2 from the options + const mcAccountsSelect = + setUpAccountsPage.getMCAccountsSelect(); + await mcAccountsSelect.selectOption( { + label: 'MC Account 2 ・ https://example.com (23456)', + } ); + + // Click connect button + const connectButton = setUpAccountsPage.getConnectButton(); + await connectButton.click(); + + const mcConnectedLabel = + setUpAccountsPage.getGoogleComboConnectedLabel(); + await expect( mcConnectedLabel ).toContainText( + 'Connected' + ); + + const mcDescriptionRow = + setUpAccountsPage.getMCDescriptionRow(); + await expect( mcDescriptionRow ).toContainText( + 'Merchant Center ID: 23456' + ); + } ); + } ); + } ); + } ); + test.describe( 'Google Ads card', () => { test.beforeAll( async () => { await setUpAccountsPage.mockJetpackConnected(); @@ -283,20 +554,7 @@ test.describe( 'Set up accounts', () => { await setUpAccountsPage.mockMCHasAccounts(); await setUpAccountsPage.mockMCConnected(); await setUpAccountsPage.mockAdsAccountDisconnected(); - await setUpAccountsPage.fulfillAdsAccounts( [ - { - id: 111111, - name: 'gla', - }, - { - id: 222222, - name: 'gla', - }, - { - id: 333333, - name: 'gla', - }, - ] ); + await setUpAccountsPage.fulfillAdsAccounts( ADS_ACCOUNTS ); await setUpAccountsPage.goto(); } ); @@ -420,10 +678,7 @@ test.describe( 'Set up accounts', () => { test.describe( 'Continue button', () => { test.beforeAll( async () => { // Mock Jetpack as connected - await setUpAccountsPage.mockJetpackConnected( - 'Test user', - 'jetpack@example.com' - ); + await setUpAccountsPage.mockJetpackConnected(); // Mock google as connected. await setUpAccountsPage.mockGoogleConnected(); @@ -447,6 +702,7 @@ test.describe( 'Set up accounts', () => { test.describe( 'When only MC is connected', async () => { test.beforeAll( async () => { await setUpAccountsPage.mockAdsAccountDisconnected(); + await setUpAccountsPage.fulfillAdsAccounts( ADS_ACCOUNTS ); await setUpAccountsPage.mockMCConnected(); await setUpAccountsPage.goto(); 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 57d3e6b9c6..57493c903d 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 @@ -55,8 +55,7 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get MC "Create account" button from the page. */ getMCCreateAccountButtonFromPage() { - const button = this.getCreateAccountButton(); - return button.locator( ':scope.is-secondary' ).nth( 1 ); + return this.getMCCardFooterButton(); } /** @@ -86,9 +85,9 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Merchant Center description row. */ getMCDescriptionRow() { - return this.getMCAccountCard().locator( - '.gla-account-card__description' - ); + return this.getGoogleDescriptionRow().locator( 'p', { + hasText: 'Merchant Center ID', + } ); } /** @@ -154,6 +153,17 @@ export default class SetUpAccountsPage extends MockRequests { return this.getMCAccountCard().locator( '.gla-connected-icon-label' ); } + /** + * Get Google combo card connected label. + * + * @return {import('@playwright/test').Locator} Get Google combo card connected label. + */ + getGoogleComboConnectedLabel() { + return this.getGoogleComboAccountCard().locator( + '.gla-connected-icon-label' + ); + } + /** * Get "Reclaim my URL" button. * @@ -193,9 +203,7 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get select existing Merchant Center account title. */ getSelectExistingMCAccountTitle() { - return this.getMCAccountCard() - .locator( '.wcdl-subsection-title' ) - .nth( 1 ); + return this.getMCAccountCard().locator( '.gla-account-card__title' ); } /** @@ -204,7 +212,7 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get select MC accounts select element. */ getMCAccountsSelect() { - return this.page.locator( 'select[id*="inspector-select-control"]' ); + return this.getMCAccountCard().getByRole( 'combobox' ); } /** @@ -220,13 +228,13 @@ export default class SetUpAccountsPage extends MockRequests { } /** - * Get account cards. + * Get Google combo account card. * * @param {Object} options - * @return {import('@playwright/test').Locator} Get account cards. + * @return {import('@playwright/test').Locator} Get Google combo account card. */ - getAccountCards( options = {} ) { - return this.page.locator( '.gla-account-card', options ); + getGoogleComboAccountCard( options = {} ) { + return this.page.locator( '.gla-google-combo-account-card', options ); } /** @@ -235,7 +243,9 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get WordPress account card. */ getWPAccountCard() { - return this.getAccountCards( { hasText: 'WordPress.com' } ).first(); + return this.page.locator( '.gla-account-card', { + hasText: 'WordPress.com', + } ); } /** @@ -244,11 +254,9 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Google account card. */ getGoogleAccountCard() { - return this.getAccountCards( { - has: this.page.locator( '.gla-account-card__title', { - hasText: 'Google', - } ), - } ).first(); + return this.page.locator( + '.gla-google-combo-service-account-card--google' + ); } /** @@ -268,11 +276,9 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Merchant Center account card. */ getMCAccountCard() { - return this.getAccountCards( { - has: this.page.locator( '.gla-account-card__title', { - hasText: 'Google Merchant Center', - } ), - } ).first(); + return this.page.locator( + '.gla-google-combo-service-account-card--mc' + ); } /** @@ -281,7 +287,7 @@ export default class SetUpAccountsPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Merchant Center card footer. */ getMCCardFooter() { - return this.getMCAccountCard().locator( '.wcdl-section-card-footer' ); + return this.getMCAccountCard().locator( '.gla-account-card__actions' ); } /**