diff --git a/assets/js/components/Banner/index.js b/assets/js/components/Banner/index.js index e15a94aeff9..6ef9caf30e8 100644 --- a/assets/js/components/Banner/index.js +++ b/assets/js/components/Banner/index.js @@ -43,6 +43,25 @@ import LearnMoreLink from './LearnMoreLink'; import Title from './Title'; import TitleIcon from './TitleIcon'; +function getSVGDataByBreakpoint( breakpoint, svg ) { + const isMobile = breakpoint === BREAKPOINT_SMALL; + const isTablet = breakpoint === BREAKPOINT_TABLET; + + if ( isTablet && svg?.tablet ) { + return svg.tablet; + } + + if ( ( isMobile || isTablet ) && svg?.mobile ) { + return svg.mobile; + } + + if ( ! isMobile && ! isTablet && svg?.desktop ) { + return svg.desktop; + } + + return null; +} + const Banner = forwardRef( ( { @@ -62,15 +81,7 @@ const Banner = forwardRef( ref ) => { const breakpoint = useBreakpoint(); - const isMobileOrTablet = - breakpoint === BREAKPOINT_SMALL || breakpoint === BREAKPOINT_TABLET; - - let SVGData = null; - if ( isMobileOrTablet && svg?.mobile ) { - SVGData = svg.mobile; - } else if ( ! isMobileOrTablet && svg?.desktop ) { - SVGData = svg.desktop; - } + const SVGData = getSVGDataByBreakpoint( breakpoint, svg ); const svgMode = svg?.verticalPosition ? svg.verticalPosition : 'center'; @@ -139,6 +150,7 @@ Banner.propTypes = { svg: PropTypes.shape( { desktop: PropTypes.elementType, mobile: PropTypes.elementType, + tablet: PropTypes.elementType, verticalPosition: PropTypes.oneOf( [ 'top', 'center', 'bottom' ] ), } ), footer: PropTypes.node, diff --git a/assets/js/components/Banner/index.test.js b/assets/js/components/Banner/index.test.js index 4895813cab7..b71c79ec3fb 100644 --- a/assets/js/components/Banner/index.test.js +++ b/assets/js/components/Banner/index.test.js @@ -27,6 +27,17 @@ import { createElement } from '@wordpress/element'; import { render } from '@tests/js/test-utils'; import Banner from './index'; +let mockBreakpoint = 'desktop'; + +jest.mock( '@/js/hooks/useBreakpoint', () => { + const actual = jest.requireActual( '@/js/hooks/useBreakpoint' ); + + return { + ...actual, + useBreakpoint: () => mockBreakpoint, + }; +} ); + describe( 'Banner', () => { describe( 'description prop', () => { it( 'renders string description correctly', () => { @@ -211,4 +222,49 @@ describe( 'Banner', () => { expect( container.firstChild ).toHaveClass( 'custom-class' ); } ); } ); + + describe( 'svg prop', () => { + beforeEach( () => { + mockBreakpoint = 'desktop'; + } ); + + it( 'renders tablet SVG at tablet breakpoint when provided', () => { + mockBreakpoint = 'tablet'; + + const { container } = render( + + ); + + expect( + container.querySelector( '.googlesitekit-banner__svg-wrapper' ) + ).toHaveStyle( 'background-image: url(tablet.svg)' ); + } ); + + it( 'falls back to mobile SVG at tablet breakpoint when tablet SVG is not provided', () => { + mockBreakpoint = 'tablet'; + + const { container } = render( + + ); + + expect( + container.querySelector( '.googlesitekit-banner__svg-wrapper' ) + ).toHaveStyle( 'background-image: url(mobile.svg)' ); + } ); + } ); } ); diff --git a/assets/js/googlesitekit/notifications/components/layout/SetupCTA.js b/assets/js/googlesitekit/notifications/components/layout/SetupCTA.js index 8f213c6bf7f..10979164137 100644 --- a/assets/js/googlesitekit/notifications/components/layout/SetupCTA.js +++ b/assets/js/googlesitekit/notifications/components/layout/SetupCTA.js @@ -161,6 +161,7 @@ SetupCTA.propTypes = { svg: PropTypes.shape( { desktop: PropTypes.elementType, mobile: PropTypes.elementType, + tablet: PropTypes.elementType, verticalPosition: PropTypes.oneOf( [ 'top', 'center', 'bottom' ] ), } ), footer: PropTypes.node, 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 85ca5c64591..14700d1359f 100644 --- a/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.js +++ b/assets/js/modules/reader-revenue-manager/components/dashboard/ReaderRevenueManagerSetupCTABanner.js @@ -24,8 +24,14 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { useCallback, useEffect, useState } from '@wordpress/element'; +import { + createInterpolateElement, + useCallback, + useEffect, + useState, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -33,12 +39,19 @@ import { __ } from '@wordpress/i18n'; import { useDispatch, useSelect } from 'googlesitekit-data'; import { useShowTooltip } from '@/js/components/AdminScreenTooltip'; import useRetriableNotificationDismissButtonLabel from '@/js/components/notifications/useRetriableNotificationDismissButtonLabel'; +import { CORE_LOCATION } from '@/js/googlesitekit/datastore/location/constants'; +import { CORE_SITE } from '@/js/googlesitekit/datastore/site/constants'; import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants'; +import { CORE_MODULES } from '@/js/googlesitekit/modules/datastore/constants'; import SetupCTA from '@/js/googlesitekit/notifications/components/layout/SetupCTA'; import { CORE_NOTIFICATIONS } from '@/js/googlesitekit/notifications/datastore/constants'; import useActivateModuleCallback from '@/js/hooks/useActivateModuleCallback'; +import { useFeature } from '@/js/hooks/useFeature'; import { MODULE_SLUG_READER_REVENUE_MANAGER } from '@/js/modules/reader-revenue-manager/constants'; import { WEEK_IN_SECONDS } from '@/js/util'; +import BannerExpressSetupSVGMobile from '@/svg/graphics/banner-rrm-express-setup-cta-mobile.svg?url'; +import BannerExpressSetupSVGTablet from '@/svg/graphics/banner-rrm-express-setup-cta-tablet.svg?url'; +import BannerExpressSetupSVGDesktop from '@/svg/graphics/banner-rrm-express-setup-cta.svg?url'; import BannerSVGMobile from '@/svg/graphics/banner-rrm-setup-cta-mobile.svg?url'; import BannerSVGDesktop from '@/svg/graphics/banner-rrm-setup-cta.svg?url'; @@ -46,6 +59,7 @@ export default function ReaderRevenueManagerSetupCTABanner( { id, Notification, } ) { + const rrmExpressSetupEnabled = useFeature( 'rrmExpressSetup' ); const [ isSaving, setIsSaving ] = useState( false ); const onSetupActivate = useActivateModuleCallback( @@ -68,6 +82,16 @@ export default function ReaderRevenueManagerSetupCTABanner( { const showTooltip = useShowTooltip( tooltipSettings ); const { triggerSurvey } = useDispatch( CORE_USER ); + const { navigateTo } = useDispatch( CORE_LOCATION ); + const { activateModule } = useDispatch( CORE_MODULES ); + const { setInternalServerError } = useDispatch( CORE_SITE ); + + const existingSetupURL = useSelect( ( select ) => + select( CORE_SITE ).getAdminURL( 'googlesitekit-dashboard', { + slug: MODULE_SLUG_READER_REVENUE_MANAGER, + reAuth: true, + } ) + ); const isDismissalFinal = useSelect( ( select ) => select( CORE_NOTIFICATIONS ).isNotificationDismissalFinal( id ) @@ -80,24 +104,77 @@ export default function ReaderRevenueManagerSetupCTABanner( { triggerSurvey( 'view_reader_revenue_manager_cta' ); }, [ triggerSurvey ] ); + const onExpressSetupCallback = useCallback( () => { + async function navigateToExpressSetup() { + setIsSaving( true ); + + const { error, response } = await activateModule( + MODULE_SLUG_READER_REVENUE_MANAGER + ); + + if ( ! error ) { + navigateTo( + addQueryArgs( response.moduleReauthURL, { + expressSetup: true, + cta: 'newsletter', + } ) + ); + } else { + setInternalServerError( { + id: `${ MODULE_SLUG_READER_REVENUE_MANAGER }-setup-error`, + description: error.message, + } ); + setIsSaving( false ); + } + } + + navigateToExpressSetup(); + }, [ activateModule, navigateTo, setInternalServerError ] ); + + const title = rrmExpressSetupEnabled + ? __( 'Turn casual visitors into loyal readers', 'google-site-kit' ) + : __( + 'Grow your revenue and deepen reader engagement', + 'google-site-kit' + ); + + const description = rrmExpressSetupEnabled + ? createInterpolateElement( + __( + 'Add a simple signup form to your site to start building your email subscriber list, powered by Reader Revenue Manager. Want to do more? Explore other features like reader contributions, paywalls, or surveys.', + 'google-site-kit' + ), + { + link: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + } + ) + : __( + 'Turn casual visitors into loyal readers and earn more from your content with paywalls, contributions, surveys, newsletter sign-ups and reader insight tools.', + 'google-site-kit' + ); + return ( ); 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 2ccd6f22fb6..0b28c72b1a0 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 @@ -44,6 +44,13 @@ export const Default = Template.bind( {} ); Default.storyName = 'Default'; Default.scenario = {}; +export const ExpressSetup = Template.bind( {} ); +ExpressSetup.storyName = 'Express Setup'; +ExpressSetup.parameters = { + features: [ 'rrmExpressSetup' ], +}; +ExpressSetup.scenario = {}; + export default { title: 'Modules/ReaderRevenueManager/Components/Dashboard/ReaderRevenueManagerSetupCTABanner', decorators: [ @@ -62,7 +69,7 @@ export default { .dispatch( CORE_USER ) .finishResolution( 'getDismissedPrompts', [] ); - fetchMock.postOnce( + fetchMock.post( new RegExp( '^/google-site-kit/v1/core/user/data/dismiss-prompt' ), @@ -74,7 +81,8 @@ export default { }, }, status: 200, - } + }, + { overwriteRoutes: true } ); } 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 c0d4994d537..be45eb8e9bb 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 @@ -25,6 +25,7 @@ import fetchMock from 'fetch-mock'; * Internal dependencies */ import { VIEW_CONTEXT_MAIN_DASHBOARD } from '@/js/googlesitekit/constants'; +import { CORE_LOCATION } from '@/js/googlesitekit/datastore/location/constants'; import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants'; import { CORE_MODULES } from '@/js/googlesitekit/modules/datastore/constants'; import { CORE_NOTIFICATIONS } from '@/js/googlesitekit/notifications/datastore/constants'; @@ -54,6 +55,17 @@ import { } from '@tests/js/test-utils'; import ReaderRevenueManagerSetupCTABanner from './ReaderRevenueManagerSetupCTABanner'; +let mockBreakpoint = 'desktop'; + +jest.mock( '@/js/hooks/useBreakpoint', () => { + const actual = jest.requireActual( '@/js/hooks/useBreakpoint' ); + + return { + ...actual, + useBreakpoint: () => mockBreakpoint, + }; +} ); + jest.mock( '../../../../hooks/useActivateModuleCallback' ); describe( 'ReaderRevenueManagerSetupCTABanner', () => { @@ -70,6 +82,7 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { beforeEach( () => { registry = createTestRegistry(); + mockBreakpoint = 'desktop'; activateModuleCallbackMock = jest.fn(); activateModuleMock = jest.fn( () => activateModuleCallbackMock ); @@ -113,6 +126,35 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { ).toBeInTheDocument(); } ); + it( 'should render express setup copy when the feature flag is enabled', async () => { + mockSurveyEndpoints(); + + const { getByText, getByRole, waitForRegistry } = render( + , + { + registry, + features: [ 'rrmExpressSetup' ], + } + ); + + await waitForRegistry(); + + expect( + getByText( /Turn casual visitors into loyal readers/ ) + ).toBeInTheDocument(); + expect( + getByRole( 'button', { name: /Set up a sign-up form/i } ) + ).toBeInTheDocument(); + expect( + getByRole( 'link', { name: /Explore other features/i } ) + ).toHaveAttribute( + 'href', + expect.stringContaining( + 'page=googlesitekit-dashboard&slug=reader-revenue-manager&reAuth=true' + ) + ); + } ); + it( 'should call the "useActivateModuleCallback" hook and dismiss the notification when the setup CTA is clicked', async () => { mockSurveyEndpoints(); @@ -152,6 +194,120 @@ describe( 'ReaderRevenueManagerSetupCTABanner', () => { expect( fetchMock ).toHaveFetched( dismissPromptEndpoint ); } ); + it( 'should navigate to express setup when the feature flag is enabled', async () => { + mockSurveyEndpoints(); + + fetchMock.postOnce( dismissPromptEndpoint, { + body: { + 'rrm-setup-notification': { expires: 0, count: 1 }, + }, + } ); + + const navigateToSpy = jest.spyOn( + registry.dispatch( CORE_LOCATION ), + 'navigateTo' + ); + navigateToSpy.mockImplementation( () => {} ); + + const activateModuleSpy = jest.spyOn( + registry.dispatch( CORE_MODULES ), + 'activateModule' + ); + activateModuleSpy.mockResolvedValue( { + error: null, + response: { + moduleReauthURL: + 'http://test.test/wp-admin/admin.php?page=googlesitekit-dashboard&slug=reader-revenue-manager&reAuth=true', + }, + } ); + + const { getByRole, waitForRegistry } = render( + , + { + registry, + features: [ 'rrmExpressSetup' ], + } + ); + + await waitForRegistry(); + + // eslint-disable-next-line require-await + await act( async () => { + fireEvent.click( + getByRole( 'button', { + name: /Set up a sign-up form/i, + } ) + ); + } ); + + expect( activateModuleCallbackMock ).not.toHaveBeenCalled(); + expect( activateModuleSpy ).toHaveBeenCalledWith( + MODULE_SLUG_READER_REVENUE_MANAGER + ); + expect( navigateToSpy ).toHaveBeenCalledWith( + expect.stringContaining( 'expressSetup=true&cta=newsletter' ) + ); + expect( navigateToSpy ).toHaveBeenCalledWith( + expect.stringContaining( 'slug=reader-revenue-manager&reAuth=true' ) + ); + expect( fetchMock ).toHaveFetched( dismissPromptEndpoint ); + } ); + + it( 'should render legacy desktop banner graphic when the feature flag is disabled', async () => { + mockSurveyEndpoints(); + + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); + + expect( + container.querySelector( '.googlesitekit-banner__svg-wrapper' ) + ).toBeInTheDocument(); + } ); + + it( 'should render express tablet banner graphic when the feature flag is enabled at tablet width', async () => { + mockSurveyEndpoints(); + mockBreakpoint = 'tablet'; + + const { container, waitForRegistry } = render( + , + { + registry, + features: [ 'rrmExpressSetup' ], + } + ); + + await waitForRegistry(); + + expect( + container.querySelector( '.googlesitekit-banner__svg-wrapper' ) + ).toBeInTheDocument(); + } ); + + it( 'should render express mobile banner graphic when the feature flag is enabled at mobile width', async () => { + mockSurveyEndpoints(); + mockBreakpoint = 'small'; + + const { container, waitForRegistry } = render( + , + { + registry, + features: [ 'rrmExpressSetup' ], + } + ); + + await waitForRegistry(); + + expect( + container.querySelector( '.googlesitekit-banner__svg-wrapper' ) + ).toBeInTheDocument(); + } ); + it( 'should call the dismiss item endpoint when the banner is dismissed', async () => { mockSurveyEndpoints(); diff --git a/assets/sass/components/banner/_googlesitekit-banner.scss b/assets/sass/components/banner/_googlesitekit-banner.scss index c77ab6a0dfa..d4c18a0ac0a 100644 --- a/assets/sass/components/banner/_googlesitekit-banner.scss +++ b/assets/sass/components/banner/_googlesitekit-banner.scss @@ -115,6 +115,10 @@ } } + a.googlesitekit-banner__description_link { + font-weight: $fw-medium; + } + p.googlesitekit-banner__help-text { color: $c-surfaces-on-surface-variant; font-family: $f-primary; diff --git a/assets/svg/graphics/banner-rrm-express-setup-cta-mobile.svg b/assets/svg/graphics/banner-rrm-express-setup-cta-mobile.svg new file mode 100644 index 00000000000..15432f45704 --- /dev/null +++ b/assets/svg/graphics/banner-rrm-express-setup-cta-mobile.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/graphics/banner-rrm-express-setup-cta-tablet.svg b/assets/svg/graphics/banner-rrm-express-setup-cta-tablet.svg new file mode 100644 index 00000000000..e54e869025f --- /dev/null +++ b/assets/svg/graphics/banner-rrm-express-setup-cta-tablet.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/graphics/banner-rrm-express-setup-cta.svg b/assets/svg/graphics/banner-rrm-express-setup-cta.svg new file mode 100644 index 00000000000..c63875ebdd8 --- /dev/null +++ b/assets/svg/graphics/banner-rrm-express-setup-cta.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_0_small.png new file mode 100644 index 00000000000..91534897248 Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_0_small.png differ diff --git a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_1_medium.png new file mode 100644 index 00000000000..de173c7944b Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_2_large.png new file mode 100644 index 00000000000..1c2bd9a117f Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_ReaderRevenueManager_Components_Dashboard_ReaderRevenueManagerSetupCTABanner_Express_Setup_0_document_2_large.png differ