PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-my-jetpack/src/class-initializer.php +447 -56 16.2-beta → 16.3-beta View file →
@@ -11,8 +11,9 @@
11 11 use Automattic\Jetpack\Assets;
12 12 use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
13 13 use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
14 14 use Automattic\Jetpack\Connection\Client;
15 +use Automattic\Jetpack\Connection\Error_Handler as Connection_Error_Handler;
15 16 use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
16 17 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
17 18 use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
18 19 use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
@@ -17,13 +18,15 @@
17 18 use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
18 19 use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
19 20 use Automattic\Jetpack\Constants as Jetpack_Constants;
20 21 use Automattic\Jetpack\ExPlat;
22 +use Automattic\Jetpack\Feature_Flags\Feature_Flags;
21 23 use Automattic\Jetpack\JITMS\JITM;
22 24 use Automattic\Jetpack\Licensing;
23 25 use Automattic\Jetpack\Menu_Badges\Menu_Badges;
24 26 use Automattic\Jetpack\Menu_Badges\Notification_Counts;
25 27 use Automattic\Jetpack\Modules;
28 +use Automattic\Jetpack\Partner_Coupon;
26 29 use Automattic\Jetpack\Plugins_Installer;
27 30 use Automattic\Jetpack\Status;
28 31 use Automattic\Jetpack\Status\Host as Status_Host;
29 32 use Automattic\Jetpack\Sync\Functions as Sync_Functions;
@@ -29,8 +32,9 @@
29 32 use Automattic\Jetpack\Sync\Functions as Sync_Functions;
30 33 use Automattic\Jetpack\Terms_Of_Service;
31 34 use Automattic\Jetpack\Tracking;
32 35 use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
36 +use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
33 37 use Jetpack;
34 38 use WP_Error;
35 39
36 40 /**
@@ -42,11 +46,33 @@
42 46 * My Jetpack package version
43 47 *
44 48 * @var string
45 49 */
46 - const PACKAGE_VERSION = '6.2.0';
50 + const PACKAGE_VERSION = '6.8.0';
47 51
48 52 /**
53 + * Feature flag that swaps the My Jetpack Products tab for a Features tab.
54 + */
55 + const FEATURES_TAB_FEATURE_FLAG = 'my-jetpack-features-tab';
56 +
57 + /**
58 + * Handle for the classic script that carries the React initial state.
59 + *
60 + * Plugins that render My Jetpack components on their own pages (Boost) depend on this name.
61 + */
62 + const DATA_SCRIPT_HANDLE = 'my_jetpack_main_app';
63 +
64 + /**
65 + * Handle for the webpack bundle that renders the onboarding takeover.
66 + */
67 + const ONBOARDING_SCRIPT_HANDLE = 'my_jetpack_onboarding';
68 +
69 + /**
70 + * The wp-build page ID, which the generated enqueue check expects as the screen ID.
71 + */
72 + const WP_BUILD_PAGE_ID = 'my-jetpack-dashboard';
73 +
74 + /**
49 75 * HTML container ID for the IDC screen on My Jetpack page.
50 76 */
51 77 private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
52 78
@@ -58,8 +84,9 @@
58 84 'jetpack-protect',
59 85 'jetpack-social',
60 86 'jetpack-videopress',
61 87 'jetpack-search',
88 + 'jetpack-stats',
62 89 );
63 90
64 91 private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
65 92
@@ -70,17 +97,34 @@
70 97 */
71 98 public static $site_info;
72 99
73 100 /**
101 + * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
102 + *
103 + * @var string|null
104 + */
105 + private static $wp_build_original_screen_id = null;
106 +
107 + /**
74 108 * Initialize My Jetpack
75 109 *
76 110 * @return void
77 111 */
78 112 public static function init() {
113 + // Before the gate, so the flag stays listed while diagnosing why My Jetpack is off.
114 + self::register_feature_flags();
115 +
116 + // Before the gate: the Jetpack plugin renders this package's connection screen and footer
117 + // links even where My Jetpack is off, and `myJetpackInitialState` only exists on its own page.
118 + add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_admin_script_data' ) );
119 +
79 120 if ( ! self::should_initialize() || did_action( 'my_jetpack_init' ) ) {
80 121 return;
81 122 }
82 123
124 + // Answer "is this product on?" for admin menu registration.
125 + Menu_Visibility::init();
126 +
83 127 // Extend jetpack plugins action links.
84 128 Products::extend_plugins_action_links();
85 129
86 130 // Set up the REST authentication hooks.
@@ -95,8 +139,13 @@
95 139
96 140 // Add custom WP REST API endoints.
97 141 add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
98 142
143 + // Both of wp-build's deadlines fall later in this request: its enqueue check must be
144 + // hooked before `admin_enqueue_scripts`, and its render function defined before
145 + // the page callback.
146 + add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
147 +
99 148 add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
100 149
101 150 add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
102 151 // Registered on admin_menu (not admin_init) and well before priority 100000, so the
@@ -158,17 +207,21 @@
158 207 *
159 208 * @return void
160 209 */
161 210 public static function add_my_jetpack_menu_item() {
211 + $position = defined( Admin_Menu::class . '::POSITION_FIRST' ) ? Admin_Menu::POSITION_FIRST : -10;
212 + $menu_slug = 'my-jetpack';
162 213 $page_suffix = Admin_Menu::add_menu(
163 214 __( 'My Jetpack', 'jetpack-my-jetpack' ),
164 215 __( 'My Jetpack', 'jetpack-my-jetpack' ),
165 216 'edit_posts',
166 - 'my-jetpack',
217 + $menu_slug,
167 218 array( __CLASS__, 'admin_page' ),
168 - -1
219 + $position
169 220 );
170 221 add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
222 + // Users who can edit posts but have no Jetpack menu get an admin_page_ hook instead.
223 + add_action( 'load-admin_page_' . $menu_slug, array( __CLASS__, 'admin_init' ) );
171 224 }
172 225
173 226 /**
174 227 * Callback for the load my jetpack page hook.
@@ -180,20 +233,16 @@
180 233
181 234 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control
182 235 $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : '';
183 236
184 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking for partner coupon redemption flow
185 - $show_coupon_redemption = isset( $_GET['showCouponRedemption'] );
237 + // Handle onboarding redirects based on connection status. Onboarding's only action is the
238 + // connect request, which answers a user without `jetpack_connect` with a 403.
239 + $redirect_args = self::get_onboarding_redirect_args(
240 + $step,
241 + $connection->is_connected(),
242 + self::is_onboarding_available() && current_user_can( 'jetpack_connect' )
243 + );
186 244
187 - // Redirect to Jetpack dashboard for partner coupon redemption
188 - if ( $show_coupon_redemption ) {
189 - wp_safe_redirect( admin_url( 'admin.php?page=jetpack&showCouponRedemption=1#/dashboard' ) );
190 - exit( 0 );
191 - }
192 -
193 - // Handle onboarding redirects based on connection status
194 - $redirect_args = self::get_onboarding_redirect_args( $step, $connection->is_connected(), self::is_onboarding_available() );
195 -
196 245 if ( null !== $redirect_args ) {
197 246 $admin_page = add_query_arg( $redirect_args, admin_url( 'admin.php' ) );
198 247 $location = wp_sanitize_redirect( $admin_page );
199 248
@@ -218,9 +267,9 @@
218 267 * Whether the My Jetpack onboarding flow is available on this site.
219 268 *
220 269 * WordPress.com Simple sites are connected by definition and don't manage their
221 270 * connection through My Jetpack, so the onboarding flow (which asks the user to
222 - * connect) never applies there.
271 + * connect) never applies there. A pending partner coupon brings its own connect screen.
223 272 *
224 273 * @internal Not part of the package's public API.
225 274 *
226 275 * @return bool
@@ -225,12 +274,46 @@
225 274 *
226 275 * @return bool
227 276 */
228 277 public static function is_onboarding_available() {
229 - return ! ( new Status_Host() )->is_wpcom_simple();
278 + return ! ( new Status_Host() )->is_wpcom_simple() && null === self::get_partner_coupon_screen();
230 279 }
231 280
232 281 /**
282 + * The partner coupon screen's data, when it should replace the dashboard.
283 + *
284 + * Coupons exist only with the Jetpack plugin, which supplies their products and images.
285 + *
286 + * @since 6.5.0
287 + *
288 + * @return array{coupon: array, assetBaseUrl: string}|null
289 + */
290 + public static function get_partner_coupon_screen() {
291 + if (
292 + ! self::should_initialize()
293 + || ! current_user_can( 'manage_options' )
294 + || ! Jetpack_Constants::is_defined( 'JETPACK__PLUGIN_FILE' )
295 + ) {
296 + return null;
297 + }
298 +
299 + $coupon = Partner_Coupon::get_coupon();
300 + if ( ! $coupon ) {
301 + return null;
302 + }
303 +
304 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks which screen renders.
305 + if ( ! isset( $_GET['showCouponRedemption'] ) && ( new Connection_Manager() )->has_connected_owner() ) {
306 + return null;
307 + }
308 +
309 + return array(
310 + 'coupon' => $coupon,
311 + 'assetBaseUrl' => plugins_url( '', Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) ),
312 + );
313 + }
314 +
315 + /**
233 316 * Decide whether the current My Jetpack request should redirect, and where to.
234 317 *
235 318 * @internal Not part of the package's public API.
236 319 *
@@ -280,43 +363,239 @@
280 363 return $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
281 364 }
282 365
283 366 /**
284 - * Register polyfills for the wp-notices / wp-private-apis / wp-rich-text / wp-theme
285 - * handles the My Jetpack app bundle depends on but WP < 7.0 does not ship (or ships
286 - * with an incomplete allowlist).
367 + * Register the package's feature flags.
287 368 *
288 - * `wp-rich-text` is needed because the bundle reaches `@wordpress/dataviews` (via
289 - * `@wordpress/ui`), whose dataform controls unlock rich-text's `privateApis` at module
290 - * scope. WP 6.9 exports none, so without the polyfill the bundle throws "Cannot unlock
291 - * an undefined object" and the page renders blank.
369 + * @since 6.3.0
292 370 *
293 371 * @return void
294 372 */
295 - public static function register_wp_build_polyfills() {
296 - if ( ! class_exists( WP_Build_Polyfills::class ) ) {
373 + public static function register_feature_flags() {
374 + Feature_Flags::register(
375 + self::FEATURES_TAB_FEATURE_FLAG,
376 + array(
377 + 'default' => true,
378 + 'description' => 'Replace the My Jetpack Products tab with a Features tab.',
379 + 'owner' => 'my-jetpack',
380 + )
381 + );
382 + }
383 +
384 + /**
385 + * Whether the dashboard shows a Features tab in place of the Products tab.
386 + *
387 + * @since 6.4.0
388 + *
389 + * @return bool
390 + */
391 + public static function is_features_tab_enabled() {
392 + return Feature_Flags::is_enabled( self::FEATURES_TAB_FEATURE_FLAG );
393 + }
394 +
395 + /**
396 + * Get the slug and label that replace the Products tab, for links to it.
397 + *
398 + * Null while the tab is unchanged, so links keep their own translated "Products" label.
399 + *
400 + * @since 6.4.0
401 + *
402 + * @return array{slug: string, label: string}|null
403 + */
404 + public static function get_products_section() {
405 + if ( ! self::is_features_tab_enabled() ) {
406 + return null;
407 + }
408 +
409 + return array(
410 + 'slug' => 'features',
411 + 'label' => _x( 'Features', 'Navigation item', 'jetpack-my-jetpack' ),
412 + );
413 + }
414 +
415 + /**
416 + * Whether the current request targets the My Jetpack admin page.
417 + *
418 + * @since 6.3.0
419 + *
420 + * @return bool
421 + */
422 + public static function is_my_jetpack_admin_request() {
423 + if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
424 + return false;
425 + }
426 +
427 + return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'my-jetpack'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
428 + }
429 +
430 + /**
431 + * Whether the current request is the full-viewport onboarding takeover.
432 + *
433 + * Onboarding hides all wp-admin chrome and never renders through wp-build.
434 + *
435 + * @since 6.3.0
436 + *
437 + * @return bool
438 + */
439 + public static function is_onboarding_request() {
440 + if ( ! isset( $_GET['step'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
441 + return false;
442 + }
443 +
444 + return sanitize_text_field( wp_unslash( $_GET['step'] ) ) === 'onboarding'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
445 + }
446 +
447 + /**
448 + * Whether this request is the onboarding takeover rather than the dashboard.
449 + *
450 + * Loading wp-build, enqueueing scripts and rendering the page must all agree,
451 + * so they share this one expression.
452 + *
453 + * @since 6.3.0
454 + *
455 + * @return bool
456 + */
457 + public static function is_onboarding_takeover() {
458 + return self::is_onboarding_request() && self::is_onboarding_available();
459 + }
460 +
461 + /**
462 + * Alias the screen ID to satisfy wp-build's generated enqueue check.
463 + *
464 + * @since 6.3.0
465 + *
466 + * @return void
467 + */
468 + public static function alias_screen_id_for_wp_build() {
469 + $screen = get_current_screen();
470 +
471 + if ( ! $screen ) {
297 472 return;
298 473 }
299 474
475 + self::$wp_build_original_screen_id = $screen->id;
476 + $screen->id = self::WP_BUILD_PAGE_ID;
477 + }
478 +
479 + /**
480 + * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID.
481 + *
482 + * @since 6.3.0
483 + *
484 + * @return void
485 + */
486 + public static function restore_screen_id_after_wp_build() {
487 + $screen = get_current_screen();
488 +
489 + if ( ! $screen || null === self::$wp_build_original_screen_id ) {
490 + return;
491 + }
492 +
493 + $screen->id = self::$wp_build_original_screen_id;
494 + self::$wp_build_original_screen_id = null;
495 + }
496 +
497 + /**
498 + * Whether this request should load wp-build at all.
499 + *
500 + * Also what keeps WP_Build_Polyfills from replacing core scripts on every other admin page.
501 + *
502 + * @since 6.3.0
503 + *
504 + * @return bool
505 + */
506 + public static function should_load_wp_build() {
507 + return self::is_my_jetpack_admin_request() && ! self::is_onboarding_takeover();
508 + }
509 +
510 + /**
511 + * Whether this request renders the dashboard through wp-build.
512 + *
513 + * The generated render function is missing where the package was never built.
514 + *
515 + * @since 6.3.0
516 + *
517 + * @return bool
518 + */
519 + public static function should_render_wp_build() {
520 + return ! self::is_onboarding_takeover()
521 + && function_exists( 'jetpack_my_jetpack_my_jetpack_dashboard_wp_admin_render_page' );
522 + }
523 +
524 + /**
525 + * Load wp-build for the My Jetpack dashboard.
526 + *
527 + * @since 6.3.0
528 + *
529 + * @return void
530 + */
531 + public static function maybe_load_wp_build() {
532 + if ( ! self::should_load_wp_build() ) {
533 + return;
534 + }
535 +
536 + $build_index = dirname( __DIR__ ) . '/build/build.php';
537 +
538 + if ( file_exists( $build_index ) ) {
539 + self::load_wp_build( $build_index );
540 + }
541 + }
542 +
543 + /**
544 + * Require the generated wp-build index and wire it into this request.
545 + *
546 + * @since 6.3.0
547 + *
548 + * @param string $build_index Path to the generated `build.php`.
549 + * @return void
550 + */
551 + public static function load_wp_build( $build_index ) {
552 + self::require_wp_build_with_screen_alias( $build_index );
553 +
554 + // wp-build hooks module registration to wp_default_scripts, which has
555 + // already fired by admin_menu — call it directly or the init module
556 + // never reaches the import map.
557 + if ( function_exists( 'jetpack_my_jetpack_register_script_modules' ) ) {
558 + jetpack_my_jetpack_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
559 + }
560 +
300 561 WP_Build_Polyfills::register(
301 562 'my-jetpack',
302 - array( 'wp-notices', 'wp-private-apis', 'wp-rich-text', 'wp-theme' )
563 + array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
303 564 );
304 565 }
305 566
306 567 /**
568 + * Require the generated build file with the screen ID aliased across its enqueue check.
569 + *
570 + * @see WP_Build_Screen_Id::load_with_alias()
571 + * @param string $build_index Path to the generated `build.php`.
572 + * @return void
573 + */
574 + private static function require_wp_build_with_screen_alias( $build_index ) {
575 + // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
576 + if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
577 + WP_Build_Screen_Id::load_with_alias(
578 + array( __CLASS__, 'alias_screen_id_for_wp_build' ),
579 + array( __CLASS__, 'restore_screen_id_after_wp_build' ),
580 + function () use ( $build_index ) {
581 + require_once $build_index;
582 + }
583 + );
584 + return;
585 + }
586 +
587 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
588 + require_once $build_index;
589 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
590 + }
591 +
592 + /**
307 593 * Enqueue admin page assets.
308 594 *
309 595 * @return void
310 596 */
311 597 public static function enqueue_scripts() {
312 - // Register the wp-build-polyfills shim before the extension hook below or
313 - // the app script can enqueue against wp-theme / wp-private-apis / wp-notices.
314 - // WP_Build_Polyfills registers synchronously on its first caller, so calling
315 - // it after a hook consumer would leave our handles recorded but unregistered
316 - // for this request.
317 - self::register_wp_build_polyfills();
318 -
319 598 /**
320 599 * Fires after the My Jetpack page is initialized.
321 600 * Allows for enqueuing additional scripts only on the My Jetpack page.
322 601 *
@@ -323,18 +602,31 @@
323 602 * @since 4.35.7
324 603 */
325 604 do_action( 'myjetpack_enqueue_scripts' );
326 605 add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
327 - Assets::register_script(
328 - 'my_jetpack_main_app',
329 - '../build/index.js',
330 - __FILE__,
331 - array(
332 - 'enqueue' => true,
333 - 'in_footer' => true,
334 - 'textdomain' => 'jetpack-my-jetpack',
335 - )
336 - );
606 +
607 + // Script-less: it only prints the state below, before the dashboard's boot runs on DOMContentLoaded.
608 + $data_handle = self::DATA_SCRIPT_HANDLE;
609 + wp_register_script( $data_handle, false, array(), self::PACKAGE_VERSION, true );
610 + wp_enqueue_script( $data_handle );
611 +
612 + if ( self::is_my_jetpack_admin_request() && self::is_onboarding_takeover() ) {
613 + Assets::register_script(
614 + self::ONBOARDING_SCRIPT_HANDLE,
615 + '../build/onboarding.js',
616 + __FILE__,
617 + array(
618 + 'dependencies' => array( $data_handle ),
619 + 'enqueue' => true,
620 + 'in_footer' => true,
621 + 'textdomain' => 'jetpack-my-jetpack',
622 + )
623 + );
624 + } elseif ( self::should_render_wp_build() && wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
625 + // Registered on every admin page but only enqueued when depended on; the esbuild bundles don't pull it in.
626 + wp_enqueue_script( 'wp-jp-i18n-loader' );
627 + }
628 +
337 629 $modules = new Modules();
338 630 $connection = new Connection_Manager();
339 631 $speed_score_history = new Speed_Score_History( get_site_url() );
340 632 $latest_score = $speed_score_history->latest();
@@ -350,15 +642,19 @@
350 642 $is_dev_version = Jetpack::is_development_version();
351 643 $sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : '';
352 644 }
353 645
646 + $features_tab_enabled = self::is_features_tab_enabled();
647 +
354 648 wp_localize_script(
355 - 'my_jetpack_main_app',
649 + $data_handle,
356 650 'myJetpackInitialState',
357 651 array(
358 652 'products' => array(
359 653 'items' => Products::get_products(),
360 654 ),
655 + 'mainFeatures' => $features_tab_enabled ? Main_Features::get_state() : null,
656 + 'featuresBanner' => $features_tab_enabled ? array( 'isDismissed' => REST_Main_Features::is_banner_dismissed() ) : null,
361 657 'plugins' => Plugins_Installer::get_plugins(),
362 658 'themes' => Sync_Functions::get_themes(),
363 659 'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
364 660 'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ),
@@ -370,8 +666,9 @@
370 666 'myJetpackFlags' => self::get_my_jetpack_flags(),
371 667 'fileSystemWriteAccess' => self::has_file_system_write_access(),
372 668 'loadAddLicenseScreen' => self::is_licensing_ui_enabled(),
373 669 'adminUrl' => esc_url( admin_url() ),
670 + 'assetsUrl' => self::get_assets_url(),
374 671 'IDCContainerID' => static::get_idc_container_id(),
375 672 'userIsAdmin' => current_user_can( 'manage_options' ),
376 673 'lifecycleStats' => array(
377 674 'jetpackPlugins' => self::get_installed_jetpack_plugins(),
@@ -387,8 +684,9 @@
387 684 'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
388 685 ),
389 686 'isStatsModuleActive' => $modules->is_active( 'stats' ),
390 687 'canUserViewStats' => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ),
688 + 'hiddenFeatures' => Feature_Visibility::get_hidden(),
391 689 'sandboxedDomain' => $sandboxed_domain,
392 690 'isDevVersion' => $is_dev_version,
393 691 'isAtomic' => ( new Status_Host() )->is_woa_site(),
394 692 'isJetpackPluginActive' => class_exists( 'Jetpack' ),
@@ -393,13 +691,14 @@
393 691 'isAtomic' => ( new Status_Host() )->is_woa_site(),
394 692 'isJetpackPluginActive' => class_exists( 'Jetpack' ),
395 693 'latestBoostSpeedScores' => $latest_score,
396 694 'seoOptIn' => self::get_seo_opt_in_state(),
695 + 'partnerCoupon' => self::get_partner_coupon_screen(),
397 696 )
398 697 );
399 698
400 699 wp_localize_script(
401 - 'my_jetpack_main_app',
700 + $data_handle,
402 701 'myJetpackRest',
403 702 array(
404 703 'apiRoot' => esc_url_raw( rest_url() ),
405 704 'apiNonce' => wp_create_nonce( 'wp_rest' ),
@@ -406,9 +705,9 @@
406 705 )
407 706 );
408 707
409 708 // Connection Initial State.
410 - Connection_Initial_State::render_script( 'my_jetpack_main_app' );
709 + Connection_Initial_State::render_script( $data_handle );
411 710
412 711 // Required for Analytics.
413 712 if ( self::can_use_analytics() ) {
414 713 Tracking::register_tracks_functions_scripts( true );
@@ -429,8 +728,10 @@
429 728 $data['myJetpack']['siteEditor'] = array(
430 729 'isBlockTheme' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(),
431 730 'isSharingBlockAvailable' => isset( $block_availability['sharing-buttons'] )
432 731 && $block_availability['sharing-buttons']['available'],
732 + 'isLikeBlockAvailable' => isset( $block_availability['like'] )
733 + && $block_availability['like']['available'],
433 734 'activeThemeStylesheet' => get_stylesheet(),
434 735 );
435 736
436 737 return $data;
@@ -436,8 +737,55 @@
436 737 return $data;
437 738 }
438 739
439 740 /**
741 + * Add My Jetpack availability, image base URL, and products tab to admin script data.
742 + *
743 + * Printed on every admin page by Script_Data, so the connection screen can resolve its
744 + * illustrations and Jetpack footers can link to the products tab off the My Jetpack page.
745 + *
746 + * @since 6.3.0
747 + *
748 + * @param array $data Script data.
749 + * @return array
750 + */
751 + public static function add_admin_script_data( $data ) {
752 + $data['myJetpack']['isAvailable'] = self::is_admin_page_available();
753 + $data['myJetpack']['assetsUrl'] = self::get_assets_url();
754 + $data['myJetpack']['productsSection'] = self::get_products_section();
755 +
756 + return $data;
757 + }
758 +
759 + /**
760 + * Whether My Jetpack's admin page is available to the current user.
761 + *
762 + * Meaningful only after admin_menu has registered the page.
763 + *
764 + * @since 6.5.0
765 + *
766 + * @return bool
767 + */
768 + public static function is_admin_page_available() {
769 + global $_registered_pages;
770 +
771 + return did_action( 'my_jetpack_init' ) > 0
772 + && isset( $_registered_pages[ get_plugin_page_hookname( 'my-jetpack', 'jetpack' ) ] )
773 + && current_user_can( 'edit_posts' );
774 + }
775 +
776 + /**
777 + * Get the base URL of the package's built images, with a trailing slash.
778 + *
779 + * @since 6.3.0
780 + *
781 + * @return string
782 + */
783 + public static function get_assets_url() {
784 + return trailingslashit( Assets::normalize_path( plugins_url( '../build/images/', __FILE__ ) ) );
785 + }
786 +
787 + /**
440 788 * Get installed Jetpack plugins
441 789 *
442 790 * @return array
443 791 */
@@ -526,9 +874,9 @@
526 874 */
527 875 public static function get_my_jetpack_flags() {
528 876 $flags = array(
529 877 'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
530 - 'showFullJetpackStatsCard' => class_exists( 'Jetpack' ),
878 + 'showFullJetpackStatsCard' => class_exists( 'Jetpack' ) || Products\Stats::is_standalone_plugin_active(),
531 879 // Only says which destination `manage_url` is: the legacy Stats page
532 880 // caches its report and wants a `force_refresh` hint the dashboard does not.
533 881 'premiumAnalyticsEnabled' => Products\Stats::is_premium_analytics_enabled(),
534 882 'showAiModuleToggle' => Products\Jetpack_Ai::is_feature_ui_enabled(),
@@ -581,17 +929,19 @@
581 929 *
582 930 * @return void
583 931 */
584 932 public static function admin_page() {
585 - $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
933 + // No connection check needed here: admin_init() has already redirected connected users
934 + // away from onboarding. Availability is re-checked inside the helper on purpose — this
935 + // render can run even when that redirect did not.
936 + if ( self::is_onboarding_takeover() ) {
937 + echo '<div id="my-jetpack-container"></div>';
938 + return;
939 + }
586 940
587 - // No connection check needed here: admin_init() has already redirected connected users away from onboarding.
588 - // Availability IS re-checked on purpose: this render can run even when that redirect did not,
589 - // and the check below is what keeps the onboarding route off WordPress.com Simple sites.
590 - $is_onboarding = $step === 'onboarding' && self::is_onboarding_available();
591 -
592 - // Add data attribute for onboarding, otherwise render normal container
593 - echo '<div id="my-jetpack-container" ' . ( $is_onboarding ? 'data-route="onboarding"' : '' ) . '></div>';
941 + if ( self::should_render_wp_build() ) {
942 + jetpack_my_jetpack_my_jetpack_dashboard_wp_admin_render_page(); // @phan-suppress-current-line PhanUndeclaredFunction -- should_render_wp_build() checks function_exists(); defined in the generated build/pages/, which Phan excludes.
943 + }
594 944 }
595 945
596 946 /**
597 947 * Register the REST API routes.
@@ -600,12 +950,15 @@
600 950 */
601 951 public static function register_rest_endpoints() {
602 952 new REST_Products();
603 953 new REST_Purchases();
604 - new REST_Zendesk_Chat();
605 954 ( new REST_Jetpack_AI_JWT() )->register_rest_route();
606 955 new REST_Recommendations_Evaluation();
607 956
957 + if ( self::is_features_tab_enabled() ) {
958 + ( new REST_Main_Features() )->register_rest_routes();
959 + }
960 +
608 961 Products::register_product_endpoints();
609 962 Historically_Active_Modules::register_rest_endpoints();
610 963 Jetpack_Manage::register_rest_endpoints();
611 964 Red_Bubble_Notifications::register_rest_endpoints();
@@ -889,8 +1242,23 @@
889 1242
890 1243 // Report each non-silent alert to the central menu-badges registry as an
891 1244 // attention entry (count 1). The registry + renderer own the badge.
892 1245 Menu_Badges::init(); // idempotent; wires the renderer.
1246 +
1247 + // Connection errors are owned by the Error Handler (the single source of truth):
1248 + // surface any it reports for this viewer as their own attention entry. Read live
1249 + // rather than from the red-bubble transient, since that cache is not viewer-keyed.
1250 + $has_connection_error = self::has_connection_error();
1251 + if ( $has_connection_error ) {
1252 + Notification_Counts::register(
1253 + 'my-jetpack-connection-error',
1254 + array(
1255 + 'menu_slug' => 'my-jetpack',
1256 + 'type' => 'attention',
1257 + )
1258 + );
1259 + }
1260 +
893 1261 foreach ( array_keys( $red_bubble_alerts ) as $slug ) {
894 1262 // Protect reports its own count directly to the registry, but only when its
895 1263 // standalone plugin is active (see class-jetpack-protect.php::admin_page_init()).
896 1264 // If the standalone plugin isn't active, nobody else registers this count, so we
@@ -897,8 +1265,13 @@
897 1265 // must not skip it here or the alert silently disappears from the menu total.
898 1266 if ( 'protect_has_threats' === $slug && Products\Protect::is_standalone_plugin_active() ) {
899 1267 continue;
900 1268 }
1269 + // The missing-connection slug and a connection error describe the same broken
1270 + // connection; count it once (the error, above, is the more specific signal).
1271 + if ( $has_connection_error && 'missing-connection' === $slug ) {
1272 + continue;
1273 + }
901 1274 Notification_Counts::register(
902 1275 'my-jetpack-' . $slug,
903 1276 array(
904 1277 'menu_slug' => 'my-jetpack',
@@ -905,8 +1278,26 @@
905 1278 'type' => 'attention',
906 1279 )
907 1280 );
908 1281 }
1282 + }
1283 +
1284 + /**
1285 + * Whether the Connection Error Handler reports a displayable connection error
1286 + * for the current viewer.
1287 + *
1288 + * Read live (not via the red-bubble transient): get_displayable_errors() is a
1289 + * cached option read that the Error Handler already scopes and caches per viewer.
1290 + * Guarded for the mid-plugin-update window, where a stale connection package
1291 + * predating the method can be loaded.
1292 + *
1293 + * @return bool
1294 + */
1295 + private static function has_connection_error() {
1296 + if ( ! class_exists( Connection_Error_Handler::class ) || ! method_exists( Connection_Error_Handler::class, 'get_displayable_errors' ) ) {
1297 + return false;
1298 + }
1299 + return ! empty( Connection_Error_Handler::get_instance()->get_displayable_errors() );
909 1300 }
910 1301
911 1302 /**
912 1303 * Enqueue the notification bubble script.