diff --git a/assets/js/components/notifications/FirstPartyModeSetupBanner.js b/assets/js/components/notifications/FirstPartyModeSetupBanner.js
index be0f84c0fc6..aba0155193f 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 );
@@ -182,6 +184,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-reader-revenue-manager-block-editor.js b/assets/js/googlesitekit-reader-revenue-manager-block-editor.js
new file mode 100644
index 00000000000..cc450da9143
--- /dev/null
+++ b/assets/js/googlesitekit-reader-revenue-manager-block-editor.js
@@ -0,0 +1,17 @@
+/**
+ * Reader Revenue Manager module's block editor entrypoint.
+ *
+ * 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.
+ */
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 }
context->url( 'dist/assets/' );
- return array(
+ $assets = array(
new Script(
'googlesitekit-modules-reader-revenue-manager',
array(
@@ -400,6 +402,19 @@ protected function setup_assets() {
)
),
);
+
+ if ( Feature_Flags::enabled( 'rrmModuleV2' ) ) {
+ $assets[] = new Script(
+ 'googlesitekit-reader-revenue-manager-block-editor',
+ array(
+ 'src' => $base_url . 'js/googlesitekit-reader-revenue-manager-block-editor.js',
+ 'dependencies' => array(),
+ 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ),
+ )
+ );
+ }
+
+ return $assets;
}
/**
diff --git a/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_0_small.png
new file mode 100644
index 00000000000..713fb895238
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_0_small.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_1_medium.png
new file mode 100644
index 00000000000..9c6ad0b907f
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_1_medium.png differ
diff --git a/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_2_large.png
new file mode 100644
index 00000000000..925f8b2a31b
Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_FirstPartyMode_Dashboard_FirstPartyModeSetupBanner_ErrorOnCTAClick_0_document_2_large.png differ
diff --git a/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php b/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php
index bcf3d83e181..1315ba143cd 100644
--- a/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php
+++ b/tests/phpunit/integration/Modules/Reader_Revenue_ManagerTest.php
@@ -486,6 +486,36 @@ public function test_check_service_entity_access_no_access_unavailable_publicati
$this->assertEquals( false, $access );
}
+ public function test_block_editor_script_enqueued() {
+ $this->enable_feature( 'rrmModuleV2' );
+
+ $registerable_asset_handles = array_map(
+ function ( $asset ) {
+ return $asset->get_handle();
+ },
+ $this->reader_revenue_manager->get_assets()
+ );
+
+ $this->assertContains(
+ 'googlesitekit-reader-revenue-manager-block-editor',
+ $registerable_asset_handles
+ );
+ }
+
+ public function test_block_editor_script_not_enqueued() {
+ $registerable_asset_handles = array_map(
+ function ( $asset ) {
+ return $asset->get_handle();
+ },
+ $this->reader_revenue_manager->get_assets()
+ );
+
+ $this->assertNotContains(
+ 'googlesitekit-reader-revenue-manager-block-editor',
+ $registerable_asset_handles
+ );
+ }
+
/**
* @return Module_With_Scopes
*/
diff --git a/webpack.config.js b/webpack.config.js
index 981f643ae03..fab7b57cc34 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -22,6 +22,7 @@
const { createRules } = require( './webpack/common' );
const adminCssConfig = require( './webpack/adminCss.config' );
const basicModulesConfig = require( './webpack/basicModules.config' );
+const blockEditorConfig = require( './webpack/blockEditor.config' );
const frontendModules = require( './webpack/frontendModules.config' );
const modulesConfig = require( './webpack/modules.config' );
const testBundleConfig = require( './webpack/testBundle.config' );
@@ -45,6 +46,9 @@ function* webpackConfig( env, argv ) {
// Build modules that will be used on the frontend, that require wider browser support.
yield frontendModules( mode );
+ // Build the block editor js.
+ yield blockEditorConfig( mode );
+
// Build the main plugin admin css.
yield adminCssConfig( mode );
}
diff --git a/webpack/blockEditor.config.js b/webpack/blockEditor.config.js
new file mode 100644
index 00000000000..9fa60762fc7
--- /dev/null
+++ b/webpack/blockEditor.config.js
@@ -0,0 +1,81 @@
+/**
+ * Block editor config webpack partial.
+ *
+ * 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
+ */
+const CircularDependencyPlugin = require( 'circular-dependency-plugin' );
+const ESLintPlugin = require( 'eslint-webpack-plugin' );
+const ManifestPlugin = require( 'webpack-manifest-plugin' );
+const WebpackBar = require( 'webpackbar' );
+
+/**
+ * Internal dependencies
+ */
+const {
+ rootDir,
+ manifestArgs,
+ resolve,
+ gutenbergExternals,
+ createRules,
+ createMinimizerRules,
+} = require( './common' );
+
+module.exports = ( mode ) => ( {
+ entry: {
+ 'googlesitekit-reader-revenue-manager-block-editor':
+ './assets/js/googlesitekit-reader-revenue-manager-block-editor.js',
+ },
+ externals: gutenbergExternals,
+ output: {
+ filename:
+ mode === 'production' ? '[name]-[contenthash].js' : '[name].js',
+ path: rootDir + '/dist/assets/js',
+ publicPath: '',
+ },
+ module: {
+ rules: createRules( mode ),
+ },
+ plugins: [
+ new WebpackBar( {
+ name: 'Block Editor Entry Points',
+ color: '#deff13',
+ } ),
+ new CircularDependencyPlugin( {
+ exclude: /node_modules/,
+ failOnError: true,
+ allowAsyncCycles: false,
+ cwd: process.cwd(),
+ } ),
+ new ManifestPlugin( {
+ ...manifestArgs( mode ),
+ filter( file ) {
+ return ( file.name || '' ).match( /\.js$/ );
+ },
+ } ),
+ new ESLintPlugin( {
+ emitError: true,
+ emitWarning: true,
+ failOnError: true,
+ } ),
+ ],
+ optimization: {
+ minimizer: createMinimizerRules( mode ),
+ },
+ resolve,
+} );
diff --git a/webpack/modules.config.js b/webpack/modules.config.js
index 8f69abe4758..f34576c4138 100644
--- a/webpack/modules.config.js
+++ b/webpack/modules.config.js
@@ -117,7 +117,7 @@ module.exports = ( mode, rules, ANALYZE ) => {
// If multiple webpack runtimes (from different compilations) are used on the
// same webpage, there is a risk of conflicts of on-demand chunks in the global
// namespace.
- // See: https://webpack.js.org/configuration/output/#outputjsonpfunction.
+ // See: https://v4.webpack.js.org/configuration/output/#outputjsonpfunction.
jsonpFunction: '__googlesitekit_webpackJsonp',
},
performance: {
|