Commit 89a773af2da for woocommerce

commit 89a773af2dada71ce37fb3aa3028a1a29efc4a13
Author: Ján Mikláš <neosinner@gmail.com>
Date:   Fri Sep 4 16:03:14 2026 +0200

    Avoid Inbox Note cleanup during admin AJAX (#68088)

    * Move Inbox Note cleanup off admin initialization

    * Add changelog entry for Inbox Note cleanup

    * Run survey note cleanup before the daily fetch

    Hook possibly_delete_survey_notes at priority 9 so the cleanup runs ahead of
    Events::do_wc_admin_daily, which adds notes and refreshes the remote data
    source pollers at the default priority. Both sat at 10 before, leaving the
    order dependent on registration.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01SQuqFGRsLMyceRFW1KY3qF

    ---------

    Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/performance-admin-ajax-inbox-note-cleanup b/plugins/woocommerce/changelog/performance-admin-ajax-inbox-note-cleanup
new file mode 100644
index 00000000000..243042f3275
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-admin-ajax-inbox-note-cleanup
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Avoid running Inbox Note cleanup during admin initialization.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 29f0be80edb..dc7eef11f6d 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -353,6 +353,7 @@ class WC_Install {
 		'11.2.0'   => array(
 			'wc_update_1120_remove_abandoned_cart_recovery',
 			'wc_update_1120_migrate_stock_notifications_alpha_constant',
+			'wc_update_1120_delete_surface_cart_checkout_note',
 		),
 		'11.2.0-1' => array(
 			'wc_update_11201_migrate_tax_lookup_order_items',
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index a77e5d63d48..d7a6ff8862e 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -44,6 +44,7 @@ use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Internal\VariationGallery\Package as VariationGalleryPackage;
 use Automattic\WooCommerce\Utilities\StringUtil;
+use Automattic\WooCommerce\Blocks\InboxNotifications;
 use Automattic\WooCommerce\Blocks\Options as BlockOptions;
 use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;

@@ -3704,6 +3705,17 @@ function wc_update_1120_migrate_stock_notifications_alpha_constant() {
 	update_option( StockNotifications::ENABLE_OPTION_NAME, 'yes', true );
 }

+/**
+ * Delete the retired Surface Cart and Checkout inbox note.
+ *
+ * @since 11.2.0
+ *
+ * @return void
+ */
+function wc_update_1120_delete_surface_cart_checkout_note(): void {
+	InboxNotifications::delete_surface_cart_checkout_blocks_notification();
+}
+
 /**
  * Invalidate the Analytics report cache.
  *
diff --git a/plugins/woocommerce/src/Admin/Notes/Notes.php b/plugins/woocommerce/src/Admin/Notes/Notes.php
index c790ba62083..59d3cab5f7f 100644
--- a/plugins/woocommerce/src/Admin/Notes/Notes.php
+++ b/plugins/woocommerce/src/Admin/Notes/Notes.php
@@ -24,7 +24,7 @@ class Notes {
 	 */
 	public static function init() {
 		add_action( 'admin_init', array( __CLASS__, 'schedule_unsnooze_notes' ) );
-		add_action( 'admin_init', array( __CLASS__, 'possibly_delete_survey_notes' ) );
+		add_action( 'wc_admin_daily', array( __CLASS__, 'possibly_delete_survey_notes' ), 9 );
 		add_action( 'update_option_woocommerce_show_marketplace_suggestions', array( __CLASS__, 'possibly_delete_marketing_notes' ), 10, 2 );
 		add_action( self::UNSNOOZE_HOOK, array( __CLASS__, 'unsnooze_notes' ) );
 	}
diff --git a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
index 2bb55635556..1499c6733b1 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
@@ -22,7 +22,6 @@ use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
 use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
 use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend;
 use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutLink;
-use Automattic\WooCommerce\Blocks\InboxNotifications;
 use Automattic\WooCommerce\Blocks\Installer;
 use Automattic\WooCommerce\Blocks\Payments\Api as PaymentsApi;
 use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer;
@@ -93,17 +92,6 @@ class Bootstrap {
 		$this->register_dependencies();
 		$this->register_payment_methods();

-		add_action(
-			'admin_init',
-			function () {
-				// Delete this notification because the blocks are included in WC Core now. This will handle any sites
-				// with lingering notices.
-				InboxNotifications::delete_surface_cart_checkout_blocks_notification();
-			},
-			10,
-			0
-		);
-
 		// We need to initialize BlockTemplatesController and BlockTemplatesRegistry at the end of `after_setup_theme`
 		// so themes had the opportunity to declare support for template parts.
 		add_action(
diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index 2744f289539..227ae24ce0c 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -7,6 +7,9 @@

 use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Admin\Notes\Notes;
+use Automattic\WooCommerce\Blocks\InboxNotifications;
 use Automattic\WooCommerce\Blocks\Options as BlockOptions;
 use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
 use Automattic\WooCommerce\Internal\Admin\OrderTaxLookupMigrator;
@@ -498,6 +501,34 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
 		$batch_processor->remove_processor( OrderTaxLookupMigrator::class );
 	}

+	/**
+	 * @testdox Migration registers for WooCommerce 11.2.0 and deletes the retired Surface Cart and Checkout note.
+	 */
+	public function test_wc_update_1120_delete_surface_cart_checkout_note(): void {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+		$db_updates = WC_Install::get_db_update_callbacks();
+		$this->assertArrayHasKey( '11.2.0', $db_updates );
+		$this->assertContains( 'wc_update_1120_delete_surface_cart_checkout_note', $db_updates['11.2.0'] );
+
+		$note = new Note();
+		$note->set_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME );
+		$note->set_title( 'Surface Cart and Checkout' );
+		$note->set_content( 'Test content' );
+		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
+		$note->set_source( 'PHPUNIT_TEST' );
+		$note->add_action( 'learn-more', 'Learn more', 'https://woocommerce.com/' );
+		$note->save();
+		$this->assertNotFalse( Notes::get_note_by_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME ), 'The retired note fixture should exist before the update.' );
+
+		wc_update_1120_delete_surface_cart_checkout_note();
+
+		$this->assertFalse( Notes::get_note_by_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME ), 'The retired note should be deleted during the update.' );
+
+		wc_update_1120_delete_surface_cart_checkout_note();
+		$this->assertFalse( Notes::get_note_by_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME ), 'The update should remain safe when the note is already absent.' );
+	}
+
 	/**
 	 * @testdox Migration invalidates the Analytics report cache, so a response cached before the update stops being served.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Admin/Notes/NotesTest.php b/plugins/woocommerce/tests/php/src/Admin/Notes/NotesTest.php
new file mode 100644
index 00000000000..5d831213315
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/Notes/NotesTest.php
@@ -0,0 +1,63 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\Notes;
+
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Admin\Notes\Notes;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for admin note cleanup.
+ */
+class NotesTest extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Survey cleanup runs daily instead of during admin initialization.
+	 */
+	public function test_survey_cleanup_runs_daily(): void {
+		$callback = array( Notes::class, 'possibly_delete_survey_notes' );
+
+		$this->assertFalse( has_action( 'admin_init', $callback ), 'Survey cleanup should not run during admin initialization.' );
+
+		// Priority 9 so the cleanup runs before Events::do_wc_admin_daily, which adds notes and
+		// refreshes the remote data source pollers at the default priority.
+		$this->assertSame( 9, has_action( 'wc_admin_daily', $callback ), 'Survey cleanup should run from the daily admin event, ahead of the fetch.' );
+	}
+
+	/**
+	 * @testdox Survey cleanup deletes only actioned survey notes.
+	 */
+	public function test_survey_cleanup_deletes_only_actioned_survey_notes(): void {
+		$actioned_note_id   = $this->create_note( 'phpunit-actioned-survey-note', Note::E_WC_ADMIN_NOTE_SURVEY, Note::E_WC_ADMIN_NOTE_ACTIONED );
+		$unactioned_note_id = $this->create_note( 'phpunit-unactioned-survey-note', Note::E_WC_ADMIN_NOTE_SURVEY, Note::E_WC_ADMIN_NOTE_UNACTIONED );
+		$other_note_id      = $this->create_note( 'phpunit-actioned-info-note', Note::E_WC_ADMIN_NOTE_INFORMATIONAL, Note::E_WC_ADMIN_NOTE_ACTIONED );
+
+		Notes::possibly_delete_survey_notes();
+
+		$this->assertTrue( ( new Note( $actioned_note_id ) )->get_is_deleted(), 'The actioned survey note should be deleted.' );
+		$this->assertFalse( ( new Note( $unactioned_note_id ) )->get_is_deleted(), 'The unactioned survey note should remain visible.' );
+		$this->assertFalse( ( new Note( $other_note_id ) )->get_is_deleted(), 'Actioned notes of other types should remain visible.' );
+	}
+
+	/**
+	 * Create an admin note.
+	 *
+	 * @param string $name   Note name.
+	 * @param string $type   Note type.
+	 * @param string $status Note status.
+	 * @return int Note ID.
+	 */
+	private function create_note( string $name, string $type, string $status ): int {
+		$note = new Note();
+		$note->set_name( $name );
+		$note->set_title( 'Test note' );
+		$note->set_content( 'Test content' );
+		$note->set_type( $type );
+		$note->set_source( 'PHPUNIT_TEST' );
+		$note->set_status( $status );
+		$note->save();
+
+		return $note->get_id();
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
index 09a218625cf..ae532bda43b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
@@ -88,6 +88,32 @@ class BootstrapTest extends WC_Unit_Test_Case {
 		$property->setValue( null, $has_run );
 	}

+	/**
+	 * @testdox Bootstrap does not register admin-init work that also runs during admin AJAX.
+	 */
+	public function test_bootstrap_registers_no_admin_init_callbacks(): void {
+		global $wp_filter;
+
+		$bootstrap_file = wp_normalize_path( WC_ABSPATH . 'src/Blocks/Domain/Bootstrap.php' );
+		$callbacks      = $wp_filter['admin_init']->callbacks ?? array();
+
+		foreach ( $callbacks as $callbacks_at_priority ) {
+			foreach ( $callbacks_at_priority as $callback ) {
+				$callback_function = $callback['function'] ?? null;
+				if ( ! is_callable( $callback_function ) ) {
+					continue;
+				}
+				$reflection = new \ReflectionFunction( \Closure::fromCallable( $callback_function ) );
+
+				$this->assertNotSame(
+					$bootstrap_file,
+					wp_normalize_path( (string) $reflection->getFileName() ),
+					'Bootstrap callbacks on admin_init also run during unrelated admin AJAX requests.'
+				);
+			}
+		}
+	}
+
 	/**
 	 * @testdox Bootstrap hooks on-demand block-type registration to woocommerce_short_description before do_blocks.
 	 */