diff --git a/assets/js/components/email-reporting/AnalyticsDisconnectedNotice.js b/assets/js/components/email-reporting/AnalyticsDisconnectedNotice.js
new file mode 100644
index 00000000000..7c14b18d0af
--- /dev/null
+++ b/assets/js/components/email-reporting/AnalyticsDisconnectedNotice.js
@@ -0,0 +1,99 @@
+/**
+ * AnalyticsDisconnectedNotice component.
+ *
+ * Site Kit by Google, Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useCallback } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { useDispatch, useSelect } from 'googlesitekit-data';
+import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants';
+import Notice from '@/js/components/Notice';
+import { TYPES } from '@/js/components/Notice/constants';
+import { CORE_SITE } from '@/js/googlesitekit/datastore/site/constants';
+import { CORE_MODULES } from '@/js/googlesitekit/modules/datastore/constants';
+import { MODULE_SLUG_ANALYTICS_4 } from '@/js/modules/analytics-4/constants';
+import useActivateModuleCallback from '@/js/hooks/useActivateModuleCallback';
+
+export const EMAIL_REPORTING_ANALYTICS_DISCONNECTED_NOTICE_DISMISSED_ITEM =
+ 'email-reporting-analytics-disconnected-notice';
+
+export default function AnalyticsDisconnectedNotice() {
+ const isEmailReportingEnabled = useSelect( ( select ) =>
+ select( CORE_SITE ).isEmailReportingEnabled()
+ );
+
+ const isAnalyticsConnected = useSelect( ( select ) =>
+ select( CORE_MODULES ).isModuleConnected( MODULE_SLUG_ANALYTICS_4 )
+ );
+
+ const wasAnalyticsConnected = useSelect( ( select ) =>
+ select( CORE_SITE ).getWasAnalytics4Connected()
+ );
+
+ const isDismissed = useSelect( ( select ) =>
+ select( CORE_USER ).isItemDismissed(
+ EMAIL_REPORTING_ANALYTICS_DISCONNECTED_NOTICE_DISMISSED_ITEM
+ )
+ );
+
+ const { dismissItem } = useDispatch( CORE_USER );
+
+ const activateAnalytics = useActivateModuleCallback(
+ MODULE_SLUG_ANALYTICS_4
+ );
+
+ const handleDismiss = useCallback( async () => {
+ await dismissItem(
+ EMAIL_REPORTING_ANALYTICS_DISCONNECTED_NOTICE_DISMISSED_ITEM
+ );
+ }, [ dismissItem ] );
+
+ if (
+ ! isEmailReportingEnabled ||
+ isDismissed !== false ||
+ isAnalyticsConnected ||
+ ! wasAnalyticsConnected
+ ) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/assets/js/components/email-reporting/AnalyticsDisconnectedNotice.test.js b/assets/js/components/email-reporting/AnalyticsDisconnectedNotice.test.js
new file mode 100644
index 00000000000..1c6768f4842
--- /dev/null
+++ b/assets/js/components/email-reporting/AnalyticsDisconnectedNotice.test.js
@@ -0,0 +1,167 @@
+/**
+ * AnalyticsDisconnectedNotice component tests.
+ *
+ * Site Kit by Google, Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import { waitFor } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import AnalyticsDisconnectedNotice, {
+ EMAIL_REPORTING_ANALYTICS_DISCONNECTED_NOTICE_DISMISSED_ITEM,
+} from './AnalyticsDisconnectedNotice';
+import {
+ createTestRegistry,
+ render,
+ fireEvent,
+ provideModules,
+ provideUserCapabilities,
+ provideUserAuthentication,
+ provideModuleRegistrations,
+ provideSiteInfo,
+} from '../../../../tests/js/test-utils';
+import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants';
+import { MODULE_SLUG_ANALYTICS_4 } from '@/js/modules/analytics-4/constants';
+import { CORE_SITE } from '@/js/googlesitekit/datastore/site/constants';
+import { mockLocation } from '../../../../tests/js/mock-browser-utils';
+
+describe( 'AnalyticsDisconnectedNotice', () => {
+ mockLocation();
+ let registry;
+
+ const fetchDismissItem = new RegExp(
+ '^/google-site-kit/v1/core/user/data/dismiss-item'
+ );
+ const fetchGetDismissedItems = new RegExp(
+ '^/google-site-kit/v1/core/user/data/dismissed-items'
+ );
+
+ beforeEach( () => {
+ registry = createTestRegistry();
+ provideSiteInfo( registry );
+ provideUserAuthentication( registry );
+ provideUserCapabilities( registry );
+ provideModules( registry, [
+ {
+ slug: MODULE_SLUG_ANALYTICS_4,
+ active: false,
+ connected: false,
+ },
+ ] );
+ provideModuleRegistrations( registry );
+
+ registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] );
+ registry.dispatch( CORE_SITE ).receiveGetEmailReportingSettings( {
+ enabled: true,
+ } );
+ registry.dispatch( CORE_SITE ).receiveGetWasAnalytics4Connected( true );
+ } );
+
+ it( 'renders the notice when email reporting is enabled, analytics is disconnected but was once connected and notice is not dismissed', () => {
+ const { getByText } = render( , {
+ registry,
+ } );
+
+ // Title and description should be present.
+ expect( getByText( /Analytics is disconnected/i ) ).toBeInTheDocument();
+ expect(
+ getByText(
+ /Email reports won’t include Analytics data and metrics/i
+ )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'renders the "Reconnect Analytics" button and activates the module on click', async () => {
+ const moduleActivationEndpoint = RegExp(
+ 'google-site-kit/v1/core/modules/data/activation'
+ );
+
+ const userAuthenticationEndpoint = RegExp(
+ '^/google-site-kit/v1/core/user/data/authentication'
+ );
+
+ fetchMock.getOnce( userAuthenticationEndpoint, {
+ body: { needsReauthentication: false },
+ } );
+
+ fetchMock.postOnce( moduleActivationEndpoint, {
+ body: { success: true },
+ } );
+
+ const { getByRole } = render( , {
+ registry,
+ } );
+
+ fireEvent.click(
+ getByRole( 'button', {
+ name: /connect analytics/i,
+ } )
+ );
+
+ await waitFor( () =>
+ expect( fetchMock ).toHaveFetched( moduleActivationEndpoint )
+ );
+ } );
+
+ it( 'dismisses the notice when "Got it" is clicked', async () => {
+ fetchMock.getOnce( fetchGetDismissedItems, { body: [] } );
+ fetchMock.postOnce( fetchDismissItem, {
+ body: [
+ EMAIL_REPORTING_ANALYTICS_DISCONNECTED_NOTICE_DISMISSED_ITEM,
+ ],
+ } );
+
+ const { getByRole } = render( , {
+ registry,
+ } );
+
+ fireEvent.click( getByRole( 'button', { name: /got it/i } ) );
+
+ await waitFor( () =>
+ expect( fetchMock ).toHaveFetched( fetchDismissItem )
+ );
+ } );
+
+ it( 'does not render when email reporting is disabled', () => {
+ registry.dispatch( CORE_SITE ).receiveGetEmailReportingSettings( {
+ enabled: false,
+ } );
+
+ const { container } = render( , {
+ registry,
+ } );
+
+ expect( container ).toBeEmptyDOMElement();
+ } );
+
+ it( 'does not render when notice is dismissed', () => {
+ registry
+ .dispatch( CORE_USER )
+ .receiveGetDismissedItems( [
+ EMAIL_REPORTING_ANALYTICS_DISCONNECTED_NOTICE_DISMISSED_ITEM,
+ ] );
+
+ const { container } = render( , {
+ registry,
+ } );
+
+ expect( container ).toBeEmptyDOMElement();
+ } );
+} );
diff --git a/assets/js/components/settings/SettingsCardEmailReporting.test.js b/assets/js/components/settings/SettingsCardEmailReporting.test.js
index ff8ccf9b5f3..3082b37c02e 100644
--- a/assets/js/components/settings/SettingsCardEmailReporting.test.js
+++ b/assets/js/components/settings/SettingsCardEmailReporting.test.js
@@ -24,6 +24,8 @@ import {
createTestRegistry,
provideUserAuthentication,
freezeFetch,
+ provideModules,
+ provideUserCapabilities,
} from '../../../../tests/js/utils';
import { CORE_SITE } from '@/js/googlesitekit/datastore/site/constants';
import SettingsCardEmailReporting from './SettingsCardEmailReporting';
@@ -35,9 +37,12 @@ describe( 'SettingsCardEmailReporting', () => {
beforeEach( () => {
registry = createTestRegistry();
provideUserAuthentication( registry );
+ provideUserCapabilities( registry );
+ provideModules( registry );
// Prevent network request/resolver from running to avoid console errors.
registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] );
+ registry.dispatch( CORE_SITE ).receiveGetWasAnalytics4Connected( true );
} );
it( 'should render the layout with correct title', async () => {
diff --git a/assets/js/components/settings/SettingsEmailReporting.js b/assets/js/components/settings/SettingsEmailReporting.js
index 72a237a5af1..4c1fd97c6f1 100644
--- a/assets/js/components/settings/SettingsEmailReporting.js
+++ b/assets/js/components/settings/SettingsEmailReporting.js
@@ -42,6 +42,7 @@ import Typography from '@/js/components/Typography';
import EmailReportingCardNotice, {
EMAIL_REPORTING_CARD_NOTICE_DISMISSED_ITEM,
} from '@/js/components/email-reporting/EmailReportingCardNotice';
+import AnalyticsDisconnectedNotice from '@/js/components/email-reporting/AnalyticsDisconnectedNotice';
export default function SettingsEmailReporting( { loading = false } ) {
const isEnabled = useSelect( ( select ) =>
@@ -145,6 +146,7 @@ export default function SettingsEmailReporting( { loading = false } ) {
) }
+
);
}
diff --git a/assets/js/components/settings/SettingsEmailReporting.test.js b/assets/js/components/settings/SettingsEmailReporting.test.js
index cefbd176a20..ea19af3efe3 100644
--- a/assets/js/components/settings/SettingsEmailReporting.test.js
+++ b/assets/js/components/settings/SettingsEmailReporting.test.js
@@ -24,6 +24,8 @@ import {
createTestRegistry,
provideUserAuthentication,
freezeFetch,
+ provideUserCapabilities,
+ provideModules,
} from '../../../../tests/js/utils';
import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants';
import { CORE_SITE } from '@/js/googlesitekit/datastore/site/constants';
@@ -37,9 +39,12 @@ describe( 'SettingsEmailReporting', () => {
beforeEach( () => {
registry = createTestRegistry();
provideUserAuthentication( registry );
+ provideUserCapabilities( registry );
+ provideModules( registry );
// Prevent network request/resolver from running to avoid console errors.
registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] );
+ registry.dispatch( CORE_SITE ).receiveGetWasAnalytics4Connected( true );
} );
it( 'should render the toggle with correct label', () => {
diff --git a/assets/js/googlesitekit/datastore/site/email-reporting.js b/assets/js/googlesitekit/datastore/site/email-reporting.js
index cff74e72ef7..35de5a65ab9 100644
--- a/assets/js/googlesitekit/datastore/site/email-reporting.js
+++ b/assets/js/googlesitekit/datastore/site/email-reporting.js
@@ -77,6 +77,16 @@ const fetchSaveEmailReportingSettingsStore = createFetchStore( {
},
} );
+const fetchGetWasAnalytics4Connected = createFetchStore( {
+ baseName: 'getWasAnalytics4Connected',
+ controlCallback: () => {
+ return get( 'core', 'site', 'was-analytics-4-connected', undefined );
+ },
+ reducerCallback: createReducer( ( state, wasAnalytics4Connected ) => {
+ state.emailReporting.wasAnalytics4Connected = wasAnalytics4Connected;
+ } ),
+} );
+
// Actions
const SET_EMAIL_REPORTING_SETTINGS = 'SET_EMAIL_REPORTING_SETTINGS';
@@ -150,6 +160,17 @@ const baseResolvers = {
yield fetchGetEmailReportingSettingsStore.actions.fetchGetEmailReportingSettings();
}
},
+ *getWasAnalytics4Connected() {
+ const registry = yield commonActions.getRegistry();
+
+ const wasAnalytics4Connected = registry
+ .select( CORE_SITE )
+ .getWasAnalytics4Connected();
+
+ if ( wasAnalytics4Connected === undefined ) {
+ yield fetchGetWasAnalytics4Connected.actions.fetchGetWasAnalytics4Connected();
+ }
+ },
};
const baseSelectors = {
@@ -177,11 +198,24 @@ const baseSelectors = {
const settings = state.emailReporting?.settings;
return !! settings?.enabled;
},
+
+ /**
+ * Gets whether Analytics 4 was ever connected.
+ *
+ * @since n.e.x.t
+ *
+ * @param {Object} state Data store's state.
+ * @return {(boolean|undefined)} TRUE if Analytics 4 was connected, FALSE if not, or `undefined` if not loaded yet.
+ */
+ getWasAnalytics4Connected( state ) {
+ return state.emailReporting?.wasAnalytics4Connected;
+ },
};
const store = combineStores(
fetchGetEmailReportingSettingsStore,
fetchSaveEmailReportingSettingsStore,
+ fetchGetWasAnalytics4Connected,
{
initialState: baseInitialState,
actions: baseActions,
diff --git a/assets/js/googlesitekit/datastore/site/email-reporting.test.js b/assets/js/googlesitekit/datastore/site/email-reporting.test.js
index 0bdbd0162d8..a2f2923bd03 100644
--- a/assets/js/googlesitekit/datastore/site/email-reporting.test.js
+++ b/assets/js/googlesitekit/datastore/site/email-reporting.test.js
@@ -273,5 +273,74 @@ describe( 'core/site Email Reporting', () => {
).toBe( false );
} );
} );
+
+ describe( 'getWasAnalytics4Connected', () => {
+ it( 'uses a resolver to make a network request', async () => {
+ fetchMock.getOnce(
+ /^\/google-site-kit\/v1\/core\/site\/data\/was-analytics-4-connected/,
+ {
+ body: true,
+ status: 200,
+ }
+ );
+
+ const initialValue = registry
+ .select( CORE_SITE )
+ .getWasAnalytics4Connected();
+
+ expect( initialValue ).toBeUndefined();
+
+ await untilResolved(
+ registry,
+ CORE_SITE
+ ).getWasAnalytics4Connected();
+
+ const value = registry
+ .select( CORE_SITE )
+ .getWasAnalytics4Connected();
+
+ expect( value ).toBe( true );
+
+ expect( fetchMock ).toHaveFetched(
+ /^\/google-site-kit\/v1\/core\/site\/data\/was-analytics-4-connected/
+ );
+ } );
+
+ it( 'returns undefined if the request fails', async () => {
+ fetchMock.getOnce(
+ /^\/google-site-kit\/v1\/core\/site\/data\/was-analytics-4-connected/,
+ {
+ body: { error: 'something went wrong' },
+ status: 500,
+ }
+ );
+
+ const initialValue = registry
+ .select( CORE_SITE )
+ .getWasAnalytics4Connected();
+
+ expect( initialValue ).toBeUndefined();
+
+ await untilResolved(
+ registry,
+ CORE_SITE
+ ).getWasAnalytics4Connected();
+
+ const value = registry
+ .select( CORE_SITE )
+ .getWasAnalytics4Connected();
+
+ // Verify the value is still undefined after the selector is resolved.
+ expect( value ).toBeUndefined();
+
+ await waitForDefaultTimeouts();
+
+ expect( fetchMock ).toHaveFetched(
+ /^\/google-site-kit\/v1\/core\/site\/data\/was-analytics-4-connected/
+ );
+
+ expect( console ).toHaveErrored();
+ } );
+ } );
} );
} );
diff --git a/assets/sass/components/settings/_googlesitekit-settings-email-reporting.scss b/assets/sass/components/settings/_googlesitekit-settings-email-reporting.scss
index 821dd952eac..dd00d713340 100644
--- a/assets/sass/components/settings/_googlesitekit-settings-email-reporting.scss
+++ b/assets/sass/components/settings/_googlesitekit-settings-email-reporting.scss
@@ -39,5 +39,9 @@
.googlesitekit-settings-email-reporting__manage {
margin-top: $grid-gap-desktop;
}
+
+ .googlesitekit-email-reporting__analytics-disconnected-notice {
+ margin-top: $grid-gap-desktop;
+ }
}
}
diff --git a/includes/Core/Email_Reporting/Email_Reporting.php b/includes/Core/Email_Reporting/Email_Reporting.php
index 2d36386102e..f8cbdcf1e77 100644
--- a/includes/Core/Email_Reporting/Email_Reporting.php
+++ b/includes/Core/Email_Reporting/Email_Reporting.php
@@ -157,7 +157,7 @@ public function __construct(
$max_execution_limiter = new Max_Execution_Limiter( (int) ini_get( 'max_execution_time' ) );
$batch_query = new Email_Log_Batch_Query();
- $this->rest_controller = new REST_Email_Reporting_Controller( $this->settings );
+ $this->rest_controller = new REST_Email_Reporting_Controller( $this->settings, $this->options );
$this->email_log = new Email_Log( $this->context );
$this->scheduler = new Email_Reporting_Scheduler( $frequency_planner );
$this->initiator_task = new Initiator_Task( $this->scheduler, $subscribed_users_query );
diff --git a/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php b/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php
index 130f4ae148b..78e72608453 100644
--- a/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php
+++ b/includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php
@@ -13,6 +13,7 @@
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\REST_Route;
use Google\Site_Kit\Core\REST_API\REST_Routes;
+use Google\Site_Kit\Core\Storage\Options;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
@@ -34,15 +35,25 @@ class REST_Email_Reporting_Controller {
*/
private $settings;
+ /**
+ * Was_Analytics_4_Connected instance.
+ *
+ * @since n.e.x.t
+ * @var Was_Analytics_4_Connected
+ */
+ private $was_analytics_4_connected;
+
/**
* Constructor.
*
* @since 1.162.0
*
* @param Email_Reporting_Settings $settings Email_Reporting_Settings instance.
+ * @param Options $options Options instance.
*/
- public function __construct( Email_Reporting_Settings $settings ) {
- $this->settings = $settings;
+ public function __construct( Email_Reporting_Settings $settings, Options $options ) {
+ $this->settings = $settings;
+ $this->was_analytics_4_connected = new Was_Analytics_4_Connected( $options );
}
/**
@@ -125,6 +136,18 @@ protected function get_rest_routes() {
),
)
),
+ new REST_Route(
+ 'core/site/data/was-analytics-4-connected',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => function () {
+ return new WP_REST_Response( $this->was_analytics_4_connected->get() );
+ },
+ 'permission_callback' => $can_access,
+ ),
+ )
+ ),
);
}
}
diff --git a/includes/Core/Email_Reporting/Was_Analytics_4_Connected.php b/includes/Core/Email_Reporting/Was_Analytics_4_Connected.php
new file mode 100644
index 00000000000..c64a68b5952
--- /dev/null
+++ b/includes/Core/Email_Reporting/Was_Analytics_4_Connected.php
@@ -0,0 +1,41 @@
+custom_dimensions_data_available->reset_data_available();
$this->reset_audiences->reset_audience_data();
$this->audience_settings->delete();
+
+ $was_analytics_4_connected = new Was_Analytics_4_Connected( $this->options );
+ $was_analytics_4_connected->set( true );
}
/**
diff --git a/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php b/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php
index 7f2f9d33707..22932051838 100644
--- a/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php
+++ b/tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php
@@ -45,7 +45,7 @@ public function set_up() {
$context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE );
$options = new Options( $context );
$this->settings = new Email_Reporting_Settings( $options );
- $this->controller = new REST_Email_Reporting_Controller( $this->settings );
+ $this->controller = new REST_Email_Reporting_Controller( $this->settings, $options );
}
public function tear_down() {
@@ -70,6 +70,7 @@ public function test_get_routes() {
$server = rest_get_server();
$routes = array(
'/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting',
+ '/' . REST_Routes::REST_ROOT . '/core/site/data/was-analytics-4-connected',
);
$get_routes = array_intersect( $routes, array_keys( $server->get_routes() ) );