Commit 7d1d2248020 for woocommerce

commit 7d1d22480201b31f296989d8deb54fe1cf784448
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Thu Oct 29 10:33:46 2026 +0100

    Show free trial copy instead of the price on Marketplace cards (#70290)

    * Show free trial copy instead of the price on Marketplace cards

    When WooCommerce.com sends a positive trial length (free_trial_length_days on
    search, freeTrialLengthDays on featured), the card shows "14 day free trial"
    in place of the price, sale price and billing period. Missing or non-positive
    values keep the price as before.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Clarify the free trial comment and translator note

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Check the hidden price details with separate assertions

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Cover Add to Store taking precedence over the trial copy

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * Keep the free and freemium labels when a trial length is sent

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/woomkt-1064-in-app-free-trial-copy b/plugins/woocommerce/changelog/woomkt-1064-in-app-free-trial-copy
new file mode 100644
index 00000000000..a210cde0364
--- /dev/null
+++ b/plugins/woocommerce/changelog/woomkt-1064-in-app-free-trial-copy
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Show the free trial length instead of the price on Marketplace product cards when WooCommerce.com offers a trial.
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx
index d038e9df7a5..cf652216bb0 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx
@@ -2,7 +2,7 @@
  * External dependencies
  */
 import { Button, Icon } from '@wordpress/components';
-import { __, sprintf } from '@wordpress/i18n';
+import { __, _n, sprintf } from '@wordpress/i18n';
 import { useContext } from '@wordpress/element';
 import { recordEvent } from '@woocommerce/tracks';
 import { navigateTo, getNewPath } from '@woocommerce/navigation';
@@ -76,7 +76,31 @@ function ProductCardFooter( props: { product: Product } ) {
 		return currencyFormats[ currencyCode ] || '%s';
 	};

+	// The trial replaces only a paid price, so free and freemium cards keep their label. Anything but a positive day count means no trial.
+	const freeTrialDays = product.freeTrialLengthDays;
+	const freeTrialLabel =
+		product.price > 0 &&
+		product.freemium_type !== 'primary' &&
+		typeof freeTrialDays === 'number' &&
+		Number.isInteger( freeTrialDays ) &&
+		freeTrialDays > 0
+			? sprintf(
+					/* translators: %d: free trial length in days. English uses the same form for singular and plural. */
+					_n(
+						'%d day free trial',
+						'%d day free trial',
+						freeTrialDays,
+						'woocommerce'
+					),
+					freeTrialDays
+				)
+			: '';
+
 	function getPriceLabel(): string {
+		if ( freeTrialLabel ) {
+			return freeTrialLabel;
+		}
+
 		if ( product.price === 0 ) {
 			return __( 'Free download', 'woocommerce' );
 		}
@@ -134,7 +158,7 @@ function ProductCardFooter( props: { product: Product } ) {
 	}

 	function getBillingText(): string {
-		if ( product.freemium_type === 'primary' ) {
+		if ( freeTrialLabel || product.freemium_type === 'primary' ) {
 			return '';
 		}

@@ -146,6 +170,10 @@ function ProductCardFooter( props: { product: Product } ) {
 	}

 	const getReaderPriceLabel = () => {
+		if ( freeTrialLabel ) {
+			return freeTrialLabel;
+		}
+
 		if ( product.isOnSale ) {
 			return sprintf(
 				//translators: %1$s is the sale price of the product, %2$s is the regular price of the product, %3$s is the billing period
@@ -212,7 +240,7 @@ function ProductCardFooter( props: { product: Product } ) {
 					<span aria-hidden>{ getPriceLabel() }</span>
 				</span>

-				{ product.isOnSale && (
+				{ product.isOnSale && ! freeTrialLabel && (
 					<span
 						className="woocommerce-marketplace__product-card__on-sale"
 						aria-hidden
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx
index 966f719cda1..020562d838d 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx
@@ -3,6 +3,7 @@
  */
 import { render } from '@testing-library/react';
 import React from 'react';
+import { useUser } from '@woocommerce/data';

 jest.mock( '@woocommerce/navigation', () => ( {
 	getNewPath: jest.fn( () => '/new-path' ),
@@ -111,3 +112,109 @@ describe( 'ProductCardFooter rating', () => {
 		).toBeNull();
 	} );
 } );
+
+describe( 'ProductCardFooter free trial', () => {
+	const paidProduct: Product = {
+		...product,
+		price: 79,
+		regularPrice: 99,
+		isOnSale: true,
+		billingPeriod: 'year',
+		billingPeriodInterval: 1,
+	};
+
+	function renderPaidFooter( freeTrialLengthDays?: number | null ) {
+		return render(
+			<MarketplaceContext.Provider value={ context }>
+				<ProductCardFooter
+					product={ { ...paidProduct, freeTrialLengthDays } }
+				/>
+			</MarketplaceContext.Provider>
+		);
+	}
+
+	it( 'shows the trial copy instead of the price, sale price and billing period', () => {
+		const { container, getAllByText } = renderPaidFooter( 14 );
+
+		// Once visible, once for screen readers.
+		expect( getAllByText( '14 day free trial' ) ).toHaveLength( 2 );
+		expect( container.textContent ).not.toContain( '$' );
+		expect( container.textContent ).not.toContain( 'annually' );
+	} );
+
+	it.each( [ undefined, null, 0, -7, 1.5 ] )(
+		'shows the price when the trial length is %p',
+		( freeTrialLengthDays ) => {
+			const { container, getByText } =
+				renderPaidFooter( freeTrialLengthDays );
+
+			expect( container.textContent ).not.toContain( 'free trial' );
+			expect( getByText( '$79' ) ).toBeInTheDocument();
+			expect( getByText( '$99' ) ).toBeInTheDocument();
+			expect( getByText( 'annually' ) ).toBeInTheDocument();
+		}
+	);
+
+	it.each( [
+		[ 'Free download', { price: 0, isOnSale: false } ],
+		[ 'Free plan available', { freemium_type: 'primary' } ],
+	] as const )( 'keeps "%s" over the trial copy', ( label, overrides ) => {
+		const { getAllByText, queryByText } = render(
+			<MarketplaceContext.Provider value={ context }>
+				<ProductCardFooter
+					product={ {
+						...paidProduct,
+						...overrides,
+						freeTrialLengthDays: 14,
+					} }
+				/>
+			</MarketplaceContext.Provider>
+		);
+
+		expect( getAllByText( label ).length ).toBeGreaterThan( 0 );
+		expect( queryByText( '14 day free trial' ) ).toBeNull();
+	} );
+
+	it( 'shows "Installed" over the trial copy', () => {
+		const { getByText, queryByText } = render(
+			<MarketplaceContext.Provider
+				value={ { ...context, isProductInstalled: () => true } }
+			>
+				<ProductCardFooter
+					product={ {
+						...paidProduct,
+						slug: 'test-extension',
+						freeTrialLengthDays: 14,
+					} }
+				/>
+			</MarketplaceContext.Provider>
+		);
+
+		expect( getByText( 'Installed' ) ).toBeInTheDocument();
+		expect( queryByText( '14 day free trial' ) ).toBeNull();
+	} );
+
+	it( 'shows "Add to Store" over the trial copy', () => {
+		( useUser as jest.Mock ).mockReturnValueOnce( {
+			user: { id: 1 },
+			currentUserCan: () => true,
+		} );
+
+		const { getByRole, queryByText } = render(
+			<MarketplaceContext.Provider value={ context }>
+				<ProductCardFooter
+					product={ {
+						...paidProduct,
+						isInstallable: true,
+						freeTrialLengthDays: 14,
+					} }
+				/>
+			</MarketplaceContext.Provider>
+		);
+
+		expect(
+			getByRole( 'button', { name: 'Add to Store' } )
+		).toBeInTheDocument();
+		expect( queryByText( '14 day free trial' ) ).toBeNull();
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-list-content/product-list-content.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-list-content/product-list-content.tsx
index a5872d5d12d..2be344389ad 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-list-content/product-list-content.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-list-content/product-list-content.tsx
@@ -160,6 +160,8 @@ export default function ProductListContent( props: {
 								isOnSale: product.isOnSale,
 								regularPrice: product.regularPrice,
 								hasQualityBadge: product.hasQualityBadge,
+								freeTrialLengthDays:
+									product.freeTrialLengthDays,
 							} }
 							tracksData={ {
 								position: index + 1,
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce/client/admin/client/marketplace/components/product-list/types.ts
index cc10014ce3d..8eea5f10b70 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-list/types.ts
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-list/types.ts
@@ -32,6 +32,7 @@ export type SearchAPIProductType = {
 	is_on_sale: boolean;
 	regular_price: number;
 	has_quality_badge?: boolean;
+	free_trial_length_days?: number | null;
 };

 export interface Product {
@@ -66,6 +67,7 @@ export interface Product {
 	isOnSale: boolean;
 	regularPrice: number;
 	hasQualityBadge?: boolean;
+	freeTrialLengthDays?: number | null;
 }

 export interface ProductTracksData {
diff --git a/plugins/woocommerce/client/admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce/client/admin/client/marketplace/utils/functions.tsx
index d63543e00d3..b928fd46feb 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/utils/functions.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/utils/functions.tsx
@@ -178,6 +178,8 @@ async function fetchSearchResults(
 								product.billing_period_interval,
 							currency: product.currency,
 							hasQualityBadge: product.has_quality_badge ?? false,
+							freeTrialLengthDays:
+								product.free_trial_length_days ?? null,
 						};
 					}
 				);