Commit 1ff725769b6 for woocommerce
commit 1ff725769b6ead4124341b2a71aafced825012e1
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Tue Feb 24 12:06:11 2026 +0300
[WOOPLUG-6269] add: shipping settings: update partner integration placements (#63383)
* [WOOPLUG-6269] add: shipping settings: update partner integration placements
* Add shipstation and packlink to the recommendations list
* Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin
* Improve error handling, reuse existing plugin install hook, revert global object after executing test
* Update shipping item components to use isPluginInstalled prop consistently
* Enhance shipping plugin integration with setup handling and success notices
* Fix test
* Break install step into two: Install & Activate, add click tracking
* Fix lint error
* Update plugin installation and activation to use correct types
* Fix styling and outdated copy
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63383-add-wooplug-6269-shipping-settings-update-partner-integration-placements b/plugins/woocommerce/changelog/63383-add-wooplug-6269-shipping-settings-update-partner-integration-placements
new file mode 100644
index 00000000000..140ea8657f0
--- /dev/null
+++ b/plugins/woocommerce/changelog/63383-add-wooplug-6269-shipping-settings-update-partner-integration-placements
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add ShipStation and Packlink PRO as recommended shipping solutions on the Shipping settings page, filtered by store country.
\ 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 ca584b2be3d..ef4bf9d0e20 100644
--- a/plugins/woocommerce/client/admin/client/shipping/experimental-shipping-recommendations.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/experimental-shipping-recommendations.tsx
@@ -2,7 +2,6 @@
* External dependencies
*/
import { useSelect } from '@wordpress/data';
-
import {
pluginsStore,
settingsStore,
@@ -14,11 +13,42 @@ import {
*/
import { getCountryCode } from '~/dashboard/utils';
import WooCommerceShippingItem from './experimental-woocommerce-shipping-item';
-import { ShippingRecommendationsList } from './shipping-recommendations';
+import ShipStationItem from './shipstation-item';
+import PacklinkItem from './packlink-item';
+import {
+ ShippingRecommendationsList,
+ useInstallPlugin,
+} from './shipping-recommendations';
import './shipping-recommendations.scss';
import { ShippingTour } from '../guided-tours/shipping-tour';
+type ExtensionId = 'woocommerce-shipping' | 'shipstation' | 'packlink';
+
+const COUNTRY_EXTENSIONS_MAP: Record< string, ExtensionId[] > = {
+ US: [ 'woocommerce-shipping', 'shipstation' ],
+ CA: [ 'shipstation' ],
+ FR: [ 'packlink' ],
+ ES: [ 'packlink' ],
+ IT: [ 'packlink' ],
+ DE: [ 'shipstation', 'packlink' ],
+ GB: [ 'shipstation', 'packlink' ],
+ NL: [ 'packlink' ],
+ AT: [ 'packlink' ],
+ BE: [ 'packlink' ],
+ AU: [ 'shipstation' ],
+ NZ: [ 'shipstation' ],
+};
+
+const EXTENSION_PLUGIN_SLUGS: Record< ExtensionId, string > = {
+ 'woocommerce-shipping': 'woocommerce-shipping',
+ shipstation: 'woocommerce-shipstation-integration',
+ packlink: 'packlink-pro-shipping',
+};
+
const ShippingRecommendations = () => {
+ const [ pluginsBeingSetup, , handleInstall, handleActivate ] =
+ useInstallPlugin();
+
const {
activePlugins,
installedPlugins,
@@ -44,25 +74,66 @@ const ShippingRecommendations = () => {
};
}, [] );
- if ( activePlugins.includes( 'woocommerce-shipping' ) ) {
+ if ( isSellingDigitalProductsOnly ) {
return <ShippingTour showShippingRecommendationsStep={ false } />;
}
- if ( countryCode !== 'US' || isSellingDigitalProductsOnly ) {
+ const extensionsForCountry =
+ COUNTRY_EXTENSIONS_MAP[ countryCode ?? '' ] ?? [];
+
+ const visibleExtensions = extensionsForCountry.filter(
+ ( ext ) => ! activePlugins.includes( EXTENSION_PLUGIN_SLUGS[ ext ] )
+ );
+
+ if ( visibleExtensions.length === 0 ) {
return <ShippingTour showShippingRecommendationsStep={ false } />;
}
return (
- <>
+ <div style={ { paddingBottom: 60 } }>
<ShippingTour showShippingRecommendationsStep={ true } />
<ShippingRecommendationsList>
- <WooCommerceShippingItem
- isPluginInstalled={ installedPlugins.includes(
- 'woocommerce-shipping'
- ) }
- />
+ { visibleExtensions.map( ( ext ) => {
+ const isPluginInstalled = installedPlugins.includes(
+ EXTENSION_PLUGIN_SLUGS[ ext ]
+ );
+ switch ( ext ) {
+ case 'woocommerce-shipping':
+ return (
+ <WooCommerceShippingItem
+ key={ ext }
+ isPluginInstalled={ isPluginInstalled }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ />
+ );
+ case 'shipstation':
+ return (
+ <ShipStationItem
+ key={ ext }
+ isPluginInstalled={ isPluginInstalled }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ />
+ );
+ case 'packlink':
+ return (
+ <PacklinkItem
+ key={ ext }
+ isPluginInstalled={ isPluginInstalled }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ />
+ );
+ default:
+ return null;
+ }
+ } ) }
</ShippingRecommendationsList>
- </>
+ </div>
);
};
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 88c85fd1585..d7eb64a22f0 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
@@ -2,11 +2,10 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
+import { useDispatch } from '@wordpress/data';
import { Button, ExternalLink } from '@wordpress/components';
import { Pill } from '@woocommerce/components';
-import { getNewPath, navigateTo } from '@woocommerce/navigation';
import { recordEvent } from '@woocommerce/tracks';
-import { useLayoutContext } from '@woocommerce/admin-layout';
/**
* Internal dependencies
@@ -14,21 +13,41 @@ import { useLayoutContext } from '@woocommerce/admin-layout';
import './woocommerce-shipping-item.scss';
import WooIcon from './woo-icon.svg';
+const WOOCOMMERCE_SHIPPING_PLUGIN_SLUG = 'woocommerce-shipping';
+
const WooCommerceShippingItem = ( {
isPluginInstalled,
+ onInstallClick,
+ onActivateClick,
+ pluginsBeingSetup,
}: {
- isPluginInstalled: boolean | undefined;
+ isPluginInstalled: boolean;
+ pluginsBeingSetup: Array< string >;
+ onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
+ onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
} ) => {
- const { layoutString } = useLayoutContext();
+ const { createSuccessNotice } = useDispatch( 'core/notices' );
- const handleSetupClick = () => {
- recordEvent( 'tasklist_click', {
- task_name: 'shipping-recommendation',
- context: `${ layoutString }/wc-settings`,
- } );
- navigateTo( {
- url: getNewPath( { task: 'shipping-recommendation' }, '/', {} ),
+ const handleClick = () => {
+ recordEvent( 'settings_shipping_recommendation_setup_click', {
+ plugin: WOOCOMMERCE_SHIPPING_PLUGIN_SLUG,
+ action: isPluginInstalled ? 'activate' : 'install',
} );
+ const action = isPluginInstalled ? onActivateClick : onInstallClick;
+ action( [ WOOCOMMERCE_SHIPPING_PLUGIN_SLUG ] ).then(
+ () => {
+ createSuccessNotice(
+ isPluginInstalled
+ ? __( 'WooCommerce Shipping activated!', 'woocommerce' )
+ : __(
+ 'WooCommerce Shipping is installed!',
+ 'woocommerce'
+ ),
+ {}
+ );
+ },
+ () => {}
+ );
};
return (
@@ -57,10 +76,17 @@ const WooCommerceShippingItem = ( {
</span>
</div>
<div className="woocommerce-list__item-after">
- <Button isSecondary onClick={ handleSetupClick }>
+ <Button
+ variant={ isPluginInstalled ? 'primary' : 'secondary' }
+ onClick={ handleClick }
+ isBusy={ pluginsBeingSetup.includes(
+ WOOCOMMERCE_SHIPPING_PLUGIN_SLUG
+ ) }
+ disabled={ pluginsBeingSetup.length > 0 }
+ >
{ isPluginInstalled
? __( 'Activate', 'woocommerce' )
- : __( 'Get started', '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
new file mode 100644
index 00000000000..acff31e74b7
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/shipping/packlink-item.tsx
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useDispatch } from '@wordpress/data';
+import { Button, ExternalLink } from '@wordpress/components';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import './woocommerce-shipping-item.scss';
+
+const PACKLINK_PLUGIN_SLUG = 'packlink-pro-shipping';
+
+const PacklinkItem = ( {
+ isPluginInstalled,
+ onInstallClick,
+ onActivateClick,
+ pluginsBeingSetup,
+}: {
+ isPluginInstalled: boolean;
+ pluginsBeingSetup: Array< string >;
+ onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
+ onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
+} ) => {
+ const { createSuccessNotice } = useDispatch( 'core/notices' );
+
+ const handleClick = () => {
+ recordEvent( 'settings_shipping_recommendation_setup_click', {
+ plugin: PACKLINK_PLUGIN_SLUG,
+ action: isPluginInstalled ? 'activate' : 'install',
+ } );
+ const action = isPluginInstalled ? onActivateClick : onInstallClick;
+ action( [ PACKLINK_PLUGIN_SLUG ] ).then(
+ () => {
+ createSuccessNotice(
+ isPluginInstalled
+ ? __( 'Packlink PRO activated!', 'woocommerce' )
+ : __( 'Packlink PRO is installed!', 'woocommerce' ),
+ {}
+ );
+ },
+ () => {}
+ );
+ };
+
+ return (
+ <div className="woocommerce-list__item-inner woocommerce-shipping-plugin-item">
+ <div className="woocommerce-list__item-before">
+ <img
+ className="woocommerce-shipping-plugin-item__logo"
+ src="https://ps.w.org/packlink-pro-shipping/assets/icon-128x128.png"
+ alt=""
+ />
+ </div>
+ <div className="woocommerce-list__item-text">
+ <span className="woocommerce-list__item-title">
+ { __( 'Packlink PRO', 'woocommerce' ) }
+ </span>
+ <span className="woocommerce-list__item-content">
+ { __(
+ 'Leverage a multi-carrier shipping platform that automates order shipping and delivery, optimizes logistics, and offers pre-negotiated rates with carriers such as Royal Mail, Evri, UPS, DPD, Yodel and GlobalPost. Manage orders, print shipping labels individually or in bulk, track shipments in real time, and handle returns from a single dashboard.',
+ 'woocommerce'
+ ) }
+ <br />
+ <ExternalLink href="https://woocommerce.com/products/packlink-pro/">
+ { __( 'Learn more', 'woocommerce' ) }
+ </ExternalLink>
+ </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>
+ </div>
+ </div>
+ );
+};
+
+export default PacklinkItem;
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
index 361b75a06a1..eaf8a2ef77b 100644
--- a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
@@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import { useState, Children } from '@wordpress/element';
import { Text } from '@woocommerce/experimental';
-import { pluginsStore } from '@woocommerce/data';
+import { PluginNames, pluginsStore } from '@woocommerce/data';
import { getAdminLink } from '@woocommerce/settings';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore VisuallyHidden is present, it's just not typed
@@ -24,12 +24,13 @@ import WoocommerceShippingItem from './woocommerce-shipping-item';
import './shipping-recommendations.scss';
import { TrackedLink } from '~/components/tracked-link/tracked-link';
-const useInstallPlugin = () => {
+export const useInstallPlugin = () => {
const [ pluginsBeingSetup, setPluginsBeingSetup ] = useState<
Array< string >
>( [] );
- const { installAndActivatePlugins } = useDispatch( pluginsStore );
+ const { installAndActivatePlugins, installPlugins, activatePlugins } =
+ useDispatch( pluginsStore );
const handleSetup = ( slugs: string[] ): PromiseLike< void > => {
if ( pluginsBeingSetup.length > 0 ) {
@@ -50,7 +51,50 @@ const useInstallPlugin = () => {
} );
};
- return [ pluginsBeingSetup, handleSetup ] as const;
+ const handleInstall = ( slugs: string[] ): PromiseLike< void > => {
+ if ( pluginsBeingSetup.length > 0 ) {
+ return Promise.resolve();
+ }
+
+ setPluginsBeingSetup( slugs );
+
+ return installPlugins( slugs as Partial< PluginNames >[] )
+ .then( () => {
+ setPluginsBeingSetup( [] );
+ } )
+ .catch( ( response: { errors: Record< string, string > } ) => {
+ createNoticesFromResponse( response );
+ setPluginsBeingSetup( [] );
+
+ return Promise.reject();
+ } );
+ };
+
+ const handleActivate = ( slugs: string[] ): PromiseLike< void > => {
+ if ( pluginsBeingSetup.length > 0 ) {
+ return Promise.resolve();
+ }
+
+ setPluginsBeingSetup( slugs );
+
+ return activatePlugins( slugs as Partial< PluginNames >[] )
+ .then( () => {
+ setPluginsBeingSetup( [] );
+ } )
+ .catch( ( response: { errors: Record< string, string > } ) => {
+ createNoticesFromResponse( response );
+ setPluginsBeingSetup( [] );
+
+ return Promise.reject();
+ } );
+ };
+
+ return [
+ pluginsBeingSetup,
+ handleSetup,
+ handleInstall,
+ handleActivate,
+ ] as const;
};
export const ShippingRecommendationsList = ( {
@@ -74,7 +118,7 @@ export const ShippingRecommendationsList = ( {
lineHeight="16px"
>
{ __(
- 'We recommend adding one of the following shipping extensions to your store. The extension will be installed and activated for you when you click "Get started".',
+ 'We recommend adding one of the following shipping extensions to your store.',
'woocommerce'
) }
</Text>
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx b/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx
new file mode 100644
index 00000000000..14845b07a46
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/shipping/shipstation-item.tsx
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useDispatch } from '@wordpress/data';
+import { Button, ExternalLink } from '@wordpress/components';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import './woocommerce-shipping-item.scss';
+
+const SHIPSTATION_PLUGIN_SLUG = 'woocommerce-shipstation-integration';
+
+const ShipStationItem = ( {
+ isPluginInstalled,
+ onInstallClick,
+ onActivateClick,
+ pluginsBeingSetup,
+}: {
+ isPluginInstalled: boolean;
+ pluginsBeingSetup: Array< string >;
+ onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
+ onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
+} ) => {
+ const { createSuccessNotice } = useDispatch( 'core/notices' );
+
+ const handleClick = () => {
+ recordEvent( 'settings_shipping_recommendation_setup_click', {
+ plugin: SHIPSTATION_PLUGIN_SLUG,
+ action: isPluginInstalled ? 'activate' : 'install',
+ } );
+ const action = isPluginInstalled ? onActivateClick : onInstallClick;
+ action( [ SHIPSTATION_PLUGIN_SLUG ] ).then(
+ () => {
+ createSuccessNotice(
+ isPluginInstalled
+ ? __( 'ShipStation activated!', 'woocommerce' )
+ : __( 'ShipStation is installed!', 'woocommerce' ),
+ {}
+ );
+ },
+ () => {}
+ );
+ };
+
+ return (
+ <div className="woocommerce-list__item-inner woocommerce-shipping-plugin-item">
+ <div className="woocommerce-list__item-before">
+ <img
+ className="woocommerce-shipping-plugin-item__logo"
+ src="https://ps.w.org/woocommerce-shipstation-integration/assets/icon-128x128.png"
+ alt=""
+ />
+ </div>
+ <div className="woocommerce-list__item-text">
+ <span className="woocommerce-list__item-title">
+ { __( 'ShipStation', 'woocommerce' ) }
+ </span>
+ <span className="woocommerce-list__item-content">
+ { __(
+ 'Ship your WooCommerce orders with confidence, save on top carriers, and automate your processes with ShipStation.',
+ 'woocommerce'
+ ) }
+ <br />
+ <ExternalLink href="https://woocommerce.com/products/shipstation-integration/">
+ { __( 'Learn more', 'woocommerce' ) }
+ </ExternalLink>
+ </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>
+ </div>
+ </div>
+ );
+};
+
+export default ShipStationItem;
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 731b412965e..54ada43e5c2 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
@@ -1,9 +1,10 @@
/**
* External dependencies
*/
-import { render, screen, fireEvent } from '@testing-library/react';
-import { useSelect } from '@wordpress/data';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { useSelect, useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
+import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
@@ -13,11 +14,15 @@ import ShippingRecommendations from '../experimental-shipping-recommendations';
jest.mock( '@wordpress/data', () => ( {
...jest.requireActual( '@wordpress/data' ),
useSelect: jest.fn(),
+ useDispatch: jest.fn(),
} ) );
jest.mock( '../../settings-recommendations/dismissable-list', () => ( {
DismissableList: ( ( { children } ) => children ) as React.FC,
DismissableListHeading: ( ( { children } ) => children ) as React.FC,
} ) );
+jest.mock( '../../lib/notices', () => ( {
+ createNoticesFromResponse: () => null,
+} ) );
jest.mock( '@woocommerce/admin-layout', () => {
const mockContext = {
layoutPath: [ 'home' ],
@@ -51,100 +56,523 @@ const defaultSelectReturn = {
getOption: jest.fn(),
};
+const mockSelectForCountry = (
+ countryCode: string,
+ activePlugins: string[] = [],
+ overrides: Record< string, unknown > = {}
+) => {
+ ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
+ fn( () => ( {
+ ...defaultSelectReturn,
+ getActivePlugins: () => activePlugins,
+ getSettings: () => ( {
+ general: {
+ woocommerce_default_country: countryCode,
+ },
+ } ),
+ ...overrides,
+ } ) )
+ );
+};
+
describe( 'ShippingRecommendations', () => {
beforeEach( () => {
( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
fn( () => ( { ...defaultSelectReturn } ) )
);
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: () => Promise.resolve(),
+ installPlugins: () => Promise.resolve(),
+ activatePlugins: () => Promise.resolve(),
+ createSuccessNotice: () => null,
+ } );
} );
- it( `should not render if the following plugins are active: woocommerce-shipping`, () => {
- ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
- fn( () => ( {
- ...defaultSelectReturn,
- getActivePlugins: () => 'woocommerce-shipping',
- } ) )
+ describe( 'country-based filtering', () => {
+ it( 'should show WooCommerce Shipping and ShipStation for US', () => {
+ mockSelectForCountry( 'US' );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ expect(
+ screen.queryByText( 'Packlink PRO' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should show only ShipStation for CA', () => {
+ mockSelectForCountry( 'CA' );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show only Packlink PRO for FR', () => {
+ mockSelectForCountry( 'FR' );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'ShipStation' )
+ ).not.toBeInTheDocument();
+ expect( screen.queryByText( 'Packlink PRO' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show ShipStation and Packlink PRO for DE', () => {
+ mockSelectForCountry( 'DE' );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'Packlink PRO' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show ShipStation and Packlink PRO for GB', () => {
+ mockSelectForCountry( 'GB' );
+ render( <ShippingRecommendations /> );
+
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'Packlink PRO' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show only ShipStation for AU', () => {
+ mockSelectForCountry( 'AU' );
+ render( <ShippingRecommendations /> );
+
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ expect(
+ screen.queryByText( 'Packlink PRO' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should show only ShipStation for NZ', () => {
+ mockSelectForCountry( 'NZ' );
+ render( <ShippingRecommendations /> );
+
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ } );
+
+ it.each( [ 'ES', 'IT', 'NL', 'AT', 'BE' ] )(
+ 'should show only Packlink PRO for %s',
+ ( country ) => {
+ mockSelectForCountry( country );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'Packlink PRO' )
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText( 'ShipStation' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ }
);
- render( <ShippingRecommendations /> );
+ it( 'should not render recommendations for unsupported countries', () => {
+ mockSelectForCountry( 'JP' );
+ render( <ShippingRecommendations /> );
- expect(
- screen.queryByText( 'WooCommerce Shipping' )
- ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'ShipStation' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'Packlink PRO' )
+ ).not.toBeInTheDocument();
+ } );
} );
- it( 'should not render when store location is not US', () => {
- ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
- fn( () => ( {
- ...defaultSelectReturn,
- getSettings: () => ( {
- general: {
- woocommerce_default_country: 'JP',
- },
- } ),
- } ) )
- );
- render( <ShippingRecommendations /> );
+ describe( 'active plugin filtering', () => {
+ it( 'should not show WooCommerce Shipping when it is already active', () => {
+ mockSelectForCountry( 'US', [ 'woocommerce-shipping' ] );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should not show ShipStation when it is already active', () => {
+ mockSelectForCountry( 'US', [
+ 'woocommerce-shipstation-integration',
+ ] );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText( 'ShipStation' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not show Packlink PRO when it is already active', () => {
+ mockSelectForCountry( 'DE', [ 'packlink-pro-shipping' ] );
+ render( <ShippingRecommendations /> );
- expect(
- screen.queryByText( 'WooCommerce Shipping' )
- ).not.toBeInTheDocument();
+ expect( screen.queryByText( 'ShipStation' ) ).toBeInTheDocument();
+ expect(
+ screen.queryByText( 'Packlink PRO' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not render recommendations when all extensions for a country are active', () => {
+ mockSelectForCountry( 'US', [
+ 'woocommerce-shipping',
+ 'woocommerce-shipstation-integration',
+ ] );
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'ShipStation' )
+ ).not.toBeInTheDocument();
+ } );
} );
- it( 'should not render when store sells digital products only', () => {
- ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
- fn( () => ( {
- ...defaultSelectReturn,
- getProfileItems: () => ( {
- product_types: [ 'downloads' ],
- } ),
- } ) )
- );
- render( <ShippingRecommendations /> );
+ describe( 'digital products only', () => {
+ it( 'should not render when store sells digital products only', () => {
+ ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
+ fn( () => ( {
+ ...defaultSelectReturn,
+ getProfileItems: () => ( {
+ product_types: [ 'downloads' ],
+ } ),
+ } ) )
+ );
+ render( <ShippingRecommendations /> );
- expect(
- screen.queryByText( 'WooCommerce Shipping' )
- ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).not.toBeInTheDocument();
+ } );
} );
- it( 'should render WC Shipping when not installed', () => {
- render( <ShippingRecommendations /> );
+ describe( 'WooCommerce Shipping item', () => {
+ it( 'should render WC Shipping when not installed', () => {
+ render( <ShippingRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should trigger event settings_shipping_recommendation_visit_marketplace_click when clicking the WooCommerce Marketplace link', () => {
+ render( <ShippingRecommendations /> );
+
+ fireEvent.click(
+ screen.getByText( 'the WooCommerce Marketplace' )
+ );
+
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_visit_marketplace_click',
+ {}
+ );
+ } );
+
+ it( 'should navigate to the marketplace when clicking the WooCommerce Marketplace link', async () => {
+ const { isFeatureEnabled } = jest.requireMock( '~/utils/features' );
+ const originalLocation = global.window.location;
+ ( isFeatureEnabled as jest.Mock ).mockReturnValue( true );
+
+ const mockLocation = {
+ href: 'test',
+ } as Location;
- expect(
- screen.queryByText( 'WooCommerce Shipping' )
- ).toBeInTheDocument();
+ mockLocation.href = 'test';
+ Object.defineProperty( global.window, 'location', {
+ value: mockLocation,
+ } );
+
+ render( <ShippingRecommendations /> );
+
+ fireEvent.click(
+ screen.getByText( 'the WooCommerce Marketplace' )
+ );
+
+ expect( mockLocation.href ).toContain(
+ 'admin.php?page=wc-admin&tab=extensions&path=/extensions&category=shipping'
+ );
+
+ Object.defineProperty( global.window, 'location', {
+ value: originalLocation,
+ } );
+ } );
} );
- it( 'should trigger event settings_shipping_recommendation_visit_marketplace_click when clicking the WooCommerce Marketplace link', () => {
- render( <ShippingRecommendations /> );
+ describe( 'plugin installation', () => {
+ it( 'allows to install WooCommerce Shipping', async () => {
+ const installPluginsMock = jest.fn().mockResolvedValue( undefined );
+ const successNoticeMock = jest.fn();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: jest
+ .fn()
+ .mockResolvedValue( undefined ),
+ installPlugins: installPluginsMock,
+ activatePlugins: jest.fn().mockResolvedValue( undefined ),
+ createSuccessNotice: successNoticeMock,
+ } );
+ mockSelectForCountry( 'US', [
+ 'woocommerce-shipstation-integration',
+ ] );
+ render( <ShippingRecommendations /> );
- fireEvent.click( screen.getByText( 'the WooCommerce Marketplace' ) );
+ userEvent.click( screen.getByText( 'Install' ) );
- expect( recordEvent ).toHaveBeenCalledWith(
- 'settings_shipping_recommendation_visit_marketplace_click',
- {}
- );
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-shipping',
+ action: 'install',
+ }
+ );
+ expect( installPluginsMock ).toHaveBeenCalledWith( [
+ 'woocommerce-shipping',
+ ] );
+ await waitFor( () => {
+ expect( successNoticeMock ).toHaveBeenCalledWith(
+ 'WooCommerce Shipping is installed!',
+ expect.anything()
+ );
+ } );
+ } );
+
+ it( 'allows to install ShipStation', async () => {
+ const installPluginsMock = jest.fn().mockResolvedValue( undefined );
+ const successNoticeMock = jest.fn();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: jest
+ .fn()
+ .mockResolvedValue( undefined ),
+ installPlugins: installPluginsMock,
+ activatePlugins: jest.fn().mockResolvedValue( undefined ),
+ createSuccessNotice: successNoticeMock,
+ } );
+ mockSelectForCountry( 'CA' );
+ render( <ShippingRecommendations /> );
+
+ userEvent.click( screen.getByText( 'Install' ) );
+
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-shipstation-integration',
+ action: 'install',
+ }
+ );
+ expect( installPluginsMock ).toHaveBeenCalledWith( [
+ 'woocommerce-shipstation-integration',
+ ] );
+ await waitFor( () => {
+ expect( successNoticeMock ).toHaveBeenCalledWith(
+ 'ShipStation is installed!',
+ expect.anything()
+ );
+ } );
+ } );
+
+ it( 'allows to install Packlink PRO', async () => {
+ const installPluginsMock = jest.fn().mockResolvedValue( undefined );
+ const successNoticeMock = jest.fn();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: jest
+ .fn()
+ .mockResolvedValue( undefined ),
+ installPlugins: installPluginsMock,
+ activatePlugins: jest.fn().mockResolvedValue( undefined ),
+ createSuccessNotice: successNoticeMock,
+ } );
+ mockSelectForCountry( 'FR' );
+ render( <ShippingRecommendations /> );
+
+ userEvent.click( screen.getByText( 'Install' ) );
+
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'packlink-pro-shipping',
+ action: 'install',
+ }
+ );
+ expect( installPluginsMock ).toHaveBeenCalledWith( [
+ 'packlink-pro-shipping',
+ ] );
+ await waitFor( () => {
+ expect( successNoticeMock ).toHaveBeenCalledWith(
+ 'Packlink PRO is installed!',
+ expect.anything()
+ );
+ } );
+ } );
} );
- it( 'should navigate to the marketplace when clicking the WooCommerce Marketplace link', async () => {
- const { isFeatureEnabled } = jest.requireMock( '~/utils/features' );
- ( isFeatureEnabled as jest.Mock ).mockReturnValue( true );
+ describe( 'plugin activation (installed but not active)', () => {
+ it( 'shows Activate button for WooCommerce Shipping when installed but not active', () => {
+ mockSelectForCountry( 'US', [], {
+ getInstalledPlugins: () => [ 'woocommerce-shipping' ],
+ } );
+ render( <ShippingRecommendations /> );
- const mockLocation = {
- href: 'test',
- } as Location;
+ const buttons = screen.getAllByText( 'Activate' );
+ expect( buttons ).toHaveLength( 1 );
+ expect(
+ screen.queryByText( 'WooCommerce Shipping' )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'shows Activate button for ShipStation when installed but not active', () => {
+ mockSelectForCountry( 'CA', [], {
+ getInstalledPlugins: () => [
+ 'woocommerce-shipstation-integration',
+ ],
+ } );
+ render( <ShippingRecommendations /> );
- mockLocation.href = 'test';
- Object.defineProperty( global.window, 'location', {
- value: mockLocation,
+ expect( screen.getByText( 'Activate' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'Install' ) ).not.toBeInTheDocument();
} );
- render( <ShippingRecommendations /> );
+ it( 'shows Activate button for Packlink PRO when installed but not active', () => {
+ mockSelectForCountry( 'FR', [], {
+ getInstalledPlugins: () => [ 'packlink-pro-shipping' ],
+ } );
+ render( <ShippingRecommendations /> );
- fireEvent.click( screen.getByText( 'the WooCommerce Marketplace' ) );
+ expect( screen.getByText( 'Activate' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'Install' ) ).not.toBeInTheDocument();
+ } );
- expect( mockLocation.href ).toContain(
- 'admin.php?page=wc-admin&tab=extensions&path=/extensions&category=shipping'
- );
+ it( 'shows activated notice for WooCommerce Shipping when activating installed plugin', async () => {
+ const activatePluginsMock = jest
+ .fn()
+ .mockResolvedValue( undefined );
+ const successNoticeMock = jest.fn();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: jest
+ .fn()
+ .mockResolvedValue( undefined ),
+ installPlugins: jest.fn().mockResolvedValue( undefined ),
+ activatePlugins: activatePluginsMock,
+ createSuccessNotice: successNoticeMock,
+ } );
+ mockSelectForCountry(
+ 'US',
+ [ 'woocommerce-shipstation-integration' ],
+ {
+ getInstalledPlugins: () => [ 'woocommerce-shipping' ],
+ }
+ );
+ render( <ShippingRecommendations /> );
+
+ userEvent.click( screen.getByText( 'Activate' ) );
+
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-shipping',
+ action: 'activate',
+ }
+ );
+ expect( activatePluginsMock ).toHaveBeenCalledWith( [
+ 'woocommerce-shipping',
+ ] );
+ await waitFor( () => {
+ expect( successNoticeMock ).toHaveBeenCalledWith(
+ 'WooCommerce Shipping activated!',
+ expect.anything()
+ );
+ } );
+ } );
+
+ it( 'shows activated notice for ShipStation when activating installed plugin', async () => {
+ const activatePluginsMock = jest
+ .fn()
+ .mockResolvedValue( undefined );
+ const successNoticeMock = jest.fn();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: jest
+ .fn()
+ .mockResolvedValue( undefined ),
+ installPlugins: jest.fn().mockResolvedValue( undefined ),
+ activatePlugins: activatePluginsMock,
+ createSuccessNotice: successNoticeMock,
+ } );
+ mockSelectForCountry( 'CA', [], {
+ getInstalledPlugins: () => [
+ 'woocommerce-shipstation-integration',
+ ],
+ } );
+ render( <ShippingRecommendations /> );
+
+ userEvent.click( screen.getByText( 'Activate' ) );
+
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-shipstation-integration',
+ action: 'activate',
+ }
+ );
+ expect( activatePluginsMock ).toHaveBeenCalledWith( [
+ 'woocommerce-shipstation-integration',
+ ] );
+ await waitFor( () => {
+ expect( successNoticeMock ).toHaveBeenCalledWith(
+ 'ShipStation activated!',
+ expect.anything()
+ );
+ } );
+ } );
+
+ it( 'shows activated notice for Packlink PRO when activating installed plugin', async () => {
+ const activatePluginsMock = jest
+ .fn()
+ .mockResolvedValue( undefined );
+ const successNoticeMock = jest.fn();
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ installAndActivatePlugins: jest
+ .fn()
+ .mockResolvedValue( undefined ),
+ installPlugins: jest.fn().mockResolvedValue( undefined ),
+ activatePlugins: activatePluginsMock,
+ createSuccessNotice: successNoticeMock,
+ } );
+ mockSelectForCountry( 'FR', [], {
+ getInstalledPlugins: () => [ 'packlink-pro-shipping' ],
+ } );
+ render( <ShippingRecommendations /> );
+
+ userEvent.click( screen.getByText( 'Activate' ) );
+
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'packlink-pro-shipping',
+ action: 'activate',
+ }
+ );
+ expect( activatePluginsMock ).toHaveBeenCalledWith( [
+ 'packlink-pro-shipping',
+ ] );
+ await waitFor( () => {
+ expect( successNoticeMock ).toHaveBeenCalledWith(
+ 'Packlink PRO activated!',
+ expect.anything()
+ );
+ } );
+ } );
} );
} );
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 2938e0400c4..21da32cc5f0 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
@@ -2,12 +2,17 @@
* External dependencies
*/
import { render, screen } from '@testing-library/react';
+import { useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import WooCommerceShippingItem from '../experimental-woocommerce-shipping-item';
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ useDispatch: jest.fn(),
+} ) );
jest.mock( '@woocommerce/tracks', () => ( {
...jest.requireActual( '@woocommerce/tracks' ),
recordEvent: jest.fn(),
@@ -28,20 +33,42 @@ jest.mock( '@woocommerce/admin-layout', () => {
} );
describe( 'WooCommerceShippingItem', () => {
- it( 'should render WC Shipping item with CTA = "Get started" when WC Shipping is not installed', () => {
- render( <WooCommerceShippingItem isPluginInstalled={ false } /> );
+ const defaultProps = {
+ pluginsBeingSetup: [] as string[],
+ onInstallClick: jest.fn( () => Promise.resolve() ),
+ onActivateClick: jest.fn( () => Promise.resolve() ),
+ };
+
+ beforeEach( () => {
+ ( useDispatch as jest.Mock ).mockReturnValue( {
+ createSuccessNotice: jest.fn(),
+ } );
+ } );
+
+ it( 'should render WC Shipping item with CTA = "Install" when WC Shipping is not installed', () => {
+ render(
+ <WooCommerceShippingItem
+ isPluginInstalled={ false }
+ { ...defaultProps }
+ />
+ );
expect(
screen.queryByText( 'WooCommerce Shipping' )
).toBeInTheDocument();
expect(
- screen.queryByRole( 'button', { name: 'Get started' } )
+ screen.queryByRole( 'button', { name: 'Install' } )
).toBeInTheDocument();
} );
it( 'should render WC Shipping item with CTA = "Activate" when WC Shipping is installed', () => {
- render( <WooCommerceShippingItem isPluginInstalled={ true } /> );
+ render(
+ <WooCommerceShippingItem
+ isPluginInstalled={ true }
+ { ...defaultProps }
+ />
+ );
expect(
screen.queryByText( 'WooCommerce Shipping' )
@@ -52,13 +79,73 @@ describe( 'WooCommerceShippingItem', () => {
).toBeInTheDocument();
} );
- it( 'should record track when clicking setup button', () => {
- render( <WooCommerceShippingItem isPluginInstalled={ false } /> );
+ it( 'should call onInstallClick when clicking Install button', () => {
+ const onInstallClick = jest.fn( () => Promise.resolve() );
+ render(
+ <WooCommerceShippingItem
+ isPluginInstalled={ false }
+ pluginsBeingSetup={ [] }
+ onInstallClick={ onInstallClick }
+ onActivateClick={ jest.fn( () => Promise.resolve() ) }
+ />
+ );
- screen.queryByRole( 'button', { name: 'Get started' } )?.click();
- expect( recordEvent ).toHaveBeenCalledWith( 'tasklist_click', {
- context: 'root/wc-settings',
- task_name: 'shipping-recommendation',
- } );
+ screen.queryByRole( 'button', { name: 'Install' } )?.click();
+ expect( onInstallClick ).toHaveBeenCalledWith( [
+ 'woocommerce-shipping',
+ ] );
+ } );
+
+ it( 'should record track when clicking Install button', () => {
+ render(
+ <WooCommerceShippingItem
+ isPluginInstalled={ false }
+ { ...defaultProps }
+ />
+ );
+
+ screen.queryByRole( 'button', { name: 'Install' } )?.click();
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-shipping',
+ action: 'install',
+ }
+ );
+ } );
+
+ it( 'should record track when clicking Activate button', () => {
+ render(
+ <WooCommerceShippingItem
+ isPluginInstalled={ true }
+ { ...defaultProps }
+ />
+ );
+
+ screen.queryByRole( 'button', { name: 'Activate' } )?.click();
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_shipping_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-shipping',
+ action: 'activate',
+ }
+ );
+ } );
+
+ it( 'should call onActivateClick when clicking Activate button', () => {
+ const onActivateClick = jest.fn( () => Promise.resolve() );
+ render(
+ <WooCommerceShippingItem
+ isPluginInstalled={ true }
+ pluginsBeingSetup={ [] }
+ onInstallClick={ jest.fn( () => Promise.resolve() ) }
+ onActivateClick={ onActivateClick }
+ />
+ );
+
+ screen.queryByRole( 'button', { name: 'Activate' } )?.click();
+ expect( onActivateClick ).toHaveBeenCalledWith( [
+ 'woocommerce-shipping',
+ ] );
} );
} );