diff --git a/assets/js/components/DashboardMainApp.js b/assets/js/components/DashboardMainApp.js
index 19656b31d8d..b4f5df010be 100644
--- a/assets/js/components/DashboardMainApp.js
+++ b/assets/js/components/DashboardMainApp.js
@@ -72,8 +72,11 @@ import { CORE_FORMS } from '../googlesitekit/datastore/forms/constants';
import OfflineNotification from './notifications/OfflineNotification';
import OverlayNotificationsRenderer from './OverlayNotification/OverlayNotificationsRenderer';
import ModuleDashboardEffects from './ModuleDashboardEffects';
-import { useMonitorInternetConnection } from '../hooks/useMonitorInternetConnection';
+import { useBreakpoint } from '../hooks/useBreakpoint';
import { useFeature } from '../hooks/useFeature';
+import { useMonitorInternetConnection } from '../hooks/useMonitorInternetConnection';
+import useQueryArg from '../hooks/useQueryArg';
+import { getContextScrollTop } from '../util/scroll';
export default function DashboardMainApp() {
const audienceSegmentationEnabled = useFeature( 'audienceSegmentation' );
@@ -82,6 +85,10 @@ export default function DashboardMainApp() {
const viewOnlyDashboard = useViewOnly();
+ const breakpoint = useBreakpoint();
+
+ const [ widgetArea, setWidgetArea ] = useQueryArg( 'widgetArea' );
+
const { setValues } = useDispatch( CORE_FORMS );
const grantedScopes = useSelect( ( select ) =>
@@ -100,10 +107,24 @@ export default function DashboardMainApp() {
);
useMount( () => {
+ // Render the current survey portal in 5 seconds after the initial rendering.
if ( ! viewOnlyDashboard ) {
- // Render the current survey portal in 5 seconds after the initial rendering.
setTimeout( () => setShowSurveyPortal( true ), 5000 );
}
+
+ // Scroll to a widget area if specified in the URL.
+ if ( widgetArea ) {
+ const widgetClass = `.googlesitekit-widget-area--${ widgetArea }`;
+
+ setTimeout( () => {
+ global.scrollTo( {
+ top: getContextScrollTop( widgetClass, breakpoint ),
+ behavior: 'smooth',
+ } );
+
+ setWidgetArea( undefined );
+ }, 100 );
+ }
} );
useEffect( () => {
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationSetupCTAWidget.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationSetupCTAWidget.js
index 5cf5c56b943..aa096163688 100644
--- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationSetupCTAWidget.js
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationSetupCTAWidget.js
@@ -25,9 +25,8 @@ import PropTypes from 'prop-types';
* WordPress dependencies
*/
import { compose } from '@wordpress/compose';
-import { addQueryArgs } from '@wordpress/url';
import { __ } from '@wordpress/i18n';
-import { Fragment, useCallback, useState, useEffect } from '@wordpress/element';
+import { Fragment, useCallback, useState } from '@wordpress/element';
/**
* Internal dependencies
@@ -42,9 +41,9 @@ import { CORE_USER } from '../../../../../googlesitekit/datastore/user/constants
import { CORE_SITE } from '../../../../../googlesitekit/datastore/site/constants';
import {
MODULES_ANALYTICS_4,
- EDIT_SCOPE,
AUDIENCE_SEGMENTATION_SETUP_FORM,
} from '../../../datastore/constants';
+import { SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION } from '../settings/SettingsCardVisitorGroups/SetupSuccess';
import { Button, SpinnerButton } from 'googlesitekit-components';
import { Cell, Grid, Row } from '../../../../../material-components';
import {
@@ -52,7 +51,6 @@ import {
BREAKPOINT_TABLET,
useBreakpoint,
} from '../../../../../hooks/useBreakpoint';
-import { ERROR_CODE_MISSING_REQUIRED_SCOPE } from '../../../../../util/errors';
import {
AdminMenuTooltip,
useShowTooltip,
@@ -60,19 +58,18 @@ import {
} from '../../../../../components/AdminMenuTooltip';
import { withWidgetComponentProps } from '../../../../../googlesitekit/widgets/util';
import { WEEK_IN_SECONDS } from '../../../../../util';
+import useEnableAudienceGroup from '../../../hooks/useEnableAudienceGroup';
import AudienceErrorModal from './AudienceErrorModal';
export const AUDIENCE_SEGMENTATION_SETUP_CTA_NOTIFICATION =
'audience_segmentation_setup_cta-notification';
function AudienceSegmentationSetupCTAWidget( { Widget, WidgetNull } ) {
- const [ isSaving, setIsSaving ] = useState( false );
const breakpoint = useBreakpoint();
const isMobileBreakpoint = breakpoint === BREAKPOINT_SMALL;
const isTabletBreakpoint = breakpoint === BREAKPOINT_TABLET;
const { setValues } = useDispatch( CORE_FORMS );
- const { setPermissionScopeError } = useDispatch( CORE_USER );
const showTooltip = useShowTooltip(
AUDIENCE_SEGMENTATION_SETUP_CTA_NOTIFICATION
);
@@ -91,16 +88,10 @@ function AudienceSegmentationSetupCTAWidget( { Widget, WidgetNull } ) {
)
);
- const { enableAudienceGroup } = useDispatch( MODULES_ANALYTICS_4 );
-
const configuredAudiences = useSelect( ( select ) =>
select( MODULES_ANALYTICS_4 ).getConfiguredAudiences()
);
- const hasAnalytics4EditScope = useSelect( ( select ) =>
- select( CORE_USER ).hasScope( EDIT_SCOPE )
- );
-
const autoSubmit = useSelect( ( select ) =>
select( CORE_FORMS ).getValue(
AUDIENCE_SEGMENTATION_SETUP_FORM,
@@ -108,80 +99,23 @@ function AudienceSegmentationSetupCTAWidget( { Widget, WidgetNull } ) {
)
);
- const redirectURL = addQueryArgs( global.location.href, {
- notification: 'audience_segmentation',
- } );
-
- const [ apiErrors, setApiErrors ] = useState( [] );
- const [ failedAudiences, setFailedAudiences ] = useState( [] );
const [ showErrorModal, setShowErrorModal ] = useState( false );
- const onEnableGroups = useCallback( async () => {
- setIsSaving( true );
-
- // If scope not granted, trigger scope error right away. These are
- // typically handled automatically based on API responses, but
- // this particular case has some special handling to improve UX.
- if ( ! hasAnalytics4EditScope ) {
- setValues( AUDIENCE_SEGMENTATION_SETUP_FORM, {
- autoSubmit: true,
- } );
-
- setPermissionScopeError( {
- code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
- message: __(
- 'Additional permissions are required to create new audiences in Analytics.',
- 'google-site-kit'
- ),
- data: {
- status: 403,
- scopes: [ EDIT_SCOPE ],
- skipModal: true,
- skipDefaultErrorNotifications: true,
- redirectURL,
- },
- } );
-
- return;
- }
-
- setValues( AUDIENCE_SEGMENTATION_SETUP_FORM, {
- autoSubmit: false,
+ const { dismissItem, dismissPrompt } = useDispatch( CORE_USER );
+
+ const { apiErrors, failedAudiences, isSaving, onEnableGroups } =
+ useEnableAudienceGroup( {
+ onSuccess: () => {
+ // Dismiss success notification in settings.
+ dismissItem(
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION
+ );
+ },
+ onError: () => {
+ setShowErrorModal( true );
+ },
} );
- const { error, failedSiteKitAudienceSlugs } =
- ( await enableAudienceGroup( failedAudiences ) ) || {};
-
- if ( error ) {
- setApiErrors( [ error ] );
- setFailedAudiences( [] );
- } else if ( Array.isArray( failedSiteKitAudienceSlugs ) ) {
- setFailedAudiences( failedSiteKitAudienceSlugs );
- setApiErrors( [] );
- } else {
- setApiErrors( [] );
- setFailedAudiences( [] );
- }
-
- setShowErrorModal( !! error || !! failedSiteKitAudienceSlugs );
- setIsSaving( false );
- }, [
- hasAnalytics4EditScope,
- setValues,
- enableAudienceGroup,
- failedAudiences,
- setPermissionScopeError,
- redirectURL,
- ] );
-
- // If the user ends up back on this component with the required scope granted,
- // and already submitted the form, trigger the submit again.
- useEffect( () => {
- if ( hasAnalytics4EditScope && autoSubmit ) {
- onEnableGroups();
- }
- }, [ hasAnalytics4EditScope, autoSubmit, onEnableGroups ] );
-
const analyticsIsDataAvailableOnLoad = useSelect( ( select ) => {
// We should call isGatheringData() within this component for completeness
// as we do not want to rely on it being called in other components.
@@ -192,7 +126,6 @@ function AudienceSegmentationSetupCTAWidget( { Widget, WidgetNull } ) {
return select( MODULES_ANALYTICS_4 ).isDataAvailableOnLoad();
} );
- const { dismissPrompt } = useDispatch( CORE_USER );
const handleDismissClick = async () => {
showTooltip();
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupCTA.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupCTA.js
new file mode 100644
index 00000000000..8226a8bb18c
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupCTA.js
@@ -0,0 +1,77 @@
+/**
+ * SettingsCardVisitorGroups SetupCTA component.
+ *
+ * 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.
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { AUDIENCE_SEGMENTATION_SETUP_CTA_NOTIFICATION } from '../../dashboard/AudienceSegmentationSetupCTAWidget';
+import { AUDIENCE_SEGMENTATION_SETUP_SUCCESS_NOTIFICATION } from '../../dashboard/AudienceSegmentationSetupSuccessSubtleNotification';
+import { CORE_USER } from '../../../../../../googlesitekit/datastore/user/constants';
+import { useDispatch, useSelect } from 'googlesitekit-data';
+import useEnableAudienceGroup from '../../../../hooks/useEnableAudienceGroup';
+import { ProgressBar } from 'googlesitekit-components';
+import Link from '../../../../../../components/Link';
+
+export default function SetupCTA() {
+ const { dismissItem } = useDispatch( CORE_USER );
+
+ const { isSaving, onEnableGroups } = useEnableAudienceGroup( {
+ redirectURL: global.location.href,
+ onSuccess: () => {
+ // Dismiss success notification in dashboard.
+ dismissItem( AUDIENCE_SEGMENTATION_SETUP_SUCCESS_NOTIFICATION );
+ },
+ } );
+
+ const isDismissed = useSelect( ( select ) =>
+ select( CORE_USER ).isPromptDismissed(
+ AUDIENCE_SEGMENTATION_SETUP_CTA_NOTIFICATION
+ )
+ );
+
+ if ( isDismissed === undefined || isDismissed ) {
+ return null;
+ }
+
+ return (
+
+
+ { __(
+ 'To set up new visitor groups for your site, Site Kit needs to update your Google Analytics property.',
+ 'google-site-kit'
+ ) }
+
+ { isSaving && (
+
+
{ __( 'Enabling groups', 'google-site-kit' ) }
+
+
+ ) }
+ { ! isSaving && (
+
+ { __( 'Enable groups', 'google-site-kit' ) }
+
+ ) }
+
+ );
+}
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupCTA.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupCTA.test.js
new file mode 100644
index 00000000000..d6a9c9cb499
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupCTA.test.js
@@ -0,0 +1,159 @@
+/**
+ * SettingsCardVisitorGroups SetupCTA component 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 { availableAudiences as audiencesFixture } from '../../../../datastore/__fixtures__';
+import {
+ createTestRegistry,
+ fireEvent,
+ freezeFetch,
+ muteFetch,
+ provideModules,
+ provideUserAuthentication,
+ render,
+} from '../../../../../../../../tests/js/test-utils';
+import { AUDIENCE_SEGMENTATION_SETUP_CTA_NOTIFICATION } from '../../dashboard/AudienceSegmentationSetupCTAWidget';
+import { CORE_USER } from '../../../../../../googlesitekit/datastore/user/constants';
+import {
+ EDIT_SCOPE,
+ MODULES_ANALYTICS_4,
+} from '../../../../datastore/constants';
+import SetupCTA from './SetupCTA';
+
+describe( 'SettingsCardVisitorGroups SetupCTA', () => {
+ let registry;
+
+ const audienceSettingsEndpoint = new RegExp(
+ '^/google-site-kit/v1/modules/analytics-4/data/audience-settings'
+ );
+ const reportEndpoint = new RegExp(
+ '^/google-site-kit/v1/modules/analytics-4/data/report'
+ );
+ const syncAvailableAudiencesEndpoint = new RegExp(
+ '^/google-site-kit/v1/modules/analytics-4/data/sync-audiences'
+ );
+
+ beforeEach( () => {
+ registry = createTestRegistry();
+
+ provideUserAuthentication( registry, {
+ grantedScopes: [ EDIT_SCOPE ],
+ } );
+
+ provideModules( registry, [
+ {
+ slug: 'analytics-4',
+ active: true,
+ connected: true,
+ },
+ ] );
+
+ registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( [] );
+
+ registry.dispatch( MODULES_ANALYTICS_4 ).setSettings( {
+ availableAudiences: null,
+ availableCustomDimensions: [ 'googlesitekit_post_type' ],
+ propertyID: '123456789',
+ } );
+
+ registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( {
+ configuredAudiences: null,
+ isAudienceSegmentationWidgetHidden: false,
+ } );
+ } );
+
+ it( 'should render the setup CTA', () => {
+ const { getByText, getByRole } = render( , { registry } );
+
+ expect(
+ getByText(
+ 'To set up new visitor groups for your site, Site Kit needs to update your Google Analytics property.'
+ )
+ ).toBeInTheDocument();
+
+ expect(
+ getByRole( 'button', { name: /Enable groups/i } )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should not render the setup CTA if dismissed', () => {
+ registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( {
+ [ AUDIENCE_SEGMENTATION_SETUP_CTA_NOTIFICATION ]: {
+ expires: 0,
+ count: 1,
+ },
+ } );
+
+ const { queryByText, queryByRole } = render( , {
+ registry,
+ } );
+
+ expect(
+ queryByText(
+ 'To set up new visitor groups for your site, Site Kit needs to update your Google Analytics property.'
+ )
+ ).not.toBeInTheDocument();
+
+ expect(
+ queryByRole( 'button', { name: /Enable groups/i } )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should show in progress state when enabling groups', () => {
+ freezeFetch( syncAvailableAudiencesEndpoint );
+
+ const { getByText, getByRole } = render( , { registry } );
+
+ fireEvent.click( getByRole( 'button', { name: /Enable groups/i } ) );
+
+ expect( getByText( 'Enabling groups' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should initialize the list of configured audiences when the CTA is clicked', () => {
+ fetchMock.postOnce( syncAvailableAudiencesEndpoint, {
+ status: 200,
+ body: audiencesFixture,
+ } );
+
+ fetchMock.postOnce( audienceSettingsEndpoint, {
+ status: 200,
+ body: {
+ configuredAudiences: [
+ audiencesFixture[ 3 ].name,
+ audiencesFixture[ 4 ].name,
+ ],
+ isAudienceSegmentationWidgetHidden: false,
+ },
+ } );
+
+ muteFetch( reportEndpoint );
+
+ const { getByText, getByRole } = render( , { registry } );
+
+ expect(
+ getByRole( 'button', { name: /Enable groups/i } )
+ ).toBeInTheDocument();
+
+ fireEvent.click( getByRole( 'button', { name: /Enable groups/i } ) );
+
+ expect( getByText( 'Enabling groups' ) ).toBeInTheDocument();
+ expect( fetchMock ).toHaveFetched( syncAvailableAudiencesEndpoint );
+ } );
+} );
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupSuccess.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupSuccess.js
new file mode 100644
index 00000000000..5cde9517e82
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupSuccess.js
@@ -0,0 +1,96 @@
+/**
+ * SettingsCardVisitorGroups SetupSuccess component.
+ *
+ * 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.
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import { AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION } from '../../../../../../googlesitekit/widgets/default-areas';
+import { CORE_LOCATION } from '../../../../../../googlesitekit/datastore/location/constants';
+import { CORE_SITE } from '../../../../../../googlesitekit/datastore/site/constants';
+import { CORE_USER } from '../../../../../../googlesitekit/datastore/user/constants';
+import { useDispatch, useSelect } from 'googlesitekit-data';
+import { Button } from 'googlesitekit-components';
+import CheckFill from '../../../../../../../svg/icons/check-fill.svg';
+
+export const SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION =
+ 'settings_visitor_groups_setup_success_notification';
+
+export default function SetupSuccess() {
+ const isDismissed = useSelect( ( select ) =>
+ select( CORE_USER ).isItemDismissed(
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION
+ )
+ );
+ const dashboardURL = useSelect( ( select ) => {
+ const url = select( CORE_SITE ).getAdminURL(
+ 'googlesitekit-dashboard'
+ );
+
+ return addQueryArgs( url, {
+ widgetArea: AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION,
+ } );
+ } );
+
+ const { navigateTo } = useDispatch( CORE_LOCATION );
+ const { dismissItem } = useDispatch( CORE_USER );
+
+ function dismissNotificationForUser() {
+ return dismissItem(
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION
+ );
+ }
+
+ async function scrollToWidgetArea() {
+ await dismissNotificationForUser();
+ navigateTo( dashboardURL );
+ }
+
+ if ( isDismissed === undefined || isDismissed ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ { __(
+ 'We’ve added the audiences section to your dashboard!',
+ 'google-site-kit'
+ ) }
+
+
+
+
+
+
+
+ );
+}
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupSuccess.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupSuccess.test.js
new file mode 100644
index 00000000000..016f4649139
--- /dev/null
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/SetupSuccess.test.js
@@ -0,0 +1,163 @@
+/**
+ * SettingsCardVisitorGroups SetupSuccess component 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.
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import {
+ createTestRegistry,
+ fireEvent,
+ provideSiteInfo,
+ render,
+ waitFor,
+} from '../../../../../../../../tests/js/test-utils';
+import { mockLocation } from '../../../../../../../../tests/js/mock-browser-utils';
+import { AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION } from '../../../../../../googlesitekit/widgets/default-areas';
+import { CORE_SITE } from '../../../../../../googlesitekit/datastore/site/constants';
+import { CORE_USER } from '../../../../../../googlesitekit/datastore/user/constants';
+import SetupSuccess, {
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION,
+} from './SetupSuccess';
+
+describe( 'SettingsCardVisitorGroups SetupSuccess', () => {
+ let registry;
+ let dismissItemSpy;
+
+ mockLocation();
+
+ beforeEach( () => {
+ registry = createTestRegistry();
+
+ provideSiteInfo( registry );
+
+ registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] );
+
+ dismissItemSpy = jest.spyOn(
+ registry.dispatch( CORE_USER ),
+ 'dismissItem'
+ );
+
+ dismissItemSpy.mockImplementation( () => Promise.resolve() );
+ } );
+
+ afterEach( () => {
+ dismissItemSpy.mockReset();
+ } );
+
+ it( 'should render the setup success notification', () => {
+ const { getByText, getByRole } = render( , {
+ registry,
+ } );
+
+ expect(
+ getByText( 'We’ve added the audiences section to your dashboard!' )
+ ).toBeInTheDocument();
+
+ expect(
+ getByRole( 'button', { name: /Got it/i } )
+ ).toBeInTheDocument();
+
+ expect(
+ getByRole( 'button', { name: /Show me/i } )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should not render the setup success notification if dismissed', () => {
+ registry
+ .dispatch( CORE_USER )
+ .receiveGetDismissedItems( [
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION,
+ ] );
+
+ const { queryByText, queryByRole } = render( , {
+ registry,
+ } );
+
+ expect(
+ queryByText(
+ 'We’ve added the audiences section to your dashboard!'
+ )
+ ).not.toBeInTheDocument();
+
+ expect(
+ queryByRole( 'button', { name: /Got it/i } )
+ ).not.toBeInTheDocument();
+
+ expect(
+ queryByRole( 'button', { name: /Show me/i } )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should dismiss the notification if "Got it" is clicked on', () => {
+ const { queryByText, getByRole } = render( , {
+ registry,
+ } );
+
+ expect(
+ queryByText(
+ 'We’ve added the audiences section to your dashboard!'
+ )
+ ).toBeInTheDocument();
+
+ fireEvent.click( getByRole( 'button', { name: /Got it/i } ) );
+
+ expect( dismissItemSpy ).toHaveBeenCalledTimes( 1 );
+ expect( dismissItemSpy ).toHaveBeenCalledWith(
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION
+ );
+ } );
+
+ it( 'should dismiss the notification and navigate to dashboard if "Show me" is clicked on', async () => {
+ const { queryByText, getByRole } = render( , {
+ registry,
+ } );
+
+ expect(
+ queryByText(
+ 'We’ve added the audiences section to your dashboard!'
+ )
+ ).toBeInTheDocument();
+
+ fireEvent.click( getByRole( 'button', { name: /Show me/i } ) );
+
+ expect( dismissItemSpy ).toHaveBeenCalledTimes( 1 );
+ expect( dismissItemSpy ).toHaveBeenCalledWith(
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION
+ );
+
+ const expectedURL = addQueryArgs(
+ registry
+ .select( CORE_SITE )
+ .getAdminURL( 'googlesitekit-dashboard' ),
+ {
+ widgetArea: AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION,
+ }
+ );
+
+ await waitFor( () => {
+ expect( global.location.assign ).toHaveBeenCalledWith(
+ expectedURL
+ );
+ } );
+ } );
+} );
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.js
similarity index 64%
rename from assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.js
rename to assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.js
index eac3d66dfe5..872d114b727 100644
--- a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.js
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.js
@@ -19,22 +19,27 @@
/**
* WordPress dependencies
*/
+import { Fragment, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useSelect, useDispatch } from 'googlesitekit-data';
+import { MODULES_ANALYTICS_4 } from '../../../../datastore/constants';
import { Switch } from 'googlesitekit-components';
-import { Cell, Grid, Row } from '../../../../../material-components';
-import Layout from '../../../../../components/layout/Layout';
-import { MODULES_ANALYTICS_4 } from '../../../datastore/constants';
+import { Cell, Grid, Row } from '../../../../../../material-components';
+import Layout from '../../../../../../components/layout/Layout';
+import SetupCTA from './SetupCTA';
+import SetupSuccess from './SetupSuccess';
export default function SettingsCardVisitorGroups() {
const audienceSegmentationWidgetHidden = useSelect( ( select ) =>
select( MODULES_ANALYTICS_4 ).isAudienceSegmentationWidgetHidden()
);
+ const configuredAudiences = useSelect( ( select ) =>
+ select( MODULES_ANALYTICS_4 ).getConfiguredAudiences()
+ );
const { setAudienceSegmentationWidgetHidden, saveAudienceSettings } =
useDispatch( MODULES_ANALYTICS_4 );
@@ -50,6 +55,10 @@ export default function SettingsCardVisitorGroups() {
setAudienceSegmentationWidgetHidden,
] );
+ if ( configuredAudiences === undefined ) {
+ return null;
+ }
+
return (
-
+ { ! configuredAudiences?.length && }
+ { !! configuredAudiences?.length && (
+
+
+
+
+ ) }
|
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.stories.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.stories.js
similarity index 57%
rename from assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.stories.js
rename to assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.stories.js
index dc2236e5f77..acaff376c8e 100644
--- a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.stories.js
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.stories.js
@@ -24,9 +24,11 @@ import fetchMock from 'fetch-mock';
/**
* Internal dependencies
*/
-import { MODULES_ANALYTICS_4 } from '../../../datastore/constants';
-import WithRegistrySetup from '../../../../../../../tests/js/WithRegistrySetup';
-import SettingsCardVisitorGroups from './SettingsCardVisitorGroups';
+import { CORE_USER } from '../../../../../../googlesitekit/datastore/user/constants';
+import { MODULES_ANALYTICS_4 } from '../../../../datastore/constants';
+import { SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION } from './SetupSuccess';
+import WithRegistrySetup from '../../../../../../../../tests/js/WithRegistrySetup';
+import SettingsCardVisitorGroups from './';
function Template() {
return ;
@@ -38,11 +40,42 @@ Default.scenario = {
label: 'Modules/Analytics4/Components/AudienceSegmentation/Settings/SettingsCardVisitorGroups/Default',
};
+export const WithSetupCTA = Template.bind( {} );
+WithSetupCTA.storyName = 'With setup CTA';
+WithSetupCTA.args = {
+ setupRegistry: ( registry ) => {
+ registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( {
+ configuredAudiences: [],
+ isAudienceSegmentationWidgetHidden: false,
+ } );
+ },
+};
+WithSetupCTA.scenario = {
+ label: 'Modules/Analytics4/Components/AudienceSegmentation/Settings/SettingsCardVisitorGroups/WithSetupCTA',
+};
+
+export const WithSetupSuccessNotification = Template.bind( {} );
+WithSetupSuccessNotification.storyName = 'With setup success notification';
+WithSetupSuccessNotification.args = {
+ setupRegistry: ( registry ) => {
+ registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] );
+ },
+};
+WithSetupSuccessNotification.scenario = {
+ label: 'Modules/Analytics4/Components/AudienceSegmentation/Settings/SettingsCardVisitorGroups/WithSetupSuccessNotification',
+};
+
export default {
title: 'Modules/Analytics4/Components/AudienceSegmentation/Settings/SettingsCardVisitorGroups',
decorators: [
( Story, { args } ) => {
const setupRegistry = ( registry ) => {
+ registry
+ .dispatch( CORE_USER )
+ .receiveGetDismissedItems( [
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION,
+ ] );
+
registry
.dispatch( MODULES_ANALYTICS_4 )
.receiveGetAudienceSettings( {
diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.test.js
similarity index 64%
rename from assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.test.js
rename to assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.test.js
index 72279be1cd8..631f366de7d 100644
--- a/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups.test.js
+++ b/assets/js/modules/analytics-4/components/audience-segmentation/settings/SettingsCardVisitorGroups/index.test.js
@@ -16,16 +16,62 @@
* limitations under the License.
*/
-import { render, waitFor } from '../../../../../../../tests/js/test-utils';
-import { createTestRegistry } from '../../../../../../../tests/js/utils';
-import { MODULES_ANALYTICS_4 } from '../../../datastore/constants';
-import SettingsCardVisitorGroups from './SettingsCardVisitorGroups';
+import { render, waitFor } from '../../../../../../../../tests/js/test-utils';
+import {
+ createTestRegistry,
+ provideUserAuthentication,
+} from '../../../../../../../../tests/js/utils';
+import { CORE_USER } from '../../../../../../googlesitekit/datastore/user/constants';
+import { MODULES_ANALYTICS_4 } from '../../../../datastore/constants';
+import { SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION } from './SetupSuccess';
+import SettingsCardVisitorGroups from './';
describe( 'SettingsCardVisitorGroups', () => {
let registry;
beforeEach( () => {
registry = createTestRegistry();
+
+ provideUserAuthentication( registry );
+
+ registry
+ .dispatch( CORE_USER )
+ .receiveGetDismissedItems( [
+ SETTINGS_VISITOR_GROUPS_SETUP_SUCCESS_NOTIFICATION,
+ ] );
+
+ registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( [] );
+ } );
+
+ it( 'should render the setup CTA if groups are not configured', () => {
+ registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( {
+ configuredAudiences: [],
+ isAudienceSegmentationWidgetHidden: false,
+ } );
+
+ const { getByRole } = render( , {
+ registry,
+ } );
+
+ expect(
+ getByRole( 'button', { name: /Enable groups/i } )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should render the setup success notification once groups are configured', () => {
+ registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] );
+ registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( {
+ configuredAudiences: [ 'audienceA', 'audienceB' ],
+ isAudienceSegmentationWidgetHidden: false,
+ } );
+
+ const { getByText } = render( , {
+ registry,
+ } );
+
+ expect(
+ getByText( 'We’ve added the audiences section to your dashboard!' )
+ ).toBeInTheDocument();
} );
it( 'should render the visitor groups switch correctly', async () => {
diff --git a/assets/js/modules/analytics-4/hooks/useEnableAudienceGroup.js b/assets/js/modules/analytics-4/hooks/useEnableAudienceGroup.js
new file mode 100644
index 00000000000..4a6fbc950c1
--- /dev/null
+++ b/assets/js/modules/analytics-4/hooks/useEnableAudienceGroup.js
@@ -0,0 +1,155 @@
+/**
+ * Analytics useEnableAudienceGroup custom hook.
+ *
+ * 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 { useMountedState } from 'react-use';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useCallback, useEffect, useState } from '@wordpress/element';
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import { useDispatch, useSelect } from 'googlesitekit-data';
+import { CORE_FORMS } from '../../../googlesitekit/datastore/forms/constants';
+import { CORE_USER } from '../../../googlesitekit/datastore/user/constants';
+import { ERROR_CODE_MISSING_REQUIRED_SCOPE } from '../../../util/errors';
+import {
+ AUDIENCE_SEGMENTATION_SETUP_FORM,
+ EDIT_SCOPE,
+ MODULES_ANALYTICS_4,
+} from '../datastore/constants';
+
+export default function useEnableAudienceGroup( {
+ redirectURL,
+ onSuccess,
+ onError,
+} = {} ) {
+ const isMounted = useMountedState();
+
+ const [ apiErrors, setApiErrors ] = useState( [] );
+ const [ failedAudiences, setFailedAudiences ] = useState( [] );
+ const [ isSaving, setIsSaving ] = useState( false );
+
+ const hasAnalytics4EditScope = useSelect( ( select ) =>
+ select( CORE_USER ).hasScope( EDIT_SCOPE )
+ );
+ const autoSubmit = useSelect( ( select ) =>
+ select( CORE_FORMS ).getValue(
+ AUDIENCE_SEGMENTATION_SETUP_FORM,
+ 'autoSubmit'
+ )
+ );
+
+ const { setValues } = useDispatch( CORE_FORMS );
+ const { setPermissionScopeError } = useDispatch( CORE_USER );
+ const { enableAudienceGroup } = useDispatch( MODULES_ANALYTICS_4 );
+
+ if ( ! redirectURL ) {
+ redirectURL = addQueryArgs( global.location.href, {
+ notification: 'audience_segmentation',
+ } );
+ }
+
+ const onEnableGroups = useCallback( async () => {
+ setIsSaving( true );
+
+ // If scope is not granted, trigger scope error right away. These are
+ // typically handled automatically based on API responses, but
+ // this particular case has some special handling to improve UX.
+ if ( ! hasAnalytics4EditScope ) {
+ setValues( AUDIENCE_SEGMENTATION_SETUP_FORM, {
+ autoSubmit: true,
+ } );
+
+ setPermissionScopeError( {
+ code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
+ message: __(
+ 'Additional permissions are required to create new audiences in Analytics.',
+ 'google-site-kit'
+ ),
+ data: {
+ status: 403,
+ scopes: [ EDIT_SCOPE ],
+ skipModal: true,
+ skipDefaultErrorNotifications: true,
+ redirectURL,
+ },
+ } );
+
+ return;
+ }
+
+ setValues( AUDIENCE_SEGMENTATION_SETUP_FORM, { autoSubmit: false } );
+
+ const { error, failedSiteKitAudienceSlugs } =
+ ( await enableAudienceGroup( failedAudiences ) ) || {};
+
+ if ( !! error || !! failedSiteKitAudienceSlugs ) {
+ onError?.();
+ } else {
+ onSuccess?.();
+ }
+
+ if ( isMounted() ) {
+ if ( error ) {
+ setApiErrors( [ error ] );
+ setFailedAudiences( [] );
+ } else if ( Array.isArray( failedSiteKitAudienceSlugs ) ) {
+ setFailedAudiences( failedSiteKitAudienceSlugs );
+ setApiErrors( [] );
+ } else {
+ setApiErrors( [] );
+ setFailedAudiences( [] );
+ }
+
+ setIsSaving( false );
+ }
+ }, [
+ hasAnalytics4EditScope,
+ setValues,
+ enableAudienceGroup,
+ failedAudiences,
+ isMounted,
+ setPermissionScopeError,
+ redirectURL,
+ onError,
+ onSuccess,
+ ] );
+
+ // If the user returns to the component using this hook with the required
+ // scope granted, and already submitted the form, trigger the submit again.
+ useEffect( () => {
+ if ( hasAnalytics4EditScope && autoSubmit ) {
+ onEnableGroups();
+ }
+ }, [ hasAnalytics4EditScope, autoSubmit, onEnableGroups ] );
+
+ return {
+ apiErrors,
+ failedAudiences,
+ isSaving,
+ onEnableGroups,
+ };
+}
diff --git a/assets/js/modules/analytics-4/hooks/useEnableAudienceGroup.test.js b/assets/js/modules/analytics-4/hooks/useEnableAudienceGroup.test.js
new file mode 100644
index 00000000000..c280ee14b5d
--- /dev/null
+++ b/assets/js/modules/analytics-4/hooks/useEnableAudienceGroup.test.js
@@ -0,0 +1,224 @@
+/**
+ * Analytics useEnableAudienceGroup hook 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 {
+ AUDIENCE_SEGMENTATION_SETUP_FORM,
+ EDIT_SCOPE,
+ MODULES_ANALYTICS_4,
+} from '../datastore/constants';
+import { CORE_FORMS } from '../../../googlesitekit/datastore/forms/constants';
+import { CORE_USER } from '../../../googlesitekit/datastore/user/constants';
+import { availableAudiences as audiencesFixture } from '../datastore/__fixtures__';
+import { actHook, renderHook } from '../../../../../tests/js/test-utils';
+import {
+ createTestRegistry,
+ freezeFetch,
+ muteFetch,
+ provideModules,
+ provideUserAuthentication,
+ waitForTimeouts,
+} from '../../../../../tests/js/utils';
+import useEnableAudienceGroup from './useEnableAudienceGroup';
+
+describe( 'useEnableAudienceGroup', () => {
+ let registry;
+ let enableAudienceGroupSpy;
+
+ const audienceSettingsEndpoint = new RegExp(
+ '^/google-site-kit/v1/modules/analytics-4/data/audience-settings'
+ );
+ const reportEndpoint = new RegExp(
+ '^/google-site-kit/v1/modules/analytics-4/data/report'
+ );
+ const syncAvailableAudiencesEndpoint = new RegExp(
+ '^/google-site-kit/v1/modules/analytics-4/data/sync-audiences'
+ );
+
+ beforeEach( () => {
+ registry = createTestRegistry();
+
+ enableAudienceGroupSpy = jest.spyOn(
+ registry.dispatch( MODULES_ANALYTICS_4 ),
+ 'enableAudienceGroup'
+ );
+
+ provideUserAuthentication( registry, {
+ grantedScopes: [ EDIT_SCOPE ],
+ } );
+
+ provideModules( registry, [
+ {
+ slug: 'analytics-4',
+ active: true,
+ connected: true,
+ },
+ ] );
+
+ registry.dispatch( MODULES_ANALYTICS_4 ).setSettings( {
+ availableAudiences: null,
+ availableCustomDimensions: [ 'googlesitekit_post_type' ],
+ propertyID: '123456789',
+ } );
+
+ registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( {
+ configuredAudiences: null,
+ isAudienceSegmentationWidgetHidden: false,
+ } );
+ } );
+
+ afterEach( () => {
+ enableAudienceGroupSpy.mockRestore();
+ } );
+
+ it( 'should return an object with appropriate properties', () => {
+ const { result } = renderHook( () => useEnableAudienceGroup(), {
+ registry,
+ } );
+
+ expect( Object.keys( result.current ) ).toEqual( [
+ 'apiErrors',
+ 'failedAudiences',
+ 'isSaving',
+ 'onEnableGroups',
+ ] );
+
+ const { isSaving, onEnableGroups } = result.current;
+
+ expect( typeof isSaving ).toBe( 'boolean' );
+ expect( typeof onEnableGroups ).toBe( 'function' );
+ } );
+
+ it( 'should set `isSaving` to true when `onEnableGroups` is called', () => {
+ freezeFetch( syncAvailableAudiencesEndpoint );
+
+ const { result } = renderHook( () => useEnableAudienceGroup(), {
+ registry,
+ } );
+
+ const { isSaving, onEnableGroups } = result.current;
+
+ expect( isSaving ).toBe( false );
+
+ actHook( () => {
+ onEnableGroups();
+ } );
+
+ expect( result.current.isSaving ).toBe( true );
+ } );
+
+ it( 'should set permission scope error when `onEnableGroups` is called but the user does not have the required scope', () => {
+ provideUserAuthentication( registry, {
+ grantedScopes: [],
+ } );
+
+ const { result } = renderHook( () => useEnableAudienceGroup(), {
+ registry,
+ } );
+
+ const { onEnableGroups } = result.current;
+
+ actHook( () => {
+ onEnableGroups();
+ } );
+
+ const { message } = registry
+ .select( CORE_USER )
+ .getPermissionScopeError();
+
+ expect( message ).toBe(
+ 'Additional permissions are required to create new audiences in Analytics.'
+ );
+
+ expect( enableAudienceGroupSpy ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should automatically call `onEnableGroups` function when user returns from the OAuth screen', async () => {
+ fetchMock.postOnce( syncAvailableAudiencesEndpoint, {
+ status: 200,
+ body: audiencesFixture,
+ } );
+
+ fetchMock.postOnce( audienceSettingsEndpoint, {
+ status: 200,
+ body: {
+ configuredAudiences: [
+ audiencesFixture[ 3 ].name,
+ audiencesFixture[ 4 ].name,
+ ],
+ isAudienceSegmentationWidgetHidden: false,
+ },
+ } );
+
+ muteFetch( reportEndpoint );
+
+ // Set autoSubmit to true.
+ registry
+ .dispatch( CORE_FORMS )
+ .setValues( AUDIENCE_SEGMENTATION_SETUP_FORM, {
+ autoSubmit: true,
+ } );
+
+ // eslint-disable-next-line require-await
+ await actHook( async () => {
+ renderHook( () => useEnableAudienceGroup(), {
+ registry,
+ } );
+ } );
+
+ expect( enableAudienceGroupSpy ).toHaveBeenCalledTimes( 1 );
+
+ await actHook( () => waitForTimeouts( 30 ) );
+ } );
+
+ it( 'should dispatch the `enableAudienceGroup` action when `onEnableGroups` is called', async () => {
+ fetchMock.postOnce( syncAvailableAudiencesEndpoint, {
+ status: 200,
+ body: audiencesFixture,
+ } );
+
+ fetchMock.postOnce( audienceSettingsEndpoint, {
+ status: 200,
+ body: {
+ configuredAudiences: [
+ audiencesFixture[ 3 ].name,
+ audiencesFixture[ 4 ].name,
+ ],
+ isAudienceSegmentationWidgetHidden: false,
+ },
+ } );
+
+ muteFetch( reportEndpoint );
+
+ const { result } = renderHook( () => useEnableAudienceGroup(), {
+ registry,
+ } );
+
+ const { onEnableGroups } = result.current;
+
+ await actHook( async () => {
+ await onEnableGroups();
+ } );
+
+ expect( enableAudienceGroupSpy ).toHaveBeenCalledTimes( 1 );
+
+ await actHook( () => waitForTimeouts( 30 ) );
+ } );
+} );
diff --git a/assets/sass/admin.scss b/assets/sass/admin.scss
index dc14f79e308..36b269b93e0 100644
--- a/assets/sass/admin.scss
+++ b/assets/sass/admin.scss
@@ -182,6 +182,7 @@
@import "components/settings/googlesitekit-settings-module";
@import "components/settings/googlesitekit-settings-notice";
@import "components/settings/googlesitekit-settings-user-input";
+@import "components/settings/googlesitekit-settings-visitor-groups";
// User Input
@import "components/user-input/googlesitekit-user-input-module";
diff --git a/assets/sass/components/settings/_googlesitekit-settings-visitor-groups.scss b/assets/sass/components/settings/_googlesitekit-settings-visitor-groups.scss
new file mode 100644
index 00000000000..f239942e968
--- /dev/null
+++ b/assets/sass/components/settings/_googlesitekit-settings-visitor-groups.scss
@@ -0,0 +1,47 @@
+/**
+ * Settings Visitor Groups styles.
+ *
+ * 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.
+ */
+
+.googlesitekit-plugin {
+
+ .googlesitekit-settings-visitor-groups__setup {
+
+ p {
+ font-size: $fs-body-sm;
+ letter-spacing: $ls-xs;
+ line-height: $lh-body-sm;
+ margin-top: 0;
+ }
+
+ .googlesitekit-cta-link {
+ font-weight: $fw-medium;
+ margin-top: 6px;
+ }
+ }
+
+ .googlesitekit-settings-visitor-groups__setup-progress {
+ margin-top: $grid-gap-phone;
+
+ p {
+ margin-bottom: 10px;
+ }
+ }
+
+ .googlesitekit-settings-visitor-groups__setup-success {
+ margin-bottom: 30px;
+ }
+}
diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_0_small.png
new file mode 100644
index 00000000000..91b1ead4eb2
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_0_small.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_1_medium.png
new file mode 100644
index 00000000000..0192952091c
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_1_medium.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_2_large.png
new file mode 100644
index 00000000000..52cfc537e39
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupCTA_0_document_2_large.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_0_small.png
new file mode 100644
index 00000000000..00d664922cf
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_0_small.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_1_medium.png
new file mode 100644
index 00000000000..62b91f2b0f2
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_1_medium.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_2_large.png
new file mode 100644
index 00000000000..21a850cc89e
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Settings_SettingsCardVisitorGroups_WithSetupSuccessNotification_0_document_2_large.png differ