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() ) );