* Move woocommerce tax flow to subdirectory

* Add partner cards

* Use png for logo files

* Add partner card other actions

* Add partner card styling

* Add in mobile styles

* Interpolate links and html elements

* Redirect to tax configuration if Avalara not supported

* Mark task complete if Alavara is installed

* Record events when task partners are shown or selected

* Add changelog entry

* Update task card flow based on visible partners

* Skip plugin step if all plugins installed

* Move reusable components into subdirectory

* Record available partner options

* Fix type reference

* Wrap callback functions in useCallback to avoid rerenders

* Handle PR feedback

* Add key to partner card container

* Add testing instructions
This commit is contained in:
Joshua T Flowers 2021-11-05 16:32:02 -04:00 committed by GitHub
parent 675cf379c1
commit abc47adc95
26 changed files with 1206 additions and 276 deletions

View File

@ -1,5 +1,46 @@
# Testing instructions # Testing instructions
## 2.9.0
### Add Avalara to tax task #7874
**Avalara supported, WooCommerce Tax supported**
1. Select an Avalara and WC Tax supported country (e.g., `US`) for your store's country
2. Visit the tax task.
3. Make sure your shown the "WooCommerce Tax" and "Avalara" options in the task list
**Avalara supported, WooCommerce Tax not supported**
1. Install the TaxJar plugin so that WC Tax is not supported and set your country to an Avalara supported country
2. Visit the task tax.
3. Make sure you are shown only the partner card of Avalara
**Avalara not supported, WooCommerce Tax not supported**
1. Set your store country to one not supported by Avalara or WC Tax (e.g., New Caledonia)
2. Visit the task tax.
3. Make sure you are immediately shown the manual set up flow with the "Configure" button
**Partner actions**
1. Visit the task tax with an Avalara supported country
2. Click Avalara and check that a new tab opens with the WCCOM plugin page
3. Back on the task tax, click on WooCommerce Tax
4. Make sure you are dropped into the old configuration flow (which should be identical to the old flow)
**Events**
1. Enter `localStorage.setItem( 'debug', 'wc-admin:*' );` in your browser's console
2. Set your store's country to an Avalara supported country
3. Note the `wcadmin_tasklist_tax_view_options` event occurs
4. Click on each of the partner action buttons
5. Make sure that `wcadmin_tasklist_tax_select_option` is recorded with the respective `selected_option` partner key.
**Completion**
1. Create a fresh site without ever having set any taxes
2. Note the task is incomplete
3. Install the WC Avalara plugin
4. Check that the task is now marked complete
## 2.8.0 ## 2.8.0
### Store Profiler and Product task - include Subscriptions #7734 ### Store Profiler and Product task - include Subscriptions #7734

View File

@ -0,0 +1,4 @@
Significance: minor
Type: Add
Add Avalara to tax task #7874

View File

@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { getAdminLink } from '@woocommerce/wc-admin-settings';
import interpolateComponents from 'interpolate-components';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { PartnerCard } from '../components/partner-card';
import logo from './logo.png';
export const Card = ( { isPending, tasksStatus } ) => {
const { avalaraActivated } = tasksStatus;
return (
<PartnerCard
name={ __( 'Avalara', 'woocommerce-admin' ) }
isPending={ isPending }
logo={ logo }
description={ __(
'Powerful all-in-one tax tool',
'woocommerce-admin'
) }
benefits={ [
__( 'Real-time sales tax calculation', 'woocommerce-admin' ),
interpolateComponents( {
mixedString: __(
'{{strong}}Multi{{/strong}}-economic nexus compliance',
'woocommerce-admin'
),
components: {
strong: <strong />,
},
} ),
__(
'Cross-border and multi-channel compliance',
'woocommerce-admin'
),
__( 'Automate filing & remittance', 'woocommerce-admin' ),
__(
'Return-ready, jurisdiction-level reporting.',
'woocommerce-admin'
),
] }
terms={ __(
'30-day free trial. No credit card needed.',
'woocommerce-admin'
) }
actionText={
avalaraActivated
? __( 'Continue setup', 'woocommerce-admin' )
: __( 'Enable & set up', 'woocommerce-admin' )
}
onClick={ () => {
recordEvent( 'tasklist_tax_select_option', {
selected_option: 'avalara',
} );
if ( avalaraActivated ) {
window.location.href = getAdminLink(
'/admin.php?page=wc-settings&tab=tax&section=avatax'
);
return;
}
window.open(
new URL(
'https://woocommerce.com/products/woocommerce-avatax/'
),
'_blank'
);
} }
/>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,17 @@
export const Bullet: React.FC = () => {
return (
<svg
width="13"
height="10"
viewBox="0 0 13 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.1883 1.1814L4.7091 8.66062L1.48438 5.4359"
stroke="#4AB866"
strokeWidth="1.5"
/>
</svg>
);
};

View File

@ -0,0 +1,47 @@
.woocommerce-tax-partner-card {
border: 1px solid $gray-300;
border-radius: 3px;
margin: 0 $gap-smaller $gap $gap-smaller;
padding: 20px;
display: flex;
flex-direction: column;
a {
text-decoration: none;
}
}
.woocommerce-tax-partner-card__logo {
margin-bottom: $gap-smaller;
img {
max-height: 35px;
}
}
.woocommerce-tax-partner-card__description {
color: $gray-700;
}
.woocommerce-tax-partner-card__benefits {
color: $gray-900;
list-style: none;
li {
margin-bottom: $gap-smaller;
display: flex;
svg {
margin-right: 10px;
}
}
}
.woocommerce-tax-partner-card__action {
margin-top: auto;
}
.woocommerce-tax-partner-card__terms {
color: $gray-600;
font-size: 9px;
margin-bottom: $gap-smaller;
}

View File

@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import { Bullet } from './bullet';
import './partner-card.scss';
export const PartnerCard: React.FC< {
name: string;
logo: string;
description: string;
benefits: string[];
terms: string;
actionText: string;
onClick: () => void;
isPending: boolean;
} > = ( {
name,
logo,
description,
benefits,
terms,
actionText,
onClick,
isPending,
} ) => {
return (
<div className="woocommerce-tax-partner-card">
<div className="woocommerce-tax-partner-card__logo">
<img src={ logo } alt={ name } />
</div>
<div className="woocommerce-tax-partner-card__description">
{ description }
</div>
<ul className="woocommerce-tax-partner-card__benefits">
{ benefits.map( ( benefit, i ) => {
return (
<li
className="woocommerce-tax-partner-card__benefit"
key={ i }
>
<span className="woocommerce-tax-partner-card__benefit-bullet">
<Bullet />
</span>
<span className="woocommerce-tax-partner-card__benefit-text">
{ benefit }
</span>
</li>
);
} ) }
</ul>
<div className="woocommerce-tax-partner-card__action">
<div className="woocommerce-tax-partner-card__terms">
{ terms }
</div>
<Button
isSecondary
onClick={ onClick }
isBusy={ isPending }
disabled={ isPending }
>
{ actionText }
</Button>
</div>
</div>
);
};

View File

@ -0,0 +1,69 @@
.woocommerce-tax-partners__partners {
display: grid;
grid-template-columns: 1fr 1fr;
&.woocommerce-tax-partners__partners-count-1 {
grid-template-columns: 1fr;
}
@include breakpoint( '<782px' ) {
grid-template-columns: 1fr;
}
}
.woocommerce-tax-partners__partners-count-1 .woocommerce-tax-partners__partners {
grid-template-columns: 1fr;
justify-items: center;
}
.woocommerce-tax-partners {
.components-card__body.is-size-medium {
padding: $gap-larger;
}
.components-card__header {
line-height: 28px;
font-size: 20px;
}
}
.woocommerce-tax-partners__other-actions {
text-align: center;
list-style: none;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
@include breakpoint( '<782px' ) {
flex-direction: column;
}
li {
margin-top: $gap;
margin-right: $gap-smallest;
button.is-tertiary {
padding: 0;
height: auto;
}
&::after {
content: '';
color: #bbb;
margin-left: 4px;
@include breakpoint( '<782px' ) {
content: '';
}
}
&:last-child {
margin-right: 0;
&::after {
content: '';
}
}
}
}

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Card, CardBody, CardHeader } from '@wordpress/components';
import { Children } from '@wordpress/element';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import { TaxChildProps } from '../utils';
import './partners.scss';
export const Partners: React.FC< TaxChildProps > = ( {
children,
isPending,
onManual,
onDisable,
} ) => {
const classes = classnames(
'woocommerce-task-card',
'woocommerce-tax-partners',
`woocommerce-tax-partners__partners-count-${ Children.count(
children
) }`
);
return (
<Card className={ classes }>
<CardHeader>
{ __( 'Choose a tax partner', 'woocommerce-admin' ) }
</CardHeader>
<CardBody>
<div className="woocommerce-tax-partners__partners">
{ children }
</div>
<ul className="woocommerce-tax-partners__other-actions">
<li>
<Button
isTertiary
disabled={ isPending }
isBusy={ isPending }
onClick={ () => {
onManual();
} }
>
{ __(
'Set up taxes manually',
'woocommerce-admin'
) }
</Button>
</li>
<li>
<Button
isTertiary
disabled={ isPending }
isBusy={ isPending }
onClick={ () => {
onDisable();
} }
>
{ __(
"I don't charge sales tax",
'woocommerce-admin'
) }
</Button>
</li>
</ul>
</CardBody>
</Card>
);
};

View File

@ -10,25 +10,26 @@ import { useSelect, useDispatch } from '@wordpress/data';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { ConfigurationStepProps } from '.'; import { getCountryCode } from '~/dashboard/utils';
import { getCountryCode } from '../../../../dashboard/utils'; import { hasCompleteAddress, SettingsSelector, TaxChildProps } from '../utils';
import { hasCompleteAddress, SettingsSelector } from '../utils'; import { default as StoreLocationForm } from '~/tasks/fills/steps/location';
import { default as StoreLocationForm } from '../../steps/location';
export const StoreLocation: React.FC< ConfigurationStepProps > = ( { export const StoreLocation: React.FC< {
isResolving, nextStep: () => void;
nextStep, } > = ( { nextStep } ) => {
} ) => {
const { updateAndPersistSettingsForGroup } = useDispatch( const { updateAndPersistSettingsForGroup } = useDispatch(
SETTINGS_STORE_NAME SETTINGS_STORE_NAME
); );
const { generalSettings } = useSelect( ( select ) => { const { generalSettings, isResolving } = useSelect( ( select ) => {
const { getSettings } = select( const { getSettings, hasFinishedResolution } = select(
SETTINGS_STORE_NAME SETTINGS_STORE_NAME
) as SettingsSelector; ) as SettingsSelector;
return { return {
generalSettings: getSettings( 'general' )?.general, generalSettings: getSettings( 'general' )?.general,
isResolving: ! hasFinishedResolution( 'getSettings', [
'general',
] ),
}; };
} ); } );

View File

@ -1,179 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { difference, filter } from 'lodash';
import { useEffect, useState } from '@wordpress/element';
import { Stepper } from '@woocommerce/components';
import {
OPTIONS_STORE_NAME,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { AUTOMATION_PLUGINS, SettingsSelector } from '../utils';
import { Connect } from './connect';
import { ManualConfiguration } from './manual-configuration';
import { Plugins } from './plugins';
import { StoreLocation } from './store-location';
export type ConfigurationStepperProps = {
isPending: boolean;
onDisable: () => void;
onAutomate: () => void;
onManual: () => void;
supportsAutomatedTaxes: boolean;
};
export type ConfigurationStepProps = {
isPending: boolean;
isResolving: boolean;
nextStep: () => void;
onDisable: () => void;
onAutomate: () => void;
onManual: () => void;
pluginsToActivate: string[];
};
export const ConfigurationStepper: React.FC< ConfigurationStepperProps > = ( {
isPending,
onDisable,
onAutomate,
onManual,
supportsAutomatedTaxes,
} ) => {
const [ pluginsToActivate, setPluginsToActivate ] = useState( [] );
const {
activePlugins,
isJetpackConnected,
isResolving,
tosAccepted,
} = useSelect( ( select ) => {
const { getSettings } = select(
SETTINGS_STORE_NAME
) as SettingsSelector;
const { getOption, hasFinishedResolution } = select(
OPTIONS_STORE_NAME
) as SettingsSelector;
const { getActivePlugins } = select( PLUGINS_STORE_NAME );
return {
activePlugins: getActivePlugins(),
generalSettings: getSettings( 'general' )?.general,
isJetpackConnected: select(
PLUGINS_STORE_NAME
).isJetpackConnected(),
isResolving:
! hasFinishedResolution( 'getOption', [
'woocommerce_setup_jetpack_opted_in',
] ) ||
! hasFinishedResolution( 'getOption', [
'wc_connect_options',
] ),
tosAccepted:
getOption( 'wc_connect_options' )?.tos_accepted ||
getOption( 'woocommerce_setup_jetpack_opted_in' ) === '1',
};
} );
const [ stepIndex, setStepIndex ] = useState( 0 );
useEffect( () => {
const remainingPlugins = difference(
AUTOMATION_PLUGINS,
activePlugins
);
if ( remainingPlugins.length <= pluginsToActivate.length ) {
return;
}
setPluginsToActivate( remainingPlugins );
}, [ activePlugins ] );
const nextStep = () => {
setStepIndex( stepIndex + 1 );
};
const stepProps = {
isPending,
isResolving,
onAutomate,
onDisable,
nextStep,
onManual,
pluginsToActivate,
};
const getVisibleSteps = () => {
const allSteps = [
{
key: 'store_location',
label: __( 'Set store location', 'woocommerce-admin' ),
description: __(
'The address from which your business operates',
'woocommerce-admin'
),
content: <StoreLocation { ...stepProps } />,
visible: true,
},
{
key: 'plugins',
label: pluginsToActivate.includes( 'woocommerce-services' )
? __(
'Install Jetpack and WooCommerce Tax',
'woocommerce-admin'
)
: __( 'Install Jetpack', 'woocommerce-admin' ),
description: __(
'Jetpack and WooCommerce Tax allow you to automate sales tax calculations',
'woocommerce-admin'
),
content: <Plugins { ...stepProps } />,
visible:
! isResolving &&
( pluginsToActivate.length || ! tosAccepted ) &&
supportsAutomatedTaxes,
},
{
key: 'connect',
label: __( 'Connect your store', 'woocommerce-admin' ),
description: __(
'Connect your store to WordPress.com to enable automated sales tax calculations',
'woocommerce-admin'
),
content: <Connect { ...stepProps } />,
visible:
! isResolving &&
! isJetpackConnected &&
supportsAutomatedTaxes,
},
{
key: 'manual_configuration',
label: __( 'Configure tax rates', 'woocommerce-admin' ),
description: __(
'Head over to the tax rate settings screen to configure your tax rates',
'woocommerce-admin'
),
content: <ManualConfiguration { ...stepProps } />,
visible: ! supportsAutomatedTaxes,
},
];
return filter( allSteps, ( step ) => step.visible );
};
const steps = getVisibleSteps();
const step = steps[ stepIndex ];
return (
<Stepper
isPending={ isResolving }
isVertical={ true }
currentStep={ step.key }
steps={ steps }
/>
);
};

View File

@ -2,36 +2,48 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Card, CardBody } from '@wordpress/components'; import { Card, CardBody, Spinner } from '@wordpress/components';
import { difference } from 'lodash';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { Spinner } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/wc-admin-settings'; import { getAdminLink } from '@woocommerce/wc-admin-settings';
import { import {
ONBOARDING_STORE_NAME, ONBOARDING_STORE_NAME,
OPTIONS_STORE_NAME, OPTIONS_STORE_NAME,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME, SETTINGS_STORE_NAME,
} from '@woocommerce/data'; } from '@woocommerce/data';
import { queueRecordEvent } from '@woocommerce/tracks'; import { queueRecordEvent, recordEvent } from '@woocommerce/tracks';
import { registerPlugin } from '@wordpress/plugins'; import { registerPlugin } from '@wordpress/plugins';
import { useEffect, useState } from '@wordpress/element'; import { updateQueryString } from '@woocommerce/navigation';
import {
useCallback,
useEffect,
useState,
createElement,
} from '@wordpress/element';
import { WooOnboardingTask } from '@woocommerce/onboarding'; import { WooOnboardingTask } from '@woocommerce/onboarding';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { import {
AUTOMATION_PLUGINS,
hasCompleteAddress,
redirectToTaxSettings, redirectToTaxSettings,
SettingsSelector, SettingsSelector,
supportsAvalara,
} from './utils'; } from './utils';
import { AutomatedTaxes } from './automated-taxes'; import { Card as AvalaraCard } from './avalara/card';
import { ConfigurationStepper } from './configuration-stepper'; import { Card as WooCommerceTaxCard } from './woocommerce-tax/card';
import { createNoticesFromResponse } from '../../../lib/notices'; import { createNoticesFromResponse } from '../../../lib/notices';
import { getCountryCode } from '../../../dashboard/utils'; import { getCountryCode } from '~/dashboard/utils';
import './tax.scss'; import { ManualConfiguration } from './manual-configuration';
import { Partners } from './components/partners';
import { WooCommerceTax } from './woocommerce-tax';
const TaskCard = ( { children } ) => {
return (
<Card className="woocommerce-task-card">
<CardBody>{ children }</CardBody>
</Card>
);
};
const Tax = ( { onComplete, query } ) => { const Tax = ( { onComplete, query } ) => {
const [ isPending, setIsPending ] = useState( false ); const [ isPending, setIsPending ] = useState( false );
@ -42,64 +54,28 @@ const Tax = ( { onComplete, query } ) => {
); );
const { const {
generalSettings, generalSettings,
isJetpackConnected,
isResolving, isResolving,
pluginsToActivate,
tasksStatus, tasksStatus,
taxSettings, taxSettings,
} = useSelect( ( select ) => { } = useSelect( ( select ) => {
const { getSettings } = select( const { getSettings, hasFinishedResolution } = select(
SETTINGS_STORE_NAME SETTINGS_STORE_NAME
) as SettingsSelector; ) as SettingsSelector;
const { getActivePlugins } = select( PLUGINS_STORE_NAME );
const activePlugins = getActivePlugins();
return { return {
generalSettings: getSettings( 'general' ).general, generalSettings: getSettings( 'general' ).general,
isJetpackConnected: select(
PLUGINS_STORE_NAME
).isJetpackConnected(),
isResolving: isResolving:
! select( PLUGINS_STORE_NAME ).hasFinishedResolution( ! hasFinishedResolution( 'getSettings', [ 'general' ] ) ||
'isJetpackConnected'
) ||
! select(
SETTINGS_STORE_NAME
).hasFinishedResolution( 'getSettings', [ 'general' ] ) ||
! select( ONBOARDING_STORE_NAME ).hasFinishedResolution( ! select( ONBOARDING_STORE_NAME ).hasFinishedResolution(
'getTasksStatus' 'getTasksStatus'
), ),
pluginsToActivate: difference( AUTOMATION_PLUGINS, activePlugins ),
// @Todo this should be removed as soon as https://github.com/woocommerce/woocommerce-admin/pull/7841 is merged. // @Todo this should be removed as soon as https://github.com/woocommerce/woocommerce-admin/pull/7841 is merged.
tasksStatus: select( ONBOARDING_STORE_NAME ).getTasksStatus(), tasksStatus: select( ONBOARDING_STORE_NAME ).getTasksStatus(),
taxSettings: getSettings( 'tax' ).tax || {}, taxSettings: getSettings( 'tax' ).tax || {},
}; };
} ); } );
const supportsAutomatedTaxes = () => { const onManual = useCallback( async () => {
const {
automatedTaxSupportedCountries = [],
taxJarActivated,
} = tasksStatus;
return (
! taxJarActivated && // WCS integration doesn't work with the official TaxJar plugin.
automatedTaxSupportedCountries.includes(
getCountryCode( generalSettings?.woocommerce_default_country )
)
);
};
const canAutomateTaxes = () => {
return (
hasCompleteAddress( generalSettings ) &&
! pluginsToActivate.length &&
isJetpackConnected &&
supportsAutomatedTaxes()
);
};
const onManual = async () => {
setIsPending( true ); setIsPending( true );
if ( generalSettings.woocommerce_calc_taxes !== 'yes' ) { if ( generalSettings.woocommerce_calc_taxes !== 'yes' ) {
updateAndPersistSettingsForGroup( 'tax', { updateAndPersistSettingsForGroup( 'tax', {
@ -122,9 +98,9 @@ const Tax = ( { onComplete, query } ) => {
} else { } else {
redirectToTaxSettings(); redirectToTaxSettings();
} }
}; }, [] );
const onAutomate = () => { const onAutomate = useCallback( () => {
setIsPending( true ); setIsPending( true );
updateAndPersistSettingsForGroup( 'tax', { updateAndPersistSettingsForGroup( 'tax', {
tax: { tax: {
@ -147,9 +123,9 @@ const Tax = ( { onComplete, query } ) => {
) )
); );
onComplete(); onComplete();
}; }, [] );
const onDisable = () => { const onDisable = useCallback( () => {
setIsPending( true ); setIsPending( true );
queueRecordEvent( 'tasklist_tax_connect_store', { queueRecordEvent( 'tasklist_tax_connect_store', {
connect: false, connect: false,
@ -162,40 +138,121 @@ const Tax = ( { onComplete, query } ) => {
} ).then( () => { } ).then( () => {
window.location.href = getAdminLink( 'admin.php?page=wc-admin' ); window.location.href = getAdminLink( 'admin.php?page=wc-admin' );
} ); } );
}, [] );
const getVisiblePartners = () => {
const countryCode = getCountryCode(
generalSettings?.woocommerce_default_country
);
const {
automatedTaxSupportedCountries = [],
taxJarActivated,
} = tasksStatus;
const partners = [
{
id: 'woocommerce-tax',
card: WooCommerceTaxCard,
component: WooCommerceTax,
isVisible:
! taxJarActivated && // WCS integration doesn't work with the official TaxJar plugin.
automatedTaxSupportedCountries.includes(
getCountryCode(
generalSettings?.woocommerce_default_country
)
),
},
{
id: 'avalara',
card: AvalaraCard,
component: null,
isVisible: supportsAvalara( countryCode ),
},
];
return partners.filter( ( partner ) => partner.isVisible );
}; };
const partners = getVisiblePartners();
useEffect( () => { useEffect( () => {
const { auto } = query; const { auto } = query;
if ( auto === 'true' ) { if ( auto === 'true' ) {
onAutomate(); onAutomate();
return;
} }
if ( query.partner ) {
return;
}
recordEvent( 'tasklist_tax_view_options', {
options: partners.map( ( partner ) => partner.id ),
} );
}, [] ); }, [] );
if ( isResolving ) { const getCurrentPartner = () => {
return <Spinner />; if ( ! query.partner ) {
} return null;
}
return (
partners.find( ( partner ) => partner.id === query.partner ) || null
);
};
useEffect( () => {
if ( partners.length > 1 || query.partner ) {
return;
}
if ( partners.length === 1 && partners[ 0 ].component ) {
updateQueryString( {
partner: partners[ 0 ].id,
} );
}
}, [ partners ] );
const childProps = { const childProps = {
isPending, isPending,
onAutomate, onAutomate,
onManual, onManual,
onDisable, onDisable,
supportsAutomatedTaxes: supportsAutomatedTaxes(), tasksStatus,
}; };
if ( isResolving ) {
return <Spinner />;
}
const currentPartner = getCurrentPartner();
if ( ! partners.length ) {
return (
<TaskCard>
<ManualConfiguration { ...childProps } />
</TaskCard>
);
}
if ( currentPartner ) {
return (
<TaskCard>
{ createElement( currentPartner.component, childProps ) }
</TaskCard>
);
}
return ( return (
<div className="woocommerce-task-tax"> <Partners>
<Card className="woocommerce-task-card"> { partners.map( ( partner ) =>
<CardBody> createElement( partner.card, {
{ canAutomateTaxes() ? ( key: partner.id,
<AutomatedTaxes { ...childProps } /> ...childProps,
) : ( } )
<ConfigurationStepper { ...childProps } /> ) }
) } </Partners>
</CardBody>
</Card>
</div>
); );
}; };

View File

@ -12,10 +12,9 @@ import { useSelect } from '@wordpress/data';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { ConfigurationStepProps } from '.'; import { SettingsSelector, TaxChildProps } from '../utils';
import { SettingsSelector } from '../utils';
export const ManualConfiguration: React.FC< ConfigurationStepProps > = ( { export const Configure: React.FC< TaxChildProps > = ( {
isPending, isPending,
onManual, onManual,
} ) => { } ) => {

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { filter } from 'lodash';
import { useState } from '@wordpress/element';
import { Stepper } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { Configure } from './configure';
import { StoreLocation } from '../components/store-location';
export type ManualConfigurationProps = {
isPending: boolean;
onDisable: () => void;
onAutomate: () => void;
onManual: () => void;
};
export const ManualConfiguration: React.FC< ManualConfigurationProps > = ( {
isPending,
onDisable,
onAutomate,
onManual,
} ) => {
const [ stepIndex, setStepIndex ] = useState( 0 );
const nextStep = () => {
setStepIndex( stepIndex + 1 );
};
const stepProps = {
isPending,
onAutomate,
onDisable,
nextStep,
onManual,
};
const steps = [
{
key: 'store_location',
label: __( 'Set store location', 'woocommerce-admin' ),
description: __(
'The address from which your business operates',
'woocommerce-admin'
),
content: <StoreLocation { ...stepProps } />,
},
{
key: 'manual_configuration',
label: __( 'Configure tax rates', 'woocommerce-admin' ),
description: __(
'Head over to the tax rate settings screen to configure your tax rates',
'woocommerce-admin'
),
content: <Configure { ...stepProps } />,
},
];
const step = steps[ stepIndex ];
return (
<Stepper isVertical={ true } currentStep={ step.key } steps={ steps } />
);
};

View File

@ -13,8 +13,15 @@ export const AUTOMATION_PLUGINS = [ 'jetpack', 'woocommerce-services' ];
* Check if a store has a complete address given general settings. * Check if a store has a complete address given general settings.
* *
* @param {Object} generalSettings General settings. * @param {Object} generalSettings General settings.
* @param {Object} generalSettings.woocommerce_store_address Store address.
* @param {Object} generalSettings.woocommerce_default_country Store default country.
* @param {Object} generalSettings.woocommerce_store_postcode Store postal code.
*/ */
export const hasCompleteAddress = ( generalSettings ): boolean => { export const hasCompleteAddress = ( generalSettings: {
woocommerce_store_address?: string;
woocommerce_default_country?: string;
woocommerce_store_postcode?: string;
} ): boolean => {
const { const {
woocommerce_store_address: storeAddress, woocommerce_store_address: storeAddress,
woocommerce_default_country: defaultCountry, woocommerce_default_country: defaultCountry,
@ -47,3 +54,246 @@ export type SettingsSelector = WPDataSelectors & {
}; };
}; };
}; };
/**
* Types for child tax components.
*/
export type TaxChildProps = {
isPending: boolean;
onAutomate: () => void;
onManual: () => void;
onDisable: () => void;
};
/**
* Check if a given country is supported by Avalara.
*
* @param {string} countryCode Country code.
* @return {boolean} If the country is supported.
*/
export const supportsAvalara = ( countryCode: string ): boolean => {
const countries = [
'AF',
'AL',
'DZ',
'AD',
'AO',
'AI',
'AG',
'AR',
'AM',
'AW',
'AU',
'AT',
'AZ',
'BS',
'BH',
'BD',
'BB',
'BY',
'BE',
'BZ',
'BJ',
'BM',
'BO',
'BA',
'BW',
'BR',
'BN',
'BG',
'BF',
'BI',
'KH',
'CM',
'CA',
'IC',
'CV',
'KY',
'CF',
'TD',
'CL',
'CN',
'CC',
'CO',
'KM',
'CD',
'CK',
'CR',
'CI',
'HR',
'CU',
'CW',
'CY',
'CZ',
'DK',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'GQ',
'ER',
'EE',
'ET',
'FK',
'FO',
'FJ',
'FI',
'FR',
'PF',
'TF',
'GA',
'GM',
'GE',
'DE',
'GH',
'GI',
'GR',
'GL',
'GD',
'GP',
'GT',
'GG',
'GN',
'GW',
'GY',
'HT',
'HN',
'HK',
'HU',
'IS',
'IN',
'ID',
'IR',
'IQ',
'IE',
'IL',
'IT',
'JM',
'JP',
'JE',
'JO',
'KZ',
'KE',
'KI',
'KP',
'KV',
'KW',
'KG',
'LA',
'LV',
'LB',
'LS',
'LR',
'LY',
'LI',
'LT',
'LU',
'MO',
'MK',
'MG',
'MW',
'MY',
'MV',
'ML',
'MT',
'MQ',
'MR',
'MU',
'MX',
'MD',
'MC',
'MN',
'ME',
'MS',
'MA',
'MZ',
'MM',
'NA',
'NR',
'NP',
'NL',
'NZ',
'NI',
'NE',
'NG',
'NU',
'NF',
'NO',
'OM',
'PK',
'PS',
'PA',
'PG',
'PY',
'PE',
'PH',
'PL',
'PT',
'QA',
'KR',
'RE',
'RO',
'RU',
'RW',
'SH',
'KN',
'LC',
'MF',
'VC',
'WS',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SG',
'SX',
'SK',
'SI',
'SB',
'SO',
'ZA',
'SD',
'ES',
'LK',
'SD',
'SR',
'SZ',
'SE',
'CH',
'SY',
'TW',
'TJ',
'TZ',
'TH',
'TL',
'TG',
'TO',
'TT',
'TN',
'TR',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'GB',
'US',
'UY',
'UZ',
'VU',
'VE',
'VN',
'VG',
'YE',
'ZM',
'ZW',
];
return countries.includes( countryCode );
};

View File

@ -7,7 +7,12 @@ import interpolateComponents from 'interpolate-components';
import { H } from '@woocommerce/components'; import { H } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
export const AutomatedTaxes = ( { /**
* Internal dependencies
*/
import { SetupStepProps } from './setup';
export const AutomatedTaxes: React.FC< SetupStepProps > = ( {
isPending, isPending,
onAutomate, onAutomate,
onManual, onManual,

View File

@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import interpolateComponents from 'interpolate-components';
import { Link } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
import { updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { PartnerCard } from '../components/partner-card';
import logo from './logo.png';
import { TaxChildProps } from '../utils';
export const Card: React.FC< TaxChildProps > = ( { isPending } ) => {
return (
<PartnerCard
name={ __( 'WooCommerce Tax', 'woocommerce-admin' ) }
isPending={ isPending }
logo={ logo }
description={ __( 'Best for new stores', 'woocommerce-admin' ) }
benefits={ [
__( 'Real-time sales tax calculation', 'woocommerce-admin' ),
interpolateComponents( {
mixedString: __(
'{{strong}}Single{{/strong}} economic nexus compliance',
'woocommerce-admin'
),
components: {
strong: <strong />,
},
} ),
interpolateComponents( {
mixedString: __(
'Powered by {{link}}Jetpack{{/link}}',
'woocommerce-admin'
),
components: {
link: (
<Link
type="external"
href="https://woocommerce.com/products/jetpack/?utm_medium=product"
target="_blank"
/>
),
},
} ),
// eslint-disable-next-line @wordpress/i18n-translator-comments
__( '100% free', 'woocommerce-admin' ),
] }
terms={ interpolateComponents( {
mixedString: __(
'By installing WooCommerce Tax and Jetpack you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce-admin'
),
components: {
link: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
/>
),
},
} ) }
actionText={ __( 'Continue setup', 'woocommerce-admin' ) }
onClick={ () => {
recordEvent( 'tasklist_tax_select_option', {
selected_option: 'woocommerce-tax',
} );
updateQueryString( {
partner: 'woocommerce-tax',
} );
} }
/>
);
};

View File

@ -7,10 +7,10 @@ import { recordEvent, queueRecordEvent } from '@woocommerce/tracks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { default as ConnectForm } from '../../../../dashboard/components/connect'; import { default as ConnectForm } from '~/dashboard/components/connect';
import { ConfigurationStepProps } from '.'; import { SetupStepProps } from './setup';
export const Connect: React.FC< ConfigurationStepProps > = ( { export const Connect: React.FC< SetupStepProps > = ( {
onDisable, onDisable,
onManual, onManual,
} ) => { } ) => {

View File

@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { difference } from 'lodash';
import { useSelect } from '@wordpress/data';
import { Spinner } from '@woocommerce/components';
import { PLUGINS_STORE_NAME, SETTINGS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import {
AUTOMATION_PLUGINS,
hasCompleteAddress,
SettingsSelector,
TaxChildProps,
} from '../utils';
import { AutomatedTaxes } from './automated-taxes';
import { Setup } from './setup';
export const WooCommerceTax: React.FC< TaxChildProps > = ( {
isPending,
onAutomate,
onManual,
onDisable,
} ) => {
const {
generalSettings,
isJetpackConnected,
isResolving,
pluginsToActivate,
} = useSelect( ( select ) => {
const { getSettings } = select(
SETTINGS_STORE_NAME
) as SettingsSelector;
const { getActivePlugins, hasFinishedResolution } = select(
PLUGINS_STORE_NAME
);
const activePlugins = getActivePlugins();
return {
generalSettings: getSettings( 'general' ).general,
isJetpackConnected: select(
PLUGINS_STORE_NAME
).isJetpackConnected(),
isResolving:
! hasFinishedResolution( 'isJetpackConnected' ) ||
! select(
SETTINGS_STORE_NAME
).hasFinishedResolution( 'getSettings', [ 'general' ] ) ||
! hasFinishedResolution( 'getActivePlugins' ),
pluginsToActivate: difference( AUTOMATION_PLUGINS, activePlugins ),
};
} );
const canAutomateTaxes = () => {
return (
hasCompleteAddress( generalSettings ) &&
! pluginsToActivate.length &&
isJetpackConnected
);
};
if ( isResolving ) {
return <Spinner />;
}
const childProps = {
isPending,
onAutomate,
onManual,
onDisable,
};
if ( canAutomateTaxes() ) {
return <AutomatedTaxes { ...childProps } />;
}
return <Setup { ...childProps } />;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -8,33 +8,49 @@ import { OPTIONS_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data';
import { recordEvent, queueRecordEvent } from '@woocommerce/tracks'; import { recordEvent, queueRecordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental'; import { Text } from '@woocommerce/experimental';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { createNoticesFromResponse } from '../../../../lib/notices'; import { createNoticesFromResponse } from '~/lib/notices';
import { ConfigurationStepProps } from '.'; import { SetupStepProps } from './setup';
import { SettingsSelector } from '../utils'; import { SettingsSelector } from '../utils';
export const Plugins: React.FC< ConfigurationStepProps > = ( { export const Plugins: React.FC< SetupStepProps > = ( {
nextStep, nextStep,
onDisable, onDisable,
onManual, onManual,
pluginsToActivate, pluginsToActivate,
} ) => { } ) => {
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { tosAccepted } = useSelect( ( select ) => { const { isResolving, tosAccepted } = useSelect( ( select ) => {
const { getOption } = select( OPTIONS_STORE_NAME ) as SettingsSelector; const { getOption, hasFinishedResolution } = select(
const { getActivePlugins } = select( PLUGINS_STORE_NAME ); OPTIONS_STORE_NAME
) as SettingsSelector;
return { return {
activePlugins: getActivePlugins(), isResolving:
! hasFinishedResolution( 'getOption', [
'woocommerce_setup_jetpack_opted_in',
] ) ||
! hasFinishedResolution( 'getOption', [
'wc_connect_options',
] ),
tosAccepted: tosAccepted:
getOption( 'wc_connect_options' )?.tos_accepted || getOption( 'wc_connect_options' )?.tos_accepted ||
getOption( 'woocommerce_setup_jetpack_opted_in' ) === '1', getOption( 'woocommerce_setup_jetpack_opted_in' ) === '1',
}; };
} ); } );
useEffect( () => {
if ( ! tosAccepted || pluginsToActivate.length ) {
return;
}
nextStep();
}, [ isResolving ] );
const agreementText = pluginsToActivate.includes( 'woocommerce-services' ) const agreementText = pluginsToActivate.includes( 'woocommerce-services' )
? __( ? __(
'By installing Jetpack and WooCommerce Tax you agree to the {{link}}Terms of Service{{/link}}.', 'By installing Jetpack and WooCommerce Tax you agree to the {{link}}Terms of Service{{/link}}.',
@ -45,6 +61,10 @@ export const Plugins: React.FC< ConfigurationStepProps > = ( {
'woocommerce-admin' 'woocommerce-admin'
); );
if ( isResolving ) {
return null;
}
return ( return (
<> <>
<PluginInstaller <PluginInstaller

View File

@ -0,0 +1,141 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { difference } from 'lodash';
import { useEffect, useState } from '@wordpress/element';
import { Stepper } from '@woocommerce/components';
import {
OPTIONS_STORE_NAME,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { AUTOMATION_PLUGINS, SettingsSelector } from '../utils';
import { Connect } from './connect';
import { Plugins } from './plugins';
import { StoreLocation } from '../components/store-location';
import './setup.scss';
export type SetupProps = {
isPending: boolean;
onDisable: () => void;
onAutomate: () => void;
onManual: () => void;
};
export type SetupStepProps = {
isPending: boolean;
isResolving: boolean;
nextStep: () => void;
onDisable: () => void;
onAutomate: () => void;
onManual: () => void;
pluginsToActivate: string[];
};
export const Setup: React.FC< SetupProps > = ( {
isPending,
onDisable,
onAutomate,
onManual,
} ) => {
const [ pluginsToActivate, setPluginsToActivate ] = useState( [] );
const { activePlugins, isResolving } = useSelect( ( select ) => {
const { getSettings } = select(
SETTINGS_STORE_NAME
) as SettingsSelector;
const { hasFinishedResolution } = select(
OPTIONS_STORE_NAME
) as SettingsSelector;
const { getActivePlugins } = select( PLUGINS_STORE_NAME );
return {
activePlugins: getActivePlugins(),
generalSettings: getSettings( 'general' )?.general,
isResolving:
! hasFinishedResolution( 'getOption', [
'woocommerce_setup_jetpack_opted_in',
] ) ||
! hasFinishedResolution( 'getOption', [
'wc_connect_options',
] ),
};
} );
const [ stepIndex, setStepIndex ] = useState( 0 );
useEffect( () => {
const remainingPlugins = difference(
AUTOMATION_PLUGINS,
activePlugins
);
if ( remainingPlugins.length <= pluginsToActivate.length ) {
return;
}
setPluginsToActivate( remainingPlugins );
}, [ activePlugins ] );
const nextStep = () => {
setStepIndex( stepIndex + 1 );
};
const stepProps = {
isPending,
isResolving,
onAutomate,
onDisable,
nextStep,
onManual,
pluginsToActivate,
};
const steps = [
{
key: 'store_location',
label: __( 'Set store location', 'woocommerce-admin' ),
description: __(
'The address from which your business operates',
'woocommerce-admin'
),
content: <StoreLocation { ...stepProps } />,
},
{
key: 'plugins',
label: pluginsToActivate.includes( 'woocommerce-services' )
? __(
'Install Jetpack and WooCommerce Tax',
'woocommerce-admin'
)
: __( 'Install Jetpack', 'woocommerce-admin' ),
description: __(
'Jetpack and WooCommerce Tax allow you to automate sales tax calculations',
'woocommerce-admin'
),
content: <Plugins { ...stepProps } />,
},
{
key: 'connect',
label: __( 'Connect your store', 'woocommerce-admin' ),
description: __(
'Connect your store to WordPress.com to enable automated sales tax calculations',
'woocommerce-admin'
),
content: <Connect { ...stepProps } />,
},
];
const step = steps[ stepIndex ];
return (
<Stepper
isPending={ isResolving }
isVertical={ true }
currentStep={ step.key }
steps={ steps }
/>
);
};

View File

@ -1,6 +1,6 @@
{ {
"name": "@woocommerce/admin-library", "name": "@woocommerce/admin-library",
"version": "2.7.0-dev", "version": "2.9.0-dev",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -10907,6 +10907,7 @@
"@wordpress/i18n": "3.17.0", "@wordpress/i18n": "3.17.0",
"@wordpress/url": "2.21.0", "@wordpress/url": "2.21.0",
"md5": "^2.3.0", "md5": "^2.3.0",
"qs": "6.9.6",
"rememo": "^3.0.0" "rememo": "^3.0.0"
}, },
"dependencies": { "dependencies": {

View File

@ -12,6 +12,7 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedOptions;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Appearance; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Appearance;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Products; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Products;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Tax; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Tax;
use Automattic\WooCommerce\Admin\PluginsHelper;
/** /**
* Contains the logic for completing onboarding tasks. * Contains the logic for completing onboarding tasks.
@ -72,6 +73,7 @@ class Init {
$settings['hasProducts'] = Products::has_products(); $settings['hasProducts'] = Products::has_products();
$settings['stylesheet'] = get_option( 'stylesheet' ); $settings['stylesheet'] = get_option( 'stylesheet' );
$settings['taxJarActivated'] = class_exists( 'WC_Taxjar' ); $settings['taxJarActivated'] = class_exists( 'WC_Taxjar' );
$settings['avalaraActivated'] = PluginsHelper::is_plugin_active( 'woocommerce-avatax' );
$settings['themeMods'] = get_theme_mods(); $settings['themeMods'] = get_theme_mods();
return $settings; return $settings;

View File

@ -6,6 +6,7 @@ use Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore as TaxDataSto
use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\Loader; use Automattic\WooCommerce\Admin\Loader;
use Automattic\WooCommerce\Admin\PluginsHelper;
/** /**
* Tax Task * Tax Task
@ -69,7 +70,8 @@ class Tax {
: __( "Let's go", 'woocommerce-admin' ), : __( "Let's go", 'woocommerce-admin' ),
'is_complete' => get_option( 'wc_connect_taxes_enabled' ) || 'is_complete' => get_option( 'wc_connect_taxes_enabled' ) ||
count( TaxDataStore::get_taxes( array() ) ) > 0 || count( TaxDataStore::get_taxes( array() ) ) > 0 ||
false !== get_option( 'woocommerce_no_sales_tax' ), false !== get_option( 'woocommerce_no_sales_tax' ) ||
PluginsHelper::is_plugin_active( 'woocommerce-avatax' ),
'is_visible' => true, 'is_visible' => true,
'time' => __( '1 minute', 'woocommerce-admin' ), 'time' => __( '1 minute', 'woocommerce-admin' ),
); );