Commit a949bc83aa4 for woocommerce

commit a949bc83aa413932877fdbf203dc4ffe2b3031e1
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Thu Aug 27 17:29:31 2026 +0300

    Dispatch thrown plugin errors as error notices in createNoticesFromResponse (#68095)

    fix(notices): dispatch thrown errors as error notices

    createNoticesFromResponse() was written for REST response shapes: it
    loops over `errors`, and otherwise picks the notice status from the
    presence of `code`. The plugin actions in @woocommerce/data reject with
    a PluginError, an Error subclass carrying a message and per-plugin data
    but no code, and around seventeen client/admin callers hand that
    rejection straight to the helper. It fell into the generic branch and
    dispatched the failure to core/notices with status `success`: right
    text, wrong status.

    No surface renders that difference today. wc-admin's only consumer of
    core/notices is TransientNotices -> SnackbarList -> Snackbar, which
    drops `status` and hard-codes a polite announcement, as upstream's
    Snackbar does. So this corrects the data written to a shared store
    rather than anything a merchant currently sees. It stays worth fixing
    because the moment any surface reads `status`, every plugin install
    failure reads as a success.

    Add an `instanceof Error` branch ahead of the generic one that always
    dispatches `error`. It sits after the existing TypeError/offline check
    so that copy is unchanged, and no REST response object is an Error, so
    every existing branch keeps its behaviour. Fixing the helper covers all
    current callers, and future ones, without touching them.

    Refs #68092

    Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-68092-error-notices-from-plugin-error b/plugins/woocommerce/changelog/fix-68092-error-notices-from-plugin-error
new file mode 100644
index 00000000000..588db584f51
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-68092-error-notices-from-plugin-error
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Dispatch plugin install/activate failures as error notices instead of success notices when the rejected PluginError is passed to createNoticesFromResponse().
diff --git a/plugins/woocommerce/client/admin/client/lib/notices/index.js b/plugins/woocommerce/client/admin/client/lib/notices/index.js
index 62c87778251..3b99c59603d 100644
--- a/plugins/woocommerce/client/admin/client/lib/notices/index.js
+++ b/plugins/woocommerce/client/admin/client/lib/notices/index.js
@@ -67,6 +67,12 @@ export function createNoticesFromResponse( response ) {
 		Object.keys( response.errors ).forEach( ( errorKey ) => {
 			createNotice( 'error', response.errors[ errorKey ].join( ' ' ) );
 		} );
+	} else if ( response instanceof Error ) {
+		// A thrown Error (e.g. the PluginError rejected by @woocommerce/data's
+		// plugin actions) is always a failure, even though it carries no code.
+		if ( response.message ) {
+			createNotice( 'error', response.message );
+		}
 	} else if ( response.message ) {
 		// Handle generic messages.
 		createNotice( response.code ? 'error' : 'success', response.message );
diff --git a/plugins/woocommerce/client/admin/client/lib/notices/test/index.js b/plugins/woocommerce/client/admin/client/lib/notices/test/index.js
index 1620722e1cd..7c4311f516d 100644
--- a/plugins/woocommerce/client/admin/client/lib/notices/test/index.js
+++ b/plugins/woocommerce/client/admin/client/lib/notices/test/index.js
@@ -81,6 +81,35 @@ describe( 'createNoticesFromResponse', () => {
 		expect( call2 ).toEqual( [ 'error', response.errors.item2[ 0 ] ] );
 	} );

+	test( 'should create an error notice when the response is a thrown Error', () => {
+		// Mirrors the PluginError thrown by @woocommerce/data's plugin actions: an
+		// Error subclass carrying a message and data, but no code.
+		class PluginError extends Error {
+			constructor( message, data ) {
+				super( message );
+				this.data = data;
+			}
+		}
+		const response = new PluginError( 'Could not install foo. Reason.', {
+			foo: [ 'Reason.' ],
+		} );
+
+		createNoticesFromResponse( response );
+		expect( createNotice ).toHaveBeenCalledWith(
+			'error',
+			response.message
+		);
+		expect( createNotice ).toHaveBeenCalledTimes( 1 );
+	} );
+
+	test( 'should not create an empty error notice when an Error has no message', () => {
+		// Pins the message guard inside the Error branch: an Error inherits an
+		// empty `message` from the prototype, so without the guard this would
+		// dispatch a notice with no text.
+		createNoticesFromResponse( new Error() );
+		expect( createNotice ).not.toHaveBeenCalled();
+	} );
+
 	test( 'should not call createNotice when no message or errors exist', () => {
 		const response = { data: {} };