Commit fabe01e5b4a for woocommerce

commit fabe01e5b4a7e855ff692999d32a0c5d822442c4
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Thu Jun 4 16:56:48 2026 +0200

    Fix Add to Cart Button not working on Products (Beta) block (#65511)

    * Add changelog

    * Fix Add to Cart Button inside the Products (Query) block

    * Linting

diff --git a/plugins/woocommerce/changelog/fix-products-beta-add-to-cart-button b/plugins/woocommerce/changelog/fix-products-beta-add-to-cart-button
new file mode 100644
index 00000000000..f4e78f61325
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-products-beta-add-to-cart-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Add to Cart Button not working on Products (Beta) block
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/products/products.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/products/products.block_theme.spec.ts
index a376bd354d5..6c4427a6f9a 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/products/products.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/products/products.block_theme.spec.ts
@@ -97,6 +97,51 @@ test.describe( `${ blockData.name } Block `, () => {
 		await expect( advancedFilterOption ).toBeVisible();
 		await expect( inheritQueryFromTemplateOption ).toBeVisible();
 	} );
+
+	test( 'product button should add product to the cart when inheriting query from template', async ( {
+		admin,
+		editor,
+		page,
+		frontendUtils,
+	} ) => {
+		await admin.visitSiteEditor( {
+			postId: `${ BLOCK_THEME_SLUG }//archive-product`,
+			postType: 'wp_template',
+			canvas: 'edit',
+		} );
+		await editor.setContent( '' );
+		await insertProductsQuery( editor );
+		await editor.saveSiteEditorEntities( {
+			isOnlyCurrentEntityDirty: true,
+		} );
+		await frontendUtils.goToShop();
+
+		const addToCartButton = page.getByRole( 'button', {
+			name: 'Add to cart: “Single”',
+		} );
+		await addToCartButton.click();
+		await expect( addToCartButton ).toHaveText( '1 in cart' );
+		const cartLink = page.getByRole( 'link', { name: 'View cart' } );
+		await expect( cartLink ).toBeVisible();
+	} );
+
+	test( 'product button should add product to the cart when not inheriting query from template', async ( {
+		admin,
+		editor,
+		page,
+	} ) => {
+		await admin.createNewPost();
+		await insertProductsQuery( editor, { inherit: false } );
+		await editor.publishAndVisitPost();
+
+		const addToCartButton = page.getByRole( 'button', {
+			name: 'Add to cart: “Single”',
+		} );
+		await addToCartButton.click();
+		await expect( addToCartButton ).toHaveText( '1 in cart' );
+		const cartLink = page.getByRole( 'link', { name: 'View cart' } );
+		await expect( cartLink ).toBeVisible();
+	} );
 } );

 for ( const {
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/products/utils.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/products/utils.ts
index e5a71b72f26..dec142f1903 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/products/utils.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/products/utils.ts
@@ -38,13 +38,18 @@ export const productQueryInnerBlocksTemplate = [
 	{ name: 'core/query-no-results' },
 ];

-export const insertProductsQuery = async ( editor: Editor ) => {
+export const insertProductsQuery = async (
+	editor: Editor,
+	options: {
+		inherit?: boolean;
+	} = {}
+) => {
 	await editor.insertBlock( {
 		name: 'core/query',
 		attributes: {
 			namespace: 'woocommerce/product-query',
 			query: {
-				inherit: true,
+				inherit: options.inherit ?? true,
 			},
 		},
 		innerBlocks: productQueryInnerBlocksTemplate,
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
index 68f0b068e12..c88541c7ad2 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
@@ -82,6 +82,12 @@ class ProductQuery extends AbstractBlock {
 			10,
 			2
 		);
+		add_filter(
+			'render_block_core/post-template',
+			array( $this, 'add_iapi_context' ),
+			10,
+			2
+		);
 		add_filter( 'rest_product_query', array( $this, 'update_rest_query' ), 10, 2 );
 		add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
 	}
@@ -166,6 +172,86 @@ class ProductQuery extends AbstractBlock {
 		return $block_content;
 	}

+	/**
+	 * Add product interactivity directives to each loop item in Products (Beta).
+	 *
+	 * @internal
+	 *
+	 * @param string $block_content Rendered block HTML.
+	 * @param array  $block         Parsed block data.
+	 * @return string
+	 */
+	public function add_iapi_context( string $block_content, array $block ): string {
+		$namespace = $block['attrs']['__woocommerceNamespace'] ?? '';
+		if ( 'woocommerce/product-query/product-template' !== $namespace ) {
+			return $block_content;
+		}
+
+		$processor = new \WP_HTML_Tag_Processor( $block_content );
+
+		while (
+			$processor->next_tag(
+				array(
+					'tag_name'   => 'LI',
+					'class_name' => 'wp-block-post',
+				)
+			)
+		) {
+			$class_attribute = $processor->get_attribute( 'class' );
+			$product_id      = $this->get_product_id_from_class_attribute( is_string( $class_attribute ) ? $class_attribute : '' );
+
+			if ( ! $product_id || 'product' !== get_post_type( $product_id ) ) {
+				continue;
+			}
+
+			wc_interactivity_api_load_product( 'I acknowledge that using experimental APIs means my theme or plugin will inevitably break in the next version of WooCommerce', $product_id );
+
+			$product_context = array(
+				'productId'   => $product_id,
+				'variationId' => null,
+			);
+
+			$processor->set_attribute( 'data-wp-interactive', 'woocommerce/products' );
+			$processor->set_attribute(
+				'data-wp-context',
+				'woocommerce/products::' . wp_json_encode( $product_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP )
+			);
+			$processor->set_attribute( 'data-wp-key', 'product-item-' . $product_id );
+		}
+
+		return $processor->get_updated_html();
+	}
+
+	/**
+	 * Extract a post ID from the `post-{id}` class WordPress adds to loop items.
+	 *
+	 * @internal
+	 *
+	 * @param string $class_attribute The element class attribute.
+	 * @return int|null
+	 */
+	private function get_product_id_from_class_attribute( $class_attribute ): ?int {
+		if ( '' === $class_attribute ) {
+			return null;
+		}
+
+		$classes = explode( ' ', $class_attribute );
+
+		foreach ( $classes as $class ) {
+			if ( ! str_starts_with( $class, 'post-' ) ) {
+				continue;
+			}
+
+			$product_id = (int) substr( $class, 5 );
+
+			if ( $product_id > 0 ) {
+				return $product_id;
+			}
+		}
+
+		return null;
+	}
+
 	/**
 	 * Update the query for the product query block.
 	 *