Commit 1af73caf45 for woocommerce

commit 1af73caf45abe14fb714e907e4304b162aead5c7
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date:   Tue Feb 3 16:18:00 2026 +0000

    Move product sale badge alongside individual prices in cart and update design (#63012)

    * Make badge use theme colour as background, and small font size

    * Prevent discount showing twice on mini cart and line prices vertically with button

    * Add changelog

    * Only show individual save badge if quantity is 1 in cart block

    * Remove old badge after merge

    * Add state for single item discount badge visibility

    * allow badge onto second line on Checkout order summary if small

    * Move mini-cart badge back in line after merge conflict

    * Add sale badges to checkout order summary

    * Make remove button use theme colour

    * Align price content centrally

    * Don't hide sale badge on mobile

    * Align items in checkout order summary

    * Extract sale amount calculation to shared util

    * Only show quantity on right under item total price

    * Remove low stock badge from cart line items

    * Remove individual saving and low stock badge in mini cart

    * Remove low stock badge and sale badge from checkout order summary

    * Reduce spacing under title

    * Lint fix

diff --git a/plugins/woocommerce/changelog/wooprd-1450-saved-amount-badges b/plugins/woocommerce/changelog/wooprd-1450-saved-amount-badges
new file mode 100644
index 0000000000..21cc7f4f06
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooprd-1450-saved-amount-badges
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Move the "Save $X" badges in Cart/Mini-cart alongside the individual item prices to save vertical space.
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
index d7acc27190..a15efeb3ac 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
@@ -23,13 +23,13 @@ import type { CartItem } from '@woocommerce/types';
 import { objectHasProp, Currency } from '@woocommerce/types';
 import { getSetting } from '@woocommerce/settings';
 import { Icon, trash } from '@wordpress/icons';
+import { calculateSaleAmount } from '@woocommerce/base-utils';

 /**
  * Internal dependencies
  */
 import ProductBackorderBadge from '../product-backorder-badge';
 import ProductImage from '../product-image';
-import ProductLowStockBadge from '../product-low-stock-badge';
 import ProductMetadata from '../product-metadata';
 import ProductSaleBadge from '../product-sale-badge';

@@ -68,7 +68,6 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 			catalog_visibility: catalogVisibility = 'visible',
 			short_description: shortDescription = '',
 			description: fullDescription = '',
-			low_stock_remaining: lowStockRemaining = null,
 			show_backorder_badge: showBackorderBadge = false,
 			quantity_limits: quantityLimits = {
 				minimum: 1,
@@ -146,9 +145,10 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 			amount: parseInt( prices.raw_prices.price, 10 ),
 			precision: prices.raw_prices.precision,
 		} );
-		const saleAmountSingle =
-			regularAmountSingle.subtract( purchaseAmountSingle );
-		const saleAmount = saleAmountSingle.multiply( quantity );
+		const saleAmountSingle = calculateSaleAmount(
+			prices,
+			priceCurrency.minorUnit
+		);
 		const totalsCurrency = getCurrencyFromPriceResponse( totals );
 		let lineSubtotal = parseInt( totals.line_subtotal, 10 );
 		if ( getSetting( 'displayCartPricesIncludingTax', false ) ) {
@@ -245,15 +245,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 							name={ name }
 							permalink={ permalink }
 						/>
-						{ showBackorderBadge ? (
-							<ProductBackorderBadge />
-						) : (
-							!! lowStockRemaining && (
-								<ProductLowStockBadge
-									lowStockRemaining={ lowStockRemaining }
-								/>
-							)
-						) }
+						{ showBackorderBadge && <ProductBackorderBadge /> }

 						<div className="wc-block-cart-item__prices">
 							<ProductPrice
@@ -270,15 +262,6 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 							/>
 						</div>

-						<ProductSaleBadge
-							currency={ priceCurrency }
-							saleAmount={ getAmountFromRawPrice(
-								saleAmountSingle,
-								priceCurrency
-							) }
-							format={ saleBadgePriceFormat }
-						/>
-
 						<ProductMetadata
 							shortDescription={ shortDescription }
 							fullDescription={ fullDescription }
@@ -356,16 +339,11 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 							price={ subtotalPrice.getAmount() }
 						/>

-						{ quantity > 1 && (
-							<ProductSaleBadge
-								currency={ priceCurrency }
-								saleAmount={ getAmountFromRawPrice(
-									saleAmount,
-									priceCurrency
-								) }
-								format={ saleBadgePriceFormat }
-							/>
-						) }
+						<ProductSaleBadge
+							currency={ priceCurrency }
+							saleAmount={ saleAmountSingle * quantity }
+							format={ saleBadgePriceFormat }
+						/>
 					</div>
 				</td>
 			</tr>
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss
index d95c773fd9..b759f53b80 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/style.scss
@@ -45,6 +45,24 @@ table.wc-block-cart-items {
 		}
 		.wc-block-cart-item__prices {
 			@include font-small-locked;
+			display: flex;
+			flex-direction: row;
+			flex-wrap: nowrap;
+			gap: $gap-smaller;
+			align-items: center;
+
+			@include cart-checkout-mobile-container {
+				flex-wrap: wrap;
+			}
+
+			.price {
+				display: flex;
+				align-items: center;
+
+				&[hidden] {
+					display: none;
+				}
+			}
 		}

 		.wc-block-cart-item__quantity {
@@ -64,6 +82,10 @@ table.wc-block-cart-items {
 				&[hidden] {
 					display: none;
 				}
+
+				svg {
+					fill: currentColor;
+				}
 			}
 		}
 		.wc-block-components-product-name {
@@ -71,6 +93,8 @@ table.wc-block-cart-items {
 			display: block;
 			max-width: max-content;
 			line-height: 1.4;
+			margin-bottom: 5px;
+
 			&[hidden] {
 				display: none;
 			}
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx
index 5908ba9944..367153919c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/order-summary-item.tsx
@@ -19,14 +19,15 @@ import { getSetting } from '@woocommerce/settings';
 import { useMemo } from '@wordpress/element';
 import { useStoreCart } from '@woocommerce/base-context/hooks';
 import { CartItem, isString } from '@woocommerce/types';
+import { calculateSaleAmount } from '@woocommerce/base-utils';

 /**
  * Internal dependencies
  */
 import ProductBackorderBadge from '../product-backorder-badge';
 import ProductImage from '../product-image';
-import ProductLowStockBadge from '../product-low-stock-badge';
 import ProductMetadata from '../product-metadata';
+import ProductSaleBadge from '../product-sale-badge';

 interface OrderSummaryProps {
 	cartItem: CartItem;
@@ -39,7 +40,6 @@ const OrderSummaryItem = ( {
 }: OrderSummaryProps ): JSX.Element => {
 	const {
 		images,
-		low_stock_remaining: lowStockRemaining,
 		show_backorder_badge: showBackorderBadge,
 		name: initialName,
 		permalink,
@@ -102,6 +102,12 @@ const OrderSummaryItem = ( {
 		amount: lineSubtotal,
 		precision: totalsCurrency.minorUnit,
 	} ).getAmount();
+
+	const saleAmountSingle = calculateSaleAmount(
+		prices,
+		priceCurrency.minorUnit
+	);
+
 	const subtotalPriceFormat = applyCheckoutFilter( {
 		filterName: 'subtotalPriceFormat',
 		defaultValue: '<price/>',
@@ -110,6 +116,14 @@ const OrderSummaryItem = ( {
 		validation: productPriceValidation,
 	} );

+	const saleBadgePriceFormat = applyCheckoutFilter( {
+		filterName: 'saleBadgePriceFormat',
+		defaultValue: '<price/>',
+		extensions,
+		arg,
+		validation: productPriceValidation,
+	} );
+
 	// Allow extensions to filter how the price is displayed. Ie: prepending or appending some values.
 	const productPriceFormat = applyCheckoutFilter( {
 		filterName: 'cartItemPrice',
@@ -175,24 +189,18 @@ const OrderSummaryItem = ( {
 					permalink={ permalink }
 					disabledTagName="h3"
 				/>
-				<ProductPrice
-					currency={ priceCurrency }
-					price={ priceSingle }
-					regularPrice={ regularPriceSingle }
-					className="wc-block-components-order-summary-item__individual-prices"
-					priceClassName="wc-block-components-order-summary-item__individual-price"
-					regularPriceClassName="wc-block-components-order-summary-item__regular-individual-price"
-					format={ subtotalPriceFormat }
-				/>
-				{ showBackorderBadge ? (
-					<ProductBackorderBadge />
-				) : (
-					!! lowStockRemaining && (
-						<ProductLowStockBadge
-							lowStockRemaining={ lowStockRemaining }
-						/>
-					)
-				) }
+				<div className="wc-block-cart-item__prices">
+					<ProductPrice
+						currency={ priceCurrency }
+						price={ priceSingle }
+						regularPrice={ regularPriceSingle }
+						className="wc-block-components-order-summary-item__individual-prices"
+						priceClassName="wc-block-components-order-summary-item__individual-price"
+						regularPriceClassName="wc-block-components-order-summary-item__regular-individual-price"
+						format={ subtotalPriceFormat }
+					/>
+				</div>
+				{ showBackorderBadge && <ProductBackorderBadge /> }
 				<ProductMetadata { ...productMetaProps } />
 			</div>
 			<span className="screen-reader-text">
@@ -213,11 +221,18 @@ const OrderSummaryItem = ( {
 				className="wc-block-components-order-summary-item__total-price"
 				aria-hidden="true"
 			>
-				<ProductPrice
-					currency={ totalsCurrency }
-					format={ productPriceFormat }
-					price={ subtotalPrice }
-				/>
+				<div className="wc-block-cart-item__total-price-and-sale-badge-wrapper">
+					<ProductPrice
+						currency={ totalsCurrency }
+						format={ productPriceFormat }
+						price={ subtotalPrice }
+					/>
+					<ProductSaleBadge
+						currency={ priceCurrency }
+						saleAmount={ saleAmountSingle * quantity }
+						format={ saleBadgePriceFormat }
+					/>
+				</div>
 			</div>
 		</div>
 	);
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss
index 4631205628..d3bd823257 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/order-summary/style.scss
@@ -98,6 +98,35 @@
 		}
 	}

+	.wc-block-cart-item__prices {
+		display: flex;
+		flex-direction: row;
+		flex-wrap: wrap;
+		gap: $gap-smaller;
+		align-items: center;
+	}
+
+	.wc-block-components-order-summary-item__individual-prices {
+		del {
+			margin-right: 0.5em;
+		}
+
+		ins {
+			display: inline-block;
+			margin-left: 0;
+		}
+	}
+
+	.wc-block-cart-item__total-price-and-sale-badge-wrapper {
+		display: flex;
+		flex-direction: column;
+		align-items: flex-end;
+
+		.wc-block-components-sale-badge {
+			margin-top: $gap-smallest;
+		}
+	}
+
 	.wc-block-components-order-summary-item__total-price {
 		margin-left: auto;
 		text-align: right;
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss
index 1a8021d74f..a49e29e11f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-badge/style.scss
@@ -1,11 +1,8 @@
 .wc-block-components-product-badge {
-	@include font-size(smaller);
-	border-radius: $universal-border-radius;
-	border: 1px solid;
+	@include font-x-small-locked;
 	display: inline-block;
-	font-weight: 600;
-	padding: 0 0.66em;
-	text-transform: uppercase;
+	background: color-mix(in srgb, currentColor 10%, transparent);
+	padding: 2px 4px;
 	white-space: nowrap;

 	&[hidden] {
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/calculate-sale-amount.ts b/plugins/woocommerce/client/blocks/assets/js/base/utils/calculate-sale-amount.ts
new file mode 100644
index 0000000000..74ec6dae5e
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/calculate-sale-amount.ts
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import Dinero from 'dinero.js';
+import type { CartItem } from '@woocommerce/types';
+
+/**
+ * Calculate the per-unit sale amount from raw prices.
+ *
+ * @param prices          Cart item prices containing raw_prices.
+ * @param targetPrecision The target currency minor unit precision.
+ * @return Per-unit sale amount as a number, or 0 if no discount.
+ */
+export function calculateSaleAmount(
+	prices: CartItem[ 'prices' ],
+	targetPrecision: number
+): number {
+	const rawPrecision =
+		typeof prices.raw_prices.precision === 'string'
+			? parseInt( prices.raw_prices.precision, 10 )
+			: prices.raw_prices.precision;
+
+	const regular = Dinero( {
+		amount: parseInt( prices.raw_prices.regular_price, 10 ),
+		precision: rawPrecision,
+	} );
+
+	const purchase = Dinero( {
+		amount: parseInt( prices.raw_prices.price, 10 ),
+		precision: rawPrecision,
+	} );
+
+	return regular
+		.subtract( purchase )
+		.convertPrecision( targetPrecision )
+		.getAmount();
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js b/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js
index deefaf74e5..d2a6219304 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/index.js
@@ -1,5 +1,6 @@
 export * from './errors';
 export * from './address';
+export * from './calculate-sale-amount';
 export * from './shipping-rates';
 export * from './legacy-events';
 export * from './render-frontend';
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss
index a9f89521af..65e0522f56 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/style.scss
@@ -105,12 +105,6 @@
 }

 @include cart-checkout-below-large-container {
-	.wc-block-cart-item__total {
-		.wc-block-components-sale-badge {
-			display: none;
-		}
-	}
-
 	.wc-block-cart {
 		.wc-block-components-sidebar {
 			.wc-block-cart__totals-title {
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 e804695bd3..a6ac7069e0 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
@@ -45,7 +45,6 @@ const {
 	increaseQuantityLabel,
 	quantityDescriptionLabel,
 	removeFromCartLabel,
-	lowInStockLabel,
 } = getConfig( 'woocommerce/mini-cart-products-table-block' );
 const { itemsInCartTextTemplate } = getConfig(
 	'woocommerce/mini-cart-title-items-counter-block'
@@ -410,45 +409,6 @@ const { state: cartItemState } = store(
 				);
 			},

-			get cartItemDiscount(): string {
-				const { extensions } = cartItemState.cartItem;
-
-				const discountPrice =
-					cartItemState.regularAmountSingle -
-					cartItemState.purchaseAmountSingle;
-
-				const price = formatPriceWithCurrency(
-					discountPrice,
-					cartItemState.currency
-				);
-
-				// TODO: Add deprecation notice urging to replace with a
-				// `data-wp-text` directive or an alternative solution.
-				if (
-					// eslint-disable-next-line @typescript-eslint/no-explicit-any
-					( window.wc as any )?.blocksCheckout?.applyCheckoutFilter
-				) {
-					const priceText =
-						// eslint-disable-next-line @typescript-eslint/no-explicit-any
-						( window.wc as any ).blocksCheckout.applyCheckoutFilter(
-							{
-								filterName: 'saleBadgePriceFormat',
-								defaultValue: '<price/>',
-								extensions,
-								arg: {
-									context: 'cart',
-									cartItem: cartItemState.cartItem,
-									cart: woocommerceState.cart,
-								},
-							}
-						);
-
-					return priceText.replace( '<price/>', price );
-				}
-
-				return price;
-			},
-
 			get lineItemDiscount(): string {
 				const { quantity, extensions } = cartItemState.cartItem;

@@ -717,13 +677,6 @@ const { state: cartItemState } = store(
 				return price;
 			},

-			get isLineItemTotalDiscountVisible(): boolean {
-				return (
-					cartItemState.cartItemHasDiscount &&
-					cartItemState.cartItem.quantity > 1
-				);
-			},
-
 			get isProductHiddenFromCatalog(): boolean {
 				const context = getContext< { isImageHidden: boolean } >();
 				const { catalog_visibility: catalogVisibility } =
@@ -735,20 +688,6 @@ const { state: cartItemState } = store(
 				);
 			},

-			get isLowInStockVisible(): boolean {
-				return (
-					! cartItemState.cartItem.show_backorder_badge &&
-					!! cartItemState.cartItem.low_stock_remaining
-				);
-			},
-
-			get lowInStockLabel(): string {
-				return lowInStockLabel.replace(
-					'%d',
-					cartItemState.cartItem.low_stock_remaining
-				);
-			},
-
 			get itemShowRemoveItemLink(): boolean {
 				// eslint-disable-next-line @typescript-eslint/no-explicit-any
 				return ( window.wc as any )?.blocksCheckout?.applyCheckoutFilter
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
index 52607b5924..050c626ca4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
@@ -61,16 +61,11 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {

 		/* translators: %s is the discount amount. */
 		$save_format             = __( 'Save %s', 'woocommerce' );
-		$cart_item_discount_span = '<span data-wp-text="state.cartItemDiscount" class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount"></span>';
-		$cart_item_save_badge    = sprintf( $save_format, $cart_item_discount_span );
 		$line_item_discount_span = '<span data-wp-text="state.lineItemDiscount" class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount"></span>';
 		$line_item_save_badge    = sprintf( $save_format, $line_item_discount_span );

 		$available_on_backorder_label = __( 'Available on backorder', 'woocommerce' );

-		/* translators: %d stock amount (number of items in stock for product) */
-		$low_in_stock_label = __( '%d left in stock', 'woocommerce' );
-
 		wp_interactivity_config(
 			$this->get_full_block_name(),
 			array(
@@ -78,7 +73,6 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
 				'increaseQuantityLabel'    => $increase_quantity_label,
 				'quantityDescriptionLabel' => $quantity_description_label,
 				'removeFromCartLabel'      => $remove_from_cart_label,
-				'lowInStockLabel'          => $low_in_stock_label,
 			)
 		);

@@ -145,12 +139,6 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
 									<div data-wp-bind--hidden="!state.cartItem.show_backorder_badge" class="wc-block-components-product-badge wc-block-components-product-backorder-badge">
 										<?php echo esc_html( $available_on_backorder_label ); ?>
 									</div>
-									<div
-										class="wc-block-components-product-badge wc-block-components-product-low-stock-badge"
-										data-wp-bind--hidden="!state.isLowInStockVisible"
-										data-wp-text="state.lowInStockLabel"
-									>
-									</div>
 									<div class="wc-block-cart-item__prices">
 										<span data-wp-bind--hidden="!state.cartItemHasDiscount" class="price wc-block-components-product-price">
 											<span data-wp-text="state.beforeItemPrice"></span>
@@ -171,22 +159,6 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
 											<span data-wp-text="state.afterItemPrice"></span>
 										</span>
 									</div>
-									<div
-										data-wp-bind--hidden="!state.cartItemHasDiscount"
-										class="wc-block-components-product-badge wc-block-components-sale-badge"
-									>
-									<?php
-										echo wp_kses(
-											$cart_item_save_badge,
-											array(
-												'span' => array(
-													'data-wp-text' => true,
-													'class'        => true,
-												),
-											)
-										);
-									?>
-									</div>
 									<div class="wc-block-components-product-metadata">
 										<div data-wp-watch="callbacks.itemShortDescription" >
 											<div class="wc-block-components-product-metadata__description"></div>
@@ -247,7 +219,7 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
 										</span>
 									</span>
 									<div
-										data-wp-bind--hidden="!state.isLineItemTotalDiscountVisible"
+										data-wp-bind--hidden="!state.cartItemHasDiscount"
 										class="wc-block-components-product-badge wc-block-components-sale-badge"
 									>
 									<?php