Commit 36d8b2a7269 for woocommerce
commit 36d8b2a72696fdad3130686e4d6e7e5356e5a358
Author: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
Date: Tue Mar 10 16:10:42 2026 +0100
Fix: Add multiple variable products with "Any" in attributes (#63535)
* Create `findExistingCartItem` helper
* Read `attribute` in `doesCartItemMatchAttributes`
* Use `findExistingCartItem` in blocks
* Use helper in the product button quantity
* Create `itemInCart` derived state
* Add cart import
* Add a second variation in tests
* Add changefile(s) from automation for the following project(s): woocommerce
* Move eslint rule
* Change arguments pass to the function
* Prioritize always the key check
* Improve naming
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63535-wooplug-6375-add-to-cart-options-cant-add-two-different-variations-if b/plugins/woocommerce/changelog/63535-wooplug-6375-add-to-cart-options-cant-add-two-different-variations-if
new file mode 100644
index 00000000000..fce18db5d36
--- /dev/null
+++ b/plugins/woocommerce/changelog/63535-wooplug-6375-add-to-cart-options-cant-add-two-different-variations-if
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+Comment: Fix adding multiple variable products with "any" attributes
+
diff --git a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/button/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/button/frontend.ts
index 3fe96b1eae4..922dab937fd 100644
--- a/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/button/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/atomic/blocks/product-elements/button/frontend.ts
@@ -8,8 +8,10 @@ import type { ProductDataStore } from '@woocommerce/stores/woocommerce/product-d
/**
* Internal dependencies
*/
-import { doesCartItemMatchAttributes } from '../../../../base/utils/variations/does-cart-item-match-attributes';
-import type { AddToCartWithOptionsStore } from '../../../../blocks/add-to-cart-with-options/frontend';
+import type {
+ Context as AddToCartWithOptionsContext,
+ AddToCartWithOptionsStore,
+} from '../../../../blocks/add-to-cart-with-options/frontend';
// Stores are locked to prevent 3PD usage until the API is stable.
const universalLock =
@@ -63,32 +65,16 @@ const { state: productDataState } = store< ProductDataStore >(
const productButtonStore = {
state: {
get quantity(): number {
- const products = wooState.cart?.items.filter(
- ( item ) => item.id === state.productId
+ const formContext = getContext< AddToCartWithOptionsContext >(
+ 'woocommerce/add-to-cart-with-options'
);
- if ( products.length === 0 ) {
- return 0;
- }
-
- // Return the product quantity when the item is a non-variable product.
- if ( products[ 0 ]?.type !== 'variation' ) {
- return products.reduce(
- ( acc, item ) => acc + item.quantity,
- 0
- );
- }
+ const item = wooState.findItemInCart( {
+ id: state.productId,
+ variation: formContext?.selectedAttributes,
+ } );
- const selectedAttributes =
- addToCartWithOptionsState?.selectedAttributes;
- const selectedVariableProducts = products.filter( ( item ) =>
- doesCartItemMatchAttributes( item, selectedAttributes )
- );
-
- return selectedVariableProducts.reduce(
- ( acc, item ) => acc + item.quantity,
- 0
- );
+ return item?.quantity ?? 0;
},
get slideInAnimation() {
const { animationStatus } = getContext< Context >();
@@ -121,9 +107,9 @@ const productButtonStore = {
if ( productType === 'grouped' ) {
const groupedProductIdsInCart = groupedProductIds?.map(
( productId ) => {
- const product = wooState.cart?.items.find(
- ( item ) => item.id === productId
- );
+ const product = wooState.findItemInCart( {
+ id: productId,
+ } );
return product?.quantity || 0;
}
);
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 fcd160be9fd..ecca02fe331 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
@@ -26,6 +26,7 @@ import {
type MutationQueue,
type MutationResult,
} from './mutation-batcher';
+import { doesCartItemMatchAttributes } from '../../utils/variations/does-cart-item-match-attributes';
export type WooCommerceConfig = {
products?: {
@@ -101,6 +102,11 @@ export type Store = {
};
restUrl: string;
nonce: string;
+ findItemInCart: ( args: {
+ id: ClientCartItem[ 'id' ];
+ key?: ClientCartItem[ 'key' ];
+ variation?: ClientCartItem[ 'variation' ];
+ } ) => CartItem | OptimisticCartItem | undefined;
cart: Omit< Cart, 'items' > & {
items: ( OptimisticCartItem | CartItem )[];
totals: CartResponseTotals;
@@ -207,40 +213,6 @@ const getInfoNoticesFromCartUpdates = (
];
};
-// Same as the one in /assets/js/base/utils/variations/does-cart-item-match-attributes.ts.
-const doesCartItemMatchAttributes = (
- cartItem: OptimisticCartItem,
- selectedAttributes: SelectedAttributes[]
-) => {
- if (
- ! Array.isArray( cartItem.variation ) ||
- ! Array.isArray( selectedAttributes )
- ) {
- return false;
- }
-
- if ( cartItem.variation.length !== selectedAttributes.length ) {
- return false;
- }
-
- return cartItem.variation.every(
- ( {
- // eslint-disable-next-line
- raw_attribute,
- value,
- }: {
- raw_attribute: string;
- value: string;
- } ) =>
- selectedAttributes.some( ( item: SelectedAttributes ) => {
- return (
- item.attribute === raw_attribute &&
- item.value.toLowerCase() === value?.toLowerCase()
- );
- } )
- );
-};
-
let pendingRefresh = false;
let refreshTimeout = 3000;
let resolveNonceReady: ( () => void ) | null = null;
@@ -311,6 +283,38 @@ async function sendCartRequest(
const { state, actions } = store< Store >(
'woocommerce',
{
+ state: {
+ findItemInCart( {
+ id,
+ key,
+ variation,
+ }: {
+ id: ClientCartItem[ 'id' ];
+ key?: ClientCartItem[ 'key' ];
+ variation?: ClientCartItem[ 'variation' ];
+ } ) {
+ return state.cart.items.find( ( cartItem ) => {
+ if ( key ) {
+ return key === cartItem.key;
+ }
+ if ( cartItem.type === 'variation' ) {
+ if (
+ id !== cartItem.id ||
+ ! cartItem.variation ||
+ ! variation ||
+ cartItem.variation.length !== variation.length
+ ) {
+ return false;
+ }
+ return doesCartItemMatchAttributes(
+ cartItem,
+ variation
+ );
+ }
+ return id === cartItem.id;
+ } );
+ },
+ },
actions: {
*removeCartItem( key: string ): AsyncAction< void > {
// Track what changes we're making for the sync event.
@@ -377,22 +381,10 @@ const { state, actions } = store< Store >(
const a11yModulePromise = import( '@wordpress/a11y' );
// Find existing item
- const existingItem = state.cart.items.find( ( cartItem ) => {
- if ( cartItem.type === 'variation' ) {
- if (
- id !== cartItem.id ||
- ! cartItem.variation ||
- ! variation ||
- cartItem.variation.length !== variation.length
- ) {
- return false;
- }
- return doesCartItemMatchAttributes(
- cartItem,
- variation
- );
- }
- return key ? key === cartItem.key : id === cartItem.id;
+ const existingItem = state.findItemInCart( {
+ id,
+ key,
+ variation,
} );
// Determine the target quantity.
@@ -535,9 +527,11 @@ const { state, actions } = store< Store >(
// Submit each item through the batcher. They'll be
// collected into a single batch request automatically.
const promises = items.map( ( item, index ) => {
- const existingItem = state.cart.items.find(
- ( { id: productId } ) => item.id === productId
- );
+ const existingItem = state.findItemInCart( {
+ id: item.id,
+ key: item.key,
+ variation: item.variation,
+ } );
let quantity: number;
if ( typeof item.quantityToAdd === 'number' ) {
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts
index d6878faf126..c7e6b69a67f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts
@@ -28,17 +28,22 @@ export const doesCartItemMatchAttributes = (
return cartItem.variation.every(
( {
+ attribute,
// eslint-disable-next-line
raw_attribute,
value,
}: {
+ attribute: string;
raw_attribute: string;
value: string;
} ) =>
selectedAttributes.some( ( item: SelectedAttributes ) => {
return (
- attributeNamesMatch( item.attribute, raw_attribute ) &&
- item.value.toLowerCase() === value?.toLowerCase()
+ attributeNamesMatch(
+ item.attribute,
+ // It needs to check both because it uses different keys from the same value depending on the context.
+ raw_attribute ?? attribute
+ ) && item.value.toLowerCase() === value?.toLowerCase()
);
} )
);
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
index 23a68f5d94a..dbf28c9a39e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
@@ -445,12 +445,14 @@ const { state: cartItemState } = store(
// state.cartItem to get the cart item.
get cartItem() {
const {
- cartItem: { id, key },
+ cartItem: { id, key, variation },
} = getContext< CartItemContext >( 'woocommerce' );
- const cartItem = ( woocommerceState.cart.items.find( ( item ) =>
- key ? item.key === key : item.id === id
- ) || {} ) as CartItem;
+ const cartItem = ( woocommerceState.findItemInCart( {
+ id,
+ key,
+ variation,
+ } ) || {} ) as CartItem;
cartItem.variation = cartItem.variation || [];
cartItem.item_data = cartItem.item_data || [];
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
index 372deef19ec..d8854e73079 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
@@ -369,6 +369,11 @@ test.describe( 'Add to Cart + Options Block', () => {
await colorRedOption.click();
await expect( page.getByText( '1 in cart' ) ).toBeHidden();
+
+ // Add a second variation (Red + Large).
+ await addToCartButton.click();
+
+ await expect( page.getByText( '1 in cart' ) ).toBeVisible();
} );
test( 'allows adding grouped products to cart', async ( {