Commit 1791b9aaca for woocommerce

commit 1791b9aaca1b055d5197ab325aa8f9efcc8f1615
Author: Sam Seay <samueljseay@gmail.com>
Date:   Fri Jan 16 17:42:33 2026 +1300

    Add experimental `wc_interactivity_api_load_product` API for hydrating product state for iAPI powered blocks. (#62167)

diff --git a/plugins/woocommerce/changelog/62167-dev-use-server-state-simple-product b/plugins/woocommerce/changelog/62167-dev-use-server-state-simple-product
new file mode 100644
index 0000000000..066b5c170a
--- /dev/null
+++ b/plugins/woocommerce/changelog/62167-dev-use-server-state-simple-product
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add experimental wc_interactivity_api_load_product API for hydrating product state for iAPI powered blocks.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
index 69885603a6..e397461b09 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
@@ -21,6 +21,7 @@ import type { GroupedProductAddToCartWithOptionsStore } from './grouped-product-
 import type { Context as QuantitySelectorContext } from './quantity-selector/frontend';
 import type { VariableProductAddToCartWithOptionsStore } from './variation-selector/frontend';
 import type { NormalizedProductData, NormalizedVariationData } from './types';
+import type { ProductResponseItem } from '../../types';

 export type Context = {
 	selectedAttributes: SelectedAttributes[];
@@ -69,10 +70,67 @@ const { state: productDataState } = store< ProductDataStore >(
 	{ lock: universalLock }
 );

+const { state: productsStoreState } = store( 'woocommerce/products' );
+
+/**
+ * Helper to extract quantity constraints from product data.
+ * Supports both REST API format (new shared store) and config format (legacy).
+ *
+ * @param product Product data in either format
+ * @return Quantity constraints
+ */
+const getQuantityConstraints = ( product: ProductResponseItem ) => {
+	// New format (REST API from shared store)
+	if ( product.add_to_cart ) {
+		const maximum = product.add_to_cart.maximum ?? Number.MAX_SAFE_INTEGER;
+		return {
+			min: product.add_to_cart.minimum ?? 1,
+			max: maximum > 0 ? maximum : Number.MAX_SAFE_INTEGER,
+			step: product.add_to_cart.multiple_of ?? 1,
+		};
+	}
+
+	// Old format (config)
+	return {
+		min: typeof product.min === 'number' ? product.min : 1,
+		max:
+			typeof product.max === 'number'
+				? Math.max( product.max, 0 )
+				: Number.MAX_SAFE_INTEGER,
+		step:
+			typeof product.step === 'number' && product.step > 0
+				? product.step
+				: 1,
+	};
+};
+
 export const getProductData = (
 	id: number,
 	selectedAttributes: SelectedAttributes[]
 ): NormalizedProductData | NormalizedVariationData | null => {
+	// Try to get product from the new shared store first (for simple products)
+	try {
+		if ( productsStoreState?.products?.[ id ] ) {
+			const productFromStore = productsStoreState.products[ id ];
+
+			// Return REST API format directly with normalized constraints
+			const constraints = getQuantityConstraints( productFromStore );
+
+			return {
+				id,
+				type: productFromStore.type,
+				is_in_stock:
+					productFromStore.is_purchasable &&
+					productFromStore.is_in_stock,
+				sold_individually: productFromStore.sold_individually,
+				...constraints,
+			};
+		}
+	} catch ( error ) {
+		// If the store doesn't exist or there's an error, fall through to config
+	}
+
+	// Fall back to existing config approach for variable/grouped products
 	const { products } = getConfig( 'woocommerce' ) as WooCommerceConfig;

 	if ( ! products || ! products[ id ] ) {
@@ -102,17 +160,11 @@ export const getProductData = (
 		}
 	}

-	const min = typeof product.min === 'number' ? product.min : 1;
-	const max =
-		typeof product.max === 'number' ? Math.max( product.max, 0 ) : Infinity;
-	const step =
-		typeof product.step === 'number' && product.step > 0 ? product.step : 1;
+	const constraints = getQuantityConstraints( product );

 	return {
 		...product,
-		min,
-		max,
-		step,
+		...constraints,
 	};
 };

diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php
index a915bba9e6..31ac208216 100644
--- a/plugins/woocommerce/includes/wc-core-functions.php
+++ b/plugins/woocommerce/includes/wc-core-functions.php
@@ -34,6 +34,7 @@ require WC_ABSPATH . 'includes/wc-rest-functions.php';
 require WC_ABSPATH . 'includes/wc-widget-functions.php';
 require WC_ABSPATH . 'includes/wc-webhook-functions.php';
 require WC_ABSPATH . 'includes/wc-order-step-logger-functions.php';
+require WC_ABSPATH . 'includes/wc-interactivity-api-functions.php';

 /**
  * Filters on data used in admin and frontend.
diff --git a/plugins/woocommerce/includes/wc-interactivity-api-functions.php b/plugins/woocommerce/includes/wc-interactivity-api-functions.php
new file mode 100644
index 0000000000..5df6113ef9
--- /dev/null
+++ b/plugins/woocommerce/includes/wc-interactivity-api-functions.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * WooCommerce Interactivity API Functions
+ *
+ * Procedural wrappers for interactivity API shared stores.
+ * These are experimental APIs and may change in future versions.
+ *
+ * @package WooCommerce\Functions
+ */
+
+declare( strict_types = 1 );
+
+use Automattic\WooCommerce\Blocks\SharedStores\ProductsStore;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Load a product into the interactivity API state.
+ *
+ * This is an experimental API and may change in future versions.
+ *
+ * @param string $consent_statement The consent statement acknowledging this is an experimental API.
+ * @param int    $product_id        The product ID to load.
+ * @return array The product data.
+ * @throws InvalidArgumentException If consent statement doesn't match.
+ */
+function wc_interactivity_api_load_product( string $consent_statement, int $product_id ): array {
+	return ProductsStore::load_product( $consent_statement, $product_id );
+}
+
+/**
+ * Load all purchasable child products of a parent product into the interactivity API state.
+ *
+ * This is an experimental API and may change in future versions.
+ *
+ * @param string $consent_statement The consent statement acknowledging this is an experimental API.
+ * @param int    $parent_id         The parent product ID.
+ * @return array The purchasable child products keyed by ID.
+ * @throws InvalidArgumentException If consent statement doesn't match.
+ */
+function wc_interactivity_api_load_purchasable_child_products( string $consent_statement, int $parent_id ): array {
+	return ProductsStore::load_purchasable_child_products( $consent_statement, $parent_id );
+}
+
+/**
+ * Load all variations of a variable product into the interactivity API state.
+ *
+ * This is an experimental API and may change in future versions.
+ *
+ * @param string $consent_statement The consent statement acknowledging this is an experimental API.
+ * @param int    $parent_id         The parent product ID.
+ * @return array The variations keyed by ID.
+ * @throws InvalidArgumentException If consent statement doesn't match.
+ */
+function wc_interactivity_api_load_variations( string $consent_statement, int $parent_id ): array {
+	return ProductsStore::load_variations( $consent_statement, $parent_id );
+}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
index abdbe1a2a1..ec3b3bc0eb 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
@@ -281,18 +281,28 @@ class AddToCartWithOptions extends AbstractBlock {
 				)
 			);

-			wp_interactivity_config(
-				'woocommerce',
-				array(
-					'products' => array(
-						$product->get_id() => array(
-							'type'              => $product->get_type(),
-							'is_in_stock'       => $product->is_in_stock(),
-							'sold_individually' => $product->is_sold_individually(),
+			// Load product data based on product type.
+			if ( ProductType::SIMPLE === $product_type ) {
+				// Simple products use the new shared store with full REST API data.
+				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->get_id()
+				);
+			} else {
+				// Variable/grouped products continue using config.
+				wp_interactivity_config(
+					'woocommerce',
+					array(
+						'products' => array(
+							$product->get_id() => array(
+								'type'              => $product->get_type(),
+								'is_in_stock'       => $product->is_in_stock(),
+								'sold_individually' => $product->is_sold_individually(),
+							),
 						),
-					),
-				)
-			);
+					)
+				);
+			}

 			$context = array(
 				'quantity'         => array( $product->get_id() => $default_quantity ),
diff --git a/plugins/woocommerce/src/Blocks/SharedStores/ProductsStore.php b/plugins/woocommerce/src/Blocks/SharedStores/ProductsStore.php
new file mode 100644
index 0000000000..528ce4ba77
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/SharedStores/ProductsStore.php
@@ -0,0 +1,162 @@
+<?php
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Blocks\SharedStores;
+
+use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
+use Automattic\WooCommerce\Blocks\Package;
+use InvalidArgumentException;
+
+/**
+ * Manages the registration of interactivity state that provides product data
+ * to interactive blocks. This is shared store data that is not tied to one
+ * specific block.
+ *
+ * This is an experimental API and may change in future versions.
+ */
+class ProductsStore {
+
+	/**
+	 * The consent statement for using this experimental API.
+	 *
+	 * @var string
+	 */
+	private static string $consent_statement = 'I acknowledge that using experimental APIs means my theme or plugin will inevitably break in the next version of WooCommerce';
+
+	/**
+	 * The namespace for the store.
+	 *
+	 * @var string
+	 */
+	private static string $store_namespace = 'woocommerce/products';
+
+	/**
+	 * Products that have been loaded into state.
+	 *
+	 * @var array
+	 */
+	private static array $products = array();
+
+	/**
+	 * Product variations that have been loaded into state.
+	 *
+	 * @var array
+	 */
+	private static array $product_variations = array();
+
+	/**
+	 * Check that the consent statement was passed.
+	 *
+	 * @param string $consent_statement The consent statement string.
+	 * @return true
+	 * @throws InvalidArgumentException If the statement does not match.
+	 */
+	private static function check_consent( string $consent_statement ): bool {
+		if ( $consent_statement !== self::$consent_statement ) {
+			throw new InvalidArgumentException( 'This method cannot be called without consenting that the API may change.' );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Register the interactivity state if products have been loaded.
+	 *
+	 * @return void
+	 */
+	private static function register_state(): void {
+		$state = array();
+
+		if ( ! empty( self::$products ) ) {
+			$state['products'] = self::$products;
+		}
+
+		if ( ! empty( self::$product_variations ) ) {
+			$state['productVariations'] = self::$product_variations;
+		}
+
+		if ( ! empty( $state ) ) {
+			wp_interactivity_state( self::$store_namespace, $state );
+		}
+	}
+
+	/**
+	 * Load a product into state.
+	 *
+	 * @param string $consent_statement The consent statement string.
+	 * @param int    $product_id        The product ID.
+	 * @return array The product data.
+	 * @throws InvalidArgumentException If consent statement doesn't match.
+	 */
+	public static function load_product( string $consent_statement, int $product_id ): array {
+		self::check_consent( $consent_statement );
+
+		// Skip loading if product is already in state.
+		if ( isset( self::$products[ $product_id ] ) ) {
+			return self::$products[ $product_id ];
+		}
+
+		$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products/' . $product_id );
+
+		self::$products[ $product_id ] = $response['body'] ?? array();
+		self::register_state();
+
+		return self::$products[ $product_id ];
+	}
+
+	/**
+	 * Load all purchasable child products of a parent product into state.
+	 *
+	 * @param string $consent_statement The consent statement string.
+	 * @param int    $parent_id         The parent product ID.
+	 * @return array The purchasable child products keyed by ID.
+	 * @throws InvalidArgumentException If consent statement doesn't match.
+	 */
+	public static function load_purchasable_child_products( string $consent_statement, int $parent_id ): array {
+		self::check_consent( $consent_statement );
+
+		$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products?parent[]=' . $parent_id );
+
+		if ( empty( $response['body'] ) ) {
+			return array();
+		}
+
+		// Filter to only purchasable products.
+		$purchasable_products = array_filter(
+			$response['body'],
+			fn( $product ) => $product['is_purchasable']
+		);
+
+		// Re-key array by product ID and merge into state.
+		$keyed_products = array_column( $purchasable_products, null, 'id' );
+		self::$products = array_merge( self::$products, $keyed_products );
+		self::register_state();
+
+		return $keyed_products;
+	}
+
+	/**
+	 * Load all variations of a variable product into state.
+	 *
+	 * @param string $consent_statement The consent statement string.
+	 * @param int    $parent_id         The parent product ID.
+	 * @return array The variations keyed by ID.
+	 * @throws InvalidArgumentException If consent statement doesn't match.
+	 */
+	public static function load_variations( string $consent_statement, int $parent_id ): array {
+		self::check_consent( $consent_statement );
+
+		$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products?parent[]=' . $parent_id . '&type=variation' );
+
+		if ( empty( $response['body'] ) ) {
+			return array();
+		}
+
+		// Re-key array by variation ID and merge into state.
+		$keyed_variations         = array_column( $response['body'], null, 'id' );
+		self::$product_variations = array_merge( self::$product_variations, $keyed_variations );
+		self::register_state();
+
+		return $keyed_variations;
+	}
+}