Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1cbc66a
Admin Menu: Reorder menu items so external links appear last
DevinWalker Mar 2, 2026
cd60b97
Remove unnecessary paragraph bottom padding
DevinWalker Mar 2, 2026
9c736c8
Add devin's wp.org handle to readme
DevinWalker Mar 3, 2026
fad3e8f
Remove paragraph bottom padding causing vertical alignment issues
DevinWalker Mar 3, 2026
36d5cea
add devin (dlocc) to contributors list
DevinWalker Mar 3, 2026
0a1cc55
Backup: Replace Jetpack Button with WordPress Button component
DevinWalker Mar 3, 2026
7259e4d
Admin Menu: Simplify menu item titles
DevinWalker Mar 3, 2026
d611f0e
Add missing changelog entries for admin header improvements
DevinWalker Mar 3, 2026
a21d087
Update projects/packages/backup/src/class-jetpack-backup.php
DevinWalker Mar 3, 2026
649069d
Update projects/packages/my-jetpack/src/class-activitylog.php
DevinWalker Mar 3, 2026
10b8090
Update BackupNowButton propTypes to include new variant options
DevinWalker Mar 3, 2026
84fc4af
Update projects/plugins/jetpack/readme.txt
DevinWalker Mar 3, 2026
be32c94
Update projects/js-packages/components/components/admin-page/style.mo…
DevinWalker Mar 3, 2026
ebdfb8e
admin-ui: Add Upgrade to Pro menu item for free users
DevinWalker Mar 3, 2026
3f16dcc
admin-ui: Update "Upgrade to Pro" menu item styles and tests
DevinWalker Mar 3, 2026
657b5d6
admin-ui: Update upgrade menu redirect slug to use SaaS-managed URL
DevinWalker Mar 3, 2026
23f24f7
Merge branch 'trunk' of github.com:Automattic/jetpack into feature/up…
DevinWalker Mar 10, 2026
eed8e20
Address code review feedback
DevinWalker Mar 10, 2026
f342bc0
Merge branch 'trunk' of github.com:Automattic/jetpack into feature/up…
DevinWalker Mar 10, 2026
d4b6491
Merge branch 'trunk' of github.com:Automattic/jetpack into feature/up…
DevinWalker Mar 13, 2026
683eba1
Admin UI: Update menu item text to "Upgrade Jetpack"
DevinWalker Mar 13, 2026
617ffd3
Admin UI: Clean up changelog entries for this PR
DevinWalker Mar 13, 2026
4812910
Address code review feedback for upgrade menu feature
DevinWalker Mar 13, 2026
f5c0603
Merge branch 'trunk' of github.com:Automattic/jetpack into feature/up…
DevinWalker Mar 13, 2026
1653308
Fix upgrade menu showing when site has active license
DevinWalker Mar 13, 2026
34cf1f0
Fix upgrade menu detection by checking is_free field
DevinWalker Mar 13, 2026
fa2df07
Add comprehensive tests for upgrade menu plan detection
DevinWalker Mar 13, 2026
de846ec
Update composer.lock to include new Jetpack dependencies and update e…
DevinWalker Mar 13, 2026
5b70ad8
Admin UI: Update docblock to reference "Upgrade Jetpack"
DevinWalker Mar 13, 2026
dbf2f01
Add changelog entries.
Mar 13, 2026
8e39460
Merge branch 'feature/upsell-to-pro-wp-admin-menu' of github.com:Auto…
DevinWalker Mar 13, 2026
7395c74
Merge branch 'trunk' into feature/upsell-to-pro-wp-admin-menu
simison Mar 31, 2026
ec433d9
Update composer.lock
simison Mar 31, 2026
0b07819
Satisfy Phan
simison Mar 31, 2026
bda9b52
Ran ./tools/composer-update-monorepo.sh
simison Mar 31, 2026
25b78be
Remove circular dependency
simison Apr 1, 2026
0cecf4d
Update composer locks after circular dependency fix
simison Apr 1, 2026
d40e4ba
Update also A4A client
simison Apr 1, 2026
7aab45f
Changelogs
simison Apr 1, 2026
345ad70
Update Admin_Menu_Test.php
simison Apr 1, 2026
676964e
Add legacy active_plan['class'] check
simison Apr 1, 2026
8f51d33
Use true null but still suppress Phan
simison Apr 1, 2026
c27bfbc
Update tests
simison Apr 1, 2026
c611366
See if separate process for each test helps
simison Apr 1, 2026
ce1b5dc
Run tests regularly, remove the static caching of $is_free
simison Apr 1, 2026
fa1702f
Remove deprecated setAccessible
simison Apr 1, 2026
f49f943
Run deprecated method on older PHP versions
simison Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Part of admin header normalization work

Remove padding from admin page header subtitle for consistent spacing.
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@
:global(.jetpack-admin-page #dolly) {
background-color: #fff;
}

:global(.jetpack-admin-page .admin-ui-page__header-subtitle) {
padding-block-end: 0;
padding-bottom: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add Upgrade to Pro menu item for free users in the Jetpack admin menu.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Simplify Akismet admin menu title from 'Akismet Anti-spam' to 'Anti-spam'.
4 changes: 3 additions & 1 deletion projects/packages/admin-ui/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"type": "jetpack-library",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.2"
"php": ">=7.2",
"automattic/jetpack-plans": "@dev",
"automattic/jetpack-redirect": "@dev"
},
"require-dev": {
"yoast/phpunit-polyfills": "^4.0.0",
Expand Down
115 changes: 114 additions & 1 deletion projects/packages/admin-ui/src/class-admin-menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@

const PACKAGE_VERSION = '0.5.11';

/**
* Redirect source slug used as the upgrade URL identifier and CSS class.
*
* @var string
*/
const UPGRADE_MENU_SLUG = 'jetpack-upgrade-menu';

/**
* Fallback upgrade URL when the Redirect class is unavailable.
*
* @var string
*/
const UPGRADE_MENU_FALLBACK_URL = 'https://jetpack.com/upgrade/';

/**
* Whether this class has been initialized
*
Expand All @@ -40,6 +54,7 @@
self::handle_akismet_menu();
add_action( 'admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 ); // Jetpack uses 998.
add_action( 'network_admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 ); // Jetpack uses 998.
add_action( 'admin_head', array( __CLASS__, 'add_upgrade_menu_item_styles' ) );
}
}

Expand All @@ -58,7 +73,7 @@
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );

// Add an Anti-spam menu item for Jetpack.
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ), 6 );
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ), 6 );
},
4
);
Expand Down Expand Up @@ -140,6 +155,8 @@
if ( ! $can_see_toplevel_menu ) {
remove_menu_page( 'jetpack' );
}

self::maybe_add_upgrade_menu_item();
}

/**
Expand Down Expand Up @@ -225,4 +242,100 @@
$url = $fallback ? $fallback : admin_url();
return $url;
}

/**
* Checks whether the current site is on a free Jetpack plan with no active paid license.
*
* @return bool True if the site has no paid plan.
*/
private static function is_free_plan() {
if ( class_exists( '\Automattic\Jetpack\Current_Plan' ) ) {
$plan = \Automattic\Jetpack\Current_Plan::get();
return 'free' === ( $plan['class'] ?? 'free' );
}

$plan = get_option( 'jetpack_active_plan', array() );
return 'free' === ( $plan['class'] ?? 'free' );
}

/**
* 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
Copy Markdown
Member

@simison simison Mar 13, 2026

Choose a reason for hiding this comment

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

Not a biggie but these could be put to one show_menu_upgrade_nudge or similar function and used in both places where we do this check.

What is a biggie and I think we need here is a check that the user isn't on WP.com platform, because we shouldn't show Jetpack upsells there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Fixed! Created should_show_upgrade_menu() helper method that consolidates all checks (capability, platform, and plan) - used in both places.


$upgrade_url = class_exists( '\Automattic\Jetpack\Redirect' )
? \Automattic\Jetpack\Redirect::get_url( self::UPGRADE_MENU_SLUG )
: self::UPGRADE_MENU_FALLBACK_URL;

$menu_title = '<span class="dashicons dashicons-star-filled jetpack-upgrade-menu__icon" aria-hidden="true"></span>'
. esc_html__( 'Upgrade to Pro', 'jetpack-admin-ui' );

add_submenu_page(
'jetpack',
__( 'Upgrade to Pro', 'jetpack-admin-ui' ),
$menu_title,
'manage_options',
esc_url( $upgrade_url ),
null,

Check failure on line 291 in projects/packages/admin-ui/src/class-admin-menu.php

View workflow job for this annotation

GitHub Actions / Static analysis

TypeError PhanTypeMismatchArgumentProbablyReal Argument 6 ($callback) is null of type null but \add_submenu_page() takes callable|string (no real type) defined at /home/runner/work/jetpack/jetpack/vendor/php-stubs/wordpress-stubs/wordpress-stubs.php:91854 (the inferred real argument type has nothing in common with the parameter's phpdoc type) FAQ on Phan issues: pdWQjU-Jb-p2
999
);

// Add a CSS class to the <li> element so styles can target it precisely.
global $submenu;
if ( ! empty( $submenu['jetpack'] ) ) {
foreach ( $submenu['jetpack'] as $index => $item ) {
if ( isset( $item[2] ) && false !== strpos( $item[2], self::UPGRADE_MENU_SLUG ) ) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$submenu['jetpack'][ $index ][4] = ( ! empty( $item[4] ) ? $item[4] . ' ' : '' ) . self::UPGRADE_MENU_SLUG;
break;
}
}
}
}

/**
* Outputs inline CSS to style the "Upgrade to Pro" menu item in Jetpack green.
*
* Only outputs on Jetpack admin pages and only for free-plan sites.
*
* @return void
*/
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;
}
?>
<style>
#adminmenu .jetpack-upgrade-menu__icon {
color: #069e08;
font-size: 16px;
line-height: 1;
vertical-align: middle;
margin-inline-end: 4px;
}
#adminmenu li.<?php echo esc_attr( self::UPGRADE_MENU_SLUG ); ?> > a,
#adminmenu li.<?php echo esc_attr( self::UPGRADE_MENU_SLUG ); ?> > a:hover {
color: #069e08;
font-weight: 600;
}
</style>
<?php
}
}
188 changes: 187 additions & 1 deletion projects/packages/admin-ui/tests/php/Admin_Menu_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,59 @@
*/
class Admin_Menu_Test extends TestCase {

/**
* Administrator user ID created once for the test class.
*
* @var int
*/
private static $admin_user_id;

/**
* Editor user ID created once for the test class.
*
* @var int
*/
private static $editor_user_id;

/**
* Create shared users once for the test class.
*
* @return void
*/
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',
)
);
}

/**
* Reset shared state before each test.
*
* @return void
*/
public function setUp(): void {
parent::setUp();
global $submenu;
$submenu = array();
delete_option( 'jetpack_active_plan' );
}

/**
* Tests whether the page_suffix we return in our method will match the page_suffix returned by the native WP methods
*
Expand Down Expand Up @@ -77,7 +130,7 @@ public static function page_suffix_matches_data() {
}

/**
* Undocumented function
* Tests that the first registered menu item is returned correctly.
*
* @return void
*/
Expand All @@ -95,4 +148,137 @@ public function test_first_menu() {

$this->assertSame( 'menu_2', $first );
}

/**
* Upgrade item appears in the submenu for an administrator on a free plan.
*
* @return void
*/
public function test_upgrade_menu_item_shown_for_free_plan_admin() {
wp_set_current_user( self::$admin_user_id );

Admin_Menu::init();
do_action( 'admin_menu' );

$this->assertUpgradeMenuItemPresent();
}

/**
* Upgrade item is absent when the site has a paid plan.
*
* @return void
*/
public function test_upgrade_menu_item_hidden_for_paid_plan() {
wp_set_current_user( self::$admin_user_id );
update_option( 'jetpack_active_plan', array( 'class' => 'security' ) );

Admin_Menu::init();
do_action( 'admin_menu' );

$this->assertUpgradeMenuItemAbsent();
}

/**
* Upgrade item is absent for users without manage_options capability.
*
* @return void
*/
public function test_upgrade_menu_item_hidden_for_non_admin() {
wp_set_current_user( self::$editor_user_id );

Admin_Menu::init();
do_action( 'admin_menu' );

$this->assertUpgradeMenuItemAbsent();
}

/**
* CSS styles are output for a free-plan admin on a Jetpack screen.
*
* @return void
*/
public function test_upgrade_menu_item_styles_output_for_free_plan() {
wp_set_current_user( self::$admin_user_id );
set_current_screen( 'jetpack' );

ob_start();
Admin_Menu::add_upgrade_menu_item_styles();
$output = ob_get_clean();

set_current_screen( 'front' );

$this->assertStringContainsString( 'jetpack-upgrade-menu__icon', $output );
$this->assertStringContainsString( '#069e08', $output );
}

/**
* No CSS output when the site has a paid plan, even on a Jetpack screen.
*
* @return void
*/
public function test_upgrade_menu_item_styles_no_output_for_paid_plan() {
wp_set_current_user( self::$admin_user_id );
update_option( 'jetpack_active_plan', array( 'class' => 'premium' ) );
set_current_screen( 'jetpack' );

ob_start();
Admin_Menu::add_upgrade_menu_item_styles();
$output = ob_get_clean();

set_current_screen( 'front' );

$this->assertSame( '', $output );
}

/**
* No CSS output when viewing a non-Jetpack admin screen, even on a free plan.
*
* @return void
*/
public function test_upgrade_menu_item_styles_no_output_outside_jetpack_screens() {
wp_set_current_user( self::$admin_user_id );
set_current_screen( 'dashboard' );

ob_start();
Admin_Menu::add_upgrade_menu_item_styles();
$output = ob_get_clean();

set_current_screen( 'front' );

$this->assertSame( '', $output );
}

/**
* Asserts the upgrade submenu item is present under the jetpack top-level menu.
*
* @return void
*/
private function assertUpgradeMenuItemPresent() {
global $submenu;
$slugs = array_column( $submenu['jetpack'] ?? array(), 2 );
$found = array_filter(
$slugs,
function ( $slug ) {
return false !== strpos( $slug, Admin_Menu::UPGRADE_MENU_SLUG );
}
);
$this->assertNotEmpty( $found, 'Expected the upgrade menu item to be registered.' );
}

/**
* Asserts the upgrade submenu item is absent from the jetpack top-level menu.
*
* @return void
*/
private function assertUpgradeMenuItemAbsent() {
global $submenu;
$slugs = array_column( $submenu['jetpack'] ?? array(), 2 );
$found = array_filter(
$slugs,
function ( $slug ) {
return false !== strpos( $slug, Admin_Menu::UPGRADE_MENU_SLUG );
}
);
$this->assertEmpty( $found, 'Expected the upgrade menu item to be absent.' );
}
}
4 changes: 4 additions & 0 deletions projects/packages/backup/changelog/simplify-menu-title
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Simplify admin menu title from 'VaultPress Backup' to 'Backups'.
Loading
Loading