Commit ca2697cafb6 for woocommerce

commit ca2697cafb6763f276959aafdfc8a3878a8665b4
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Thu Sep 3 12:26:52 2026 +0200

    Move the quality badge above the price on marketplace product cards (#68259)

    * Move the quality badge above the price on marketplace product cards

    * Add changelog entry for the quality badge placement change

    * Align the price row across cards with and without the quality badge

    ---------

    Co-authored-by: Herman <KokkieH@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/wccom-2892-quality-badge-compact-card-placement b/plugins/woocommerce/changelog/wccom-2892-quality-badge-compact-card-placement
new file mode 100644
index 00000000000..a9a2b4ce540
--- /dev/null
+++ b/plugins/woocommerce/changelog/wccom-2892-quality-badge-compact-card-placement
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Move the quality badge above the price on in-app marketplace cards (between the name and the price on compact cards), centre compact card content vertically, and pin the price row to the bottom of regular cards.
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss
index a771ef1b69a..c09c95d2a7d 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss
@@ -92,10 +92,13 @@

 		&__footer {
 			display: flex;
-			/* The quality badge, when present, wraps onto its own row below the price: */
+			/* The quality badge, when present, takes its own row above the price: */
 			flex-wrap: wrap;
 			justify-content: space-between;
 			align-items: center;
+			/* Pack rows to the bottom so the price row sits at the same height
+			   whether or not the badge row is present above it. */
+			align-content: flex-end;
 			row-gap: $grid-unit-10;
 			min-height: 36px;
 		}
@@ -104,6 +107,12 @@
 			outline: 1.5px solid var(--wp-admin-theme-color);
 		}

+		/* Fill the card so the content grid below can pin the footer to the
+		   bottom (and compact cards can centre their content). */
+		&-wrapper {
+			height: 100%;
+		}
+
 		&__content {
 			display: grid;
 			gap: $medium-gap;
@@ -420,16 +429,27 @@
 				min-height: 0;
 			}

+			/* Centre the icon and text in the card so that, in a grid mixing
+			   cards with and without the badge, the extra height of a badged
+			   card is split evenly instead of leaving a gap at the bottom. */
+			.woocommerce-marketplace__product-card__content {
+				align-content: center;
+			}
+
 			.woocommerce-marketplace__product-card__details {
-				/* Top-align the icon with the title, like regular cards, so it
-				   sits identically whether or not the badge row is present. */
-				align-items: flex-start;
+				align-items: center;
 			}

 			.woocommerce-marketplace__product-card__meta {
 				/* Optical compensation: the title's negative focus-ring margin
-				   would otherwise pin the text to the icon's top edge. */
+				   would otherwise shift the text up relative to the icon. */
 				margin-top: $grid-unit-05;
+
+				/* Title, badge and price stack with 4px between them; without
+				   the badge the title and price keep the tighter 2px gap. */
+				&:has(.woocommerce-marketplace__quality-badge) {
+					gap: $grid-unit-05;
+				}
 			}

 			.woocommerce-marketplace__product-card__link {
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
index 557200f83a4..106b9049b47 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
@@ -294,16 +294,21 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
 		);
 	};

+	const qualityBadge =
+		! isLoading && props.product ? (
+			<QualityBadge product={ props.product } />
+		) : null;
+
 	const footer = ! isBusinessService ? (
 		<footer className="woocommerce-marketplace__product-card__footer">
 			{ isLoading && (
 				<div className="woocommerce-marketplace__product-card__price" />
 			) }
+			{ /* Regular cards show the badge on its own row above the price;
+			     compact cards show it between the title and the price. */ }
+			{ ! isCompact && qualityBadge }
 			{ ! isLoading && props.product && (
-				<>
-					<ProductCardFooter product={ props.product } />
-					<QualityBadge product={ props.product } />
-				</>
+				<ProductCardFooter product={ props.product } />
 			) }
 		</footer>
 	) : null;
@@ -371,6 +376,7 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
 								) }
 							</p>
 						) }
+						{ isCompact && qualityBadge }
 						{ isCompact && footer }
 					</div>
 				</div>
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
new file mode 100644
index 00000000000..ce6bb4876b3
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
@@ -0,0 +1,138 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+import React from 'react';
+
+jest.mock( '@woocommerce/navigation', () => ( {
+	getNewPath: jest.fn( () => '/new-path' ),
+	navigateTo: jest.fn(),
+	useQuery: jest.fn( () => ( {} ) ),
+} ) );
+
+jest.mock( '@woocommerce/tracks', () => ( {
+	recordEvent: jest.fn(),
+	queueRecordEvent: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/data', () => ( {
+	useUser: jest.fn( () => ( {
+		user: null,
+		currentUserCan: jest.fn( () => false ),
+	} ) ),
+} ) );
+
+/**
+ * Internal dependencies
+ */
+import ProductCard from '../product-card';
+import { MarketplaceContext } from '../../../contexts/marketplace-context';
+import { MarketplaceContextType } from '../../../contexts/types';
+import {
+	Product,
+	ProductCardType,
+	ProductType,
+} from '../../product-list/types';
+
+const context = {
+	selectedTab: 'discover',
+	isProductInstalled: () => false,
+	iamSettings: {
+		quality_badge: {
+			enabled: true,
+			label: 'Excellence Verified',
+			tooltip: 'Verified against WooCommerce standards.',
+		},
+	},
+} as unknown as MarketplaceContextType;
+
+const product: Product = {
+	id: 1,
+	title: 'Test extension',
+	image: '',
+	type: ProductType.extension,
+	description: '',
+	vendorName: '',
+	vendorUrl: '',
+	icon: '',
+	url: '',
+	price: 0,
+	isInstallable: false,
+	currency: 'USD',
+	isOnSale: false,
+	regularPrice: 0,
+	averageRating: 4.5,
+	reviewsCount: 10,
+	hasQualityBadge: true,
+};
+
+function renderCard( cardType: ProductCardType ) {
+	return render(
+		<MarketplaceContext.Provider value={ context }>
+			<ProductCard
+				type={ ProductType.extension }
+				product={ product }
+				cardType={ cardType }
+				tracksData={ { position: 1 } }
+			/>
+		</MarketplaceContext.Provider>
+	);
+}
+
+function getBadge( container: HTMLElement ) {
+	return container.querySelector( '.woocommerce-marketplace__quality-badge' );
+}
+
+describe( 'ProductCard quality badge placement', () => {
+	it( 'renders the badge between the title and the price on compact cards', () => {
+		const { container } = renderCard( ProductCardType.compact );
+		const badge = getBadge( container );
+		const meta = container.querySelector(
+			'.woocommerce-marketplace__product-card__meta'
+		);
+		const footer = container.querySelector(
+			'.woocommerce-marketplace__product-card__footer'
+		);
+
+		expect( badge ).not.toBeNull();
+		expect( badge?.parentElement ).toBe( meta );
+		expect( footer?.contains( badge ) ).toBe( false );
+
+		const title = container.querySelector(
+			'.woocommerce-marketplace__product-card__title'
+		);
+		expect( title?.nextElementSibling ).toBe( badge );
+		expect( badge?.nextElementSibling ).toBe( footer );
+	} );
+
+	it( 'renders the badge in the footer above the price on regular cards', () => {
+		const { container } = renderCard( ProductCardType.regular );
+		const badge = getBadge( container );
+		const footer = container.querySelector(
+			'.woocommerce-marketplace__product-card__footer'
+		);
+		const price = container.querySelector(
+			'.woocommerce-marketplace__product-card__price'
+		);
+
+		expect( badge ).not.toBeNull();
+		expect( badge?.parentElement ).toBe( footer );
+		expect( footer?.firstElementChild ).toBe( badge );
+		expect( badge?.nextElementSibling ).toBe( price );
+	} );
+
+	it( 'renders no badge when the product does not have one', () => {
+		const { container } = render(
+			<MarketplaceContext.Provider value={ context }>
+				<ProductCard
+					type={ ProductType.extension }
+					product={ { ...product, hasQualityBadge: false } }
+					cardType={ ProductCardType.compact }
+					tracksData={ {} }
+				/>
+			</MarketplaceContext.Provider>
+		);
+
+		expect( getBadge( container ) ).toBeNull();
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss
index 3d0b0d68af8..c64021b9a54 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss
@@ -1,8 +1,9 @@
 @import "../../stylesheets/_variables.scss";

 .woocommerce-marketplace {
-	// Chip on product cards. Sits on its own row at the bottom of the card
-	// footer, below the price, to avoid crowding the title on narrow cards.
+	// Chip on product cards. Regular cards show it on its own row in the
+	// footer, above the price; compact cards show it between the title and
+	// the price.
 	&__quality-badge {
 		width: 100%;

@@ -10,8 +11,8 @@
 			align-items: center;
 			background: $studio-gray-0;
 			border: 0;
-			border-radius: $grid-unit-05;
-			color: $gray-900;
+			border-radius: $grid-unit-10;
+			color: $studio-gray-70;
 			cursor: pointer;
 			display: inline-flex;
 			font-family: inherit;
@@ -19,7 +20,7 @@
 			font-weight: 500;
 			gap: $grid-unit-05;
 			line-height: 20px;
-			padding: 0 $grid-unit-10 0 $grid-unit-05;
+			padding: 2px $grid-unit-10;
 			/* Punch through the "whole card clickable" trick so the popover opens: */
 			position: relative;
 		}