Commit 27917d17631 for woocommerce
commit 27917d17631bff4771e9a2aee6943b8600d88ea8
Author: Ann <annchichi@users.noreply.github.com>
Date: Wed Jul 15 22:43:11 2026 +0800
Fix duplicate shipping marketplace link (#66110)
* Fix duplicate shipping marketplace link
* Add changelog entry for duplicate shipping link fix
* Move shipping Marketplace fallback into recommendations
* Use dismiss hook for shipping recommendations
* Keep ShippingTour visibility logic unchanged
* Restore explicit ShippingTour fallback condition
* Render shipping fallback inside dismissed list
* Fix hidden shipping recommendations fallback
* Add spacing to shipping fallback link
* Align shipping fallback background
* Align shipping fallback row background
* Fix shipping fallback selector lint
* Keep shipping fallback on settings background
* Keep shipping fallback spacing on grey background
* Tighten shipping fallback review coverage
* Fix shipping recommendations fallback display
* Fix shipping recommendations refresh flicker
diff --git a/plugins/woocommerce/changelog/fix-duplicate-shipping-marketplace-link b/plugins/woocommerce/changelog/fix-duplicate-shipping-marketplace-link
new file mode 100644
index 00000000000..334c0bddf4b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-duplicate-shipping-marketplace-link
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Remove duplicate Shipping settings Marketplace link when shipping recommendations are shown.
diff --git a/plugins/woocommerce/client/admin/client/hooks/test/use-endpoint-dismiss.test.ts b/plugins/woocommerce/client/admin/client/hooks/test/use-endpoint-dismiss.test.ts
index f112e298fa2..f1976b6b1d7 100644
--- a/plugins/woocommerce/client/admin/client/hooks/test/use-endpoint-dismiss.test.ts
+++ b/plugins/woocommerce/client/admin/client/hooks/test/use-endpoint-dismiss.test.ts
@@ -28,6 +28,7 @@ describe( 'useEndpointDismiss', () => {
const { result } = renderHook( () => useEndpointDismiss( PATH, true ) );
expect( result.current.isDismissed ).toBe( true );
+ expect( result.current.hasResolved ).toBe( true );
} );
it( 'optimistically dismisses and POSTs to the endpoint', async () => {
diff --git a/plugins/woocommerce/client/admin/client/hooks/test/use-option-dismiss.test.ts b/plugins/woocommerce/client/admin/client/hooks/test/use-option-dismiss.test.ts
index 59338abe850..cb55bd14c03 100644
--- a/plugins/woocommerce/client/admin/client/hooks/test/use-option-dismiss.test.ts
+++ b/plugins/woocommerce/client/admin/client/hooks/test/use-option-dismiss.test.ts
@@ -46,6 +46,7 @@ describe( 'useOptionDismiss', () => {
const { result } = renderHook( () => useOptionDismiss( OPTION_NAME ) );
expect( result.current.isDismissed ).toBe( true );
+ expect( result.current.hasResolved ).toBe( false );
} );
it( 'is dismissed when the resolved option is "yes"', () => {
@@ -54,6 +55,7 @@ describe( 'useOptionDismiss', () => {
const { result } = renderHook( () => useOptionDismiss( OPTION_NAME ) );
expect( result.current.isDismissed ).toBe( true );
+ expect( result.current.hasResolved ).toBe( true );
} );
it( 'is not dismissed when the resolved option is not "yes"', () => {
@@ -62,6 +64,7 @@ describe( 'useOptionDismiss', () => {
const { result } = renderHook( () => useOptionDismiss( OPTION_NAME ) );
expect( result.current.isDismissed ).toBe( false );
+ expect( result.current.hasResolved ).toBe( true );
} );
it( 'persists the dismissal through updateOptions', () => {
diff --git a/plugins/woocommerce/client/admin/client/hooks/use-endpoint-dismiss.ts b/plugins/woocommerce/client/admin/client/hooks/use-endpoint-dismiss.ts
index 54d6b5c6cd4..55dd73489d0 100644
--- a/plugins/woocommerce/client/admin/client/hooks/use-endpoint-dismiss.ts
+++ b/plugins/woocommerce/client/admin/client/hooks/use-endpoint-dismiss.ts
@@ -43,5 +43,5 @@ export const useEndpointDismiss = (
} );
};
- return { isDismissed, onDismiss };
+ return { isDismissed, hasResolved: true, onDismiss };
};
diff --git a/plugins/woocommerce/client/admin/client/hooks/use-option-dismiss.ts b/plugins/woocommerce/client/admin/client/hooks/use-option-dismiss.ts
index 8880370d859..b8e56592de2 100644
--- a/plugins/woocommerce/client/admin/client/hooks/use-option-dismiss.ts
+++ b/plugins/woocommerce/client/admin/client/hooks/use-option-dismiss.ts
@@ -6,6 +6,7 @@ import { optionsStore } from '@woocommerce/data';
export type DismissState = {
isDismissed: boolean;
+ hasResolved: boolean;
onDismiss: () => void;
};
@@ -20,7 +21,7 @@ export type DismissState = {
* @return The current dismissal state and a callback to dismiss.
*/
export const useOptionDismiss = ( optionName: string ): DismissState => {
- const isDismissed = useSelect(
+ const { isDismissed, hasResolved } = useSelect(
( select ) => {
const { getOption, hasFinishedResolution } = select( optionsStore );
@@ -31,13 +32,16 @@ export const useOptionDismiss = ( optionName: string ): DismissState => {
// the card would stay hidden forever.
const value = getOption( optionName );
- const hasResolved = hasFinishedResolution( 'getOption', [
+ const optionHasResolved = hasFinishedResolution( 'getOption', [
optionName,
] );
// Treat "not yet resolved" as dismissed so the card does not flash
// before the option value is known.
- return ! hasResolved || value === 'yes';
+ return {
+ isDismissed: ! optionHasResolved || value === 'yes',
+ hasResolved: optionHasResolved,
+ };
},
[ optionName ]
);
@@ -48,5 +52,5 @@ export const useOptionDismiss = ( optionName: string ): DismissState => {
updateOptions( { [ optionName ]: 'yes' } );
};
- return { isDismissed, onDismiss };
+ return { isDismissed, hasResolved, onDismiss };
};
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations-utils.tsx b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations-utils.tsx
index c1a7ab75b05..84633f96b50 100644
--- a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations-utils.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations-utils.tsx
@@ -18,7 +18,17 @@ import {
DismissableListHeading,
} from '../settings-recommendations/dismissable-list';
import { TrackedLink } from '~/components/tracked-link/tracked-link';
-import { useOptionDismiss } from '~/hooks/use-option-dismiss';
+import { type DismissState } from '~/hooks/use-option-dismiss';
+
+export const SHIPPING_RECOMMENDATIONS_DISMISS_OPTION =
+ 'woocommerce_settings_shipping_recommendations_hidden';
+
+type ShippingRecommendationsMarketplaceLinkProps = {
+ textProps?: {
+ as?: string;
+ className?: string;
+ };
+};
export const useInstallPlugin = () => {
const [ pluginsBeingSetup, setPluginsBeingSetup ] = useState<
@@ -68,14 +78,32 @@ export const useInstallPlugin = () => {
return [ pluginsBeingSetup, handleInstall, handleActivate ] as const;
};
+export const ShippingRecommendationsMarketplaceLink = ( {
+ textProps,
+}: ShippingRecommendationsMarketplaceLinkProps ) => (
+ <TrackedLink
+ textProps={ textProps }
+ message={ __(
+ // translators: {{Link}} is a placeholder for a html element.
+ 'Visit {{Link}}the WooCommerce Marketplace{{/Link}} to find more shipping, delivery, and fulfillment solutions.',
+ 'woocommerce'
+ ) }
+ targetUrl={ getAdminLink(
+ 'admin.php?page=wc-admin&tab=extensions&path=/extensions&category=shipping-delivery-and-fulfillment'
+ ) }
+ linkType="wc-admin"
+ eventName="settings_shipping_recommendation_visit_marketplace_click"
+ />
+);
+
export const ShippingRecommendationsList = ( {
children,
+ dismissState,
}: {
children: React.ReactNode;
+ dismissState: DismissState;
} ) => {
- const { isDismissed, onDismiss } = useOptionDismiss(
- 'woocommerce_settings_shipping_recommendations_hidden'
- );
+ const { isDismissed, onDismiss } = dismissState;
return (
<DismissableList
@@ -105,18 +133,7 @@ export const ShippingRecommendationsList = ( {
) ) }
</ul>
<CardFooter>
- <TrackedLink
- message={ __(
- // translators: {{Link}} is a placeholder for a html element.
- 'Visit {{Link}}the WooCommerce Marketplace{{/Link}} to find more shipping, delivery, and fulfillment solutions.',
- 'woocommerce'
- ) }
- targetUrl={ getAdminLink(
- 'admin.php?page=wc-admin&tab=extensions&path=/extensions&category=shipping-delivery-and-fulfillment'
- ) }
- linkType="wc-admin"
- eventName="settings_shipping_recommendation_visit_marketplace_click"
- />
+ <ShippingRecommendationsMarketplaceLink />
</CardFooter>
</DismissableList>
);
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.scss b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.scss
index 7fd64251b55..d85085d8cdd 100644
--- a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.scss
+++ b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.scss
@@ -41,3 +41,9 @@
max-width: 749px;
}
}
+
+.woocommerce-recommended-shipping__fallback-link {
+ margin: 0;
+ padding: 16px 0 60px;
+ color: $gray-700;
+}
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
index abc40fbedc3..770fffdf5a9 100644
--- a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
@@ -14,11 +14,14 @@ import { recordEvent } from '@woocommerce/tracks';
* Internal dependencies
*/
import { getCountryCode } from '~/dashboard/utils';
+import { useOptionDismiss } from '~/hooks/use-option-dismiss';
import WooCommerceShippingItem from './woocommerce-shipping-item';
import ShipStationItem from './shipstation-item';
import PacklinkItem from './packlink-item';
import {
+ SHIPPING_RECOMMENDATIONS_DISMISS_OPTION,
ShippingRecommendationsList,
+ ShippingRecommendationsMarketplaceLink,
useInstallPlugin,
} from './shipping-recommendations-utils';
import './shipping-recommendations.scss';
@@ -52,20 +55,35 @@ const EXTENSION_PLUGIN_SLUGS: Record< ExtensionId, string > = {
const ShippingRecommendations = () => {
const [ pluginsBeingSetup, handleInstall, handleActivate ] =
useInstallPlugin();
+ const recommendationsDismissState = useOptionDismiss(
+ SHIPPING_RECOMMENDATIONS_DISMISS_OPTION
+ );
+ const {
+ isDismissed: isRecommendationsHidden,
+ hasResolved: hasRecommendationsDismissResolved,
+ } = recommendationsDismissState;
const {
installedPlugins,
activePlugins,
countryCode,
isSellingDigitalProductsOnly,
+ hasRecommendationEligibilityResolved,
} = useSelect( ( select ) => {
- const settings = select( settingsStore ).getSettings( 'general' );
+ const {
+ getSettings,
+ hasFinishedResolution: hasSettingsFinishedResolution,
+ } = select( settingsStore );
+ const {
+ getProfileItems,
+ hasFinishedResolution: hasOnboardingFinishedResolution,
+ } = select( onboardingStore );
+ const settings = getSettings( 'general' );
const { getInstalledPlugins, getActivePlugins } =
select( pluginsStore );
- const profileItems =
- select( onboardingStore ).getProfileItems()?.product_types;
+ const profileItems = getProfileItems()?.product_types;
return {
installedPlugins: getInstalledPlugins(),
@@ -75,6 +93,9 @@ const ShippingRecommendations = () => {
),
isSellingDigitalProductsOnly:
profileItems?.length === 1 && profileItems[ 0 ] === 'downloads',
+ hasRecommendationEligibilityResolved:
+ hasSettingsFinishedResolution( 'getSettings', [ 'general' ] ) &&
+ hasOnboardingFinishedResolution( 'getProfileItems', [] ),
};
}, [] );
@@ -92,13 +113,40 @@ const ShippingRecommendations = () => {
? []
: extensionsForCountry;
+ const hasVisibleExtensions = visibleExtensions.length > 0;
+ // Country and product type both determine which final state should render.
+ // Wait for them to settle so the fallback and recommendations do not swap.
+ const shouldWaitForRecommendationData =
+ ! hasRecommendationEligibilityResolved ||
+ ( ! hasRecommendationsDismissResolved && hasVisibleExtensions );
+ const shouldShowRecommendationsFallback =
+ ( hasRecommendationsDismissResolved && isRecommendationsHidden ) ||
+ ! hasVisibleExtensions ||
+ isSellingDigitalProductsOnly;
+ const shouldTrackRecommendationsImpression =
+ hasRecommendationEligibilityResolved &&
+ hasRecommendationsDismissResolved &&
+ ! isRecommendationsHidden &&
+ hasVisibleExtensions;
+
const visiblePluginSlugs = visibleExtensions
.map( ( ext ) => EXTENSION_PLUGIN_SLUGS[ ext ] )
.join( ',' );
+ const marketplaceFallbackLink = (
+ <ShippingRecommendationsMarketplaceLink
+ textProps={ {
+ as: 'p',
+ className: 'woocommerce-recommended-shipping__fallback-link',
+ } }
+ />
+ );
const impressionFired = useRef( false );
useEffect( () => {
- if ( visibleExtensions.length > 0 && ! impressionFired.current ) {
+ if (
+ shouldTrackRecommendationsImpression &&
+ ! impressionFired.current
+ ) {
recordEvent( 'shipping_partner_impression', {
context: 'settings',
country: normalizedCountry,
@@ -106,75 +154,94 @@ const ShippingRecommendations = () => {
} );
impressionFired.current = true;
}
- }, [ visibleExtensions.length, normalizedCountry, visiblePluginSlugs ] );
-
- if ( isSellingDigitalProductsOnly ) {
- return <ShippingTour showShippingRecommendationsStep={ false } />;
+ }, [
+ shouldTrackRecommendationsImpression,
+ normalizedCountry,
+ visiblePluginSlugs,
+ ] );
+
+ if ( shouldWaitForRecommendationData ) {
+ return (
+ <>
+ <ShippingTour showShippingRecommendationsStep={ false } />
+ </>
+ );
}
- if ( visibleExtensions.length === 0 ) {
- return <ShippingTour showShippingRecommendationsStep={ false } />;
+ if ( shouldShowRecommendationsFallback ) {
+ return (
+ <>
+ <ShippingTour showShippingRecommendationsStep={ false } />
+ { marketplaceFallbackLink }
+ </>
+ );
}
return (
- <div style={ { paddingBottom: 60 } }>
- <ShippingTour showShippingRecommendationsStep={ true } />
- <ShippingRecommendationsList>
- { visibleExtensions.map( ( ext ) => {
- const isPluginInstalled = installedPlugins.includes(
- EXTENSION_PLUGIN_SLUGS[ ext ]
- );
- const isPluginActive = activePlugins.includes(
- EXTENSION_PLUGIN_SLUGS[ ext ]
- );
- const trackingProps = {
- context: 'settings' as const,
- country: normalizedCountry,
- plugins: visiblePluginSlugs,
- };
- switch ( ext ) {
- case 'woocommerce-shipping':
- return (
- <WooCommerceShippingItem
- key={ ext }
- isPluginInstalled={ isPluginInstalled }
- isPluginActive={ isPluginActive }
- pluginsBeingSetup={ pluginsBeingSetup }
- onInstallClick={ handleInstall }
- onActivateClick={ handleActivate }
- tracking={ trackingProps }
- />
- );
- case 'shipstation':
- return (
- <ShipStationItem
- key={ ext }
- isPluginInstalled={ isPluginInstalled }
- isPluginActive={ isPluginActive }
- pluginsBeingSetup={ pluginsBeingSetup }
- onInstallClick={ handleInstall }
- onActivateClick={ handleActivate }
- tracking={ trackingProps }
- />
- );
- case 'packlink':
- return (
- <PacklinkItem
- key={ ext }
- isPluginInstalled={ isPluginInstalled }
- isPluginActive={ isPluginActive }
- pluginsBeingSetup={ pluginsBeingSetup }
- onInstallClick={ handleInstall }
- onActivateClick={ handleActivate }
- tracking={ trackingProps }
- />
- );
- default:
- return null;
- }
- } ) }
- </ShippingRecommendationsList>
- </div>
+ <>
+ <ShippingTour
+ showShippingRecommendationsStep={ ! isRecommendationsHidden }
+ />
+ <div style={ { paddingBottom: 60 } }>
+ <ShippingRecommendationsList
+ dismissState={ recommendationsDismissState }
+ >
+ { visibleExtensions.map( ( ext ) => {
+ const isPluginInstalled = installedPlugins.includes(
+ EXTENSION_PLUGIN_SLUGS[ ext ]
+ );
+ const isPluginActive = activePlugins.includes(
+ EXTENSION_PLUGIN_SLUGS[ ext ]
+ );
+ const trackingProps = {
+ context: 'settings' as const,
+ country: normalizedCountry,
+ plugins: visiblePluginSlugs,
+ };
+ switch ( ext ) {
+ case 'woocommerce-shipping':
+ return (
+ <WooCommerceShippingItem
+ key={ ext }
+ isPluginInstalled={ isPluginInstalled }
+ isPluginActive={ isPluginActive }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ tracking={ trackingProps }
+ />
+ );
+ case 'shipstation':
+ return (
+ <ShipStationItem
+ key={ ext }
+ isPluginInstalled={ isPluginInstalled }
+ isPluginActive={ isPluginActive }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ tracking={ trackingProps }
+ />
+ );
+ case 'packlink':
+ return (
+ <PacklinkItem
+ key={ ext }
+ isPluginInstalled={ isPluginInstalled }
+ isPluginActive={ isPluginActive }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ tracking={ trackingProps }
+ />
+ );
+ default:
+ return null;
+ }
+ } ) }
+ </ShippingRecommendationsList>
+ </div>
+ </>
);
};
diff --git a/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx b/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx
new file mode 100644
index 00000000000..6c06500bf95
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx
@@ -0,0 +1,294 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import ShippingRecommendations from '../shipping-recommendations';
+import { SHIPPING_RECOMMENDATIONS_DISMISS_OPTION } from '../shipping-recommendations-utils';
+
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ useSelect: jest.fn(),
+ useDispatch: jest.fn(),
+} ) );
+jest.mock( '~/components/tracked-link/tracked-link', () => ( {
+ TrackedLink: ( { textProps }: { textProps?: { className?: string } } ) => (
+ <span className={ textProps?.className }>
+ the WooCommerce Marketplace
+ </span>
+ ),
+} ) );
+jest.mock( '../../settings-recommendations/dismissable-list', () => ( {
+ DismissableList: ( {
+ children,
+ isDismissed,
+ }: {
+ children: React.ReactNode;
+ isDismissed?: boolean;
+ } ) => (
+ <div
+ data-dismissed={ String( Boolean( isDismissed ) ) }
+ data-testid="dismissable-list"
+ >
+ { ! isDismissed && children }
+ </div>
+ ),
+ DismissableListHeading: ( { children }: { children: React.ReactNode } ) =>
+ children,
+} ) );
+jest.mock( '~/guided-tours/shipping-tour', () => ( {
+ ShippingTour: ( {
+ showShippingRecommendationsStep,
+ }: {
+ showShippingRecommendationsStep: boolean;
+ } ) => (
+ <div
+ data-show-recommendations-step={ String(
+ showShippingRecommendationsStep
+ ) }
+ data-testid="shipping-tour"
+ />
+ ),
+} ) );
+jest.mock( '../woocommerce-shipping-item', () => () => (
+ <div>WooCommerce Shipping</div>
+) );
+jest.mock( '../shipstation-item', () => () => <div>ShipStation</div> );
+jest.mock( '../packlink-item', () => () => <div>Packlink PRO</div> );
+jest.mock( '../../lib/notices', () => ( {
+ createNoticesFromResponse: () => null,
+} ) );
+jest.mock( '@woocommerce/tracks', () => ( {
+ recordEvent: jest.fn(),
+} ) );
+
+const defaultSelectReturn = {
+ getActivePlugins: () => [],
+ getInstalledPlugins: () => [],
+ getSettings: () => ( {
+ general: {
+ woocommerce_default_country: 'US',
+ },
+ } ),
+ getProfileItems: () => ( {} ),
+ getOption: jest.fn().mockReturnValue( 'no' ),
+ hasFinishedResolution: jest.fn().mockReturnValue( true ),
+};
+
+const mockSelect = ( overrides: Record< string, unknown > = {} ) => {
+ ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
+ fn( () => ( {
+ ...defaultSelectReturn,
+ ...overrides,
+ } ) )
+ );
+};
+
+describe( 'ShippingRecommendations', () => {
+ beforeEach( () => {
+ mockSelect();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installPlugins: () => Promise.resolve(),
+ activatePlugins: () => Promise.resolve(),
+ } );
+ ( recordEvent as jest.Mock ).mockClear();
+ } );
+
+ it( 'renders recommendations and the shipping tour recommendations step', () => {
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'true'
+ );
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ {
+ context: 'settings',
+ country: 'US',
+ plugins:
+ 'woocommerce-shipping,woocommerce-shipstation-integration',
+ }
+ );
+ } );
+
+ it( 'waits for the dismissal option without remounting the shipping tour', () => {
+ let hasDismissResolved = false;
+ mockSelect( {
+ hasFinishedResolution: ( selector: string ) =>
+ selector === 'getOption' ? hasDismissResolved : true,
+ } );
+
+ const { rerender } = render( <ShippingRecommendations /> );
+ const initialShippingTour = screen.getByTestId( 'shipping-tour' );
+
+ expect(
+ screen.queryByText( 'the WooCommerce Marketplace' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId( 'dismissable-list' )
+ ).not.toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'false'
+ );
+ expect( recordEvent ).not.toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ expect.anything()
+ );
+
+ hasDismissResolved = true;
+ rerender( <ShippingRecommendations /> );
+
+ expect( screen.getByTestId( 'shipping-tour' ) ).toBe(
+ initialShippingTour
+ );
+ expect( screen.getByTestId( 'dismissable-list' ) ).toBeInTheDocument();
+ expect( recordEvent ).toHaveBeenCalledTimes( 1 );
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ {
+ context: 'settings',
+ country: 'US',
+ plugins:
+ 'woocommerce-shipping,woocommerce-shipstation-integration',
+ }
+ );
+ } );
+
+ it( 'does not render recommendations before the country settings resolve', () => {
+ mockSelect( {
+ getSettings: () => ( { general: {} } ),
+ hasFinishedResolution: ( selector: string ) =>
+ selector !== 'getSettings',
+ } );
+
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'the WooCommerce Marketplace' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId( 'dismissable-list' )
+ ).not.toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'false'
+ );
+ expect( recordEvent ).not.toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ expect.anything()
+ );
+ } );
+
+ it( 'does not render recommendations before the product profile resolves', () => {
+ mockSelect( {
+ hasFinishedResolution: ( selector: string ) =>
+ selector !== 'getProfileItems',
+ } );
+
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'the WooCommerce Marketplace' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId( 'dismissable-list' )
+ ).not.toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'false'
+ );
+ expect( recordEvent ).not.toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ expect.anything()
+ );
+ } );
+
+ it( 'renders the marketplace fallback when recommendations are dismissed', () => {
+ mockSelect( {
+ getOption: ( option: string ) =>
+ option === SHIPPING_RECOMMENDATIONS_DISMISS_OPTION
+ ? 'yes'
+ : undefined,
+ } );
+
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'the WooCommerce Marketplace' )
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByTestId( 'dismissable-list' )
+ ).not.toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'false'
+ );
+ expect( recordEvent ).not.toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ expect.anything()
+ );
+ } );
+
+ it( 'renders the marketplace fallback when there are no country recommendations', () => {
+ mockSelect( {
+ getSettings: () => ( {
+ general: {
+ woocommerce_default_country: 'JP',
+ },
+ } ),
+ } );
+
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'the WooCommerce Marketplace' )
+ ).toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'false'
+ );
+ } );
+
+ it( 'renders the marketplace fallback for stores selling digital products only', () => {
+ mockSelect( {
+ getProfileItems: () => ( {
+ product_types: [ 'downloads' ],
+ } ),
+ } );
+
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'the WooCommerce Marketplace' )
+ ).toBeInTheDocument();
+ expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
+ 'data-show-recommendations-step',
+ 'false'
+ );
+ expect( recordEvent ).not.toHaveBeenCalledWith(
+ 'shipping_partner_impression',
+ expect.anything()
+ );
+ } );
+} );
diff --git a/plugins/woocommerce/includes/admin/views/html-admin-settings.php b/plugins/woocommerce/includes/admin/views/html-admin-settings.php
index 378fa9c4484..84e9fb1c7c4 100644
--- a/plugins/woocommerce/includes/admin/views/html-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/views/html-admin-settings.php
@@ -87,12 +87,6 @@ $marketplace_links = array(
/* translators: %1$s: opening link tag, %2$s: closing link tag */
'message' => __( '%1$sExplore solutions%2$s that help with tax calculations, compliance, and regional requirements.', 'woocommerce' ),
),
- 'shipping' => array(
- 'url' => $marketplace_base_url . 'shipping-delivery-and-fulfillment/',
- 'is_external' => true,
- /* translators: %1$s: opening link tag, %2$s: closing link tag */
- 'message' => __( '%1$sExplore solutions%2$s that enhance shipping, delivery, and fulfillment workflows.', 'woocommerce' ),
- ),
'account' => array(
'url' => $marketplace_base_url . 'store-content-and-customizations/cart-and-checkout-features/',
'is_external' => true,
diff --git a/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php b/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php
new file mode 100644
index 00000000000..30bf6b48394
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php
@@ -0,0 +1,127 @@
+<?php
+declare( strict_types = 1 );
+/**
+ * Class WC_Admin_Settings_View_Test file.
+ *
+ * @package WooCommerce\Tests\Admin\Views
+ */
+
+/**
+ * Unit tests for admin settings views.
+ */
+class WC_Admin_Settings_View_Test extends WC_Unit_Test_Case {
+
+ /**
+ * Hook priority for admin feature mocks.
+ *
+ * @var int
+ */
+ private const HOOK_PRIORITY = 9999;
+
+ /**
+ * Mocked WooCommerce Admin feature flags.
+ *
+ * @var string[]
+ */
+ private $enabled_admin_features_mock = array();
+
+ /**
+ * Admin user ID.
+ *
+ * @var int
+ */
+ private $admin_user_id = 0;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ add_filter( 'woocommerce_admin_features', array( $this, 'get_mocked_admin_features' ), self::HOOK_PRIORITY );
+
+ $this->admin_user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $this->admin_user_id );
+ update_option( 'woocommerce_show_marketplace_suggestions', 'yes' );
+ update_option( 'woocommerce_default_country', 'US:CA' );
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ remove_filter( 'woocommerce_admin_features', array( $this, 'get_mocked_admin_features' ), self::HOOK_PRIORITY );
+ unset( $_GET['zone_id'] );
+ delete_option( 'woocommerce_show_marketplace_suggestions' );
+ delete_option( 'woocommerce_default_country' );
+ delete_option( 'woocommerce_onboarding_profile' );
+ wp_set_current_user( 0 );
+
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox Should not render the generic shipping marketplace link on Shipping settings.
+ */
+ public function test_shipping_marketplace_link_is_not_rendered_on_shipping_settings(): void {
+ $output = $this->render_settings_view( 'shipping' );
+
+ $this->assertStringNotContainsString(
+ 'wc-settings-marketplace-link',
+ $output,
+ 'Shipping settings marketplace fallback links are rendered by the shipping recommendations component.'
+ );
+ $this->assertStringNotContainsString(
+ 'shipping-delivery-and-fulfillment',
+ $output,
+ 'The generic Shipping settings marketplace URL should not be rendered by the PHP settings template.'
+ );
+ }
+
+ /**
+ * @testdox Should keep non-shipping marketplace links when shipping smart defaults are enabled.
+ */
+ public function test_non_shipping_marketplace_links_are_rendered_when_shipping_smart_defaults_are_enabled(): void {
+ $this->enabled_admin_features_mock = array( 'shipping-smart-defaults' );
+
+ $output = $this->render_settings_view( 'products' );
+
+ $this->assertStringContainsString(
+ 'data-settings-tab="products"',
+ $output,
+ 'Marketplace links for unrelated settings tabs should not be affected.'
+ );
+ $this->assertStringContainsString(
+ 'merchandising',
+ $output,
+ 'Marketplace URLs for unrelated settings tabs should not be affected.'
+ );
+ }
+
+ /**
+ * Render the admin settings view.
+ *
+ * @param string $current_tab The current settings tab.
+ * @param string $current_section The current settings section.
+ * @return string
+ */
+ private function render_settings_view( string $current_tab, string $current_section = '' ): string {
+ $tabs = array(
+ 'products' => 'Products',
+ 'shipping' => 'Shipping',
+ );
+
+ ob_start();
+ include WC_ABSPATH . 'includes/admin/views/html-admin-settings.php';
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * Returns a mocked list of enabled WC Admin features.
+ *
+ * @return string[]
+ */
+ public function get_mocked_admin_features(): array {
+ return $this->enabled_admin_features_mock;
+ }
+}