Skip to content

admin-ui: Add Upgrade to Pro menu item for free users#47418

Open
DevinWalker wants to merge 16 commits intotrunkfrom
feature/upsell-to-pro-wp-admin-menu
Open

admin-ui: Add Upgrade to Pro menu item for free users#47418
DevinWalker wants to merge 16 commits intotrunkfrom
feature/upsell-to-pro-wp-admin-menu

Conversation

@DevinWalker
Copy link
Contributor

@DevinWalker DevinWalker commented Mar 3, 2026

Fixes #

Proposed changes:

2026-03-02_22-54-22

Adds a styled "Upgrade to Pro" submenu item to the Jetpack wp-admin menu for sites on the free plan.

  • Displays a filled star icon and "Upgrade to Pro" label in Jetpack green (#069e08) at the bottom of the Jetpack submenu (position 999)
  • Suppressed automatically for any site with an active paid plan or license (jetpack_active_plan option class is not free)
  • Only visible to users with manage_options capability
  • Implemented once in the shared packages/admin-ui Admin_Menu class — propagates automatically to every plugin that registers menus through it (Jetpack, Backup, Boost, Protect, Social, Search, VideoPress)
  • Inline styles are scoped to Jetpack admin screens only, avoiding unnecessary DB reads on unrelated admin pages

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

Does this pull request change what data or activity we track or use?

No.

Testing instructions:

  1. Activate any Jetpack plugin (Jetpack, Backup, Boost, Protect, Social, Search, or VideoPress) on a local WordPress install
  2. Log in as an administrator
  3. Open the Jetpack submenu in wp-admin — a green "Upgrade to Pro" item with a star icon should appear at the bottom
  4. To test the paid-plan suppression, run: wp option update jetpack_active_plan '{"class":"security"}' --format=json
  5. Refresh — the menu item should be gone
  6. Delete the option to restore: wp option delete jetpack_active_plan
  7. Confirm the item does not appear for editor-role users (only administrators)

Unit tests: jp test php packages/admin-ui

Changelog

  • Generate changelog entries for this PR (using AI).

Made with Cursor

DevinWalker and others added 14 commits March 2, 2026 14:58
Adjusts the menu positions for Jetpack admin menu items to place all links that open in new windows (external links marked with ↗) after internal links. This improves the user experience by grouping similar link types together.

Changes:
- Activity Log: moved from position 8 to 14
- Subscribers: moved from position 11 to 15
- Jetpack Manage: moved from position 15 to 16
- Scan & VaultPress Backup (external): base offset changed from 9 to 17
- Updated test to verify external links appear after Settings

Internal links (Settings at position 13) now appear before all external links.

Made-with: Cursor
Replace @automattic/jetpack-components Button with @wordpress/components Button in the BackupNowButton component for better consistency with WordPress core components.

Changes:
- Updated import to use @wordpress/components Button
- Removed custom weight prop (not supported by WordPress Button)
- Updated variant default to 'solid'
- Added size='compact' prop for appropriate button sizing

Made-with: Cursor
Simplify Jetpack admin menu item titles for better readability:
- "Akismet Anti-spam" → "Anti-spam"
- "VaultPress Backup" → "Backups"

These shorter titles provide a cleaner menu experience while maintaining clarity about the product functionality.

Made-with: Cursor
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Changed the variant prop in the BackupNowButton component from 'primary', 'secondary', 'tertiary' to 'solid', 'outline', 'minimal', 'unstyled' for improved flexibility in button styling.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…dule.scss

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Adds a styled "Upgrade to Pro" submenu item to the Jetpack wp-admin menu
for sites on the free plan. The item shows a star icon in Jetpack green
(#069e08) and links to the Jetpack upgrade page. It is suppressed for any
site with an active paid plan or license.

Because all Jetpack standalone plugins (Backup, Boost, Protect, Social,
Search, VideoPress) register their menus through the shared Admin_Menu
class, this single change propagates to every plugin automatically.

Made-with: Cursor
Copilot AI review requested due to automatic review settings March 3, 2026 06:13
@DevinWalker DevinWalker added the [Status] Needs Review This PR is ready for review. label Mar 3, 2026
@DevinWalker DevinWalker self-assigned this Mar 3, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Are you an Automattician? The PR will need to be tested on WordPress.com. This comment will be updated with testing instructions as soon the build is complete.

@github-actions github-actions bot added [Feature] Scan [JS Package] Components [Package] Admin Ui [Package] Backup [Package] My Jetpack [Plugin] Boost A feature to speed up the site and improve performance. [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Tests] Includes Tests RNA labels Mar 3, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Boost plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an “Upgrade to Pro” upsell entry to the Jetpack wp-admin submenu for free-plan sites (via the shared packages/admin-ui menu wrapper), and adjusts related menu ordering/styling across several Jetpack packages/plugins.

Changes:

  • Add conditional “Upgrade to Pro” submenu item + inline styling in Automattic\Jetpack\Admin_UI\Admin_Menu (free-plan + manage_options).
  • Reorder/admin-menu-position tweaks so external links appear after internal links in the Jetpack submenu.
  • Misc UI/text updates (e.g., Backup menu label, Boost header padding, admin page subtitle padding, readme contributor lists) with changelog entries.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
projects/packages/admin-ui/src/class-admin-menu.php Adds free-plan detection, registers the “Upgrade to Pro” submenu item, and outputs inline CSS styling on Jetpack screens.
projects/packages/admin-ui/tests/php/Admin_Menu_Test.php Adds unit tests for upgrade menu visibility and CSS output gating.
projects/packages/admin-ui/composer.json Adds package deps for plans/redirect utilities used by the new logic.
projects/packages/admin-ui/changelog/feature-upsell-to-pro-wp-admin-menu Changelog entry for the new upsell menu item.
projects/packages/admin-ui/changelog/update-header-and-nav-cleanup-and-improvements Changelog entry for Akismet menu label change.
projects/plugins/jetpack/tests/php/general/Jetpack_Admin_Menu_Test.php Updates ordering assertions to ensure external links come after internal links.
projects/plugins/jetpack/modules/subscriptions.php Adjusts submenu position to support new ordering.
projects/plugins/jetpack/modules/scan/class-admin-sidebar-link.php Adjusts submenu insertion offset to support new ordering.
projects/packages/my-jetpack/src/class-activitylog.php Moves Activity Log submenu position later (external-link ordering).
projects/packages/my-jetpack/src/class-jetpack-manage.php Moves Jetpack Manage submenu position later (external-link ordering).
projects/packages/my-jetpack/changelog/update-header-and-nav-cleanup-and-improvements Changelog entry for menu reordering behavior.
projects/packages/backup/src/class-jetpack-backup.php Renames submenu label from “VaultPress Backup” to “Backups”.
projects/packages/backup/src/js/components/back-up-now/index.jsx Switches Button import and updates props/defaults for the BackupNowButton UI.
projects/packages/backup/changelog/simplify-menu-title Changelog entry for “Backups” label change.
projects/packages/backup/changelog/update-header-and-nav-cleanup-and-improvements Changelog entry for Button component change.
projects/plugins/jetpack/readme.txt Updates contributor list.
projects/plugins/boost/readme.txt Updates contributor list.
projects/plugins/boost/app/assets/src/js/layout/header/header.module.scss Removes header description padding.
projects/plugins/boost/changelog/update-header-and-nav-cleanup-and-improvements Changelog entry for Boost readme/style adjustments.
projects/plugins/jetpack/changelog/update-header-and-nav-cleanup-and-improvements Changelog entry for admin menu ordering change.
projects/js-packages/components/components/admin-page/style.module.scss Removes subtitle padding in Jetpack admin page header styling.
projects/js-packages/components/changelog/update-header-and-nav-cleanup-and-improvements Changelog entry for the admin-page subtitle padding change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 22 to +25
font-size: 13px;
color: #757575;
margin: 0;
padding-block-end: 8px;
padding: 0;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description focuses on adding an "Upgrade to Pro" Jetpack admin menu item, but this change set also modifies Boost header styling (and there are other unrelated changes in this PR such as readme contributor updates and a Backup button component swap). Please either update the PR description/title to reflect the additional scope or split these unrelated changes into separate PRs so they can be reviewed and shipped independently.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to 71
delete_option( 'jetpack_active_plan' );
}

/**
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admin_Menu keeps state in static properties ($initialized, $menu_items). The new setUp() resets $submenu and the plan option, but it doesn't reset Admin_Menu's static state, so earlier tests that call Admin_Menu::add_menu() can leak menu items into later tests and make the suite order-dependent. Consider clearing those static properties in setUp() (via a dedicated reset method or reflection) to keep tests isolated.

Suggested change
delete_option( 'jetpack_active_plan' );
}
/**
delete_option( 'jetpack_active_plan' );
$this->reset_admin_menu_static_state();
}
/**
* Reset Admin_Menu static state to its class defaults.
*
* @return void
*/
private function reset_admin_menu_static_state(): void {
if ( ! class_exists( Admin_Menu::class ) ) {
return;
}
$reflection = new \ReflectionClass( Admin_Menu::class );
$defaults = $reflection->getDefaultProperties();
foreach ( array( 'initialized', 'menu_items' ) as $property_name ) {
if ( ! $reflection->hasProperty( $property_name ) ) {
continue;
}
$property = $reflection->getProperty( $property_name );
if ( ! $property->isStatic() ) {
continue;
}
if ( ! array_key_exists( $property_name, $defaults ) ) {
continue;
}
$property->setAccessible( true );
$property->setValue( null, $defaults[ $property_name ] );
}
}
/**

Copilot uses AI. Check for mistakes.
Comment on lines +315 to +323
public static function add_upgrade_menu_item_styles() {
$screen = get_current_screen();
if ( ! $screen || false === strpos( $screen->id, 'jetpack' ) ) {
return;
}

if ( ! self::is_free_plan() ) {
return;
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_upgrade_menu_item_styles() only checks the screen and plan, but not the user capability. Since the upgrade menu item is only registered for manage_options, this can output CSS for users who will never see the item (e.g. editors), and it also adds an extra plan lookup on Jetpack screens for those users. Consider early-returning when ! current_user_can( 'manage_options' ) to keep behavior aligned and avoid unnecessary work.

Copilot uses AI. Check for mistakes.
Comment on lines +262 to +276
* Conditionally adds an "Upgrade to Pro" submenu item for free-plan sites.
*
* The item is only added when the Jetpack top-level menu is visible and the
* site has not yet purchased a paid Jetpack plan or license.
*
* @return void
*/
private static function maybe_add_upgrade_menu_item() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

if ( ! self::is_free_plan() ) {
return;
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docblock for maybe_add_upgrade_menu_item() says the item is only added when the Jetpack top-level menu is visible, but the implementation doesn't actually check that the jetpack top-level menu still exists (it may have been removed when $can_see_toplevel_menu is false). Either enforce the condition (e.g. check that the jetpack menu page is present before calling add_submenu_page) or update the docblock to match the real behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +57
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();

self::$admin_user_id = wp_insert_user(
array(
'user_login' => 'upgrade_test_admin',
'user_pass' => 'pass',
'user_email' => 'upgrade_admin@example.com',
'role' => 'administrator',
)
);

self::$editor_user_id = wp_insert_user(
array(
'user_login' => 'upgrade_test_editor',
'user_pass' => 'pass',
'user_email' => 'upgrade_editor@example.com',
'role' => 'editor',
)
);
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setUpBeforeClass() uses wp_insert_user() but doesn't assert the return value is a valid user ID (it can return WP_Error, e.g. if a username/email already exists), and the created users are never deleted. This can cause flaky tests and cross-test pollution. Consider using the WP test factory (if available) or at least validate the IDs and delete them in tearDownAfterClass() via wp_delete_user().

Copilot uses AI. Check for mistakes.
tracksEventName,
variant = 'primary',
weight = 'regular',
variant = 'solid',
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BackupNowButton now defaults variant to 'solid'. In this package there are existing call sites passing variant="primary" to BackupNowButton (e.g. src/js/components/Admin/index.js), so this default/value set looks inconsistent and may not match the variants supported by @wordpress/components Button. Consider keeping the previous variant naming (primary/secondary/tertiary) or updating all callers and verifying the underlying WP Button supports the new values.

Suggested change
variant = 'solid',
variant = 'primary',

Copilot uses AI. Check for mistakes.
tracksEventName: PropTypes.string,
variant: PropTypes.oneOf( [ 'primary', 'secondary', 'tertiary' ] ),
weight: PropTypes.oneOf( [ 'regular', 'bold' ] ),
variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled' ] ),
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variant PropTypes were changed to [ 'solid', 'outline', 'minimal', 'unstyled' ], but other code in this package already passes variant="primary" / variant="tertiary" to buttons. This will produce PropTypes warnings and suggests the accepted values don't match actual usage. Either expand the allowed values to include the variants you use in this repo, or update the callers and confirm the WP Button variant API supports the new set.

Suggested change
variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled' ] ),
variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled', 'primary', 'tertiary' ] ),

Copilot uses AI. Check for mistakes.
@jp-launch-control
Copy link

jp-launch-control bot commented Mar 3, 2026

Code Coverage Summary

Cannot generate coverage summary while tests are failing. 🤐

Please fix the tests, or re-run the Code coverage job if it was something being flaky.

Full summary · PHP report · JS report

Enhances the "Upgrade to Pro" submenu item by adding a link icon and adjusting CSS styles for better visibility. The styles are now applied globally across all admin screens for free-plan sites, ensuring consistent presentation. Additionally, the related tests have been updated to reflect these changes, removing unnecessary screen checks and clarifying the conditions for CSS output.
Use the jetpack-wpadmin-sidebar-free-plan-upsell-menu-item redirect slug
so the destination URL can be updated via the Jetpack redirect SaaS tool
without requiring a code change.

Made-with: Cursor
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Test that external links (those that open in new windows) appear after Settings.
if ( in_array( 'Activity Log', $submenu_names, true ) ) {
$activity_log_submenu_position = array_search( 'Activity Log', $submenu_names, true );
$this->assertLessThan( $activity_log_submenu_position, $settings_submenu_position, 'Settings should be above Activity Log in the submenu order (external links should be last).' );
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion for external-link ordering is reversed: to enforce “Activity Log appears after Settings”, the test should assert that $settings_submenu_position is less than $activity_log_submenu_position (not the other way around). As written, it enforces Activity Log before Settings, contradicting the comment and message.

Suggested change
$this->assertLessThan( $activity_log_submenu_position, $settings_submenu_position, 'Settings should be above Activity Log in the submenu order (external links should be last).' );
$this->assertLessThan( $settings_submenu_position, $activity_log_submenu_position, 'Settings should be above Activity Log in the submenu order (external links should be last).' );

Copilot uses AI. Check for mistakes.
parent::setUp();
global $submenu;
$submenu = array();
delete_option( 'jetpack_active_plan' );
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test isolation: this setUp resets $submenu, but Admin_Menu keeps static state (e.g., the private static $menu_items and $initialized flag) across tests. Because other tests in this class call Admin_Menu::add_menu(), later tests can become order-dependent/flaky. Consider resetting Admin_Menu’s static properties in setUp (e.g., via Reflection) or adding a dedicated reset method on Admin_Menu for tests.

Suggested change
delete_option( 'jetpack_active_plan' );
delete_option( 'jetpack_active_plan' );
// Ensure Admin_Menu static state does not leak between tests.
// This keeps tests independent even when Admin_Menu::add_menu() has been called.
$reflection = new \ReflectionClass( Admin_Menu::class );
if ( $reflection->hasProperty( 'menu_items' ) ) {
$menu_items = $reflection->getProperty( 'menu_items' );
$menu_items->setAccessible( true );
$menu_items->setValue( null, array() );
}
if ( $reflection->hasProperty( 'initialized' ) ) {
$initialized = $reflection->getProperty( 'initialized' );
$initialized->setAccessible( true );
$initialized->setValue( null, false );
}

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +57
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();

self::$admin_user_id = wp_insert_user(
array(
'user_login' => 'upgrade_test_admin',
'user_pass' => 'pass',
'user_email' => 'upgrade_admin@example.com',
'role' => 'administrator',
)
);

self::$editor_user_id = wp_insert_user(
array(
'user_login' => 'upgrade_test_editor',
'user_pass' => 'pass',
'user_email' => 'upgrade_editor@example.com',
'role' => 'editor',
)
);
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_insert_user() can return WP_Error (e.g., if the username already exists), but these IDs are assumed to be ints later in wp_set_current_user(). Consider using the WP test factory (or assert !is_wp_error and cast to int) and cleaning up created users in tearDownAfterClass to avoid cross-test pollution.

Copilot uses AI. Check for mistakes.
Comment on lines 106 to 112
BackupNowButton.propTypes = {
children: PropTypes.node,
tooltipText: PropTypes.string,
tracksEventName: PropTypes.string,
variant: PropTypes.oneOf( [ 'primary', 'secondary', 'tertiary' ] ),
weight: PropTypes.oneOf( [ 'regular', 'bold' ] ),
variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled' ] ),
onClick: PropTypes.func,
};
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PropTypes for variant no longer include values that are actually passed by consumers (e.g., "primary"), and the listed values ("solid", "outline", etc.) don’t match the Button variant values used elsewhere with @wordpress/components in this repo. Update the allowed variants to match @wordpress/components (and current usage) so runtime warnings don’t occur.

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +255
private static function is_free_plan() {
if ( class_exists( '\Automattic\Jetpack\Current_Plan' ) ) {
$plan = \Automattic\Jetpack\Current_Plan::get();
return 'free' === ( $plan['class'] ?? 'free' );
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: is_free_plan() calls Automattic\Jetpack\Current_Plan::get(), which does more than reading the option (it computes plan details and enumerates modules). Because add_upgrade_menu_item_styles is hooked to admin_head, this runs on every wp-admin request for free-plan sites. Consider a cheaper check (e.g., read jetpack_active_plan["class"] directly) and/or caching the result in Admin_Menu for the request.

Copilot uses AI. Check for mistakes.
Comment on lines 22 to 26
font-size: 13px;
color: #757575;
margin: 0;
padding-block-end: 8px;
padding: 0;
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR scope mismatch: this change adjusts generic header spacing (padding) in Boost, which isn’t mentioned in the PR title/description focused on the Jetpack “Upgrade to Pro” menu item. Consider splitting unrelated UI tweaks into a separate PR or updating the PR description/title to reflect the additional changes.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants