diff --git a/.github/workflows/php-lint-tests.yml b/.github/workflows/php-lint-tests.yml index ba71f5cc352..e0496399e90 100644 --- a/.github/workflows/php-lint-tests.yml +++ b/.github/workflows/php-lint-tests.yml @@ -105,6 +105,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install SVN + run: sudo apt-get update && sudo apt-get install -y subversion + - uses: shivammathur/setup-php@v2 with: extensions: mysqli, runkit7, uopz diff --git a/assets/js/components/KeyMetrics/ChipTabGroup/Chip.js b/assets/js/components/KeyMetrics/ChipTabGroup/Chip.js index 2fb83574def..ef1d44866e5 100644 --- a/assets/js/components/KeyMetrics/ChipTabGroup/Chip.js +++ b/assets/js/components/KeyMetrics/ChipTabGroup/Chip.js @@ -26,8 +26,18 @@ import classnames from 'classnames'; * Internal dependencies */ import { Button } from 'googlesitekit-components'; -import { KEY_METRICS_CURRENT_SELECTION_GROUP_SLUG } from '../constants'; +import { + KEY_METRICS_GROUP_CURRENT, + KEY_METRICS_GROUP_SUGGESTED, +} from '../constants'; import CheckMark from '../../../../svg/icons/check-2.svg'; +import StarFill from '../../../../svg/icons/star-fill.svg'; +import Null from '../../../components/Null'; + +const icons = { + [ KEY_METRICS_GROUP_CURRENT.SLUG ]: CheckMark, + [ KEY_METRICS_GROUP_SUGGESTED.SLUG ]: StarFill, +}; export default function Chip( { slug, @@ -37,15 +47,19 @@ export default function Chip( { hasNewBadge = false, selectedCount = 0, } ) { + const Icon = icons[ slug ] || Null; + return ( + ) } + { ctaLabel && ( + ) } - ) } -
-

{ title }

- { description && ( -

- { description } -

- ) } -
-
- { dismissLabel && ( - - ) } - { ctaLabel && ( - - ) }
- - ); -} + ); + } +); SubtleNotification.propTypes = { title: PropTypes.node.isRequired, diff --git a/assets/js/components/settings/SettingsActiveModule/Footer.js b/assets/js/components/settings/SettingsActiveModule/Footer.js index 9bc29c5dfdd..ff9ac93535d 100644 --- a/assets/js/components/settings/SettingsActiveModule/Footer.js +++ b/assets/js/components/settings/SettingsActiveModule/Footer.js @@ -58,6 +58,9 @@ export default function Footer( props ) { const dialogActiveKey = `module-${ slug }-dialogActive`; const isSavingKey = `module-${ slug }-isSaving`; + const areSettingsEditDependenciesLoaded = useSelect( ( select ) => + select( CORE_MODULES ).areSettingsEditDependenciesLoaded( slug ) + ); const canSubmitChanges = useSelect( ( select ) => select( CORE_MODULES ).canSubmitChanges( slug ) ); @@ -147,30 +150,6 @@ export default function Footer( props ) { ); }, [ slug, viewContext ] ); - // Check if the resolution for the specified selector has finished. - // This allows us to determine if the data needed by the module is still being loaded. - // The primary reason for this loading check is to disable the submit button - // while the necessary data for the settings is still being loaded, preventing - // premature interactions by the user. - const isLoading = useSelect( ( select ) => { - const resolutionMapping = { - 'analytics-4': 'getAccountSummaries', - tagmanager: 'getAccounts', - 'search-console': 'getMatchedProperties', - }; - const resolutionSelector = resolutionMapping[ slug ]; - - if ( ! module || ! resolutionSelector ) { - return false; - } - - const storeName = module.storeName; - - return ! select( storeName ).hasFinishedResolution( - resolutionSelector - ); - } ); - let buttonText = __( 'Save', 'google-site-kit' ); if ( haveSettingsChanged ) { @@ -195,7 +174,7 @@ export default function Footer( props ) { { + if ( ! isViewed && inView ) { + // Handle internal tracking. + trackEvent( + `${ viewContext }_kmw-settings-suggested-site-purpose-edit-notification`, + 'view_notification', + 'conversion_reporting' + ); + + setIsViewed( true ); + } + }, [ isViewed, inView, viewContext ] ); const isDismissed = useSelect( ( select ) => select( CORE_USER ).isItemDismissed( @@ -43,7 +72,13 @@ export default function KeyMetricsSettingsSellProductsSubtleNotification() { const onDismiss = useCallback( async () => { await dismissItem( USER_INPUT_LEGACY_SITE_PURPOSE_DISMISSED_ITEM_KEY ); - }, [ dismissItem ] ); + // Handle internal tracking. + trackEvent( + `${ viewContext }_kmw-settings-suggested-site-purpose-edit-notification`, + 'confirm_notification', + 'conversion_reporting' + ); + }, [ dismissItem, viewContext ] ); if ( isDismissed ) { return null; @@ -51,6 +86,7 @@ export default function KeyMetricsSettingsSellProductsSubtleNotification() { return ( - select( CORE_USER ).getUserPickedMetrics() - ); - const { saveUserInputSettings, resetKeyMetricsSelection } = - useDispatch( CORE_USER ); + const { saveUserInputSettings } = useDispatch( CORE_USER ); const { navigateTo } = useDispatch( CORE_LOCATION ); const dashboardURL = useSelect( ( select ) => @@ -191,9 +187,6 @@ export default function UserInputQuestionnaire() { const response = await saveUserInputSettings(); if ( ! response.error ) { - if ( !! userPickedMetrics ) { - await resetKeyMetricsSelection(); - } const url = new URL( dashboardURL ); navigateTo( url.toString() ); } @@ -205,8 +198,6 @@ export default function UserInputQuestionnaire() { setUserInputSetting, navigateTo, isConversionReportingEnabled, - userPickedMetrics, - resetKeyMetricsSelection, ] ); const settings = useSelect( ( select ) => diff --git a/assets/js/googlesitekit/api/middleware/preloading.js b/assets/js/googlesitekit/api/middleware/preloading.js index 03105d25ff6..7c18e5acd92 100644 --- a/assets/js/googlesitekit/api/middleware/preloading.js +++ b/assets/js/googlesitekit/api/middleware/preloading.js @@ -45,7 +45,7 @@ function createPreloadingMiddleware( preloadedData ) { } setTimeout( () => { cacheHasExpired = true; - }, 1000 ); + }, 3000 ); const { parse = true } = options; const uri = options.path; diff --git a/assets/js/googlesitekit/datastore/user/conversion-reporting-settings.js b/assets/js/googlesitekit/datastore/user/conversion-reporting-settings.js new file mode 100644 index 00000000000..42568ce1932 --- /dev/null +++ b/assets/js/googlesitekit/datastore/user/conversion-reporting-settings.js @@ -0,0 +1,228 @@ +/** + * Site Kit by Google, Copyright 2024 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 invariant from 'invariant'; +import { isPlainObject } from 'lodash'; + +/** + * Internal dependencies + */ +import API from 'googlesitekit-api'; +import { + createRegistrySelector, + commonActions, + combineStores, +} from 'googlesitekit-data'; +import { CORE_USER } from './constants'; +import { createFetchStore } from '../../data/create-fetch-store'; +import { createValidatedAction } from '../../data/utils'; + +const baseInitialState = { + conversionReportingSettings: undefined, +}; + +const fetchGetConversionReportingSettingsStore = createFetchStore( { + baseName: 'getConversionReportingSettings', + controlCallback: () => + API.get( 'core', 'user', 'conversion-reporting-settings', undefined, { + // Never cache conversion reporting settings requests, we want them to be + // up-to-date with what's in settings, and they don't + // make requests to Google APIs so it's not a slow request. + useCache: false, + } ), + reducerCallback: ( state, conversionReportingSettings ) => ( { + ...state, + conversionReportingSettings, + } ), +} ); + +const fetchSaveConversionReportingSettingsStore = createFetchStore( { + baseName: 'saveConversionReportingSettings', + controlCallback: ( settings ) => + API.set( 'core', 'user', 'conversion-reporting-settings', { + settings, + } ), + reducerCallback: ( state, conversionReportingSettings ) => ( { + ...state, + conversionReportingSettings, + } ), + argsToParams: ( settings ) => settings, + validateParams: ( settings ) => { + invariant( + isPlainObject( settings ), + 'Conversion reporting settings should be an object.' + ); + if ( settings.newEventsCalloutDismissedAt ) { + invariant( + Number.isInteger( settings.newEventsCalloutDismissedAt ), + 'newEventsCalloutDismissedAt should be a timestamp.' + ); + } + if ( settings.lostEventsCalloutDismissedAt ) { + invariant( + Number.isInteger( settings.lostEventsCalloutDismissedAt ), + 'lostEventsCalloutDismissedAt should be an integer.' + ); + } + }, +} ); + +const baseActions = { + /** + * Saves the conversion reporting settings. + * + * @since n.e.x.t + * + * @param {Object} settings Optional. By default, this saves whatever there is in the store. Use this object to save additional settings. + * @return {Object} Object with `response` and `error`. + */ + saveConversionReportingSettings: createValidatedAction( + ( settings = {} ) => { + invariant( + isPlainObject( settings ), + 'Conversion reporting settings should be an object to save.' + ); + }, + function* ( settings = {} ) { + return yield fetchSaveConversionReportingSettingsStore.actions.fetchSaveConversionReportingSettings( + settings + ); + } + ), +}; + +const baseResolvers = { + *getConversionReportingSettings() { + const registry = yield commonActions.getRegistry(); + + const conversionReportingSettings = registry + .select( CORE_USER ) + .getConversionReportingSettings(); + + if ( conversionReportingSettings === undefined ) { + yield fetchGetConversionReportingSettingsStore.actions.fetchGetConversionReportingSettings(); + } + }, +}; + +const baseSelectors = { + /** + * Gets the conversion reporting settings. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @return {(Object|undefined)} Conversion reporting settings; `undefined` if not loaded. + */ + getConversionReportingSettings( state ) { + return state.conversionReportingSettings; + }, + + /** + * Determines whether the conversion reporting settings are being saved or not. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @return {boolean} TRUE if the key metrics settings are being saved, otherwise FALSE. + */ + isSavingConversionReportingSettings( state ) { + // Since isFetchingSaveConversionReportingSettings holds information based on specific values but we only need + // generic information here, we need to check whether ANY such request is in progress. + return Object.values( + state.isFetchingSaveConversionReportingSettings + ).some( Boolean ); + }, + + /** + * Determines whether the new events callout should be shown or not. + * + * @since n.e.x.t + * + * @return {boolean} TRUE if the there were new events detected after the callout was dismissed, otherwise FALSE. + */ + haveNewConversionEventsAfterDismiss: createRegistrySelector( + ( select ) => ( state, newEventsLastSyncedAt ) => { + const { getConversionReportingSettings } = select( CORE_USER ); + const conversionReportingSettings = + getConversionReportingSettings(); + + if ( ! conversionReportingSettings ) { + return false; + } + + if ( + newEventsLastSyncedAt > + conversionReportingSettings.newEventsCalloutDismissedAt + ) { + return true; + } + + return false; + } + ), + + /** + * Determines whether the lost events callout should be shown or not. + * + * @since n.e.x.t + * + * @return {boolean} TRUE if the there were lost events detected after the callout was dismissed, otherwise FALSE. + */ + haveLostConversionEventsAfterDismiss: createRegistrySelector( + ( select ) => ( state, lostEventsLastSyncedAt ) => { + const { getConversionReportingSettings } = select( CORE_USER ); + const conversionReportingSettings = + getConversionReportingSettings(); + + if ( ! conversionReportingSettings ) { + return false; + } + + if ( + lostEventsLastSyncedAt > + conversionReportingSettings.lostEventsCalloutDismissedAt + ) { + return true; + } + + return false; + } + ), +}; + +const store = combineStores( + fetchGetConversionReportingSettingsStore, + fetchSaveConversionReportingSettingsStore, + { + initialState: baseInitialState, + actions: baseActions, + resolvers: baseResolvers, + selectors: baseSelectors, + } +); + +export const initialState = store.initialState; +export const actions = store.actions; +export const controls = store.controls; +export const reducer = store.reducer; +export const resolvers = store.resolvers; +export const selectors = store.selectors; + +export default store; diff --git a/assets/js/googlesitekit/datastore/user/conversion-reporting-settings.test.js b/assets/js/googlesitekit/datastore/user/conversion-reporting-settings.test.js new file mode 100644 index 00000000000..92b2fdc031e --- /dev/null +++ b/assets/js/googlesitekit/datastore/user/conversion-reporting-settings.test.js @@ -0,0 +1,271 @@ +/** + * `modules/analytics-4` data store: conversion reporting settings tests. + * + * Site Kit by Google, Copyright 2024 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. + */ + +/** + * Internal dependencies + */ +import API from 'googlesitekit-api'; +import { CORE_USER } from './constants'; +import { + createTestRegistry, + freezeFetch, + muteFetch, + provideModules, + untilResolved, + waitForDefaultTimeouts, +} from '../../../../../tests/js/utils'; + +describe( 'core/user conversion reporting settings', () => { + let registry; + let store; + + const conversionReportingSettingsEndpoint = new RegExp( + '^/google-site-kit/v1/core/user/data/conversion-reporting-settings' + ); + + let conversionReportingSettingsResponse; + + beforeAll( () => { + API.setUsingCache( false ); + } ); + + beforeEach( () => { + registry = createTestRegistry(); + provideModules( registry ); + store = registry.stores[ CORE_USER ].store; + + conversionReportingSettingsResponse = { + newEventsCalloutDismissedAt: 0, + lostEventsCalloutDismissedAt: 0, + }; + } ); + + afterAll( () => { + API.setUsingCache( true ); + } ); + + describe( 'actions', () => { + describe( 'saveConversionReportingSettings', () => { + it( 'should save settings and add it to the store', async () => { + const newEventsCalloutDismissedAt = 1734470013; + + fetchMock.postOnce( conversionReportingSettingsEndpoint, { + body: { + ...conversionReportingSettingsResponse, + newEventsCalloutDismissedAt, + }, + } ); + + await registry + .dispatch( CORE_USER ) + .saveConversionReportingSettings( { + newEventsCalloutDismissedAt, + } ); + + // Ensure the proper body parameters were sent. + expect( fetchMock ).toHaveFetched( + conversionReportingSettingsEndpoint, + { + body: { + data: { + settings: { newEventsCalloutDismissedAt }, + }, + }, + } + ); + + const conversionReportingSettings = + store.getState().conversionReportingSettings; + + expect( conversionReportingSettings ).toEqual( { + ...conversionReportingSettingsResponse, + newEventsCalloutDismissedAt, + } ); + expect( fetchMock ).toHaveFetchedTimes( 1 ); + } ); + + it( 'dispatches an error if the request fails', async () => { + const response = { + code: 'internal_server_error', + message: 'Internal server error', + data: { status: 500 }, + }; + + fetchMock.post( conversionReportingSettingsEndpoint, { + body: response, + status: 500, + } ); + + const settingsPartial = { + newEventsCalloutDismissedAt: 1734466109, + }; + + await registry + .dispatch( CORE_USER ) + .saveConversionReportingSettings( settingsPartial ); + + expect( + registry + .select( CORE_USER ) + .getErrorForAction( 'saveConversionReportingSettings', [ + settingsPartial, + ] ) + ).toMatchObject( response ); + + expect( console ).toHaveErrored(); + } ); + } ); + } ); + + describe( 'selectors', () => { + describe( 'getConversionReportingSettings', () => { + it( 'should return undefined while conversion reporting settings are loading', async () => { + freezeFetch( conversionReportingSettingsEndpoint ); + + expect( + registry + .select( CORE_USER ) + .getConversionReportingSettings() + ).toBeUndefined(); + + await waitForDefaultTimeouts(); + } ); + + it( 'should not make a network request if conversion reporting settings exist', async () => { + registry + .dispatch( CORE_USER ) + .receiveGetConversionReportingSettings( + conversionReportingSettingsResponse + ); + + registry.select( CORE_USER ).getConversionReportingSettings(); + + await untilResolved( + registry, + CORE_USER + ).getConversionReportingSettings(); + + expect( fetchMock ).not.toHaveFetched( + conversionReportingSettingsEndpoint + ); + } ); + + it( 'should use a resolver to make a network request if data is not available', async () => { + fetchMock.getOnce( conversionReportingSettingsEndpoint, { + body: conversionReportingSettingsResponse, + status: 200, + } ); + + registry.select( CORE_USER ).getConversionReportingSettings(); + + await untilResolved( + registry, + CORE_USER + ).getConversionReportingSettings(); + + expect( fetchMock ).toHaveFetched( + conversionReportingSettingsEndpoint, + { + body: { + settings: conversionReportingSettingsResponse, + }, + } + ); + + expect( + registry + .select( CORE_USER ) + .getConversionReportingSettings() + ).toMatchObject( conversionReportingSettingsResponse ); + + expect( fetchMock ).toHaveFetchedTimes( 1 ); + } ); + } ); + + describe( 'isSavingConversionReportingSettings', () => { + it( 'should return false if conversion reporting settings are not being saved', () => { + expect( + registry + .select( CORE_USER ) + .isSavingConversionReportingSettings() + ).toBe( false ); + } ); + + it( 'should return true if conversion reporting settings are being saved', async () => { + muteFetch( conversionReportingSettingsEndpoint ); + + const promise = registry + .dispatch( CORE_USER ) + .fetchSaveConversionReportingSettings( + conversionReportingSettingsResponse + ); + + expect( + registry + .select( CORE_USER ) + .isSavingConversionReportingSettings() + ).toBe( true ); + + await promise; + + expect( + registry + .select( CORE_USER ) + .isSavingConversionReportingSettings() + ).toBe( false ); + } ); + } ); + + describe.each( [ + [ 'haveNewConversionEventsAfterDismiss' ], + [ 'haveLostConversionEventsAfterDismiss' ], + ] )( '%s', ( selector ) => { + it.each( [ + [ + true, + 'after the callout was dismissed', + 1734512930, + 1734253730, + ], + [ true, 'and the callout was never dismissed', 1734512930, 0 ], + [ + false, + 'before the callout was dismissed', + 1734253730, + 1734512930, + ], + ] )( + 'should return %s when most recent new events sync happened %s', + ( expected, _, eventsLastSyncedAt, calloutDismissedAt ) => { + registry + .dispatch( CORE_USER ) + .receiveGetConversionReportingSettings( { + newEventsCalloutDismissedAt: calloutDismissedAt, + lostEventsCalloutDismissedAt: calloutDismissedAt, + } ); + + const result = registry + .select( CORE_USER ) + [ selector ]( eventsLastSyncedAt ); + + expect( result ).toBe( expected ); + } + ); + } ); + } ); +} ); diff --git a/assets/js/googlesitekit/datastore/user/index.js b/assets/js/googlesitekit/datastore/user/index.js index 931b696e509..0708d694b49 100644 --- a/assets/js/googlesitekit/datastore/user/index.js +++ b/assets/js/googlesitekit/datastore/user/index.js @@ -39,6 +39,7 @@ import surveys from './surveys'; import tracking from './tracking'; import userInfo from './user-info'; import userInputSettings from './user-input-settings'; +import conversionReportingSettings from './conversion-reporting-settings'; const store = combineStores( commonStore, @@ -59,7 +60,8 @@ const store = combineStores( surveys, tracking, userInfo, - userInputSettings + userInputSettings, + conversionReportingSettings ); export const { diff --git a/assets/js/googlesitekit/datastore/user/key-metrics.js b/assets/js/googlesitekit/datastore/user/key-metrics.js index 3b24e8c6244..febc00df131 100644 --- a/assets/js/googlesitekit/datastore/user/key-metrics.js +++ b/assets/js/googlesitekit/datastore/user/key-metrics.js @@ -104,16 +104,6 @@ const fetchSaveKeyMetricsSettingsStore = createFetchStore( { }, } ); -const fetchResetKeyMetricsSelectionStore = createFetchStore( { - baseName: 'resetKeyMetricsSelection', - controlCallback: () => - API.set( 'core', 'user', 'reset-key-metrics-selection' ), - reducerCallback: ( state, keyMetricsSettings ) => ( { - ...state, - keyMetricsSettings, - } ), -} ); - const baseActions = { /** * Sets key metrics setting. @@ -179,33 +169,6 @@ const baseActions = { return { response, error }; }, - - /** - * Resets key metrics selecton. - * - * @since 1.141.0 - * - * @param {Object} settings Optional. By default, this saves whatever there is in the store. Use this object to save additional settings. - * @return {Object} Object with `response` and `error`. - */ - *resetKeyMetricsSelection( settings = {} ) { - invariant( - isPlainObject( settings ), - 'key metric settings should be an object to save.' - ); - - yield clearError( 'resetKeyMetricsSelection', [] ); - - const { response, error } = - yield fetchResetKeyMetricsSelectionStore.actions.fetchResetKeyMetricsSelection(); - - if ( error ) { - // Store error manually since resetKeyMetricsSelection signature differs from fetchResetKeyMetricsSelectionStore. - yield receiveError( error, 'resetKeyMetricsSelection', [] ); - } - - return { response, error }; - }, }; const baseControls = {}; @@ -722,7 +685,6 @@ const baseSelectors = { const store = combineStores( fetchGetKeyMetricsSettingsStore, fetchSaveKeyMetricsSettingsStore, - fetchResetKeyMetricsSelectionStore, { initialState: baseInitialState, actions: baseActions, diff --git a/assets/js/googlesitekit/datastore/user/key-metrics.test.js b/assets/js/googlesitekit/datastore/user/key-metrics.test.js index 5d2d0e94280..af2642be4e2 100644 --- a/assets/js/googlesitekit/datastore/user/key-metrics.test.js +++ b/assets/js/googlesitekit/datastore/user/key-metrics.test.js @@ -91,14 +91,6 @@ describe( 'core/user key metrics', () => { }, }; - const coreKeyMetricsResetMetricSelectionEndpointRegExp = new RegExp( - '^/google-site-kit/v1/core/user/data/reset-key-metrics-selection' - ); - const coreKeyMetricsResetMetricSelectionExpectedResponse = { - widgetSlugs: [], - isWidgetHidden: false, - }; - beforeAll( () => { API.setUsingCache( false ); } ); @@ -824,53 +816,6 @@ describe( 'core/user key metrics', () => { expect( console ).not.toHaveErrored(); } ); } ); - - describe( 'resetKeyMetricsSelection', () => { - it( 'should clear widgetSlugs on resetKeyMetricsSelection', async () => { - const userID = 123; - provideUserInfo( registry, { id: userID } ); - provideUserAuthentication( registry ); - - fetchMock.postOnce( coreKeyMetricsEndpointRegExp, { - body: coreKeyMetricsExpectedResponse, - status: 200, - } ); - - fetchMock.postOnce( - coreKeyMetricsResetMetricSelectionEndpointRegExp, - { - body: coreKeyMetricsResetMetricSelectionExpectedResponse, - status: 200, - } - ); - - await registry - .dispatch( CORE_USER ) - .receiveGetKeyMetricsSettings( { - widgetSlugs: [ - KM_ANALYTICS_NEW_VISITORS, - KM_ANALYTICS_PAGES_PER_VISIT, - ], - isWidgetHidden: false, - } ); - - expect( store.getState().keyMetricsSettings ).toMatchObject( { - widgetSlugs: [ - KM_ANALYTICS_NEW_VISITORS, - KM_ANALYTICS_PAGES_PER_VISIT, - ], - isWidgetHidden: false, - } ); - - await registry.dispatch( CORE_USER ).resetKeyMetricsSelection(); - - expect( store.getState().keyMetricsSettings ).toMatchObject( { - widgetSlugs: [], - } ); - - expect( console ).not.toHaveErrored(); - } ); - } ); } ); describe( 'selectors', () => { diff --git a/assets/js/googlesitekit/modules/datastore/__fixtures__/list.json b/assets/js/googlesitekit/modules/datastore/__fixtures__/list.json index 76c144df16c..f773b636ed7 100644 --- a/assets/js/googlesitekit/modules/datastore/__fixtures__/list.json +++ b/assets/js/googlesitekit/modules/datastore/__fixtures__/list.json @@ -37,7 +37,7 @@ "description": "Track conversions for your existing Google Ads campaigns", "homepage": "https://google.com/ads", "internal": false, - "order": 1, + "order": 10, "forceActive": false, "recoverable": false, "shareable": false, @@ -53,7 +53,7 @@ "description": "Earn money by placing ads on your website. It’s free and easy.", "homepage": "https://adsense.google.com/start?source=site-kit&url=https://example.com", "internal": false, - "order": 2, + "order": 10, "forceActive": false, "recoverable": false, "shareable": false, @@ -69,7 +69,7 @@ "description": "Get a deeper understanding of your customers. Google Analytics gives you the free tools you need to analyze data for your business in one place.", "homepage": "https://analytics.google.com/analytics/web", "internal": false, - "order": 3, + "order": 10, "forceActive": false, "recoverable": false, "shareable": false, @@ -85,7 +85,7 @@ "description": "Google PageSpeed Insights gives you metrics about performance, accessibility, SEO and PWA", "homepage": "https://pagespeed.web.dev", "internal": false, - "order": 4, + "order": 10, "forceActive": false, "recoverable": false, "shareable": true, @@ -101,7 +101,7 @@ "description": "Reader Revenue Manager helps publishers grow, retain, and engage their audiences, creating new revenue opportunities", "homepage": "https://readerrevenue.withgoogle.com/", "internal": false, - "order": 5, + "order": 10, "forceActive": false, "recoverable": false, "shareable": false, @@ -117,7 +117,7 @@ "description": "Tag Manager creates an easy to manage way to create tags on your site without updating code", "homepage": "https://tagmanager.google.com/", "internal": false, - "order": 6, + "order": 10, "forceActive": false, "recoverable": false, "shareable": false, diff --git a/assets/js/googlesitekit/modules/datastore/modules.js b/assets/js/googlesitekit/modules/datastore/modules.js index d65d059d38e..dc73df01675 100644 --- a/assets/js/googlesitekit/modules/datastore/modules.js +++ b/assets/js/googlesitekit/modules/datastore/modules.js @@ -99,7 +99,9 @@ const normalizeModules = memize( ( serverDefinitions, clientDefinitions ) => { return module; } ) - .sort( ( a, b ) => a.order - b.order ) + .sort( + ( a, b ) => a.order - b.order || a.name?.localeCompare( b.name ) + ) .reduce( ( acc, module ) => { return { ...acc, [ module.slug ]: module }; }, {} ); diff --git a/assets/js/googlesitekit/modules/datastore/settings.js b/assets/js/googlesitekit/modules/datastore/settings.js index 5ae428e2192..b4a5303295c 100644 --- a/assets/js/googlesitekit/modules/datastore/settings.js +++ b/assets/js/googlesitekit/modules/datastore/settings.js @@ -123,6 +123,33 @@ export const controls = { }; export const selectors = { + /** + * Checks whether settings edit dependencies are currently loading for a module. + * + * @since n.e.x.t + * + * @param {string} slug Module slug. + * @return {boolean?} Whether or not settings edit dependencies are currently loading for the module, + * or `undefined` if the store doesn't exist. + */ + areSettingsEditDependenciesLoaded: createRegistrySelector( + ( select ) => ( state, slug ) => { + invariant( slug, 'slug is required.' ); + const storeName = select( CORE_MODULES ).getModuleStoreName( slug ); + const moduleSelectors = select( storeName ); + if ( ! moduleSelectors ) { + return undefined; + } + + return ( + // If the module doesn't implement the selector, consider dependencies loaded, + ! moduleSelectors.areSettingsEditDependenciesLoaded || + // otherwise defer to the result of the selector. + !! moduleSelectors.areSettingsEditDependenciesLoaded() + ); + } + ), + /** * Checks whether changes are currently being submitted for a module. * diff --git a/assets/js/googlesitekit/modules/datastore/settings.test.js b/assets/js/googlesitekit/modules/datastore/settings.test.js index e00c0b023f2..9c9f35ba94a 100644 --- a/assets/js/googlesitekit/modules/datastore/settings.test.js +++ b/assets/js/googlesitekit/modules/datastore/settings.test.js @@ -20,6 +20,7 @@ * Internal dependencies */ import Modules from 'googlesitekit-modules'; +import { combineStores } from 'googlesitekit-data'; import { CORE_MODULES } from './constants'; import { createTestRegistry, @@ -29,6 +30,7 @@ import { describe( 'core/modules settings', () => { let registry; + let areSettingsEditDependenciesLoaded; let submitChanges; const slug = 'test-module'; const nonExistentModuleSlug = 'not-module'; @@ -37,22 +39,28 @@ describe( 'core/modules settings', () => { let validateCanSubmitChangesError = false; beforeEach( () => { + areSettingsEditDependenciesLoaded = jest.fn(); submitChanges = jest.fn(); registry = createTestRegistry(); registry.registerStore( moduleStoreName, - Modules.createModuleStore( slug, { - storeName: moduleStoreName, - submitChanges, - validateCanSubmitChanges: () => { - if ( validateCanSubmitChangesError ) { - throw new Error( validateCanSubmitChangesError ); - } - }, - settingSlugs: [ 'testSetting' ], - } ) + combineStores( + Modules.createModuleStore( slug, { + storeName: moduleStoreName, + submitChanges, + validateCanSubmitChanges: () => { + if ( validateCanSubmitChangesError ) { + throw new Error( validateCanSubmitChangesError ); + } + }, + settingSlugs: [ 'testSetting' ], + } ), + { + selectors: { areSettingsEditDependenciesLoaded }, + } + ) ); registry @@ -64,7 +72,7 @@ describe( 'core/modules settings', () => { describe( 'actions', () => { describe( 'submitChanges', () => { - it( 'should return an error if a module doesnt exist', async () => { + it( "should return an error if a module doesn't exist", async () => { const expectedError = { error: `The module '${ nonExistentModuleSlug }' does not have a store.`, }; @@ -93,8 +101,66 @@ describe( 'core/modules settings', () => { } ); describe( 'selectors', () => { + describe( 'areSettingsEditDependenciesLoaded', () => { + it( 'should return undefined if the module store has not been registered', () => { + expect( + registry + .select( CORE_MODULES ) + .areSettingsEditDependenciesLoaded( + nonExistentModuleSlug + ) + ).toBe( undefined ); + } ); + + it( 'should return true for module which does not implement areSettingsEditDependenciesLoaded', () => { + const notImplementedSlug = 'not-implemented-module'; + const notImplementedModuleStoreName = `test/${ notImplementedSlug }`; + + registry.registerStore( + notImplementedModuleStoreName, + Modules.createModuleStore( notImplementedSlug, { + storeName: notImplementedModuleStoreName, + } ) + ); + + registry + .dispatch( CORE_MODULES ) + .registerModule( notImplementedSlug, { + storeName: notImplementedModuleStoreName, + } ); + + provideModules( registry ); + + expect( + registry + .select( CORE_MODULES ) + .areSettingsEditDependenciesLoaded( notImplementedSlug ) + ).toBe( true ); + } ); + + it( 'should proxy the selector call to the module with the given slug', () => { + areSettingsEditDependenciesLoaded.mockImplementation( + () => false + ); + expect( + registry + .select( CORE_MODULES ) + .areSettingsEditDependenciesLoaded( slug ) + ).toBe( false ); + + areSettingsEditDependenciesLoaded.mockImplementation( + () => true + ); + expect( + registry + .select( CORE_MODULES ) + .areSettingsEditDependenciesLoaded( slug ) + ).toBe( true ); + } ); + } ); + describe( 'isDoingSubmitChanges', () => { - it( 'should return FALSE for non existing module', () => { + it( 'should return FALSE for non existent module', () => { expect( registry .select( CORE_MODULES ) diff --git a/assets/js/googlesitekit/notifications/components/layout/SubtleNotification.js b/assets/js/googlesitekit/notifications/components/layout/SubtleNotification.js index d8fa827c76b..f97a1943ca6 100644 --- a/assets/js/googlesitekit/notifications/components/layout/SubtleNotification.js +++ b/assets/js/googlesitekit/notifications/components/layout/SubtleNotification.js @@ -22,6 +22,11 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + /** * Internal dependencies */ @@ -29,58 +34,63 @@ import CheckFill from '../../../../../svg/icons/check-fill.svg'; import WarningSVG from '../../../../../svg/icons/warning.svg'; import { Grid, Cell, Row } from '../../../../material-components'; -export default function SubtleNotification( { - className, - title, - description, - dismissCTA, - additionalCTA, - type = 'success', - icon, -} ) { - return ( - - - -
- { icon } - { type === 'success' && ! icon && ( - +const SubtleNotification = forwardRef( + ( + { + className, + title, + description, + dismissCTA, + additionalCTA, + type = 'success', + icon, + }, + ref + ) => { + return ( + + + - ) } -
+ > +
+ { icon } + { type === 'success' && ! icon && ( + + ) } + { type === 'warning' && ! icon && ( + + ) } +
-
-

{ title }

-

- { description } -

-
-
- { dismissCTA } +
+

{ title }

+

+ { description } +

+
+
+ { dismissCTA } - { additionalCTA } -
- - - - ); -} + { additionalCTA } +
+
+
+
+ ); + } +); SubtleNotification.propTypes = { className: PropTypes.string, @@ -91,3 +101,5 @@ SubtleNotification.propTypes = { type: PropTypes.string, icon: PropTypes.object, }; + +export default SubtleNotification; diff --git a/assets/js/googlesitekit/notifications/constants.js b/assets/js/googlesitekit/notifications/constants.js index f7fed08ea66..b7fa4d5a440 100644 --- a/assets/js/googlesitekit/notifications/constants.js +++ b/assets/js/googlesitekit/notifications/constants.js @@ -16,5 +16,7 @@ * limitations under the License. */ -export const FPM_SETUP_CTA_BANNER_NOTIFICATION = - 'first-party-mode-setup-cta-banner'; +export const FPM_HEALTH_CHECK_WARNING_NOTIFICATION_ID = + 'warning-notification-fpm'; + +export const FPM_SETUP_CTA_BANNER_NOTIFICATION = 'fpm-setup-cta'; diff --git a/assets/js/googlesitekit/notifications/datastore/constants.js b/assets/js/googlesitekit/notifications/datastore/constants.js index c8fff22ed85..a60ab84f9a3 100644 --- a/assets/js/googlesitekit/notifications/datastore/constants.js +++ b/assets/js/googlesitekit/notifications/datastore/constants.js @@ -42,6 +42,3 @@ export const NOTIFICATION_VIEW_CONTEXTS = [ VIEW_CONTEXT_MAIN_DASHBOARD_VIEW_ONLY, VIEW_CONTEXT_ENTITY_DASHBOARD_VIEW_ONLY, ]; - -export const FPM_HEALTH_CHECK_WARNING_NOTIFICATION_ID = - 'fpm-warning-notification'; diff --git a/assets/js/googlesitekit/notifications/register-defaults.js b/assets/js/googlesitekit/notifications/register-defaults.js index 3732408482a..6d8beb20da1 100644 --- a/assets/js/googlesitekit/notifications/register-defaults.js +++ b/assets/js/googlesitekit/notifications/register-defaults.js @@ -29,8 +29,11 @@ import { CORE_NOTIFICATIONS, NOTIFICATION_AREAS, NOTIFICATION_GROUPS, - FPM_HEALTH_CHECK_WARNING_NOTIFICATION_ID, } from './datastore/constants'; +import { + FPM_HEALTH_CHECK_WARNING_NOTIFICATION_ID, + FPM_SETUP_CTA_BANNER_NOTIFICATION, +} from './constants'; import { CORE_FORMS } from '../datastore/forms/constants'; import { CORE_SITE } from '../datastore/site/constants'; import { @@ -58,7 +61,6 @@ import FirstPartyModeSetupBanner, { FPM_SHOW_SETUP_SUCCESS_NOTIFICATION, } from '../../components/notifications/FirstPartyModeSetupBanner'; import FirstPartyModeSetupSuccessSubtleNotification from '../../components/notifications/FirstPartyModeSetupSuccessSubtleNotification'; -import { FPM_SETUP_CTA_BANNER_NOTIFICATION } from './constants'; import { isFeatureEnabled } from '../../features'; export const DEFAULT_NOTIFICATIONS = { @@ -237,30 +239,24 @@ export const DEFAULT_NOTIFICATIONS = { VIEW_CONTEXT_ENTITY_DASHBOARD, ], checkRequirements: async ( { select, resolveSelect, dispatch } ) => { - await Promise.all( [ - // The getAdSenseLinked selector relies on the resolution - // of the getSettings() resolver. - resolveSelect( MODULES_ANALYTICS_4 ).getSettings(), - // The isModuleConnected() selector relies on the resolution - // of the getModules() resolver. - resolveSelect( CORE_MODULES ).getModules(), - ] ); + const adSenseModuleConnected = await resolveSelect( + CORE_MODULES + ).isModuleConnected( 'adsense' ); - const adSenseModuleConnected = - select( CORE_MODULES ).isModuleConnected( 'adsense' ); + const analyticsModuleConnected = await resolveSelect( + CORE_MODULES + ).isModuleConnected( 'analytics-4' ); - const analyticsModuleConnected = - select( CORE_MODULES ).isModuleConnected( 'analytics-4' ); + if ( ! ( adSenseModuleConnected && analyticsModuleConnected ) ) { + return false; + } + + await resolveSelect( MODULES_ANALYTICS_4 ).getSettings(); const isAdSenseLinked = select( MODULES_ANALYTICS_4 ).getAdSenseLinked(); - const analyticsAndAdsenseConnectedAndLinked = - adSenseModuleConnected && - analyticsModuleConnected && - isAdSenseLinked; - - if ( ! analyticsAndAdsenseConnectedAndLinked ) { + if ( ! isAdSenseLinked ) { return false; } @@ -297,10 +293,7 @@ export const DEFAULT_NOTIFICATIONS = { // we show them a different notification and should not show this one. Check // to see if the user already has data and dismiss this notification without // showing it. - if ( - isZeroReport( report ) === false && - analyticsAndAdsenseConnectedAndLinked - ) { + if ( isZeroReport( report ) === false ) { await dispatch( CORE_NOTIFICATIONS ).dismissNotification( 'top-earning-pages-success-notification' ); diff --git a/assets/js/modules/ads/pax/services.js b/assets/js/modules/ads/pax/services.js index ed5f24dd70b..c58442fd7d9 100644 --- a/assets/js/modules/ads/pax/services.js +++ b/assets/js/modules/ads/pax/services.js @@ -167,8 +167,7 @@ export function createPaxServices( registry, options = {} ) { getSupportedConversionTrackingTypes: async () => { return { conversionTrackingTypes: [ - // @TODO: Include TYPE_CONVERSION_EVENT in a future update. - // 'TYPE_CONVERSION_EVENT', + 'TYPE_CONVERSION_EVENT', 'TYPE_PAGE_VIEW', ], }; diff --git a/assets/js/modules/ads/pax/services.test.js b/assets/js/modules/ads/pax/services.test.js index 3f75b5973aa..52abf55a25e 100644 --- a/assets/js/modules/ads/pax/services.test.js +++ b/assets/js/modules/ads/pax/services.test.js @@ -301,7 +301,10 @@ describe( 'PAX partner services', () => { ); expect( supportedTypes ).toMatchObject( { - conversionTrackingTypes: [ 'TYPE_PAGE_VIEW' ], + conversionTrackingTypes: [ + 'TYPE_CONVERSION_EVENT', + 'TYPE_PAGE_VIEW', + ], } ); } ); } ); diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.js index 6bf857e204c..868e908d536 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.js @@ -41,11 +41,12 @@ import { MODULES_ANALYTICS_4 } from '../../../datastore/constants'; import useDashboardType, { DASHBOARD_TYPE_MAIN, } from '../../../../../hooks/useDashboardType'; +import whenActive from '../../../../../util/when-active'; export const AUDIENCE_SEGMENTATION_INTRODUCTORY_OVERLAY_NOTIFICATION = 'audienceSegmentationIntroductoryOverlayNotification'; -export default function AudienceSegmentationIntroductoryOverlayNotification() { +function AudienceSegmentationIntroductoryOverlayNotification() { const viewContext = useViewContext(); const isViewOnly = useViewOnly(); const breakpoint = useBreakpoint(); @@ -178,3 +179,7 @@ export default function AudienceSegmentationIntroductoryOverlayNotification() { ); } + +export default whenActive( { moduleName: 'analytics-4' } )( + AudienceSegmentationIntroductoryOverlayNotification +); diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.stories.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.stories.js index a0c963c1b7e..29a2c813e3c 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.stories.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.stories.js @@ -51,6 +51,7 @@ export default { { slug: 'analytics-4', active: true, + connected: true, setupComplete: true, }, ] ); diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.test.js index 1354f3e0297..3f7681d830e 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.test.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationIntroductoryOverlayNotification.test.js @@ -68,6 +68,7 @@ describe( 'AudienceSegmentationIntroductoryOverlayNotification', () => { { slug: 'analytics-4', active: true, + connected: true, setupComplete: true, }, ] ); diff --git a/assets/js/modules/analytics-4/components/setup/SetupEnhancedMeasurementSwitch.js b/assets/js/modules/analytics-4/components/setup/SetupEnhancedMeasurementSwitch.js index 9a4631106b4..64fc2ff228c 100644 --- a/assets/js/modules/analytics-4/components/setup/SetupEnhancedMeasurementSwitch.js +++ b/assets/js/modules/analytics-4/components/setup/SetupEnhancedMeasurementSwitch.js @@ -15,10 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** - * External dependencies + * WordPress dependencies */ -import { useMount } from 'react-use'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -108,17 +109,25 @@ export default function SetupEnhancedMeasurementSwitch() { ); } ); - const { setValues } = useDispatch( CORE_FORMS ); - const { getValue } = useSelect( ( select ) => select( CORE_FORMS ) ); + const isAutoSubmit = useSelect( ( select ) => + select( CORE_FORMS ).getValue( FORM_SETUP, 'autoSubmit' ) + ); - useMount( () => { - const autoSubmit = getValue( FORM_SETUP, 'autoSubmit' ); - if ( ! autoSubmit ) { + const isEnhancedMeasurementEnabled = useSelect( ( select ) => + select( CORE_FORMS ).getValue( + ENHANCED_MEASUREMENT_FORM, + ENHANCED_MEASUREMENT_ENABLED + ) + ); + + const { setValues } = useDispatch( CORE_FORMS ); + useEffect( () => { + if ( ! isAutoSubmit && isEnhancedMeasurementEnabled === undefined ) { setValues( ENHANCED_MEASUREMENT_FORM, { [ ENHANCED_MEASUREMENT_ENABLED ]: true, } ); } - } ); + }, [ isAutoSubmit, isEnhancedMeasurementEnabled, setValues ] ); if ( ! isValidAccountID( accountID ) ) { return null; diff --git a/assets/js/modules/analytics-4/datastore/base.js b/assets/js/modules/analytics-4/datastore/base.js index 18472cd07b0..a0aa982c3d5 100644 --- a/assets/js/modules/analytics-4/datastore/base.js +++ b/assets/js/modules/analytics-4/datastore/base.js @@ -65,6 +65,8 @@ const baseModuleStore = Modules.createModuleStore( 'analytics-4', { 'availableAudiencesLastSyncedAt', 'audienceSegmentationSetupCompletedBy', 'detectedEvents', + 'newConversionEventsLastUpdateAt', + 'lostConversionEventsLastUpdateAt', ], submitChanges, rollbackChanges, diff --git a/assets/js/modules/analytics-4/datastore/conversion-reporting.js b/assets/js/modules/analytics-4/datastore/conversion-reporting.js index d8c9ee77ee6..cbbd1a353f8 100644 --- a/assets/js/modules/analytics-4/datastore/conversion-reporting.js +++ b/assets/js/modules/analytics-4/datastore/conversion-reporting.js @@ -25,10 +25,8 @@ import { isEqual } from 'lodash'; /** * Internal dependencies */ -import API from 'googlesitekit-api'; import { commonActions, - combineStores, createRegistrySelector, createReducer, } from 'googlesitekit-data'; @@ -49,7 +47,6 @@ import { MODULES_ANALYTICS_4, } from './constants'; import { USER_INPUT_PURPOSE_TO_CONVERSION_EVENTS_MAPPING } from '../../../components/user-input/util/constants'; -import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store'; import { negateDefined } from '../../../util/negate'; import { safelySort } from '../../../util'; @@ -65,54 +62,6 @@ function hasConversionReportingEventsOfType( propName ) { } ); } -const dismissNewConversionReportingEventsStore = createFetchStore( { - baseName: 'dismissNewConversionReportingEvents', - controlCallback: () => { - return API.set( - 'modules', - 'analytics-4', - 'clear-conversion-reporting-new-events' - ); - }, - reducerCallback: ( state, values ) => { - if ( values === false ) { - return state; - } - - return { - ...state, - detectedEventsChange: { - ...state.detectedEventsChange, - newEvents: [], - }, - }; - }, -} ); - -const dismissLostConversionReportingEventsStore = createFetchStore( { - baseName: 'dismissLostConversionReportingEvents', - controlCallback: () => { - return API.set( - 'modules', - 'analytics-4', - 'clear-conversion-reporting-lost-events' - ); - }, - reducerCallback: ( state, values ) => { - if ( values === false ) { - return state; - } - - return { - ...state, - detectedEventsChange: { - ...state.detectedEventsChange, - lostEvents: [], - }, - }; - }, -} ); - // Actions. const RECEIVE_CONVERSION_REPORTING_INLINE_DATA = 'RECEIVE_CONVERSION_REPORTING_INLINE_DATA'; @@ -150,28 +99,6 @@ export const resolvers = { }; export const actions = { - /** - * Dismiss new conversion reporting events. - * - * @since 1.138.0 - * - * @return {boolean} Transient deletion response. - */ - dismissNewConversionReportingEvents() { - return dismissNewConversionReportingEventsStore.actions.fetchDismissNewConversionReportingEvents(); - }, - - /** - * Dismiss lost conversion reporting events. - * - * @since 1.138.0 - * - * @return {boolean} Transient deletion response. - */ - dismissLostConversionReportingEvents() { - return dismissLostConversionReportingEventsStore.actions.fetchDismissLostConversionReportingEvents(); - }, - /** * Stores conversion reporting inline data in the datastore. * @@ -554,14 +481,12 @@ export const selectors = { ), }; -export default combineStores( - dismissNewConversionReportingEventsStore, - dismissLostConversionReportingEventsStore, - { - initialState, - actions, - resolvers, - selectors, - reducer, - } -); +const store = { + initialState, + actions, + resolvers, + selectors, + reducer, +}; + +export default store; diff --git a/assets/js/modules/analytics-4/datastore/conversion-reporting.test.js b/assets/js/modules/analytics-4/datastore/conversion-reporting.test.js index 07846bc9842..e376a8b211d 100644 --- a/assets/js/modules/analytics-4/datastore/conversion-reporting.test.js +++ b/assets/js/modules/analytics-4/datastore/conversion-reporting.test.js @@ -80,51 +80,6 @@ describe( 'modules/analytics-4 conversion-reporting', () => { ); } ); } ); - describe( 'dismissNewConversionReportingEvents', () => { - it( 'fetches clear new events endpoint', async () => { - fetchMock.postOnce( - new RegExp( - '^/google-site-kit/v1/modules/analytics-4/data/clear-conversion-reporting-new-events' - ), - true - ); - - const { response } = await registry - .dispatch( MODULES_ANALYTICS_4 ) - .dismissNewConversionReportingEvents(); - - expect( fetchMock ).toHaveFetchedTimes( 1 ); - expect( fetchMock ).toHaveFetched( - new RegExp( - '^/google-site-kit/v1/modules/analytics-4/data/clear-conversion-reporting-new-events' - ) - ); - expect( response ).toEqual( true ); - } ); - } ); - - describe( 'dismissLostConversionReportingEvents', () => { - it( 'fetches clear lost events endpoint', async () => { - fetchMock.postOnce( - new RegExp( - '^/google-site-kit/v1/modules/analytics-4/data/clear-conversion-reporting-lost-events' - ), - true - ); - - const { response } = await registry - .dispatch( MODULES_ANALYTICS_4 ) - .dismissLostConversionReportingEvents(); - - expect( fetchMock ).toHaveFetchedTimes( 1 ); - expect( fetchMock ).toHaveFetched( - new RegExp( - '^/google-site-kit/v1/modules/analytics-4/data/clear-conversion-reporting-lost-events' - ) - ); - expect( response ).toEqual( true ); - } ); - } ); } ); describe( 'selectors', () => { diff --git a/assets/js/modules/analytics-4/datastore/index.js b/assets/js/modules/analytics-4/datastore/index.js index 2ed957d9a78..f95fd78875a 100644 --- a/assets/js/modules/analytics-4/datastore/index.js +++ b/assets/js/modules/analytics-4/datastore/index.js @@ -35,6 +35,7 @@ import properties from './properties'; import report from './report'; import pivotReport from './pivot-report'; import service from './service'; +import settings from './settings'; import tags from './tags'; import webdatastreams from './webdatastreams'; import { createSnapshotStore } from '../../../googlesitekit/data/create-snapshot-store'; @@ -51,9 +52,10 @@ const store = combineStores( customDimensionsGatheringData, enhancedMeasurement, partialData, + pivotReport, properties, report, - pivotReport, + settings, service, tags, webdatastreams diff --git a/assets/js/modules/analytics-4/datastore/settings.js b/assets/js/modules/analytics-4/datastore/settings.js index e1986ac3833..0bc18dd8450 100644 --- a/assets/js/modules/analytics-4/datastore/settings.js +++ b/assets/js/modules/analytics-4/datastore/settings.js @@ -26,6 +26,7 @@ import { isEqual, pick } from 'lodash'; * Internal dependencies */ import API from 'googlesitekit-api'; +import { createRegistrySelector } from 'googlesitekit-data'; import { createStrictSelect } from '../../../googlesitekit/data/utils'; import { isValidPropertyID, @@ -67,6 +68,18 @@ export const INVARIANT_WEBDATASTREAM_ALREADY_EXISTS = export const INVARIANT_INVALID_ADS_CONVERSION_ID = 'a valid ads adsConversionID is required to submit changes'; +const store = { + selectors: { + areSettingsEditDependenciesLoaded: createRegistrySelector( + ( select ) => () => + select( MODULES_ANALYTICS_4 ).hasFinishedResolution( + 'getAccountSummaries' + ) + ), + }, +}; +export default store; + export async function submitChanges( { select, dispatch } ) { let propertyID = select( MODULES_ANALYTICS_4 ).getPropertyID(); if ( propertyID === PROPERTY_CREATE ) { diff --git a/assets/js/modules/analytics-4/datastore/settings.test.js b/assets/js/modules/analytics-4/datastore/settings.test.js index 6578325a45a..7a1d8d665a7 100644 --- a/assets/js/modules/analytics-4/datastore/settings.test.js +++ b/assets/js/modules/analytics-4/datastore/settings.test.js @@ -972,6 +972,32 @@ describe( 'modules/analytics-4 settings', () => { } ); describe( 'selectors', () => { + describe( 'areSettingsEditDependenciesLoaded', () => { + it( 'should return false if getAccountSummaries selector has not resolved', () => { + registry + .dispatch( MODULES_ANALYTICS_4 ) + .startResolution( 'getAccountSummaries', [] ); + + expect( + registry + .select( MODULES_ANALYTICS_4 ) + .areSettingsEditDependenciesLoaded() + ).toBe( false ); + } ); + + it( 'should return true if getAccountSummaries selector has resolved', () => { + registry + .dispatch( MODULES_ANALYTICS_4 ) + .finishResolution( 'getAccountSummaries', [] ); + + expect( + registry + .select( MODULES_ANALYTICS_4 ) + .areSettingsEditDependenciesLoaded() + ).toBe( true ); + } ); + } ); + describe( 'canSubmitChanges', () => { const propertyID = '1000'; const webDataStreamID = '2000'; diff --git a/assets/js/modules/analytics-4/index.js b/assets/js/modules/analytics-4/index.js index 057a3a8954c..2a35f2a9637 100644 --- a/assets/js/modules/analytics-4/index.js +++ b/assets/js/modules/analytics-4/index.js @@ -695,6 +695,14 @@ export const registerNotifications = ( notifications ) => { areaSlug: NOTIFICATION_AREAS.BANNERS_BELOW_NAV, viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], checkRequirements: async ( { select, resolveSelect } ) => { + const analyticsConnected = await resolveSelect( + CORE_MODULES + ).isModuleConnected( 'analytics-4' ); + + if ( ! analyticsConnected ) { + return false; + } + await resolveSelect( MODULES_ANALYTICS_4 ).getSettings(); const configuredAudiences = select( CORE_USER ).getConfiguredAudiences(); diff --git a/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.js b/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.js index c415960ace9..60224b9bc68 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.js @@ -40,16 +40,18 @@ import { CORE_UI } from '../../../../googlesitekit/datastore/ui/constants'; import { VIEW_CONTEXT_MAIN_DASHBOARD } from '../../../../googlesitekit/constants'; import { MODULES_READER_REVENUE_MANAGER, + READER_REVENUE_MANAGER_MODULE_SLUG, UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION, PUBLICATION_ONBOARDING_STATES, } from '../../datastore/constants'; +import whenActive from '../../../../util/when-active'; const { ONBOARDING_COMPLETE } = PUBLICATION_ONBOARDING_STATES; export const RRM_PUBLICATION_APPROVED_OVERLAY_NOTIFICATION = 'rrmPublicationApprovedOverlayNotification'; -export default function PublicationApprovedOverlayNotification() { +function PublicationApprovedOverlayNotification() { const viewContext = useViewContext(); const isViewOnly = useViewOnly(); const dashboardType = useDashboardType(); @@ -205,3 +207,7 @@ export default function PublicationApprovedOverlayNotification() { ); } + +export default whenActive( { moduleName: READER_REVENUE_MANAGER_MODULE_SLUG } )( + PublicationApprovedOverlayNotification +); diff --git a/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.stories.js b/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.stories.js index 5bb4bedf353..f66a551ff4e 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.stories.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.stories.js @@ -24,10 +24,12 @@ import WithRegistrySetup from '../../../../../../tests/js/WithRegistrySetup'; import { CORE_UI } from '../../../../googlesitekit/datastore/ui/constants'; import { MODULES_READER_REVENUE_MANAGER, + READER_REVENUE_MANAGER_MODULE_SLUG, UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION, } from '../../datastore/constants'; import { VIEW_CONTEXT_MAIN_DASHBOARD } from '../../../../googlesitekit/constants'; import { Provider as ViewContextProvider } from '../../../../components/Root/ViewContextContext'; +import { provideModules } from '../../../../../../tests/js/utils'; function Template() { return ( @@ -48,6 +50,14 @@ export default { decorators: [ ( Story, { args } ) => { const setupRegistry = ( registry ) => { + provideModules( registry, [ + { + slug: READER_REVENUE_MANAGER_MODULE_SLUG, + active: true, + connected: true, + }, + ] ); + registry .dispatch( MODULES_READER_REVENUE_MANAGER ) .receiveGetSettings( { diff --git a/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.test.js b/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.test.js index 6900659660f..7891c477b34 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.test.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/PublicationApprovedOverlayNotification.test.js @@ -24,7 +24,10 @@ import fetchMock from 'fetch-mock'; /** * Internal dependencies */ -import { createTestRegistry } from '../../../../../../tests/js/utils'; +import { + createTestRegistry, + provideModules, +} from '../../../../../../tests/js/utils'; import { act, fireEvent, render } from '../../../../../../tests/js/test-utils'; import PublicationApprovedOverlayNotification, { RRM_PUBLICATION_APPROVED_OVERLAY_NOTIFICATION, @@ -38,6 +41,7 @@ import * as tracking from '../../../../util/tracking'; import { Provider as ViewContextProvider } from '../../../../components/Root/ViewContextContext'; import { MODULES_READER_REVENUE_MANAGER, + READER_REVENUE_MANAGER_MODULE_SLUG, UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION, } from '../../datastore/constants'; import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; @@ -59,6 +63,15 @@ describe( 'PublicationApprovedOverlayNotification', () => { beforeEach( () => { mockTrackEvent.mockClear(); registry = createTestRegistry(); + + provideModules( registry, [ + { + slug: READER_REVENUE_MANAGER_MODULE_SLUG, + active: true, + connected: true, + }, + ] ); + registry .dispatch( CORE_UI ) .setValue( diff --git a/assets/js/modules/reader-revenue-manager/index.js b/assets/js/modules/reader-revenue-manager/index.js index ddc51e0efa5..9367ad5d4f7 100644 --- a/assets/js/modules/reader-revenue-manager/index.js +++ b/assets/js/modules/reader-revenue-manager/index.js @@ -38,6 +38,7 @@ import { isURLUsingHTTPS } from './utils/validation'; import { RRMSetupSuccessSubtleNotification } from './components/dashboard'; import { NOTIFICATION_AREAS } from '../../googlesitekit/notifications/datastore/constants'; import { VIEW_CONTEXT_MAIN_DASHBOARD } from '../../googlesitekit/constants'; +import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants'; export { registerStore } from './datastore'; @@ -82,6 +83,14 @@ export const registerNotifications = ( notifications ) => { areaSlug: NOTIFICATION_AREAS.BANNERS_BELOW_NAV, viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], checkRequirements: async ( { select, resolveSelect } ) => { + const rrmConnected = await resolveSelect( + CORE_MODULES + ).isModuleConnected( READER_REVENUE_MANAGER_MODULE_SLUG ); + + if ( ! rrmConnected ) { + return false; + } + const notification = getQueryArg( location.href, 'notification' ); const slug = getQueryArg( location.href, 'slug' ); diff --git a/assets/js/modules/search-console/datastore/index.js b/assets/js/modules/search-console/datastore/index.js index ef7c67f77a8..5e78477ed60 100644 --- a/assets/js/modules/search-console/datastore/index.js +++ b/assets/js/modules/search-console/datastore/index.js @@ -24,9 +24,16 @@ import { MODULES_SEARCH_CONSOLE } from './constants'; import baseModuleStore from './base'; import report from './report'; import service from './service'; +import settings from './settings'; import properties from './properties'; -const store = combineStores( baseModuleStore, report, service, properties ); +const store = combineStores( + baseModuleStore, + report, + service, + settings, + properties +); export const initialState = store.initialState; export const actions = store.actions; diff --git a/assets/js/modules/search-console/datastore/settings.js b/assets/js/modules/search-console/datastore/settings.js index 72f9e796a8d..0bf18183c14 100644 --- a/assets/js/modules/search-console/datastore/settings.js +++ b/assets/js/modules/search-console/datastore/settings.js @@ -25,6 +25,7 @@ import invariant from 'invariant'; * Internal dependencies */ import API from 'googlesitekit-api'; +import { createRegistrySelector } from 'googlesitekit-data'; import { createStrictSelect } from '../../../googlesitekit/data/utils'; import { isValidPropertyID } from '../util'; import { INVARIANT_SETTINGS_NOT_CHANGED } from '../../../googlesitekit/data/create-settings-store'; @@ -34,6 +35,18 @@ import { MODULES_SEARCH_CONSOLE } from './constants'; export const INVARIANT_INVALID_PROPERTY_SELECTION = 'a valid propertyID is required to submit changes'; +const store = { + selectors: { + areSettingsEditDependenciesLoaded: createRegistrySelector( + ( select ) => () => + select( MODULES_SEARCH_CONSOLE ).hasFinishedResolution( + 'getMatchedProperties' + ) + ), + }, +}; +export default store; + export async function submitChanges( { select, dispatch } ) { // This action shouldn't be called if settings haven't changed, // but this prevents errors in tests. diff --git a/assets/js/modules/search-console/datastore/settings.test.js b/assets/js/modules/search-console/datastore/settings.test.js index fe1214d3a9a..2a592d5d3f6 100644 --- a/assets/js/modules/search-console/datastore/settings.test.js +++ b/assets/js/modules/search-console/datastore/settings.test.js @@ -125,4 +125,30 @@ describe( 'modules/search-console settings', () => { ).not.toThrow( INVARIANT_SETTINGS_NOT_CHANGED ); } ); } ); + + describe( 'areSettingsEditDependenciesLoaded', () => { + it( 'should return false if getMatchedProperties selector has not resolved', () => { + registry + .dispatch( MODULES_SEARCH_CONSOLE ) + .startResolution( 'getMatchedProperties', [] ); + + expect( + registry + .select( MODULES_SEARCH_CONSOLE ) + .areSettingsEditDependenciesLoaded() + ).toBe( false ); + } ); + + it( 'should return true if getMatchedProperties selector has resolved', () => { + registry + .dispatch( MODULES_SEARCH_CONSOLE ) + .finishResolution( 'getMatchedProperties', [] ); + + expect( + registry + .select( MODULES_SEARCH_CONSOLE ) + .areSettingsEditDependenciesLoaded() + ).toBe( true ); + } ); + } ); } ); diff --git a/assets/js/modules/tagmanager/datastore/index.js b/assets/js/modules/tagmanager/datastore/index.js index 4f41e405f21..67e0a41af64 100644 --- a/assets/js/modules/tagmanager/datastore/index.js +++ b/assets/js/modules/tagmanager/datastore/index.js @@ -28,6 +28,7 @@ import containers from './containers'; import tags from './tags'; import versions from './versions'; import service from './service'; +import settings from './settings'; const store = combineStores( baseModuleStore, @@ -36,6 +37,7 @@ const store = combineStores( tags, versions, createSnapshotStore( MODULES_TAGMANAGER ), + settings, service ); diff --git a/assets/js/modules/tagmanager/datastore/settings.js b/assets/js/modules/tagmanager/datastore/settings.js index 139ee74f268..0bfb306c143 100644 --- a/assets/js/modules/tagmanager/datastore/settings.js +++ b/assets/js/modules/tagmanager/datastore/settings.js @@ -25,6 +25,7 @@ import invariant from 'invariant'; * Internal dependencies */ import API from 'googlesitekit-api'; +import { createRegistrySelector } from 'googlesitekit-data'; import { CORE_FORMS } from '../../../googlesitekit/datastore/forms/constants'; import { isValidAccountID, @@ -67,6 +68,18 @@ export const INVARIANT_INVALID_CONTAINER_NAME = export const INVARIANT_GTM_GA_PROPERTY_ID_MISMATCH = 'single GTM Analytics property ID must match Analytics property ID'; +const store = { + selectors: { + areSettingsEditDependenciesLoaded: createRegistrySelector( + ( select ) => () => + select( MODULES_TAGMANAGER ).hasFinishedResolution( + 'getAccounts' + ) + ), + }, +}; +export default store; + export async function submitChanges( { select, dispatch } ) { const accountID = select( MODULES_TAGMANAGER ).getAccountID(); const containerID = select( MODULES_TAGMANAGER ).getContainerID(); diff --git a/assets/js/modules/tagmanager/datastore/settings.test.js b/assets/js/modules/tagmanager/datastore/settings.test.js index bb47ac4d4a2..a661ce3c604 100644 --- a/assets/js/modules/tagmanager/datastore/settings.test.js +++ b/assets/js/modules/tagmanager/datastore/settings.test.js @@ -480,6 +480,32 @@ describe( 'modules/tagmanager settings', () => { } ); describe( 'selectors', () => { + describe( 'areSettingsEditDependenciesLoaded', () => { + it( 'should return false if getAccounts selector has not resolved', () => { + registry + .dispatch( MODULES_TAGMANAGER ) + .startResolution( 'getAccounts', [] ); + + expect( + registry + .select( MODULES_TAGMANAGER ) + .areSettingsEditDependenciesLoaded() + ).toBe( false ); + } ); + + it( 'should return true if getAccounts selector has resolved', () => { + registry + .dispatch( MODULES_TAGMANAGER ) + .finishResolution( 'getAccounts', [] ); + + expect( + registry + .select( MODULES_TAGMANAGER ) + .areSettingsEditDependenciesLoaded() + ).toBe( true ); + } ); + } ); + describe( 'isDoingSubmitChanges', () => { it( 'returns true while submitting changes', async () => { registry diff --git a/assets/sass/components/first-party-mode/_googlesitekit-first-party-mode-setup-cta-banner.scss b/assets/sass/components/first-party-mode/_googlesitekit-first-party-mode-setup-cta-banner.scss index 047244e44ae..391b9edfdef 100644 --- a/assets/sass/components/first-party-mode/_googlesitekit-first-party-mode-setup-cta-banner.scss +++ b/assets/sass/components/first-party-mode/_googlesitekit-first-party-mode-setup-cta-banner.scss @@ -18,7 +18,7 @@ .googlesitekit-plugin { - .googlesitekit-setup-cta-banner--first-party-mode-setup-cta-banner { + .googlesitekit-setup-cta-banner--fpm-setup-cta { .googlesitekit-setup-cta-banner__actions-wrapper { gap: 4px; // Fix buttons being one on top of another on medium mobile viewports. @@ -28,7 +28,7 @@ } } - .googlesitekit-setup-cta-banner__svg-wrapper--first-party-mode-setup-cta-banner { + .googlesitekit-setup-cta-banner__svg-wrapper--fpm-setup-cta { align-items: flex-end; display: flex; justify-content: center; diff --git a/assets/sass/components/key-metrics/_googlesitekit-chip-tab-group.scss b/assets/sass/components/key-metrics/_googlesitekit-chip-tab-group.scss index c09deb635e1..fb3b754b5d4 100644 --- a/assets/sass/components/key-metrics/_googlesitekit-chip-tab-group.scss +++ b/assets/sass/components/key-metrics/_googlesitekit-chip-tab-group.scss @@ -135,6 +135,12 @@ background-color: $c-site-kit-sk-100; } + .googlesitekit-chip-tab-group__chip-item-svg__suggested { + path { + fill: $c-neutral-n-900; + } + } + .googlesitekit-chip-tab-group__chip-item-count { margin-left: 3px; } @@ -156,14 +162,6 @@ } } - .googlesitekit-chip-tab-group__tab-item-mobile-svg { - margin-right: 7px; - - svg { - display: block; - } - } - .googlesitekit-chip-tab-group__graphic { align-items: center; display: flex; @@ -178,5 +176,23 @@ } } } + + .googlesitekit-chip-tab-group__tab-item-mobile-svg { + margin-right: 7px; + + svg { + display: block; + } + + &.googlesitekit-chip-tab-group__tab-item-mobile-svg--suggested { + + svg { + + path { + fill: $c-neutral-n-900; + } + } + } + } } } diff --git a/includes/Core/Authentication/Clients/OAuth_Client.php b/includes/Core/Authentication/Clients/OAuth_Client.php index b600d844dce..636cd18f076 100644 --- a/includes/Core/Authentication/Clients/OAuth_Client.php +++ b/includes/Core/Authentication/Clients/OAuth_Client.php @@ -569,24 +569,6 @@ public function refresh_profile_data( $retry_after = 0 ) { } } - /** - * Determines whether the authentication proxy is used. - * - * In order to streamline the setup and authentication flow, the plugin uses a proxy mechanism based on an external - * service. This can be overridden by providing actual GCP credentials with the {@see 'googlesitekit_oauth_secret'} - * filter. - * - * @since 1.0.0 - * @deprecated 1.9.0 - * - * @return bool True if proxy authentication is used, false otherwise. - */ - public function using_proxy() { - _deprecated_function( __METHOD__, '1.9.0', Credentials::class . '::using_proxy' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - - return $this->credentials->using_proxy(); - } - /** * Determines whether the current owner ID must be changed or not. * diff --git a/includes/Core/Key_Metrics/REST_Key_Metrics_Controller.php b/includes/Core/Key_Metrics/REST_Key_Metrics_Controller.php index e287b8afcfb..aa3e849995e 100644 --- a/includes/Core/Key_Metrics/REST_Key_Metrics_Controller.php +++ b/includes/Core/Key_Metrics/REST_Key_Metrics_Controller.php @@ -184,18 +184,6 @@ protected function get_rest_routes() { ), ) ), - new REST_Route( - 'core/user/data/reset-key-metrics-selection', - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => function () { - $this->settings->merge( array( 'widgetSlugs' => array() ) ); - - return new WP_REST_Response( $this->settings->get() ); - }, - 'permission_callback' => $has_capabilities, - ) - ), ); } } diff --git a/includes/Core/Tags/First_Party_Mode/First_Party_Mode.php b/includes/Core/Tags/First_Party_Mode/First_Party_Mode.php index 37776fdc855..376b2567d7e 100644 --- a/includes/Core/Tags/First_Party_Mode/First_Party_Mode.php +++ b/includes/Core/Tags/First_Party_Mode/First_Party_Mode.php @@ -186,22 +186,33 @@ public function on_admin_init() { * * @since 1.141.0 * @since 1.142.0 Relocated from REST_First_Party_Mode_Controller. + * @since n.e.x.t Uses Google\FirstPartyLibrary\RequestHelper to send requests. * * @param string $endpoint The endpoint to check. * @return bool True if the endpoint is healthy, false otherwise. */ protected function is_endpoint_healthy( $endpoint ) { - try { - // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents - $response = file_get_contents( $endpoint ); - } catch ( \Exception $e ) { + if ( ! defined( 'IS_FIRST_PARTY_MODE_TEST' ) ) { + // TODO: This is a workaround to allow the measurement.php file to be loaded without making a + // request, in order to use the RequestHelper class that it defines. We should find a better + // solution in the future, but this will involve changes to the measurement.php file. + define( 'IS_FIRST_PARTY_MODE_TEST', true ); + } + + require_once GOOGLESITEKIT_PLUGIN_DIR_PATH . 'fpm/measurement.php'; + + $request_helper = new \Google\FirstPartyLibrary\RequestHelper(); + + $response = $request_helper->sendRequest( $endpoint ); + + if ( 200 !== $response['statusCode'] ) { return false; } - if ( 'ok' !== $response ) { + if ( 'ok' !== $response['body'] ) { return false; } - return strpos( $http_response_header[0], '200 OK' ) !== false; + return true; } } diff --git a/includes/Core/User/Conversion_Reporting.php b/includes/Core/User/Conversion_Reporting.php new file mode 100644 index 00000000000..3a517ebe263 --- /dev/null +++ b/includes/Core/User/Conversion_Reporting.php @@ -0,0 +1,61 @@ +conversion_reporting_settings = new Conversion_Reporting_Settings( $user_options ); + $this->rest_controller = new REST_Conversion_Reporting_Controller( $this->conversion_reporting_settings ); + } + + /** + * Registers functionality through WordPress hooks. + * + * @since n.e.x.t + */ + public function register() { + $this->conversion_reporting_settings->register(); + $this->rest_controller->register(); + } +} diff --git a/includes/Core/User/Conversion_Reporting_Settings.php b/includes/Core/User/Conversion_Reporting_Settings.php new file mode 100644 index 00000000000..7e17a00fda1 --- /dev/null +++ b/includes/Core/User/Conversion_Reporting_Settings.php @@ -0,0 +1,109 @@ + 0, + 'lostEventsCalloutDismissedAt' => 0, + ); + } + + /** + * Merges an array of settings to update. + * + * @since n.e.x.t + * + * @param array $partial Partial settings array to save. + * @return bool True on success, false on failure. + */ + public function merge( array $partial ) { + $settings = $this->get(); + $partial = array_filter( + $partial, + function ( $value ) { + return null !== $value; + } + ); + + $allowed_settings = array( + 'newEventsCalloutDismissedAt' => true, + 'lostEventsCalloutDismissedAt' => true, + ); + + $updated = array_intersect_key( $partial, $allowed_settings ); + + return $this->set( array_merge( $settings, $updated ) ); + } + + /** + * Gets the callback for sanitizing the setting's value before saving. + * + * @since n.e.x.t + * + * @return callable Sanitize callback. + */ + protected function get_sanitize_callback() { + return function ( $settings ) { + if ( ! is_array( $settings ) ) { + return array(); + } + + if ( isset( $settings['newEventsCalloutDismissedAt'] ) ) { + if ( ! is_int( $settings['newEventsCalloutDismissedAt'] ) ) { + $settings['newEventsCalloutDismissedAt'] = 0; + } + } + + if ( isset( $settings['lostEventsCalloutDismissedAt'] ) ) { + if ( ! is_int( $settings['lostEventsCalloutDismissedAt'] ) ) { + $settings['lostEventsCalloutDismissedAt'] = 0; + } + } + + return $settings; + }; + } +} diff --git a/includes/Core/User/REST_Conversion_Reporting_Controller.php b/includes/Core/User/REST_Conversion_Reporting_Controller.php new file mode 100644 index 00000000000..a1819f4b1ab --- /dev/null +++ b/includes/Core/User/REST_Conversion_Reporting_Controller.php @@ -0,0 +1,143 @@ +conversion_reporting_settings = $conversion_reporting_settings; + } + + /** + * Registers functionality through WordPress hooks. + * + * @since n.e.x.t + */ + public function register() { + add_filter( + 'googlesitekit_rest_routes', + function ( $routes ) { + return array_merge( $routes, $this->get_rest_routes() ); + } + ); + + add_filter( + 'googlesitekit_apifetch_preload_paths', + function ( $paths ) { + if ( Feature_Flags::enabled( 'conversionReporting' ) ) { + return array_merge( + $paths, + array( + '/' . REST_Routes::REST_ROOT . '/core/user/data/conversion-reporting-settings', + ) + ); + } + + return $paths; + } + ); + } + + /** + * Gets REST route instances. + * + * @since n.e.x.t + * + * @return REST_Route[] List of REST_Route objects. + */ + protected function get_rest_routes() { + $can_view_dashboard = function () { + return current_user_can( Permissions::VIEW_DASHBOARD ); + }; + + if ( ! Feature_Flags::enabled( 'conversionReporting' ) ) { + return array(); + } + + return array( + new REST_Route( + 'core/user/data/conversion-reporting-settings', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => function () { + return new WP_REST_Response( $this->conversion_reporting_settings->get() ); + }, + 'permission_callback' => $can_view_dashboard, + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => function ( WP_REST_Request $request ) { + $settings = $request['data']['settings']; + + $this->conversion_reporting_settings->merge( $settings ); + + return new WP_REST_Response( $this->conversion_reporting_settings->get() ); + }, + 'permission_callback' => $can_view_dashboard, + 'args' => array( + 'data' => array( + 'type' => 'object', + 'required' => true, + 'properties' => array( + 'settings' => array( + 'type' => 'object', + 'required' => true, + 'minProperties' => 1, + 'additionalProperties' => false, + 'properties' => array( + 'newEventsCalloutDismissedAt' => array( + 'type' => 'integer', + ), + 'lostEventsCalloutDismissedAt' => array( + 'type' => 'integer', + ), + ), + ), + ), + ), + ), + ), + ) + ), + ); + } +} diff --git a/includes/Core/User/User.php b/includes/Core/User/User.php index b5b4685f95e..cbb83e5a83a 100644 --- a/includes/Core/User/User.php +++ b/includes/Core/User/User.php @@ -13,7 +13,7 @@ use Google\Site_Kit\Core\Storage\User_Options; /** - * Class for handling audience settings rest routes. + * Class for handling user settings rest routes. * * @since 1.134.0 * @access private @@ -29,6 +29,14 @@ class User { */ private $audience_segmentation; + /** + * Conversion_Reporting instance. + * + * @since n.e.x.t + * @var Conversion_Reporting + */ + private $conversion_reporting; + /** * Constructor. * @@ -38,6 +46,7 @@ class User { */ public function __construct( User_Options $user_options ) { $this->audience_segmentation = new Audience_Segmentation( $user_options ); + $this->conversion_reporting = new Conversion_Reporting( $user_options ); } /** @@ -47,5 +56,6 @@ public function __construct( User_Options $user_options ) { */ public function register() { $this->audience_segmentation->register(); + $this->conversion_reporting->register(); } } diff --git a/includes/Core/User_Input/User_Input.php b/includes/Core/User_Input/User_Input.php index 6371b8cf1e5..718ffe0e44b 100644 --- a/includes/Core/User_Input/User_Input.php +++ b/includes/Core/User_Input/User_Input.php @@ -203,6 +203,12 @@ public function are_settings_empty( $settings = array() ) { } } + // Conversion events may be empty during setup if no events have been detected. + // Since this setting does not affect whether user input is considered "set up", + // we are excluding it from this check. It relates to user input initially being + // set up with detected events or events added later. + unset( $settings['includeConversionEvents'] ); + foreach ( $settings as $setting ) { if ( empty( $setting['values'] ) ) { return true; diff --git a/includes/Modules/AdSense.php b/includes/Modules/AdSense.php index eea8952db2a..6e83dcd2067 100644 --- a/includes/Modules/AdSense.php +++ b/includes/Modules/AdSense.php @@ -748,7 +748,6 @@ protected function setup_info() { 'slug' => self::MODULE_SLUG, 'name' => _x( 'AdSense', 'Service name', 'google-site-kit' ), 'description' => __( 'Earn money by placing ads on your website. It’s free and easy.', 'google-site-kit' ), - 'order' => 2, 'homepage' => add_query_arg( $idenfifier_args, 'https://adsense.google.com/start' ), ); } diff --git a/includes/Modules/Ads.php b/includes/Modules/Ads.php index 279af468092..5bdbc1844a4 100644 --- a/includes/Modules/Ads.php +++ b/includes/Modules/Ads.php @@ -208,7 +208,6 @@ protected function setup_info() { 'slug' => 'ads', 'name' => _x( 'Ads', 'Service name', 'google-site-kit' ), 'description' => Feature_Flags::enabled( 'adsPax' ) ? __( 'Grow sales, leads or awareness for your business by advertising with Google Ads', 'google-site-kit' ) : __( 'Track conversions for your existing Google Ads campaigns', 'google-site-kit' ), - 'order' => 1, 'homepage' => __( 'https://google.com/ads', 'google-site-kit' ), ); } diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index d9deecab838..649378bd037 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -707,15 +707,6 @@ protected function get_datapoint_definitions() { ); } - if ( Feature_Flags::enabled( 'conversionReporting' ) ) { - $datapoints['POST:clear-conversion-reporting-new-events'] = array( - 'service' => '', - ); - $datapoints['POST:clear-conversion-reporting-lost-events'] = array( - 'service' => '', - ); - } - return $datapoints; } @@ -1692,14 +1683,6 @@ protected function create_data_request( Data_Request $data ) { return function () use ( $data ) { return $this->transients->set( 'googlesitekit_inline_tag_id_mismatch', $data['hasMismatchedTag'] ); }; - case 'POST:clear-conversion-reporting-new-events': - return function () { - return $this->transients->delete( Conversion_Reporting_Events_Sync::DETECTED_EVENTS_TRANSIENT ); - }; - case 'POST:clear-conversion-reporting-lost-events': - return function () { - return $this->transients->delete( Conversion_Reporting_Events_Sync::LOST_EVENTS_TRANSIENT ); - }; } return parent::create_data_request( $data ); @@ -1839,7 +1822,6 @@ protected function setup_info() { 'slug' => self::MODULE_SLUG, 'name' => _x( 'Analytics', 'Service name', 'google-site-kit' ), 'description' => __( 'Get a deeper understanding of your customers. Google Analytics gives you the free tools you need to analyze data for your business in one place.', 'google-site-kit' ), - 'order' => 3, 'homepage' => __( 'https://analytics.google.com/analytics/web', 'google-site-kit' ), ); } diff --git a/includes/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_Sync.php b/includes/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_Sync.php index e291792ec55..ee72924ef34 100644 --- a/includes/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_Sync.php +++ b/includes/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_Sync.php @@ -127,10 +127,15 @@ public function sync_detected_events() { foreach ( $report->rows as $row ) { $detected_events[] = $row['dimensionValues'][0]['value']; } + $settings_partial = array( 'detectedEvents' => $detected_events ); - $this->maybe_update_new_and_lost_events( $detected_events, $saved_detected_events ); + $this->maybe_update_new_and_lost_events( + $detected_events, + $saved_detected_events, + $settings_partial + ); - $this->settings->merge( array( 'detectedEvents' => $detected_events ) ); + $this->settings->merge( $settings_partial ); } /** @@ -140,18 +145,21 @@ public function sync_detected_events() { * * @param array $detected_events Currently detected events array. * @param array $saved_detected_events Previously saved detected events array. + * @param array $settings_partial Analaytics settings partial. */ - protected function maybe_update_new_and_lost_events( $detected_events, $saved_detected_events ) { + protected function maybe_update_new_and_lost_events( $detected_events, $saved_detected_events, &$settings_partial ) { $new_events = array_diff( $detected_events, $saved_detected_events ); $lost_events = array_diff( $saved_detected_events, $detected_events ); if ( ! empty( $new_events ) ) { $this->transients->set( self::DETECTED_EVENTS_TRANSIENT, array_values( $new_events ) ); $this->new_badge_events_sync->sync_new_badge_events( $new_events ); + $settings_partial['newConversionEventsLastUpdateAt'] = time(); } if ( ! empty( $lost_events ) ) { $this->transients->set( self::LOST_EVENTS_TRANSIENT, array_values( $lost_events ) ); + $settings_partial['lostConversionEventsLastUpdateAt'] = time(); } if ( empty( $saved_detected_events ) ) { diff --git a/includes/Modules/Analytics_4/Settings.php b/includes/Modules/Analytics_4/Settings.php index 7a5976684e4..a113b452fc0 100644 --- a/includes/Modules/Analytics_4/Settings.php +++ b/includes/Modules/Analytics_4/Settings.php @@ -74,6 +74,8 @@ public function get_view_only_keys() { 'availableAudiences', 'audienceSegmentationSetupCompletedBy', 'detectedEvents', + 'newConversionEventsLastUpdateAt', + 'lostConversionEventsLastUpdateAt', ); } @@ -110,6 +112,8 @@ protected function get_default() { 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => null, 'detectedEvents' => array(), + 'newConversionEventsLastUpdateAt' => 0, + 'lostConversionEventsLastUpdateAt' => 0, ); } @@ -213,6 +217,18 @@ function ( $dimension ) { $option['audienceSegmentationSetupCompletedBy'] = null; } } + + if ( isset( $option['newConversionEventsLastUpdateAt'] ) ) { + if ( ! is_int( $option['newConversionEventsLastUpdateAt'] ) ) { + $option['newConversionEventsLastUpdateAt'] = 0; + } + } + + if ( isset( $option['lostConversionEventsLastUpdateAt'] ) ) { + if ( ! is_int( $option['lostConversionEventsLastUpdateAt'] ) ) { + $option['lostConversionEventsLastUpdateAt'] = 0; + } + } } return $option; diff --git a/includes/Modules/PageSpeed_Insights.php b/includes/Modules/PageSpeed_Insights.php index c4982f116f3..149d5cb9e51 100644 --- a/includes/Modules/PageSpeed_Insights.php +++ b/includes/Modules/PageSpeed_Insights.php @@ -180,7 +180,6 @@ protected function setup_info() { 'slug' => 'pagespeed-insights', 'name' => _x( 'PageSpeed Insights', 'Service name', 'google-site-kit' ), 'description' => __( 'Google PageSpeed Insights gives you metrics about performance, accessibility, SEO and PWA', 'google-site-kit' ), - 'order' => 4, 'homepage' => __( 'https://pagespeed.web.dev', 'google-site-kit' ), ); } diff --git a/includes/Modules/Reader_Revenue_Manager.php b/includes/Modules/Reader_Revenue_Manager.php index 78492224da5..c525a5892a7 100644 --- a/includes/Modules/Reader_Revenue_Manager.php +++ b/includes/Modules/Reader_Revenue_Manager.php @@ -332,7 +332,6 @@ protected function setup_info() { 'slug' => self::MODULE_SLUG, 'name' => _x( 'Reader Revenue Manager', 'Service name', 'google-site-kit' ), 'description' => __( 'Reader Revenue Manager helps publishers grow, retain, and engage their audiences, creating new revenue opportunities', 'google-site-kit' ), - 'order' => 5, 'homepage' => 'https://publishercenter.google.com', ); } diff --git a/includes/Modules/Sign_In_With_Google.php b/includes/Modules/Sign_In_With_Google.php index 3ecbfb0d9b4..fa2e0cc2a7d 100644 --- a/includes/Modules/Sign_In_With_Google.php +++ b/includes/Modules/Sign_In_With_Google.php @@ -226,7 +226,6 @@ protected function setup_info() { 'slug' => self::MODULE_SLUG, 'name' => _x( 'Sign in with Google', 'Service name', 'google-site-kit' ), 'description' => __( 'Improve user engagement, trust and data privacy, while creating a simple, secure and personalized experience for your visitors', 'google-site-kit' ), - 'order' => 10, 'homepage' => __( 'https://developers.google.com/identity/gsi/web/guides/overview', 'google-site-kit' ), ); } @@ -341,7 +340,7 @@ private function render_signin_button() { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams( response ) - }); + } ); if ( res.ok && res.redirected ) { location.assign( res.url ); } @@ -353,6 +352,7 @@ private function render_signin_button() { google.accounts.id.initialize( { client_id: '', callback: handleCredentialResponse, + library_name: 'Site-Kit', } ); google.accounts.id.renderButton( parent, ); @@ -369,7 +369,7 @@ private function render_signin_button() { } )(); \n", esc_html__( 'End Sign in with Google button added by Site Kit', 'google-site-kit' ) ); + print( "\n\n" ); } /** diff --git a/includes/Modules/Tag_Manager.php b/includes/Modules/Tag_Manager.php index c973e3b554b..5e3a9a7750c 100644 --- a/includes/Modules/Tag_Manager.php +++ b/includes/Modules/Tag_Manager.php @@ -466,7 +466,6 @@ protected function setup_info() { 'slug' => self::MODULE_SLUG, 'name' => _x( 'Tag Manager', 'Service name', 'google-site-kit' ), 'description' => __( 'Tag Manager creates an easy to manage way to create tags on your site without updating code', 'google-site-kit' ), - 'order' => 6, 'homepage' => __( 'https://tagmanager.google.com/', 'google-site-kit' ), ); } diff --git a/package-lock.json b/package-lock.json index 744a908e7eb..b7c9d500664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26990,9 +26990,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001687", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz", - "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==" + "version": "1.0.30001688", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", + "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==" }, "capture-exit": { "version": "2.0.0", diff --git a/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_0_small.png new file mode 100644 index 00000000000..47f7370baad Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_0_small.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_1_medium.png new file mode 100644 index 00000000000..326b00742db Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_2_large.png new file mode 100644 index 00000000000..edf26bf232c Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Components_KeyMetrics_ChipTabGroup_WithSuggestedGroup_0_document_2_large.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_0_small.png index 5ff33ce4a1f..71c88713561 100644 Binary files a/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_0_small.png and b/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_0_small.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_1_medium.png index 239a4cc6700..905cba4be97 100644 Binary files a/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_1_medium.png and b/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_2_large.png index 6cce80b6ce3..362db18fb83 100644 Binary files a/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_2_large.png and b/tests/backstop/reference/google-site-kit_Components_ReportTable_Basic_0_document_2_large.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_0_small.png index 146e7e548b0..11c3f52a2e7 100644 Binary files a/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_0_small.png and b/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_0_small.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_1_medium.png index 3ea782a9d5f..06c09698602 100644 Binary files a/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_1_medium.png and b/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_2_large.png index 160c09558b7..a8675ca1c97 100644 Binary files a/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_2_large.png and b/tests/backstop/reference/google-site-kit_Components_ReportTable_Tabbed_Layout_0_document_2_large.png differ diff --git a/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_0_small.png index 1e0262e06a0..79a95874ec2 100644 Binary files a/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_0_small.png and b/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_0_small.png differ diff --git a/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_1_medium.png index 54085f6c742..6c20ff648c2 100644 Binary files a/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_1_medium.png and b/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_2_large.png index fd5fa438c0d..5d790a530e5 100644 Binary files a/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_2_large.png and b/tests/backstop/reference/google-site-kit_Settings_Connect_More_Services_0_document_2_large.png differ diff --git a/tests/phpunit/integration/Core/Authentication/Clients/OAuth_ClientTest.php b/tests/phpunit/integration/Core/Authentication/Clients/OAuth_ClientTest.php index f265b53c30a..29778b7be9f 100644 --- a/tests/phpunit/integration/Core/Authentication/Clients/OAuth_ClientTest.php +++ b/tests/phpunit/integration/Core/Authentication/Clients/OAuth_ClientTest.php @@ -709,23 +709,6 @@ function ( Request $request ) { $this->assertTrue( $google_client_mock->shouldDefer() ); } - public function test_using_proxy() { - $this->setExpectedDeprecated( OAuth_Client::class . '::using_proxy' ); - $context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); - $client = new OAuth_Client( $context ); - - // Use proxy by default. - $this->assertTrue( $client->using_proxy() ); - - // Don't use proxy when regular OAuth client ID is used. - $this->fake_site_connection(); - $this->assertFalse( $client->using_proxy() ); - - // Use proxy when proxy site ID is used. - $this->fake_proxy_site_connection(); - $this->assertTrue( $client->using_proxy() ); - } - public function test_get_proxy_permissions_url() { $context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); diff --git a/tests/phpunit/integration/Core/Key_Metrics/REST_Key_Metrics_ControllerTest.php b/tests/phpunit/integration/Core/Key_Metrics/REST_Key_Metrics_ControllerTest.php index 4c4879a2bf3..dbd062363b1 100644 --- a/tests/phpunit/integration/Core/Key_Metrics/REST_Key_Metrics_ControllerTest.php +++ b/tests/phpunit/integration/Core/Key_Metrics/REST_Key_Metrics_ControllerTest.php @@ -309,29 +309,4 @@ public function provider_wrong_data() { ), ); } - - public function test_reset_key_metrics_selection() { - remove_all_filters( 'googlesitekit_rest_routes' ); - $this->controller->register(); - $this->register_rest_routes(); - - $original_settings = array( - 'widgetSlugs' => array( 'widgetA' ), - 'isWidgetHidden' => false, - ); - - $expected_change_in_settings = array( - 'widgetSlugs' => array(), - 'isWidgetHidden' => false, - ); - - $this->settings->register(); - $this->settings->set( $original_settings ); - - $request = new WP_REST_Request( 'POST', '/' . REST_Routes::REST_ROOT . '/core/user/data/reset-key-metrics-selection' ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertEqualSetsWithIndex( $expected_change_in_settings, $response->get_data() ); - $this->assertEqualSetsWithIndex( $expected_change_in_settings, $this->settings->get() ); - } } diff --git a/tests/phpunit/integration/Core/Site_Health/Tag_PlacementTest.php b/tests/phpunit/integration/Core/Site_Health/Tag_PlacementTest.php index 5a7afa1ffb5..f63a65c5147 100644 --- a/tests/phpunit/integration/Core/Site_Health/Tag_PlacementTest.php +++ b/tests/phpunit/integration/Core/Site_Health/Tag_PlacementTest.php @@ -130,7 +130,7 @@ public function test_get_active_modules_with_tags() { $result = $get_active_modules->invokeArgs( $site_status, array() ); - $this->assertEquals( + $this->assertEqualSets( array( 'adsense', 'analytics-4', diff --git a/tests/phpunit/integration/Core/User/Conversion_Reporting_SettingsTest.php b/tests/phpunit/integration/Core/User/Conversion_Reporting_SettingsTest.php new file mode 100644 index 00000000000..b0a0e74cf84 --- /dev/null +++ b/tests/phpunit/integration/Core/User/Conversion_Reporting_SettingsTest.php @@ -0,0 +1,146 @@ +factory()->user->create(); + $context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); + $user_options = new User_Options( $context, $user_id ); + $meta_key = $user_options->get_meta_key( Conversion_Reporting_Settings::OPTION ); + + unregister_meta_key( 'user', $meta_key ); + // Needed to unregister the instance registered during plugin bootstrap. + remove_all_filters( "sanitize_user_meta_{$meta_key}" ); + + $this->conversion_reporting_settings = new Conversion_Reporting_Settings( $user_options ); + + $this->conversion_reporting_settings->register(); + } + + public function test_get_default() { + $this->assertEquals( + array( + 'newEventsCalloutDismissedAt' => 0, + 'lostEventsCalloutDismissedAt' => 0, + ), + $this->conversion_reporting_settings->get() + ); + } + + public function data_conversion_reporting_settings() { + return array( + 'empty by default' => array( + null, + array(), + ), + 'non-array - bool' => array( + false, + array(), + ), + 'non-array - int' => array( + 123, + array(), + ), + 'non integer for newEventsCalloutDismissedAt and null lostEventsCalloutDismissedAt' => array( + array( + 'newEventsCalloutDismissedAt' => 'string', + 'lostEventsCalloutDismissedAt' => null, + ), + array( + 'newEventsCalloutDismissedAt' => 0, + 'lostEventsCalloutDismissedAt' => 0, + ), + ), + 'integer for both lostEventsCalloutDismissedAt and newEventsCalloutDismissedAt' => array( + array( + 'newEventsCalloutDismissedAt' => 1734519924, + 'lostEventsCalloutDismissedAt' => 1734519928, + ), + array( + 'newEventsCalloutDismissedAt' => 1734519924, + 'lostEventsCalloutDismissedAt' => 1734519928, + ), + ), + ); + } + + /** + * @dataProvider data_conversion_reporting_settings + * + * @param mixed $input Values to pass to the `set()` method. + * @param array $expected The expected sanitized array. + */ + public function test_get_sanitize_callback( $input, $expected ) { + $this->conversion_reporting_settings->set( $input ); + $this->assertEquals( $expected, $this->conversion_reporting_settings->get() ); + } + + public function test_merge() { + $original_settings = array( + 'newEventsCalloutDismissedAt' => 0, + 'lostEventsCalloutDismissedAt' => 0, + ); + + $changed_settings = array( + 'newEventsCalloutDismissedAt' => 1734519924, + 'lostEventsCalloutDismissedAt' => 0, + ); + + // Make sure settings can be updated even without having them set initially + $this->conversion_reporting_settings->merge( $original_settings ); + $this->assertEqualSetsWithIndex( $original_settings, $this->conversion_reporting_settings->get() ); + + // Make sure invalid keys aren't set + $this->conversion_reporting_settings->merge( array( 'test_key' => 'test_value' ) ); + $this->assertEqualSetsWithIndex( $original_settings, $this->conversion_reporting_settings->get() ); + + // Make sure that we can update settings partially + $this->conversion_reporting_settings->set( $original_settings ); + $this->conversion_reporting_settings->merge( array( 'newEventsCalloutDismissedAt' => 1734519924 ) ); + $this->assertEqualSetsWithIndex( + array( + 'newEventsCalloutDismissedAt' => 1734519924, + 'lostEventsCalloutDismissedAt' => $original_settings['lostEventsCalloutDismissedAt'], + ), + $this->conversion_reporting_settings->get() + ); + + // Make sure that we can update all settings at once + $this->conversion_reporting_settings->set( $original_settings ); + $this->conversion_reporting_settings->merge( $changed_settings ); + $this->assertEqualSetsWithIndex( $changed_settings, $this->conversion_reporting_settings->get() ); + + // Make sure that we can't set wrong format for the newEventsCalloutDismissedAt property + $this->conversion_reporting_settings->set( $original_settings ); + $this->conversion_reporting_settings->merge( array( 'newEventsCalloutDismissedAt' => null ) ); + $this->assertEqualSetsWithIndex( $original_settings, $this->conversion_reporting_settings->get() ); + + // Make sure that we can't set wrong format for the lostEventsCalloutDismissedAt property + $this->conversion_reporting_settings->set( $original_settings ); + $this->conversion_reporting_settings->merge( array( 'lostEventsCalloutDismissedAt' => null ) ); + $this->assertEqualSetsWithIndex( $original_settings, $this->conversion_reporting_settings->get() ); + } +} diff --git a/tests/phpunit/integration/Core/User/REST_Conversion_Reporting_ControllerTest.php b/tests/phpunit/integration/Core/User/REST_Conversion_Reporting_ControllerTest.php new file mode 100644 index 00000000000..6060e59e145 --- /dev/null +++ b/tests/phpunit/integration/Core/User/REST_Conversion_Reporting_ControllerTest.php @@ -0,0 +1,102 @@ +factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + + $context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); + $this->user_options = new User_Options( $context ); + $this->conversion_reporting_settings = new Conversion_Reporting_Settings( $this->user_options ); + $this->controller = new REST_Conversion_Reporting_Controller( + $this->conversion_reporting_settings + ); + } + + public function tear_down() { + parent::tear_down(); + // This ensures the REST server is initialized fresh for each test using it. + unset( $GLOBALS['wp_rest_server'] ); + } + + public function test_register() { + remove_all_filters( 'googlesitekit_rest_routes' ); + remove_all_filters( 'googlesitekit_apifetch_preload_paths' ); + + $this->controller->register(); + + $this->assertTrue( has_filter( 'googlesitekit_rest_routes' ) ); + $this->assertTrue( has_filter( 'googlesitekit_apifetch_preload_paths' ) ); + } + + public function test_get_routes__no_feature_flag() { + $this->controller->register(); + + $server = rest_get_server(); + $routes = array( + '/' . REST_Routes::REST_ROOT . '/core/user/data/conversion-reporting-settings', + + ); + $get_routes = array_intersect( $routes, array_keys( $server->get_routes() ) ); + + $this->assertTrue( empty( $get_routes ) ); + } + + public function test_get_routes__with_feature_flag() { + $this->enable_feature( 'conversionReporting' ); + $this->controller->register(); + + $server = rest_get_server(); + $routes = array( + '/' . REST_Routes::REST_ROOT . '/core/user/data/conversion-reporting-settings', + + ); + $get_routes = array_intersect( $routes, array_keys( $server->get_routes() ) ); + + $this->assertEqualSets( $routes, $get_routes ); + } +} diff --git a/tests/phpunit/integration/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_SyncTest.php b/tests/phpunit/integration/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_SyncTest.php index 81656eda6c4..cf7c79ac97b 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_SyncTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Conversion_Reporting/Conversion_Reporting_Events_SyncTest.php @@ -119,6 +119,70 @@ public function test_sync_detected_events_lost( $initially_saved_events, $lost_e $this->assertEquals( $transient_lost_events, $lost_events ); } + public function test_sync__newConversionEventsLastUpdateAt() { + $this->setup_fake_handler_and_analytics( + array( + array( + 'dimensionValues' => array( + array( + 'value' => 'contact', + ), + ), + ), + ) + ); + + $event_check = $this->get_instance(); + $this->settings->merge( + array( + 'detectedEvents' => array(), + ) + ); + + $this->assertEquals( 0, $this->settings->get()['newConversionEventsLastUpdateAt'] ); + + $event_check->sync_detected_events(); + + $transient_detected_events = $this->transients->get( Conversion_Reporting_Events_Sync::DETECTED_EVENTS_TRANSIENT ); + + $this->assertSame( $transient_detected_events, array( 'contact' ) ); + + // Verify that newConversionEventsLastUpdateAt is updated. + $this->assertEqualsWithDelta( time(), $this->settings->get()['newConversionEventsLastUpdateAt'], 2 ); + } + + public function test_sync__lostConversionEventsLastUpdateAt() { + $this->setup_fake_handler_and_analytics( + array( + array( + 'dimensionValues' => array( + array( + 'value' => 'contact', + ), + ), + ), + ) + ); + + $event_check = $this->get_instance(); + $this->settings->merge( + array( + 'detectedEvents' => array( 'purchase' ), + ) + ); + + $this->assertEquals( 0, $this->settings->get()['lostConversionEventsLastUpdateAt'] ); + + $event_check->sync_detected_events(); + + $transient_lost_events = $this->transients->get( Conversion_Reporting_Events_Sync::LOST_EVENTS_TRANSIENT ); + + $this->assertSame( $transient_lost_events, array( 'purchase' ) ); + + // Verify that lostConversionEventsLastUpdateAt is updated. + $this->assertEqualsWithDelta( time(), $this->settings->get()['lostConversionEventsLastUpdateAt'], 2 ); + } + public function get_instance() { return new Conversion_Reporting_Events_Sync( $this->settings, diff --git a/tests/phpunit/integration/Modules/Analytics_4/SettingsTest.php b/tests/phpunit/integration/Modules/Analytics_4/SettingsTest.php index 59e832e9f56..ea1669b3de2 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/SettingsTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/SettingsTest.php @@ -88,6 +88,8 @@ public function test_get_default() { 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => null, 'detectedEvents' => array(), + 'lostConversionEventsLastUpdateAt' => 0, + 'newConversionEventsLastUpdateAt' => 0, ), get_option( Settings::OPTION ) ); diff --git a/tests/phpunit/integration/Modules/Analytics_4Test.php b/tests/phpunit/integration/Modules/Analytics_4Test.php index 4db55df868c..59ab52291f5 100644 --- a/tests/phpunit/integration/Modules/Analytics_4Test.php +++ b/tests/phpunit/integration/Modules/Analytics_4Test.php @@ -551,6 +551,8 @@ function ( Request $request ) use ( $property_id, $webdatastream_id, $measuremen 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => null, 'detectedEvents' => array(), + 'lostConversionEventsLastUpdateAt' => 0, + 'newConversionEventsLastUpdateAt' => 0, ), $options->get( Settings::OPTION ) ); @@ -585,6 +587,8 @@ function ( Request $request ) use ( $property_id, $webdatastream_id, $measuremen 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => false, 'detectedEvents' => array(), + 'lostConversionEventsLastUpdateAt' => 0, + 'newConversionEventsLastUpdateAt' => 0, ), $options->get( Settings::OPTION ) ); @@ -712,6 +716,8 @@ function ( Request $request ) use ( $property_id, $webdatastream_id, $measuremen 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => null, 'detectedEvents' => array(), + 'lostConversionEventsLastUpdateAt' => 0, + 'newConversionEventsLastUpdateAt' => 0, ), $options->get( Settings::OPTION ) ); @@ -840,6 +846,8 @@ function ( Request $request ) use ( $property_id, $webdatastream_id, $measuremen 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => null, 'detectedEvents' => array(), + 'lostConversionEventsLastUpdateAt' => 0, + 'newConversionEventsLastUpdateAt' => 0, ), $options->get( Settings::OPTION ) ); @@ -877,6 +885,8 @@ function ( Request $request ) use ( $property_id, $webdatastream_id, $measuremen 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => false, 'detectedEvents' => array(), + 'lostConversionEventsLastUpdateAt' => 0, + 'newConversionEventsLastUpdateAt' => 0, ), $options->get( Settings::OPTION ) ); @@ -1307,8 +1317,6 @@ public function test_get_datapoints__conversionReporting() { 'sync-custom-dimensions', 'custom-dimension-data-available', 'set-google-tag-id-mismatch', - 'clear-conversion-reporting-new-events', - 'clear-conversion-reporting-lost-events', ), $this->analytics->get_datapoints() ); diff --git a/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php b/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php index 6f188e46816..bcf3d83e181 100644 --- a/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php +++ b/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php @@ -99,7 +99,7 @@ public function test_magic_methods() { $this->assertEquals( 'Reader Revenue Manager', $this->reader_revenue_manager->name ); $this->assertEquals( 'https://publishercenter.google.com', $this->reader_revenue_manager->homepage ); $this->assertEquals( 'Reader Revenue Manager helps publishers grow, retain, and engage their audiences, creating new revenue opportunities', $this->reader_revenue_manager->description ); - $this->assertEquals( 5, $this->reader_revenue_manager->order ); + $this->assertEquals( 10, $this->reader_revenue_manager->order ); // Since order is not set, it uses the default value. } public function test_get_scopes() {