From 30c86c30b6a41acbc8adb1fe515ce1b57d5fc2ec Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Mon, 11 May 2026 13:51:48 -0400 Subject: [PATCH 1/8] Podcast: add product-access gate and grandfather sticker constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `Podcast_Gate::has_product_access()` as the single entry point downstream gates (dashboard tab, stats endpoint, episode block, AI shownotes) will call. Two ways to pass: the `podcasting-grandfathered` blog sticker, or `Current_Plan::supports( 'podcasting' )` — which short-circuits to `wpcom_site_has_feature` on Simple/Atomic and falls through to cached plan data on self-hosted Jetpack, so the same helper is correct on every host. --- .../podcast/changelog/pods-141-add-gate | 4 + projects/packages/podcast/composer.json | 1 + .../podcast/src/class-podcast-gate.php | 63 ++++++++++++++++ .../podcast/tests/php/Podcast_Gate_Test.php | 75 +++++++++++++++++++ .../packages/podcast/tests/php/bootstrap.php | 11 +++ 5 files changed, 154 insertions(+) create mode 100644 projects/packages/podcast/changelog/pods-141-add-gate create mode 100644 projects/packages/podcast/src/class-podcast-gate.php create mode 100644 projects/packages/podcast/tests/php/Podcast_Gate_Test.php diff --git a/projects/packages/podcast/changelog/pods-141-add-gate b/projects/packages/podcast/changelog/pods-141-add-gate new file mode 100644 index 000000000000..893ecc9f7587 --- /dev/null +++ b/projects/packages/podcast/changelog/pods-141-add-gate @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Podcast: add product-access gate (Podcast_Gate::has_product_access) and grandfather sticker constant. diff --git a/projects/packages/podcast/composer.json b/projects/packages/podcast/composer.json index fdeb581799fc..2cd76044a05c 100644 --- a/projects/packages/podcast/composer.json +++ b/projects/packages/podcast/composer.json @@ -5,6 +5,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=7.2", + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev" }, diff --git a/projects/packages/podcast/src/class-podcast-gate.php b/projects/packages/podcast/src/class-podcast-gate.php new file mode 100644 index 000000000000..8180e9d507ac --- /dev/null +++ b/projects/packages/podcast/src/class-podcast-gate.php @@ -0,0 +1,63 @@ +clear_options(); + self::reset_active_plan_cache(); + parent::tearDown(); + } + + /** + * `Current_Plan::get()` memoizes the resolved plan for the duration of the + * request, so option writes in one test would otherwise leak into the next. + */ + private static function reset_active_plan_cache(): void { + $property = ( new \ReflectionClass( Current_Plan::class ) )->getProperty( 'active_plan_cache' ); + // @todo Remove once we drop PHP < 8.1 support. + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + $property->setValue( null, null ); + } + + public function test_grandfather_sticker_grants_access(): void { + $GLOBALS['jetpack_podcast_test_stickers'][ get_current_blog_id() ] = array( Podcast_Gate::GRANDFATHER_STICKER ); + + $this->assertTrue( Podcast_Gate::has_product_access() ); + } + + public function test_plan_supports_feature_grants_access(): void { + $plan = Current_Plan::PLAN_DATA['free']; + $plan['features']['active'] = array( Podcast_Gate::FEATURE_SLUG ); + update_option( Current_Plan::PLAN_OPTION, $plan, true ); + + $this->assertTrue( Podcast_Gate::has_product_access() ); + } + + public function test_no_sticker_and_unsupported_plan_denies_access(): void { + $plan = Current_Plan::PLAN_DATA['free']; + $plan['features']['active'] = array(); + update_option( Current_Plan::PLAN_OPTION, $plan, true ); + + $this->assertFalse( Podcast_Gate::has_product_access() ); + } +} diff --git a/projects/packages/podcast/tests/php/bootstrap.php b/projects/packages/podcast/tests/php/bootstrap.php index 0417e0a852e4..289bf1a8267e 100644 --- a/projects/packages/podcast/tests/php/bootstrap.php +++ b/projects/packages/podcast/tests/php/bootstrap.php @@ -26,3 +26,14 @@ function tracks_record_event( $user, $event_name, $properties = array() ) { return true; } } + +/** + * Mirror the jetpack-mu-wpcom sticker dispatcher. Tests seed + * `$GLOBALS['jetpack_podcast_test_stickers']` as `[ blog_id => [ sticker, ... ] ]`. + */ +if ( ! function_exists( 'wpcom_has_blog_sticker' ) ) { + function wpcom_has_blog_sticker( $sticker, $blog_id ) { + $stickers = $GLOBALS['jetpack_podcast_test_stickers'][ $blog_id ] ?? array(); + return in_array( $sticker, $stickers, true ); + } +} From 6421e19ca6fb42b98c2ed6bf9e4c7252d51418b5 Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Mon, 11 May 2026 13:54:53 -0400 Subject: [PATCH 2/8] Podcast: trim docblock narrative on gate class and tests --- .../podcast/src/class-podcast-gate.php | 20 +------------------ .../podcast/tests/php/Podcast_Gate_Test.php | 9 +-------- .../packages/podcast/tests/php/bootstrap.php | 3 +-- 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/projects/packages/podcast/src/class-podcast-gate.php b/projects/packages/podcast/src/class-podcast-gate.php index 8180e9d507ac..d91d8fc87663 100644 --- a/projects/packages/podcast/src/class-podcast-gate.php +++ b/projects/packages/podcast/src/class-podcast-gate.php @@ -10,18 +10,7 @@ use Automattic\Jetpack\Current_Plan; /** - * Single source of truth for "is this blog allowed to use Premium podcast - * features?". Downstream gates (dashboard tab, stats endpoint, episode block, - * AI shownotes) call {@see self::has_product_access()} so the rule lives in - * one place. - * - * Two ways to pass: - * 1. The blog carries the `podcasting-grandfathered` sticker (backfill for - * sites that uploaded audio before the paid gate). - * 2. `Current_Plan::supports( 'podcasting' )` returns true. On Simple/Atomic - * this falls through to `wpcom_site_has_feature`; on self-hosted Jetpack - * it consults the cached plan data. Either way the answer is correct for - * the current host. + * Premium podcast feature gate. * * Operates on the current blog. `Current_Plan::supports` reads request-scoped * state, so callers that need to gate a different blog must `switch_to_blog` @@ -29,15 +18,8 @@ */ class Podcast_Gate { - /** - * Blog sticker that grants access independent of the current plan. - */ const GRANDFATHER_STICKER = 'podcasting-grandfathered'; - /** - * Feature slug shared with `WPCOM_Features::PODCASTING` on Simple/Atomic - * and with the Jetpack Plans API on self-hosted. - */ const FEATURE_SLUG = 'podcasting'; /** diff --git a/projects/packages/podcast/tests/php/Podcast_Gate_Test.php b/projects/packages/podcast/tests/php/Podcast_Gate_Test.php index c9cf7592a6c0..155867488d5f 100644 --- a/projects/packages/podcast/tests/php/Podcast_Gate_Test.php +++ b/projects/packages/podcast/tests/php/Podcast_Gate_Test.php @@ -14,12 +14,6 @@ use WorDBless\Options as WorDBless_Options; /** - * The sticker branch is exercised through the `wpcom_has_blog_sticker` stub - * in `bootstrap.php` — tests seed `$GLOBALS['jetpack_podcast_test_stickers']` - * keyed by `get_current_blog_id()`. The plan-supports branch is exercised - * through `Current_Plan::PLAN_OPTION`; with `wpcom_site_has_feature` undefined - * in tests, `Current_Plan::supports` falls through to plan-data lookup. - * * @covers \Automattic\Jetpack\Podcast\Podcast_Gate */ #[CoversClass( Podcast_Gate::class )] @@ -39,8 +33,7 @@ protected function tearDown(): void { } /** - * `Current_Plan::get()` memoizes the resolved plan for the duration of the - * request, so option writes in one test would otherwise leak into the next. + * `Current_Plan::get()` memoizes for the request, leaking option writes between tests. */ private static function reset_active_plan_cache(): void { $property = ( new \ReflectionClass( Current_Plan::class ) )->getProperty( 'active_plan_cache' ); diff --git a/projects/packages/podcast/tests/php/bootstrap.php b/projects/packages/podcast/tests/php/bootstrap.php index 289bf1a8267e..dea0c2c62cbd 100644 --- a/projects/packages/podcast/tests/php/bootstrap.php +++ b/projects/packages/podcast/tests/php/bootstrap.php @@ -28,8 +28,7 @@ function tracks_record_event( $user, $event_name, $properties = array() ) { } /** - * Mirror the jetpack-mu-wpcom sticker dispatcher. Tests seed - * `$GLOBALS['jetpack_podcast_test_stickers']` as `[ blog_id => [ sticker, ... ] ]`. + * Mirror the jetpack-mu-wpcom sticker dispatcher. */ if ( ! function_exists( 'wpcom_has_blog_sticker' ) ) { function wpcom_has_blog_sticker( $sticker, $blog_id ) { From 6347c57b8ef5e3b70bc31c781a04daee5c295041 Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Mon, 11 May 2026 14:22:11 -0400 Subject: [PATCH 3/8] Podcast: fix CI failures after jetpack-plans require * Drop @phan-suppress-next-line PhanUndeclaredClassMethod on the two Atomic-only branches in class-tracks (Current_Plan::get, Tracking::tracks_record_event). Adding automattic/jetpack-plans to the package require pulls in the connection package transitively, so phan now resolves both classes and the suppressions are unused. * Regenerate the three plugin composer.locks (jetpack, mu-wpcom-plugin, wpcomsh) that reference automattic/jetpack-podcast, picking up the new jetpack-plans dep. --- projects/packages/podcast/src/class-tracks.php | 2 -- projects/plugins/jetpack/composer.lock | 3 ++- projects/plugins/mu-wpcom-plugin/composer.lock | 3 ++- projects/plugins/wpcomsh/composer.lock | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/projects/packages/podcast/src/class-tracks.php b/projects/packages/podcast/src/class-tracks.php index a19dbdb571da..60a860d33f16 100644 --- a/projects/packages/podcast/src/class-tracks.php +++ b/projects/packages/podcast/src/class-tracks.php @@ -330,7 +330,6 @@ private static function maybe_record_status_change( int $old_value, int $new_val // pattern as `Masterbar\Dashboard_Switcher_Tracking::get_plan()`. $plan = class_exists( '\WPCOM_Store_API' ) ? \WPCOM_Store_API::get_current_plan( (int) get_current_blog_id() ) - // @phan-suppress-next-line PhanUndeclaredClassMethod -- Provided by the connection package on Atomic; not a hard dep. : ( class_exists( '\Automattic\Jetpack\Current_Plan' ) ? \Automattic\Jetpack\Current_Plan::get() : array() ); self::record_event( @@ -423,7 +422,6 @@ private static function record_event( string $event_name, array $properties, ?WP } if ( class_exists( '\Automattic\Jetpack\Tracking' ) ) { - // @phan-suppress-next-line PhanUndeclaredClassMethod -- Provided by the connection package on Atomic; not a hard dep. return ( new \Automattic\Jetpack\Tracking() )->tracks_record_event( $user, $event_name, $properties ); } } catch ( Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index b102cce4e505..230b66b0aa99 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/composer.lock @@ -2575,9 +2575,10 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "7871589d1af53fe45a71bca9e1afffeab27e9322" + "reference": "e1e46c25ed6ddb72a9c599686d48a4f87bb96e99" }, "require": { + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "php": ">=7.2" diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index 57ebb71e980c..31f6c3534280 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1511,9 +1511,10 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "7871589d1af53fe45a71bca9e1afffeab27e9322" + "reference": "e1e46c25ed6ddb72a9c599686d48a4f87bb96e99" }, "require": { + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "php": ">=7.2" diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index b2fa4c83547e..08080d737281 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -1718,9 +1718,10 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "7871589d1af53fe45a71bca9e1afffeab27e9322" + "reference": "e1e46c25ed6ddb72a9c599686d48a4f87bb96e99" }, "require": { + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "php": ">=7.2" From 7b6d7b622107a5a5161d097db22f344ca3bfd589 Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Mon, 11 May 2026 14:22:19 -0400 Subject: [PATCH 4/8] Podcast: bump pods-141 changelog significance to minor Matches sibling changelog entries in projects/packages/podcast/changelog (add-podcast-tracks, add-podcast-settings, add-podcast-package) which all use `minor` for new added surface area. --- projects/packages/podcast/changelog/pods-141-add-gate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/podcast/changelog/pods-141-add-gate b/projects/packages/podcast/changelog/pods-141-add-gate index 893ecc9f7587..dd6095cbca78 100644 --- a/projects/packages/podcast/changelog/pods-141-add-gate +++ b/projects/packages/podcast/changelog/pods-141-add-gate @@ -1,4 +1,4 @@ -Significance: patch +Significance: minor Type: added Podcast: add product-access gate (Podcast_Gate::has_product_access) and grandfather sticker constant. From c969fde6731a526f319fd13f31b12a77efd15fe5 Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Mon, 11 May 2026 14:23:21 -0400 Subject: [PATCH 5/8] Podcast: add lockfile-bump changelog entries for the three plugins Required by the pre-push changelog check now that the previous commit touched jetpack/mu-wpcom-plugin/wpcomsh composer.lock files. --- projects/plugins/jetpack/changelog/pods-141-add-gate | 4 ++++ projects/plugins/mu-wpcom-plugin/changelog/pods-141-add-gate | 4 ++++ projects/plugins/wpcomsh/changelog/pods-141-add-gate | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/pods-141-add-gate create mode 100644 projects/plugins/mu-wpcom-plugin/changelog/pods-141-add-gate create mode 100644 projects/plugins/wpcomsh/changelog/pods-141-add-gate diff --git a/projects/plugins/jetpack/changelog/pods-141-add-gate b/projects/plugins/jetpack/changelog/pods-141-add-gate new file mode 100644 index 000000000000..9208d403d7b3 --- /dev/null +++ b/projects/plugins/jetpack/changelog/pods-141-add-gate @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Comment: Podcast: composer.lock picks up jetpack-plans transitively from the podcast package; no plugin-level behavior change. diff --git a/projects/plugins/mu-wpcom-plugin/changelog/pods-141-add-gate b/projects/plugins/mu-wpcom-plugin/changelog/pods-141-add-gate new file mode 100644 index 000000000000..3c47617223e1 --- /dev/null +++ b/projects/plugins/mu-wpcom-plugin/changelog/pods-141-add-gate @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Updated composer.lock to pick up jetpack-plans transitively via jetpack-podcast. diff --git a/projects/plugins/wpcomsh/changelog/pods-141-add-gate b/projects/plugins/wpcomsh/changelog/pods-141-add-gate new file mode 100644 index 000000000000..3c47617223e1 --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/pods-141-add-gate @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Updated composer.lock to pick up jetpack-plans transitively via jetpack-podcast. From 4a928ad695257fb93eef7aea2d39c852af869187 Mon Sep 17 00:00:00 2001 From: Rob Pugh <52668747+robertbpugh@users.noreply.github.com> Date: Mon, 11 May 2026 15:15:57 -0400 Subject: [PATCH 6/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- projects/plugins/jetpack/changelog/pods-141-add-gate | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/plugins/jetpack/changelog/pods-141-add-gate b/projects/plugins/jetpack/changelog/pods-141-add-gate index 9208d403d7b3..34e4a48b95ae 100644 --- a/projects/plugins/jetpack/changelog/pods-141-add-gate +++ b/projects/plugins/jetpack/changelog/pods-141-add-gate @@ -1,4 +1,3 @@ Significance: patch Type: other - Comment: Podcast: composer.lock picks up jetpack-plans transitively from the podcast package; no plugin-level behavior change. From 7d15d7e707b6e762bb0bba2700fffcac8a363d3e Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Sun, 17 May 2026 10:59:12 -0400 Subject: [PATCH 7/8] Regenerate wpcomsh composer.lock for jetpack-plans dependency --- projects/plugins/wpcomsh/composer.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index 3a5499ef29e2..1e93ef48358c 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -1638,12 +1638,13 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "99472f9cf83ec522499746f7736c5282bb3c70ba" + "reference": "165176b0f879970e605cf1219a0c286a0f3fcb0d" }, "require": { "automattic/jetpack-assets": "@dev", "automattic/jetpack-blocks": "@dev", "automattic/jetpack-connection": "@dev", + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "php": ">=7.2" From 4fc2452e060e647436b3a2fec891482e61ea7082 Mon Sep 17 00:00:00 2001 From: Rob Pugh Date: Sun, 17 May 2026 11:14:53 -0400 Subject: [PATCH 8/8] Regenerate jetpack and mu-wpcom composer locks for jetpack-plans --- projects/plugins/jetpack/composer.lock | 3 ++- projects/plugins/mu-wpcom-plugin/composer.lock | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index 540c4862a0a3..689d7cccbf31 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/composer.lock @@ -2575,12 +2575,13 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "99472f9cf83ec522499746f7736c5282bb3c70ba" + "reference": "165176b0f879970e605cf1219a0c286a0f3fcb0d" }, "require": { "automattic/jetpack-assets": "@dev", "automattic/jetpack-blocks": "@dev", "automattic/jetpack-connection": "@dev", + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "php": ">=7.2" diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index c9860b768083..7db7f63877d2 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1431,12 +1431,13 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "99472f9cf83ec522499746f7736c5282bb3c70ba" + "reference": "165176b0f879970e605cf1219a0c286a0f3fcb0d" }, "require": { "automattic/jetpack-assets": "@dev", "automattic/jetpack-blocks": "@dev", "automattic/jetpack-connection": "@dev", + "automattic/jetpack-plans": "@dev", "automattic/jetpack-status": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "php": ">=7.2"