Commit da01771f612 for woocommerce
commit da01771f612799b6ea36efb26f2d8685fbe17bcf
Author: Jason Kytros <jason.kytros@automattic.com>
Date: Fri Jun 26 11:47:42 2026 +0300
Add recommended tax solutions to tax settings (#65786)
* Add tax extension recommendations to settings
* Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin
* Add tax recommendation tracking events
* Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin
* Remove duplicate tax settings changefiles
* Remove recommended badge from tax recommendations
* Update recommendation tests to await async clicks
* Fix recommendation test lint errors
* Remove shipping test changes from PR
* Fix tax recommendation plugin aliases and logo
* Use WordPress.org Anrok logo in tax recommendations
* Remove unused tax recommendation badge remnants
* Filter and track tax recommendations by country
* Normalize tax recommendation country checks
* Add active state coverage for tax recommendations
* Updated copy
* Updated Anrok copy
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/65786-tax-settings-recommendations b/plugins/woocommerce/changelog/65786-tax-settings-recommendations
new file mode 100644
index 00000000000..b97e6860e41
--- /dev/null
+++ b/plugins/woocommerce/changelog/65786-tax-settings-recommendations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Added Tax extensions recommendations.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/embedded-body-layout/embedded-body-layout.tsx b/plugins/woocommerce/client/admin/client/embedded-body-layout/embedded-body-layout.tsx
index 63a85518b1d..7be6d6b5905 100644
--- a/plugins/woocommerce/client/admin/client/embedded-body-layout/embedded-body-layout.tsx
+++ b/plugins/woocommerce/client/admin/client/embedded-body-layout/embedded-body-layout.tsx
@@ -15,6 +15,7 @@ import QueryString, { parse } from 'qs';
*/
import { PaymentRecommendations } from '../payments';
import { ShippingRecommendations } from '../shipping';
+import { TaxRecommendations } from '../tax';
import { AbandonedCartRecoveryRecommendations } from '../abandoned-cart-recovery';
import { EmbeddedBodyProps } from './embedded-body-props';
import './style.scss';
@@ -30,6 +31,7 @@ function isWPPage(
const EMBEDDED_BODY_COMPONENT_LIST: React.ElementType[] = [
PaymentRecommendations,
ShippingRecommendations,
+ TaxRecommendations,
AbandonedCartRecoveryRecommendations,
];
diff --git a/plugins/woocommerce/client/admin/client/task-lists/fills/tax/utils.ts b/plugins/woocommerce/client/admin/client/task-lists/fills/tax/utils.ts
index 7752dbdba05..087dbee3b9f 100644
--- a/plugins/woocommerce/client/admin/client/task-lists/fills/tax/utils.ts
+++ b/plugins/woocommerce/client/admin/client/task-lists/fills/tax/utils.ts
@@ -9,6 +9,45 @@ import { TaskType } from '@woocommerce/data';
*/
export const AUTOMATION_PLUGINS = [ 'woocommerce-services' ];
+/**
+ * Countries where WooCommerce Tax automated taxes are supported.
+ *
+ * Mirrors the support gate used for automated taxes in core.
+ */
+export const WOOCOMMERCE_TAX_SUPPORTED_COUNTRIES = [
+ 'US',
+ 'CA',
+ 'AU',
+ 'GB',
+ 'AT',
+ 'BE',
+ 'BG',
+ 'HR',
+ 'CY',
+ 'CZ',
+ 'DK',
+ 'EE',
+ 'FI',
+ 'FR',
+ 'DE',
+ 'GR',
+ 'HU',
+ 'IE',
+ 'IT',
+ 'LV',
+ 'LT',
+ 'LU',
+ 'MT',
+ 'NL',
+ 'PL',
+ 'PT',
+ 'RO',
+ 'SK',
+ 'SI',
+ 'ES',
+ 'SE',
+];
+
/**
* Check if a store has a complete address given general settings.
*
@@ -53,6 +92,24 @@ export type TaxChildProps = {
children?: React.ReactNode;
};
+/**
+ * Check if WooCommerce Tax is supported for a given store country.
+ *
+ * @param {string|null} countryCode Country code.
+ * @return {boolean} If WooCommerce Tax is supported.
+ */
+export const supportsWooCommerceTax = (
+ countryCode: string | null
+): boolean => {
+ if ( ! countryCode ) {
+ return false;
+ }
+
+ return WOOCOMMERCE_TAX_SUPPORTED_COUNTRIES.includes(
+ countryCode.trim().toUpperCase()
+ );
+};
+
/**
* Check if a given country is supported by Avalara.
*
diff --git a/plugins/woocommerce/client/admin/client/tax/index.ts b/plugins/woocommerce/client/admin/client/tax/index.ts
new file mode 100644
index 00000000000..fe605d9e621
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/tax/index.ts
@@ -0,0 +1 @@
+export * from './tax-recommendations-wrapper';
diff --git a/plugins/woocommerce/client/admin/client/tax/tax-recommendations-wrapper.tsx b/plugins/woocommerce/client/admin/client/tax/tax-recommendations-wrapper.tsx
new file mode 100644
index 00000000000..734e260b578
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/tax/tax-recommendations-wrapper.tsx
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import { lazy, Suspense } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { EmbeddedBodyProps } from '../embedded-body-layout/embedded-body-props';
+import RecommendationsEligibilityWrapper from '../settings-recommendations/recommendations-eligibility-wrapper';
+
+const TaxRecommendationsLoader = lazy(
+ () =>
+ import(
+ /* webpackChunkName: "tax-recommendations" */ './tax-recommendations'
+ )
+);
+
+export const TaxRecommendations = ( {
+ page,
+ tab,
+ section,
+}: EmbeddedBodyProps ) => {
+ if ( page !== 'wc-settings' ) {
+ return null;
+ }
+
+ if ( tab !== 'tax' ) {
+ return null;
+ }
+
+ if ( Boolean( section ) ) {
+ return null;
+ }
+
+ return (
+ <RecommendationsEligibilityWrapper>
+ <Suspense fallback={ null }>
+ <TaxRecommendationsLoader />
+ </Suspense>
+ </RecommendationsEligibilityWrapper>
+ );
+};
diff --git a/plugins/woocommerce/client/admin/client/tax/tax-recommendations.scss b/plugins/woocommerce/client/admin/client/tax/tax-recommendations.scss
new file mode 100644
index 00000000000..5a3ffe1dea0
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/tax/tax-recommendations.scss
@@ -0,0 +1,43 @@
+.woocommerce-recommended-tax-extensions-wrapper {
+ padding-bottom: 60px;
+}
+
+.woocommerce-recommended-tax-extensions {
+ .woocommerce-list__item {
+ > .woocommerce-list__item-inner {
+ align-items: flex-start;
+ }
+
+ &:hover {
+ background-color: $white;
+
+ .woocommerce-list__item-title {
+ color: $gray-900;
+ }
+ }
+ }
+
+ .woocommerce-list__item-title {
+ font-size: 14px;
+ color: $gray-900;
+ font-weight: 600;
+ }
+
+ .woocommerce-list__item-after .components-button {
+ margin-left: $gap-small;
+ }
+
+ .woocommerce-list__item-text,
+ .woocommerce-recommended-tax__header-heading {
+ max-width: 749px;
+ }
+}
+
+.woocommerce-tax-recommendation-item {
+ &__logo {
+ display: block;
+ width: 36px;
+ height: 36px;
+ object-fit: contain;
+ }
+}
diff --git a/plugins/woocommerce/client/admin/client/tax/tax-recommendations.tsx b/plugins/woocommerce/client/admin/client/tax/tax-recommendations.tsx
new file mode 100644
index 00000000000..ec01f0e180d
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/tax/tax-recommendations.tsx
@@ -0,0 +1,385 @@
+/**
+ * External dependencies
+ */
+import { Button, CardFooter, ExternalLink } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { Children, useEffect, useRef, useState } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+import { Text } from '@woocommerce/experimental';
+import { PluginNames, pluginsStore, settingsStore } from '@woocommerce/data';
+import { getAdminLink } from '@woocommerce/settings';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import taxLogo from '../task-lists/fills/tax/woocommerce-tax/logo.png';
+import { createNoticesFromResponse } from '../lib/notices';
+import {
+ DismissableList,
+ DismissableListHeading,
+} from '../settings-recommendations/dismissable-list';
+import { supportsWooCommerceTax } from '../task-lists/fills/tax/utils';
+import { TrackedLink } from '~/components/tracked-link/tracked-link';
+import { getCountryCode } from '~/dashboard/utils';
+import './tax-recommendations.scss';
+
+const ANROK_LOGO_URL = 'https://ps.w.org/anrok-tax/assets/icon.svg';
+
+type TaxRecommendation = {
+ id: 'anrok-tax' | 'woocommerce-tax';
+ title: string;
+ description: string;
+ productUrl: string;
+ logo: React.ReactNode;
+ pluginSlugs: string[];
+};
+
+const useInstallPlugin = () => {
+ const [ pluginsBeingSetup, setPluginsBeingSetup ] = useState<
+ Array< string >
+ >( [] );
+
+ const { installPlugins, activatePlugins } = useDispatch( pluginsStore );
+
+ 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, handleInstall, handleActivate ] as const;
+};
+
+const TaxRecommendationItem = ( {
+ pluginSlug,
+ isPluginInstalled,
+ isPluginActive,
+ pluginsBeingSetup,
+ onInstallClick,
+ onActivateClick,
+ title,
+ description,
+ productUrl,
+ logo,
+}: TaxRecommendation & {
+ pluginSlug: string;
+ isPluginInstalled: boolean;
+ isPluginActive: boolean;
+ pluginsBeingSetup: Array< string >;
+ onInstallClick: ( slugs: string[] ) => PromiseLike< void >;
+ onActivateClick: ( slugs: string[] ) => PromiseLike< void >;
+} ) => {
+ const { createSuccessNotice } = useDispatch( 'core/notices' );
+
+ const handleLearnMoreClick = () => {
+ recordEvent( 'settings_tax_recommendation_click', {
+ extension: title,
+ } );
+ };
+
+ const handleClick = () => {
+ const trackingBase = {
+ context: 'settings',
+ selected_plugin: pluginSlug,
+ };
+
+ recordEvent( 'tax_partner_click', trackingBase );
+ recordEvent( 'settings_tax_recommendation_setup_click', {
+ plugin: pluginSlug,
+ action: isPluginInstalled ? 'activate' : 'install',
+ } );
+
+ const action = isPluginInstalled ? onActivateClick : onInstallClick;
+ const eventName = isPluginInstalled
+ ? 'tax_partner_activate'
+ : 'tax_partner_install';
+
+ action( [ pluginSlug ] ).then(
+ () => {
+ recordEvent( eventName, {
+ ...trackingBase,
+ success: true,
+ } );
+ createSuccessNotice(
+ isPluginInstalled
+ ? sprintf(
+ /* translators: %s: extension name. */
+ __( '%s activated!', 'woocommerce' ),
+ title
+ )
+ : sprintf(
+ /* translators: %s: extension name. */
+ __( '%s is installed!', 'woocommerce' ),
+ title
+ ),
+ {}
+ );
+ },
+ () => {
+ recordEvent( eventName, {
+ ...trackingBase,
+ success: false,
+ } );
+ }
+ );
+ };
+
+ return (
+ <div className="woocommerce-list__item-inner woocommerce-tax-recommendation-item">
+ <div className="woocommerce-list__item-before">{ logo }</div>
+ <div className="woocommerce-list__item-text">
+ <span className="woocommerce-list__item-title">{ title }</span>
+ <span className="woocommerce-list__item-content">
+ { description }
+ <br />
+ <ExternalLink
+ href={ productUrl }
+ onClick={ handleLearnMoreClick }
+ >
+ { __( 'Learn more', 'woocommerce' ) }
+ </ExternalLink>
+ </span>
+ </div>
+ <div className="woocommerce-list__item-after">
+ { isPluginActive ? (
+ <Button
+ variant="secondary"
+ aria-disabled="true"
+ aria-label={ sprintf(
+ /* translators: %s: extension name. */
+ __( '%s is already active', 'woocommerce' ),
+ title
+ ) }
+ >
+ { __( 'Active', 'woocommerce' ) }
+ </Button>
+ ) : (
+ <Button
+ variant={ isPluginInstalled ? 'primary' : 'secondary' }
+ onClick={ handleClick }
+ isBusy={ pluginsBeingSetup.includes( pluginSlug ) }
+ disabled={ pluginsBeingSetup.length > 0 }
+ >
+ { isPluginInstalled
+ ? __( 'Activate', 'woocommerce' )
+ : __( 'Install', 'woocommerce' ) }
+ </Button>
+ ) }
+ </div>
+ </div>
+ );
+};
+
+const TaxRecommendationsList = ( {
+ children,
+}: {
+ children: React.ReactNode;
+} ) => (
+ <DismissableList
+ className="woocommerce-recommended-tax-extensions"
+ dismissOptionName="woocommerce_settings_tax_recommendations_hidden"
+ >
+ <DismissableListHeading>
+ <Text variant="title.small" as="p" size="20" lineHeight="28px">
+ { __( 'Recommended tax solutions', 'woocommerce' ) }
+ </Text>
+ <Text
+ className="woocommerce-recommended-tax__header-heading"
+ variant="caption"
+ as="p"
+ size="12"
+ lineHeight="16px"
+ >
+ { __(
+ 'Explore tax extensions that can help automate calculations and compliance for your store.',
+ 'woocommerce'
+ ) }
+ </Text>
+ </DismissableListHeading>
+ <ul className="woocommerce-list">
+ { Children.map( children, ( item ) => (
+ <li className="woocommerce-list__item">{ item }</li>
+ ) ) }
+ </ul>
+ <CardFooter>
+ <TrackedLink
+ message={ __(
+ // translators: {{Link}} is a placeholder for a html element.
+ 'Visit {{Link}}the WooCommerce Marketplace{{/Link}} to find more tax solutions.',
+ 'woocommerce'
+ ) }
+ targetUrl={ getAdminLink(
+ 'admin.php?page=wc-admin&tab=extensions&path=/extensions&category=operations'
+ ) }
+ linkType="wc-admin"
+ eventName="settings_tax_recommendation_visit_marketplace_click"
+ />
+ </CardFooter>
+ </DismissableList>
+);
+
+const getPluginSlugForAction = (
+ pluginSlugs: string[],
+ installedPlugins: string[],
+ activePlugins: string[]
+) =>
+ pluginSlugs.find(
+ ( pluginSlug ) =>
+ activePlugins.includes( pluginSlug ) ||
+ installedPlugins.includes( pluginSlug )
+ ) ?? pluginSlugs[ 0 ];
+
+const TaxRecommendations = () => {
+ const [ pluginsBeingSetup, handleInstall, handleActivate ] =
+ useInstallPlugin();
+ const { activePlugins, installedPlugins, countryCode } = useSelect(
+ ( select ) => {
+ const settings = select( settingsStore ).getSettings( 'general' );
+ const { getActivePlugins, getInstalledPlugins } =
+ select( pluginsStore );
+
+ return {
+ activePlugins: getActivePlugins() ?? [],
+ installedPlugins: getInstalledPlugins() ?? [],
+ countryCode: getCountryCode(
+ settings.general?.woocommerce_default_country
+ ),
+ };
+ },
+ []
+ ) ?? {
+ activePlugins: [],
+ installedPlugins: [],
+ countryCode: '',
+ };
+
+ const recommendations: TaxRecommendation[] = [
+ {
+ id: 'woocommerce-tax',
+ title: __( 'WooCommerce Tax', 'woocommerce' ),
+ description: __(
+ 'Free, one-click tool to automate essential sales tax on every WooCommerce order.',
+ 'woocommerce'
+ ),
+ productUrl: 'https://woocommerce.com/products/tax/',
+ pluginSlugs: [ 'woocommerce-services', 'woocommerce-tax' ],
+ logo: (
+ <img
+ className="woocommerce-tax-recommendation-item__logo"
+ src={ taxLogo }
+ alt=""
+ />
+ ),
+ },
+ {
+ id: 'anrok-tax',
+ title: __( 'Anrok', 'woocommerce' ),
+ description: __(
+ 'Advanced tax compliance for growing brands selling within the US and around the globe.',
+ 'woocommerce'
+ ),
+ productUrl: 'https://woocommerce.com/products/anrok-tax/',
+ pluginSlugs: [ 'anrok-tax' ],
+ logo: (
+ <img
+ className="woocommerce-tax-recommendation-item__logo"
+ src={ ANROK_LOGO_URL }
+ alt=""
+ />
+ ),
+ },
+ ];
+ const visibleRecommendations = recommendations.filter(
+ ( recommendation ) =>
+ recommendation.id === 'anrok-tax' ||
+ supportsWooCommerceTax( countryCode )
+ );
+ const visiblePluginSlugs = visibleRecommendations
+ .map( ( recommendation ) => recommendation.pluginSlugs[ 0 ] )
+ .join( ',' );
+ const impressionFired = useRef( false );
+
+ useEffect( () => {
+ if (
+ countryCode &&
+ visibleRecommendations.length > 0 &&
+ ! impressionFired.current
+ ) {
+ recordEvent( 'tax_partner_impression', {
+ context: 'settings',
+ country: countryCode,
+ plugins: visiblePluginSlugs,
+ } );
+ impressionFired.current = true;
+ }
+ }, [ countryCode, visiblePluginSlugs, visibleRecommendations.length ] );
+
+ return (
+ <div className="woocommerce-recommended-tax-extensions-wrapper">
+ <TaxRecommendationsList>
+ { visibleRecommendations.map( ( recommendation ) => {
+ const isPluginActive = recommendation.pluginSlugs.some(
+ ( pluginSlug ) => activePlugins.includes( pluginSlug )
+ );
+ const isPluginInstalled =
+ isPluginActive ||
+ recommendation.pluginSlugs.some( ( pluginSlug ) =>
+ installedPlugins.includes( pluginSlug )
+ );
+
+ return (
+ <TaxRecommendationItem
+ key={ recommendation.id }
+ pluginSlug={ getPluginSlugForAction(
+ recommendation.pluginSlugs,
+ installedPlugins,
+ activePlugins
+ ) }
+ isPluginInstalled={ isPluginInstalled }
+ isPluginActive={ isPluginActive }
+ pluginsBeingSetup={ pluginsBeingSetup }
+ onInstallClick={ handleInstall }
+ onActivateClick={ handleActivate }
+ { ...recommendation }
+ />
+ );
+ } ) }
+ </TaxRecommendationsList>
+ </div>
+ );
+};
+
+export default TaxRecommendations;
diff --git a/plugins/woocommerce/client/admin/client/tax/test/tax-recommendations-wrapper.test.tsx b/plugins/woocommerce/client/admin/client/tax/test/tax-recommendations-wrapper.test.tsx
new file mode 100644
index 00000000000..ea0c4d6fd86
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/tax/test/tax-recommendations-wrapper.test.tsx
@@ -0,0 +1,134 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+import { useSelect } from '@wordpress/data';
+import { useUser } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import { TaxRecommendations } from '../tax-recommendations-wrapper';
+
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ useSelect: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/data', () => ( {
+ ...jest.requireActual( '@woocommerce/data' ),
+ useUser: jest.fn(),
+} ) );
+
+jest.mock( '@wordpress/element', () => ( {
+ ...jest.requireActual( '@wordpress/element' ),
+ Suspense: () => <div>Recommended tax solutions</div>,
+} ) );
+
+describe( 'TaxRecommendations', () => {
+ beforeEach( () => {
+ ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
+ fn( () => ( {
+ getOption: () => 'yes',
+ hasFinishedResolution: () => true,
+ } ) )
+ );
+
+ ( useUser as jest.Mock ).mockReturnValue( {
+ currentUserCan: () => true,
+ } );
+ } );
+
+ it( 'should not render when page is not wc-settings', () => {
+ const { queryByText } = render(
+ <TaxRecommendations
+ page="wc-admin"
+ tab="tax"
+ section={ undefined }
+ />
+ );
+
+ expect(
+ queryByText( 'Recommended tax solutions' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not render when tab is not tax', () => {
+ const { queryByText } = render(
+ <TaxRecommendations
+ page="wc-settings"
+ tab="shipping"
+ section={ undefined }
+ />
+ );
+
+ expect(
+ queryByText( 'Recommended tax solutions' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not render when section is not empty', () => {
+ const { queryByText } = render(
+ <TaxRecommendations
+ page="wc-settings"
+ tab="tax"
+ section="standard"
+ />
+ );
+
+ expect(
+ queryByText( 'Recommended tax solutions' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not render when marketplace suggestions are disabled', () => {
+ ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
+ fn( () => ( {
+ getOption: () => 'no',
+ hasFinishedResolution: () => true,
+ } ) )
+ );
+
+ const { queryByText } = render(
+ <TaxRecommendations
+ page="wc-settings"
+ tab="tax"
+ section={ undefined }
+ />
+ );
+
+ expect(
+ queryByText( 'Recommended tax solutions' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should not render when the current user cannot install plugins', () => {
+ ( useUser as jest.Mock ).mockReturnValue( {
+ currentUserCan: () => false,
+ } );
+
+ const { queryByText } = render(
+ <TaxRecommendations
+ page="wc-settings"
+ tab="tax"
+ section={ undefined }
+ />
+ );
+
+ expect(
+ queryByText( 'Recommended tax solutions' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'should render on the default tax settings section', () => {
+ const { getByText } = render(
+ <TaxRecommendations
+ page="wc-settings"
+ tab="tax"
+ section={ undefined }
+ />
+ );
+
+ expect( getByText( 'Recommended tax solutions' ) ).toBeInTheDocument();
+ } );
+} );
diff --git a/plugins/woocommerce/client/admin/client/tax/test/tax-recommendations.test.tsx b/plugins/woocommerce/client/admin/client/tax/test/tax-recommendations.test.tsx
new file mode 100644
index 00000000000..503940ec45c
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/tax/test/tax-recommendations.test.tsx
@@ -0,0 +1,337 @@
+/**
+ * External dependencies
+ */
+import { act, render, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import TaxRecommendations from '../tax-recommendations';
+
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ useDispatch: jest.fn(),
+ useSelect: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/tracks', () => ( {
+ recordEvent: jest.fn(),
+} ) );
+
+jest.mock( '~/components/tracked-link/tracked-link', () => ( {
+ TrackedLink: ( { message } ) => <div>{ message }</div>,
+} ) );
+
+jest.mock( '../../settings-recommendations/dismissable-list', () => ( {
+ DismissableList: ( { children } ) => children,
+ DismissableListHeading: ( { children } ) => children,
+} ) );
+
+jest.mock( '../../lib/notices', () => ( {
+ createNoticesFromResponse: () => null,
+} ) );
+
+const clickAndFlush = async ( element: Element ) => {
+ await act( async () => {
+ await userEvent.click( element );
+ await Promise.resolve();
+ } );
+};
+
+describe( 'TaxRecommendations', () => {
+ const installPluginsMock = jest.fn().mockResolvedValue( undefined );
+ const activatePluginsMock = jest.fn().mockResolvedValue( undefined );
+ const createSuccessNoticeMock = jest.fn();
+ let installedPlugins: string[] = [];
+ let activePlugins: string[] = [];
+ let countryCode = 'US';
+
+ beforeEach( () => {
+ installPluginsMock.mockClear();
+ activatePluginsMock.mockClear();
+ createSuccessNoticeMock.mockClear();
+ ( recordEvent as jest.Mock ).mockClear();
+ installedPlugins = [];
+ activePlugins = [];
+ countryCode = 'US';
+
+ ( useSelect as jest.Mock ).mockImplementation( ( fn ) =>
+ fn( () => ( {
+ getSettings: () => ( {
+ general: {
+ woocommerce_default_country: countryCode,
+ },
+ } ),
+ getInstalledPlugins: () => installedPlugins,
+ getActivePlugins: () => activePlugins,
+ } ) )
+ );
+
+ ( useDispatch as jest.Mock ).mockImplementation( ( store ) => {
+ if ( store === 'core/notices' ) {
+ return {
+ createSuccessNotice: createSuccessNoticeMock,
+ };
+ }
+
+ return {
+ installPlugins: installPluginsMock,
+ activatePlugins: activatePluginsMock,
+ };
+ } );
+ } );
+
+ it( 'renders WooCommerce Tax and Anrok with install buttons when no related plugins are present', () => {
+ render( <TaxRecommendations /> );
+ expect( screen.getByText( 'WooCommerce Tax' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Anrok' ) ).toBeInTheDocument();
+ expect( screen.getAllByText( 'Install' ) ).toHaveLength( 2 );
+ } );
+
+ it( 'shows Activate when Anrok is installed but inactive', () => {
+ installedPlugins = [ 'anrok-tax' ];
+
+ render( <TaxRecommendations /> );
+
+ expect( screen.getByText( 'WooCommerce Tax' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Anrok' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Activate' ) ).toBeInTheDocument();
+ } );
+
+ it( 'shows a disabled Active button when Anrok is already active', async () => {
+ activePlugins = [ 'anrok-tax' ];
+
+ render( <TaxRecommendations /> );
+
+ const activeButton = screen.getByRole( 'button', {
+ name: 'Anrok is already active',
+ } );
+
+ expect( activeButton ).toHaveTextContent( 'Active' );
+ expect( activeButton ).toHaveAttribute( 'aria-disabled', 'true' );
+
+ await clickAndFlush( activeButton );
+
+ expect( installPluginsMock ).not.toHaveBeenCalled();
+ expect( activatePluginsMock ).not.toHaveBeenCalled();
+ } );
+
+ it( 'shows Active for WooCommerce Tax when the services alias is active', () => {
+ activePlugins = [ 'woocommerce-services' ];
+
+ render( <TaxRecommendations /> );
+
+ expect(
+ screen.getByRole( 'button', {
+ name: 'WooCommerce Tax is already active',
+ } )
+ ).toHaveTextContent( 'Active' );
+ } );
+
+ it( 'renders only Anrok for unsupported countries', () => {
+ countryCode = 'BR';
+
+ render( <TaxRecommendations /> );
+
+ expect(
+ screen.queryByText( 'WooCommerce Tax' )
+ ).not.toBeInTheDocument();
+ expect( screen.getByText( 'Anrok' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Install' ) ).toBeInTheDocument();
+ } );
+
+ it( 'fires tax_partner_impression with both recommendations for supported countries', () => {
+ render( <TaxRecommendations /> );
+
+ expect( recordEvent ).toHaveBeenCalledWith( 'tax_partner_impression', {
+ context: 'settings',
+ country: 'US',
+ plugins: 'woocommerce-services,anrok-tax',
+ } );
+ } );
+
+ it( 'fires tax_partner_impression with only Anrok for unsupported countries', () => {
+ countryCode = 'BR';
+
+ render( <TaxRecommendations /> );
+
+ expect( recordEvent ).toHaveBeenCalledWith( 'tax_partner_impression', {
+ context: 'settings',
+ country: 'BR',
+ plugins: 'anrok-tax',
+ } );
+ } );
+
+ it( 'does not fire tax_partner_impression before the store country is available', () => {
+ countryCode = '';
+
+ render( <TaxRecommendations /> );
+
+ expect( recordEvent ).not.toHaveBeenCalledWith(
+ 'tax_partner_impression',
+ expect.anything()
+ );
+ } );
+
+ it( 'installs WooCommerce Tax using the WooCommerce Services slug', async () => {
+ render( <TaxRecommendations /> );
+
+ const wooCommerceTaxItem = screen
+ .getByText( 'WooCommerce Tax' )
+ .closest( '.woocommerce-list__item' );
+
+ expect( wooCommerceTaxItem ).not.toBeNull();
+
+ await clickAndFlush(
+ within( wooCommerceTaxItem as HTMLElement ).getByRole( 'button', {
+ name: 'Install',
+ } )
+ );
+
+ await waitFor( () => {
+ expect( installPluginsMock ).toHaveBeenCalledWith( [
+ 'woocommerce-services',
+ ] );
+ } );
+
+ expect( recordEvent ).toHaveBeenCalledWith( 'tax_partner_click', {
+ context: 'settings',
+ selected_plugin: 'woocommerce-services',
+ } );
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_tax_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-services',
+ action: 'install',
+ }
+ );
+
+ await waitFor( () => {
+ expect( createSuccessNoticeMock ).toHaveBeenCalledWith(
+ 'WooCommerce Tax is installed!',
+ expect.anything()
+ );
+ } );
+
+ await waitFor( () => {
+ expect( recordEvent ).toHaveBeenCalledWith( 'tax_partner_install', {
+ context: 'settings',
+ selected_plugin: 'woocommerce-services',
+ success: true,
+ } );
+ } );
+ } );
+
+ it( 'activates the installed WooCommerce Tax alias when one is already present', async () => {
+ installedPlugins = [ 'woocommerce-tax' ];
+
+ render( <TaxRecommendations /> );
+
+ const wooCommerceTaxItem = screen
+ .getByText( 'WooCommerce Tax' )
+ .closest( '.woocommerce-list__item' );
+
+ expect( wooCommerceTaxItem ).not.toBeNull();
+
+ await clickAndFlush(
+ within( wooCommerceTaxItem as HTMLElement ).getByRole( 'button', {
+ name: 'Activate',
+ } )
+ );
+
+ await waitFor( () => {
+ expect( activatePluginsMock ).toHaveBeenCalledWith( [
+ 'woocommerce-tax',
+ ] );
+ } );
+
+ expect( recordEvent ).toHaveBeenCalledWith( 'tax_partner_click', {
+ context: 'settings',
+ selected_plugin: 'woocommerce-tax',
+ } );
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'settings_tax_recommendation_setup_click',
+ {
+ plugin: 'woocommerce-tax',
+ action: 'activate',
+ }
+ );
+
+ await waitFor( () => {
+ expect( createSuccessNoticeMock ).toHaveBeenCalledWith(
+ 'WooCommerce Tax activated!',
+ expect.anything()
+ );
+ } );
+
+ await waitFor( () => {
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'tax_partner_activate',
+ {
+ context: 'settings',
+ selected_plugin: 'woocommerce-tax',
+ success: true,
+ }
+ );
+ } );
+ } );
+
+ it( 'records a failed tax_partner_install event when install fails', async () => {
+ installPluginsMock.mockRejectedValueOnce( undefined );
+
+ render( <TaxRecommendations /> );
+
+ const wooCommerceTaxItem = screen
+ .getByText( 'WooCommerce Tax' )
+ .closest( '.woocommerce-list__item' );
+
+ expect( wooCommerceTaxItem ).not.toBeNull();
+
+ await clickAndFlush(
+ within( wooCommerceTaxItem as HTMLElement ).getByRole( 'button', {
+ name: 'Install',
+ } )
+ );
+
+ await waitFor( () => {
+ expect( recordEvent ).toHaveBeenCalledWith( 'tax_partner_install', {
+ context: 'settings',
+ selected_plugin: 'woocommerce-services',
+ success: false,
+ } );
+ } );
+ } );
+
+ it( 'records a failed tax_partner_activate event when activation fails', async () => {
+ activatePluginsMock.mockRejectedValueOnce( undefined );
+ installedPlugins = [ 'anrok-tax' ];
+
+ render( <TaxRecommendations /> );
+
+ const anrokItem = screen
+ .getByText( 'Anrok' )
+ .closest( '.woocommerce-list__item' );
+
+ expect( anrokItem ).not.toBeNull();
+
+ await clickAndFlush(
+ within( anrokItem as HTMLElement ).getByRole( 'button', {
+ name: 'Activate',
+ } )
+ );
+
+ await waitFor( () => {
+ expect( recordEvent ).toHaveBeenCalledWith(
+ 'tax_partner_activate',
+ {
+ context: 'settings',
+ selected_plugin: 'anrok-tax',
+ success: false,
+ }
+ );
+ } );
+ } );
+} );