This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,296 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.11.1] - 2026-06-29
### Changed
- Settings: Link the "Button only" style hint to platform-appropriate support documentation. [#50009]
- Update package dependencies. [#49271]
## [0.11.0] - 2026-06-25
### Changed
- Introduce the modernized dashboard and WP Admin subscriber management behind the `rsm_jetpack_ui_modernization_newsletter` and `jetpack_wp_admin_subscriber_management_enabled` filters, off by default. [#49036]
- Update package dependencies. [#49831]
### Fixed
- Align the settings DataForm field `Edit` controls, the import-subscribers notice `ActionLink`, and the dashboard tab panels with the `@wordpress/dataviews` 16 and `@wordpress/ui` 0.15 type APIs. [#49801]
## [0.10.0] - 2026-06-22
### Added
- Add a transitional Subscribers announcement page (wp-build) shown in place of the legacy Subscribers menu link when the Newsletter modernization filter is enabled. [#49496]
- Register the transitional Subscribers announcement page directly under the Jetpack menu on WordPress.com sites. [#49675]
### Changed
- Subscribers: Link the Substack importer button to the WordPress.com site-setup importer flow. [#49538]
- Update package dependencies. [#49631] [#49691] [#49757]
### Fixed
- Close the subscriber detail panel when its subscriber is removed instead of leaving it open with stale data. [#49598]
## [0.9.1] - 2026-06-15
### Added
- Add a setting for the Subscribe modal heading shown by Subscribe blocks using the Button-only style. [#49171]
- Daily Writing Prompt: Record Tracks events when interacting with the widget's actions. [#49534]
### Changed
- Daily Writing Prompt: Link to the WordPress.com Reader and move the responses button next to the Post Answer button. [#49462]
- Label the subscription placement options with a "Homepage and posts" section header. [#49466]
- Update package dependencies. [#49273] [#49492]
### Fixed
- Daily Writing Prompt: Defer the connection-readiness check to Dashboard setup to avoid a fatal error on Atomic sites. [#49525]
- Fix the "Add plans" link for WordPress installs in a subdirectory. [#49553]
- Keep the Subscribers/Settings tab bar pinned while scrolling the modernized Settings tab. [#49460]
- Only show the subscription placement "Preview and edit" link once the placement is enabled and saved. [#49532]
- Render the same initials avatar in the subscriber row and detail panel when a subscriber has no Gravatar. [#49581]
- Stop the modernized Settings tab from flashing a full-page loading spinner on every visit. [#49530]
- Subscribers: Hide the "Comp a subscription" action when the site has no paid newsletter plans. [#49531]
## [0.9.0] - 2026-06-08
### Added
- Add Jetpack branding to the Daily Writing Prompt dashboard widget. [#49438]
- Add the Daily Writing Prompt dashboard widget, moved here from the jetpack-mu-wpcom package. [#49425]
### Changed
- Remove the per-page Hello Dolly rule, as it is now handled by `@automattic/jetpack-components`'s AdminPage component. [#48472]
### Fixed
- Include the routes directory in the TypeScript config so dashboard route files type-check correctly. [#49368]
- Hide the WordPress.com connection prompt on Simple sites, which are already connected. [#49365]
- Restore body padding by wrapping content in Dialog.Content in Add subscribers and Comp modals. [#49327]
- Subscribers Dashboard: Fix several Comp modal issues. [#49441]
- Subscribers Dashboard: Show the empty state when the site owner is the only subscriber, and point the empty-state copy at the subscription form documentation. [#49410]
## [0.8.10] - 2026-06-01
### Changed
- Update package dependencies. [#48404] [#49152]
### Fixed
- Record a `jetpack_newsletter_tab_view` Tracks event on initial page load in Newsletter dashboard (matching its tab-switch behavior). [#49243]
## [0.8.9] - 2026-05-25
### Changed
- Normalize page tabs onto the shared minimal variant and `jp-admin-page-tabs--minimal` wrapper modifier. [#48964]
- Update package dependencies. [#48405] [#49012]
### Fixed
- Drop dead `@wordpress/admin-ui` build-style import that no longer exists in 2.x. [#49007]
- Fix Settings tab toggles that controlled the wrong setting due to duplicate element IDs. [#49102]
## [0.8.8] - 2026-05-19
### Changed
- Build: Run webpack and wp-build scripts concurrently. [#48794]
- Exclude development files from production builds. [#47365]
### Fixed
- Fix saving settings on the Newsletter admin page on Atomic and self-hosted sites. [#48813]
## [0.8.7] - 2026-05-11
### Changed
- Components: Use Link from `@wordpress/ui` instead of ExternalLink. [#48529]
## [0.8.6] - 2026-05-04
### Changed
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
## [0.8.5] - 2026-04-27
### Fixed
- Reader Link: Only enqueue styles when the admin bar is visible. [#48145]
## [0.8.4] - 2026-04-20
### Changed
- Adopt the shared Jetpack admin-page-layout mixin on the Newsletter admin page: pinned header, scrolling middle, pinned footer, no window-level scroll. [#48109]
- Update package dependencies. [#48106] [#48141]
### Removed
- Remove the jetpack_wp_admin_newsletter_settings_enabled filter; the wp-admin newsletter settings page is now always enabled. [#48092]
## [0.8.3] - 2026-04-15
### Changed
- Update package dependencies. [#47907]
## [0.8.2] - 2026-04-10
### Changed
- Update dependencies. [#48049]
## [0.8.1] - 2026-04-09
### Changed
- Update package dependencies. [#47890] [#47998]
### Fixed
- Defer is_connected() check from init to admin_menu callback to avoid caching a false result before External Storage providers are registered. [#48005]
## [0.8.0] - 2026-04-06
### Added
- Newsletter settings: Disable settings and show a connection notice when the site has no connected owner. [#47927]
### Fixed
- Admin menu: Hide when a site is not connected to WordPress.com yet. [#47927]
## [0.7.0] - 2026-03-30
### Changed
- Enable WP Admin newsletter settings by default. [#47750]
- Update DataViews dependency. [#46973]
- Update legacy Node calls. [#47770]
- Update package dependencies. [#47799]
### Fixed
- Fix spacing between the send-default toggle description and the "Manage all subscribers" link in the Newsletter settings card. [#47830]
- Move admin notices and JITMs below the page header. [#47714]
- Reader Link: Add compatibility check for Urls::maybe_add_origin_site_id() to prevent fatal errors with older connection package versions. [#47808]
- Settings: Avoid fatals for disconnected jetpack errors. [#47782]
## [0.6.2] - 2026-03-23
### Changed
- Add "Send newsletter by default" toggle to Newsletter settings section. [#47565]
- Update newsletter settings max width to 660px to match MSD and future settings pages. [#47626]
- Update package dependencies. [#47684]
### Fixed
- Fix translator comment concatenation caused by dataviews function aliasing. [#47602]
## [0.6.1] - 2026-03-16
### Added
- Settings: Add privacy information link. [#47587]
### Changed
- Settings: Fix inconsistent punctuation and casing on labels. [#47559]
- Settings: Link to WordPress.com for all WordPress.com sites. [#47578]
- Update dependencies. [#47472]
### Fixed
- Add spacing below warning notice in Email content settings section. [#47516]
- Subscriptions: Fix mobile alignment of Preview and edit links in settings. [#47561]
- Ensure module configure URL points to the new newsletter settings page. [#47584]
## [0.6.0] - 2026-03-09
### Added
- Add `jetpack_show_newsletter_menu_item` filter to show menu regardless of subscriptions module state. [#47347]
### Changed
- Migrate admin page header to use unified header pattern. [#47313]
- Switch to Native TypeScript compiler based on Go. [#47375]
- Update newsletter sections to use new component patterns. [#47406]
- Update package dependencies. [#47496]
### Fixed
- Settings: Fix Hello Dolly banner display and box-sizing on the newsletter settings page. [#47313]
## [0.5.2] - 2026-03-02
### Changed
- Update dependencies. [#47038]
## [0.5.1] - 2026-02-26
### Added
- Add clarifying note to Reading settings page linking to Newsletter settings for email content control. [#47299]
### Changed
- Refactor settings to use `@automattic/jetpack-script-data` utilities for site info and admin URLs. [#47259]
- Update package dependencies. [#47285] [#47300]
- Use `Status` class methods for `isSitePublic` check (filterable and handles coming soon state). [#47321]
## [0.5.0] - 2026-02-23
### Added
- Add Tracks analytics to track user interactions on the newsletter settings page. [#47100]
### Changed
- Reader Link: Open the Reader in a different tab on self-hosted sites. [#47246]
- Settings: Wrap page with `AdminPage` component for consistency with other Jetpack pages. [#47086]
- Update package dependencies. [#47173]
## [0.4.0] - 2026-02-16
### Added
- Automatically enable the admin bar link on sites newly connected to WordPress.com. [#47033]
- WordPress.com Reader: Add new class to render a new navigation menu in the admin bar. [#46783]
### Changed
- Update package dependencies. [#47099]
### Fixed
- Settings: Improve feedback with toast notifications for save success/error and inline error for category load failures. [#46987]
## [0.3.2] - 2026-02-10
### Changed
- CSS: Ensure dataforms css is loaded. [#46877]
- Update dependencies. [#46931] [#47002]
### Fixed
- Fix settings and categories API endpoints on WordPress.com Simple sites. [#46930]
## [0.3.1] - 2026-02-02
### Added
- Add a newsletter categories section to the settings screen. [#46708]
- Add settings sections: subscriptions, paid newsletter, and welcome message. [#46473]
### Changed
- Update package dependencies. [#46430] [#46853] [#46854]
## [0.3.0] - 2026-01-26
### Added
- Add newsletter settings to the new screen. [#46471]
## [0.2.0] - 2026-01-19
### Added
- Implement bits of infrastructure for newsletter settings UI. [#46470]
### Changed
- Update package dependencies. [#46552] [#46647]
## [0.1.4] - 2026-01-12
### Changed
- Update package dependencies. [#46456]
## [0.1.3] - 2025-12-22
### Changed
- Update dependencies. [#46381]
## [0.1.2] - 2025-12-15
### Changed
- Update package dependencies. [#46244]
## [0.1.1] - 2025-12-08
### Changed
- Update package dependencies. [#45914]
## 0.1.0 - 2025-12-01
### Added
- Initial version. [#46049]
### Changed
- Update package dependencies. [#46143]
[0.11.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.11.0...v0.11.1
[0.11.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.10...v0.9.0
[0.8.10]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.9...v0.8.10
[0.8.9]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.8...v0.8.9
[0.8.8]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.7...v0.8.8
[0.8.7]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.6...v0.8.7
[0.8.6]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.5...v0.8.6
[0.8.5]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.4...v0.8.5
[0.8.4]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.3...v0.8.4
[0.8.3]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.2...v0.8.3
[0.8.2]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.6.2...v0.7.0
[0.6.2]: https://github.com/Automattic/jetpack-newsletter/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.5.2...v0.6.0
[0.5.2]: https://github.com/Automattic/jetpack-newsletter/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.3.2...v0.4.0
[0.3.2]: https://github.com/Automattic/jetpack-newsletter/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/Automattic/jetpack-newsletter/compare/v0.1.4...v0.2.0
[0.1.4]: https://github.com/Automattic/jetpack-newsletter/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/Automattic/jetpack-newsletter/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/Automattic/jetpack-newsletter/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/Automattic/jetpack-newsletter/compare/v0.1.0...v0.1.1
@@ -0,0 +1,43 @@
<?php
/**
* Main entry point for auto-generated asset registration.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
// Load script module registration.
$modules_file = __DIR__ . '/modules.php';
if ( file_exists( $modules_file ) ) {
require_once $modules_file;
}
// Load script registration.
$scripts_file = __DIR__ . '/scripts.php';
if ( file_exists( $scripts_file ) ) {
require_once $scripts_file;
}
// Load style registration.
$styles_file = __DIR__ . '/styles.php';
if ( file_exists( $styles_file ) ) {
require_once $styles_file;
}
// Load routes registration.
$routes_file = __DIR__ . '/routes.php';
if ( file_exists( $routes_file ) ) {
require_once $routes_file;
}
// Load widgets registration.
$widgets_file = __DIR__ . '/widgets.php';
if ( file_exists( $widgets_file ) ) {
require_once $widgets_file;
}
// Load pages registration.
$pages_file = __DIR__ . '/pages.php';
if ( file_exists( $pages_file ) ) {
require_once $pages_file;
}
@@ -0,0 +1,14 @@
<?php
/**
* Plugin constants - Auto-generated by build process.
* Do not edit this file manually.
*
* Returns an array of constants for use in other templates.
*
* @package jetpack_newsletter
*/
return array(
'version' => '0.11.1',
'build_url' => plugin_dir_url( __FILE__ ),
);
@@ -0,0 +1,17 @@
<svg width="380" height="183" viewBox="0 0 380 183" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.75" filter="url(#filter0_f_5953_11924)">
<path d="M360 -60C360 7.93102 304.931 63 237 63C169.069 63 114 7.93102 114 -60C114 -127.931 169.069 -183 237 -183C304.931 -183 360 -127.931 360 -60Z" fill="url(#paint0_linear_5953_11924)"/>
</g>
<defs>
<filter id="filter0_f_5953_11924" x="-6" y="-303" width="486" height="486" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_5953_11924"/>
</filter>
<linearGradient id="paint0_linear_5953_11924" x1="360" y1="-60" x2="114" y2="-60" gradientUnits="userSpaceOnUse">
<stop stop-color="#48FF50"/>
<stop offset="0.471846" stop-color="#108642"/>
<stop offset="1" stop-color="#2E38FA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 990 B

@@ -0,0 +1,63 @@
<?php
/**
* Script module registration - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
/**
* Register all script modules.
*/
function jetpack_newsletter_register_script_modules() {
// Ensure this only runs once. wp_default_scripts can fire multiple times,
// and each wp_deregister_script_module() call also dequeues the module.
// If a module was enqueued between calls, repeated deregister/register
// cycles would lose the enqueue state.
static $already_registered = false;
if ( $already_registered ) {
return;
}
$already_registered = true;
// Load build constants
$constants_file = __DIR__ . '/constants.php';
if ( ! file_exists( $constants_file ) ) {
return;
}
$build_constants = require $constants_file;
$modules_dir = __DIR__ . '/modules';
$modules_file = $modules_dir . '/registry.php';
if ( ! file_exists( $modules_file ) ) {
return;
}
$modules = require $modules_file;
$base_url = $build_constants['build_url'] . 'modules/';
foreach ( $modules as $module ) {
// WASM-only modules (e.g., vips worker) only have .min.js; use it even when SCRIPT_DEBUG is `true`.
$extension = '.min.js';
$asset_path = $modules_dir . '/' . $module['asset'];
$asset = file_exists( $asset_path ) ? require $asset_path : array();
// Deregister first to override any previously registered version
// (e.g., Core's default modules when running as a plugin).
wp_deregister_script_module( $module['id'] );
wp_register_script_module(
$module['id'],
$base_url . $module['path'] . $extension,
$asset['module_dependencies'] ?? array(),
$asset['version'] ?? false,
array(
'fetchpriority' => 'low',
'in_footer' => true,
)
);
}
}
add_action( 'wp_default_scripts', 'jetpack_newsletter_register_script_modules' );
@@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-commands', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-notices', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/lazy-editor', 'import' => 'dynamic'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => '188faf6c64fdd1b4');
@@ -0,0 +1,11 @@
<?php
/**
* Script module registry - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
return array(
);
@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-connection', 'jetpack-script-data', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-polyfill', 'wp-primitives', 'wp-private-apis', 'wp-url'), 'version' => 'ffce2c4026fe2ea03c4f');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
<?php
/**
* Pages loader - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
foreach ( [
__DIR__ . '/pages/jetpack-newsletter-dashboard/page.php',
__DIR__ . '/pages/jetpack-newsletter-dashboard/page-wp-admin.php',
__DIR__ . '/pages/jetpack-subscribers-announcement/page.php',
__DIR__ . '/pages/jetpack-subscribers-announcement/page-wp-admin.php',
] as $file ) {
if ( file_exists( $file ) ) {
require_once $file;
}
}
@@ -0,0 +1 @@
// Empty module loader for page dependencies
@@ -0,0 +1,317 @@
<?php
/**
* Page: jetpack-newsletter-dashboard (wp-admin integrated)
* Auto-generated by build process.
* Do not edit this file manually.
*
* This version integrates with the standard WordPress admin interface,
* keeping the wp-admin sidebar and scripts/styles intact.
*
* @package jetpack_newsletter
*/
// Global storage for jetpack-newsletter-dashboard routes and menu items
global $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_routes, $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_menu_items;
$jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_routes = array();
$jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_menu_items = array();
/**
* Register a route for the jetpack-newsletter-dashboard-wp-admin page.
*
* @param string $path Route path (e.g., '/types/$type/edit/$id').
* @param string|null $content_module Script module ID for content (stage/inspector).
* @param string|null $route_module Script module ID for route lifecycle hooks.
*/
function jetpack_newsletter_register_jetpack_newsletter_dashboard_wp_admin_route( $path, $content_module = null, $route_module = null ) {
global $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_routes;
$route = array( 'path' => $path );
if ( ! empty( $content_module ) ) {
$route['content_module'] = $content_module;
}
if ( ! empty( $route_module ) ) {
$route['route_module'] = $route_module;
}
$jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_routes[] = $route;
}
/**
* Register a menu item for the jetpack-newsletter-dashboard-wp-admin page.
* Note: Menu items are registered but not displayed in single-page mode.
*
* @param string $id Menu item ID.
* @param string $label Display label.
* @param string $to Route path to navigate to.
* @param string $parent_id Optional. Parent menu item ID.
*/
function jetpack_newsletter_register_jetpack_newsletter_dashboard_wp_admin_menu_item( $id, $label, $to, $parent_id = '' ) {
global $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_menu_items;
$menu_item = array(
'id' => $id,
'label' => $label,
'to' => $to,
);
if ( ! empty( $parent_id ) ) {
$menu_item['parent'] = $parent_id;
}
$jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_menu_items[] = $menu_item;
}
/**
* Get all registered routes for the jetpack-newsletter-dashboard-wp-admin page.
*
* @return array Array of route objects.
*/
function jetpack_newsletter_get_jetpack_newsletter_dashboard_wp_admin_routes() {
global $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_routes;
return $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_routes ?? array();
}
/**
* Get all registered menu items for the jetpack-newsletter-dashboard-wp-admin page.
*
* @return array Array of menu item objects.
*/
function jetpack_newsletter_get_jetpack_newsletter_dashboard_wp_admin_menu_items() {
global $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_menu_items;
return $jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_menu_items ?? array();
}
/**
* Preload REST API data for the jetpack-newsletter-dashboard-wp-admin page.
* Automatically called during page rendering.
*/
function jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_preload_data() {
// Define paths to preload - same for all pages
// Please also change packages/core-data/src/entities.js when changing this.
$preload_paths = array(
'/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
array( '/wp/v2/settings', 'OPTIONS' ),
);
// Use rest_preload_api_request to gather the preloaded data
$preload_data = array_reduce(
$preload_paths,
'rest_preload_api_request',
array()
);
// Register the preloading middleware with wp-api-fetch
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
}
/**
* Enqueue scripts and styles for the jetpack-newsletter-dashboard-wp-admin page.
* Hooked to admin_enqueue_scripts.
*
* @param string $hook_suffix The current admin page.
*/
function jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_enqueue_scripts( $hook_suffix ) {
// Check all possible ways this page can be accessed:
// 1. Menu page via admin.php?page=jetpack-newsletter-dashboard-wp-admin (plugin)
// 2. Direct file via jetpack-newsletter-dashboard.php (Core) - screen ID will be 'jetpack-newsletter-dashboard'
$current_screen = get_current_screen();
$is_our_page = (
( isset( $_GET['page'] ) && 'jetpack-newsletter-dashboard-wp-admin' === $_GET['page'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
( $current_screen && 'jetpack-newsletter-dashboard' === $current_screen->id )
);
if ( ! $is_our_page ) {
return;
}
// Load build constants
$build_constants = require __DIR__ . '/../../constants.php';
/**
* Fires when the jetpack-newsletter-dashboard admin page is initialized so extensions can register routes and menu items.
*/
do_action( 'jetpack-newsletter-dashboard-wp-admin_init' );
// Preload REST API data
jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_preload_data();
// Get all registered routes
$routes = jetpack_newsletter_get_jetpack_newsletter_dashboard_wp_admin_routes();
// Get boot module asset file for dependencies
$asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
if ( file_exists( $asset_file ) ) {
$asset = require $asset_file;
// This script serves two purposes:
// 1. It ensures all the globals that are made available to the modules are loaded.
// 2. It initializes the boot module as an inline script.
wp_register_script( 'jetpack-newsletter-dashboard-wp-admin-prerequisites', '', $asset['dependencies'], $asset['version'], true );
/*
* Add inline script to initialize the app using initSinglePage (no menuItems).
* The dynamic import is deferred until DOMContentLoaded so that all classic
* script dependencies of @wordpress/boot (wp-private-apis, wp-components,
* wp-theme, etc.) have finished parsing and executing before the boot module
* evaluates. Otherwise, a modulepreloaded @wordpress/boot can win the race
* against the classic-script-printing pass on fast CDN-fronted hosts in
* Chrome, evaluating before wp.theme.privateApis is defined and throwing
* "Cannot unlock an undefined object". See <https://core.trac.wordpress.org/ticket/65103>.
*/
$init_js_function = <<<'JS'
( mountId, routes ) => {
const run = async () => {
const mod = await import( "@wordpress/boot" );
mod.initSinglePage( { mountId, routes } );
};
if ( document.readyState === "loading" ) {
document.addEventListener( "DOMContentLoaded", run );
} else {
run();
}
}
JS;
wp_add_inline_script(
'jetpack-newsletter-dashboard-wp-admin-prerequisites',
sprintf(
'( %s )( %s, %s );',
$init_js_function,
wp_json_encode( 'jetpack-newsletter-dashboard-wp-admin-app', JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
)
);
// Register prerequisites style by filtering script dependencies to find registered styles
$style_dependencies = array_filter(
$asset['dependencies'],
function ( $handle ) {
return wp_style_is( $handle, 'registered' );
}
);
wp_register_style( 'jetpack-newsletter-dashboard-wp-admin-prerequisites', false, $style_dependencies, $asset['version'] );
// Build dependencies for jetpack-newsletter-dashboard-wp-admin module
$boot_dependencies = array(
array(
'import' => 'static',
'id' => '@wordpress/boot',
),
);
// Add all registered routes as dependencies
foreach ( $routes as $route ) {
if ( isset( $route['route_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'static',
'id' => $route['route_module'],
);
}
if ( isset( $route['content_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'dynamic',
'id' => $route['content_module'],
);
}
}
/**
* Filters the boot script-module dependencies for the
* jetpack-newsletter-dashboard-wp-admin page.
*
* Surfaces extending this page can append entries to the boot
* dependency list. Each entry is an array with 'import' (string
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
*
* @param array $boot_dependencies Boot dependencies for the page.
*/
$boot_dependencies = apply_filters(
'jetpack-newsletter-dashboard-wp-admin_boot_dependencies',
$boot_dependencies
);
// Dummy script module to ensure dependencies are loaded
wp_register_script_module(
'jetpack-newsletter-dashboard-wp-admin',
$build_constants['build_url'] . 'pages/jetpack-newsletter-dashboard/loader.js',
$boot_dependencies
);
// Enqueue the boot scripts and styles
wp_enqueue_script( 'jetpack-newsletter-dashboard-wp-admin-prerequisites' );
wp_enqueue_script_module( 'jetpack-newsletter-dashboard-wp-admin' );
wp_enqueue_style( 'jetpack-newsletter-dashboard-wp-admin-prerequisites' );
}
}
/**
* Render the jetpack-newsletter-dashboard-wp-admin page.
* Call this function from add_menu_page or add_submenu_page.
* This renders within the normal WordPress admin interface.
*/
function jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_render_page() {
?>
<style>
/* Critical styles to prevent layout shifts - inlined for immediate application */
#wpwrap {
overflow-y: auto;
}
body {
background: #fff;
}
/* Reset wp-admin padding */
#wpcontent {
padding-inline-start: 0;
}
#wpbody-content {
padding-bottom: 0;
}
/* Hide legacy admin elements */
#wpbody-content > div:not(.boot-layout-container):not(#screen-meta) {
display: none;
}
#wpfooter {
display: none;
}
/* Accessibility regions */
.a11y-speak-region {
inset-inline-start: -1px;
top: -1px;
}
/* Admin menu indicators */
ul#adminmenu a.wp-has-current-submenu::after,
ul#adminmenu > li.current > a.current::after {
border-inline-end-color: #fff;
}
/* Media frame fix */
.media-frame select.attachment-filters:last-of-type {
width: auto;
max-width: 100%;
}
/* Responsive overflow fix for #wpwrap */
@media (min-width: 782px) {
#wpwrap {
overflow-y: initial;
}
}
</style>
<div id="jetpack-newsletter-dashboard-wp-admin-app" class="boot-layout-container"></div>
<?php
}
// Hook the enqueue function to admin_enqueue_scripts
add_action( 'admin_enqueue_scripts', 'jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_enqueue_scripts' );
@@ -0,0 +1,318 @@
<?php
/**
* Page: jetpack-newsletter-dashboard
* Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
// Global storage for jetpack-newsletter-dashboard routes and menu items
global $jetpack_newsletter_jetpack_newsletter_dashboard_routes, $jetpack_newsletter_jetpack_newsletter_dashboard_menu_items;
$jetpack_newsletter_jetpack_newsletter_dashboard_routes = array();
$jetpack_newsletter_jetpack_newsletter_dashboard_menu_items = array();
/**
* Register a route for the jetpack-newsletter-dashboard page.
*
* @param string $path Route path (e.g., '/types/$type/edit/$id').
* @param string|null $content_module Script module ID for content (stage/inspector).
* @param string|null $route_module Script module ID for route lifecycle hooks.
*/
function jetpack_newsletter_register_jetpack_newsletter_dashboard_route( $path, $content_module = null, $route_module = null ) {
global $jetpack_newsletter_jetpack_newsletter_dashboard_routes;
$route = array( 'path' => $path );
if ( ! empty( $content_module ) ) {
$route['content_module'] = $content_module;
}
if ( ! empty( $route_module ) ) {
$route['route_module'] = $route_module;
}
$jetpack_newsletter_jetpack_newsletter_dashboard_routes[] = $route;
}
/**
* Register a menu item for the jetpack-newsletter-dashboard page.
*
* @param string $id Menu item ID.
* @param string $label Display label.
* @param string $to Route path to navigate to.
* @param string $parent_id Optional. Parent menu item ID.
* @param string $parent_type Optional. Parent type: 'drilldown' or 'dropdown'.
*/
function jetpack_newsletter_register_jetpack_newsletter_dashboard_menu_item( $id, $label, $to, $parent_id = '', $parent_type = '' ) {
global $jetpack_newsletter_jetpack_newsletter_dashboard_menu_items;
$menu_item = array(
'id' => $id,
'label' => $label,
'to' => $to,
);
if ( ! empty( $parent_id ) ) {
$menu_item['parent'] = $parent_id;
}
if ( ! empty( $parent_type ) && in_array( $parent_type, array( 'drilldown', 'dropdown' ), true ) ) {
$menu_item['parent_type'] = $parent_type;
}
$jetpack_newsletter_jetpack_newsletter_dashboard_menu_items[] = $menu_item;
}
/**
* Get all registered routes for the jetpack-newsletter-dashboard page.
*
* @return array Array of route objects.
*/
function jetpack_newsletter_get_jetpack_newsletter_dashboard_routes() {
global $jetpack_newsletter_jetpack_newsletter_dashboard_routes;
return $jetpack_newsletter_jetpack_newsletter_dashboard_routes ?? array();
}
/**
* Get all registered menu items for the jetpack-newsletter-dashboard page.
*
* @return array Array of menu item objects.
*/
function jetpack_newsletter_get_jetpack_newsletter_dashboard_menu_items() {
global $jetpack_newsletter_jetpack_newsletter_dashboard_menu_items;
return $jetpack_newsletter_jetpack_newsletter_dashboard_menu_items ?? array();
}
/**
* Preload REST API data for the jetpack-newsletter-dashboard page.
* Automatically called during page rendering.
*/
function jetpack_newsletter_jetpack_newsletter_dashboard_preload_data() {
// Define paths to preload - same for all pages
// Please also change packages/core-data/src/entities.js when changing this.
$preload_paths = array(
'/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
array( '/wp/v2/settings', 'OPTIONS' ),
);
// Use rest_preload_api_request to gather the preloaded data
$preload_data = array_reduce(
$preload_paths,
'rest_preload_api_request',
array()
);
// Register the preloading middleware with wp-api-fetch
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
}
/**
* Render the jetpack-newsletter-dashboard page.
* Call this function from add_menu_page or add_submenu_page.
*/
function jetpack_newsletter_jetpack_newsletter_dashboard_render_page() {
// Load build constants
$build_constants = require __DIR__ . '/../../constants.php';
// Set current screen
set_current_screen();
// Remove unwanted deprecated handler
remove_action( 'admin_head', 'wp_admin_bar_header' );
// Remove unwanted scripts and styles that were enqueued during `admin_init`
foreach ( wp_scripts()->queue as $script ) {
wp_dequeue_script( $script );
}
foreach ( wp_styles()->queue as $style ) {
wp_dequeue_style( $style );
}
/**
* Fires when the jetpack-newsletter-dashboard page is initialized so extensions can register routes and menu items.
*/
do_action( 'jetpack-newsletter-dashboard_init' );
// Enqueue command palette assets for boot-based pages
if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
wp_enqueue_command_palette_assets();
}
// Preload REST API data
jetpack_newsletter_jetpack_newsletter_dashboard_preload_data();
// Get all registered routes and menu items
$menu_items = jetpack_newsletter_get_jetpack_newsletter_dashboard_menu_items();
$routes = jetpack_newsletter_get_jetpack_newsletter_dashboard_routes();
// Get boot module asset file for dependencies
$asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
if ( file_exists( $asset_file ) ) {
$asset = require $asset_file;
// This script serves two purposes:
// 1. It ensures all the globals that are made available to the modules are loaded.
// 2. It initializes the boot module as an inline script.
wp_register_script( 'jetpack-newsletter-dashboard-prerequisites', '', $asset['dependencies'], $asset['version'], true );
// Add inline script to initialize the app
$init_modules = [];
wp_add_inline_script(
'jetpack-newsletter-dashboard-prerequisites',
sprintf(
'import("@wordpress/boot").then(mod => mod.init({mountId: "%s", menuItems: %s, routes: %s, initModules: %s, dashboardLink: "%s"}));',
'jetpack-newsletter-dashboard-app',
wp_json_encode( $menu_items, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
wp_json_encode( $init_modules, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
esc_url( admin_url( '/' ) )
)
);
// Register prerequisites style by filtering script dependencies to find registered styles
$style_dependencies = array_filter(
$asset['dependencies'],
function ( $handle ) {
return wp_style_is( $handle, 'registered' );
}
);
wp_register_style( 'jetpack-newsletter-dashboard-prerequisites', false, $style_dependencies, $asset['version'] );
// Build dependencies for jetpack-newsletter-dashboard module
$boot_dependencies = array(
array(
'import' => 'static',
'id' => '@wordpress/boot',
),
);
// Add init modules as static dependencies
// No init modules configured
// Add all registered routes as dependencies
foreach ( $routes as $route ) {
if ( isset( $route['route_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'static',
'id' => $route['route_module'],
);
}
if ( isset( $route['content_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'dynamic',
'id' => $route['content_module'],
);
}
}
/**
* Filters the boot script-module dependencies for the
* jetpack-newsletter-dashboard page.
*
* Surfaces extending this page can append entries to the boot
* dependency list. Each entry is an array with 'import' (string
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
*
* @param array $boot_dependencies Boot dependencies for the page.
*/
$boot_dependencies = apply_filters(
'jetpack-newsletter-dashboard_boot_dependencies',
$boot_dependencies
);
// Dummy script module to ensure dependencies are loaded
wp_register_script_module(
'jetpack-newsletter-dashboard',
$build_constants['build_url'] . 'pages/jetpack-newsletter-dashboard/loader.js',
$boot_dependencies
);
// Enqueue the boot scripts and styles
wp_enqueue_script( 'jetpack-newsletter-dashboard-prerequisites' );
wp_enqueue_script_module( 'jetpack-newsletter-dashboard' );
wp_enqueue_style( 'jetpack-newsletter-dashboard-prerequisites' );
}
// Output the HTML
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo esc_html( get_admin_page_title() ); ?></title>
<style>
html {
background: #f1f1f1;
color: #444;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.4em;
}
body {
margin: 0;
}
#wpadminbar { display: none; }
</style>
<?php
global $hook_suffix;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$hook_suffix = 'jetpack-newsletter-dashboard';
// BEGIN see wp-admin/admin-header.php
print_admin_styles();
print_head_scripts();
/** This action is documented in wp-admin/admin-header.php */
do_action( "admin_head-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_head' );
// END see wp-admin/admin-header.php
?>
</head>
<body class="jetpack-newsletter-dashboard">
<div id="jetpack-newsletter-dashboard-app" style="height: 100vh; box-sizing: border-box;"></div>
<?php
// BEGIN see wp-admin/admin-footer.php
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_footer', '' );
// Print import map first so it's available for inline scripts
wp_script_modules()->print_import_map();
print_footer_scripts();
wp_script_modules()->print_enqueued_script_modules();
wp_script_modules()->print_script_module_preloads();
wp_script_modules()->print_script_module_data();
/** This action is documented in wp-admin/admin-footer.php */
do_action( "admin_footer-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
// END see wp-admin/admin-footer.php
?>
</body>
</html>
<?php
exit;
}
/**
* Intercept admin_init to render the page early.
* This bypasses the default WordPress admin template.
*/
function jetpack_newsletter_jetpack_newsletter_dashboard_intercept_render() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['page'] ) && 'jetpack-newsletter-dashboard' === $_GET['page'] ) {
jetpack_newsletter_jetpack_newsletter_dashboard_render_page();
exit;
}
}
// Hook the interceptor to admin_init
add_action( 'admin_init', 'jetpack_newsletter_jetpack_newsletter_dashboard_intercept_render' );
@@ -0,0 +1 @@
// Empty module loader for page dependencies
@@ -0,0 +1,317 @@
<?php
/**
* Page: jetpack-subscribers-announcement (wp-admin integrated)
* Auto-generated by build process.
* Do not edit this file manually.
*
* This version integrates with the standard WordPress admin interface,
* keeping the wp-admin sidebar and scripts/styles intact.
*
* @package jetpack_newsletter
*/
// Global storage for jetpack-subscribers-announcement routes and menu items
global $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_routes, $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_menu_items;
$jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_routes = array();
$jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_menu_items = array();
/**
* Register a route for the jetpack-subscribers-announcement-wp-admin page.
*
* @param string $path Route path (e.g., '/types/$type/edit/$id').
* @param string|null $content_module Script module ID for content (stage/inspector).
* @param string|null $route_module Script module ID for route lifecycle hooks.
*/
function jetpack_newsletter_register_jetpack_subscribers_announcement_wp_admin_route( $path, $content_module = null, $route_module = null ) {
global $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_routes;
$route = array( 'path' => $path );
if ( ! empty( $content_module ) ) {
$route['content_module'] = $content_module;
}
if ( ! empty( $route_module ) ) {
$route['route_module'] = $route_module;
}
$jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_routes[] = $route;
}
/**
* Register a menu item for the jetpack-subscribers-announcement-wp-admin page.
* Note: Menu items are registered but not displayed in single-page mode.
*
* @param string $id Menu item ID.
* @param string $label Display label.
* @param string $to Route path to navigate to.
* @param string $parent_id Optional. Parent menu item ID.
*/
function jetpack_newsletter_register_jetpack_subscribers_announcement_wp_admin_menu_item( $id, $label, $to, $parent_id = '' ) {
global $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_menu_items;
$menu_item = array(
'id' => $id,
'label' => $label,
'to' => $to,
);
if ( ! empty( $parent_id ) ) {
$menu_item['parent'] = $parent_id;
}
$jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_menu_items[] = $menu_item;
}
/**
* Get all registered routes for the jetpack-subscribers-announcement-wp-admin page.
*
* @return array Array of route objects.
*/
function jetpack_newsletter_get_jetpack_subscribers_announcement_wp_admin_routes() {
global $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_routes;
return $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_routes ?? array();
}
/**
* Get all registered menu items for the jetpack-subscribers-announcement-wp-admin page.
*
* @return array Array of menu item objects.
*/
function jetpack_newsletter_get_jetpack_subscribers_announcement_wp_admin_menu_items() {
global $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_menu_items;
return $jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_menu_items ?? array();
}
/**
* Preload REST API data for the jetpack-subscribers-announcement-wp-admin page.
* Automatically called during page rendering.
*/
function jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_preload_data() {
// Define paths to preload - same for all pages
// Please also change packages/core-data/src/entities.js when changing this.
$preload_paths = array(
'/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
array( '/wp/v2/settings', 'OPTIONS' ),
);
// Use rest_preload_api_request to gather the preloaded data
$preload_data = array_reduce(
$preload_paths,
'rest_preload_api_request',
array()
);
// Register the preloading middleware with wp-api-fetch
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
}
/**
* Enqueue scripts and styles for the jetpack-subscribers-announcement-wp-admin page.
* Hooked to admin_enqueue_scripts.
*
* @param string $hook_suffix The current admin page.
*/
function jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_enqueue_scripts( $hook_suffix ) {
// Check all possible ways this page can be accessed:
// 1. Menu page via admin.php?page=jetpack-subscribers-announcement-wp-admin (plugin)
// 2. Direct file via jetpack-subscribers-announcement.php (Core) - screen ID will be 'jetpack-subscribers-announcement'
$current_screen = get_current_screen();
$is_our_page = (
( isset( $_GET['page'] ) && 'jetpack-subscribers-announcement-wp-admin' === $_GET['page'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
( $current_screen && 'jetpack-subscribers-announcement' === $current_screen->id )
);
if ( ! $is_our_page ) {
return;
}
// Load build constants
$build_constants = require __DIR__ . '/../../constants.php';
/**
* Fires when the jetpack-subscribers-announcement admin page is initialized so extensions can register routes and menu items.
*/
do_action( 'jetpack-subscribers-announcement-wp-admin_init' );
// Preload REST API data
jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_preload_data();
// Get all registered routes
$routes = jetpack_newsletter_get_jetpack_subscribers_announcement_wp_admin_routes();
// Get boot module asset file for dependencies
$asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
if ( file_exists( $asset_file ) ) {
$asset = require $asset_file;
// This script serves two purposes:
// 1. It ensures all the globals that are made available to the modules are loaded.
// 2. It initializes the boot module as an inline script.
wp_register_script( 'jetpack-subscribers-announcement-wp-admin-prerequisites', '', $asset['dependencies'], $asset['version'], true );
/*
* Add inline script to initialize the app using initSinglePage (no menuItems).
* The dynamic import is deferred until DOMContentLoaded so that all classic
* script dependencies of @wordpress/boot (wp-private-apis, wp-components,
* wp-theme, etc.) have finished parsing and executing before the boot module
* evaluates. Otherwise, a modulepreloaded @wordpress/boot can win the race
* against the classic-script-printing pass on fast CDN-fronted hosts in
* Chrome, evaluating before wp.theme.privateApis is defined and throwing
* "Cannot unlock an undefined object". See <https://core.trac.wordpress.org/ticket/65103>.
*/
$init_js_function = <<<'JS'
( mountId, routes ) => {
const run = async () => {
const mod = await import( "@wordpress/boot" );
mod.initSinglePage( { mountId, routes } );
};
if ( document.readyState === "loading" ) {
document.addEventListener( "DOMContentLoaded", run );
} else {
run();
}
}
JS;
wp_add_inline_script(
'jetpack-subscribers-announcement-wp-admin-prerequisites',
sprintf(
'( %s )( %s, %s );',
$init_js_function,
wp_json_encode( 'jetpack-subscribers-announcement-wp-admin-app', JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
)
);
// Register prerequisites style by filtering script dependencies to find registered styles
$style_dependencies = array_filter(
$asset['dependencies'],
function ( $handle ) {
return wp_style_is( $handle, 'registered' );
}
);
wp_register_style( 'jetpack-subscribers-announcement-wp-admin-prerequisites', false, $style_dependencies, $asset['version'] );
// Build dependencies for jetpack-subscribers-announcement-wp-admin module
$boot_dependencies = array(
array(
'import' => 'static',
'id' => '@wordpress/boot',
),
);
// Add all registered routes as dependencies
foreach ( $routes as $route ) {
if ( isset( $route['route_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'static',
'id' => $route['route_module'],
);
}
if ( isset( $route['content_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'dynamic',
'id' => $route['content_module'],
);
}
}
/**
* Filters the boot script-module dependencies for the
* jetpack-subscribers-announcement-wp-admin page.
*
* Surfaces extending this page can append entries to the boot
* dependency list. Each entry is an array with 'import' (string
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
*
* @param array $boot_dependencies Boot dependencies for the page.
*/
$boot_dependencies = apply_filters(
'jetpack-subscribers-announcement-wp-admin_boot_dependencies',
$boot_dependencies
);
// Dummy script module to ensure dependencies are loaded
wp_register_script_module(
'jetpack-subscribers-announcement-wp-admin',
$build_constants['build_url'] . 'pages/jetpack-subscribers-announcement/loader.js',
$boot_dependencies
);
// Enqueue the boot scripts and styles
wp_enqueue_script( 'jetpack-subscribers-announcement-wp-admin-prerequisites' );
wp_enqueue_script_module( 'jetpack-subscribers-announcement-wp-admin' );
wp_enqueue_style( 'jetpack-subscribers-announcement-wp-admin-prerequisites' );
}
}
/**
* Render the jetpack-subscribers-announcement-wp-admin page.
* Call this function from add_menu_page or add_submenu_page.
* This renders within the normal WordPress admin interface.
*/
function jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page() {
?>
<style>
/* Critical styles to prevent layout shifts - inlined for immediate application */
#wpwrap {
overflow-y: auto;
}
body {
background: #fff;
}
/* Reset wp-admin padding */
#wpcontent {
padding-inline-start: 0;
}
#wpbody-content {
padding-bottom: 0;
}
/* Hide legacy admin elements */
#wpbody-content > div:not(.boot-layout-container):not(#screen-meta) {
display: none;
}
#wpfooter {
display: none;
}
/* Accessibility regions */
.a11y-speak-region {
inset-inline-start: -1px;
top: -1px;
}
/* Admin menu indicators */
ul#adminmenu a.wp-has-current-submenu::after,
ul#adminmenu > li.current > a.current::after {
border-inline-end-color: #fff;
}
/* Media frame fix */
.media-frame select.attachment-filters:last-of-type {
width: auto;
max-width: 100%;
}
/* Responsive overflow fix for #wpwrap */
@media (min-width: 782px) {
#wpwrap {
overflow-y: initial;
}
}
</style>
<div id="jetpack-subscribers-announcement-wp-admin-app" class="boot-layout-container"></div>
<?php
}
// Hook the enqueue function to admin_enqueue_scripts
add_action( 'admin_enqueue_scripts', 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_enqueue_scripts' );
@@ -0,0 +1,318 @@
<?php
/**
* Page: jetpack-subscribers-announcement
* Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
// Global storage for jetpack-subscribers-announcement routes and menu items
global $jetpack_newsletter_jetpack_subscribers_announcement_routes, $jetpack_newsletter_jetpack_subscribers_announcement_menu_items;
$jetpack_newsletter_jetpack_subscribers_announcement_routes = array();
$jetpack_newsletter_jetpack_subscribers_announcement_menu_items = array();
/**
* Register a route for the jetpack-subscribers-announcement page.
*
* @param string $path Route path (e.g., '/types/$type/edit/$id').
* @param string|null $content_module Script module ID for content (stage/inspector).
* @param string|null $route_module Script module ID for route lifecycle hooks.
*/
function jetpack_newsletter_register_jetpack_subscribers_announcement_route( $path, $content_module = null, $route_module = null ) {
global $jetpack_newsletter_jetpack_subscribers_announcement_routes;
$route = array( 'path' => $path );
if ( ! empty( $content_module ) ) {
$route['content_module'] = $content_module;
}
if ( ! empty( $route_module ) ) {
$route['route_module'] = $route_module;
}
$jetpack_newsletter_jetpack_subscribers_announcement_routes[] = $route;
}
/**
* Register a menu item for the jetpack-subscribers-announcement page.
*
* @param string $id Menu item ID.
* @param string $label Display label.
* @param string $to Route path to navigate to.
* @param string $parent_id Optional. Parent menu item ID.
* @param string $parent_type Optional. Parent type: 'drilldown' or 'dropdown'.
*/
function jetpack_newsletter_register_jetpack_subscribers_announcement_menu_item( $id, $label, $to, $parent_id = '', $parent_type = '' ) {
global $jetpack_newsletter_jetpack_subscribers_announcement_menu_items;
$menu_item = array(
'id' => $id,
'label' => $label,
'to' => $to,
);
if ( ! empty( $parent_id ) ) {
$menu_item['parent'] = $parent_id;
}
if ( ! empty( $parent_type ) && in_array( $parent_type, array( 'drilldown', 'dropdown' ), true ) ) {
$menu_item['parent_type'] = $parent_type;
}
$jetpack_newsletter_jetpack_subscribers_announcement_menu_items[] = $menu_item;
}
/**
* Get all registered routes for the jetpack-subscribers-announcement page.
*
* @return array Array of route objects.
*/
function jetpack_newsletter_get_jetpack_subscribers_announcement_routes() {
global $jetpack_newsletter_jetpack_subscribers_announcement_routes;
return $jetpack_newsletter_jetpack_subscribers_announcement_routes ?? array();
}
/**
* Get all registered menu items for the jetpack-subscribers-announcement page.
*
* @return array Array of menu item objects.
*/
function jetpack_newsletter_get_jetpack_subscribers_announcement_menu_items() {
global $jetpack_newsletter_jetpack_subscribers_announcement_menu_items;
return $jetpack_newsletter_jetpack_subscribers_announcement_menu_items ?? array();
}
/**
* Preload REST API data for the jetpack-subscribers-announcement page.
* Automatically called during page rendering.
*/
function jetpack_newsletter_jetpack_subscribers_announcement_preload_data() {
// Define paths to preload - same for all pages
// Please also change packages/core-data/src/entities.js when changing this.
$preload_paths = array(
'/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
array( '/wp/v2/settings', 'OPTIONS' ),
);
// Use rest_preload_api_request to gather the preloaded data
$preload_data = array_reduce(
$preload_paths,
'rest_preload_api_request',
array()
);
// Register the preloading middleware with wp-api-fetch
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
}
/**
* Render the jetpack-subscribers-announcement page.
* Call this function from add_menu_page or add_submenu_page.
*/
function jetpack_newsletter_jetpack_subscribers_announcement_render_page() {
// Load build constants
$build_constants = require __DIR__ . '/../../constants.php';
// Set current screen
set_current_screen();
// Remove unwanted deprecated handler
remove_action( 'admin_head', 'wp_admin_bar_header' );
// Remove unwanted scripts and styles that were enqueued during `admin_init`
foreach ( wp_scripts()->queue as $script ) {
wp_dequeue_script( $script );
}
foreach ( wp_styles()->queue as $style ) {
wp_dequeue_style( $style );
}
/**
* Fires when the jetpack-subscribers-announcement page is initialized so extensions can register routes and menu items.
*/
do_action( 'jetpack-subscribers-announcement_init' );
// Enqueue command palette assets for boot-based pages
if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
wp_enqueue_command_palette_assets();
}
// Preload REST API data
jetpack_newsletter_jetpack_subscribers_announcement_preload_data();
// Get all registered routes and menu items
$menu_items = jetpack_newsletter_get_jetpack_subscribers_announcement_menu_items();
$routes = jetpack_newsletter_get_jetpack_subscribers_announcement_routes();
// Get boot module asset file for dependencies
$asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
if ( file_exists( $asset_file ) ) {
$asset = require $asset_file;
// This script serves two purposes:
// 1. It ensures all the globals that are made available to the modules are loaded.
// 2. It initializes the boot module as an inline script.
wp_register_script( 'jetpack-subscribers-announcement-prerequisites', '', $asset['dependencies'], $asset['version'], true );
// Add inline script to initialize the app
$init_modules = [];
wp_add_inline_script(
'jetpack-subscribers-announcement-prerequisites',
sprintf(
'import("@wordpress/boot").then(mod => mod.init({mountId: "%s", menuItems: %s, routes: %s, initModules: %s, dashboardLink: "%s"}));',
'jetpack-subscribers-announcement-app',
wp_json_encode( $menu_items, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
wp_json_encode( $init_modules, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
esc_url( admin_url( '/' ) )
)
);
// Register prerequisites style by filtering script dependencies to find registered styles
$style_dependencies = array_filter(
$asset['dependencies'],
function ( $handle ) {
return wp_style_is( $handle, 'registered' );
}
);
wp_register_style( 'jetpack-subscribers-announcement-prerequisites', false, $style_dependencies, $asset['version'] );
// Build dependencies for jetpack-subscribers-announcement module
$boot_dependencies = array(
array(
'import' => 'static',
'id' => '@wordpress/boot',
),
);
// Add init modules as static dependencies
// No init modules configured
// Add all registered routes as dependencies
foreach ( $routes as $route ) {
if ( isset( $route['route_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'static',
'id' => $route['route_module'],
);
}
if ( isset( $route['content_module'] ) ) {
$boot_dependencies[] = array(
'import' => 'dynamic',
'id' => $route['content_module'],
);
}
}
/**
* Filters the boot script-module dependencies for the
* jetpack-subscribers-announcement page.
*
* Surfaces extending this page can append entries to the boot
* dependency list. Each entry is an array with 'import' (string
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
*
* @param array $boot_dependencies Boot dependencies for the page.
*/
$boot_dependencies = apply_filters(
'jetpack-subscribers-announcement_boot_dependencies',
$boot_dependencies
);
// Dummy script module to ensure dependencies are loaded
wp_register_script_module(
'jetpack-subscribers-announcement',
$build_constants['build_url'] . 'pages/jetpack-subscribers-announcement/loader.js',
$boot_dependencies
);
// Enqueue the boot scripts and styles
wp_enqueue_script( 'jetpack-subscribers-announcement-prerequisites' );
wp_enqueue_script_module( 'jetpack-subscribers-announcement' );
wp_enqueue_style( 'jetpack-subscribers-announcement-prerequisites' );
}
// Output the HTML
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo esc_html( get_admin_page_title() ); ?></title>
<style>
html {
background: #f1f1f1;
color: #444;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.4em;
}
body {
margin: 0;
}
#wpadminbar { display: none; }
</style>
<?php
global $hook_suffix;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$hook_suffix = 'jetpack-subscribers-announcement';
// BEGIN see wp-admin/admin-header.php
print_admin_styles();
print_head_scripts();
/** This action is documented in wp-admin/admin-header.php */
do_action( "admin_head-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_head' );
// END see wp-admin/admin-header.php
?>
</head>
<body class="jetpack-subscribers-announcement">
<div id="jetpack-subscribers-announcement-app" style="height: 100vh; box-sizing: border-box;"></div>
<?php
// BEGIN see wp-admin/admin-footer.php
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_footer', '' );
// Print import map first so it's available for inline scripts
wp_script_modules()->print_import_map();
print_footer_scripts();
wp_script_modules()->print_enqueued_script_modules();
wp_script_modules()->print_script_module_preloads();
wp_script_modules()->print_script_module_data();
/** This action is documented in wp-admin/admin-footer.php */
do_action( "admin_footer-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
// END see wp-admin/admin-footer.php
?>
</body>
</html>
<?php
exit;
}
/**
* Intercept admin_init to render the page early.
* This bypasses the default WordPress admin template.
*/
function jetpack_newsletter_jetpack_subscribers_announcement_intercept_render() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['page'] ) && 'jetpack-subscribers-announcement' === $_GET['page'] ) {
jetpack_newsletter_jetpack_subscribers_announcement_render_page();
exit;
}
}
// Hook the interceptor to admin_init
add_action( 'admin_init', 'jetpack_newsletter_jetpack_subscribers_announcement_intercept_render' );
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '6569e23e23e0349e92b7');
@@ -0,0 +1 @@
@media(max-width:360px){#wpadminbar .wp-admin-bar-reader{display:none!important}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-item{padding:0}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-item{display:flex;justify-content:center}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon{align-items:center;display:flex;height:100%;margin:0;padding:0 6px}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon{justify-content:center;padding:0}}@media(max-width:480px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon{max-width:40px}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon:before{background-color:currentColor;content:"";display:flex;height:20px;margin:0;mask-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="11" fill="none" viewBox="0 0 24 11"><path fill="%23000" d="m22.875 4.607-.055-.25C22.335 2.175 20.276.584 17.924.584c-1.271 0-2.488.447-3.412 1.264-.32.283-.568.593-.769.894a9.5 9.5 0 0 0-1.711-.18h-.027c-.586 0-1.163.085-1.73.188a4.7 4.7 0 0 0-.777-.91C8.573 1.013 7.356.575 6.085.575 3.724.584 1.665 2.174 1.18 4.358l-.055.249H0v1.4h1.125l.055.25c.458 2.183 2.517 3.773 4.895 3.773 2.764 0 5.005-2.114 5.005-4.718 0 0 .01-.585-.228-1.341a9 9 0 0 1 1.153-.095c.366 0 .75.035 1.143.086-.247.765-.229 1.35-.229 1.35 0 2.604 2.242 4.718 5.005 4.718 2.38 0 4.438-1.581 4.896-3.773l.055-.25H24v-1.4zm-16.8 4.022c-1.94 0-3.522-1.487-3.522-3.326 0-1.84 1.583-3.326 3.522-3.326 1.94 0 3.523 1.487 3.523 3.326 0 1.84-1.583 3.326-3.523 3.326m11.85 0c-1.94 0-3.523-1.487-3.523-3.326 0-1.84 1.583-3.326 3.522-3.326 1.94 0 3.523 1.487 3.523 3.326 0 1.84-1.583 3.326-3.523 3.326"/></svg>');mask-position:center;mask-repeat:no-repeat;width:24px}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon:before{height:36px;margin:0;mask-size:contain;width:36px}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-label{padding-inline-end:8px}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-label{display:none}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader{display:block}}@media(max-width:480px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader{width:46px}}
@@ -0,0 +1 @@
@media(max-width:360px){#wpadminbar .wp-admin-bar-reader{display:none!important}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-item{padding:0}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-item{display:flex;justify-content:center}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon{align-items:center;display:flex;height:100%;margin:0;padding:0 6px}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon{justify-content:center;padding:0}}@media(max-width:480px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon{max-width:40px}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon:before{background-color:currentColor;content:"";display:flex;height:20px;margin:0;mask-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="11" fill="none" viewBox="0 0 24 11"><path fill="%23000" d="m22.875 4.607-.055-.25C22.335 2.175 20.276.584 17.924.584c-1.271 0-2.488.447-3.412 1.264-.32.283-.568.593-.769.894a9.5 9.5 0 0 0-1.711-.18h-.027c-.586 0-1.163.085-1.73.188a4.7 4.7 0 0 0-.777-.91C8.573 1.013 7.356.575 6.085.575 3.724.584 1.665 2.174 1.18 4.358l-.055.249H0v1.4h1.125l.055.25c.458 2.183 2.517 3.773 4.895 3.773 2.764 0 5.005-2.114 5.005-4.718 0 0 .01-.585-.228-1.341a9 9 0 0 1 1.153-.095c.366 0 .75.035 1.143.086-.247.765-.229 1.35-.229 1.35 0 2.604 2.242 4.718 5.005 4.718 2.38 0 4.438-1.581 4.896-3.773l.055-.25H24v-1.4zm-16.8 4.022c-1.94 0-3.522-1.487-3.522-3.326 0-1.84 1.583-3.326 3.522-3.326 1.94 0 3.523 1.487 3.523 3.326 0 1.84-1.583 3.326-3.523 3.326m11.85 0c-1.94 0-3.523-1.487-3.523-3.326 0-1.84 1.583-3.326 3.522-3.326 1.94 0 3.523 1.487 3.523 3.326 0 1.84-1.583 3.326-3.523 3.326"/></svg>');mask-position:center;mask-repeat:no-repeat;width:24px}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-icon:before{height:36px;margin:0;mask-size:contain;width:36px}}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-label{padding-inline-end:8px}@media(max-width:782px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader .ab-label{display:none}#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader{display:block}}@media(max-width:480px){#wpadminbar #wp-admin-bar-top-secondary #wp-admin-bar-reader{width:46px}}
@@ -0,0 +1,132 @@
<?php
/**
* Routes registration - Auto-generated by build process.
* Registers all routes on their respective page init hooks.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
// Load routes registry
$routes_file = __DIR__ . '/routes/registry.php';
if ( ! file_exists( $routes_file ) ) {
return;
}
$routes = require $routes_file;
// Group routes by page and store in globals for page-specific functions
$routes_by_page = array();
foreach ( $routes as $route ) {
$page_slug = $route['page'];
if ( ! isset( $routes_by_page[ $page_slug ] ) ) {
$routes_by_page[ $page_slug ] = array();
}
$routes_by_page[ $page_slug ][] = $route;
}
// Store routes data in globals for each page
foreach ( $routes_by_page as $page_slug => $page_routes ) {
$page_slug_underscore = str_replace( '-', '_', $page_slug );
$global_name = 'jetpack_newsletter_' . $page_slug_underscore . '_routes_data';
$GLOBALS[ $global_name ] = $page_routes;
}
/**
* Generic helper function to register routes for a page.
*
* @param array $page_routes Array of route data for the page.
* @param string $register_function_name Name of the function to call for registering each route.
*/
function jetpack_newsletter_register_page_routes( $page_routes, $register_function_name ) {
// Load build constants
$build_constants = require __DIR__ . '/constants.php';
foreach ( $page_routes as $route ) {
$content_handle = null;
$route_handle = null;
// Register content module if exists
if ( $route['has_content'] ) {
$content_asset_path = __DIR__ . "/routes/{$route['name']}/content.min.asset.php";
if ( file_exists( $content_asset_path ) ) {
$content_asset = require $content_asset_path;
$content_handle = 'jetpack-newsletter/routes/' . $route['name'] . '/content';
$extension = '.min.js';
// Deregister first to override any previously registered version
// (e.g., Core's default modules when running as a plugin).
wp_deregister_script_module( $content_handle );
wp_register_script_module(
$content_handle,
$build_constants['build_url'] . 'routes/' . $route['name'] . '/content' . $extension,
$content_asset['module_dependencies'] ?? array(),
$content_asset['version'] ?? false
);
}
}
// Register route module if exists
if ( $route['has_route'] ) {
$route_asset_path = __DIR__ . "/routes/{$route['name']}/route.min.asset.php";
if ( file_exists( $route_asset_path ) ) {
$route_asset = require $route_asset_path;
$route_handle = 'jetpack-newsletter/routes/' . $route['name'] . '/route';
$extension = '.min.js';
// Deregister first to override any previously registered version
// (e.g., Core's default modules when running as a plugin).
wp_deregister_script_module( $route_handle );
wp_register_script_module(
$route_handle,
$build_constants['build_url'] . 'routes/' . $route['name'] . '/route' . $extension,
$route_asset['module_dependencies'] ?? array(),
$route_asset['version'] ?? false
);
}
}
// Register route with page
if ( function_exists( $register_function_name ) ) {
call_user_func( $register_function_name, $route['path'], $content_handle, $route_handle );
}
}
}
// Page-specific route registration functions
// Page-specific route registration functions for jetpack-newsletter-dashboard
/**
* Register routes for jetpack-newsletter-dashboard page (full-page mode).
*/
function jetpack_newsletter_register_jetpack_newsletter_dashboard_page_routes() {
global $jetpack_newsletter_jetpack_newsletter_dashboard_routes_data;
jetpack_newsletter_register_page_routes( $jetpack_newsletter_jetpack_newsletter_dashboard_routes_data, 'jetpack_newsletter_register_jetpack_newsletter_dashboard_route' );
}
add_action( 'jetpack-newsletter-dashboard_init', 'jetpack_newsletter_register_jetpack_newsletter_dashboard_page_routes' );
/**
* Register routes for jetpack-newsletter-dashboard page (wp-admin mode).
*/
function jetpack_newsletter_register_jetpack_newsletter_dashboard_wp_admin_page_routes() {
global $jetpack_newsletter_jetpack_newsletter_dashboard_routes_data;
jetpack_newsletter_register_page_routes( $jetpack_newsletter_jetpack_newsletter_dashboard_routes_data, 'jetpack_newsletter_register_jetpack_newsletter_dashboard_wp_admin_route' );
}
add_action( 'jetpack-newsletter-dashboard-wp-admin_init', 'jetpack_newsletter_register_jetpack_newsletter_dashboard_wp_admin_page_routes' );
// Page-specific route registration functions for jetpack-subscribers-announcement
/**
* Register routes for jetpack-subscribers-announcement page (full-page mode).
*/
function jetpack_newsletter_register_jetpack_subscribers_announcement_page_routes() {
global $jetpack_newsletter_jetpack_subscribers_announcement_routes_data;
jetpack_newsletter_register_page_routes( $jetpack_newsletter_jetpack_subscribers_announcement_routes_data, 'jetpack_newsletter_register_jetpack_subscribers_announcement_route' );
}
add_action( 'jetpack-subscribers-announcement_init', 'jetpack_newsletter_register_jetpack_subscribers_announcement_page_routes' );
/**
* Register routes for jetpack-subscribers-announcement page (wp-admin mode).
*/
function jetpack_newsletter_register_jetpack_subscribers_announcement_wp_admin_page_routes() {
global $jetpack_newsletter_jetpack_subscribers_announcement_routes_data;
jetpack_newsletter_register_page_routes( $jetpack_newsletter_jetpack_subscribers_announcement_routes_data, 'jetpack_newsletter_register_jetpack_subscribers_announcement_wp_admin_route' );
}
add_action( 'jetpack-subscribers-announcement-wp-admin_init', 'jetpack_newsletter_register_jetpack_subscribers_announcement_wp_admin_page_routes' );
@@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-notices', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url', 'wp-warning'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => 'aaece2843ec8199bb63f');
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '214f7487552bc80b234d');
@@ -0,0 +1 @@
var e={inspector:({search:r})=>r?.tab==="settings"?!1:!!(r?.subscriber||r?.u)};export{e as route};
@@ -0,0 +1,24 @@
<?php
/**
* Route registry - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
return array(
array(
'name' => 'dashboard',
'path' => '/',
'page' => 'jetpack-newsletter-dashboard',
'has_route' => true,
'has_content' => true,
),
array(
'name' => 'subscribers-announcement',
'path' => '/',
'page' => 'jetpack-subscribers-announcement',
'has_route' => false,
'has_content' => true,
)
);
@@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => 'ad0755c97021548579db');
@@ -0,0 +1,85 @@
<?php
/**
* Script registration - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
/**
* Registers a script according to `wp_register_script`. Honors this request by
* reassigning internal dependency properties of any script handle already
* registered by that name. It does not deregister the original script, to
* avoid losing inline scripts which may have been attached.
*
* @param WP_Scripts $scripts WP_Scripts instance.
* @param string $handle Name of the script. Should be unique.
* @param string $src Full URL of the script, or path of the script relative to the WordPress root directory.
* @param array $deps Optional. An array of registered script handles this script depends on. Default empty array.
* @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version.
* If set to null, no version is added.
* @param bool $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>.
* Default 'false'.
*/
function jetpack_newsletter_override_script( $scripts, $handle, $src, $deps = array(), $ver = false, $in_footer = false ) {
$script = $scripts->query( $handle, 'registered' );
if ( $script ) {
/*
* In many ways, this is a reimplementation of `wp_register_script` but
* bypassing consideration of whether a script by the given handle had
* already been registered.
*/
// See: `_WP_Dependency::__construct` .
$script->src = $src;
$script->deps = $deps;
$script->ver = $ver;
$script->args = $in_footer ? 1 : null;
} else {
$scripts->add( $handle, $src, $deps, $ver, ( $in_footer ? 1 : null ) );
}
if ( in_array( 'wp-i18n', $deps, true ) ) {
$scripts->set_translations( $handle );
}
}
/**
* Register all package scripts.
*/
function jetpack_newsletter_register_package_scripts( $scripts ) {
// Load build constants
$build_constants = require __DIR__ . '/constants.php';
$default_version = ! SCRIPT_DEBUG ? $build_constants['version'] : time();
$extension = '.min.js';
$scripts_dir = __DIR__ . '/scripts';
$scripts_file = $scripts_dir . '/registry.php';
if ( ! file_exists( $scripts_file ) ) {
return;
}
$scripts_data = require $scripts_file;
$plugin_dir = dirname( __FILE__ );
foreach ( $scripts_data as $script_data ) {
$asset_file = $scripts_dir . '/' . $script_data['asset'];
$asset = file_exists( $asset_file ) ? require $asset_file : array();
$dependencies = $asset['dependencies'] ?? array();
$version = $asset['version'] ?? $default_version;
jetpack_newsletter_override_script(
$scripts,
$script_data['handle'],
$build_constants['build_url'] . 'scripts/' . $script_data['path'] . $extension,
$dependencies,
$version,
true
);
}
}
add_action( 'wp_default_scripts', 'jetpack_newsletter_register_package_scripts' );
@@ -0,0 +1,11 @@
<?php
/**
* Script registry - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
return array(
);
@@ -0,0 +1,73 @@
<?php
/**
* Style registration - Auto-generated by build process.
* Do not edit this file manually.
*
* Note: These styles can be overridden by calling jetpack_newsletter_override_style()
* in lib/client-assets.php for complex cases (multiple files, conditional deps, etc.)
*
* @package jetpack_newsletter
*/
/**
* Registers a style according to `wp_register_style`. Honors this request by
* deregistering any style by the same handler before registration.
*
* @param WP_Styles $styles WP_Styles instance.
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string|bool|null $ver Optional. String specifying stylesheet version number, if it has one, which is added to the URL
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version.
* If set to null, no version is added.
* @param string $media Optional. The media for which this stylesheet has been defined.
* Default 'all'. Accepts media types like 'all', 'print' and 'screen', or media queries like
* '(orientation: portrait)' and '(max-width: 640px)'.
*/
function jetpack_newsletter_override_style( $styles, $handle, $src, $deps = array(), $ver = false, $media = 'all' ) {
$style = $styles->query( $handle, 'registered' );
if ( $style ) {
$styles->remove( $handle );
}
$styles->add( $handle, $src, $deps, $ver, $media );
}
/**
* Register all package styles (simple cases only).
* Complex cases should be manually registered in lib/client-assets.php.
*
* @param WP_Styles $styles WP_Styles instance.
*/
function jetpack_newsletter_register_package_styles( $styles ) {
// Load build constants
$build_constants = require __DIR__ . '/constants.php';
$default_version = ! SCRIPT_DEBUG ? $build_constants['version'] : time();
$suffix = '.min';
$styles_dir = __DIR__ . '/styles';
$styles_file = $styles_dir . '/registry.php';
if ( ! file_exists( $styles_file ) ) {
return;
}
$styles_data = require $styles_file;
$plugin_dir = dirname( __FILE__ );
foreach ( $styles_data as $style_data ) {
jetpack_newsletter_override_style(
$styles,
$style_data['handle'],
$build_constants['build_url'] . 'styles/' . $style_data['path'] . $suffix . '.css',
$style_data['dependencies'],
$default_version
);
// Enable RTL support (WordPress automatically loads -rtl.css variant)
$styles->add_data( $style_data['handle'], 'rtl', 'replace' );
$styles->add_data( $style_data['handle'], 'suffix', $suffix );
}
}
add_action( 'wp_default_styles', 'jetpack_newsletter_register_package_styles' );
@@ -0,0 +1,11 @@
<?php
/**
* Style registry - Auto-generated by build process.
* Do not edit this file manually.
*
* @package jetpack_newsletter
*/
return array(
);
@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-script-data', 'react', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-element', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => '2225f15b2a464a861605');
@@ -0,0 +1 @@
.wpcom-daily-writing-prompt--prompt{background-color:var(--wpds-color-bg-surface-neutral-weak);border-radius:var(--wpds-border-radius-md);padding:var(--wpds-dimension-padding-md)}.wpcom-daily-writing-prompt--branding{border-block-start:var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak);padding-block-start:var(--wpds-dimension-padding-md)}.wpcom-daily-writing-prompt--answered-users a{color:var(--wp-ui-button-foreground-color)}.wpcom-daily-writing-prompt--answered-users img{border:var(--wpds-border-width-sm) solid var(--wpds-color-bg-surface-neutral);border-radius:50%;margin-inline-start:calc((var(--wpds-dimension-gap-sm) + var(--wpds-border-width-sm))*-1);vertical-align:middle}.wpcom-daily-writing-prompt--answered-users img:first-child{margin-inline-start:0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.wpcom-daily-writing-prompt--prompt{background-color:var(--wpds-color-bg-surface-neutral-weak);border-radius:var(--wpds-border-radius-md);padding:var(--wpds-dimension-padding-md)}.wpcom-daily-writing-prompt--branding{border-block-start:var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak);padding-block-start:var(--wpds-dimension-padding-md)}.wpcom-daily-writing-prompt--answered-users a{color:var(--wp-ui-button-foreground-color)}.wpcom-daily-writing-prompt--answered-users img{border:var(--wpds-border-width-sm) solid var(--wpds-color-bg-surface-neutral);border-radius:50%;margin-inline-start:calc((var(--wpds-dimension-gap-sm) + var(--wpds-border-width-sm))*-1);vertical-align:middle}.wpcom-daily-writing-prompt--answered-users img:first-child{margin-inline-start:0}
@@ -0,0 +1,130 @@
<?php
/**
* A class that adds a link to the WordPress.com Reader to the admin bar.
*
* @package automattic/jetpack-newsletter
*/
namespace Automattic\Jetpack\Newsletter;
use Automattic\Jetpack\Connection\Urls;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Status\Host;
use WP_Admin_Bar;
/**
* Add a link to the WordPress.com Reader to the admin bar.
*/
class Reader_Link {
/**
* Whether the class has been initialized.
*
* @var bool
*/
private static $initialized = false;
/**
* Initialize the Reader Link functionality.
*
* This method sets up all necessary hooks for the Reader menu item
* and its associated styles. It can be called multiple times safely
* as it will only initialize once.
*
* @since 0.4.0
*
* @return void
*/
public static function init() {
if ( self::$initialized ) {
return;
}
if ( ! is_user_logged_in() || ! is_admin_bar_showing() ) {
return;
}
self::$initialized = true;
$instance = new self();
add_action( 'admin_bar_menu', array( $instance, 'add_reader_menu' ), 11 );
add_action( 'wp_enqueue_scripts', array( $instance, 'enqueue_stylesheet' ) );
add_action( 'admin_enqueue_scripts', array( $instance, 'enqueue_stylesheet' ) );
}
/**
* Enqueue the stylesheet used to display the Reader icon.
*
* @since 0.4.0
*
* @return void
*/
public function enqueue_stylesheet() {
$build_path = dirname( __DIR__ ) . '/build/reader-link.css';
if ( ! file_exists( $build_path ) ) {
return;
}
$asset_file = dirname( __DIR__ ) . '/build/reader-link.asset.php';
$version = file_exists( $asset_file )
? include $asset_file
: array( 'version' => filemtime( $build_path ) );
wp_enqueue_style(
'jetpack-newsletter-reader-link',
plugins_url( '../build/reader-link.css', __FILE__ ),
array(),
$version['version'] ?? filemtime( $build_path )
);
}
/**
* Add the Reader menu.
*
* Hook into 'admin_bar_menu' to add to the wp-admin bar.
*
* @since 0.4.0
*
* @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar core object.
*/
public function add_reader_menu( $wp_admin_bar ) {
$reader_menu_settings = array(
'id' => 'reader',
'title' => '<span class="ab-icon" title="' . __( 'Read the blogs and topics you follow', 'jetpack-newsletter' ) . '" aria-hidden="true"></span>' .
'<span class="ab-label">' . __( 'Reader', 'jetpack-newsletter' ) . '</span>',
'href' => method_exists( Urls::class, 'maybe_add_origin_site_id' )
? Urls::maybe_add_origin_site_id( 'https://wordpress.com/reader' )
: 'https://wordpress.com/reader',
'meta' => array(
'class' => 'wp-admin-bar-reader',
),
'parent' => 'top-secondary',
);
/*
* On self-hosted sites, open the Reader link in a new tab
* since they're not necessarily logged in to WordPress.com
* and may not want to navigate away from their site.
*/
if ( ! ( new Host() )->is_wpcom_platform() ) {
$reader_menu_settings['meta']['target'] = '_blank';
}
$wp_admin_bar->add_menu( $reader_menu_settings );
}
/**
* Activate the wpcom-reader module when a site is first connected to WordPress.com.
*
* Only activates on truly fresh connections. If modules were previously initialized
* (e.g., the user disconnected and reconnected), we respect their prior module choices.
*
* @since 0.4.0
*/
public static function activate_on_connection() {
if ( \Jetpack_Options::get_option( 'active_modules_initialized' ) ) {
return;
}
( new Modules() )->activate( 'wpcom-reader', false, false );
}
}
@@ -0,0 +1,604 @@
<?php
/**
* A class that adds a newsletter settings screen to wp-admin.
*
* @package automattic/jetpack-newsletter
*/
namespace Automattic\Jetpack\Newsletter;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Status\Visitor;
use Jetpack_Tracks_Client;
/**
* A class responsible for adding a newsletter settings screen to wp-admin.
*/
class Settings {
const PACKAGE_VERSION = '0.11.1';
const ADMIN_PAGE_SLUG = 'jetpack-newsletter';
/**
* Filter name that gates the wp-buildbased dashboard.
*
* When this filter returns true, "Jetpack > Newsletter" renders the new
* wp-build dashboard instead of the legacy Newsletter Settings React app.
*/
const MODERNIZATION_FILTER = 'rsm_jetpack_ui_modernization_newsletter';
/**
* Percentage of sites the modernized Newsletter experience defaults on for
* during the staged rollout.
*
* Currently 0: the Simple-site rollout is driven from the WordPress.com backend
* instead (a server-side feature-flag value that can be rolled back instantly),
* so the Jetpack-side cohort is held at zero. The cohort code stays in place for
* Atomic (WoA) and self-hosted Jetpack sites; bumping this single number is the
* one-line change that widens the rollout once the Simple cohort is validated.
* Automatticians get the experience regardless of this percentage.
*
* Sites are bucketed deterministically by their wpcom blog ID, so every gate
* that reads this lands on the same answer for a given site.
*/
const MODERNIZATION_ROLLOUT_PERCENTAGE = 0;
/**
* Whether the class has been initialized
*
* @var boolean
*/
private static $initialized = false;
/**
* Init Newsletter Settings if it wasn't already.
*/
public static function init() {
if ( ! self::$initialized ) {
self::$initialized = true;
( new self() )->init_hooks();
}
}
/**
* Check if the subscriptions module is active.
*
* @return bool
*/
private function is_subscriptions_active() {
return ( new Modules() )->is_active( 'subscriptions' );
}
/**
* Determine whether to show the Newsletter menu item.
* When true, shown regardless of subscriptions module state.
*
* @return bool
*/
private function should_show_menu_item() {
/**
* Filter to control Newsletter menu item visibility.
* Defaults to true.
*
* @since 0.6.0
* @param bool $show Whether to show the menu item.
*/
return apply_filters(
'jetpack_show_newsletter_menu_item',
true
);
}
/**
* Subscribe to necessary hooks.
*/
public function init_hooks() {
// Transitional Subscribers announcement page (active only while the
// modernization filter is on): registers its AJAX/admin-post handlers
// and wp-build loading here so they exist on admin-ajax.php and
// admin-post.php requests. The menu itself is added by the Jetpack
// plugin's subscriptions module, which owns the Subscribers placement.
Subscribers_Announcement::init();
// Add the Reading settings notice as long as subscriptions are active.
if ( $this->is_subscriptions_active() ) {
add_action( 'admin_init', array( $this, 'add_reading_page_notice' ) );
}
// Hijack the config URLs to point to our settings page.
// Priority 20 to override the default URL set in subscriptions.php.
add_filter(
'jetpack_module_configuration_url_subscriptions',
function () {
return Urls::get_newsletter_settings_url();
},
20
);
// Defer wp-build loading to admin_menu (priority 1) on every host. The
// modernization filter — which third parties typically register from a
// plugins_loaded callback — needs to have been applied before we read it,
// and the wp-build render function needs to be defined before any menu
// callback runs (priority 999 on standalone Jetpack, priority 999999 on
// wpcom Simple via wpcom-admin-menu.php's call to add_wp_admin_submenu).
// Settings::init() runs synchronously from load-jetpack.php at
// plugin-file-include time — before any plugins_loaded callback fires —
// so an inline check here would always see the unfiltered default.
add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
$host = new Host();
// On wpcom Simple, the Jetpack menu is created at priority 999999 by wpcom-admin-menu.php,
// which will call add_wp_admin_submenu() directly. Skip adding the menu here to avoid
// trying to add a submenu before the parent menu exists.
if ( $host->is_wpcom_simple() ) {
return;
}
// Add admin menu item.
// Use priority 999 to ensure menu items are queued BEFORE Admin_Menu::admin_menu_hook_callback
// runs at priority 1000 to process all queued items.
add_action( 'admin_menu', array( $this, 'add_wp_admin_menu' ), 999 );
}
/**
* Load wp-build for the Newsletter admin page when modernization is enabled.
*
* Hooked to `admin_menu` priority 1 so the modernization filter has been
* registered by any opt-in code (mu-plugins, snippets, themes) before we
* read it, and so the wp-build render function and enqueue hook are in
* place before `add_wp_admin_menu` runs at priority 999.
*
* @return void
*/
public static function maybe_load_wp_build() {
if ( ! self::is_modernized() || ! self::is_newsletter_admin_request() ) {
return;
}
self::load_wp_build();
add_action( 'current_screen', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
}
/**
* Add the newsletter settings submenu to the Jetpack menu.
*
* Note: This method is NOT called on wpcom Simple sites. Simple sites use
* add_wp_admin_submenu() called from wpcom-admin-menu.php instead.
*/
public function add_wp_admin_menu() {
// On sites using Jetpack, only show the menu if the site is connected.
if ( ! ( new Connection_Manager() )->is_connected() ) {
return;
}
// On the modernized dashboard, the Newsletter screen is only useful when the
// subscriptions module is active, so skip registering the menu entirely when it
// is off. Gated on the modernization flag to leave legacy behavior unchanged.
if ( self::is_modernized() && ! $this->is_subscriptions_active() ) {
return;
}
$host = new Host();
// should_show_menu_item() controls visibility of the menu item.
$show_menu = $this->should_show_menu_item();
$parent_slug = $show_menu ? 'jetpack' : '';
// On Atomic, use add_submenu_page. On standalone Jetpack, use Admin_Menu when showing in menu.
$use_jetpack_menu = ! $host->is_woa_site() && $show_menu;
$callback = self::is_modernized() && function_exists( 'jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_render_page' )
? 'jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_render_page'
: array( $this, 'render' );
// Register menu item.
if ( $use_jetpack_menu ) {
$page_suffix = Admin_Menu::add_menu(
/** "Newsletter" is a product name, do not translate. */
'Newsletter',
'Newsletter',
'manage_options',
'jetpack-newsletter',
$callback,
10
);
} else {
$page_suffix = add_submenu_page(
$parent_slug,
/** "Newsletter" is a product name, do not translate. */
'Newsletter',
'Newsletter',
'manage_options',
'jetpack-newsletter',
$callback
);
}
if ( $page_suffix ) {
add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
}
}
/**
* Add the newsletter settings submenu directly under the Jetpack menu.
*
* This method is called from wpcom-admin-menu.php on Simple sites at late priority
* (999999) when the Jetpack menu already exists.
*/
public function add_wp_admin_submenu() {
// On the modernized dashboard, the Newsletter screen is only useful when the
// subscriptions module is active, so skip registering the menu entirely when it
// is off. Gated on the modernization flag to leave legacy behavior unchanged.
if ( self::is_modernized() && ! $this->is_subscriptions_active() ) {
return;
}
$parent_slug = $this->should_show_menu_item() ? 'jetpack' : '';
$callback = self::is_modernized() && function_exists( 'jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_render_page' )
? 'jetpack_newsletter_jetpack_newsletter_dashboard_wp_admin_render_page'
: array( $this, 'render' );
$page_suffix = add_submenu_page(
$parent_slug,
/** "Newsletter" is a product name, do not translate. */
'Newsletter',
'Newsletter',
'manage_options',
'jetpack-newsletter',
$callback
);
if ( $page_suffix ) {
add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
}
}
/**
* Admin init actions.
*/
public function admin_init() {
add_filter( 'jetpack_admin_js_script_data', array( $this, 'add_script_data' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
}
/**
* Add newsletter-specific data to the global JetpackScriptData object.
*
* @param array $data The existing script data.
* @return array The modified script data.
*/
public function add_script_data( $data ) {
$current_user = wp_get_current_user();
$theme = wp_get_theme();
$host = new Host();
$status = new Status();
$site_suffix = $status->get_site_suffix();
$blog_id = (int) $host->get_wpcom_site_id();
$is_wpcom = $host->is_wpcom_platform();
$is_block_theme = wp_is_block_theme();
$setup_payment_plan_url = ( $is_wpcom ? 'https://wordpress.com/earn/payments/' : 'https://cloud.jetpack.com/monetize/payments/' ) . $site_suffix;
$wp_admin_subscriber_management_enabled = apply_filters( 'jetpack_wp_admin_subscriber_management_enabled', self::is_modernization_rollout_enabled() );
// Populate blog_id which is needed for API calls on Simple sites.
$data['site']['wpcom']['blog_id'] = $blog_id;
// Add newsletter-specific data.
// Note: Common data like admin_url, rest_nonce, rest_root, title, is_wpcom_platform,
// and user.current_user.display_name are already provided by Script_Data.
$data['newsletter'] = array(
'isBlockTheme' => $is_block_theme,
'themeStylesheet' => $theme->get_stylesheet(),
'email' => $current_user->user_email,
'gravatar' => get_avatar_url( $current_user->ID ),
'dateExample' => gmdate( get_option( 'date_format' ), time() ),
'subscriberManagementUrl' => $this->get_subscriber_management_url( $wp_admin_subscriber_management_enabled, $is_wpcom, $site_suffix, $blog_id ),
'subscriberManagementEnabled' => (bool) $wp_admin_subscriber_management_enabled,
'isSubscriptionSiteEditSupported' => $is_block_theme,
'setupPaymentPlansUrl' => $setup_payment_plan_url,
'isSitePublic' => ! $status->is_private_site() && ! $status->is_coming_soon(),
'tracksUserData' => Jetpack_Tracks_Client::get_connected_user_tracks_identity(),
);
return $data;
}
/**
* Load the admin scripts.
*/
public function load_admin_scripts() {
// This callback is registered via `admin_enqueue_scripts` from `admin_init`,
// which itself fires on `load-{$page_suffix}` in `add_wp_admin_menu()` — so it
// only fires on the Newsletter admin page; no need to re-check the page here.
// The Tracks transport is required on both surfaces — `analytics.initialize`
// only queues events into `window._tkq`; without `jp-tracks` loaded, no
// pixel.gif requests fire and the queue grows forever.
wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
if ( self::is_modernized() ) {
// wp-build manages the rest of its enqueue pipeline. The legacy
// newsletter script and JetpackScriptData are intentionally skipped
// for the wp-build dashboard.
return;
}
Assets::register_script(
'jetpack-newsletter',
'../build/newsletter.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-newsletter',
'enqueue' => true,
'dependencies' => array( 'jetpack-script-data' ),
)
);
}
/**
* Get the subscriber management URL based on site type and filter settings.
*
* - If jetpack_wp_admin_subscriber_management_enabled filter is true: wp-admin subscribers page
* - If filter is false AND wpcom site: wordpress.com/subscribers/$domain
* - If filter is false AND Jetpack site: jetpack.com redirect URL
*
* @param bool $wp_admin_enabled Whether wp-admin subscriber management is enabled.
* @param bool $is_wpcom Whether this is a WordPress.com site.
* @param string $site_suffix The Calypso site suffix (home host, slashes as `::`).
* @param int $blog_id The blog ID.
* @return string The subscriber management URL.
*/
private function get_subscriber_management_url( $wp_admin_enabled, $is_wpcom, $site_suffix, $blog_id ) {
// If wp-admin subscriber management is enabled, use the wp-admin page.
if ( $wp_admin_enabled ) {
return admin_url( 'admin.php?page=subscribers' );
}
// For wpcom sites, use the wordpress.com URL.
if ( $is_wpcom ) {
return 'https://wordpress.com/subscribers/' . $site_suffix;
}
// For Jetpack sites, use the jetpack.com redirect URL.
$site_id = $blog_id ? (int) $blog_id : Connection_Manager::get_site_id( true );
$args = ( ! empty( $site_id ) )
? array( 'site' => $site_id )
: array();
return Redirect::get_url(
'jetpack-settings-jetpack-manage-subscribers',
$args
);
}
/**
* Render the newsletter settings page.
*/
public function render() {
?>
<div id="newsletter-settings-root"></div>
<?php
}
/**
* Register a notice on the Reading settings page to clarify that the RSS
* excerpt setting does not control newsletter emails.
*
* @since 0.5.1
*/
public function add_reading_page_notice() {
add_settings_field(
'jetpack_newsletter_reading_notice',
'',
array( $this, 'render_reading_page_notice' ),
'reading',
'default'
);
}
/**
* Render the clarifying notice on the Reading settings page.
*
* Uses JavaScript to relocate the notice next to the "For each post in a feed"
* (rss_use_excerpt) setting.
*
* @since 0.5.1
*/
public function render_reading_page_notice() {
$newsletter_url = Urls::get_newsletter_settings_url();
printf(
'<p class="description" id="jetpack-newsletter-reading-notice">%s</p>',
sprintf(
wp_kses(
/* translators: %s is a link to the Newsletter settings page. */
__( 'To control whats included in newsletter emails, visit your <a href="%s">Newsletter settings</a>.', 'jetpack-newsletter' ),
array(
'a' => array(
'href' => array(),
),
)
),
esc_url( $newsletter_url )
)
);
?>
<script type="text/javascript">
document.addEventListener( 'DOMContentLoaded', function() {
var notice = document.getElementById( 'jetpack-newsletter-reading-notice' );
var excerptInput = document.querySelector( 'input[name="rss_use_excerpt"]' );
var excerptRow = excerptInput ? excerptInput.closest( 'tr' ) : null;
if ( ! notice || ! excerptRow ) {
return;
}
// Remember the original parent before moving the notice.
var originalTable = notice.closest( 'table' );
var excerptTable = excerptRow.closest( 'table' );
// Move the notice into the rss_use_excerpt row's fieldset.
excerptRow.querySelector( 'td' ).appendChild( notice );
// Remove the now-empty original table (if it's different from the excerpt's table).
if ( originalTable && originalTable !== excerptTable ) {
originalTable.remove();
}
} );
</script>
<?php
}
/**
* Load the wp-build entry file and register its polyfills.
*
* Only called on `?page=jetpack-newsletter` admin requests when the
* modernization filter is enabled. Keeps wp-build off every other request.
*
* @return void
*/
private static function load_wp_build() {
$build_index = dirname( __DIR__ ) . '/build/build.php';
if ( ! file_exists( $build_index ) ) {
return;
}
require_once $build_index;
\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register(
'jetpack-newsletter',
array_merge(
\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES,
\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS
)
);
}
/**
* Alias the current screen ID to satisfy wp-build's auto-generated enqueue check.
*
* Wp-build's `<page>-wp-admin` enqueue callback enqueues only when the screen ID
* matches the wp-build page slug (`jetpack-newsletter-dashboard`). Our wp-admin
* menu slug stays `jetpack-newsletter`, so we mutate the screen object in place
* to make the check pass without changing the user-facing URL.
*
* Hooked only when modernization is on AND we're on the Newsletter admin page,
* so this never affects any other request.
*
* @param \WP_Screen|null $screen The current screen object (passed by WP).
* @return void
*/
public static function alias_screen_id_for_wp_build( $screen ) {
if ( ! is_object( $screen ) ) {
return;
}
$screen->id = 'jetpack-newsletter-dashboard';
}
/**
* Whether the modernized Newsletter experience should default on for this site.
*
* The release is staged: the modernized dashboard, wp-admin subscriber
* management, and the retired Calypso Subscribers submenu all default on for a
* deterministic slice of sites, keyed on the wpcom blog ID, plus all
* Automatticians.
*
* The percentage cohort (see `MODERNIZATION_ROLLOUT_PERCENTAGE`) spans *all*
* sites — Simple, WoA (Atomic) and self-hosted Jetpack — and is bucketed on the
* wpcom blog ID (`get_current_blog_id()` on Simple, `jetpack_options['id']`
* elsewhere), which is preserved when a Simple site is upgraded to Atomic. Keying
* on the wpcom blog ID rather than the transient `IS_WPCOM` constant means a site
* keeps its cohort decision across the transfer. The percentage is currently 0:
* the Simple-site rollout is driven from the WordPress.com backend instead, and
* this gate stays at zero until the wider rollout is opened by bumping the
* constant. A site with no resolvable wpcom blog ID (e.g. a self-hosted Jetpack
* site that isn't connected) is never bucketed in.
*
* Automatticians get the modernized experience by default regardless of the
* percentage cohort, so a12s can dogfood it and test fixes ahead of the wider
* rollout. This is a dogfooding gate, not an authorization check, so the Simple
* `is_automattician()` global is used without the usual proxied-request pairing;
* Atomic has no non-proxied a12s signal, so it falls back to
* `Visitor::is_automattician_feature_flags_only()` (true for proxied a8c requests).
*
* This is only the filter *default*: hosts (and a11ns who want the legacy view
* back) can still force the experience on or off with the
* `rsm_jetpack_ui_modernization_newsletter` /
* `jetpack_wp_admin_subscriber_management_enabled` filters.
*
* @return bool
*/
public static function is_modernization_rollout_enabled() {
// Automatticians are enrolled regardless of the percentage cohort so they
// can dogfood ahead of the wider rollout. Simple exposes the
// `is_automattician()` global; Atomic has no non-proxied a12s signal, so we
// fall back to the proxied-request check. (Dogfooding gate, so no
// `wpcom_is_proxied_request()` pairing on the Simple branch.)
if (
( function_exists( 'is_automattician' ) && is_automattician() )
|| ( new Visitor() )->is_automattician_feature_flags_only()
) {
return true;
}
// Bucket on the wpcom blog ID, which is stable across a Simple→Atomic
// transfer: the current blog ID on Simple, the stored wpcom ID elsewhere. We
// read the WoA/Jetpack ID from Jetpack options directly rather than via
// `Host::get_wpcom_site_id()`, which additionally requires the Jetpack
// connection to be "ready" — that would drop a freshly transferred site out
// of the cohort until its connection settles. Guard against an unresolvable
// ID so a site without one isn't bucketed as blog ID 0 and enrolled by
// accident once the percentage is non-zero.
$host = new Host();
$blog_id = $host->is_wpcom_simple()
? (int) get_current_blog_id()
: (int) \Jetpack_Options::get_option( 'id' );
if ( $blog_id <= 0 ) {
return false;
}
return ( $blog_id % 100 ) < self::MODERNIZATION_ROLLOUT_PERCENTAGE;
}
/**
* Returns true when the wp-build modernization filter is enabled.
*
* Defaults to the staged-rollout cohort (see
* `is_modernization_rollout_enabled()`): on for Automatticians and for the
* percentage cohort (currently 0%), off everywhere else. Hosts can opt in or out
* explicitly with
* `add_filter( self::MODERNIZATION_FILTER, '__return_true' / '__return_false' );`.
*
* @return bool
*/
private static function is_modernized() {
return (bool) apply_filters( self::MODERNIZATION_FILTER, self::is_modernization_rollout_enabled() );
}
/**
* Returns true when the current request targets the Newsletter admin page.
*
* Used to scope wp-build loading to the one page that needs it. The
* `$_GET['page']` value is populated by wp-admin/admin.php before any of
* our hooks fire, so this check is reliable from `init_hooks()` onwards.
*
* @return bool
*/
private static function is_newsletter_admin_request() {
if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return false;
}
return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === self::ADMIN_PAGE_SLUG; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
}
@@ -0,0 +1,355 @@
<?php
/**
* Transitional "Subscribers moved" announcement page.
*
* When the Newsletter modernization filter is enabled, the unified
* Jetpack → Newsletter page owns subscriber management and the legacy
* "Subscribers ↗" Calypso shortcut is retired. Instead of silently dropping
* the menu item, this page takes its place so people who rely on the link
* learn the new location before it disappears. They can also remove the
* menu item themselves once they have adopted the new flow.
*
* The whole feature is temporary and kept deliberately small: this class
* (menu, handlers, tracking) plus the `routes/subscribers-announcement`
* wp-build route can be deleted wholesale once the transition period ends.
*
* @package automattic/jetpack-newsletter
*/
namespace Automattic\Jetpack\Newsletter;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Tracking;
/**
* Renders the transitional Subscribers announcement page and handles its
* "remove from sidebar" toggle and "Take me to Newsletter" redirect.
*
* The menu itself is registered by callers that own the Subscribers menu
* placement (the Jetpack plugin's subscriptions module) via add_menu();
* this class self-registers only request handlers and wp-build loading.
*
* @since 0.10.0
*/
class Subscribers_Announcement {
/**
* Admin page slug (kept distinct from the wp-build page name; the screen
* ID is aliased so wp-build's enqueue check still matches).
*
* @var string
*/
const PAGE_SLUG = 'jetpack-subscribers';
/**
* Wp-build page name, matching `routes/subscribers-announcement/package.json`.
*
* @var string
*/
const WP_BUILD_PAGE = 'jetpack-subscribers-announcement';
/**
* Option storing whether the user removed the Subscribers menu item.
*
* @var string
*/
const REMOVED_OPTION = 'jetpack_subscribers_announcement_menu_removed';
/**
* AJAX action toggling the menu item visibility.
*
* @var string
*/
const TOGGLE_ACTION = 'jetpack_subscribers_announcement_toggle_menu';
/**
* Admin-post action tracking the "Take me to Newsletter" click before redirecting.
*
* @var string
*/
const GO_ACTION = 'jetpack_subscribers_announcement_go_to_newsletter';
/**
* Register request handlers and the wp-build loader.
*
* Called from Settings::init_hooks() so the AJAX/admin-post handlers exist
* on admin-ajax.php / admin-post.php requests, where `admin_menu` (and so
* add_menu()) never fires.
*
* @return void
*/
public static function init() {
add_action( 'wp_ajax_' . self::TOGGLE_ACTION, array( __CLASS__, 'handle_toggle_menu' ) );
add_action( 'admin_post_' . self::GO_ACTION, array( __CLASS__, 'handle_go_to_newsletter' ) );
// Priority 1 mirrors Settings::maybe_load_wp_build(): the modernization
// filter has been registered by opt-in code by then, and the wp-build
// render function must exist before menu callbacks are resolved.
add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
}
/**
* Whether the announcement page feature is active.
*
* @return bool
*/
public static function is_enabled() {
/** This filter is documented in projects/packages/newsletter/src/class-settings.php */
return (bool) apply_filters( Settings::MODERNIZATION_FILTER, false );
}
/**
* Load wp-build for the announcement page when the feature is enabled.
*
* @return void
*/
public static function maybe_load_wp_build() {
if ( ! self::is_enabled() || ! self::is_announcement_request() ) {
return;
}
$build_index = dirname( __DIR__ ) . '/build/build.php';
if ( ! file_exists( $build_index ) ) {
return;
}
require_once $build_index;
\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register(
'jetpack-newsletter',
array_merge(
\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES,
\Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS
)
);
add_action( 'current_screen', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
}
/**
* Alias the current screen ID to satisfy wp-build's auto-generated enqueue check.
*
* Mirrors Settings::alias_screen_id_for_wp_build(): wp-build enqueues only
* when the screen ID matches the wp-build page name, while our menu slug
* stays `jetpack-subscribers`.
*
* @param \WP_Screen|null $screen The current screen object (passed by WP).
* @return void
*/
public static function alias_screen_id_for_wp_build( $screen ) {
if ( ! is_object( $screen ) ) {
return;
}
$screen->id = self::WP_BUILD_PAGE;
}
/**
* Register the Subscribers announcement page under the Jetpack menu.
*
* When the user opted to remove the menu item, the page stays registered
* (so the page remains reachable directly and the choice can be undone)
* but the sidebar entry is removed.
*
* @return void
*/
public static function add_menu() {
$callback = function_exists( 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page' )
? 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page'
: array( __CLASS__, 'render_fallback' );
if ( get_option( self::REMOVED_OPTION ) ) {
// Register as a hidden page (empty parent slug): it stays reachable
// at its URL — so the choice can be undone from the page itself —
// but never appears in the sidebar.
$page_suffix = add_submenu_page(
'',
__( 'Subscribers', 'jetpack-newsletter' ),
__( 'Subscribers', 'jetpack-newsletter' ),
'manage_options',
self::PAGE_SLUG,
$callback
);
} else {
$page_suffix = Admin_Menu::add_menu(
__( 'Subscribers', 'jetpack-newsletter' ),
__( 'Subscribers', 'jetpack-newsletter' ),
'manage_options',
self::PAGE_SLUG,
$callback,
15
);
}
if ( $page_suffix ) {
add_action( 'load-' . $page_suffix, array( __CLASS__, 'on_page_load' ) );
}
}
/**
* Register the announcement page directly under the Jetpack menu.
*
* Used on WordPress.com (Simple and WoA), where jetpack-mu-wpcom's
* wpcom-admin-menu owns the Jetpack menu and registers submenus with the
* core add_submenu_page() at a late priority — not the standalone plugin's
* Admin_Menu wrapper. Mirrors Settings::add_wp_admin_submenu().
*
* As in add_menu(), an empty parent slug keeps the page reachable at its URL
* (so the "remove from sidebar" choice can be undone) while hiding it from
* the sidebar.
*
* @return void
*/
public static function add_wp_admin_submenu() {
$callback = function_exists( 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page' )
? 'jetpack_newsletter_jetpack_subscribers_announcement_wp_admin_render_page'
: array( __CLASS__, 'render_fallback' );
$parent_slug = get_option( self::REMOVED_OPTION ) ? '' : 'jetpack';
$page_suffix = add_submenu_page(
$parent_slug,
__( 'Subscribers', 'jetpack-newsletter' ),
__( 'Subscribers', 'jetpack-newsletter' ),
'manage_options',
self::PAGE_SLUG,
$callback
);
if ( $page_suffix ) {
add_action( 'load-' . $page_suffix, array( __CLASS__, 'on_page_load' ) );
}
}
/**
* Page-load actions: record the page view and expose the app data.
*
* @return void
*/
public static function on_page_load() {
add_action( 'admin_head', array( __CLASS__, 'print_app_data' ) );
self::tracking()->record_user_event(
'subscribers_announcement_page_view',
array( 'menu_removed' => (bool) get_option( self::REMOVED_OPTION ) )
);
}
/**
* Print the data the announcement app needs (URLs, nonce, current state).
*
* @return void
*/
public static function print_app_data() {
$data = array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'toggleAction' => self::TOGGLE_ACTION,
'toggleNonce' => wp_create_nonce( self::TOGGLE_ACTION ),
// Built with add_query_arg (not wp_nonce_url, which HTML-escapes
// the ampersands) because the app navigates to it via JS.
'goToNewsletterUrl' => add_query_arg(
array(
'action' => self::GO_ACTION,
'_wpnonce' => wp_create_nonce( self::GO_ACTION ),
),
admin_url( 'admin-post.php' )
),
'menuRemoved' => (bool) get_option( self::REMOVED_OPTION ),
'menuSlug' => self::PAGE_SLUG,
);
printf(
'<script>window.JetpackSubscribersAnnouncementData = %s;</script>',
wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT )
);
}
/**
* Minimal fallback when the wp-build bundle is unavailable.
*
* @return void
*/
public static function render_fallback() {
?>
<div class="wrap">
<h1><?php esc_html_e( 'Subscribers moved', 'jetpack-newsletter' ); ?></h1>
<p><?php esc_html_e( 'Now its part of Jetpack → Newsletter.', 'jetpack-newsletter' ); ?></p>
<p>
<a class="button button-primary" href="<?php echo esc_url( Urls::get_newsletter_settings_url() ); ?>">
<?php esc_html_e( 'Take me to Newsletter', 'jetpack-newsletter' ); ?>
</a>
</p>
</div>
<?php
}
/**
* AJAX handler persisting the "remove Subscribers from the sidebar" choice.
*
* @return void
*/
public static function handle_toggle_menu() {
check_ajax_referer( self::TOGGLE_ACTION );
if ( ! current_user_can( 'manage_options' ) || ! self::is_enabled() ) {
wp_send_json_error( 'unauthorized', 403, JSON_HEX_TAG | JSON_HEX_AMP );
}
$removed = isset( $_POST['removed'] ) && '1' === $_POST['removed'];
update_option( self::REMOVED_OPTION, $removed ? 1 : 0, false );
self::tracking()->record_user_event(
'subscribers_announcement_remove_menu_click',
array( 'removed' => $removed )
);
wp_send_json_success( array( 'removed' => $removed ), 200, JSON_HEX_TAG | JSON_HEX_AMP );
}
/**
* Admin-post handler recording the "Take me to Newsletter" click, then redirecting.
*
* Tracking the click server-side before the redirect avoids relying on a
* JS tracking pipeline on a page that is otherwise static.
*
* @return never
*/
public static function handle_go_to_newsletter() {
check_admin_referer( self::GO_ACTION );
if ( current_user_can( 'manage_options' ) && self::is_enabled() ) {
self::tracking()->record_user_event( 'subscribers_announcement_newsletter_click' );
}
wp_safe_redirect( admin_url( 'admin.php?page=' . Settings::ADMIN_PAGE_SLUG ) );
exit( 0 );
}
/**
* Get a Tracking instance.
*
* The product name stays `jetpack` so the events are recorded as
* `jetpack_subscribers_announcement_*` regardless of which plugin
* bundles this package.
*
* @return Tracking
*/
private static function tracking() {
return new Tracking( 'jetpack', new Connection_Manager( 'jetpack' ) );
}
/**
* Returns true when the current request targets the announcement page.
*
* @return bool
*/
private static function is_announcement_request() {
if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return false;
}
return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === self::PAGE_SLUG; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
}
@@ -0,0 +1,23 @@
<?php
/**
* URL helper for newsletter settings.
*
* @package automattic/jetpack-newsletter
*/
namespace Automattic\Jetpack\Newsletter;
/**
* A class responsible for generating newsletter settings URLs.
*/
class Urls {
/**
* Get the newsletter settings URL.
*
* @return string The newsletter settings URL.
*/
public static function get_newsletter_settings_url() {
return admin_url( 'admin.php?page=jetpack-newsletter' );
}
}
@@ -0,0 +1,201 @@
<?php
/**
* A class that adds the Daily Writing Prompt dashboard widget to wp-admin.
*
* @package automattic/jetpack-newsletter
*/
namespace Automattic\Jetpack\Newsletter;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Tracking;
use Jetpack_Tracks_Client;
/**
* Register and render the Daily Writing Prompt dashboard widget.
*/
class Writing_Prompt_Widget {
/**
* Whether the class has been initialized.
*
* @var bool
*/
private static $initialized = false;
/**
* Initialize the Daily Writing Prompt widget.
*
* Hooks the widget registration into `wp_dashboard_setup`. It can be called
* multiple times safely as it will only initialize once.
*
* @since 0.9.0
*
* @return void
*/
public static function init() {
if ( self::$initialized ) {
return;
}
self::$initialized = true;
add_action( 'wp_dashboard_setup', array( __CLASS__, 'register_widget' ) );
}
/**
* Register the Daily Writing Prompt dashboard widget.
*
* @since 0.9.0
*
* @return void
*/
public static function register_widget() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! self::should_load() ) {
return;
}
wp_add_dashboard_widget(
'wpcom_daily_writing_prompt',
__( 'Daily Writing Prompt', 'jetpack-newsletter' ),
array( __CLASS__, 'render_widget' ),
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. See https://core.trac.wordpress.org/ticket/52539.
array(),
'side',
'high'
);
// Enqueue here (on wp_dashboard_setup, which only fires on the dashboard
// screen and runs before the header is printed) rather than in the render
// callback, so the stylesheet lands in the head like it did previously.
self::enqueue_assets();
}
/**
* Whether the Daily Writing Prompt widget should load.
*
* The widget fetches prompts from WordPress.com through the Jetpack proxy, so
* it can only load where the site is able to reach WordPress.com: on Simple
* sites, or on online, connected Jetpack sites (including Atomic).
*
* This runs on `wp_dashboard_setup` rather than at plugin-load time so that
* the connection check happens after Core's pluggable functions are loaded.
* Checking the connection any earlier triggers a fatal error on Atomic.
*
* @since 0.9.1
*
* @return bool
*/
private static function should_load() {
// Simple sites can always reach WordPress.com, with no Jetpack connection involved.
if ( ( new Host() )->is_wpcom_simple() ) {
return true;
}
// Everywhere else the widget needs an online, connected site to fetch prompts.
return ( new Connection_Manager() )->is_connected() && ! ( new Status() )->is_offline_mode();
}
/**
* Enqueue the assets used to display the Daily Writing Prompt widget.
*
* @since 0.9.0
*
* @return string The script/style handle.
*/
public static function enqueue_assets() {
$handle = 'jetpack-newsletter-writing-prompt';
$js_path = dirname( __DIR__ ) . '/build/writing-prompt.js';
$asset_php = dirname( __DIR__ ) . '/build/writing-prompt.asset.php';
if ( ! file_exists( $js_path ) ) {
return $handle;
}
$asset_file = file_exists( $asset_php ) ? include $asset_php : array();
$dependencies = $asset_file['dependencies'] ?? array();
$version = $asset_file['version'] ?? filemtime( $js_path );
wp_enqueue_script(
$handle,
plugins_url( '../build/writing-prompt.js', __FILE__ ),
$dependencies,
$version,
true
);
wp_set_script_translations( $handle, 'jetpack-newsletter' );
// Load the Tracks transport so the widget's analytics events can fire.
Tracking::register_tracks_functions_scripts( true );
// Expose the connected user's Tracks identity to the widget script.
add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
$css_ext = is_rtl() ? 'rtl.css' : 'css';
$css_path = dirname( __DIR__ ) . "/build/writing-prompt.$css_ext";
if ( file_exists( $css_path ) ) {
wp_enqueue_style(
$handle,
plugins_url( "../build/writing-prompt.$css_ext", __FILE__ ),
array(),
$version
);
}
return $handle;
}
/**
* Add the connected user's Tracks identity to the admin script data.
*
* Mirrors the `newsletter.tracksUserData` shape provided by the Newsletter
* settings page so the widget can reuse the same `getNewsletterScriptData()`
* helper and `analytics.initialize()` flow.
*
* @since 0.9.1
*
* @param array $data The script data.
* @return array The filtered script data.
*/
public static function add_script_data( $data ) {
if ( ! isset( $data['newsletter'] ) || ! is_array( $data['newsletter'] ) ) {
$data['newsletter'] = array();
}
$data['newsletter']['tracksUserData'] = Jetpack_Tracks_Client::get_connected_user_tracks_identity();
return $data;
}
/**
* Render the container of the Daily Writing Prompt widget.
*
* The widget is hydrated client-side by the `writing-prompt` JS entry, which
* mounts the React app into the container rendered here.
*
* @since 0.9.0
*
* @return void
*/
public static function render_widget() {
$warning = __( 'Your Daily Writing Prompt widget requires JavaScript to function properly.', 'jetpack-newsletter' );
?>
<div>
<div class="hide-if-js">
<?php echo esc_html( $warning ); ?>
</div>
<div
id="wpcom_daily_writing_prompt_main"
class="wpcom_daily_writing_prompt hide-if-no-js"
style="height: 100%">
</div>
</div>
<?php
}
}