diff --git a/assets/js/components/DashboardMainApp.js b/assets/js/components/DashboardMainApp.js index ce4aa3a1757..007a28fe437 100644 --- a/assets/js/components/DashboardMainApp.js +++ b/assets/js/components/DashboardMainApp.js @@ -46,7 +46,6 @@ import { AudienceSegmentationSetupCTAWidget, AudienceSelectionPanel, } from '../modules/analytics-4/components/audience-segmentation/dashboard'; -import ReaderRevenueManagerSetupCTABanner from '../modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner'; import EntitySearchInput from './EntitySearchInput'; import DateRangeSelector from './DateRangeSelector'; import HelpMenu from './help/HelpMenu'; @@ -88,7 +87,6 @@ import { export default function DashboardMainApp() { const audienceSegmentationEnabled = useFeature( 'audienceSegmentation' ); - const readerRevenueManagerEnabled = useFeature( 'rrmModule' ); const [ showSurveyPortal, setShowSurveyPortal ] = useState( false ); @@ -276,14 +274,6 @@ export default function DashboardMainApp() { ) } - { ! viewOnlyDashboard && ( - - { readerRevenueManagerEnabled && ( - - ) } - - ) } - { - if ( breakpoint === BREAKPOINT_SMALL ) { - return FPMSetupCTASVGMobile; - } - - if ( breakpoint === BREAKPOINT_TABLET ) { - return FPMSetupCTASVGTablet; - } - - return FPMSetupCTASVGDesktop; + const breakpointSVGMap = { + [ BREAKPOINT_SMALL ]: FPMSetupCTASVGMobile, + [ BREAKPOINT_TABLET ]: FPMSetupCTASVGTablet, }; return ( @@ -201,7 +194,7 @@ export default function FirstPartyModeSetupBanner( { id, Notification } ) { } } /> } - SVG={ getBannerSVG() } + SVG={ breakpointSVGMap[ breakpoint ] || FPMSetupCTASVGDesktop } /> ); diff --git a/assets/js/googlesitekit/notifications/datastore/notifications.js b/assets/js/googlesitekit/notifications/datastore/notifications.js index 3176e087fc4..53a7a3d7ea5 100644 --- a/assets/js/googlesitekit/notifications/datastore/notifications.js +++ b/assets/js/googlesitekit/notifications/datastore/notifications.js @@ -64,10 +64,11 @@ export const actions = { * @param {WPComponent} [settings.Component] React component used to display the contents of this notification. * @param {number} [settings.priority] Notification's priority for ordering (lower number is higher priority, like WordPress hooks). Ideally in increments of 10. Default 10. * @param {string} [settings.areaSlug] The slug of the area where the notification should be rendered, e.g. notification-area-banners-above-nav. - * @param {string} [settings.groupID] The ID of the group of notifications that should be rendered in their own individual queue. + * @param {string} [settings.groupID] Optional. The ID of the group of notifications that should be rendered in their own individual queue. Default 'default'. * @param {Array.} [settings.viewContexts] Array of Site Kit contexts, e.g. VIEW_CONTEXT_MAIN_DASHBOARD. * @param {Function} [settings.checkRequirements] Optional. Callback function to determine if the notification should be queued. - * @param {boolean} [settings.isDismissible] Flag to check if the notification should be queued and is not dismissed. + * @param {boolean} [settings.isDismissible] Optional. Flag to check if the notification should be queued and is not dismissed. + * @param {number} [settings.dismissRetries] Optional. An integer number denoting how many times a notification should be shown again on dismissal. Default 0. * @return {Object} Redux-style action. */ registerNotification( @@ -80,6 +81,7 @@ export const actions = { viewContexts, checkRequirements, isDismissible, + dismissRetries = 0, } ) { invariant( @@ -117,6 +119,7 @@ export const actions = { viewContexts, checkRequirements, isDismissible, + dismissRetries, }, }, type: REGISTER_NOTIFICATION, @@ -226,6 +229,25 @@ export const actions = { return; } + // Use prompts if a notification should be shown again until it + // is dismissed for a certain number of retries. + if ( notification.dismissRetries > 0 ) { + const dismissCount = registry + .select( CORE_USER ) + .getPromptDismissCount( id ); + + const expirationInSeconds = + dismissCount < notification.dismissRetries + ? expiresInSeconds + : 0; + + return yield commonActions.await( + registry.dispatch( CORE_USER ).dismissPrompt( id, { + expiresInSeconds: expirationInSeconds, + } ) + ); + } + return yield commonActions.await( registry .dispatch( CORE_USER ) @@ -407,8 +429,8 @@ export const selectors = { /** * Determines whether a notification is dismissed or not. * - * Currently, this selector simply forwards the call to the dismissed items API. - * We can potentially add more notification-specific logic here in the future. + * If the notification should appear again for a certain number of times after dismissal, + * then we store them as prompts. So we check for dismissed prompts instead of dismissed items. * * @since 1.132.0 * @@ -418,9 +440,59 @@ export const selectors = { */ isNotificationDismissed: createRegistrySelector( ( select ) => ( state, id ) => { + const notification = + select( CORE_NOTIFICATIONS ).getNotification( id ); + + if ( notification === undefined ) { + return undefined; + } + + if ( notification.dismissRetries > 0 ) { + return select( CORE_USER ).isPromptDismissed( id ); + } + return select( CORE_USER ).isItemDismissed( id ); } ), + /** + * Determines whether a notification that can reappear again for a fixed number of times + * on dismissal is at its final appearance. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @param {string} id Notification id. + * @return {(boolean|undefined)} TRUE if notification is on its final retry, otherwise FALSE, `undefined` if not resolved yet. + */ + isNotificationDismissalFinal: createRegistrySelector( + ( select ) => ( state, id ) => { + const notification = + select( CORE_NOTIFICATIONS ).getNotification( id ); + + if ( notification === undefined ) { + return undefined; + } + + invariant( + notification.isDismissible, + 'Notification should be dismissible to check if a notification is on its final dismissal.' + ); + + // If a notification does not have retries, it always will be on its final render. + if ( notification.dismissRetries === 0 ) { + return true; + } + + const dismissCount = + select( CORE_USER ).getPromptDismissCount( id ); + + if ( dismissCount >= notification.dismissRetries ) { + return true; + } + + return false; + } + ), }; export default { diff --git a/assets/js/googlesitekit/notifications/datastore/notifications.test.js b/assets/js/googlesitekit/notifications/datastore/notifications.test.js index 5834c641977..15ea9c54590 100644 --- a/assets/js/googlesitekit/notifications/datastore/notifications.test.js +++ b/assets/js/googlesitekit/notifications/datastore/notifications.test.js @@ -21,6 +21,7 @@ */ import { createTestRegistry, + provideNotifications, untilResolved, } from '../../../../../tests/js/utils'; import { render } from '../../../../../tests/js/test-utils'; @@ -42,6 +43,9 @@ describe( 'core/notifications Notifications', () => { const fetchDismissItem = new RegExp( '^/google-site-kit/v1/core/user/data/dismiss-item' ); + const fetchGetDismissedPrompts = new RegExp( + '^/google-site-kit/v1/core/user/data/dismissed-prompts' + ); let registry; let store; @@ -576,30 +580,215 @@ describe( 'core/notifications Notifications', () => { } ); describe( 'isNotificationDismissed', () => { - let isNotificationDismissed; + describe( 'when using dismissed items', () => { + let isNotificationDismissed; + beforeEach( () => { + // Register the Gathering Data Notification as a test + provideNotifications( registry ); + + ( { isNotificationDismissed } = + registry.select( CORE_NOTIFICATIONS ) ); + } ); + + it( 'should return undefined if getDismissedItems selector is not resolved yet', async () => { + fetchMock.getOnce( fetchGetDismissedItems, { body: [] } ); + expect( + isNotificationDismissed( 'gathering-data-notification' ) + ).toBeUndefined(); + await untilResolved( + registry, + CORE_USER + ).getDismissedItems(); + } ); + + it( 'should return TRUE if the notification is dismissed', () => { + registry + .dispatch( CORE_USER ) + .receiveGetDismissedItems( [ + 'gathering-data-notification', + 'some-other-notification', + ] ); + expect( + isNotificationDismissed( 'gathering-data-notification' ) + ).toBe( true ); + } ); + + it( 'should return FALSE if the notification is not dismissed', () => { + registry + .dispatch( CORE_USER ) + .receiveGetDismissedItems( [ 'foo', 'bar' ] ); + expect( + isNotificationDismissed( 'gathering-data-notification' ) + ).toBe( false ); + } ); + } ); + describe( 'when using dismissed prompts', () => { + let isNotificationDismissed; + beforeEach( () => { + provideNotifications( registry, { + 'test-notification-using-prompts': { + Component: () => {}, + areaSlug: NOTIFICATION_AREAS.BANNERS_ABOVE_NAV, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + priority: 11, + checkRequirements: () => true, + isDismissible: false, + dismissRetries: 1, + }, + } ); + + ( { isNotificationDismissed } = + registry.select( CORE_NOTIFICATIONS ) ); + } ); + + it( 'should return undefined if getDismissedPrompts selector is not resolved yet', async () => { + fetchMock.getOnce( fetchGetDismissedPrompts, { body: [] } ); + expect( + isNotificationDismissed( + 'test-notification-using-prompts' + ) + ).toBeUndefined(); + await untilResolved( + registry, + CORE_USER + ).getDismissedPrompts(); + } ); + + it( 'should return TRUE if the notification is dismissed', () => { + registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { + 'test-notification-using-prompts': { + expires: 0, + count: 1, + }, + 'some-other-notification': { expires: 0, count: 2 }, + } ); + expect( + isNotificationDismissed( + 'test-notification-using-prompts' + ) + ).toBe( true ); + } ); + + it( 'should return FALSE if the notification is not dismissed', () => { + registry + .dispatch( CORE_USER ) + .receiveGetDismissedPrompts( [ 'foo', 'bar' ] ); + expect( + isNotificationDismissed( + 'test-notification-using-prompts' + ) + ).toBe( false ); + } ); + } ); + } ); + + describe( 'isNotificationDismissalFinal', () => { + let isNotificationDismissalFinal; beforeEach( () => { - ( { isNotificationDismissed } = + ( { isNotificationDismissalFinal } = registry.select( CORE_NOTIFICATIONS ) ); } ); - it( 'should return undefined if getDismissedItems selector is not resolved yet', async () => { - fetchMock.getOnce( fetchGetDismissedItems, { body: [] } ); - expect( isNotificationDismissed( 'foo' ) ).toBeUndefined(); - await untilResolved( registry, CORE_USER ).getDismissedItems(); + it( 'should return undefined if notification is undefined', () => { + expect( + isNotificationDismissalFinal( 'test-notification' ) + ).toBeUndefined(); } ); - it( 'should return TRUE if the notification is dismissed', () => { - registry - .dispatch( CORE_USER ) - .receiveGetDismissedItems( [ 'foo', 'bar' ] ); - expect( isNotificationDismissed( 'foo' ) ).toBe( true ); + it( 'requires notification to be dismissible', () => { + provideNotifications( registry, { + 'test-notification': { + Component: () => {}, + areaSlug: NOTIFICATION_AREAS.BANNERS_ABOVE_NAV, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + dismissRetries: 1, + }, + } ); + + expect( () => + isNotificationDismissalFinal( 'test-notification' ) + ).toThrow( + 'Notification should be dismissible to check if a notification is on its final dismissal.' + ); } ); - it( 'should return FALSE if the notification is not dismissed', () => { - registry - .dispatch( CORE_USER ) - .receiveGetDismissedItems( [ 'foo', 'bar' ] ); - expect( isNotificationDismissed( 'baz' ) ).toBe( false ); + it( 'returns true if notification does not have retries', () => { + provideNotifications( registry, { + 'test-notification': { + Component: () => {}, + areaSlug: NOTIFICATION_AREAS.BANNERS_ABOVE_NAV, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + isDismissible: true, + }, + } ); + + expect( + isNotificationDismissalFinal( 'test-notification' ) + ).toBe( true ); + } ); + + it( 'returns true if notification is on the final retry', () => { + provideNotifications( registry, { + 'test-notification': { + Component: () => {}, + areaSlug: NOTIFICATION_AREAS.BANNERS_ABOVE_NAV, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + isDismissible: true, + dismissRetries: 2, + }, + } ); + + registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { + 'test-notification': { + expires: 0, + count: 2, + }, + 'some-other-notification': { expires: 0, count: 2 }, + } ); + + expect( + isNotificationDismissalFinal( 'test-notification' ) + ).toBe( true ); + } ); + + it( 'returns false if notification has never been dismissed', () => { + provideNotifications( registry, { + 'test-notification': { + Component: () => {}, + areaSlug: NOTIFICATION_AREAS.BANNERS_ABOVE_NAV, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + isDismissible: true, + dismissRetries: 1, + }, + } ); + + expect( + isNotificationDismissalFinal( 'test-notification' ) + ).toBe( false ); + } ); + + it( 'returns false if notification is not on the final retry', () => { + provideNotifications( registry, { + 'test-notification': { + Component: () => {}, + areaSlug: NOTIFICATION_AREAS.BANNERS_ABOVE_NAV, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + isDismissible: true, + dismissRetries: 2, + }, + } ); + + registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { + 'test-notification': { + expires: 0, + count: 1, + }, + 'some-other-notification': { expires: 0, count: 2 }, + } ); + + expect( + isNotificationDismissalFinal( 'test-notification' ) + ).toBe( false ); } ); } ); } ); diff --git a/assets/js/modules/ads/datastore/settings.test.js b/assets/js/modules/ads/datastore/settings.test.js index 38e4fc6e2d9..19902b08d1a 100644 --- a/assets/js/modules/ads/datastore/settings.test.js +++ b/assets/js/modules/ads/datastore/settings.test.js @@ -118,6 +118,17 @@ describe( 'modules/ads settings', () => { FPM_SETUP_CTA_BANNER_NOTIFICATION, ] ); + provideNotifications( + registry, + { + [ FPM_SETUP_CTA_BANNER_NOTIFICATION ]: + DEFAULT_NOTIFICATIONS[ + FPM_SETUP_CTA_BANNER_NOTIFICATION + ], + }, + { overwrite: true } + ); + fetchMock.postOnce( settingsEndpoint, ( url, opts ) => ( { body: JSON.parse( opts.body )?.data, status: 200, diff --git a/assets/js/modules/analytics-4/datastore/settings.test.js b/assets/js/modules/analytics-4/datastore/settings.test.js index 74a61ad82de..20ed39fe53f 100644 --- a/assets/js/modules/analytics-4/datastore/settings.test.js +++ b/assets/js/modules/analytics-4/datastore/settings.test.js @@ -528,6 +528,17 @@ describe( 'modules/analytics-4 settings', () => { FPM_SETUP_CTA_BANNER_NOTIFICATION, ] ); + provideNotifications( + registry, + { + [ FPM_SETUP_CTA_BANNER_NOTIFICATION ]: + DEFAULT_NOTIFICATIONS[ + FPM_SETUP_CTA_BANNER_NOTIFICATION + ], + }, + { overwrite: true } + ); + const validSettings = { accountID: fixtures.createProperty._accountID, propertyID: fixtures.createProperty._id, diff --git a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.js b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.js index 0630964bd0b..24f0fec2b64 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.js @@ -24,11 +24,9 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; import { createInterpolateElement, Fragment, - useCallback, useEffect, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -37,130 +35,65 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { useDispatch, useSelect } from 'googlesitekit-data'; -import { Button } from 'googlesitekit-components'; import { BREAKPOINT_SMALL, BREAKPOINT_TABLET, useBreakpoint, } from '../../../../hooks/useBreakpoint'; import useActivateModuleCallback from '../../../../hooks/useActivateModuleCallback'; -import whenInactive from '../../../../util/when-inactive'; -import { withWidgetComponentProps } from '../../../../googlesitekit/widgets/util'; -import { CORE_MODULES } from '../../../../googlesitekit/modules/datastore/constants'; import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; -import { - READER_REVENUE_MANAGER_MODULE_SLUG, - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, -} from '../../datastore/constants'; -import { Cell, Grid, Row } from '../../../../material-components'; +import { READER_REVENUE_MANAGER_MODULE_SLUG } from '../../datastore/constants'; import SetupSVG from '../../../../../svg/graphics/reader-revenue-manager-setup.svg'; import SetupTabletSVG from '../../../../../svg/graphics/reader-revenue-manager-setup-tablet.svg'; import SetupMobileSVG from '../../../../../svg/graphics/reader-revenue-manager-setup-mobile.svg'; -import Link from '../../../../components/Link'; -import { trackEvent, WEEK_IN_SECONDS } from '../../../../util'; import { AdminMenuTooltip, useShowTooltip, useTooltipState, } from '../../../../components/AdminMenuTooltip'; -import useViewContext from '../../../../hooks/useViewContext'; - -function ReaderRevenueManagerSetupCTABanner( { Widget, WidgetNull } ) { - const viewContext = useViewContext(); +import { CORE_NOTIFICATIONS } from '../../../../googlesitekit/notifications/datastore/constants'; +import NotificationWithSVG from '../../../../googlesitekit/notifications/components/layout/NotificationWithSVG'; +import LearnMoreLink from '../../../../googlesitekit/notifications/components/common/LearnMoreLink'; +import ActionsCTALinkDismiss from '../../../../googlesitekit/notifications/components/common/ActionsCTALinkDismiss'; +import { WEEK_IN_SECONDS } from '../../../../util'; + +export default function ReaderRevenueManagerSetupCTABanner( { + id, + Notification, +} ) { const breakpoint = useBreakpoint(); - const isMobileBreakpoint = breakpoint === BREAKPOINT_SMALL; - const isTabletBreakpoint = breakpoint === BREAKPOINT_TABLET; const onSetupActivate = useActivateModuleCallback( READER_REVENUE_MANAGER_MODULE_SLUG ); - const onSetupCallback = useCallback( () => { - trackEvent( - `${ viewContext }_rrm-setup-notification`, - 'confirm_notification' - ).finally( () => { - onSetupActivate(); - } ); - }, [ onSetupActivate, viewContext ] ); - - const showTooltip = useShowTooltip( - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY - ); - const { isTooltipVisible } = useTooltipState( - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY - ); + const showTooltip = useShowTooltip( id ); + const { isTooltipVisible } = useTooltipState( id ); - const isDismissed = useSelect( ( select ) => - select( CORE_USER ).isPromptDismissed( - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY - ) - ); + const { triggerSurvey } = useDispatch( CORE_USER ); - const isDismissingPrompt = useSelect( ( select ) => - select( CORE_USER ).isDismissingPrompt( - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY - ) + const isDismissalFinal = useSelect( ( select ) => + select( CORE_NOTIFICATIONS ).isNotificationDismissalFinal( id ) ); - const dismissCount = useSelect( ( select ) => - select( CORE_USER ).getPromptDismissCount( - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY - ) + // See TODO note below. + const isCTADismissed = useSelect( ( select ) => + select( CORE_NOTIFICATIONS ).isNotificationDismissed( id ) ); - const dismissedPromptsLoaded = useSelect( ( select ) => select( CORE_USER ).hasFinishedResolution( 'getDismissedPrompts', [] ) ); - - const { dismissPrompt, triggerSurvey } = useDispatch( CORE_USER ); - - const onDismiss = useCallback( () => { - trackEvent( - `${ viewContext }_rrm-setup-notification`, - 'dismiss_notification' - ).finally( () => { - const expirationInSeconds = - dismissCount < 1 ? 2 * WEEK_IN_SECONDS : 0; - - showTooltip(); - - dismissPrompt( READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, { - expiresInSeconds: expirationInSeconds, - } ); - } ); - }, [ dismissCount, dismissPrompt, showTooltip, viewContext ] ); - - const readerRevenueManagerDocumentationURL = - 'https://readerrevenue.withgoogle.com'; - - const canActivateRRMModule = useSelect( ( select ) => - select( CORE_MODULES ).canActivateModule( - READER_REVENUE_MANAGER_MODULE_SLUG - ) - ); - - const showBanner = - isDismissed === false && - ! isDismissingPrompt && - canActivateRRMModule && - dismissedPromptsLoaded === true; + const hideCTABanner = isCTADismissed || ! dismissedPromptsLoaded; useEffect( () => { - if ( showBanner ) { - trackEvent( - `${ viewContext }_rrm-setup-notification`, - 'view_notification' - ); - + if ( ! hideCTABanner ) { triggerSurvey( 'view_reader_revenue_manager_cta' ); } - }, [ showBanner, triggerSurvey, viewContext ] ); + }, [ hideCTABanner, triggerSurvey ] ); if ( isTooltipVisible ) { return ( - ); } - if ( ! showBanner ) { - return ; + // TODO Remove this hack + // We "incorrectly" pass true to the `skipHidingFromQueue` option when dismissing this banner. + // This is because we don't want the component removed from the DOM as we have to still render + // the `AdminMenuTooltip` in this component. This means that we have to rely on manually + // checking for the dismissal state here. + if ( hideCTABanner ) { + return null; } + const breakpointSVGMap = { + [ BREAKPOINT_SMALL ]: SetupMobileSVG, + [ BREAKPOINT_TABLET ]: SetupTabletSVG, + }; + return ( -
- - - - - - - -

- { __( - 'Grow your revenue and deepen reader engagement', + + +

+ { createInterpolateElement( + __( + 'Turn casual visitors into loyal readers and earn more from your content with voluntary contributions, surveys, newsletter sign-ups and reader insight tools. Learn more', + 'google-site-kit' + ), + { + a: ( + -

-

- { createInterpolateElement( - __( - 'Turn casual visitors into loyal readers and earn more from your content with voluntary contributions, surveys, newsletter sign-ups and reader insight tools. Learn more', - 'google-site-kit' - ), - { - a: ( - - ), - } - ) } -
-
- { __( - '* Support for subscriptions coming soon', - 'google-site-kit' - ) } -

-
- -
- - -
- - { ! isMobileBreakpoint && - ! isTabletBreakpoint && ( - - - - ) } - { isTabletBreakpoint && ( - - - - ) } - { isMobileBreakpoint && ( - - - - ) } - - - - - - -

+ url="https://readerrevenue.withgoogle.com" + /> + ), + } + ) } +
+
+ { __( + '* Support for subscriptions coming soon', + 'google-site-kit' + ) } +

+ + } + actions={ + + } + SVG={ breakpointSVGMap[ breakpoint ] || SetupSVG } + /> + ); } ReaderRevenueManagerSetupCTABanner.propTypes = { - Widget: PropTypes.elementType.isRequired, - WidgetNull: PropTypes.elementType.isRequired, + id: PropTypes.string, + Notification: PropTypes.elementType, }; - -export default compose( - whenInactive( { - moduleName: READER_REVENUE_MANAGER_MODULE_SLUG, - } ), - withWidgetComponentProps( 'readerRevenueManagerSetupCTABanner' ) -)( ReaderRevenueManagerSetupCTABanner ); diff --git a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.stories.js b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.stories.js index e7762125eba..b6ebf1c5ebd 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.stories.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.stories.js @@ -25,22 +25,19 @@ import fetchMock from 'fetch-mock'; * Internal dependencies */ import { provideModules } from '../../../../../../tests/js/utils'; -import { withWidgetComponentProps } from '../../../../googlesitekit/widgets/util'; +import { withNotificationComponentProps } from '../../../../googlesitekit/notifications/util/component-props'; import WithRegistrySetup from '../../../../../../tests/js/WithRegistrySetup'; import ReaderRevenueManagerSetupCTABanner from './ReaderRevenueManagerSetupCTABanner'; -import { - READER_REVENUE_MANAGER_MODULE_SLUG, - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, -} from '../../datastore/constants'; +import { READER_REVENUE_MANAGER_MODULE_SLUG } from '../../datastore/constants'; import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; import { WEEK_IN_SECONDS } from '../../../../util'; -const WidgetWithComponentProps = withWidgetComponentProps( - 'readerRevenueManagerSetupCTABanner' +const NotificationWithComponentProps = withNotificationComponentProps( + 'rrm-setup-notification' )( ReaderRevenueManagerSetupCTABanner ); function Template() { - return ; + return ; } export const Default = Template.bind( {} ); @@ -60,6 +57,10 @@ export default { ] ); registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( [] ); + registry + .dispatch( CORE_USER ) + .finishResolution( 'getDismissedPrompts', [] ); fetchMock.postOnce( new RegExp( @@ -67,12 +68,10 @@ export default { ), { body: { - [ READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY ]: - { - expires: - Date.now() / 1000 + WEEK_IN_SECONDS, - count: 1, - }, + 'rrm-setup-notification': { + expires: Date.now() / 1000 + WEEK_IN_SECONDS, + count: 1, + }, }, status: 200, } diff --git a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.test.js b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.test.js index 4c99a134442..d5d38cdec47 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.test.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.test.js @@ -34,25 +34,23 @@ import { waitFor, provideUserAuthentication, } from '../../../../../../tests/js/test-utils'; -import { getWidgetComponentProps } from '../../../../googlesitekit/widgets/util'; +import { withNotificationComponentProps } from '../../../../googlesitekit/notifications/util/component-props'; import { CORE_USER } from '../../../../googlesitekit/datastore/user/constants'; import { CORE_MODULES } from '../../../../googlesitekit/modules/datastore/constants'; import { ERROR_CODE_NON_HTTPS_SITE, READER_REVENUE_MANAGER_MODULE_SLUG, - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, + LEGACY_RRM_SETUP_BANNER_DISMISSED_KEY, } from '../../datastore/constants'; import { VIEW_CONTEXT_MAIN_DASHBOARD } from '../../../../googlesitekit/constants'; -import * as tracking from '../../../../util/tracking'; import useActivateModuleCallback from '../../../../hooks/useActivateModuleCallback'; import { WEEK_IN_SECONDS } from '../../../../util'; import { mockSurveyEndpoints, surveyTriggerEndpoint, } from '../../../../../../tests/js/mock-survey-endpoints'; - -const mockTrackEvent = jest.spyOn( tracking, 'trackEvent' ); -mockTrackEvent.mockImplementation( () => Promise.resolve() ); +import { CORE_NOTIFICATIONS } from '../../../../googlesitekit/notifications/datastore/constants'; +import { NOTIFICATIONS } from '../..'; jest.mock( '../../../../hooks/useActivateModuleCallback' ); @@ -60,12 +58,14 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { let registry; let activateModuleMock; - const { Widget, WidgetNull } = getWidgetComponentProps( - 'readerRevenueManagerSetupCTABanner' - ); + const ReaderRevenueManagerSetupCTABannerComponent = + withNotificationComponentProps( 'rrm-setup-notification' )( + ReaderRevenueManagerSetupCTABanner + ); + + const notification = NOTIFICATIONS[ 'rrm-setup-notification' ]; beforeEach( () => { - mockTrackEvent.mockClear(); registry = createTestRegistry(); activateModuleMock = jest.fn( () => jest.fn() ); @@ -84,62 +84,41 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { }, ] ); + registry + .dispatch( CORE_NOTIFICATIONS ) + .registerNotification( 'rrm-setup-notification', notification ); + useActivateModuleCallback.mockImplementation( activateModuleMock ); } ); - it( 'should render the Reader Revenue Manager setup CTA banner when not dismissed', async () => { + it( 'should render the Reader Revenue Manager setup CTA banner', async () => { mockSurveyEndpoints(); const { getByText, waitForRegistry } = render( - , + , { registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, } ); await waitForRegistry(); - expect( mockTrackEvent ).toHaveBeenCalledWith( - `${ VIEW_CONTEXT_MAIN_DASHBOARD }_rrm-setup-notification`, - 'view_notification' - ); - expect( getByText( /Grow your revenue and deepen reader engagement/ ) ).toBeInTheDocument(); } ); - it( 'should not render the Reader Revenue Manager setup CTA banner when dismissed', async () => { - registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { - [ READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY ]: { - expires: Date.now() / 1000 + WEEK_IN_SECONDS, - count: 1, - }, - } ); - const { container, waitForRegistry } = render( - , + it( 'should call the "useActivateModuleCallback" hook when the setup CTA is clicked', async () => { + mockSurveyEndpoints(); + + fetchMock.postOnce( + RegExp( '^/google-site-kit/v1/core/user/data/dismiss-prompt' ), { - registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, + body: JSON.stringify( [ 'rrm-setup-notification' ] ), + status: 200, } ); - await waitForRegistry(); - - expect( container ).toBeEmptyDOMElement(); - expect( mockTrackEvent ).not.toHaveBeenCalled(); - } ); - - it( 'should call the "useActivateModuleCallback" hook when the setup CTA is clicked', async () => { - mockSurveyEndpoints(); - registry .dispatch( CORE_MODULES ) .receiveCheckRequirementsSuccess( @@ -147,13 +126,9 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { ); const { container, getByRole, waitForRegistry } = render( - , + , { registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, } ); @@ -161,25 +136,16 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { expect( container ).not.toBeEmptyDOMElement(); - fireEvent.click( - getByRole( 'button', { - name: /Set up Reader Revenue Manager/i, - } ) - ); + // eslint-disable-next-line require-await + await act( async () => { + fireEvent.click( + getByRole( 'button', { + name: /Set up Reader Revenue Manager/i, + } ) + ); + } ); expect( activateModuleMock ).toHaveBeenCalledTimes( 1 ); - - expect( mockTrackEvent ).toHaveBeenNthCalledWith( - 1, - `${ VIEW_CONTEXT_MAIN_DASHBOARD }_rrm-setup-notification`, - 'view_notification' - ); - - expect( mockTrackEvent ).toHaveBeenNthCalledWith( - 2, - `${ VIEW_CONTEXT_MAIN_DASHBOARD }_rrm-setup-notification`, - 'confirm_notification' - ); } ); it( 'should call the dismiss item endpoint when the banner is dismissed', async () => { @@ -188,12 +154,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { fetchMock.postOnce( RegExp( '^/google-site-kit/v1/core/user/data/dismiss-prompt' ), { - body: { - [ READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY ]: { - expires: Date.now() / 1000 + WEEK_IN_SECONDS, - count: 1, - }, - }, + body: JSON.stringify( [ 'rrm-setup-notification' ] ), status: 200, } ); @@ -205,14 +166,10 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { Settings - + , , { registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, } ); @@ -225,64 +182,39 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { // 3 fetches: 1 for the survey trigger, 1 for the survey timeout, 1 for the dismiss prompt. expect( fetchMock ).toHaveFetchedTimes( 3 ); - - expect( mockTrackEvent ).toHaveBeenNthCalledWith( - 1, - `${ VIEW_CONTEXT_MAIN_DASHBOARD }_rrm-setup-notification`, - 'view_notification' - ); - - expect( mockTrackEvent ).toHaveBeenNthCalledWith( - 2, - `${ VIEW_CONTEXT_MAIN_DASHBOARD }_rrm-setup-notification`, - 'dismiss_notification' - ); } ); - it( 'should not render the Reader Revenue Manager setup CTA banner when the module requirements do not meet', async () => { - // Throw error from checkRequirements to simulate non-HTTPS site error. - provideModules( registry, [ - { - slug: READER_REVENUE_MANAGER_MODULE_SLUG, - active: false, - checkRequirements: () => { - throw { - code: ERROR_CODE_NON_HTTPS_SITE, - message: - 'The site should use HTTPS to set up Reader Revenue Manager', - data: null, - }; - }, - }, - ] ); + it( 'should trigger a survey when the banner is displayed', async () => { + mockSurveyEndpoints(); - const { container, waitForRegistry } = render( - , + const { waitForRegistry } = render( + , { registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, } ); await waitForRegistry(); - expect( container ).toBeEmptyDOMElement(); - expect( mockTrackEvent ).not.toHaveBeenCalled(); + // The survey trigger endpoint should be called with the correct trigger ID. + await waitFor( () => + expect( fetchMock ).toHaveFetched( surveyTriggerEndpoint, { + body: { + data: { triggerID: 'view_reader_revenue_manager_cta' }, + }, + } ) + ); } ); - it( 'should not render the banner when the dismissed prompts selector is not resolved', async () => { - registry - .dispatch( CORE_USER ) - .startResolution( 'getDismissedPrompts', [] ); - + it( 'should not render the Reader Revenue Manager setup CTA banner when dismissed', async () => { + registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { + 'rrm-setup-notification': { + expires: Date.now() / 1000 + WEEK_IN_SECONDS, + count: 1, + }, + } ); const { container, waitForRegistry } = render( - , + , { registry, } @@ -293,24 +225,15 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render the banner when it is being dismissed', async () => { - registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( [] ); - + it( 'should not render the banner when the dismissed prompts selector is not resolved', async () => { registry .dispatch( CORE_USER ) - .setIsPromptDimissing( - READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, - true - ); + .startResolution( 'getDismissedPrompts', [] ); const { container, waitForRegistry } = render( - , + , { registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, } ); @@ -326,8 +249,8 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { RegExp( '^/google-site-kit/v1/core/user/data/dismiss-prompt' ), { body: { - [ READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY ]: { - expires: Date.now() / 1000 + 2 * WEEK_IN_SECONDS, + 'rrm-setup-notification': { + expires: 2 * WEEK_IN_SECONDS, count: 1, }, }, @@ -342,10 +265,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { Settings - + , , { registry, @@ -366,7 +286,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { expect( fetchMock ).toHaveFetched( dismissPromptEndpoint, { body: { data: { - slug: READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, + slug: 'rrm-setup-notification', expiration: WEEK_IN_SECONDS * 2, }, }, @@ -388,7 +308,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { RegExp( '^/google-site-kit/v1/core/user/data/dismiss-prompt' ), { body: { - [ READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY ]: { + 'rrm-setup-notification': { expires: 0, // Expiry of 0 permanently dismisses the prompt. count: 2, }, @@ -398,7 +318,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { ); registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { - [ READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY ]: { + 'rrm-setup-notification': { expires: Date.now() / 1000 - 2 * WEEK_IN_SECONDS + 1, count: 1, }, @@ -411,10 +331,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { Settings - + , , { registry, @@ -435,7 +352,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { expect( fetchMock ).toHaveFetched( dismissPromptEndpoint, { body: { data: { - slug: READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY, + slug: 'rrm-setup-notification', expiration: 0, }, }, @@ -444,29 +361,55 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { } ); } ); - it( 'should trigger a survey when the banner is displayed', async () => { - mockSurveyEndpoints(); + describe( 'checkRequirements', () => { + it( 'is active when all required conditions are met', async () => { + const isActive = await notification.checkRequirements( + registry, + VIEW_CONTEXT_MAIN_DASHBOARD + ); - const { waitForRegistry } = render( - , - { + expect( isActive ).toBe( true ); + } ); + + it( 'is not active when the banner was dismissed with the legacy dismissal key', async () => { + registry.dispatch( CORE_USER ).receiveGetDismissedPrompts( { + [ LEGACY_RRM_SETUP_BANNER_DISMISSED_KEY ]: { + expires: Date.now() / 1000 + WEEK_IN_SECONDS, + count: 1, + }, + } ); + + const isActive = await notification.checkRequirements( registry, - viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, - } - ); + VIEW_CONTEXT_MAIN_DASHBOARD + ); - await waitForRegistry(); + expect( isActive ).toBe( false ); + } ); - // The survey trigger endpoint should be called with the correct trigger ID. - await waitFor( () => - expect( fetchMock ).toHaveFetched( surveyTriggerEndpoint, { - body: { - data: { triggerID: 'view_reader_revenue_manager_cta' }, + it( 'is not active when the module requirements do not meet', async () => { + // Throw error from checkRequirements to simulate non-HTTPS site error. + provideModules( registry, [ + { + slug: READER_REVENUE_MANAGER_MODULE_SLUG, + active: false, + checkRequirements: () => { + throw { + code: ERROR_CODE_NON_HTTPS_SITE, + message: + 'The site should use HTTPS to set up Reader Revenue Manager', + data: null, + }; + }, }, - } ) - ); + ] ); + + const isActive = await notification.checkRequirements( + registry, + VIEW_CONTEXT_MAIN_DASHBOARD + ); + + expect( isActive ).toBe( false ); + } ); } ); } ); diff --git a/assets/js/modules/reader-revenue-manager/datastore/constants.js b/assets/js/modules/reader-revenue-manager/datastore/constants.js index c4a26d18919..c8a034972ec 100644 --- a/assets/js/modules/reader-revenue-manager/datastore/constants.js +++ b/assets/js/modules/reader-revenue-manager/datastore/constants.js @@ -32,7 +32,7 @@ export const PUBLICATION_ONBOARDING_STATES = { export const UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION = 'READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION'; -export const READER_REVENUE_MANAGER_SETUP_BANNER_DISMISSED_KEY = +export const LEGACY_RRM_SETUP_BANNER_DISMISSED_KEY = 'rrm_module_setup_banner_dismissed_key'; export const READER_REVENUE_MANAGER_SETUP_FORM = diff --git a/assets/js/modules/reader-revenue-manager/index.js b/assets/js/modules/reader-revenue-manager/index.js index 9367ad5d4f7..993f97a02f6 100644 --- a/assets/js/modules/reader-revenue-manager/index.js +++ b/assets/js/modules/reader-revenue-manager/index.js @@ -30,15 +30,24 @@ import { MODULES_READER_REVENUE_MANAGER, ERROR_CODE_NON_HTTPS_SITE, READER_REVENUE_MANAGER_MODULE_SLUG, + LEGACY_RRM_SETUP_BANNER_DISMISSED_KEY, } from './datastore/constants'; import { SetupMain } from './components/setup'; import { SettingsEdit, SettingsView } from './components/settings'; import ReaderRevenueManagerIcon from '../../../svg/graphics/reader-revenue-manager.svg'; import { isURLUsingHTTPS } from './utils/validation'; -import { RRMSetupSuccessSubtleNotification } from './components/dashboard'; -import { NOTIFICATION_AREAS } from '../../googlesitekit/notifications/datastore/constants'; +import { + ReaderRevenueManagerSetupCTABanner, + RRMSetupSuccessSubtleNotification, +} from './components/dashboard'; +import { + NOTIFICATION_AREAS, + NOTIFICATION_GROUPS, +} from '../../googlesitekit/notifications/datastore/constants'; import { VIEW_CONTEXT_MAIN_DASHBOARD } from '../../googlesitekit/constants'; import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants'; +import { isFeatureEnabled } from '../../features'; +import { CORE_USER } from '../../googlesitekit/datastore/user/constants'; export { registerStore } from './datastore'; @@ -76,8 +85,33 @@ export const registerModule = ( modules ) => { } ); }; -export const registerNotifications = ( notifications ) => { - notifications.registerNotification( 'setup-success-notification-rrm', { +export const NOTIFICATIONS = { + 'rrm-setup-notification': { + Component: ReaderRevenueManagerSetupCTABanner, + priority: 50, + areaSlug: NOTIFICATION_AREAS.BANNERS_BELOW_NAV, + groupID: NOTIFICATION_GROUPS.SETUP_CTAS, + viewContexts: [ VIEW_CONTEXT_MAIN_DASHBOARD ], + checkRequirements: async ( { select, resolveSelect } ) => { + // Check if the prompt with the legacy key used before the banner was refactored + // to use the `notification ID` as the dismissal key, is dismissed. + await resolveSelect( CORE_USER ).getDismissedPrompts(); + const isLegacyDismissed = select( CORE_USER ).isPromptDismissed( + LEGACY_RRM_SETUP_BANNER_DISMISSED_KEY + ); + + if ( isLegacyDismissed ) { + return false; + } + + return await resolveSelect( CORE_MODULES ).canActivateModule( + READER_REVENUE_MANAGER_MODULE_SLUG + ); + }, + isDismissible: true, + dismissRetries: 1, + }, + 'setup-success-notification-rrm': { Component: RRMSetupSuccessSubtleNotification, priority: 10, areaSlug: NOTIFICATION_AREAS.BANNERS_BELOW_NAV, @@ -110,5 +144,16 @@ export const registerNotifications = ( notifications ) => { return false; }, isDismissible: false, - } ); + }, +}; + +export const registerNotifications = ( notificationsAPI ) => { + if ( isFeatureEnabled( 'rrmModule' ) ) { + for ( const notificationID in NOTIFICATIONS ) { + notificationsAPI.registerNotification( + notificationID, + NOTIFICATIONS[ notificationID ] + ); + } + } }; diff --git a/assets/sass/components/reader-revenue-manager/_googlesitekit-rrm-setup-cta-banner.scss b/assets/sass/components/reader-revenue-manager/_googlesitekit-rrm-setup-cta-banner.scss index 31c02e91ec0..0ae15e30c8c 100644 --- a/assets/sass/components/reader-revenue-manager/_googlesitekit-rrm-setup-cta-banner.scss +++ b/assets/sass/components/reader-revenue-manager/_googlesitekit-rrm-setup-cta-banner.scss @@ -17,7 +17,7 @@ */ .googlesitekit-plugin { - .googlesitekit-reader-revenue-manager-setup-cta-widget { + .googlesitekit-setup-cta-banner--rrm-setup-notification { .googlesitekit-setup-cta-banner__primary-cell { padding: $grid-gap-desktop - 4 $grid-gap-phone 0; @@ -27,10 +27,6 @@ } .googlesitekit-setup-cta-banner__title { - @media (min-width: $width-tablet + 1 + px) { - font-weight: $fw-medium; - } - @media (min-width: $bp-nonMobile) { font-size: $fs-headline-sm; line-height: $lh-headline-sm; @@ -66,7 +62,13 @@ } } - .googlesitekit-setup-cta-banner__svg-wrapper { + .googlesitekit-setup-cta-banner__svg-wrapper--rrm-setup-notification { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + overflow: hidden; + position: relative; svg { flex: 1; @@ -75,12 +77,16 @@ // Increase the SVG size via scale in order to achieve the desired clipping effect // in conjunction with the parent's overflow: hidden. scale: 1; + transform-origin: center 2px; @media (min-width: $bp-nonTablet) { + height: 100%; max-height: 272px; + position: absolute; right: 70px; scale: 1.6; top: 10px; + transform-origin: initial; width: 65%; } diff --git a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_1_medium.png index d39805a31ff..e87f595c887 100644 Binary files a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_1_medium.png and b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_2_large.png index a8eefcd2530..985a1ba2e98 100644 Binary files a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_2_large.png and b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Default_0_document_2_large.png differ