Commit c90d08faebf for woocommerce

commit c90d08faebfb960bf0345c605be9f58ac66d5c89
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Wed May 6 00:26:22 2026 +0300

    Show all country-relevant shipping recommendations on the settings page (#64522)

    * Show all country-relevant shipping recommendations on the settings page

    Drop the !activePlugins.includes(...) filter from
    experimental-shipping-recommendations.tsx so every country-mapped
    extension renders on the Shipping settings page, even when one of
    the partner plugins is already installed and active.

    The settings page is meant to surface alternatives merchants can
    evaluate and switch to; hiding a card the moment its plugin is active
    collapses the slot for single-extension countries (FR, GB, DE, IE,
    PT, NL, AT, BE, AU, NZ, CA) and removes that visibility entirely.
    The onboarding wizard keeps a narrower selection because installing
    every option there would lead to every option being installed at
    once, which is not desired during initial setup. The settings page
    does not have that constraint.

    Existing card components continue to drive their CTA from
    isPluginInstalled, so the click flow shows "Install" or "Activate"
    based on the plugin state. A follow-up can add a dedicated active
    state ("Manage" / settings link); this change keeps scope tight to
    the visibility behaviour.

    Tests updated to assert that all country-mapped cards stay visible
    when their partner is already active, and to disambiguate buttons in
    test cases where US now renders two cards instead of one.

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * Guard getProfileItems() with optional chaining

    If the onboarding selector hasn't resolved yet, getProfileItems()
    returns undefined; reading .product_types directly throws and breaks
    the settings page render. Add the missing optional-chain operator so
    profileItems falls back to undefined and the existing
    isSellingDigitalProductsOnly check still resolves to false.

    Addresses CodeRabbit review on #64522.

    * Show "Active" status instead of CTA on installed shipping recommendations

    * Render the Active state as a disabled button with accessible labelling

    Replace the plain 'Active' span in the WCShip, ShipStation, and Packlink
    recommendation cards with a Button(variant=secondary, aria-disabled=true)
    so the cards line up visually with the inactive Install/Activate cards
    in the same slot.

    Use aria-disabled instead of native disabled so screen-reader users
    keep the element in tab order and the active state is announced. The
    aria-label adds the partner name so the announcement is meaningful
    without depending on adjacent DOM context.

    Addresses CodeRabbit review on #64522.

    ---------

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

diff --git a/plugins/woocommerce/changelog/64522-wooplug-6633-show-all-shipping-recommendations b/plugins/woocommerce/changelog/64522-wooplug-6633-show-all-shipping-recommendations
new file mode 100644
index 00000000000..460327835ba
--- /dev/null
+++ b/plugins/woocommerce/changelog/64522-wooplug-6633-show-all-shipping-recommendations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Show all country-relevant shipping partner recommendations on the settings page even when one of the partners is already installed.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/shipping/experimental-shipping-recommendations.tsx b/plugins/woocommerce/client/admin/client/shipping/experimental-shipping-recommendations.tsx
index af7f6e7fe3d..465fddbcefd 100644
--- a/plugins/woocommerce/client/admin/client/shipping/experimental-shipping-recommendations.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/experimental-shipping-recommendations.tsx
@@ -54,22 +54,22 @@ const ShippingRecommendations = () => {
 		useInstallPlugin();

 	const {
-		activePlugins,
 		installedPlugins,
+		activePlugins,
 		countryCode,
 		isSellingDigitalProductsOnly,
 	} = useSelect( ( select ) => {
 		const settings = select( settingsStore ).getSettings( 'general' );

-		const { getActivePlugins, getInstalledPlugins } =
+		const { getInstalledPlugins, getActivePlugins } =
 			select( pluginsStore );

 		const profileItems =
-			select( onboardingStore ).getProfileItems().product_types;
+			select( onboardingStore ).getProfileItems()?.product_types;

 		return {
-			activePlugins: getActivePlugins(),
 			installedPlugins: getInstalledPlugins(),
+			activePlugins: getActivePlugins(),
 			countryCode: getCountryCode(
 				settings.general?.woocommerce_default_country
 			),
@@ -83,12 +83,14 @@ const ShippingRecommendations = () => {
 	const extensionsForCountry =
 		COUNTRY_EXTENSIONS_MAP[ normalizedCountry ] ?? [];

+	// Render every country-mapped recommendation regardless of which partner
+	// is already installed: the settings page is meant to surface alternatives
+	// the merchant can evaluate and switch to. The onboarding wizard keeps a
+	// narrower selection because installing every option there at once is not
+	// desired during initial setup.
 	const visibleExtensions = isSellingDigitalProductsOnly
 		? []
-		: extensionsForCountry.filter(
-				( ext ) =>
-					! activePlugins.includes( EXTENSION_PLUGIN_SLUGS[ ext ] )
-		  );
+		: extensionsForCountry;

 	const visiblePluginSlugs = visibleExtensions
 		.map( ( ext ) => EXTENSION_PLUGIN_SLUGS[ ext ] )
@@ -122,6 +124,9 @@ const ShippingRecommendations = () => {
 					const isPluginInstalled = installedPlugins.includes(
 						EXTENSION_PLUGIN_SLUGS[ ext ]
 					);
+					const isPluginActive = activePlugins.includes(
+						EXTENSION_PLUGIN_SLUGS[ ext ]
+					);
 					const trackingProps = {
 						context: 'settings' as const,
 						country: normalizedCountry,
@@ -133,6 +138,7 @@ const ShippingRecommendations = () => {
 								<WooCommerceShippingItem
 									key={ ext }
 									isPluginInstalled={ isPluginInstalled }
+									isPluginActive={ isPluginActive }
 									pluginsBeingSetup={ pluginsBeingSetup }
 									onInstallClick={ handleInstall }
 									onActivateClick={ handleActivate }
@@ -144,6 +150,7 @@ const ShippingRecommendations = () => {
 								<ShipStationItem
 									key={ ext }
 									isPluginInstalled={ isPluginInstalled }
+									isPluginActive={ isPluginActive }
 									pluginsBeingSetup={ pluginsBeingSetup }
 									onInstallClick={ handleInstall }
 									onActivateClick={ handleActivate }
@@ -155,6 +162,7 @@ const ShippingRecommendations = () => {
 								<PacklinkItem
 									key={ ext }
 									isPluginInstalled={ isPluginInstalled }
+									isPluginActive={ isPluginActive }
 									pluginsBeingSetup={ pluginsBeingSetup }
 									onInstallClick={ handleInstall }
 									onActivateClick={ handleActivate }
diff --git a/plugins/woocommerce/client/admin/client/shipping/experimental-woocommerce-shipping-item.tsx b/plugins/woocommerce/client/admin/client/shipping/experimental-woocommerce-shipping-item.tsx
index 7f2ef72528a..3606d8e9930 100644
--- a/plugins/woocommerce/client/admin/client/shipping/experimental-woocommerce-shipping-item.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/experimental-woocommerce-shipping-item.tsx
@@ -23,12 +23,14 @@ export type ShippingPartnerTrackingProps = {

 const WooCommerceShippingItem = ( {
 	isPluginInstalled,
+	isPluginActive,
 	onInstallClick,
 	onActivateClick,
 	pluginsBeingSetup,
 	tracking,
 }: {
 	isPluginInstalled: boolean;
+	isPluginActive: boolean;
 	pluginsBeingSetup: Array< string >;
 	onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
 	onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
@@ -104,18 +106,31 @@ const WooCommerceShippingItem = ( {
 				</span>
 			</div>
 			<div className="woocommerce-list__item-after">
-				<Button
-					variant={ isPluginInstalled ? 'primary' : 'secondary' }
-					onClick={ handleClick }
-					isBusy={ pluginsBeingSetup.includes(
-						WOOCOMMERCE_SHIPPING_PLUGIN_SLUG
-					) }
-					disabled={ pluginsBeingSetup.length > 0 }
-				>
-					{ isPluginInstalled
-						? __( 'Activate', 'woocommerce' )
-						: __( 'Install', 'woocommerce' ) }
-				</Button>
+				{ isPluginActive ? (
+					<Button
+						variant="secondary"
+						aria-disabled="true"
+						aria-label={ __(
+							'WooCommerce Shipping is already active',
+							'woocommerce'
+						) }
+					>
+						{ __( 'Active', 'woocommerce' ) }
+					</Button>
+				) : (
+					<Button
+						variant={ isPluginInstalled ? 'primary' : 'secondary' }
+						onClick={ handleClick }
+						isBusy={ pluginsBeingSetup.includes(
+							WOOCOMMERCE_SHIPPING_PLUGIN_SLUG
+						) }
+						disabled={ pluginsBeingSetup.length > 0 }
+					>
+						{ isPluginInstalled
+							? __( 'Activate', 'woocommerce' )
+							: __( 'Install', 'woocommerce' ) }
+					</Button>
+				) }
 			</div>
 		</div>
 	);
diff --git a/plugins/woocommerce/client/admin/client/shipping/packlink-item.tsx b/plugins/woocommerce/client/admin/client/shipping/packlink-item.tsx
index 7bd4684090c..a704032a813 100644
--- a/plugins/woocommerce/client/admin/client/shipping/packlink-item.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/packlink-item.tsx
@@ -16,12 +16,14 @@ const PACKLINK_PLUGIN_SLUG = 'packlink-pro-shipping';

 const PacklinkItem = ( {
 	isPluginInstalled,
+	isPluginActive,
 	onInstallClick,
 	onActivateClick,
 	pluginsBeingSetup,
 	tracking,
 }: {
 	isPluginInstalled: boolean;
+	isPluginActive: boolean;
 	pluginsBeingSetup: Array< string >;
 	onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
 	onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
@@ -93,18 +95,31 @@ const PacklinkItem = ( {
 				</span>
 			</div>
 			<div className="woocommerce-list__item-after">
-				<Button
-					variant={ isPluginInstalled ? 'primary' : 'secondary' }
-					onClick={ handleClick }
-					isBusy={ pluginsBeingSetup.includes(
-						PACKLINK_PLUGIN_SLUG
-					) }
-					disabled={ pluginsBeingSetup.length > 0 }
-				>
-					{ isPluginInstalled
-						? __( 'Activate', 'woocommerce' )
-						: __( 'Install', 'woocommerce' ) }
-				</Button>
+				{ isPluginActive ? (
+					<Button
+						variant="secondary"
+						aria-disabled="true"
+						aria-label={ __(
+							'Packlink PRO is already active',
+							'woocommerce'
+						) }
+					>
+						{ __( 'Active', 'woocommerce' ) }
+					</Button>
+				) : (
+					<Button
+						variant={ isPluginInstalled ? 'primary' : 'secondary' }
+						onClick={ handleClick }
+						isBusy={ pluginsBeingSetup.includes(
+							PACKLINK_PLUGIN_SLUG
+						) }
+						disabled={ pluginsBeingSetup.length > 0 }
+					>
+						{ isPluginInstalled
+							? __( 'Activate', 'woocommerce' )
+							: __( 'Install', 'woocommerce' ) }
+					</Button>
+				) }
 			</div>
 		</div>
 	);
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx b/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx
index b641cb7dc91..85b93a8e734 100644
--- a/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx
@@ -16,12 +16,14 @@ const SHIPSTATION_PLUGIN_SLUG = 'woocommerce-shipstation-integration';

 const ShipStationItem = ( {
 	isPluginInstalled,
+	isPluginActive,
 	onInstallClick,
 	onActivateClick,
 	pluginsBeingSetup,
 	tracking,
 }: {
 	isPluginInstalled: boolean;
+	isPluginActive: boolean;
 	pluginsBeingSetup: Array< string >;
 	onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
 	onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
@@ -93,18 +95,31 @@ const ShipStationItem = ( {
 				</span>
 			</div>
 			<div className="woocommerce-list__item-after">
-				<Button
-					onClick={ handleClick }
-					variant={ isPluginInstalled ? 'primary' : 'secondary' }
-					isBusy={ pluginsBeingSetup.includes(
-						SHIPSTATION_PLUGIN_SLUG
-					) }
-					disabled={ pluginsBeingSetup.length > 0 }
-				>
-					{ isPluginInstalled
-						? __( 'Activate', 'woocommerce' )
-						: __( 'Install', 'woocommerce' ) }
-				</Button>
+				{ isPluginActive ? (
+					<Button
+						variant="secondary"
+						aria-disabled="true"
+						aria-label={ __(
+							'ShipStation is already active',
+							'woocommerce'
+						) }
+					>
+						{ __( 'Active', 'woocommerce' ) }
+					</Button>
+				) : (
+					<Button
+						onClick={ handleClick }
+						variant={ isPluginInstalled ? 'primary' : 'secondary' }
+						isBusy={ pluginsBeingSetup.includes(
+							SHIPSTATION_PLUGIN_SLUG
+						) }
+						disabled={ pluginsBeingSetup.length > 0 }
+					>
+						{ isPluginInstalled
+							? __( 'Activate', 'woocommerce' )
+							: __( 'Install', 'woocommerce' ) }
+					</Button>
+				) }
 			</div>
 		</div>
 	);
diff --git a/plugins/woocommerce/client/admin/client/shipping/test/experimental-shipping-recommendations.tsx b/plugins/woocommerce/client/admin/client/shipping/test/experimental-shipping-recommendations.tsx
index 88c97c7fe2a..db8693c4b02 100644
--- a/plugins/woocommerce/client/admin/client/shipping/test/experimental-shipping-recommendations.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/test/experimental-shipping-recommendations.tsx
@@ -196,18 +196,18 @@ describe( 'ShippingRecommendations', () => {
 		} );
 	} );

-	describe( 'active plugin filtering', () => {
-		it( 'should not show WooCommerce Shipping when it is already active', () => {
+	describe( 'rendering when partners are already active', () => {
+		it( 'should still show WooCommerce Shipping when it is already active', () => {
 			mockSelectForCountry( 'US', [ 'woocommerce-shipping' ] );
 			render( <ShippingRecommendations /> );

 			expect(
 				screen.queryByText( 'WooCommerce Shipping' )
-			).not.toBeInTheDocument();
+			).toBeInTheDocument();
 			expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
 		} );

-		it( 'should not show ShipStation when it is already active', () => {
+		it( 'should still show ShipStation when it is already active', () => {
 			mockSelectForCountry( 'US', [
 				'woocommerce-shipstation-integration',
 			] );
@@ -216,21 +216,17 @@ describe( 'ShippingRecommendations', () => {
 			expect(
 				screen.queryByText( 'WooCommerce Shipping' )
 			).toBeInTheDocument();
-			expect(
-				screen.queryByText( 'ShipStation' )
-			).not.toBeInTheDocument();
+			expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
 		} );

-		it( 'should not show Packlink PRO when it is already active', () => {
+		it( 'should still show Packlink PRO when it is already active', () => {
 			mockSelectForCountry( 'FR', [ 'packlink-pro-shipping' ] );
 			render( <ShippingRecommendations /> );

-			expect(
-				screen.queryByText( 'Packlink PRO' )
-			).not.toBeInTheDocument();
+			expect( screen.queryByText( 'Packlink PRO' ) ).toBeInTheDocument();
 		} );

-		it( 'should not render recommendations when all extensions for a country are active', () => {
+		it( 'should still render all recommendations when every extension for a country is active', () => {
 			mockSelectForCountry( 'US', [
 				'woocommerce-shipping',
 				'woocommerce-shipstation-integration',
@@ -239,9 +235,48 @@ describe( 'ShippingRecommendations', () => {

 			expect(
 				screen.queryByText( 'WooCommerce Shipping' )
+			).toBeInTheDocument();
+			expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+		} );
+
+		it( 'should render "Active" pills instead of CTA buttons for partners that are already active', () => {
+			mockSelectForCountry(
+				'US',
+				[
+					'woocommerce-shipping',
+					'woocommerce-shipstation-integration',
+				],
+				{
+					getInstalledPlugins: () => [
+						'woocommerce-shipping',
+						'woocommerce-shipstation-integration',
+					],
+				}
+			);
+			render( <ShippingRecommendations /> );
+
+			expect( screen.queryAllByText( 'Active' ) ).toHaveLength( 2 );
+			expect(
+				screen.queryByRole( 'button', { name: 'Install' } )
 			).not.toBeInTheDocument();
 			expect(
-				screen.queryByText( 'ShipStation' )
+				screen.queryByRole( 'button', { name: 'Activate' } )
+			).not.toBeInTheDocument();
+		} );
+
+		it( 'should render an "Active" pill only for the active partner and keep CTAs for inactive ones', () => {
+			mockSelectForCountry( 'US', [ 'woocommerce-shipping' ] );
+			render( <ShippingRecommendations /> );
+
+			// WooCommerce Shipping is active → "Active" pill.
+			// ShipStation is neither installed nor active → Install button shown.
+			expect( screen.queryAllByText( 'Active' ) ).toHaveLength( 1 );
+			const installButtons = screen.queryAllByRole( 'button', {
+				name: 'Install',
+			} );
+			expect( installButtons ).toHaveLength( 1 );
+			expect(
+				screen.queryByRole( 'button', { name: 'Activate' } )
 			).not.toBeInTheDocument();
 		} );
 	} );
@@ -394,14 +429,18 @@ describe( 'ShippingRecommendations', () => {
 			] );
 			render( <ShippingRecommendations /> );

-			userEvent.click( screen.getByText( 'Install' ) );
+			// Both cards are now rendered for US; the first "Install" button
+			// belongs to the WooCommerce Shipping card (first entry in the
+			// COUNTRY_EXTENSIONS_MAP for US).
+			userEvent.click( screen.getAllByText( 'Install' )[ 0 ] );

 			expect( recordEvent ).toHaveBeenCalledWith(
 				'shipping_partner_click',
 				{
 					context: 'settings',
 					country: 'US',
-					plugins: 'woocommerce-shipping',
+					plugins:
+						'woocommerce-shipping,woocommerce-shipstation-integration',
 					selected_plugin: 'woocommerce-shipping',
 				}
 			);
@@ -692,7 +731,8 @@ describe( 'ShippingRecommendations', () => {
 				{
 					context: 'settings',
 					country: 'US',
-					plugins: 'woocommerce-shipping',
+					plugins:
+						'woocommerce-shipping,woocommerce-shipstation-integration',
 					selected_plugin: 'woocommerce-shipping',
 				}
 			);
diff --git a/plugins/woocommerce/client/admin/client/shipping/test/experimental-woocommerce-shipping-item.tsx b/plugins/woocommerce/client/admin/client/shipping/test/experimental-woocommerce-shipping-item.tsx
index f344cd8411d..16399c20086 100644
--- a/plugins/woocommerce/client/admin/client/shipping/test/experimental-woocommerce-shipping-item.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/test/experimental-woocommerce-shipping-item.tsx
@@ -34,6 +34,7 @@ jest.mock( '@woocommerce/admin-layout', () => {

 describe( 'WooCommerceShippingItem', () => {
 	const defaultProps = {
+		isPluginActive: false,
 		pluginsBeingSetup: [] as string[],
 		onInstallClick: jest.fn( () => Promise.resolve() ),
 		onActivateClick: jest.fn( () => Promise.resolve() ),
@@ -79,11 +80,33 @@ describe( 'WooCommerceShippingItem', () => {
 		).toBeInTheDocument();
 	} );

+	it( 'should render an "Active" pill instead of a CTA button when WC Shipping is active', () => {
+		render(
+			<WooCommerceShippingItem
+				{ ...defaultProps }
+				isPluginInstalled={ true }
+				isPluginActive={ true }
+			/>
+		);
+
+		expect(
+			screen.queryByText( 'WooCommerce Shipping' )
+		).toBeInTheDocument();
+		expect( screen.queryByText( 'Active' ) ).toBeInTheDocument();
+		expect(
+			screen.queryByRole( 'button', { name: 'Install' } )
+		).not.toBeInTheDocument();
+		expect(
+			screen.queryByRole( 'button', { name: 'Activate' } )
+		).not.toBeInTheDocument();
+	} );
+
 	it( 'should call onInstallClick when clicking Install button', () => {
 		const onInstallClick = jest.fn( () => Promise.resolve() );
 		render(
 			<WooCommerceShippingItem
 				isPluginInstalled={ false }
+				isPluginActive={ false }
 				pluginsBeingSetup={ [] }
 				onInstallClick={ onInstallClick }
 				onActivateClick={ jest.fn( () => Promise.resolve() ) }
@@ -181,6 +204,7 @@ describe( 'WooCommerceShippingItem', () => {
 		render(
 			<WooCommerceShippingItem
 				isPluginInstalled={ true }
+				isPluginActive={ false }
 				pluginsBeingSetup={ [] }
 				onInstallClick={ jest.fn( () => Promise.resolve() ) }
 				onActivateClick={ onActivateClick }