Commit f729b763f38 for woocommerce
commit f729b763f3829cca961d809bf094c65e3ad2ad1d
Author: Jill Q. <jill.quek@automattic.com>
Date: Fri Apr 24 17:35:17 2026 +0800
Copy improvement: clear permission error when user cannot install plugins (#64187)
* Fix: show clear permission error when user cannot install plugins
Merchants without the install_plugins capability (e.g. editors on
managed hosting) currently see a vague "Sorry, you cannot manage
plugins" message when plugin installation fails. The system already
knows the user lacks permission (the REST API returns a 403), but
the error handler treats it generically.
This adds a permission-specific check in handlePluginAPIError() that
detects 403 responses and shows: "You do not have permission to
install plugins. Please contact your site administrator."
The core profiler (onboarding) already handles this case well via
PluginErrorBanner. This change extends the same clarity to the
general admin path (task lists, payment setup, shipping, tax).
Data context: Tracks shows 1,445 stores hit plugin install errors
with can_install_plugins=false in the last 90 days.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: update permission error message at the PHP source across all endpoints
The vague "Sorry, you cannot manage plugins." message appeared in 6
REST API permission checks across Plugins, OnboardingPlugins,
Marketing, MarketingOverview, PaymentGatewaySuggestions, and
ShippingPartnerSuggestions.
Updated all 6 to: "You do not have permission to install plugins.
Please contact your site administrator."
This aligns with the message already used in the core profiler's
PluginErrorBanner and ensures every API consumer — not just the JS
admin — gets an actionable error message.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Align permission error copy with core profiler ("permissions" plural)
Matches the exact string already used in PluginErrorBanner.tsx so
there is one consistent message across the entire codebase and only
one string for translators to handle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address CodeRabbit feedback: use "manage plugins" and simplify type guard
1. Copy: "install plugins" → "manage plugins" to accurately cover both
install and activate flows (both routed through handlePluginAPIError).
Also updated PluginErrorBanner to keep single-string alignment.
2. Type guard: removed redundant 'data' in error check; the cast alone
(with optional chaining) handles property access safely after
isRestApiError() narrows the union.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Narrow 403 check to the specific plugin-permission error code
Previously any 403 response was treated as a plugin-permission issue,
which would surface the wrong message for unrelated 403s like expired
nonces or authentication failures.
Now matches both status === 403 and code === 'woocommerce_rest_cannot_update'
so we only override the error message when it's genuinely a plugin
permission denial.
Per CodeRabbit review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update PluginErrorBanner tests to match new "manage plugins" copy
Two tests were checking for the old "install plugins" text and failing
after the component copy was updated. Swapping the regex to match the
new string makes all 5 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Re-apply structural color alignment after WP core verification
Verified against WP core's own CSS (wp-admin/css/common.css and
wp-includes/css/buttons.css) that var(--wp-admin-theme-color) is the
correct pattern for button backgrounds, focus rings, borders, and
interactive icon fills. Text links are the exception — WP core keeps
those at a hardcoded accessible blue so merchants on low-contrast
schemes (Coffee, Light, Ocean) can still read them.
Re-applying the 7 structural changes that match WP core's own
pattern for their element types:
- Fulfillments SVG icon fill + darker hover
- Marketplace product card :focus-visible ring
- Homescreen magic link button background
- My-subscriptions info icon fill (paired with text)
- Task list products stack :hover border
- Import-products :hover border
- Reminder bar background
Text link colors remain hardcoded blue, matching WP core convention.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update NoPermissions test and fix Prettier formatting
Adds the second unit test file that also referenced the old "install
plugins" copy. Runs ESLint auto-fix to satisfy Prettier formatting
rules on the regex line.
All 9 tests across PluginErrorBanner and NoPermissions pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "Re-apply structural color alignment after WP core verification"
This reverts commit ab73cda0a1ee40673edb807829b40e40178fef72.
The SCSS changes drifted onto this branch while working on the WP 7.0
alignment sprinkle in parallel. ayushpahwa's review noted the scope
creep — those 7 structural color alignment changes warrant their own
PR with proper description, changelog, and visual verification across
admin color schemes (Coffee, Light, Ocean, etc.). Reverting here so
this PR stays focused on the permission error copy fix.
The SCSS work will ship in a separate, dedicated PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update changelog type from 'fix' to 'tweak'
Per ayushpahwa's review — this is a copy adjustment, not a bug fix.
The original message worked; it was just unhelpful. 'tweak' is the
more accurate type for both the @woocommerce/data and
@woocommerce/plugin-woocommerce changelog entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/packages/js/data/changelog/fix-plugin-install-permission-error-message b/packages/js/data/changelog/fix-plugin-install-permission-error-message
new file mode 100644
index 00000000000..dece5eb7c1e
--- /dev/null
+++ b/packages/js/data/changelog/fix-plugin-install-permission-error-message
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Show a clear permission error message when users without install_plugins capability attempt to install plugins
diff --git a/packages/js/data/src/plugins/actions.ts b/packages/js/data/src/plugins/actions.ts
index 5f26ec557da..d2d7da2f786 100644
--- a/packages/js/data/src/plugins/actions.ts
+++ b/packages/js/data/src/plugins/actions.ts
@@ -174,7 +174,20 @@ function* handlePluginAPIError(
) {
let rawErrorMessage;
- if ( isPluginResponseError( plugins, error ) ) {
+ // Check for plugin-management permission errors before generic handling.
+ // Match the specific code so we don't misattribute other 403s
+ // (e.g. nonce or session failures) as permission problems.
+ const isPermissionError =
+ isRestApiError( error ) &&
+ error.code === 'woocommerce_rest_cannot_update' &&
+ ( error as { data?: { status?: number } } ).data?.status === 403;
+
+ if ( isPermissionError ) {
+ rawErrorMessage = __(
+ 'You do not have permissions to manage plugins. Please contact your site administrator.',
+ 'woocommerce'
+ );
+ } else if ( isPluginResponseError( plugins, error ) ) {
// Backend error messages are in the form of { plugin-slug: [ error messages ] }.
rawErrorMessage = Object.values( error ).join( ', \n' );
} else {
diff --git a/plugins/woocommerce/changelog/fix-plugin-install-permission-error-message b/plugins/woocommerce/changelog/fix-plugin-install-permission-error-message
new file mode 100644
index 00000000000..60cd3290357
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-plugin-install-permission-error-message
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Show a clear error message when users without plugin install permissions attempt to install plugins, instead of a generic failure message
diff --git a/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/PluginErrorBanner.tsx b/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/PluginErrorBanner.tsx
index 8e6c36cd44d..6491a79f2dd 100644
--- a/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/PluginErrorBanner.tsx
+++ b/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/PluginErrorBanner.tsx
@@ -30,7 +30,7 @@ export const PluginErrorBanner = ( {
( e ) => e.errorDetails?.data?.data?.status === 403 // 403 is the code representing rest_authorization_required_code()
):
installationErrorMessage = __(
- 'You do not have permissions to install plugins. Please contact your site administrator.',
+ 'You do not have permissions to manage plugins. Please contact your site administrator.',
'woocommerce'
);
break;
diff --git a/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/test/index.tsx b/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/test/index.tsx
index cb0315f5002..df4eda5860f 100644
--- a/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/test/index.tsx
+++ b/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/components/plugin-error-banner/test/index.tsx
@@ -14,9 +14,7 @@ describe( 'PluginErrorBanner', () => {
<PluginErrorBanner pluginsInstallationPermissionsFailure={ true } />
);
expect(
- screen.getByText(
- /You do not have permissions to install plugins/i
- )
+ screen.getByText( /You do not have permissions to manage plugins/i )
).toBeInTheDocument();
} );
@@ -103,9 +101,7 @@ describe( 'PluginErrorBanner', () => {
/>
);
expect(
- screen.getByText(
- /You do not have permissions to install plugins/i
- )
+ screen.getByText( /You do not have permissions to manage plugins/i )
).toBeInTheDocument();
} );
diff --git a/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/test/NoPermissions.test.tsx b/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/test/NoPermissions.test.tsx
index 259130d3e7c..f7df47650b4 100644
--- a/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/test/NoPermissions.test.tsx
+++ b/plugins/woocommerce/client/admin/client/core-profiler/pages/Plugins/test/NoPermissions.test.tsx
@@ -71,7 +71,7 @@ describe( 'NoPermissions', () => {
expect(
screen.getByText(
- 'You do not have permissions to install plugins. Please contact your site administrator.'
+ 'You do not have permissions to manage plugins. Please contact your site administrator.'
)
).toBeInTheDocument();
} );
diff --git a/plugins/woocommerce/src/Admin/API/Marketing.php b/plugins/woocommerce/src/Admin/API/Marketing.php
index 4a8cacf2fa5..fe430aefe34 100644
--- a/plugins/woocommerce/src/Admin/API/Marketing.php
+++ b/plugins/woocommerce/src/Admin/API/Marketing.php
@@ -102,7 +102,7 @@ class Marketing extends \WC_REST_Data_Controller {
*/
public function get_recommended_plugins_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
- return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
+ return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php
index 930dcb4c0fc..1627a2280b7 100644
--- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php
+++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php
@@ -111,7 +111,7 @@ class MarketingOverview extends \WC_REST_Data_Controller {
*/
public function install_plugins_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
- return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
+ return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
diff --git a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php
index 35310b361b4..bd6a99d2f3d 100644
--- a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php
+++ b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php
@@ -241,7 +241,7 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
if ( ! current_user_can( 'install_plugins' ) ) {
return new WP_Error(
'woocommerce_rest_cannot_update',
- __( 'Sorry, you cannot manage plugins.', 'woocommerce' ),
+ __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ),
array( 'status' => rest_authorization_required_code() )
);
}
@@ -258,7 +258,7 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'woocommerce_rest_cannot_update',
- __( 'Sorry, you cannot manage plugins.', 'woocommerce' ),
+ __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ),
array( 'status' => rest_authorization_required_code() )
);
}
diff --git a/plugins/woocommerce/src/Admin/API/PaymentGatewaySuggestions.php b/plugins/woocommerce/src/Admin/API/PaymentGatewaySuggestions.php
index 702fb3fd446..2b4fa5e4948 100644
--- a/plugins/woocommerce/src/Admin/API/PaymentGatewaySuggestions.php
+++ b/plugins/woocommerce/src/Admin/API/PaymentGatewaySuggestions.php
@@ -80,7 +80,7 @@ class PaymentGatewaySuggestions extends \WC_REST_Data_Controller {
*/
public function get_permission_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
- return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
+ return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
diff --git a/plugins/woocommerce/src/Admin/API/Plugins.php b/plugins/woocommerce/src/Admin/API/Plugins.php
index c2e16f23ddf..e153d87917b 100644
--- a/plugins/woocommerce/src/Admin/API/Plugins.php
+++ b/plugins/woocommerce/src/Admin/API/Plugins.php
@@ -215,7 +215,7 @@ class Plugins extends \WC_REST_Data_Controller {
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
- return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
+ return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
diff --git a/plugins/woocommerce/src/Admin/API/ShippingPartnerSuggestions.php b/plugins/woocommerce/src/Admin/API/ShippingPartnerSuggestions.php
index ec56e77bcd1..9d6cbd617cb 100644
--- a/plugins/woocommerce/src/Admin/API/ShippingPartnerSuggestions.php
+++ b/plugins/woocommerce/src/Admin/API/ShippingPartnerSuggestions.php
@@ -64,7 +64,7 @@ class ShippingPartnerSuggestions extends \WC_REST_Data_Controller {
*/
public function get_permission_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
- return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
+ return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'You do not have permissions to manage plugins. Please contact your site administrator.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}