diff --git a/js/src/components/google-combo-account-card/account-details.js b/js/src/components/google-combo-account-card/account-details.js
new file mode 100644
index 0000000000..40f81bcf74
--- /dev/null
+++ b/js/src/components/google-combo-account-card/account-details.js
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import useGoogleAccount from '.~/hooks/useGoogleAccount';
+import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
+import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount';
+
+/**
+ * Account details.
+ * @return {JSX.Element} JSX markup.
+ */
+const AccountDetails = () => {
+ const { google } = useGoogleAccount();
+ const { googleAdsAccount } = useGoogleAdsAccount();
+ const { googleMCAccount } = useGoogleMCAccount();
+
+ return (
+ <>
+
{ google.email }
+
+ { googleMCAccount.id > 0 &&
+ sprintf(
+ // Translators: %s is the Merchant Center ID
+ __(
+ 'Merchant Center ID: %s',
+ 'google-listings-and-ads'
+ ),
+ googleMCAccount.id
+ ) }
+
+
+ { googleAdsAccount.id > 0 &&
+ sprintf(
+ // Translators: %s is the Google Ads ID
+ __( 'Google Ads ID: %s', 'google-listings-and-ads' ),
+ googleAdsAccount.id
+ ) }
+
+ >
+ );
+};
+
+export default AccountDetails;
diff --git a/js/src/components/google-combo-account-card/connect-google-combo-account-card.js b/js/src/components/google-combo-account-card/connect-google-combo-account-card.js
index dfff4dd2c9..a20c0c3de8 100644
--- a/js/src/components/google-combo-account-card/connect-google-combo-account-card.js
+++ b/js/src/components/google-combo-account-card/connect-google-combo-account-card.js
@@ -14,7 +14,6 @@ import AppButton from '.~/components/app-button';
import readMoreLink from '../google-account-card/read-more-link';
import useGoogleConnectFlow from '../google-account-card/use-google-connect-flow';
import AppDocumentationLink from '../app-documentation-link';
-import './connect-google-combo-account-card.scss';
/**
* @param {Object} props React props
@@ -34,7 +33,6 @@ const ConnectGoogleComboAccountCard = ( { disabled } ) => {
return (
{
+ const { hasDetermined, creatingWhich } = useAutoCreateAdsMCAccounts();
+ const { text, subText } = getAccountCreationTexts( creatingWhich );
+
+ if ( ! hasDetermined ) {
+ return ;
+ }
+
+ return (
+ }
+ helper={ subText }
+ indicator={ }
+ />
+ );
+};
+
+export default ConnectedGoogleComboAccountCard;
diff --git a/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss b/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss
new file mode 100644
index 0000000000..9c65e53adf
--- /dev/null
+++ b/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss
@@ -0,0 +1,6 @@
+.gla-google-combo-account-card--connected {
+
+ .gla-account-card__description {
+ gap: 0;
+ }
+}
diff --git a/js/src/components/google-combo-account-card/constants.js b/js/src/components/google-combo-account-card/constants.js
new file mode 100644
index 0000000000..3086a154d8
--- /dev/null
+++ b/js/src/components/google-combo-account-card/constants.js
@@ -0,0 +1,3 @@
+export const CREATING_BOTH_ACCOUNTS = 'both';
+export const CREATING_ADS_ACCOUNT = 'ads';
+export const CREATING_MC_ACCOUNT = 'mc';
diff --git a/js/src/components/google-combo-account-card/getAccountCreationTexts.js b/js/src/components/google-combo-account-card/getAccountCreationTexts.js
new file mode 100644
index 0000000000..accf7da2bb
--- /dev/null
+++ b/js/src/components/google-combo-account-card/getAccountCreationTexts.js
@@ -0,0 +1,65 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import {
+ CREATING_ADS_ACCOUNT,
+ CREATING_BOTH_ACCOUNTS,
+ CREATING_MC_ACCOUNT,
+} from './constants';
+
+/**
+ * Account creation in progress description.
+ * @param {string|null} creatingWhich Which account is being created.
+ * @return {Object} Text and subtext.
+ */
+const getAccountCreationTexts = ( creatingWhich ) => {
+ let text = null;
+ let subText = null;
+
+ switch ( creatingWhich ) {
+ case CREATING_BOTH_ACCOUNTS:
+ text = __(
+ 'You don’t have Merchant Center nor Google Ads accounts, so we’re creating them for you.',
+ 'google-listings-and-ads'
+ );
+ subText = __(
+ 'Merchant Center is required to sync products so they show on Google. Google Ads is required to set up conversion measurement for your store.',
+ 'google-listings-and-ads'
+ );
+ break;
+
+ case CREATING_ADS_ACCOUNT:
+ text = __(
+ 'You don’t have Google Ads account, so we’re creating one for you.',
+ 'google-listings-and-ads'
+ );
+ subText = __(
+ 'Required to set up conversion measurement for your store.',
+ 'google-listings-and-ads'
+ );
+ break;
+
+ case CREATING_MC_ACCOUNT:
+ text = __(
+ 'You don’t have Merchant Center account, so we’re creating one for you.',
+ 'google-listings-and-ads'
+ );
+ subText = __(
+ 'Required to sync products so they show on Google.',
+ 'google-listings-and-ads'
+ );
+ break;
+ }
+
+ return {
+ text,
+ subText,
+ };
+};
+
+export default getAccountCreationTexts;
diff --git a/js/src/components/google-combo-account-card/index.js b/js/src/components/google-combo-account-card/index.js
index 098fb7b4ef..c1db3e5391 100644
--- a/js/src/components/google-combo-account-card/index.js
+++ b/js/src/components/google-combo-account-card/index.js
@@ -5,8 +5,8 @@ import useGoogleAccount from '.~/hooks/useGoogleAccount';
import AppSpinner from '.~/components/app-spinner';
import AccountCard from '.~/components/account-card';
import RequestFullAccessGoogleAccountCard from '../google-account-card/request-full-access-google-account-card';
-import { ConnectedGoogleAccountCard } from '../google-account-card';
import ConnectGoogleComboAccountCard from './connect-google-combo-account-card';
+import ConnectedGoogleComboAccountCard from './connected-google-combo-account-card';
export default function GoogleComboAccountCard( { disabled = false } ) {
const { google, scope, hasFinishedResolution } = useGoogleAccount();
@@ -18,7 +18,7 @@ export default function GoogleComboAccountCard( { disabled = false } ) {
const isConnected = google?.active === 'yes';
if ( isConnected && scope.glaRequired ) {
- return ;
+ return ;
}
if ( isConnected && ! scope.glaRequired ) {
diff --git a/js/src/components/google-combo-account-card/indicator.js b/js/src/components/google-combo-account-card/indicator.js
new file mode 100644
index 0000000000..5a65c0b6b7
--- /dev/null
+++ b/js/src/components/google-combo-account-card/indicator.js
@@ -0,0 +1,40 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import ConnectedIconLabel from '.~/components/connected-icon-label';
+import LoadingLabel from '.~/components/loading-label/loading-label';
+import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady';
+import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount';
+
+/**
+ * Account creation indicator.
+ * Displays a loading indicator when accounts are being created or a connected icon when accounts are connected.
+ * @param {Object} props Component props.
+ * @param {boolean} props.showSpinner Whether to display a spinner.
+ * @return {JSX.Element|null} Indicator component.
+ */
+const Indicator = ( { showSpinner } ) => {
+ const isGoogleAdsConnected = useGoogleAdsAccountReady();
+ const { isReady: isGoogleMCConnected } = useGoogleMCAccount();
+
+ if ( showSpinner ) {
+ return (
+
+ );
+ }
+
+ if ( isGoogleAdsConnected && isGoogleMCConnected ) {
+ return ;
+ }
+
+ return null;
+};
+
+export default Indicator;
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 a47ccfa5fc..658bc245f3 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
@@ -18,7 +18,7 @@ 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 '../useCreateMCAccount';
+import useCreateMCAccount from '.~/hooks/useCreateMCAccount';
import CreatingCard from '../creating-card';
import './index.scss';
diff --git a/js/src/components/google-mc-account-card/create-account.js b/js/src/components/google-mc-account-card/create-account.js
index f8c50b4f3b..9bf5831ad0 100644
--- a/js/src/components/google-mc-account-card/create-account.js
+++ b/js/src/components/google-mc-account-card/create-account.js
@@ -4,7 +4,7 @@
import CreateAccountCard from './create-account-card';
import CreatingCard from './creating-card';
import ReclaimUrlCard from './reclaim-url-card';
-import useCreateMCAccount from './useCreateMCAccount';
+import useCreateMCAccount from '.~/hooks/useCreateMCAccount';
/**
* Create Account flow.
diff --git a/js/src/constants.js b/js/src/constants.js
index de28ac0544..ad0ae6d99d 100644
--- a/js/src/constants.js
+++ b/js/src/constants.js
@@ -70,6 +70,12 @@ export const GOOGLE_ADS_ACCOUNT_STATUS = {
INCOMPLETE: 'incomplete',
};
+export const GOOGLE_MC_ACCOUNT_STATUS = {
+ CONNECTED: 'connected',
+ DISCONNECTED: 'disconnected',
+ INCOMPLETE: 'incomplete',
+};
+
export const GOOGLE_ADS_BILLING_STATUS = {
UNKNOWN: 'unknown',
PENDING: 'pending',
diff --git a/js/src/hooks/useAutoCreateAdsMCAccounts.js b/js/src/hooks/useAutoCreateAdsMCAccounts.js
new file mode 100644
index 0000000000..e295f263bb
--- /dev/null
+++ b/js/src/hooks/useAutoCreateAdsMCAccounts.js
@@ -0,0 +1,136 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useState, useRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+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,
+ CREATING_BOTH_ACCOUNTS,
+ CREATING_MC_ACCOUNT,
+} from '.~/components/google-combo-account-card/constants';
+
+const useShouldCreateAdsAccount = () => {
+ const {
+ hasFinishedResolution: hasResolvedAccount,
+ hasGoogleAdsConnection: hasConnection,
+ } = useGoogleAdsAccount();
+
+ const {
+ hasFinishedResolution: hasResolvedExistingAccounts,
+ existingAccounts: accounts,
+ } = useExistingGoogleAdsAccounts();
+
+ // Return null if the account hasn't been resolved or the existing accounts haven't been resolved
+ if ( ! hasResolvedAccount || ! hasResolvedExistingAccounts ) {
+ return null;
+ }
+
+ return ! hasConnection && accounts?.length === 0;
+};
+
+const useShouldCreateMCAccount = () => {
+ const {
+ hasFinishedResolution: hasResolvedAccount,
+ hasGoogleMCConnection: hasConnection,
+ } = useGoogleMCAccount();
+
+ const {
+ hasFinishedResolution: hasResolvedExistingAccounts,
+ data: accounts,
+ } = useExistingGoogleMCAccounts();
+
+ // Return null if the account hasn't been resolved or the existing accounts haven't been resolved
+ if ( ! hasResolvedAccount || ! hasResolvedExistingAccounts ) {
+ return null;
+ }
+
+ return ! hasConnection && accounts?.length === 0;
+};
+
+/**
+ * @typedef {Object} AutoCreateAdsMCAccountsData
+ * @property {boolean} hasDetermined - Whether the checks to determine if accounts should be created are finished.
+ * @property {('ads'|'mc'|'both'|null)} creatingWhich - Which accounts are being created ('ads', 'mc', 'both'), or `null` if none.
+ */
+
+/**
+ * useAutoCreateAdsMCAccounts hook.
+ * Creates Google Ads and Merchant Center accounts if the user doesn't have any existing and connected accounts.
+ *
+ * @return {AutoCreateAdsMCAccountsData} Object containing account creation data.
+ */
+const useAutoCreateAdsMCAccounts = () => {
+ 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( () => {
+ if (
+ // Wait for all determinations to be ready
+ shouldCreateMC === null ||
+ shouldCreateAds === null ||
+ // Avoid repeated calls
+ lockedRef.current
+ ) {
+ return;
+ }
+
+ let which = null;
+
+ lockedRef.current = true;
+
+ if ( shouldCreateMC && shouldCreateAds ) {
+ which = CREATING_BOTH_ACCOUNTS;
+ } else if ( shouldCreateMC ) {
+ which = CREATING_MC_ACCOUNT;
+ } else if ( shouldCreateAds ) {
+ which = CREATING_ADS_ACCOUNT;
+ }
+
+ setCreatingWhich( which );
+ setDetermined( true );
+
+ if ( which ) {
+ const handleCreateAccountCallback = async () => {
+ if ( which === CREATING_BOTH_ACCOUNTS ) {
+ await handleCreateAccount();
+ await upsertAdsAccount();
+ } else if ( which === CREATING_MC_ACCOUNT ) {
+ await handleCreateAccount();
+ } else if ( which === CREATING_ADS_ACCOUNT ) {
+ await upsertAdsAccount();
+ }
+ setCreatingWhich( null );
+ };
+
+ handleCreateAccountCallback();
+ }
+ }, [
+ handleCreateAccount,
+ shouldCreateAds,
+ shouldCreateMC,
+ upsertAdsAccount,
+ ] );
+
+ return {
+ hasDetermined,
+ creatingWhich,
+ };
+};
+
+export default useAutoCreateAdsMCAccounts;
diff --git a/js/src/hooks/useAutoCreateAdsMCAccounts.test.js b/js/src/hooks/useAutoCreateAdsMCAccounts.test.js
new file mode 100644
index 0000000000..84c3ce5da5
--- /dev/null
+++ b/js/src/hooks/useAutoCreateAdsMCAccounts.test.js
@@ -0,0 +1,148 @@
+/**
+ * External dependencies
+ */
+import { act, renderHook } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import useAutoCreateAdsMCAccounts from './useAutoCreateAdsMCAccounts';
+import useGoogleAdsAccount from './useGoogleAdsAccount';
+import useExistingGoogleAdsAccounts from './useExistingGoogleAdsAccounts';
+import useGoogleMCAccount from './useGoogleMCAccount';
+import useExistingGoogleMCAccounts from './useExistingGoogleMCAccounts';
+import useCreateMCAccount from './useCreateMCAccount';
+import useUpsertAdsAccount from './useUpsertAdsAccount';
+
+jest.mock( './useCreateMCAccount' );
+jest.mock( './useUpsertAdsAccount' );
+jest.mock( './useGoogleAdsAccount' );
+jest.mock( './useExistingGoogleAdsAccounts' );
+jest.mock( './useGoogleMCAccount' );
+jest.mock( './useExistingGoogleMCAccounts' );
+
+describe( 'useAutoCreateAdsMCAccounts hook', () => {
+ let handleCreateAccount;
+ let upsertAdsAccount;
+
+ beforeEach( () => {
+ handleCreateAccount = jest.fn( () => Promise.resolve() );
+ upsertAdsAccount = jest.fn( () => Promise.resolve() );
+
+ useGoogleAdsAccount.mockReturnValue( {
+ hasFinishedResolution: true,
+ hasGoogleAdsConnection: false,
+ } );
+
+ useExistingGoogleAdsAccounts.mockReturnValue( {
+ hasFinishedResolution: true,
+ existingAccounts: [],
+ } );
+
+ useGoogleMCAccount.mockReturnValue( {
+ hasFinishedResolution: true,
+ hasGoogleMCConnection: false,
+ } );
+
+ useExistingGoogleMCAccounts.mockReturnValue( {
+ hasFinishedResolution: true,
+ data: [],
+ } );
+ } );
+
+ describe( 'Automatic account creation', () => {
+ beforeEach( () => {
+ useCreateMCAccount.mockReturnValue( [
+ handleCreateAccount,
+ { response: undefined },
+ ] );
+ useUpsertAdsAccount.mockReturnValue( [
+ upsertAdsAccount,
+ { loading: true },
+ ] );
+ } );
+
+ it( 'should create both accounts', async () => {
+ const { result } = renderHook( () => useAutoCreateAdsMCAccounts() );
+
+ await act( async () => {
+ expect( result.current.creatingWhich ).toBe( 'both' );
+ } );
+
+ expect( handleCreateAccount ).toHaveBeenCalledTimes( 1 );
+ expect( upsertAdsAccount ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should create only the Merchant Center account', async () => {
+ useExistingGoogleAdsAccounts.mockReturnValue( {
+ hasFinishedResolution: true,
+ existingAccounts: [
+ {
+ id: '1234',
+ name: 'Test Account',
+ },
+ ],
+ } );
+
+ const { result } = renderHook( () => useAutoCreateAdsMCAccounts() );
+ await act( async () => {
+ expect( result.current.creatingWhich ).toBe( 'mc' );
+ } );
+
+ expect( handleCreateAccount ).toHaveBeenCalledTimes( 1 );
+ expect( upsertAdsAccount ).toHaveBeenCalledTimes( 0 );
+ } );
+
+ it( 'should create only the Google Ads account', async () => {
+ useExistingGoogleMCAccounts.mockReturnValue( {
+ hasFinishedResolution: true,
+ data: [
+ {
+ id: '12345',
+ name: 'Test Account',
+ },
+ ],
+ } );
+
+ const { result } = renderHook( () => useAutoCreateAdsMCAccounts() );
+ await act( async () => {
+ expect( result.current.creatingWhich ).toBe( 'ads' );
+ } );
+
+ expect( handleCreateAccount ).toHaveBeenCalledTimes( 0 );
+ expect( upsertAdsAccount ).toHaveBeenCalledTimes( 1 );
+ } );
+ } );
+
+ describe( 'Existing accounts', () => {
+ beforeEach( () => {
+ useExistingGoogleAdsAccounts.mockReturnValue( {
+ hasFinishedResolution: true,
+ existingAccounts: [
+ {
+ id: '1234',
+ name: 'Test Account',
+ },
+ ],
+ } );
+
+ useExistingGoogleMCAccounts.mockReturnValue( {
+ hasFinishedResolution: true,
+ data: [
+ {
+ id: '12345',
+ name: 'Test Account',
+ },
+ ],
+ } );
+ } );
+
+ it( 'should not create accounts if they already exist', () => {
+ const { result } = renderHook( () => useAutoCreateAdsMCAccounts() );
+
+ expect( result.current.creatingWhich ).toBe( null );
+ expect( handleCreateAccount ).not.toHaveBeenCalled();
+ expect( upsertAdsAccount ).not.toHaveBeenCalled();
+ } );
+ } );
+} );
diff --git a/js/src/components/google-mc-account-card/useCreateMCAccount.js b/js/src/hooks/useCreateMCAccount.js
similarity index 100%
rename from js/src/components/google-mc-account-card/useCreateMCAccount.js
rename to js/src/hooks/useCreateMCAccount.js
diff --git a/js/src/hooks/useExistingGoogleAdsAccounts.js b/js/src/hooks/useExistingGoogleAdsAccounts.js
index 8eda3b9a2f..1e84516710 100644
--- a/js/src/hooks/useExistingGoogleAdsAccounts.js
+++ b/js/src/hooks/useExistingGoogleAdsAccounts.js
@@ -15,8 +15,11 @@ const useExistingGoogleAdsAccounts = () => {
const isResolving = select( STORE_KEY ).isResolving(
'getExistingGoogleAdsAccounts'
);
+ const hasFinishedResolution = select( STORE_KEY ).hasFinishedResolution(
+ 'getExistingGoogleAdsAccounts'
+ );
- return { existingAccounts, isResolving };
+ return { existingAccounts, hasFinishedResolution, isResolving };
}, [] );
};
diff --git a/js/src/hooks/useGoogleAdsAccountReady.js b/js/src/hooks/useGoogleAdsAccountReady.js
new file mode 100644
index 0000000000..b593e44e2d
--- /dev/null
+++ b/js/src/hooks/useGoogleAdsAccountReady.js
@@ -0,0 +1,35 @@
+/**
+ * Internal dependencies
+ */
+import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
+import useGoogleAdsAccountStatus from '.~/hooks/useGoogleAdsAccountStatus';
+
+/**
+ * Hook to check if the Google Ads account is ready.
+ * This is used to determine if the user can proceed to the next step.
+ *
+ * @return {boolean|null} Whether the Google Ads account is ready. `null` if the state is not yet determined.
+ */
+const useGoogleAdsAccountReady = () => {
+ const {
+ hasGoogleAdsConnection,
+ hasFinishedResolution: adsAccountResolved,
+ } = useGoogleAdsAccount();
+ const {
+ hasAccess,
+ step,
+ hasFinishedResolution: adsAccountStatusResolved,
+ } = useGoogleAdsAccountStatus();
+
+ if ( ! adsAccountResolved || ! adsAccountStatusResolved ) {
+ return null;
+ }
+
+ return (
+ hasGoogleAdsConnection &&
+ hasAccess &&
+ [ '', 'billing', 'link_merchant' ].includes( step )
+ );
+};
+
+export default useGoogleAdsAccountReady;
diff --git a/js/src/hooks/useGoogleMCAccount.js b/js/src/hooks/useGoogleMCAccount.js
index 76958ea1d9..22a4ecbc24 100644
--- a/js/src/hooks/useGoogleMCAccount.js
+++ b/js/src/hooks/useGoogleMCAccount.js
@@ -8,6 +8,7 @@ import { useSelect } from '@wordpress/data';
*/
import { STORE_KEY } from '.~/data/constants';
import useGoogleAccount from './useGoogleAccount';
+import { GOOGLE_MC_ACCOUNT_STATUS } from '.~/constants';
/**
* @typedef {import('.~/data/selectors').GoogleMCAccount} GoogleMCAccount
@@ -17,8 +18,12 @@ import useGoogleAccount from './useGoogleAccount';
* @property {boolean} isResolving Whether resolution is in progress.
* @property {boolean} hasFinishedResolution Whether resolution has completed.
* @property {boolean} isPreconditionReady Whether the precondition of continued connection processing is fulfilled.
+ * @property {boolean} hasGoogleMCConnection Whether the user has a Google Merchant Center account connection established.
+ * @property {boolean} isReady Whether the user has a Google Merchant Center account is in connected state.
*/
+const googleMCAccountSelector = 'getGoogleMCAccount';
+
/**
* A hook to load the connection data of Google Merchant Center account.
*
@@ -43,18 +48,36 @@ const useGoogleMCAccount = () => {
// has not been granted necessary access permissions for Google Merchant Center, then
// the precondition doesn't meet.
isPreconditionReady: false,
+ hasGoogleMCConnection: false,
+ isReady: false,
};
}
- const { getGoogleMCAccount, isResolving, hasFinishedResolution } =
- select( STORE_KEY );
+ const selector = select( STORE_KEY );
+ const acc = selector[ googleMCAccountSelector ]();
+ const isResolvingGoogleMCAccount = selector.isResolving(
+ googleMCAccountSelector
+ );
+
+ const hasGoogleMCConnection = [
+ GOOGLE_MC_ACCOUNT_STATUS.CONNECTED,
+ GOOGLE_MC_ACCOUNT_STATUS.INCOMPLETE,
+ ].includes( acc?.status );
+
+ const isReady =
+ acc?.status === GOOGLE_MC_ACCOUNT_STATUS.CONNECTED ||
+ ( acc?.status === GOOGLE_MC_ACCOUNT_STATUS.INCOMPLETE &&
+ acc?.step === 'link_ads' );
return {
- googleMCAccount: getGoogleMCAccount(),
- isResolving: isResolving( 'getGoogleMCAccount' ),
- hasFinishedResolution:
- hasFinishedResolution( 'getGoogleMCAccount' ),
+ googleMCAccount: acc,
+ isResolving: isResolvingGoogleMCAccount,
+ hasFinishedResolution: selector.hasFinishedResolution(
+ googleMCAccountSelector
+ ),
isPreconditionReady: true,
+ hasGoogleMCConnection,
+ isReady,
};
},
[
diff --git a/js/src/setup-ads/ads-stepper/setup-accounts/index.js b/js/src/setup-ads/ads-stepper/setup-accounts/index.js
index bab89e5cc1..d9f1c3b6a3 100644
--- a/js/src/setup-ads/ads-stepper/setup-accounts/index.js
+++ b/js/src/setup-ads/ads-stepper/setup-accounts/index.js
@@ -16,31 +16,23 @@ import { ConnectedGoogleAccountCard } from '.~/components/google-account-card';
import GoogleAdsAccountCard from '.~/components/google-ads-account-card';
import FreeAdCredit from './free-ad-credit';
import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
-import useGoogleAdsAccountStatus from '.~/hooks/useGoogleAdsAccountStatus';
import useGoogleAccount from '.~/hooks/useGoogleAccount';
import useFreeAdCredit from '.~/hooks/useFreeAdCredit';
import AppSpinner from '.~/components/app-spinner';
import Section from '.~/wcdl/section';
+import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady';
const SetupAccounts = ( props ) => {
const { onContinue = () => {} } = props;
const { google } = useGoogleAccount();
- const { googleAdsAccount, hasGoogleAdsConnection } = useGoogleAdsAccount();
- const { hasAccess, step } = useGoogleAdsAccountStatus();
+ const { googleAdsAccount } = useGoogleAdsAccount();
const hasFreeAdCredit = useFreeAdCredit();
+ const isGoogleAdsReady = useGoogleAdsAccountReady();
if ( ! google || ( google.active === 'yes' && ! googleAdsAccount ) ) {
return ;
}
- // Ads is ready when we have a connection and verified and verified access.
- // Billing is not required, and the 'link_merchant' step will be resolved
- // when the MC the account is connected.
- const isGoogleAdsReady =
- hasGoogleAdsConnection &&
- hasAccess &&
- [ '', 'billing', 'link_merchant' ].includes( step );
-
const isContinueButtonDisabled = ! isGoogleAdsReady;
return (
diff --git a/js/src/setup-mc/setup-stepper/setup-accounts/index.js b/js/src/setup-mc/setup-stepper/setup-accounts/index.js
index d4bfc68f86..cf200ed4df 100644
--- a/js/src/setup-mc/setup-stepper/setup-accounts/index.js
+++ b/js/src/setup-mc/setup-stepper/setup-accounts/index.js
@@ -24,7 +24,7 @@ import GoogleComboAccountCard from '.~/components/google-combo-account-card';
import Faqs from './faqs';
import './index.scss';
import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount';
-import useGoogleAdsAccountStatus from '.~/hooks/useGoogleAdsAccountStatus';
+import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady';
/**
* Renders the disclaimer of Comparison Shopping Service (CSS).
@@ -83,11 +83,13 @@ const SetupAccounts = ( props ) => {
const { onContinue = () => {} } = props;
const { jetpack } = useJetpackAccount();
const { google, scope } = useGoogleAccount();
- const { googleMCAccount, isPreconditionReady: isGMCPreconditionReady } =
- useGoogleMCAccount();
- const { hasFinishedResolution, hasGoogleAdsConnection } =
- useGoogleAdsAccount();
- const { hasAccess, step } = useGoogleAdsAccountStatus();
+ const {
+ googleMCAccount,
+ isPreconditionReady: isGMCPreconditionReady,
+ isReady: isGoogleMCReady,
+ } = useGoogleMCAccount();
+ const { hasFinishedResolution } = useGoogleAdsAccount();
+ const isGoogleAdsReady = useGoogleAdsAccountReady();
/**
* When jetpack is loading, or when google account is loading,
@@ -110,23 +112,6 @@ const SetupAccounts = ( props ) => {
return ;
}
- // Ads is ready when we have a connection and verified and verified access.
- // Billing is not required, and the 'link_merchant' step will be resolved
- // when the MC the account is connected.
- const isGoogleAdsReady =
- hasGoogleAdsConnection &&
- hasAccess &&
- [ '', 'billing', 'link_merchant' ].includes( step );
-
- // MC is ready when we have a connection and preconditions are met.
- // The `link_ads` step will be resolved when the Ads account is connected
- // since these can be connected in any order.
- const isGoogleMCReady =
- isGMCPreconditionReady &&
- ( googleMCAccount?.status === 'connected' ||
- ( googleMCAccount?.status === 'incomplete' &&
- googleMCAccount?.step === 'link_ads' ) );
-
const isContinueButtonDisabled = ! (
hasFinishedResolution &&
isGoogleAdsReady &&
diff --git a/tests/e2e/specs/setup-mc/step-1-accounts.test.js b/tests/e2e/specs/setup-mc/step-1-accounts.test.js
index 1e8b0cf552..04b3f1cc1a 100644
--- a/tests/e2e/specs/setup-mc/step-1-accounts.test.js
+++ b/tests/e2e/specs/setup-mc/step-1-accounts.test.js
@@ -2,7 +2,6 @@
* Internal dependencies
*/
import SetUpAccountsPage from '../../utils/pages/setup-mc/step-1-set-up-accounts';
-import SetupAdsAccountPage from '../../utils/pages/setup-ads/setup-ads-accounts';
import { LOAD_STATE } from '../../utils/constants';
import {
getFAQPanelTitle,
@@ -24,11 +23,6 @@ test.describe.configure( { mode: 'serial' } );
*/
let setUpAccountsPage = null;
-/**
- * @type {import('../../utils/pages/setup-ads/setup-ads-accounts.js').default} setupAdsAccountPage
- */
-let setupAdsAccountPage = null;
-
/**
* @type {import('@playwright/test').Page} page
*/
@@ -38,7 +32,6 @@ test.describe( 'Set up accounts', () => {
test.beforeAll( async ( { browser } ) => {
page = await browser.newPage();
setUpAccountsPage = new SetUpAccountsPage( page );
- setupAdsAccountPage = new SetupAdsAccountPage( page );
} );
test.afterAll( async () => {
@@ -218,6 +211,68 @@ test.describe( 'Set up accounts', () => {
expect( page.url() ).toMatch( baseURL + 'google_auth' );
} );
+
+ test( 'should create merchant center and ads account if does not exist for the user', async () => {
+ 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();
+ await once.mockMCHasNoAccounts();
+ await once.mockAdsAccountDisconnected();
+ await once.mockMCNotConnected();
+
+ await setUpAccountsPage.goto();
+ const googleAccountCard = setUpAccountsPage.getGoogleAccountCard();
+
+ await expect(
+ googleAccountCard.getByText(
+ 'You don’t have Merchant Center nor Google Ads accounts, so we’re creating them for you.',
+ {
+ exact: true,
+ }
+ )
+ ).toBeVisible();
+ } );
+
+ test.describe( 'After connecting Google account', () => {
+ test.beforeEach( async () => {
+ await setUpAccountsPage.mockJetpackConnected();
+ await setUpAccountsPage.mockGoogleConnected();
+ await setUpAccountsPage.mockMCConnected();
+ await setUpAccountsPage.mockAdsAccountConnected();
+
+ await setUpAccountsPage.goto();
+ } );
+
+ test( 'should see the merchant center id and ads account id if connected', async () => {
+ await setUpAccountsPage.mockAdsStatusClaimed();
+
+ const googleAccountCard =
+ setUpAccountsPage.getGoogleAccountCard();
+ await expect(
+ googleAccountCard.getByText( 'Merchant Center ID: 1234', {
+ exact: true,
+ } )
+ ).toBeVisible();
+
+ await expect(
+ googleAccountCard.getByText( 'Google Ads ID: 12345', {
+ exact: true,
+ } )
+ ).toBeVisible();
+
+ await expect(
+ googleAccountCard.getByText( 'Connected', { exact: true } )
+ ).toBeVisible();
+ } );
+ } );
} );
test.describe( 'Continue button', () => {
@@ -234,7 +289,7 @@ test.describe( 'Set up accounts', () => {
test.describe( 'When only Ads is connected', async () => {
test.beforeAll( async () => {
- await setupAdsAccountPage.mockAdsAccountConnected();
+ await setUpAccountsPage.mockAdsAccountConnected();
await setUpAccountsPage.mockMCNotConnected();
await setUpAccountsPage.goto();
@@ -249,7 +304,7 @@ test.describe( 'Set up accounts', () => {
test.describe( 'When only MC is connected', async () => {
test.beforeAll( async () => {
- await setupAdsAccountPage.mockAdsAccountDisconnected();
+ await setUpAccountsPage.mockAdsAccountDisconnected();
await setUpAccountsPage.mockMCConnected();
await setUpAccountsPage.goto();
@@ -264,9 +319,9 @@ test.describe( 'Set up accounts', () => {
test.describe( 'When all accounts are connected', async () => {
test.beforeAll( async () => {
- await setupAdsAccountPage.mockAdsAccountConnected();
- await setupAdsAccountPage.mockAdsStatusClaimed();
+ await setUpAccountsPage.mockAdsAccountConnected();
await setUpAccountsPage.mockMCConnected();
+ await setUpAccountsPage.mockAdsAccountConnected();
await setUpAccountsPage.goto();
} );
diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js
index 050da0bc4e..29306ad059 100644
--- a/tests/e2e/utils/mock-requests.js
+++ b/tests/e2e/utils/mock-requests.js
@@ -11,6 +11,29 @@ export default class MockRequests {
this.page = page;
}
+ /**
+ * Fulfill a request multiple times.
+ *
+ * @param {number} times The number of times to fulfill the request.
+ * @return {this} A proxied instance intercepts the subsequent fulfillRequest calls to attach the `times` option.
+ */
+ fulfillTimes( times ) {
+ return new Proxy( this, {
+ get( target, property ) {
+ const value = Reflect.get( ...arguments );
+
+ if ( property === 'fulfillRequest' ) {
+ return function ( url, payload, status, methods ) {
+ const args = [ url, payload, status, methods, times ];
+ return value.apply( target, args );
+ };
+ }
+
+ return value;
+ },
+ } );
+ }
+
/**
* Fulfill a request with a payload.
*
@@ -18,24 +41,26 @@ export default class MockRequests {
* @param {Object} payload The payload to send.
* @param {number} status The HTTP status in the response.
* @param {Array} methods The HTTP methods in the request to be fulfill.
+ * @param {number} [times] The number of times to fulfill the request. Optional.
* @return {Promise}
*/
- async fulfillRequest( url, payload, status = 200, methods = [] ) {
- await this.page.route( url, ( route ) => {
+ async fulfillRequest( url, payload, status = 200, methods = [], times ) {
+ const handler = async ( route ) => {
if (
methods.length === 0 ||
methods.includes( route.request().method() )
) {
- route.fulfill( {
+ return route.fulfill( {
status,
- content: 'application/json',
+ contentType: 'application/json',
headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify( payload ),
} );
- } else {
- route.fallback();
}
- } );
+ return route.fallback();
+ };
+
+ await this.page.route( url, handler, { times } );
}
/**