Commit 2bf97fe9a9a for woocommerce

commit 2bf97fe9a9ab12d9c8f5b367910e127dc9109be9
Author: Jonathan Bouganim <jbouganim@users.noreply.github.com>
Date:   Fri Oct 2 05:09:34 2026 -0400

    Fix iAPI cart store request loop on pages without its interactivity state (#66951)

diff --git a/plugins/woocommerce/changelog/fix-iapi-cart-refresh-without-resturl b/plugins/woocommerce/changelog/fix-iapi-cart-refresh-without-resturl
new file mode 100644
index 00000000000..3d96da1cf85
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-iapi-cart-refresh-without-resturl
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Only enqueue the Mini-Cart view script module when the block is interactive, so it no longer loads on the cart and checkout pages, and prevent the iAPI cart store from firing a malformed cart fetch retry loop when its interactivity state is not registered.
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts
index ecc49ad6ee2..d15da31aa05 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts
@@ -1166,6 +1166,19 @@ const { actions } = store< Store >(
 			},

 			*refreshCartItems(): AsyncAction< void > {
+				// Skip when the server did not provide the interactivity
+				// state this store needs, e.g. when a block imports this
+				// store without loading the shared cart state. Release
+				// pending mutations so they fail visibly instead of
+				// waiting forever for a nonce that will never arrive.
+				if ( ! state.restUrl ) {
+					if ( resolveNonceReady ) {
+						resolveNonceReady();
+						resolveNonceReady = null;
+					}
+					return;
+				}
+
 				// Skip if queue is processing - it will apply server state when done
 				if ( cartQueue?.getStatus().isProcessing ) {
 					return;
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/test/cart.ts b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/test/cart.ts
index b12b68a78b8..82f036389a3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/test/cart.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/test/cart.ts
@@ -88,7 +88,6 @@ async function runAction( action: unknown ): Promise< unknown > {
 	let next = iterator.next();
 	while ( ! next.done ) {
 		try {
-			// eslint-disable-next-line no-await-in-loop
 			const resolved = await next.value;
 			next = iterator.next( resolved );
 		} catch ( error ) {
@@ -530,6 +529,52 @@ describe( 'WooCommerce Cart Interactivity API Store', () => {
 		);
 	} );

+	describe( 'refreshCartItems without restUrl in the interactivity state', () => {
+		const originalRestUrl = mockState.restUrl;
+
+		afterEach( () => {
+			mockState.restUrl = originalRestUrl;
+		} );
+
+		it( 'skips the cart fetch instead of requesting a malformed URL', async () => {
+			mockState.restUrl = '';
+			const mockFetch = jest.fn();
+			global.fetch = mockFetch;
+
+			jest.isolateModules( () => require( '../cart' ) );
+			await runAction( mockRegisteredStore?.actions.refreshCartItems() );
+
+			expect( mockFetch ).not.toHaveBeenCalled();
+		} );
+
+		it( 'lets a queued mutation settle instead of waiting forever for a nonce', async () => {
+			mockState.restUrl = '';
+			global.fetch = jest
+				.fn()
+				.mockRejectedValue( new TypeError( 'Failed to fetch' ) );
+
+			jest.isolateModules( () => require( '../cart' ) );
+			const actions = mockRegisteredStore?.actions as Store[ 'actions' ];
+			await runAction( actions.refreshCartItems() );
+			seedCart( [] );
+
+			const outcome = await Promise.race( [
+				runAction(
+					actions.addCartItem( {
+						id: 42,
+						quantityToAdd: 1,
+						type: 'simple',
+					} )
+				).then( () => 'settled' ),
+				new Promise( ( resolve ) =>
+					setTimeout( () => resolve( 'pending' ), 500 )
+				),
+			] );
+
+			expect( outcome ).toBe( 'settled' );
+		} );
+	} );
+
 	describe( 'addCartItem endpoint selection', () => {
 		it( 'issues add-item (never update-item) for a keyless add that matches a keyed line by product id', async () => {
 			const captured = mockBatchFetch();
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/block.json
index 67e5a008276..36b22b17746 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/block.json
@@ -6,7 +6,6 @@
 	"category": "woocommerce",
 	"keywords": [ "WooCommerce" ],
 	"textdomain": "woocommerce",
-	"viewScriptModule": "woocommerce/mini-cart",
 	"style": "file:../woocommerce/mini-cart-style.css",
 	"editorStyle": "file:../woocommerce/mini-cart-editor.css",
 	"supports": {
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
index 2f980028ab0..334b527f537 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
@@ -278,6 +278,8 @@ class MiniCart extends AbstractBlock {
 			return $content . $this->get_markup( MiniCartUtils::migrate_attributes_to_color_panel( $attributes ) );
 		}

+		// The view script module is deliberately not declared in block.json, so it is only
+		// enqueued here and never on the cart and checkout pages handled above.
 		wp_enqueue_script_module( $this->get_full_block_name() );

 		// Enqueue all integration scripts registered for this block.
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php
index 17fcba41ea6..400cc6fbb66 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php
@@ -518,4 +518,40 @@ class MiniCart extends \WP_UnitTestCase {
 		update_option( 'woocommerce_coming_soon', 'no' );
 		update_option( 'woocommerce_store_pages_only', 'no' );
 	}
+
+	/**
+	 * @testdox Should not enqueue the view script module when rendered on the cart or checkout page.
+	 *
+	 * @testWith [ "woocommerce_is_cart" ]
+	 *           [ "woocommerce_is_checkout" ]
+	 *
+	 * @param string $page_filter Conditional filter forcing the cart or checkout context.
+	 */
+	public function test_view_script_module_is_not_enqueued_on_cart_and_checkout_pages( string $page_filter ): void {
+		wp_dequeue_script_module( 'woocommerce/mini-cart' );
+		add_filter( $page_filter, '__return_true' );
+
+		$block = parse_blocks( '<!-- wp:woocommerce/mini-cart /-->' );
+		render_block( $block[0] );
+
+		$this->assertNotContains( 'woocommerce/mini-cart', $this->get_enqueued_script_module_ids(), 'View script module should not be enqueued on the cart and checkout pages.' );
+	}
+
+	/**
+	 * @testdox Should not declare a view script module so WordPress never enqueues it automatically.
+	 */
+	public function test_view_script_module_is_not_declared_on_the_block_type(): void {
+		$block_type = \WP_Block_Type_Registry::get_instance()->get_registered( 'woocommerce/mini-cart' );
+
+		$this->assertSame( array(), $block_type->view_script_module_ids, 'The view script module must only be enqueued explicitly from render().' );
+	}
+
+	/**
+	 * Return the identifiers of the currently enqueued script modules.
+	 *
+	 * @return string[]
+	 */
+	private function get_enqueued_script_module_ids(): array {
+		return wp_script_modules()->get_queue();
+	}
 }