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;
+ }
+}