diff --git a/assets/js/components/notifications/FirstPartyModeSetupBanner.js b/assets/js/components/notifications/FirstPartyModeSetupBanner.js index 18be396df31..d43a994ff76 100644 --- a/assets/js/components/notifications/FirstPartyModeSetupBanner.js +++ b/assets/js/components/notifications/FirstPartyModeSetupBanner.js @@ -75,12 +75,12 @@ export default function FirstPartyModeSetupBanner( { id, Notification } ) { select( CORE_NOTIFICATIONS ).isNotificationDismissed( id ) ); + const { invalidateResolution } = useDispatch( CORE_NOTIFICATIONS ); + const isDismissing = useSelect( ( select ) => select( CORE_USER ).isDismissingItem( id ) ); - const { dismissNotification, invalidateResolution } = - useDispatch( CORE_NOTIFICATIONS ); const { setValue } = useDispatch( CORE_UI ); const learnMoreURL = useSelect( ( select ) => { @@ -97,15 +97,17 @@ export default function FirstPartyModeSetupBanner( { id, Notification } ) { const onCTAClick = async () => { setFirstPartyModeEnabled( true ); - await saveFirstPartyModeSettings(); + const { error } = await saveFirstPartyModeSettings(); + + if ( error ) { + return { error }; + } setValue( FPM_SHOW_SETUP_SUCCESS_NOTIFICATION, true ); invalidateResolution( 'getQueuedNotifications', [ viewContext, NOTIFICATION_GROUPS.DEFAULT, ] ); - - dismissNotification( id ); }; const { triggerSurvey } = useDispatch( CORE_USER ); @@ -189,6 +191,9 @@ export default function FirstPartyModeSetupBanner( { id, Notification } ) { 'google-site-kit' ) } onCTAClick={ onCTAClick } + ctaDismissOptions={ { + skipHidingFromQueue: false, + } } dismissLabel={ __( 'Maybe later', 'google-site-kit' ) } onDismiss={ onDismiss } dismissOptions={ { diff --git a/assets/js/components/notifications/FirstPartyModeSetupBanner.stories.js b/assets/js/components/notifications/FirstPartyModeSetupBanner.stories.js index 6487d580b30..7788c23e7d8 100644 --- a/assets/js/components/notifications/FirstPartyModeSetupBanner.stories.js +++ b/assets/js/components/notifications/FirstPartyModeSetupBanner.stories.js @@ -26,6 +26,7 @@ import fetchMock from 'fetch-mock'; */ import { provideModules } from '../../../../tests/js/utils'; import WithRegistrySetup from '../../../../tests/js/WithRegistrySetup'; +import { CORE_SITE } from '../../googlesitekit/datastore/site/constants'; import { CORE_USER } from '../../googlesitekit/datastore/user/constants'; import { withNotificationComponentProps } from '../../googlesitekit/notifications/util/component-props'; import { WEEK_IN_SECONDS } from '../../util'; @@ -49,10 +50,27 @@ export const Default = Template.bind(); Default.storyName = 'FirstPartyModeSetupBanner'; Default.scenario = {}; +export const ErrorOnCTAClick = Template.bind(); +ErrorOnCTAClick.storyName = 'ErrorOnCTAClick'; +ErrorOnCTAClick.scenario = {}; +ErrorOnCTAClick.args = { + setupRegistry: ( registry ) => { + registry.dispatch( CORE_SITE ).receiveError( + { + code: 'test_error', + message: 'Test Error', + data: {}, + }, + 'notificationAction', + [ FPM_SETUP_CTA_BANNER_NOTIFICATION ] + ); + }, +}; + export default { title: 'Modules/FirstPartyMode/Dashboard/FirstPartyModeSetupBanner', decorators: [ - ( Story ) => { + ( Story, { args } ) => { const setupRegistry = ( registry ) => { provideModules( registry, [ { @@ -87,6 +105,8 @@ export default { status: 200, } ); + + args.setupRegistry?.( registry ); }; return ( diff --git a/assets/js/components/notifications/FirstPartyModeSetupBanner.test.js b/assets/js/components/notifications/FirstPartyModeSetupBanner.test.js index f89fc038a9d..2675daa19c2 100644 --- a/assets/js/components/notifications/FirstPartyModeSetupBanner.test.js +++ b/assets/js/components/notifications/FirstPartyModeSetupBanner.test.js @@ -247,6 +247,47 @@ describe( 'FirstPartyModeSetupBanner', () => { } ); } ); + it( 'should display the error message when the CTA button is clicked and the request fails', async () => { + fetchMock.postOnce( fpmSettingsEndpoint, { + body: JSON.stringify( { + code: 'test_error', + message: 'Test Error', + data: { + reason: 'test_reason', + }, + } ), + status: 500, + } ); + + const { getByRole, getByText, waitForRegistry } = render( + , + { + registry, + viewContext: VIEW_CONTEXT_MAIN_DASHBOARD, + } + ); + + await waitForRegistry(); + + fetchMock.post( dismissItemEndpoint, { + body: JSON.stringify( [ FPM_SETUP_CTA_BANNER_NOTIFICATION ] ), + status: 200, + } ); + + fireEvent.click( + getByRole( 'button', { + name: 'Enable First-party mode', + } ) + ); + + await waitFor( () => { + expect( fetchMock ).toHaveFetched( fpmSettingsEndpoint ); + expect( fetchMock ).not.toHaveFetched( dismissItemEndpoint ); + } ); + + expect( getByText( 'Error: Test Error' ) ).toBeInTheDocument(); + } ); + it( 'should set FPM_SHOW_SETUP_SUCCESS_NOTIFICATION to true and invalidate the notifications queue resolution when the CTA button is clicked', async () => { const { getByRole, waitForRegistry } = render( , { registry, diff --git a/assets/js/googlesitekit/notifications/components/common/ActionsCTALinkDismiss.js b/assets/js/googlesitekit/notifications/components/common/ActionsCTALinkDismiss.js index 0cc4c2e66ee..023a02a633f 100644 --- a/assets/js/googlesitekit/notifications/components/common/ActionsCTALinkDismiss.js +++ b/assets/js/googlesitekit/notifications/components/common/ActionsCTALinkDismiss.js @@ -14,10 +14,16 @@ * limitations under the License. */ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; /** * Internal dependencies @@ -33,8 +39,10 @@ export default function ActionsCTALinkDismiss( { ctaLink, ctaLabel, onCTAClick, + ctaDismissOptions, onDismiss = () => {}, dismissLabel = __( 'OK, Got it!', 'google-site-kit' ), + dismissOnCTAClick = true, dismissExpires = 0, dismissOptions = {}, } ) { @@ -45,24 +53,42 @@ export default function ActionsCTALinkDismiss( { } ); return ( -
- + +
+ - -
+ +
+ ); } + +ActionsCTALinkDismiss.propTypes = { + id: PropTypes.string, + className: PropTypes.string, + ctaLink: PropTypes.string, + ctaLabel: PropTypes.string, + onCTAClick: PropTypes.func, + onDismiss: PropTypes.func, + ctaDismissOptions: PropTypes.object, + dismissLabel: PropTypes.string, + dismissOnCTAClick: PropTypes.bool, + dismissExpires: PropTypes.number, + dismissOptions: PropTypes.object, +}; diff --git a/assets/js/googlesitekit/notifications/components/common/CTALink.js b/assets/js/googlesitekit/notifications/components/common/CTALink.js index 5a13586157c..1440a8241a8 100644 --- a/assets/js/googlesitekit/notifications/components/common/CTALink.js +++ b/assets/js/googlesitekit/notifications/components/common/CTALink.js @@ -31,6 +31,7 @@ import { useState } from '@wordpress/element'; import { useDispatch, useSelect } from 'googlesitekit-data'; import { CORE_NOTIFICATIONS } from '../../datastore/constants'; import { CORE_LOCATION } from '../../../datastore/location/constants'; +import { CORE_SITE } from '../../../datastore/site/constants'; import useNotificationEvents from '../../hooks/useNotificationEvents'; import { SpinnerButton } from 'googlesitekit-components'; @@ -39,7 +40,9 @@ export default function CTALink( { ctaLink, ctaLabel, onCTAClick, - dismissExpires = -1, + dismissOnCTAClick = false, + dismissExpires = 0, + dismissOptions = { skipHidingFromQueue: true }, } ) { const [ isAwaitingCTAResponse, setIsAwaitingCTAResponse ] = useState( false ); @@ -53,28 +56,39 @@ export default function CTALink( { : false; } ); + const { clearError, receiveError } = useDispatch( CORE_SITE ); + const { dismissNotification } = useDispatch( CORE_NOTIFICATIONS ); const { navigateTo } = useDispatch( CORE_LOCATION ); const handleCTAClick = async ( event ) => { + clearError( 'notificationAction', [ id ] ); + event.persist(); if ( ! event.defaultPrevented && ctaLink ) { event.preventDefault(); } setIsAwaitingCTAResponse( true ); - await onCTAClick?.( event ); + + const { error } = ( await onCTAClick?.( event ) ) || {}; + if ( isMounted() ) { setIsAwaitingCTAResponse( false ); } + if ( error ) { + receiveError( error, 'notificationAction', [ id ] ); + return; + } + const ctaClickActions = [ trackEvents.confirm() ]; - if ( dismissExpires >= 0 ) { + if ( dismissOnCTAClick ) { ctaClickActions.push( dismissNotification( id, { + ...dismissOptions, expiresInSeconds: dismissExpires, - skipHidingFromQueue: true, } ) ); } @@ -105,5 +119,7 @@ CTALink.propTypes = { ctaLink: PropTypes.string, ctaLabel: PropTypes.string, onCTAClick: PropTypes.func, + dismissOnCTAClick: PropTypes.bool, dismissExpires: PropTypes.number, + dismissOptions: PropTypes.object, }; diff --git a/assets/js/googlesitekit/notifications/components/common/Error.js b/assets/js/googlesitekit/notifications/components/common/Error.js new file mode 100644 index 00000000000..89a5bc7d0cc --- /dev/null +++ b/assets/js/googlesitekit/notifications/components/common/Error.js @@ -0,0 +1,53 @@ +/** + * Site Kit by Google, Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/* + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useDispatch, useSelect } from 'googlesitekit-data'; +import { CORE_SITE } from '../../../datastore/site/constants'; +import ErrorText from '../../../../components/ErrorText'; + +export default function Error( { id } ) { + const ctaError = useSelect( ( select ) => { + return select( CORE_SITE ).getError( 'notificationAction', [ id ] ); + } ); + + const { clearError } = useDispatch( CORE_SITE ); + + useEffect( () => { + return () => { + clearError( 'notificationAction', [ id ] ); + }; + }, [ clearError, id ] ); + + return ctaError ? : null; +} + +// eslint-disable-next-line sitekit/acronym-case +Error.propTypes = { + id: PropTypes.string, +}; diff --git a/assets/js/googlesitekit/notifications/components/layout/NotificationWithSVG.js b/assets/js/googlesitekit/notifications/components/layout/NotificationWithSVG.js index f35af34fa5b..c1e91c69b01 100644 --- a/assets/js/googlesitekit/notifications/components/layout/NotificationWithSVG.js +++ b/assets/js/googlesitekit/notifications/components/layout/NotificationWithSVG.js @@ -28,6 +28,7 @@ import { useBreakpoint, } from '../../../../hooks/useBreakpoint'; import { Cell, Grid, Row } from '../../../../material-components'; +import Error from '../common/Error'; export default function NotificationWithSVG( { id, @@ -77,6 +78,7 @@ export default function NotificationWithSVG( { { description } + { actions }