admin-ui: Add Upgrade to Pro menu item for free users#47418
admin-ui: Add Upgrade to Pro menu item for free users#47418DevinWalker wants to merge 16 commits intotrunkfrom
Conversation
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
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
| 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. |
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
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:
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. |
There was a problem hiding this comment.
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.
| font-size: 13px; | ||
| color: #757575; | ||
| margin: 0; | ||
| padding-block-end: 8px; | ||
| padding: 0; |
There was a problem hiding this comment.
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.
| delete_option( 'jetpack_active_plan' ); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
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.
| 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 ] ); | |
| } | |
| } | |
| /** |
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| * 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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', | ||
| ) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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().
| tracksEventName, | ||
| variant = 'primary', | ||
| weight = 'regular', | ||
| variant = 'solid', |
There was a problem hiding this comment.
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.
| variant = 'solid', | |
| variant = 'primary', |
| tracksEventName: PropTypes.string, | ||
| variant: PropTypes.oneOf( [ 'primary', 'secondary', 'tertiary' ] ), | ||
| weight: PropTypes.oneOf( [ 'regular', 'bold' ] ), | ||
| variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled' ] ), |
There was a problem hiding this comment.
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.
| variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled' ] ), | |
| variant: PropTypes.oneOf( [ 'solid', 'outline', 'minimal', 'unstyled', 'primary', 'tertiary' ] ), |
Code Coverage SummaryCannot generate coverage summary while tests are failing. 🤐 Please fix the tests, or re-run the Code coverage job if it was something being flaky. |
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
There was a problem hiding this comment.
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).' ); |
There was a problem hiding this comment.
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.
| $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).' ); |
| parent::setUp(); | ||
| global $submenu; | ||
| $submenu = array(); | ||
| delete_option( 'jetpack_active_plan' ); |
There was a problem hiding this comment.
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.
| 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 ); | |
| } |
| 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', | ||
| ) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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, | ||
| }; |
There was a problem hiding this comment.
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.
| private static function is_free_plan() { | ||
| if ( class_exists( '\Automattic\Jetpack\Current_Plan' ) ) { | ||
| $plan = \Automattic\Jetpack\Current_Plan::get(); | ||
| return 'free' === ( $plan['class'] ?? 'free' ); | ||
| } |
There was a problem hiding this comment.
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.
| font-size: 13px; | ||
| color: #757575; | ||
| margin: 0; | ||
| padding-block-end: 8px; | ||
| padding: 0; | ||
| } |
There was a problem hiding this comment.
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.
Fixes #
Proposed changes:
Adds a styled "Upgrade to Pro" submenu item to the Jetpack wp-admin menu for sites on the free plan.
#069e08) at the bottom of the Jetpack submenu (position 999)jetpack_active_planoption class is notfree)manage_optionscapabilitypackages/admin-uiAdmin_Menuclass — propagates automatically to every plugin that registers menus through it (Jetpack, Backup, Boost, Protect, Social, Search, VideoPress)Other information:
Jetpack product discussion
Does this pull request change what data or activity we track or use?
No.
Testing instructions:
wp option update jetpack_active_plan '{"class":"security"}' --format=jsonwp option delete jetpack_active_planUnit tests:
jp test php packages/admin-uiChangelog
Made with Cursor