Commit ab92f0b2c69 for woocommerce

commit ab92f0b2c698a19d5445d93d17d3b318fac1906d
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date:   Wed Jul 1 13:47:58 2026 +0100

    Always enable push notifications by deprecating the feature flag (#66088)

    * Always enable push notifications by deprecating the feature flag.

    - Add deprecated_since/deprecated_value to the push_notifications feature so feature_is_enabled() returns true and ignores the stored option
    - Add wc_update_10902 migration to delete the now-dead option, clearing the stale 'no' persisted by the 10.5.0 autoload migration
    - Register the cleanup migration under a new 10.9.2 DB update key

    * Remove deprecated feature-flag check from push notifications gate.

    - Drop the FeaturesUtil::feature_is_enabled('push_notifications') call in should_be_enabled(); the flag is now deprecated and always enabled, and the public API logged a deprecation notice on every request
    - Gate enablement on the Jetpack connection only, matching the marketplace/point_of_sale deprecation pattern
    - Update tests and shared trait: drop the obsolete feature-disabled test and now-dead FeaturesController mock scaffolding

    * Honour explicit 'no' option as a manual push notifications kill switch.

    - Read woocommerce_feature_push_notifications_enabled directly via get_option() in should_be_enabled(), bypassing FeaturesUtil to avoid the deprecation notice and the always-true deprecated value
    - Lets a store force the deprecated feature off (e.g. fall back to Jetpack) even though it has no settings UI

    * Replace push notifications kill-switch option with a filter.

    - Gate enhanced push notifications on the woocommerce_enhanced_push_notifications_disabled filter (default false) instead of reading the deprecated option directly
    - Early-return when disabled, skipping the Jetpack connection lookup
    - Keep the Jetpack connection requirement so the filter can only disable, never force-enable a store that can't receive notifications

    * Ensure disabled flag is a bool.

diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index ca28da6f03c..5fbc277ece6 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -336,6 +336,9 @@ class WC_Install {
 		'10.9.0'   => array(
 			'wc_update_1090_remove_task_list_reminder_bar_hidden_option',
 		),
+		'10.9.2'   => array(
+			'wc_update_10902_remove_deprecated_push_notifications_option',
+		),
 		'11.0.0'   => array(
 			'wc_update_1100_enable_point_of_sale_feature',
 		),
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index 1971606d70e..ce08857e1bd 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -3550,6 +3550,23 @@ function wc_update_1090_remove_task_list_reminder_bar_hidden_option() {
 	delete_option( 'woocommerce_task_list_reminder_bar_hidden' );
 }

+/**
+ * Remove the deprecated push_notifications feature option from the database.
+ *
+ * The push_notifications feature flag was deprecated in 10.9.2 and is now always enabled.
+ * The option is no longer needed as FeaturesUtil::feature_is_enabled('push_notifications')
+ * returns the deprecated_value directly without reading from the database. Removing it also
+ * clears the stale "no" value that wc_update_1050_enable_autoload_options() persisted for
+ * stores upgrading across the 10.5.0 boundary.
+ *
+ * @since 10.9.2
+ *
+ * @return void
+ */
+function wc_update_10902_remove_deprecated_push_notifications_option(): void {
+	delete_option( 'woocommerce_feature_push_notifications_enabled' );
+}
+
 /**
  * Set the stored value of the point_of_sale feature flag to enabled.
  *
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index f52725987d8..5177ff130e8 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -632,11 +632,13 @@ class FeaturesController {
 					'Enable push notifications for the WooCommerce mobile apps to receive order notifications and store updates.',
 					'woocommerce'
 				),
+				'is_experimental'              => false,
 				'enabled_by_default'           => true,
-				'is_experimental'              => true,
 				'disable_ui'                   => true,
-				'skip_compatibility_checks'    => false,
+				'skip_compatibility_checks'    => true,
 				'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
+				'deprecated_since'             => '10.9.2',
+				'deprecated_value'             => true,
 			),
 			'rest_api_caching'                   => array(
 				'name'                         => __( 'REST API Caching', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
index a80250d3f53..233b9911b1f 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
@@ -19,7 +19,6 @@ use Automattic\WooCommerce\Internal\PushNotifications\Triggers\NewReviewNotifica
 use Automattic\WooCommerce\Internal\PushNotifications\Triggers\StockNotificationRecoveryHandler;
 use Automattic\WooCommerce\Internal\PushNotifications\Triggers\StockNotificationTrigger;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;
 use WC_Logger;
 use Exception;

@@ -124,9 +123,8 @@ class PushNotifications {

 	/**
 	 * Determines if local push notification functionality should be enabled.
-	 * Push notifications require both the feature flag to be enabled and
-	 * Jetpack to be connected. Memoize the value so we only check once per
-	 * request.
+	 * Push notifications require Jetpack to be connected. Memoize the value so
+	 * we only check once per request.
 	 *
 	 * @return bool
 	 *
@@ -137,7 +135,24 @@ class PushNotifications {
 			return $this->enabled;
 		}

-		if ( ! FeaturesUtil::feature_is_enabled( self::FEATURE_NAME ) ) {
+		$feature_disabled = wc_string_to_bool(
+			/**
+			 * Filters whether enhanced push notifications should be disabled.
+			 *
+			 * The feature was previously controlled by a now-deprecated feature
+			 * flag. It is now enabled by default for all compatible users, but this
+			 * filter lets a store force it off (e.g. to fall back to Jetpack Sync
+			 * if something isn't working). The feature also requires a Jetpack
+			 * connection, which is checked separately below.
+			 *
+			 * @since 10.9.2
+			 *
+			 * @param bool $disabled Whether enhanced push notifications are disabled. Defaults to false.
+			 */
+			apply_filters( 'woocommerce_enhanced_push_notifications_disabled', false )
+		);
+
+		if ( $feature_disabled ) {
 			$this->enabled = false;
 			return $this->enabled;
 		}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php
index 785924f9f2c..f6c6e8adf29 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php
@@ -43,7 +43,6 @@ class NotificationPreferencesRestControllerTest extends WC_REST_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		$this->set_up_features_controller_mock();
 		$this->reset_push_notifications_cache();

 		$this->user_id       = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
index 7ab63d883c7..cfa2ef153e6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
@@ -62,7 +62,6 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		$this->set_up_features_controller_mock();
 		$this->reset_push_notifications_cache();

 		( new PushTokenRestController() )->register_routes();
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Helpers/PushNotificationsTestTrait.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Helpers/PushNotificationsTestTrait.php
index de37a767efd..99b887304c2 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Helpers/PushNotificationsTestTrait.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Helpers/PushNotificationsTestTrait.php
@@ -5,7 +5,6 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Helpers;

 use Automattic\Jetpack\Connection\Manager as JetpackConnectionManager;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
 use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
 use PHPUnit\Framework\MockObject\MockObject;
@@ -14,10 +13,9 @@ use ReflectionClass;
 /**
  * Shared test helpers for the PushNotifications module.
  *
- * Mocks the Jetpack connection state, the FeaturesController, and resets the
- * memoized enablement flag on the container's `PushNotifications` instance —
- * the three things every push-notifications-related controller test needs in
- * setUp.
+ * Mocks the Jetpack connection state and resets the memoized enablement flag on
+ * the container's `PushNotifications` instance — the two things every
+ * push-notifications-related controller test needs in setUp.
  *
  * @package WooCommerce\Tests\PushNotifications
  */
@@ -27,11 +25,6 @@ trait PushNotificationsTestTrait {
 	 */
 	protected $jetpack_connection_manager_mock;

-	/**
-	 * @var FeaturesController|MockObject|null
-	 */
-	protected $features_controller_mock;
-
 	/**
 	 * Mocks the JetpackConnectionManager so its `is_connected()` returns the
 	 * supplied value, and resets the PushNotifications enablement cache so
@@ -58,28 +51,6 @@ trait PushNotificationsTestTrait {
 		$this->reset_push_notifications_cache();
 	}

-	/**
-	 * Sets up the FeaturesController mock so the `push_notifications` feature
-	 * reports as enabled (and only that feature).
-	 */
-	protected function set_up_features_controller_mock() {
-		$this->features_controller_mock = $this
-			->getMockBuilder( FeaturesController::class )
-			->disableOriginalConstructor()
-			->onlyMethods( array( 'feature_is_enabled' ) )
-			->getMock();
-
-		$this->features_controller_mock
-			->method( 'feature_is_enabled' )
-			->willReturnCallback(
-				function ( $feature_id ) {
-					return PushNotifications::FEATURE_NAME === $feature_id;
-				}
-			);
-
-		wc_get_container()->replace( FeaturesController::class, $this->features_controller_mock );
-	}
-
 	/**
 	 * Resets the cached enablement state on the container's PushNotifications
 	 * instance so subsequent `should_be_enabled()` calls re-evaluate.
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
index c7e8a560855..c07b5c91360 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
 namespace Automattic\WooCommerce\Tests\Internal\PushNotifications;

 use Automattic\Jetpack\Connection\Manager as JetpackConnectionManager;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
 use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
@@ -25,20 +24,6 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 	 */
 	private $jetpack_connection_manager_mock;

-	/**
-	 * @var FeaturesController|MockObject
-	 */
-	private $features_controller_mock;
-
-	/**
-	 * Set up the test case.
-	 */
-	public function setUp(): void {
-		parent::setUp();
-
-		$this->set_up_features_controller_mock();
-	}
-
 	/**
 	 * Tear down the test case.
 	 */
@@ -58,54 +43,56 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox Tests the functionality is disabled if the feature flag is
-	 * disabled.
+	 * @testdox Tests the functionality is enabled when Jetpack is connected.
 	 */
-	public function test_it_can_tell_push_notifications_should_not_be_enabled_if_feature_is_disabled() {
-		$this->set_up_features_controller_mock( false );
+	public function test_it_can_tell_push_notifications_should_be_enabled_if_jetpack_is_connected() {
 		$this->set_up_jetpack_connection_manager_mock( array( 'is_connected' ) );

 		$this->jetpack_connection_manager_mock
-			->expects( $this->never() )
-			->method( 'is_connected' );
+			->expects( $this->once() )
+			->method( 'is_connected' )
+			->willReturn( true );

 		$push_notifications = new PushNotifications();

-		$this->assertFalse( $push_notifications->should_be_enabled() );
+		$this->assertTrue( $push_notifications->should_be_enabled() );
 	}

 	/**
-	 * @testdox Tests the functionality is enabled if feature flag is enabled
-	 * and Jetpack is connected.
+	 * @testdox Tests the functionality is disabled if Jetpack is not connected
+	 * even when feature flag is enabled.
 	 */
-	public function test_it_can_tell_push_notifications_should_be_enabled_if_jetpack_is_connected() {
+	public function test_it_can_tell_push_notifications_should_not_be_enabled_if_jetpack_is_not_connected() {
 		$this->set_up_jetpack_connection_manager_mock( array( 'is_connected' ) );

 		$this->jetpack_connection_manager_mock
 			->expects( $this->once() )
 			->method( 'is_connected' )
-			->willReturn( true );
+			->willReturn( false );

 		$push_notifications = new PushNotifications();

-		$this->assertTrue( $push_notifications->should_be_enabled() );
+		$this->assertFalse( $push_notifications->should_be_enabled() );
 	}

 	/**
-	 * @testdox Tests the functionality is disabled if Jetpack is not connected
-	 * even when feature flag is enabled.
+	 * @testdox Tests the functionality can be manually disabled via the
+	 * woocommerce_enhanced_push_notifications_disabled filter, skipping the Jetpack connection check.
 	 */
-	public function test_it_can_tell_push_notifications_should_not_be_enabled_if_jetpack_is_not_connected() {
+	public function test_it_can_tell_push_notifications_should_not_be_enabled_when_disabled_via_filter() {
 		$this->set_up_jetpack_connection_manager_mock( array( 'is_connected' ) );

 		$this->jetpack_connection_manager_mock
-			->expects( $this->once() )
-			->method( 'is_connected' )
-			->willReturn( false );
+			->expects( $this->never() )
+			->method( 'is_connected' );
+
+		add_filter( 'woocommerce_enhanced_push_notifications_disabled', '__return_true' );

 		$push_notifications = new PushNotifications();

 		$this->assertFalse( $push_notifications->should_be_enabled() );
+
+		remove_filter( 'woocommerce_enhanced_push_notifications_disabled', '__return_true' );
 	}

 	/**
@@ -122,7 +109,6 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 			);

 		$this->register_legacy_proxy_function_mocks( array( 'wc_get_logger' => fn () => $logger_mock ) );
-		$this->set_up_features_controller_mock( true );
 		$this->set_up_jetpack_connection_manager_mock( array( 'is_connected' ) );

 		$this->jetpack_connection_manager_mock
@@ -218,10 +204,15 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox Tests that on_init does not register post types when disabled.
+	 * @testdox Tests that on_init does not register post types when Jetpack is not connected.
 	 */
 	public function test_on_init_does_not_register_post_types_when_disabled() {
-		$this->set_up_features_controller_mock( false );
+		$this->set_up_jetpack_connection_manager_mock( array( 'is_connected' ) );
+
+		$this->jetpack_connection_manager_mock
+			->expects( $this->once() )
+			->method( 'is_connected' )
+			->willReturn( false );

 		$push_notifications = new PushNotifications();
 		$push_notifications->on_init();
@@ -232,29 +223,6 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 		);
 	}

-	/**
-	 * Sets up the FeaturesController mock.
-	 *
-	 * @param bool $feature_enabled Whether the push_notifications feature should be enabled.
-	 */
-	private function set_up_features_controller_mock( bool $feature_enabled = true ) {
-		$this->features_controller_mock = $this
-			->getMockBuilder( FeaturesController::class )
-			->disableOriginalConstructor()
-			->onlyMethods( array( 'feature_is_enabled' ) )
-			->getMock();
-
-		$this->features_controller_mock
-			->method( 'feature_is_enabled' )
-			->willReturnCallback(
-				function ( $feature_id ) use ( $feature_enabled ) {
-					return PushNotifications::FEATURE_NAME === $feature_id ? $feature_enabled : false;
-				}
-			);
-
-		wc_get_container()->replace( FeaturesController::class, $this->features_controller_mock );
-	}
-
 	/**
 	 * Sets up the Jetpack connection manager mocking.
 	 *