Repository navigation
Newsletter Mode: add WordPress.com page shell and routes #50973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: minor | ||
| Type: added | ||
|
|
||
| Newsletter Mode: Add the gated WordPress.com page shell and routes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); | ||
|
|
||
| module.exports = { | ||
| ...baseConfig, | ||
| testMatch: [ | ||
| '<rootDir>/src/features/newsletter-mode/**/*.test.[jt]s?(x)', | ||
| '<rootDir>/routes/newsletter-mode-*/**/*.test.[jt]s?(x)', | ||
| ], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,9 @@ | |
| "clean": "rm -rf src/build/ src/build-module/ build/", | ||
| "e2e-tests": "playwright test --config src/features/verbum-comments/playwright.config.ts", | ||
| "sync:newspack-blocks": "./bin/sync-newspack-blocks.sh", | ||
| "test": "node --test --test-reporter spec src/features/write/test/undo-history.test.mjs", | ||
| "test": "pnpm run test:node && pnpm run test:jest", | ||
| "test:jest": "jest", | ||
| "test:node": "node --test --test-reporter spec src/features/write/test/undo-history.test.mjs", | ||
| "test-coverage": "c8 --report-dir=\"$COVERAGE_DIR\" --temp-directory=\"$ARTIFACTS_DIR/v8\" pnpm run test", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The other Jest-based packages in the monorepo use Jest's own coverage instead: "test-coverage": "pnpm run test --coverage"That's my best guess at why the "Code coverage requirement" check is currently red, though I didn't run the coverage job to confirm. Since the |
||
| "typecheck": "tsgo --build", | ||
| "watch": "pnpm run build-production-js && pnpm webpack watch" | ||
|
|
@@ -120,8 +122,11 @@ | |
| "@babel/runtime": "7.29.7", | ||
| "@lezer/generator": "1.8.0", | ||
| "@playwright/test": "1.60.0", | ||
| "@testing-library/dom": "10.4.1", | ||
| "@testing-library/react": "16.3.2", | ||
| "@tsconfig/strictest": "2.0.5", | ||
| "@types/canvas-confetti": "1.9.0", | ||
| "@types/jest": "30.0.0", | ||
| "@types/node": "^24.12.0", | ||
| "@types/react": "^18.3.28", | ||
| "@types/react-dom": "18.3.7", | ||
|
|
@@ -132,6 +137,7 @@ | |
| "babel-plugin-transform-rename-properties": "0.1.0", | ||
| "browserslist": "^4.24.0", | ||
| "c8": "^11.0.0", | ||
| "jest": "30.4.2", | ||
| "package-directory": "^8.1.0", | ||
| "postcss": "8.5.21", | ||
| "postcss-loader": "8.2.0", | ||
|
|
@@ -151,6 +157,7 @@ | |
| "packageNamespace": "jetpack-mu-wpcom", | ||
| "handlePrefix": "jetpack-mu-wpcom", | ||
| "pages": [ | ||
| "newsletter-mode", | ||
| "site-setup" | ||
| ] | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "dependencies": { | ||
| "@types/react": "^18.3.28", | ||
| "@wordpress/i18n": "6.24.0" | ||
| }, | ||
| "route": { | ||
| "path": "/", | ||
| "page": "newsletter-mode" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { NewsletterModePageShell } from '../../src/features/newsletter-mode/js/page-shell'; | ||
|
|
||
| /** | ||
| * Newsletter Mode onboarding route placeholder. | ||
| * | ||
| * @return Onboarding route stage. | ||
| */ | ||
| const Stage = (): JSX.Element => ( | ||
| <NewsletterModePageShell> | ||
| <section | ||
| className="wpcom-newsletter-mode-page" | ||
| aria-labelledby="wpcom-newsletter-mode-onboarding-title" | ||
| > | ||
| <h2 id="wpcom-newsletter-mode-onboarding-title"> | ||
| { __( 'Newsletter setup', 'jetpack-mu-wpcom' ) } | ||
| </h2> | ||
| <p>{ __( 'This page is ready for its feature content.', 'jetpack-mu-wpcom' ) }</p> | ||
| </section> | ||
| </NewsletterModePageShell> | ||
| ); | ||
|
|
||
| export { Stage as stage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "dependencies": { | ||
| "@types/react": "^18.3.28", | ||
| "@wordpress/i18n": "6.24.0" | ||
| }, | ||
| "route": { | ||
| "path": "/stats", | ||
| "page": "newsletter-mode" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { NewsletterModePageShell } from '../../src/features/newsletter-mode/js/page-shell'; | ||
|
|
||
| /** | ||
| * Newsletter Mode stats route placeholder. | ||
| * | ||
| * @return Stats route stage. | ||
| */ | ||
| const Stage = (): JSX.Element => ( | ||
| <NewsletterModePageShell> | ||
| <section | ||
| className="wpcom-newsletter-mode-page" | ||
| aria-labelledby="wpcom-newsletter-mode-stats-title" | ||
| > | ||
| <h2 id="wpcom-newsletter-mode-stats-title"> | ||
| { __( 'Stats dashboard', 'jetpack-mu-wpcom' ) } | ||
| </h2> | ||
| <p>{ __( 'This page is ready for its feature content.', 'jetpack-mu-wpcom' ) }</p> | ||
| </section> | ||
| </NewsletterModePageShell> | ||
| ); | ||
|
|
||
| export { Stage as stage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Newsletter Mode | ||
|
|
||
| Newsletter Mode is a WordPress.com-only, per-site experience. Its page and hooks | ||
| remain unavailable until the site is explicitly enrolled. | ||
|
|
||
| Access to the Newsletter Mode shell requires the `publish_posts` capability. | ||
|
|
||
| ## Manual enrollment | ||
|
|
||
| Select the target site by site ID in the WordPress.com WP-CLI environment, then | ||
| enable the package-local option in that site's context: | ||
|
|
||
| ```bash | ||
| wp option update wpcom_newsletter_mode_enabled 1 | ||
| ``` | ||
|
|
||
| Disable the option to remove the Newsletter Mode entry point: | ||
|
|
||
| ```bash | ||
| wp option delete wpcom_newsletter_mode_enabled | ||
| ``` | ||
|
|
||
| Defining `WPCOM_NEWSLETTER_MODE_DISABLED` as a truthy value disables the feature | ||
| even when the per-site option remains enabled. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| <?php | ||
| /** | ||
| * Newsletter Mode page shell. | ||
| * | ||
| * @package automattic/jetpack-mu-wpcom | ||
| */ | ||
|
|
||
| namespace Automattic\Jetpack\Jetpack_Mu_Wpcom; | ||
|
|
||
| use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills; | ||
|
|
||
| /** | ||
| * Registers the gated Newsletter Mode page and its wp-build assets. | ||
| * | ||
| * @since $$next-version$$ | ||
| */ | ||
| class Newsletter_Mode { | ||
|
|
||
| /** | ||
| * Per-site enrollment option. | ||
| */ | ||
| const OPTION_ENABLED = 'wpcom_newsletter_mode_enabled'; | ||
|
|
||
| /** | ||
| * Emergency constant that disables the feature even for enrolled sites. | ||
| */ | ||
| const DISABLE_CONSTANT = 'WPCOM_NEWSLETTER_MODE_DISABLED'; | ||
|
|
||
| /** | ||
| * WP Build page ID. | ||
| */ | ||
| const PAGE_ID = 'newsletter-mode'; | ||
|
|
||
| /** | ||
| * WP Admin menu slug generated for a WP Build page. | ||
| */ | ||
| const MENU_SLUG = self::PAGE_ID . '-wp-admin'; | ||
|
|
||
| /** | ||
| * Render callback generated by wp-build in build/pages/newsletter-mode/page-wp-admin.php. | ||
| */ | ||
| const RENDER_CALLBACK = 'jetpack_mu_wpcom_newsletter_mode_wp_admin_render_page'; | ||
|
|
||
| /** | ||
| * Capability required to enter the shell. | ||
| * | ||
| * Features added later retain the capabilities of their corresponding | ||
| * wp-admin screens and APIs. | ||
| */ | ||
| const ACCESS_CAPABILITY = 'publish_posts'; | ||
|
|
||
| /** | ||
| * Initialize Newsletter Mode for an enrolled site. | ||
| */ | ||
| public static function init() { | ||
| if ( ! is_admin() || wp_doing_ajax() ) { | ||
| return; | ||
| } | ||
|
|
||
| if ( ! self::is_enabled() ) { | ||
| return; | ||
| } | ||
|
|
||
| add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) ); | ||
|
|
||
| if ( ! self::is_page_request() || ! current_user_can( self::ACCESS_CAPABILITY ) ) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The capability gets evaluated twice, at two different points in the request, and the two can disagree. This check runs at So if anything grants Loading if ( ! self::is_page_request() ) {
return;
}
|
||
| return; | ||
| } | ||
|
|
||
| self::load_wp_build(); | ||
| } | ||
|
|
||
| /** | ||
| * Whether the emergency kill switch allows Newsletter Mode to run. | ||
| * | ||
| * @return bool | ||
| */ | ||
| public static function is_available() { | ||
| return ! defined( self::DISABLE_CONSTANT ) || ! constant( self::DISABLE_CONSTANT ); | ||
| } | ||
|
|
||
| /** | ||
| * Whether the current site is enrolled and the feature is available. | ||
| * | ||
| * Enroll the current site from its WP-CLI context with: | ||
| * `wp option update wpcom_newsletter_mode_enabled 1`. | ||
| * | ||
| * @return bool | ||
| */ | ||
| public static function is_enabled() { | ||
| return self::is_available() && (bool) get_option( self::OPTION_ENABLED, false ); | ||
| } | ||
|
|
||
| /** | ||
| * Register the top-level Newsletter menu page for eligible users. | ||
| */ | ||
| public static function register_menu() { | ||
| if ( ! self::is_enabled() ) { | ||
| return; | ||
| } | ||
|
|
||
| // The generated callback only exists after build/build.php is loaded on | ||
| // the Newsletter Mode request. The fallback keeps menu registration safe | ||
| // in source checkouts and when build artifacts are unavailable. | ||
| $callback = function_exists( self::RENDER_CALLBACK ) | ||
| ? self::RENDER_CALLBACK | ||
| : '__return_empty_string'; | ||
|
|
||
| add_menu_page( | ||
| __( 'Newsletter', 'jetpack-mu-wpcom' ), | ||
| __( 'Newsletter', 'jetpack-mu-wpcom' ), | ||
| self::ACCESS_CAPABILITY, | ||
| self::MENU_SLUG, | ||
| $callback, | ||
| 'dashicons-email-alt', | ||
| 3 | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Load wp-build's generated registrations for the Newsletter Mode page. | ||
| */ | ||
| private static function load_wp_build() { | ||
| $wp_build_index = dirname( __DIR__, 3 ) . '/build/build.php'; | ||
|
|
||
| if ( ! file_exists( $wp_build_index ) ) { | ||
| return; | ||
| } | ||
|
|
||
| require_once $wp_build_index; | ||
|
|
||
| WP_Build_Polyfills::register( | ||
| 'jetpack-mu-wpcom', | ||
| array_merge( | ||
| WP_Build_Polyfills::SCRIPT_HANDLES, | ||
| WP_Build_Polyfills::MODULE_IDS | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Whether this request targets the Newsletter Mode wp-build page. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private static function is_page_request() { | ||
| // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing parameter. | ||
| return is_admin() && isset( $_GET['page'] ) && self::MENU_SLUG === $_GET['page']; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /// <reference types="jest" /> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the filename doesn't quite describe what's under test — this renders the two route stages and never exercises |
||
|
|
||
| import { render, screen } from '@testing-library/react'; | ||
| import { stage as OnboardingStage } from '../../../../routes/newsletter-mode-onboarding/stage'; | ||
| import { stage as StatsStage } from '../../../../routes/newsletter-mode-stats/stage'; | ||
|
|
||
| describe.each( [ | ||
| [ 'onboarding', OnboardingStage, 'Newsletter setup' ], | ||
| [ 'stats', StatsStage, 'Stats dashboard' ], | ||
| ] )( '%s route', ( _route, Stage, placeholderTitle ) => { | ||
| it( 'renders its content inside the shared AdminPage shell', () => { | ||
| render( <Stage /> ); | ||
|
|
||
| expect( screen.getByRole( 'heading', { level: 1, name: 'Newsletter' } ) ).toBeVisible(); | ||
| expect( screen.getByRole( 'heading', { level: 2, name: placeholderTitle } ) ).toBeVisible(); | ||
| expect( screen.getByText( 'Create, grow, and manage your newsletter.' ) ).toBeVisible(); | ||
| expect( screen.getByRole( 'contentinfo', { name: 'Jetpack' } ) ).toBeVisible(); | ||
| } ); | ||
| } ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import AdminPage from '@automattic/jetpack-components/admin-page'; | ||
| import { __ } from '@wordpress/i18n'; | ||
| import './style.scss'; | ||
| import type { ReactNode } from 'react'; | ||
|
|
||
| type NewsletterModePageShellProps = { | ||
| children: ReactNode; | ||
| }; | ||
|
|
||
| /** | ||
| * Shared Newsletter Mode page chrome. | ||
| * | ||
| * Route stages provide their own content while this component keeps the page | ||
| * title, footer, and other shared chrome consistent. | ||
| * | ||
| * @param props - Component props. | ||
| * @param props.children - Route content rendered inside the shell. | ||
| * @return Newsletter Mode page shell. | ||
| */ | ||
| export function NewsletterModePageShell( { children }: NewsletterModePageShellProps ): JSX.Element { | ||
| return ( | ||
| <AdminPage | ||
| title={ __( 'Newsletter', 'jetpack-mu-wpcom' ) } | ||
| subTitle={ __( 'Create, grow, and manage your newsletter.', 'jetpack-mu-wpcom' ) } | ||
| > | ||
| { children } | ||
| </AdminPage> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
testMatchis an allowlist, onlynewsletter-modepaths run. Any future*.test.tsxadded elsewhere injetpack-mu-wpcomsilently won't execute — no failure, just silence, which is an easy one to lose a day to.A short comment explaining the deliberate scoping would be enough, or broaden the match and exclude what you don't want.