Adding SlotFill support for API-driven tasks components (https://github.com/woocommerce/woocommerce-admin/pull/7616)
This commit is contained in:
parent
b8b7c94bd1
commit
f187c6763a
|
@ -177,6 +177,9 @@ class _Layout extends Component {
|
|||
{ window.wcAdminFeatures.navigation && (
|
||||
<PluginArea scope="woocommerce-navigation" />
|
||||
) }
|
||||
{ window.wcAdminFeatures.tasks && (
|
||||
<PluginArea scope="woocommerce-tasks" />
|
||||
) }
|
||||
</SlotFillProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -248,7 +248,7 @@
|
|||
}
|
||||
|
||||
.woocommerce-list {
|
||||
margin-top: $gap-smallest;
|
||||
margin-top: $gap-large;
|
||||
}
|
||||
|
||||
.woocommerce-list .woocommerce-list__item:first-child {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
.woocommerce-task-marketing {
|
||||
.components-card__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
display: flex !important;
|
||||
|
||||
h2 {
|
||||
align-self: start !important;
|
||||
color: $gray-900;
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 0;
|
||||
margin-top: $gap-smaller;
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
.woocommerce-plugin-list__plugin {
|
||||
display: flex;
|
||||
padding: $gap-large;
|
||||
border-top: 1px solid $gray-200;
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: $gap-small;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $gray-700;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-plugin-list__plugin-logo {
|
||||
margin-right: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-plugin-list__plugin-text {
|
||||
max-width: 370px;
|
||||
margin-right: $gap;
|
||||
}
|
||||
|
||||
.woocommerce-plugin-list__plugin-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './Plugin.scss';
|
||||
|
||||
export type PluginProps = {
|
||||
isActive: boolean;
|
||||
isBusy?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isInstalled: boolean;
|
||||
description?: string;
|
||||
installAndActivate?: ( slug: string ) => void;
|
||||
imageUrl?: string;
|
||||
manageUrl?: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export const Plugin: React.FC< PluginProps > = ( {
|
||||
description,
|
||||
imageUrl,
|
||||
installAndActivate = () => {},
|
||||
isActive,
|
||||
isBusy,
|
||||
isDisabled,
|
||||
isInstalled,
|
||||
manageUrl,
|
||||
name,
|
||||
slug,
|
||||
} ) => {
|
||||
return (
|
||||
<div className="woocommerce-plugin-list__plugin">
|
||||
{ imageUrl && (
|
||||
<div className="woocommerce-plugin-list__plugin-logo">
|
||||
<img
|
||||
src={ imageUrl }
|
||||
alt={ sprintf(
|
||||
/* translators: %s = name of the plugin */
|
||||
__( '%s logo', 'woocommerce-admin' ),
|
||||
name
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
<div className="woocommerce-plugin-list__plugin-text">
|
||||
<Text variant="subtitle.small" as="h4">
|
||||
{ name }
|
||||
</Text>
|
||||
<Text variant="subtitle.small">{ description }</Text>
|
||||
</div>
|
||||
<div className="woocommerce-plugin-list__plugin-action">
|
||||
{ isActive && manageUrl && (
|
||||
<Button
|
||||
disabled={ isDisabled }
|
||||
isBusy={ isBusy }
|
||||
isSecondary
|
||||
href={ getAdminLink( manageUrl ) }
|
||||
onClick={ () =>
|
||||
recordEvent( 'marketing_manage', {
|
||||
extension_name: slug,
|
||||
} )
|
||||
}
|
||||
>
|
||||
{ __( 'Manage', 'woocommmerce-admin' ) }
|
||||
</Button>
|
||||
) }
|
||||
{ isInstalled && ! isActive && (
|
||||
<Button
|
||||
disabled={ isDisabled }
|
||||
isBusy={ isBusy }
|
||||
isSecondary
|
||||
onClick={ () => installAndActivate( slug ) }
|
||||
>
|
||||
{ __( 'Activate', 'woocommmerce-admin' ) }
|
||||
</Button>
|
||||
) }
|
||||
{ ! isInstalled && (
|
||||
<Button
|
||||
disabled={ isDisabled }
|
||||
isBusy={ isBusy }
|
||||
isSecondary
|
||||
onClick={ () => installAndActivate( slug ) }
|
||||
>
|
||||
{ __( 'Get started', 'woocommmerce-admin' ) }
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
.woocommerce-plugin-list__title {
|
||||
padding: $gap-small 30px;
|
||||
background: $gray-200;
|
||||
position: relative;
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
color: $black;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Plugin, PluginProps } from './Plugin';
|
||||
import './PluginList.scss';
|
||||
|
||||
export type PluginListProps = {
|
||||
currentPlugin?: string | null;
|
||||
key?: string;
|
||||
installAndActivate?: ( slug: string ) => void;
|
||||
plugins?: PluginProps[];
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const PluginList: React.FC< PluginListProps > = ( {
|
||||
currentPlugin,
|
||||
installAndActivate = () => {},
|
||||
plugins = [],
|
||||
title,
|
||||
} ) => {
|
||||
return (
|
||||
<div className="woocommerce-plugin-list">
|
||||
{ title && (
|
||||
<div className="woocommerce-plugin-list__title">
|
||||
<Text variant="sectionheading" as="h3">
|
||||
{ title }
|
||||
</Text>
|
||||
</div>
|
||||
) }
|
||||
{ plugins.map( ( plugin ) => {
|
||||
const {
|
||||
description,
|
||||
imageUrl,
|
||||
isActive,
|
||||
isInstalled,
|
||||
manageUrl,
|
||||
slug,
|
||||
name,
|
||||
} = plugin;
|
||||
return (
|
||||
<Plugin
|
||||
key={ slug }
|
||||
description={ description }
|
||||
manageUrl={ manageUrl }
|
||||
name={ name }
|
||||
imageUrl={ imageUrl }
|
||||
installAndActivate={ installAndActivate }
|
||||
isActive={ isActive }
|
||||
isBusy={ currentPlugin === slug }
|
||||
isDisabled={ !! currentPlugin }
|
||||
isInstalled={ isInstalled }
|
||||
slug={ slug }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Card, CardHeader, Spinner } from '@wordpress/components';
|
||||
import {
|
||||
ONBOARDING_STORE_NAME,
|
||||
PLUGINS_STORE_NAME,
|
||||
OPTIONS_STORE_NAME,
|
||||
WCDataSelector,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
import { useMemo, useState } from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './Marketing.scss';
|
||||
import { createNoticesFromResponse } from '~/lib/notices';
|
||||
import { PluginList, PluginListProps } from './PluginList';
|
||||
import { PluginProps } from './Plugin';
|
||||
|
||||
const ALLOWED_PLUGIN_LISTS = [ 'reach', 'grow' ];
|
||||
const EMPTY_ARRAY = [];
|
||||
|
||||
export type ExtensionList = {
|
||||
key: string;
|
||||
title: string;
|
||||
plugins: Extension[];
|
||||
};
|
||||
|
||||
export type Extension = {
|
||||
description: string;
|
||||
key: string;
|
||||
image_url: string;
|
||||
manage_url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const transformExtensionToPlugin = (
|
||||
extension: Extension,
|
||||
activePlugins: string[],
|
||||
installedPlugins: string[]
|
||||
): PluginProps => {
|
||||
const { description, image_url, key, manage_url, name } = extension;
|
||||
const slug = key.split( ':' )[ 0 ];
|
||||
return {
|
||||
description,
|
||||
slug,
|
||||
imageUrl: image_url,
|
||||
isActive: activePlugins.includes( slug ),
|
||||
isInstalled: installedPlugins.includes( slug ),
|
||||
manageUrl: manage_url,
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMarketingExtensionLists = (
|
||||
freeExtensions: ExtensionList[],
|
||||
activePlugins: string[],
|
||||
installedPlugins: string[]
|
||||
): [ PluginProps[], PluginListProps[] ] => {
|
||||
const installed: PluginProps[] = [];
|
||||
const lists: PluginListProps[] = [];
|
||||
freeExtensions.forEach( ( list ) => {
|
||||
if ( ! ALLOWED_PLUGIN_LISTS.includes( list.key ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listPlugins: PluginProps[] = [];
|
||||
list.plugins.forEach( ( extension ) => {
|
||||
const plugin = transformExtensionToPlugin(
|
||||
extension,
|
||||
activePlugins,
|
||||
installedPlugins
|
||||
);
|
||||
if ( plugin.isInstalled ) {
|
||||
installed.push( plugin );
|
||||
return;
|
||||
}
|
||||
listPlugins.push( plugin );
|
||||
} );
|
||||
|
||||
if ( ! listPlugins.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformedList: PluginListProps = {
|
||||
...list,
|
||||
plugins: listPlugins,
|
||||
};
|
||||
lists.push( transformedList );
|
||||
} );
|
||||
|
||||
return [ installed, lists ];
|
||||
};
|
||||
|
||||
export type MarketingProps = {
|
||||
trackedCompletedActions: string[];
|
||||
};
|
||||
|
||||
const Marketing: React.FC = () => {
|
||||
const [ currentPlugin, setCurrentPlugin ] = useState< string | null >(
|
||||
null
|
||||
);
|
||||
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
const {
|
||||
activePlugins,
|
||||
freeExtensions,
|
||||
installedPlugins,
|
||||
isResolving,
|
||||
trackedCompletedActions,
|
||||
} = useSelect( ( select: WCDataSelector ) => {
|
||||
const { getActivePlugins, getInstalledPlugins } = select(
|
||||
PLUGINS_STORE_NAME
|
||||
);
|
||||
const { getFreeExtensions, hasFinishedResolution } = select(
|
||||
ONBOARDING_STORE_NAME
|
||||
);
|
||||
|
||||
const {
|
||||
getOption,
|
||||
hasFinishedResolution: optionFinishedResolution,
|
||||
} = select( OPTIONS_STORE_NAME );
|
||||
|
||||
const completedActions =
|
||||
getOption( 'woocommerce_task_list_tracked_completed_actions' ) ||
|
||||
EMPTY_ARRAY;
|
||||
|
||||
return {
|
||||
activePlugins: getActivePlugins(),
|
||||
freeExtensions: getFreeExtensions(),
|
||||
installedPlugins: getInstalledPlugins(),
|
||||
isResolving: ! (
|
||||
hasFinishedResolution( 'getFreeExtensions' ) &&
|
||||
optionFinishedResolution( 'getOption', [
|
||||
'woocommerce_task_list_tracked_completed_actions',
|
||||
] )
|
||||
),
|
||||
trackedCompletedActions: completedActions,
|
||||
};
|
||||
} );
|
||||
|
||||
const [ installedExtensions, pluginLists ] = useMemo(
|
||||
() =>
|
||||
getMarketingExtensionLists(
|
||||
freeExtensions,
|
||||
activePlugins,
|
||||
installedPlugins
|
||||
),
|
||||
[ installedPlugins, activePlugins, freeExtensions ]
|
||||
);
|
||||
|
||||
const installAndActivate = ( slug: string ) => {
|
||||
setCurrentPlugin( slug );
|
||||
installAndActivatePlugins( [ slug ] )
|
||||
.then( ( response: { errors: Record< string, string > } ) => {
|
||||
recordEvent( 'tasklist_marketing_install', {
|
||||
selected_extension: slug,
|
||||
installed_extensions: installedExtensions.map(
|
||||
( extension ) => extension.slug
|
||||
),
|
||||
} );
|
||||
|
||||
if ( ! trackedCompletedActions.includes( 'marketing' ) ) {
|
||||
updateOptions( {
|
||||
woocommerce_task_list_tracked_completed_actions: [
|
||||
...trackedCompletedActions,
|
||||
'marketing',
|
||||
],
|
||||
} );
|
||||
}
|
||||
|
||||
createNoticesFromResponse( response );
|
||||
setCurrentPlugin( null );
|
||||
} )
|
||||
.catch( ( response: { errors: Record< string, string > } ) => {
|
||||
createNoticesFromResponse( response );
|
||||
setCurrentPlugin( null );
|
||||
} );
|
||||
};
|
||||
|
||||
if ( isResolving ) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-task-marketing">
|
||||
{ !! installedExtensions.length && (
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardHeader>
|
||||
<Text
|
||||
variant="title.small"
|
||||
as="h2"
|
||||
className="woocommerce-task-card__title"
|
||||
>
|
||||
{ __(
|
||||
'Installed marketing extensions',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<PluginList
|
||||
currentPlugin={ currentPlugin }
|
||||
plugins={ installedExtensions }
|
||||
/>
|
||||
</Card>
|
||||
) }
|
||||
{ !! pluginLists.length && (
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardHeader>
|
||||
<Text
|
||||
variant="title.small"
|
||||
as="h2"
|
||||
className="woocommerce-task-card__title"
|
||||
>
|
||||
{ __(
|
||||
'Recommended marketing extensions',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Text>
|
||||
<Text as="span">
|
||||
{ __(
|
||||
'We recommend adding one of the following marketing tools for your store. The extension will be installed and activated for you when you click "Get started".',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Text>
|
||||
</CardHeader>
|
||||
{ pluginLists.map( ( list ) => {
|
||||
const { key, title, plugins } = list;
|
||||
return (
|
||||
<PluginList
|
||||
currentPlugin={ currentPlugin }
|
||||
installAndActivate={ installAndActivate }
|
||||
key={ key }
|
||||
plugins={ plugins }
|
||||
title={ title }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</Card>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-marketing', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="marketing">
|
||||
<Marketing />
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
Extension,
|
||||
transformExtensionToPlugin,
|
||||
getMarketingExtensionLists,
|
||||
} from '../';
|
||||
|
||||
const basicPlugins: Extension[] = [
|
||||
{
|
||||
key: 'basic-plugin',
|
||||
name: 'Basic Plugin',
|
||||
description: 'Basic plugin description',
|
||||
manage_url: '#',
|
||||
image_url: 'basic.jpeg',
|
||||
},
|
||||
];
|
||||
|
||||
const reachPlugins: Extension[] = [
|
||||
{
|
||||
key: 'reach-plugin',
|
||||
name: 'Reach Plugin',
|
||||
description: 'Reach plugin description',
|
||||
manage_url: '#',
|
||||
image_url: 'reach.jpeg',
|
||||
},
|
||||
];
|
||||
|
||||
const growPlugins: Extension[] = [
|
||||
{
|
||||
key: 'grow-plugin',
|
||||
name: 'Grow Plugin',
|
||||
description: 'Grow plugin description',
|
||||
manage_url: '#',
|
||||
image_url: 'grow.jpeg',
|
||||
},
|
||||
{
|
||||
key: 'grow-plugin-two:extra',
|
||||
name: 'Grow Plugin 2',
|
||||
description: 'Grow plugin 2 description',
|
||||
manage_url: '#',
|
||||
image_url: 'grow2.jpeg',
|
||||
},
|
||||
];
|
||||
|
||||
const extensionLists = [
|
||||
{
|
||||
key: 'basics',
|
||||
plugins: basicPlugins,
|
||||
title: 'Basics',
|
||||
},
|
||||
{
|
||||
key: 'reach',
|
||||
plugins: reachPlugins,
|
||||
title: 'Reach',
|
||||
},
|
||||
{
|
||||
key: 'grow',
|
||||
plugins: growPlugins,
|
||||
title: 'Grow',
|
||||
},
|
||||
];
|
||||
|
||||
describe( 'transformExtensionToPlugin', () => {
|
||||
test( 'should return the formatted extension', () => {
|
||||
const plugin = transformExtensionToPlugin( basicPlugins[ 0 ], [], [] );
|
||||
expect( plugin ).toEqual( {
|
||||
description: 'Basic plugin description',
|
||||
slug: 'basic-plugin',
|
||||
imageUrl: 'basic.jpeg',
|
||||
isActive: false,
|
||||
isInstalled: false,
|
||||
manageUrl: '#',
|
||||
name: 'Basic Plugin',
|
||||
} );
|
||||
} );
|
||||
|
||||
test( 'should get the plugin slug when a colon exists', () => {
|
||||
const plugin = transformExtensionToPlugin( growPlugins[ 1 ], [], [] );
|
||||
expect( plugin.slug ).toEqual( 'grow-plugin-two' );
|
||||
} );
|
||||
|
||||
test( 'should mark the plugin as active when in the active plugins', () => {
|
||||
const plugin = transformExtensionToPlugin(
|
||||
basicPlugins[ 0 ],
|
||||
[ 'basic-plugin' ],
|
||||
[]
|
||||
);
|
||||
expect( plugin.isActive ).toBeTruthy();
|
||||
} );
|
||||
|
||||
test( 'should mark the plugin as installed when in the installed plugins', () => {
|
||||
const plugin = transformExtensionToPlugin(
|
||||
basicPlugins[ 0 ],
|
||||
[],
|
||||
[ 'basic-plugin' ]
|
||||
);
|
||||
expect( plugin.isInstalled ).toBeTruthy();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getMarketingExtensionLists', () => {
|
||||
test( 'should only return the allowed lists', () => {
|
||||
const [ installed, lists ] = getMarketingExtensionLists(
|
||||
extensionLists,
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
expect( lists.length ).toBe( 2 );
|
||||
expect( lists[ 0 ].key ).toBe( 'reach' );
|
||||
expect( lists[ 1 ].key ).toBe( 'grow' );
|
||||
} );
|
||||
|
||||
test( 'should separate installed plugins', () => {
|
||||
const [ installed ] = getMarketingExtensionLists(
|
||||
extensionLists,
|
||||
[],
|
||||
[ 'grow-plugin' ]
|
||||
);
|
||||
|
||||
expect( installed.length ).toBe( 1 );
|
||||
expect( installed[ 0 ].slug ).toBe( 'grow-plugin' );
|
||||
} );
|
||||
|
||||
test( 'should not include installed plugins in the extensions list', () => {
|
||||
const [ installed, lists ] = getMarketingExtensionLists(
|
||||
extensionLists,
|
||||
[],
|
||||
[ 'grow-plugin' ]
|
||||
);
|
||||
|
||||
expect( lists[ 1 ].plugins.length ).toBe( 1 );
|
||||
} );
|
||||
|
||||
test( 'should only include allowed list plugins in the installed list', () => {
|
||||
const [ installed, lists ] = getMarketingExtensionLists(
|
||||
extensionLists,
|
||||
[],
|
||||
[ 'basic-plugin' ]
|
||||
);
|
||||
|
||||
expect( installed.length ).toBe( 0 );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Spinner } from '@wordpress/components';
|
||||
import { updateQueryString } from '@woocommerce/navigation';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
export const Action = ( {
|
||||
hasSetup = false,
|
||||
needsSetup = true,
|
||||
id,
|
||||
isEnabled = false,
|
||||
isLoading = false,
|
||||
isInstalled = false,
|
||||
isRecommended = false,
|
||||
hasPlugins,
|
||||
manageUrl = null,
|
||||
markConfigured,
|
||||
onSetUp = () => {},
|
||||
onSetupCallback,
|
||||
setupButtonText = __( 'Set up', 'woocommerce-admin' ),
|
||||
} ) => {
|
||||
const [ isBusy, setIsBusy ] = useState( false );
|
||||
|
||||
const classes = 'woocommerce-task-payment__action';
|
||||
|
||||
if ( isLoading ) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const handleClick = async () => {
|
||||
onSetUp( id );
|
||||
|
||||
if ( onSetupCallback ) {
|
||||
setIsBusy( true );
|
||||
await new Promise( onSetupCallback )
|
||||
.then( () => {
|
||||
setIsBusy( false );
|
||||
} )
|
||||
.catch( () => {
|
||||
setIsBusy( false );
|
||||
} );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateQueryString( {
|
||||
id,
|
||||
} );
|
||||
};
|
||||
|
||||
const ManageButton = () => (
|
||||
<Button
|
||||
className={ classes }
|
||||
isSecondary
|
||||
role="button"
|
||||
href={ manageUrl }
|
||||
onClick={ () => recordEvent( 'tasklist_payment_manage', { id } ) }
|
||||
>
|
||||
{ __( 'Manage', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
);
|
||||
|
||||
const SetupButton = () => (
|
||||
<Button
|
||||
className={ classes }
|
||||
isPrimary={ isRecommended }
|
||||
isSecondary={ ! isRecommended }
|
||||
isBusy={ isBusy }
|
||||
disabled={ isBusy }
|
||||
onClick={ () => handleClick() }
|
||||
>
|
||||
{ setupButtonText }
|
||||
</Button>
|
||||
);
|
||||
|
||||
if ( ! hasSetup ) {
|
||||
if ( ! isEnabled ) {
|
||||
return (
|
||||
<Button
|
||||
className={ classes }
|
||||
isSecondary
|
||||
onClick={ () => markConfigured( id ) }
|
||||
>
|
||||
{ __( 'Enable', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return <ManageButton />;
|
||||
}
|
||||
|
||||
// This isolates core gateways that include setup
|
||||
if ( ! hasPlugins ) {
|
||||
if ( isEnabled ) {
|
||||
return <ManageButton />;
|
||||
}
|
||||
|
||||
return <SetupButton />;
|
||||
}
|
||||
|
||||
if ( ! needsSetup ) {
|
||||
return <ManageButton />;
|
||||
}
|
||||
|
||||
if ( isInstalled && hasPlugins ) {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className={ classes }
|
||||
isPrimary={ isRecommended }
|
||||
isSecondary={ ! isRecommended }
|
||||
isBusy={ isBusy }
|
||||
disabled={ isBusy }
|
||||
onClick={ () => handleClick() }
|
||||
>
|
||||
{ __( 'Finish setup', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SetupButton />;
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { CardBody, CardMedia, CardDivider } from '@wordpress/components';
|
||||
import { RecommendedRibbon, SetupRequired } from '@woocommerce/onboarding';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Text, useSlot } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Action } from '../Action';
|
||||
import './List.scss';
|
||||
|
||||
export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
|
||||
const {
|
||||
image,
|
||||
content,
|
||||
id,
|
||||
plugins = [],
|
||||
title,
|
||||
loading,
|
||||
enabled: isEnabled = false,
|
||||
installed: isInstalled = false,
|
||||
needsSetup = true,
|
||||
requiredSettings,
|
||||
settingsUrl: manageUrl,
|
||||
is_local_partner: isLocalPartner,
|
||||
} = paymentGateway;
|
||||
|
||||
const connectSlot = useSlot(
|
||||
`woocommerce_payment_gateway_configure_${ id }`
|
||||
);
|
||||
const setupSlot = useSlot( `woocommerce_payment_gateway_setup_${ id }` );
|
||||
|
||||
const hasFills =
|
||||
Boolean( connectSlot?.fills?.length ) ||
|
||||
Boolean( setupSlot?.fills?.length );
|
||||
|
||||
const hasSetup = Boolean(
|
||||
plugins.length || requiredSettings.length || hasFills
|
||||
);
|
||||
const showRecommendedRibbon = isRecommended && needsSetup;
|
||||
|
||||
const classes = classnames(
|
||||
'woocommerce-task-payment',
|
||||
'woocommerce-task-card',
|
||||
needsSetup && 'woocommerce-task-payment-not-configured',
|
||||
'woocommerce-task-payment-' + id
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={ id }>
|
||||
<CardBody
|
||||
style={ { paddingLeft: 0, marginBottom: 0 } }
|
||||
className={ classes }
|
||||
>
|
||||
<CardMedia isBorderless>
|
||||
<img src={ image } alt={ title } />
|
||||
</CardMedia>
|
||||
<div className="woocommerce-task-payment__description">
|
||||
{ showRecommendedRibbon && (
|
||||
<RecommendedRibbon isLocalPartner={ isLocalPartner } />
|
||||
) }
|
||||
<Text as="h3" className="woocommerce-task-payment__title">
|
||||
{ title }
|
||||
{ isInstalled && needsSetup && !! plugins.length && (
|
||||
<SetupRequired />
|
||||
) }
|
||||
</Text>
|
||||
<div className="woocommerce-task-payment__content">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
<div className="woocommerce-task-payment__footer">
|
||||
<Action
|
||||
manageUrl={ manageUrl }
|
||||
id={ id }
|
||||
hasSetup={ hasSetup }
|
||||
needsSetup={ needsSetup }
|
||||
isEnabled={ isEnabled }
|
||||
isInstalled={ isInstalled }
|
||||
hasPlugins={ Boolean( plugins.length ) }
|
||||
isRecommended={ isRecommended }
|
||||
isLoading={ loading }
|
||||
markConfigured={ markConfigured }
|
||||
onSetUp={ () =>
|
||||
recordEvent( 'tasklist_payment_setup', {
|
||||
selected: id,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardDivider />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Card, CardHeader } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Item } from './Item';
|
||||
|
||||
import './List.scss';
|
||||
|
||||
export const List = ( {
|
||||
heading,
|
||||
markConfigured,
|
||||
recommendation,
|
||||
paymentGateways,
|
||||
} ) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader as="h2">{ heading }</CardHeader>
|
||||
{ paymentGateways.map( ( paymentGateway ) => {
|
||||
const { id } = paymentGateway;
|
||||
return (
|
||||
<Item
|
||||
key={ id }
|
||||
isRecommended={ recommendation === id }
|
||||
markConfigured={ markConfigured }
|
||||
paymentGateway={ paymentGateway }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
.woocommerce-task-payment {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.components-card__media {
|
||||
width: 170px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img,
|
||||
svg,
|
||||
.is-placeholder {
|
||||
margin: auto;
|
||||
max-width: 100px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.is-placeholder {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-task-payment__footer {
|
||||
.is-placeholder {
|
||||
width: 70px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
> .components-form-toggle {
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.woocommerce-task-payment__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $studio-gray-80;
|
||||
margin-top: 0;
|
||||
margin-bottom: $gap-smaller;
|
||||
}
|
||||
|
||||
.woocommerce-task-payment__content {
|
||||
font-size: 14px;
|
||||
color: $studio-gray-60;
|
||||
margin: 0;
|
||||
margin-right: $gap-larger;
|
||||
|
||||
.text-style-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.woocommerce-task-payment__description {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@include breakpoint( '<600px' ) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.woocommerce-task-payment__content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.components-card__media {
|
||||
> svg,
|
||||
> img {
|
||||
margin: 0 0 0 $gap-large;
|
||||
}
|
||||
|
||||
order: 1;
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
.woocommerce-task-payment__description {
|
||||
order: 3;
|
||||
padding: $gap-large 0 0 $gap-large;
|
||||
}
|
||||
|
||||
.woocommerce-task-payment__footer {
|
||||
flex-basis: 50%;
|
||||
align-self: flex-start;
|
||||
order: 2;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-payment-gateway-suggestions-list-placeholder {
|
||||
.is-placeholder {
|
||||
@include placeholder();
|
||||
display: inline-block;
|
||||
max-width: 240px;
|
||||
width: 80%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardMedia,
|
||||
CardDivider,
|
||||
} from '@wordpress/components';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './List.scss';
|
||||
|
||||
const PlaceholderItem = () => {
|
||||
const classes = classnames(
|
||||
'woocommerce-task-payment',
|
||||
'woocommerce-task-card'
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CardBody
|
||||
style={ { paddingLeft: 0, marginBottom: 0 } }
|
||||
className={ classes }
|
||||
>
|
||||
<CardMedia isBorderless>
|
||||
<span className="is-placeholder" />
|
||||
</CardMedia>
|
||||
<div className="woocommerce-task-payment__description">
|
||||
<Text as="h3" className="woocommerce-task-payment__title">
|
||||
<span className="is-placeholder" />
|
||||
</Text>
|
||||
<div className="woocommerce-task-payment__content">
|
||||
<span className="is-placeholder" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="woocommerce-task-payment__footer">
|
||||
<span className="is-placeholder" />
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardDivider />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const Placeholder = () => {
|
||||
const classes =
|
||||
'is-loading woocommerce-payment-gateway-suggestions-list-placeholder';
|
||||
|
||||
return (
|
||||
<Card aria-hidden="true" className={ classes }>
|
||||
<CardHeader as="h2">
|
||||
<span className="is-placeholder" />
|
||||
</CardHeader>
|
||||
<PlaceholderItem />
|
||||
<PlaceholderItem />
|
||||
<PlaceholderItem />
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export { List } from './List';
|
||||
export { Placeholder } from './Placeholder';
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { List } from '../List';
|
||||
|
||||
jest.mock( '@woocommerce/tracks', () => ( {
|
||||
recordEvent: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockGateway = {
|
||||
id: 'mock-gateway',
|
||||
title: 'Mock Gateway',
|
||||
plugins: [],
|
||||
postInstallScripts: [],
|
||||
requiredSettings: [],
|
||||
installed: false,
|
||||
needsSetup: false,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
heading: 'Test heading',
|
||||
markConfigured: jest.fn(),
|
||||
recommendation: 'testId',
|
||||
paymentGateways: [ mockGateway ],
|
||||
};
|
||||
|
||||
describe( 'PaymentGatewaySuggestions > List', () => {
|
||||
it( 'should display correct heading', () => {
|
||||
const { queryByText } = render( <List { ...defaultProps } /> );
|
||||
|
||||
expect( queryByText( defaultProps.heading ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should display gateway title', () => {
|
||||
const { queryByText } = render( <List { ...defaultProps } /> );
|
||||
|
||||
expect( queryByText( mockGateway.title ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should display the "Enable" button when setup is NOT required', () => {
|
||||
const { queryByRole } = render( <List { ...defaultProps } /> );
|
||||
|
||||
expect( queryByRole( 'button' ) ).toHaveTextContent( 'Enable' );
|
||||
} );
|
||||
|
||||
it( 'should display the "Set up" button when setup is required', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
needsSetup: true,
|
||||
plugins: [ 'test-plugins' ],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByRole } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByRole( 'button' ) ).toHaveTextContent( 'Set up' );
|
||||
} );
|
||||
|
||||
it( 'should display the SetupRequired component when appropriate', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
needsSetup: true,
|
||||
installed: true,
|
||||
plugins: [ 'test-plugin' ],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByText } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByText( 'Setup required' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should not display the SetupRequired component when not appropriate', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
needsSetup: true,
|
||||
installed: false,
|
||||
plugins: [ 'test-plugin' ],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByText } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByText( 'Setup required' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should display the Recommended ribbon when appropriate', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
recommendation: 'mock-gateway',
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
id: 'mock-gateway',
|
||||
needsSetup: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByText } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByText( 'Recommended' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should not display the Recommended ribbon when gateway id does not match', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
recommendation: 'mock-gateway',
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
id: 'mock-gateway-other',
|
||||
needsSetup: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByText } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByText( 'Recommended' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should display Manage button if not enabled and does have setup', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByRole } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByRole( 'button' ) ).toHaveTextContent( 'Manage' );
|
||||
} );
|
||||
|
||||
it( 'should display Manage button for core plugins that are enabled', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
requiredSettings: [ 'just', 'kidding' ],
|
||||
enabled: true,
|
||||
plugins: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByRole } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByRole( 'button' ) ).toHaveTextContent( 'Manage' );
|
||||
} );
|
||||
|
||||
it( 'should display Manage button if it does have plugins and does not need setup', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
plugins: [ 'nope' ],
|
||||
needsSetup: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByRole } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByRole( 'button' ) ).toHaveTextContent( 'Manage' );
|
||||
} );
|
||||
|
||||
it( 'should display Finish Setup button when installed but not setup', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateways: [
|
||||
{
|
||||
...mockGateway,
|
||||
plugins: [ 'nope' ],
|
||||
needsSetup: true,
|
||||
installed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { queryByRole } = render( <List { ...props } /> );
|
||||
|
||||
expect( queryByRole( 'button' ) ).toHaveTextContent( 'Finish setup' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { PAYMENT_GATEWAYS_STORE_NAME } from '@woocommerce/data';
|
||||
import { DynamicForm } from '@woocommerce/components';
|
||||
import { WooPaymentGatewayConfigure } from '@woocommerce/onboarding';
|
||||
import { useSlot } from '@woocommerce/experimental';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sanitizeHTML from '~/lib/sanitize-html';
|
||||
|
||||
export const validateFields = ( values, fields ) => {
|
||||
const errors = {};
|
||||
const getField = ( fieldId ) =>
|
||||
fields.find( ( field ) => field.id === fieldId );
|
||||
|
||||
for ( const [ fieldKey, value ] of Object.entries( values ) ) {
|
||||
const field = getField( fieldKey );
|
||||
// Matches any word that is capitalized aside from abrevitions like ID.
|
||||
const label = field.label.replace( /([A-Z][a-z]+)/g, ( val ) =>
|
||||
val.toLowerCase()
|
||||
);
|
||||
|
||||
if ( ! ( value || field.type === 'checkbox' ) ) {
|
||||
errors[ fieldKey ] = `Please enter your ${ label }`;
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const Configure = ( { markConfigured, paymentGateway } ) => {
|
||||
const {
|
||||
id,
|
||||
connectionUrl,
|
||||
setupHelpText,
|
||||
settingsUrl,
|
||||
title,
|
||||
requiredSettings: fields,
|
||||
} = paymentGateway;
|
||||
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const { updatePaymentGateway } = useDispatch( PAYMENT_GATEWAYS_STORE_NAME );
|
||||
const slot = useSlot( `woocommerce_payment_gateway_configure_${ id }` );
|
||||
const hasFills = Boolean( slot?.fills?.length );
|
||||
|
||||
const { isUpdating } = useSelect( ( select ) => {
|
||||
const { isPaymentGatewayUpdating } = select(
|
||||
PAYMENT_GATEWAYS_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
isUpdating: isPaymentGatewayUpdating(),
|
||||
};
|
||||
} );
|
||||
|
||||
const handleSubmit = ( values ) => {
|
||||
updatePaymentGateway( id, {
|
||||
enabled: true,
|
||||
settings: values,
|
||||
} )
|
||||
.then( ( result ) => {
|
||||
if ( result && result.id === id ) {
|
||||
markConfigured( id );
|
||||
createNotice(
|
||||
'success',
|
||||
sprintf(
|
||||
/* translators: %s = title of the payment gateway */
|
||||
__(
|
||||
'%s configured successfully',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
title
|
||||
)
|
||||
);
|
||||
}
|
||||
} )
|
||||
.catch( () => {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was a problem saving your payment settings',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
const helpText = setupHelpText && (
|
||||
<p dangerouslySetInnerHTML={ sanitizeHTML( setupHelpText ) } />
|
||||
);
|
||||
const defaultForm = (
|
||||
<DynamicForm
|
||||
fields={ fields }
|
||||
isBusy={ isUpdating }
|
||||
onSubmit={ handleSubmit }
|
||||
submitLabel={ __( 'Proceed', 'woocommerce-admin' ) }
|
||||
validate={ ( values ) => validateFields( values, fields ) }
|
||||
/>
|
||||
);
|
||||
|
||||
if ( hasFills ) {
|
||||
return (
|
||||
<WooPaymentGatewayConfigure.Slot
|
||||
fillProps={ {
|
||||
defaultForm,
|
||||
defaultSubmit: handleSubmit,
|
||||
defaultFields: fields,
|
||||
markConfigured: () => markConfigured( id ),
|
||||
paymentGateway,
|
||||
} }
|
||||
id={ id }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ( connectionUrl ) {
|
||||
return (
|
||||
<>
|
||||
{ helpText }
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={ () =>
|
||||
recordEvent( 'tasklist_payment_connect_start', {
|
||||
payment_method: id,
|
||||
} )
|
||||
}
|
||||
href={ connectionUrl }
|
||||
>
|
||||
{ __( 'Connect', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if ( fields.length ) {
|
||||
return (
|
||||
<>
|
||||
{ helpText }
|
||||
{ defaultForm }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ helpText || (
|
||||
<p>
|
||||
{ __(
|
||||
"You can manage this payment gateway's settings by clicking the button below",
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
) }
|
||||
<Button isPrimary href={ settingsUrl }>
|
||||
{ __( 'Set up', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import { Stepper } from '@woocommerce/components';
|
||||
|
||||
export const Placeholder = () => {
|
||||
const classes = classnames(
|
||||
'is-loading',
|
||||
'woocommerce-task-payment-method',
|
||||
'woocommerce-task-card'
|
||||
);
|
||||
|
||||
return (
|
||||
<Card aria-hidden="true" className={ classes }>
|
||||
<CardBody>
|
||||
<Stepper
|
||||
isVertical
|
||||
currentStep={ 'none' }
|
||||
steps={ [
|
||||
{
|
||||
key: 'first',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
key: 'second',
|
||||
label: '',
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import { enqueueScript } from '@woocommerce/wc-admin-settings';
|
||||
import {
|
||||
OPTIONS_STORE_NAME,
|
||||
PAYMENT_GATEWAYS_STORE_NAME,
|
||||
PLUGINS_STORE_NAME,
|
||||
} from '@woocommerce/data';
|
||||
import { Plugins, Stepper } from '@woocommerce/components';
|
||||
import { WooPaymentGatewaySetup } from '@woocommerce/onboarding';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useEffect, useState, useMemo } from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { useSlot } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { createNoticesFromResponse } from '~/lib/notices';
|
||||
import { Configure } from './Configure';
|
||||
import './Setup.scss';
|
||||
|
||||
export const Setup = ( { markConfigured, paymentGateway } ) => {
|
||||
const {
|
||||
id,
|
||||
plugins = [],
|
||||
title,
|
||||
postInstallScripts,
|
||||
installed: gatewayInstalled,
|
||||
} = paymentGateway;
|
||||
const slot = useSlot( `woocommerce_payment_gateway_setup_${ id }` );
|
||||
const hasFills = Boolean( slot?.fills?.length );
|
||||
const [ isPluginLoaded, setIsPluginLoaded ] = useState( false );
|
||||
|
||||
useEffect( () => {
|
||||
recordEvent( 'payments_task_stepper_view', {
|
||||
payment_method: id,
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
const { invalidateResolutionForStoreSelector } = useDispatch(
|
||||
PAYMENT_GATEWAYS_STORE_NAME
|
||||
);
|
||||
|
||||
const {
|
||||
isOptionUpdating,
|
||||
isPaymentGatewayResolving,
|
||||
needsPluginInstall,
|
||||
} = useSelect( ( select ) => {
|
||||
const { isOptionsUpdating } = select( OPTIONS_STORE_NAME );
|
||||
const { isResolving } = select( PAYMENT_GATEWAYS_STORE_NAME );
|
||||
const activePlugins = select( PLUGINS_STORE_NAME ).getActivePlugins();
|
||||
const pluginsToInstall = plugins.filter(
|
||||
( m ) => ! activePlugins.includes( m )
|
||||
);
|
||||
|
||||
return {
|
||||
isOptionUpdating: isOptionsUpdating(),
|
||||
isPaymentGatewayResolving: isResolving( 'getPaymentGateways' ),
|
||||
needsPluginInstall: !! pluginsToInstall.length,
|
||||
};
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
if ( needsPluginInstall ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( postInstallScripts && postInstallScripts.length ) {
|
||||
const scriptPromises = postInstallScripts.map( ( script ) =>
|
||||
enqueueScript( script )
|
||||
);
|
||||
Promise.all( scriptPromises ).then( () => {
|
||||
setIsPluginLoaded( true );
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPluginLoaded( true );
|
||||
}, [ postInstallScripts, needsPluginInstall ] );
|
||||
|
||||
const installStep = useMemo( () => {
|
||||
return plugins && plugins.length
|
||||
? {
|
||||
key: 'install',
|
||||
label: sprintf(
|
||||
/* translators: %s = title of the payment gateway to install */
|
||||
__( 'Install %s', 'woocommerce-admin' ),
|
||||
title
|
||||
),
|
||||
content: (
|
||||
<Plugins
|
||||
onComplete={ ( installedPlugins, response ) => {
|
||||
createNoticesFromResponse( response );
|
||||
invalidateResolutionForStoreSelector(
|
||||
'getPaymentGateways'
|
||||
);
|
||||
recordEvent(
|
||||
'tasklist_payment_install_method',
|
||||
{
|
||||
plugins,
|
||||
}
|
||||
);
|
||||
} }
|
||||
onError={ ( errors, response ) =>
|
||||
createNoticesFromResponse( response )
|
||||
}
|
||||
autoInstall
|
||||
pluginSlugs={ plugins }
|
||||
/>
|
||||
),
|
||||
}
|
||||
: null;
|
||||
}, [] );
|
||||
|
||||
const configureStep = useMemo(
|
||||
() => ( {
|
||||
key: 'configure',
|
||||
label: sprintf(
|
||||
__( 'Configure your %(title)s account', 'woocommerce-admin' ),
|
||||
{
|
||||
title,
|
||||
}
|
||||
),
|
||||
content: gatewayInstalled ? (
|
||||
<Configure
|
||||
markConfigured={ markConfigured }
|
||||
paymentGateway={ paymentGateway }
|
||||
/>
|
||||
) : null,
|
||||
} ),
|
||||
[ gatewayInstalled ]
|
||||
);
|
||||
|
||||
const stepperPending =
|
||||
needsPluginInstall ||
|
||||
isOptionUpdating ||
|
||||
isPaymentGatewayResolving ||
|
||||
! isPluginLoaded;
|
||||
|
||||
const defaultStepper = (
|
||||
<Stepper
|
||||
isVertical
|
||||
isPending={ stepperPending }
|
||||
currentStep={ needsPluginInstall ? 'install' : 'configure' }
|
||||
steps={ [ installStep, configureStep ].filter( Boolean ) }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-task-payment-method woocommerce-task-card">
|
||||
<CardBody>
|
||||
{ hasFills ? (
|
||||
<WooPaymentGatewaySetup.Slot
|
||||
fillProps={ {
|
||||
defaultStepper,
|
||||
defaultInstallStep: installStep,
|
||||
defaultConfigureStep: configureStep,
|
||||
markConfigured: () => markConfigured( id ),
|
||||
paymentGateway,
|
||||
} }
|
||||
id={ id }
|
||||
/>
|
||||
) : (
|
||||
defaultStepper
|
||||
) }
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
.woocommerce-task-payment-method.is-loading {
|
||||
.woocommerce-stepper__step-label {
|
||||
@include placeholder();
|
||||
display: inline-block;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.woocommerce-stepper__step-icon {
|
||||
@include placeholder();
|
||||
}
|
||||
|
||||
.woocommerce-stepper__step:nth-child(1) .woocommerce-stepper__step-label {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.woocommerce-stepper__step-number {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { Setup } from './Setup';
|
||||
export { Placeholder } from './Placeholder';
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Configure, validateFields } from '../Configure';
|
||||
|
||||
const mockGateway = {
|
||||
id: 'mock-gateway',
|
||||
title: 'Mock Gateway',
|
||||
connectionUrl: 'http://mockgateway.com/connect',
|
||||
setupHelpText: 'Help text',
|
||||
settingsUrl:
|
||||
'https://example.com/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=mock-gateway',
|
||||
requiredSettings: [
|
||||
{
|
||||
id: 'api_key',
|
||||
label: 'API key',
|
||||
type: 'text',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'api_secret',
|
||||
label: 'API secret',
|
||||
type: 'text',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
markConfigured: () => {},
|
||||
recordConnectStartEvent: () => {},
|
||||
paymentGateway: mockGateway,
|
||||
};
|
||||
|
||||
describe( 'Configure', () => {
|
||||
it( 'should show help text', () => {
|
||||
const { queryByText } = render( <Configure { ...defaultProps } /> );
|
||||
|
||||
expect( queryByText( 'Help text' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render a button with the connection URL', () => {
|
||||
const { container } = render( <Configure { ...defaultProps } /> );
|
||||
|
||||
const button = container.querySelector( 'a' );
|
||||
expect( button.textContent ).toBe( 'Connect' );
|
||||
expect( button.href ).toBe( mockGateway.connectionUrl );
|
||||
} );
|
||||
|
||||
it( 'should render fields when no connection URL exists', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateway: {
|
||||
...mockGateway,
|
||||
connectionUrl: null,
|
||||
},
|
||||
};
|
||||
const { container } = render( <Configure { ...props } /> );
|
||||
|
||||
const inputs = container.querySelectorAll( 'input' );
|
||||
expect( inputs.length ).toBe( 2 );
|
||||
expect( inputs[ 0 ].placeholder ).toBe( 'API key' );
|
||||
expect( inputs[ 1 ].placeholder ).toBe( 'API secret' );
|
||||
} );
|
||||
|
||||
it( 'should render the set up button when no connection URL or fields exist', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateway: {
|
||||
...mockGateway,
|
||||
connectionUrl: null,
|
||||
requiredSettings: [],
|
||||
},
|
||||
};
|
||||
const { container } = render( <Configure { ...props } /> );
|
||||
|
||||
const button = container.querySelector( 'a' );
|
||||
expect( button.textContent ).toBe( 'Set up' );
|
||||
expect( button.href ).toBe( mockGateway.settingsUrl );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'validateFields', () => {
|
||||
it( 'should return an empty object when no errors exist', () => {
|
||||
const values = {
|
||||
api_key: '123',
|
||||
api_secret: '123',
|
||||
};
|
||||
const errors = validateFields( values, mockGateway.requiredSettings );
|
||||
|
||||
expect( errors ).toMatchObject( {} );
|
||||
} );
|
||||
|
||||
it( 'should return the errors using field labels when errors exist', () => {
|
||||
const values = {
|
||||
api_key: '123',
|
||||
api_secret: null,
|
||||
};
|
||||
const errors = validateFields( values, mockGateway.requiredSettings );
|
||||
|
||||
expect( errors ).toMatchObject( {
|
||||
api_secret: 'Please enter your API secret',
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { enqueueScript } from '@woocommerce/wc-admin-settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Setup } from '../';
|
||||
|
||||
jest.mock( '@woocommerce/components', () => {
|
||||
const originalModule = jest.requireActual( '@woocommerce/components' );
|
||||
|
||||
return {
|
||||
DynamicForm: () => <div />,
|
||||
Plugins: () => <div />,
|
||||
Stepper: originalModule.Stepper,
|
||||
};
|
||||
} );
|
||||
|
||||
jest.mock( '@woocommerce/wc-admin-settings' );
|
||||
|
||||
const mockGateway = {
|
||||
id: 'mock-gateway',
|
||||
title: 'Mock Gateway',
|
||||
plugins: [],
|
||||
postInstallScripts: [],
|
||||
installed: false,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
markConfigured: () => {},
|
||||
recordConnectStartEvent: () => {},
|
||||
paymentGateway: mockGateway,
|
||||
};
|
||||
|
||||
describe( 'Setup', () => {
|
||||
it( 'should show a configure step', () => {
|
||||
const { queryByText } = render( <Setup { ...defaultProps } /> );
|
||||
|
||||
expect(
|
||||
queryByText( 'Configure your Mock Gateway account' )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should not show install step when no plugins are needed', () => {
|
||||
const { queryByText } = render( <Setup { ...defaultProps } /> );
|
||||
|
||||
expect( queryByText( 'Install' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should show install step when plugins are needed', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateway: { ...mockGateway, plugins: [ 'mock-plugin' ] },
|
||||
};
|
||||
|
||||
const { queryByText } = render( <Setup { ...props } /> );
|
||||
|
||||
expect( queryByText( 'Install Mock Gateway' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should enqueue post install scripts when plugin installation completes', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
paymentGateway: {
|
||||
...mockGateway,
|
||||
postInstallScripts: [ 'mock-post-install-script' ],
|
||||
},
|
||||
};
|
||||
await act( async () => {
|
||||
render( <Setup { ...props } /> );
|
||||
} );
|
||||
|
||||
expect( enqueueScript ).toHaveBeenCalledTimes( 1 );
|
||||
expect( enqueueScript ).toHaveBeenCalledWith(
|
||||
'mock-post-install-script'
|
||||
);
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
import { Link, Pill } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
import {
|
||||
WCPayCard,
|
||||
WCPayCardHeader,
|
||||
WCPayCardFooter,
|
||||
WCPayCardBody,
|
||||
SetupRequired,
|
||||
} from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
import { Action } from '../Action';
|
||||
|
||||
const TosPrompt = () =>
|
||||
interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Upon clicking "Get started", you agree to the {{link}}Terms of service{{/link}}. Next we’ll ask you to share a few details about your business to create your account.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ 'https://wordpress.com/tos/' }
|
||||
target="_blank"
|
||||
type="external"
|
||||
/>
|
||||
),
|
||||
},
|
||||
} );
|
||||
|
||||
export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => {
|
||||
const {
|
||||
description,
|
||||
id,
|
||||
needsSetup,
|
||||
installed,
|
||||
enabled: isEnabled,
|
||||
installed: isInstalled,
|
||||
} = paymentGateway;
|
||||
|
||||
return (
|
||||
<WCPayCard>
|
||||
<WCPayCardHeader>
|
||||
{ installed && needsSetup ? (
|
||||
<SetupRequired />
|
||||
) : (
|
||||
<Pill>{ __( 'Recommended', 'woocommerce-admin' ) }</Pill>
|
||||
) }
|
||||
</WCPayCardHeader>
|
||||
|
||||
<WCPayCardBody
|
||||
description={ description }
|
||||
onLinkClick={ () => {
|
||||
recordEvent( 'tasklist_payment_learn_more' );
|
||||
} }
|
||||
/>
|
||||
|
||||
<WCPayCardFooter>
|
||||
<>
|
||||
<Text lineHeight="1.5em">
|
||||
<TosPrompt />
|
||||
</Text>
|
||||
<Action
|
||||
id={ id }
|
||||
hasSetup={ true }
|
||||
needsSetup={ needsSetup }
|
||||
isEnabled={ isEnabled }
|
||||
isRecommended={ true }
|
||||
isInstalled={ isInstalled }
|
||||
hasPlugins={ true }
|
||||
setupButtonText={ __(
|
||||
'Get started',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
onSetupCallback={ onSetupCallback }
|
||||
/>
|
||||
</>
|
||||
</WCPayCardFooter>
|
||||
</WCPayCard>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { getQuery, updateQueryString } from '@woocommerce/navigation';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
import { Link } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Modal from '~/profile-wizard/steps/usage-modal';
|
||||
|
||||
export const UsageModal = () => {
|
||||
const query = getQuery();
|
||||
const shouldDisplayModal = query[ 'wcpay-connection-success' ] === '1';
|
||||
const [ isOpen, setIsOpen ] = useState( shouldDisplayModal );
|
||||
|
||||
if ( ! isOpen ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen( false );
|
||||
updateQueryString( { 'wcpay-connection-success': undefined } );
|
||||
};
|
||||
|
||||
const title = __(
|
||||
'Help us build a better WooCommerce Payments experience',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
const trackingMessage = interpolateComponents( {
|
||||
mixedString: __(
|
||||
'By agreeing to share non-sensitive {{link}}usage data{{/link}}, you’ll help us improve features and optimize the WooCommerce Payments experience. You can opt out at any time.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href="https://woocommerce.com/usage-tracking?utm_medium=product"
|
||||
target="_blank"
|
||||
type="external"
|
||||
/>
|
||||
),
|
||||
},
|
||||
} );
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isDismissible={ false }
|
||||
title={ title }
|
||||
message={ trackingMessage }
|
||||
acceptActionText={ __( 'I agree', 'woocommerce-admin' ) }
|
||||
dismissActionText={ __( 'No thanks', 'woocommerce-admin' ) }
|
||||
onContinue={ closeModal }
|
||||
onClose={ closeModal }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsageModal;
|
|
@ -0,0 +1,3 @@
|
|||
export * from './utils';
|
||||
export { Suggestion as WCPaySuggestion } from './Suggestion';
|
||||
export { UsageModal as WCPayUsageModal } from './UsageModal';
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { WC_ADMIN_NAMESPACE } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { createNoticesFromResponse } from '~/lib/notices';
|
||||
|
||||
export function installActivateAndConnectWcpay(
|
||||
reject,
|
||||
createNotice,
|
||||
installAndActivatePlugins
|
||||
) {
|
||||
const errorMessage = __(
|
||||
'There was an error connecting to WooCommerce Payments. Please try again or connect later in store settings.',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
|
||||
const connect = () => {
|
||||
apiFetch( {
|
||||
path: WC_ADMIN_NAMESPACE + '/plugins/connect-wcpay',
|
||||
method: 'POST',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
window.location = response.connectUrl;
|
||||
} )
|
||||
.catch( () => {
|
||||
createNotice( 'error', errorMessage );
|
||||
reject();
|
||||
} );
|
||||
};
|
||||
|
||||
installAndActivatePlugins( [ 'woocommerce-payments' ] )
|
||||
.then( () => {
|
||||
recordEvent( 'woocommerce_payments_install', {
|
||||
context: 'tasklist',
|
||||
} );
|
||||
|
||||
connect();
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
createNoticesFromResponse( error );
|
||||
reject();
|
||||
} );
|
||||
}
|
||||
|
||||
export function isWCPaySupported( countryCode ) {
|
||||
const supportedCountries = [
|
||||
'US',
|
||||
'PR',
|
||||
'AU',
|
||||
'CA',
|
||||
'DE',
|
||||
'ES',
|
||||
'FR',
|
||||
'GB',
|
||||
'IE',
|
||||
'IT',
|
||||
'NZ',
|
||||
'AT',
|
||||
'BE',
|
||||
'NL',
|
||||
'PL',
|
||||
'PT',
|
||||
'CH',
|
||||
'HK',
|
||||
'SG',
|
||||
];
|
||||
|
||||
return supportedCountries.includes( countryCode );
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||
import {
|
||||
OPTIONS_STORE_NAME,
|
||||
ONBOARDING_STORE_NAME,
|
||||
PAYMENT_GATEWAYS_STORE_NAME,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useMemo, useCallback, useEffect } from '@wordpress/element';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { List, Placeholder as ListPlaceholder } from './components/List';
|
||||
import { Setup, Placeholder as SetupPlaceholder } from './components/Setup';
|
||||
import { WCPaySuggestion } from './components/WCPay';
|
||||
import './plugins/Bacs';
|
||||
|
||||
export const PaymentGatewaySuggestions = ( { query } ) => {
|
||||
const { invalidateResolutionForStoreSelector } = useDispatch(
|
||||
ONBOARDING_STORE_NAME
|
||||
);
|
||||
const { updatePaymentGateway } = useDispatch( PAYMENT_GATEWAYS_STORE_NAME );
|
||||
const {
|
||||
getPaymentGateway,
|
||||
paymentGatewaySuggestions,
|
||||
installedPaymentGateways,
|
||||
isResolving,
|
||||
} = useSelect( ( select ) => {
|
||||
return {
|
||||
getPaymentGateway: select( PAYMENT_GATEWAYS_STORE_NAME )
|
||||
.getPaymentGateway,
|
||||
getOption: select( OPTIONS_STORE_NAME ).getOption,
|
||||
installedPaymentGateways: select(
|
||||
PAYMENT_GATEWAYS_STORE_NAME
|
||||
).getPaymentGateways(),
|
||||
isResolving: select( ONBOARDING_STORE_NAME ).isResolving(
|
||||
'getPaymentGatewaySuggestions'
|
||||
),
|
||||
paymentGatewaySuggestions: select(
|
||||
ONBOARDING_STORE_NAME
|
||||
).getPaymentGatewaySuggestions(),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const getEnrichedPaymentGateways = () => {
|
||||
const mappedPaymentGateways = installedPaymentGateways.reduce(
|
||||
( map, gateway ) => {
|
||||
map[ gateway.id ] = gateway;
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return paymentGatewaySuggestions.reduce( ( map, suggestion ) => {
|
||||
const { id } = suggestion;
|
||||
const installedGateway = mappedPaymentGateways[ suggestion.id ]
|
||||
? mappedPaymentGateways[ id ]
|
||||
: {};
|
||||
|
||||
const enrichedSuggestion = {
|
||||
installed: !! mappedPaymentGateways[ id ],
|
||||
postInstallScripts: installedGateway.post_install_scripts,
|
||||
enabled: installedGateway.enabled || false,
|
||||
needsSetup: installedGateway.needs_setup,
|
||||
settingsUrl: installedGateway.settings_url,
|
||||
connectionUrl: installedGateway.connection_url,
|
||||
setupHelpText: installedGateway.setup_help_text,
|
||||
title: installedGateway.title,
|
||||
requiredSettings: installedGateway.required_settings_keys
|
||||
? installedGateway.required_settings_keys
|
||||
.map(
|
||||
( settingKey ) =>
|
||||
installedGateway.settings[ settingKey ]
|
||||
)
|
||||
.filter( Boolean )
|
||||
: [],
|
||||
...suggestion,
|
||||
};
|
||||
|
||||
map.set( id, enrichedSuggestion );
|
||||
return map;
|
||||
}, new Map() );
|
||||
};
|
||||
|
||||
const paymentGateways = useMemo( getEnrichedPaymentGateways, [
|
||||
installedPaymentGateways,
|
||||
paymentGatewaySuggestions,
|
||||
] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( paymentGateways.size ) {
|
||||
recordEvent( 'tasklist_payments_options', {
|
||||
options: Array.from( paymentGateways.values() ).map(
|
||||
( gateway ) => gateway.id
|
||||
),
|
||||
} );
|
||||
}
|
||||
}, [ paymentGateways ] );
|
||||
|
||||
const enablePaymentGateway = ( id ) => {
|
||||
if ( ! id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gateway = getPaymentGateway( id );
|
||||
|
||||
if ( ! gateway || gateway.enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePaymentGateway( id, {
|
||||
enabled: true,
|
||||
} ).then( () => {
|
||||
invalidateResolutionForStoreSelector( 'getTasksStatus' );
|
||||
} );
|
||||
};
|
||||
|
||||
const markConfigured = useCallback(
|
||||
async ( id, queryParams = {} ) => {
|
||||
if ( ! paymentGateways.get( id ) ) {
|
||||
throw `Payment gateway ${ id } not found in available gateways list`;
|
||||
}
|
||||
|
||||
enablePaymentGateway( id );
|
||||
|
||||
recordEvent( 'tasklist_payment_connect_method', {
|
||||
payment_method: id,
|
||||
} );
|
||||
|
||||
getHistory().push( getNewPath( { ...queryParams }, '/', {} ) );
|
||||
},
|
||||
[ paymentGateways ]
|
||||
);
|
||||
|
||||
const recommendation = useMemo(
|
||||
() =>
|
||||
Array.from( paymentGateways.values() )
|
||||
.filter( ( gateway ) => gateway.recommendation_priority )
|
||||
.sort(
|
||||
( a, b ) =>
|
||||
a.recommendation_priority - b.recommendation_priority
|
||||
)
|
||||
.map( ( gateway ) => gateway.id )
|
||||
.shift(),
|
||||
[ paymentGateways ]
|
||||
);
|
||||
|
||||
const currentGateway = useMemo( () => {
|
||||
if ( ! query.id || isResolving || ! paymentGateways.size ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const gateway = paymentGateways.get( query.id );
|
||||
|
||||
if ( ! gateway ) {
|
||||
throw `Current gateway ${ query.id } not found in available gateways list`;
|
||||
}
|
||||
|
||||
return gateway;
|
||||
}, [ isResolving, query, paymentGateways ] );
|
||||
|
||||
const [ wcPayGateway, enabledGateways, additionalGateways ] = useMemo(
|
||||
() =>
|
||||
Array.from( paymentGateways.values() ).reduce(
|
||||
( all, gateway ) => {
|
||||
const [ wcPay, enabled, additional ] = all;
|
||||
|
||||
// WCPay is handled separately when not installed and configured
|
||||
if (
|
||||
gateway.id === 'woocommerce_payments' &&
|
||||
! ( gateway.installed && ! gateway.needsSetup )
|
||||
) {
|
||||
wcPay.push( gateway );
|
||||
} else if ( gateway.enabled ) {
|
||||
enabled.push( gateway );
|
||||
} else {
|
||||
additional.push( gateway );
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
[ [], [], [] ]
|
||||
),
|
||||
[ paymentGateways ]
|
||||
);
|
||||
|
||||
if ( query.id && ! currentGateway ) {
|
||||
return <SetupPlaceholder />;
|
||||
}
|
||||
|
||||
if ( currentGateway ) {
|
||||
return (
|
||||
<Setup
|
||||
paymentGateway={ currentGateway }
|
||||
markConfigured={ markConfigured }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-task-payments">
|
||||
{ ! paymentGateways.size && <ListPlaceholder /> }
|
||||
|
||||
{ !! wcPayGateway.length && (
|
||||
<WCPaySuggestion paymentGateway={ wcPayGateway[ 0 ] } />
|
||||
) }
|
||||
|
||||
{ !! enabledGateways.length && (
|
||||
<List
|
||||
heading={ __(
|
||||
'Enabled payment gateways',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
recommendation={ recommendation }
|
||||
paymentGateways={ enabledGateways }
|
||||
/>
|
||||
) }
|
||||
|
||||
{ !! additionalGateways.length && (
|
||||
<List
|
||||
heading={ __(
|
||||
'Additional payment gateways',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
recommendation={ recommendation }
|
||||
paymentGateways={ additionalGateways }
|
||||
markConfigured={ markConfigured }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-payments', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="payments">
|
||||
{ ( { query } ) => <PaymentGatewaySuggestions query={ query } /> }
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Form, H, TextControl } from '@woocommerce/components';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { WooPaymentGatewaySetup } from '@woocommerce/onboarding';
|
||||
|
||||
const initialFormValues = {
|
||||
account_name: '',
|
||||
account_number: '',
|
||||
bank_name: '',
|
||||
sort_code: '',
|
||||
iban: '',
|
||||
bic: '',
|
||||
};
|
||||
|
||||
const BacsPaymentGatewaySetup = () => {
|
||||
const isUpdating = useSelect( ( select ) => {
|
||||
return select( OPTIONS_STORE_NAME ).isOptionsUpdating();
|
||||
} );
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
|
||||
const validate = ( values ) => {
|
||||
const errors = {};
|
||||
|
||||
if ( ! values.account_number && ! values.iban ) {
|
||||
errors.account_number = errors.iban = __(
|
||||
'Please enter an account number or IBAN',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const updateSettings = async ( values, markConfigured ) => {
|
||||
const update = await updateOptions( {
|
||||
woocommerce_bacs_settings: {
|
||||
enabled: 'yes',
|
||||
},
|
||||
woocommerce_bacs_accounts: [ values ],
|
||||
} );
|
||||
|
||||
if ( update.success ) {
|
||||
markConfigured();
|
||||
createNotice(
|
||||
'success',
|
||||
__(
|
||||
'Direct bank transfer details added successfully',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was a problem saving your payment settings',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WooPaymentGatewaySetup id="bacs">
|
||||
{ ( { markConfigured } ) => {
|
||||
return (
|
||||
<Form
|
||||
initialValues={ initialFormValues }
|
||||
onSubmit={ ( values ) =>
|
||||
updateSettings( values, markConfigured )
|
||||
}
|
||||
validate={ validate }
|
||||
>
|
||||
{ ( { getInputProps, handleSubmit } ) => {
|
||||
return (
|
||||
<>
|
||||
<H>
|
||||
{ __(
|
||||
'Add your bank details',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</H>
|
||||
<p>
|
||||
{ __(
|
||||
'These details are required to receive payments via bank transfer',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</p>
|
||||
<div className="woocommerce-task-payment-method__fields">
|
||||
<TextControl
|
||||
label={ __(
|
||||
'Account name',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps(
|
||||
'account_name'
|
||||
) }
|
||||
/>
|
||||
<TextControl
|
||||
label={ __(
|
||||
'Account number',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps(
|
||||
'account_number'
|
||||
) }
|
||||
/>
|
||||
<TextControl
|
||||
label={ __(
|
||||
'Bank name',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps(
|
||||
'bank_name'
|
||||
) }
|
||||
/>
|
||||
<TextControl
|
||||
label={ __(
|
||||
'Sort code',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps(
|
||||
'sort_code'
|
||||
) }
|
||||
/>
|
||||
<TextControl
|
||||
label={ __(
|
||||
'IBAN',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps( 'iban' ) }
|
||||
/>
|
||||
<TextControl
|
||||
label={ __(
|
||||
'BIC / Swift',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps( 'bic' ) }
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isPrimary
|
||||
isBusy={ isUpdating }
|
||||
onClick={ handleSubmit }
|
||||
>
|
||||
{ __(
|
||||
'Save',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} }
|
||||
</Form>
|
||||
);
|
||||
} }
|
||||
</WooPaymentGatewaySetup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-payment-gateway-setup-bacs', {
|
||||
render: BacsPaymentGatewaySetup,
|
||||
scope: 'woocommerce-tasks',
|
||||
} );
|
|
@ -0,0 +1,455 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { Button, Card, CardBody } from '@wordpress/components';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { filter } from 'lodash';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
|
||||
import { Stepper, TextControl, ImageUpload } from '@woocommerce/components';
|
||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||
import {
|
||||
OPTIONS_STORE_NAME,
|
||||
ONBOARDING_STORE_NAME,
|
||||
WC_ADMIN_NAMESPACE,
|
||||
} from '@woocommerce/data';
|
||||
import { queueRecordEvent, recordEvent } from '@woocommerce/tracks';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
class Appearance extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
const { hasHomepage, hasProducts } = props.tasksStatus;
|
||||
|
||||
this.stepVisibility = {
|
||||
homepage: ! hasHomepage,
|
||||
import: ! hasProducts,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
isDirty: false,
|
||||
isPending: false,
|
||||
logo: null,
|
||||
stepIndex: 0,
|
||||
isUpdatingLogo: false,
|
||||
isUpdatingNotice: false,
|
||||
storeNoticeText: props.demoStoreNotice || '',
|
||||
};
|
||||
|
||||
this.completeStep = this.completeStep.bind( this );
|
||||
this.createHomepage = this.createHomepage.bind( this );
|
||||
this.importProducts = this.importProducts.bind( this );
|
||||
this.updateLogo = this.updateLogo.bind( this );
|
||||
this.updateNotice = this.updateNotice.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { themeMods } = this.props.tasksStatus;
|
||||
|
||||
if ( themeMods && themeMods.custom_logo ) {
|
||||
/* eslint-disable react/no-did-mount-set-state */
|
||||
this.setState( { logo: { id: themeMods.custom_logo } } );
|
||||
/* eslint-enable react/no-did-mount-set-state */
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
const { isPending, logo } = this.state;
|
||||
const { demoStoreNotice } = this.props;
|
||||
|
||||
if ( logo && ! logo.url && ! isPending ) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( { isPending: true } );
|
||||
wp.media
|
||||
.attachment( logo.id )
|
||||
.fetch()
|
||||
.then( () => {
|
||||
const logoUrl = wp.media.attachment( logo.id ).get( 'url' );
|
||||
this.setState( {
|
||||
isPending: false,
|
||||
logo: { id: logo.id, url: logoUrl },
|
||||
} );
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
|
||||
if (
|
||||
demoStoreNotice &&
|
||||
prevProps.demoStoreNotice !== demoStoreNotice
|
||||
) {
|
||||
/* eslint-disable react/no-did-update-set-state */
|
||||
this.setState( {
|
||||
storeNoticeText: demoStoreNotice,
|
||||
} );
|
||||
/* eslint-enable react/no-did-update-set-state */
|
||||
}
|
||||
}
|
||||
|
||||
completeStep() {
|
||||
const { stepIndex } = this.state;
|
||||
const nextStep = this.getSteps()[ stepIndex + 1 ];
|
||||
|
||||
if ( nextStep ) {
|
||||
this.setState( { stepIndex: stepIndex + 1 } );
|
||||
} else {
|
||||
getHistory().push( getNewPath( {}, '/', {} ) );
|
||||
}
|
||||
}
|
||||
|
||||
importProducts() {
|
||||
const { clearTaskStatusCache, createNotice } = this.props;
|
||||
this.setState( { isPending: true } );
|
||||
|
||||
recordEvent( 'tasklist_appearance_import_demo', {} );
|
||||
|
||||
apiFetch( {
|
||||
path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/import_sample_products`,
|
||||
method: 'POST',
|
||||
} )
|
||||
.then( ( result ) => {
|
||||
if ( result.failed && result.failed.length ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was an error importing some of the sample products',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
createNotice(
|
||||
'success',
|
||||
__(
|
||||
'All sample products have been imported',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
clearTaskStatusCache();
|
||||
}
|
||||
|
||||
this.setState( { isPending: false } );
|
||||
this.completeStep();
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
createNotice( 'error', error.message );
|
||||
this.setState( { isPending: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
createHomepage() {
|
||||
const { clearTaskStatusCache, createNotice } = this.props;
|
||||
this.setState( { isPending: true } );
|
||||
|
||||
recordEvent( 'tasklist_appearance_create_homepage', {
|
||||
create_homepage: true,
|
||||
} );
|
||||
|
||||
apiFetch( {
|
||||
path: '/wc-admin/onboarding/tasks/create_homepage',
|
||||
method: 'POST',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
clearTaskStatusCache();
|
||||
createNotice( response.status, response.message, {
|
||||
actions: response.edit_post_link
|
||||
? [
|
||||
{
|
||||
label: __(
|
||||
'Customize',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
onClick: () => {
|
||||
queueRecordEvent(
|
||||
'tasklist_appearance_customize_homepage',
|
||||
{}
|
||||
);
|
||||
window.location = `${ response.edit_post_link }&wc_onboarding_active_task=homepage`;
|
||||
},
|
||||
},
|
||||
]
|
||||
: null,
|
||||
} );
|
||||
|
||||
this.setState( { isPending: false } );
|
||||
this.completeStep();
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
createNotice( 'error', error.message );
|
||||
this.setState( { isPending: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
async updateLogo() {
|
||||
const {
|
||||
clearTaskStatusCache,
|
||||
createNotice,
|
||||
stylesheet,
|
||||
themeMods,
|
||||
updateOptions,
|
||||
} = this.props;
|
||||
const { logo } = this.state;
|
||||
const updatedThemeMods = {
|
||||
...themeMods,
|
||||
custom_logo: logo ? logo.id : null,
|
||||
};
|
||||
|
||||
recordEvent( 'tasklist_appearance_upload_logo' );
|
||||
|
||||
this.setState( { isUpdatingLogo: true } );
|
||||
const update = await updateOptions( {
|
||||
[ `theme_mods_${ stylesheet }` ]: updatedThemeMods,
|
||||
} );
|
||||
|
||||
clearTaskStatusCache();
|
||||
|
||||
if ( update.success ) {
|
||||
this.setState( { isUpdatingLogo: false } );
|
||||
createNotice(
|
||||
'success',
|
||||
__( 'Store logo updated sucessfully', 'woocommerce-admin' )
|
||||
);
|
||||
this.completeStep();
|
||||
} else {
|
||||
createNotice( 'error', update.message );
|
||||
}
|
||||
}
|
||||
|
||||
async updateNotice() {
|
||||
const {
|
||||
clearTaskStatusCache,
|
||||
createNotice,
|
||||
updateOptions,
|
||||
} = this.props;
|
||||
const { storeNoticeText } = this.state;
|
||||
|
||||
recordEvent( 'tasklist_appearance_set_store_notice', {
|
||||
added_text: Boolean( storeNoticeText.length ),
|
||||
} );
|
||||
|
||||
this.setState( { isUpdatingNotice: true } );
|
||||
const update = await updateOptions( {
|
||||
woocommerce_task_list_appearance_complete: true,
|
||||
woocommerce_demo_store: storeNoticeText.length ? 'yes' : 'no',
|
||||
woocommerce_demo_store_notice: storeNoticeText,
|
||||
} );
|
||||
|
||||
clearTaskStatusCache();
|
||||
|
||||
if ( update.success ) {
|
||||
this.setState( { isUpdatingNotice: false } );
|
||||
createNotice(
|
||||
'success',
|
||||
__(
|
||||
"🎨 Your store is looking great! Don't forget to continue personalizing it",
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
this.completeStep();
|
||||
} else {
|
||||
createNotice( 'error', update.message );
|
||||
}
|
||||
}
|
||||
|
||||
getSteps() {
|
||||
const {
|
||||
isDirty,
|
||||
isPending,
|
||||
logo,
|
||||
storeNoticeText,
|
||||
isUpdatingLogo,
|
||||
} = this.state;
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: 'import',
|
||||
label: __( 'Import sample products', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'We’ll add some products that will make it easier to see what your store looks like',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<Fragment>
|
||||
<Button
|
||||
onClick={ this.importProducts }
|
||||
isBusy={ isPending }
|
||||
isPrimary
|
||||
>
|
||||
{ __( 'Import products', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button onClick={ () => this.completeStep() }>
|
||||
{ __( 'Skip', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
),
|
||||
visible: this.stepVisibility.import,
|
||||
},
|
||||
{
|
||||
key: 'homepage',
|
||||
label: __( 'Create a custom homepage', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'Create a new homepage and customize it to suit your needs',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<Fragment>
|
||||
<Button
|
||||
isPrimary
|
||||
isBusy={ isPending }
|
||||
onClick={ this.createHomepage }
|
||||
>
|
||||
{ __( 'Create homepage', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isTertiary
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'tasklist_appearance_create_homepage',
|
||||
{ create_homepage: false }
|
||||
);
|
||||
this.completeStep();
|
||||
} }
|
||||
>
|
||||
{ __( 'Skip', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
),
|
||||
visible: this.stepVisibility.homepage,
|
||||
},
|
||||
{
|
||||
key: 'logo',
|
||||
label: __( 'Upload a logo', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'Ensure your store is on-brand by adding your logo',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: isPending ? null : (
|
||||
<Fragment>
|
||||
<ImageUpload
|
||||
image={ logo }
|
||||
onChange={ ( image ) =>
|
||||
this.setState( { isDirty: true, logo: image } )
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
disabled={ ! logo && ! isDirty }
|
||||
onClick={ this.updateLogo }
|
||||
isBusy={ isUpdatingLogo }
|
||||
isPrimary
|
||||
>
|
||||
{ __( 'Proceed', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isTertiary
|
||||
onClick={ () => this.completeStep() }
|
||||
>
|
||||
{ __( 'Skip', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'notice',
|
||||
label: __( 'Set a store notice', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'Optionally display a prominent notice across all pages of your store',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<Fragment>
|
||||
<TextControl
|
||||
label={ __(
|
||||
'Store notice text',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
placeholder={ __(
|
||||
'Store notice text',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
value={ storeNoticeText }
|
||||
onChange={ ( value ) =>
|
||||
this.setState( { storeNoticeText: value } )
|
||||
}
|
||||
/>
|
||||
<Button onClick={ this.updateNotice } isPrimary>
|
||||
{ __( 'Complete task', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
),
|
||||
visible: true,
|
||||
},
|
||||
];
|
||||
|
||||
return filter( steps, ( step ) => step.visible );
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isPending,
|
||||
stepIndex,
|
||||
isUpdatingLogo,
|
||||
isUpdatingNotice,
|
||||
} = this.state;
|
||||
const currentStep = this.getSteps()[ stepIndex ].key;
|
||||
|
||||
return (
|
||||
<div className="woocommerce-task-appearance">
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardBody>
|
||||
<Stepper
|
||||
isPending={
|
||||
isUpdatingNotice || isUpdatingLogo || isPending
|
||||
}
|
||||
isVertical
|
||||
currentStep={ currentStep }
|
||||
steps={ this.getSteps() }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AppearanceWrapper = compose(
|
||||
withSelect( ( select ) => {
|
||||
const { getOption } = select( OPTIONS_STORE_NAME );
|
||||
const { getTasksStatus } = select( ONBOARDING_STORE_NAME );
|
||||
const tasksStatus = getTasksStatus();
|
||||
|
||||
return {
|
||||
demoStoreNotice: getOption( 'woocommerce_demo_store_notice' ),
|
||||
stylesheet: getOption( 'stylesheet' ),
|
||||
tasksStatus,
|
||||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
const { updateOptions } = dispatch( OPTIONS_STORE_NAME );
|
||||
const { invalidateResolutionForStoreSelector } = dispatch(
|
||||
ONBOARDING_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
clearTaskStatusCache: () =>
|
||||
invalidateResolutionForStoreSelector( 'getTasksStatus' ),
|
||||
createNotice,
|
||||
updateOptions,
|
||||
};
|
||||
} )
|
||||
)( Appearance );
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-appearance', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="appearance">
|
||||
<AppearanceWrapper />
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { omit } from 'lodash';
|
||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||
import { ONBOARDING_STORE_NAME, WC_ADMIN_NAMESPACE } from '@woocommerce/data';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
class Connect extends Component {
|
||||
componentDidMount() {
|
||||
document.body.classList.add( 'woocommerce-admin-is-loading' );
|
||||
const { query } = this.props;
|
||||
|
||||
if ( query.deny === '1' ) {
|
||||
this.errorMessage(
|
||||
__(
|
||||
'You must click approve to install your extensions and connect to WooCommerce.com',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! query[ 'wccom-connected' ] || ! query.request_token ) {
|
||||
this.request();
|
||||
return;
|
||||
}
|
||||
this.finish();
|
||||
}
|
||||
|
||||
baseQuery() {
|
||||
const { query } = this.props;
|
||||
const baseQuery = omit( { ...query, page: 'wc-admin' }, [
|
||||
'task',
|
||||
'wccom-connected',
|
||||
'request_token',
|
||||
'deny',
|
||||
] );
|
||||
return getNewPath( {}, '/', baseQuery );
|
||||
}
|
||||
|
||||
errorMessage(
|
||||
message = __(
|
||||
'There was an error connecting to WooCommerce.com. Please try again',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
) {
|
||||
document.body.classList.remove( 'woocommerce-admin-is-loading' );
|
||||
getHistory().push( this.baseQuery() );
|
||||
this.props.createNotice( 'error', message );
|
||||
}
|
||||
|
||||
async request() {
|
||||
try {
|
||||
const connectResponse = await apiFetch( {
|
||||
path: `${ WC_ADMIN_NAMESPACE }/plugins/request-wccom-connect`,
|
||||
method: 'POST',
|
||||
} );
|
||||
if ( connectResponse && connectResponse.connectAction ) {
|
||||
window.location = connectResponse.connectAction;
|
||||
return;
|
||||
}
|
||||
throw new Error();
|
||||
} catch ( err ) {
|
||||
this.errorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
async finish() {
|
||||
const { query } = this.props;
|
||||
try {
|
||||
const connectResponse = await apiFetch( {
|
||||
path: `${ WC_ADMIN_NAMESPACE }/plugins/finish-wccom-connect`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
request_token: query.request_token,
|
||||
},
|
||||
} );
|
||||
if ( connectResponse && connectResponse.success ) {
|
||||
await this.props.updateProfileItems( {
|
||||
wccom_connected: true,
|
||||
} );
|
||||
if ( ! this.props.isProfileItemsError ) {
|
||||
this.props.createNotice(
|
||||
'success',
|
||||
__(
|
||||
'Store connected to WooCommerce.com and extensions are being installed',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
|
||||
// @todo Show a notice for when extensions are correctly installed.
|
||||
|
||||
document.body.classList.remove(
|
||||
'woocommerce-admin-is-loading'
|
||||
);
|
||||
getHistory().push( this.baseQuery() );
|
||||
} else {
|
||||
this.errorMessage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
throw new Error();
|
||||
} catch ( err ) {
|
||||
this.errorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectWrapper = compose(
|
||||
withSelect( ( select ) => {
|
||||
const { getOnboardingError } = select( ONBOARDING_STORE_NAME );
|
||||
|
||||
const isProfileItemsError = Boolean(
|
||||
getOnboardingError( 'updateProfileItems' )
|
||||
);
|
||||
|
||||
return { isProfileItemsError };
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME );
|
||||
return {
|
||||
createNotice,
|
||||
updateProfileItems,
|
||||
};
|
||||
} )
|
||||
)( Connect );
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-connect', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="connect">
|
||||
<ConnectWrapper />
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './PaymentGatewaySuggestions';
|
||||
import './shipping';
|
||||
import './Marketing';
|
||||
import './products';
|
||||
import './appearance';
|
||||
import './connect';
|
||||
import './tax';
|
||||
import './woocommerce-payments';
|
||||
import './purchase';
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './products';
|
||||
import ProductTemplateModal from './product-template-modal';
|
||||
|
||||
export { ProductTemplateModal };
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Modal, RadioControl } from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import { ITEMS_STORE_NAME } from '@woocommerce/data';
|
||||
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './product-template-modal.scss';
|
||||
import { createNoticesFromResponse } from '../../../lib/notices';
|
||||
|
||||
export const ONBOARDING_PRODUCT_TEMPLATES_FILTER =
|
||||
'woocommerce_admin_onboarding_product_templates';
|
||||
|
||||
const PRODUCT_TEMPLATES = [
|
||||
{
|
||||
key: 'physical',
|
||||
title: __( 'Physical product', 'woocommerce-admin' ),
|
||||
subtitle: __(
|
||||
'Tangible items that get delivered to customers',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'digital',
|
||||
title: __( 'Digital product', 'woocommerce-admin' ),
|
||||
subtitle: __(
|
||||
'Items that customers download or access through your website',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'variable',
|
||||
title: __( 'Variable product', 'woocommerce-admin' ),
|
||||
subtitle: __(
|
||||
'Products with several versions that customers can choose from',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function ProductTemplateModal( { onClose } ) {
|
||||
const [ selectedTemplate, setSelectedTemplate ] = useState( null );
|
||||
const [ isRedirecting, setIsRedirecting ] = useState( false );
|
||||
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );
|
||||
|
||||
const createTemplate = () => {
|
||||
setIsRedirecting( true );
|
||||
recordEvent( 'tasklist_product_template_selection', {
|
||||
product_type: selectedTemplate,
|
||||
} );
|
||||
if ( selectedTemplate ) {
|
||||
createProductFromTemplate(
|
||||
{
|
||||
template_name: selectedTemplate,
|
||||
status: 'draft',
|
||||
},
|
||||
{ _fields: [ 'id' ] }
|
||||
).then(
|
||||
( data ) => {
|
||||
if ( data && data.id ) {
|
||||
const link = getAdminLink(
|
||||
`post.php?post=${ data.id }&action=edit&wc_onboarding_active_task=products&tutorial=true`
|
||||
);
|
||||
window.location = link;
|
||||
}
|
||||
},
|
||||
( error ) => {
|
||||
// failed creating product with template
|
||||
createNoticesFromResponse( error );
|
||||
setIsRedirecting( false );
|
||||
}
|
||||
);
|
||||
} else if ( onClose ) {
|
||||
recordEvent( 'tasklist_product_template_dismiss' );
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const templates = applyFilters(
|
||||
ONBOARDING_PRODUCT_TEMPLATES_FILTER,
|
||||
PRODUCT_TEMPLATES
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Start with a template' ) }
|
||||
isDismissible={ true }
|
||||
onRequestClose={ () => onClose() }
|
||||
className="woocommerce-product-template-modal"
|
||||
>
|
||||
<div className="woocommerce-product-template-modal__wrapper">
|
||||
<div className="woocommerce-product-template-modal__list">
|
||||
<RadioControl
|
||||
selected={ selectedTemplate }
|
||||
options={ templates.map( ( item ) => {
|
||||
return {
|
||||
label: (
|
||||
<>
|
||||
<span className="woocommerce-product-template-modal__list-title">
|
||||
{ item.title }
|
||||
</span>
|
||||
<span className="woocommerce-product-template-modal__list-subtitle">
|
||||
{ item.subtitle }
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
value: item.key,
|
||||
};
|
||||
} ) }
|
||||
onChange={ setSelectedTemplate }
|
||||
/>
|
||||
</div>
|
||||
<div className="woocommerce-product-template-modal__actions">
|
||||
<Button
|
||||
isPrimary
|
||||
isBusy={ isRedirecting }
|
||||
disabled={ ! selectedTemplate || isRedirecting }
|
||||
onClick={ createTemplate }
|
||||
>
|
||||
{ __( 'Go' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
$border-color: $gray-100;
|
||||
|
||||
.woocommerce-product-template-modal {
|
||||
@include breakpoint( '>600px' ) {
|
||||
min-width: 565px;
|
||||
}
|
||||
|
||||
.woocommerce-product-template-modal__actions {
|
||||
padding-top: $gap-large;
|
||||
}
|
||||
}
|
||||
.woocommerce-product-template-modal__list {
|
||||
.components-base-control {
|
||||
margin: 0 -#{$gap-large};
|
||||
.components-base-control__field {
|
||||
.components-radio-control__option {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: $gap $gap-large;
|
||||
margin-bottom: 0;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid $gray-100;
|
||||
&:first-child {
|
||||
border-top: 1px solid $gray-100;
|
||||
}
|
||||
.components-radio-control__input {
|
||||
margin-right: $gap;
|
||||
flex: none;
|
||||
&:checked::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.woocommerce-product-template-modal__list-title {
|
||||
color: $gray-900;
|
||||
display: block;
|
||||
}
|
||||
.woocommerce-product-template-modal__list-subtitle {
|
||||
color: $gray-700;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.woocommerce-product-template-modal__actions {
|
||||
text-align: right;
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Fragment, useState } from '@wordpress/element';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import {
|
||||
Icon,
|
||||
sidebar,
|
||||
chevronRight,
|
||||
plusCircle,
|
||||
archive,
|
||||
download,
|
||||
} from '@wordpress/icons';
|
||||
import { List, Pill } from '@woocommerce/components';
|
||||
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ProductTemplateModal from './product-template-modal';
|
||||
|
||||
const subTasks = [
|
||||
{
|
||||
key: 'addProductTemplate',
|
||||
title: (
|
||||
<>
|
||||
{ __( 'Start with a template', 'woocommerce-admin' ) }
|
||||
<Pill>{ __( 'Recommended', 'woocommerce-admin' ) }</Pill>
|
||||
</>
|
||||
),
|
||||
content: __(
|
||||
'Use a template to add physical, digital, and variable products',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
before: <Icon icon={ sidebar }></Icon>,
|
||||
after: <Icon icon={ chevronRight } />,
|
||||
onClick: () =>
|
||||
recordEvent( 'tasklist_add_product', {
|
||||
method: 'product_template',
|
||||
} ),
|
||||
},
|
||||
{
|
||||
key: 'addProductManually',
|
||||
title: __( 'Add manually', 'woocommerce-admin' ),
|
||||
content: __(
|
||||
'For small stores we recommend adding products manually',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
before: <Icon icon={ plusCircle } />,
|
||||
after: <Icon icon={ chevronRight } />,
|
||||
onClick: () =>
|
||||
recordEvent( 'tasklist_add_product', { method: 'manually' } ),
|
||||
href: getAdminLink(
|
||||
'post-new.php?post_type=product&wc_onboarding_active_task=products&tutorial=true'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'importProducts',
|
||||
title: __( 'Import via CSV', 'woocommerce-admin' ),
|
||||
content: __(
|
||||
'For larger stores we recommend importing all products at once via CSV file',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
before: <Icon icon={ archive } />,
|
||||
after: <Icon icon={ chevronRight } />,
|
||||
onClick: () =>
|
||||
recordEvent( 'tasklist_add_product', { method: 'import' } ),
|
||||
href: getAdminLink(
|
||||
'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=product-import'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'migrateProducts',
|
||||
title: __( 'Import from another service', 'woocommerce-admin' ),
|
||||
content: __(
|
||||
'For stores currently selling elsewhere we suggest using a product migration service',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
before: <Icon icon={ download } />,
|
||||
after: <Icon icon={ chevronRight } />,
|
||||
onClick: () =>
|
||||
recordEvent( 'tasklist_add_product', { method: 'migrate' } ),
|
||||
// @todo This should be replaced with the in-app purchase iframe when ready.
|
||||
href: 'https://woocommerce.com/products/cart2cart/?utm_medium=product',
|
||||
target: '_blank',
|
||||
},
|
||||
];
|
||||
|
||||
const Products = () => {
|
||||
const [ selectTemplate, setSelectTemplate ] = useState( null );
|
||||
|
||||
const onTaskClick = ( task ) => {
|
||||
task.onClick();
|
||||
if ( task.key === 'addProductTemplate' ) {
|
||||
setSelectTemplate( true );
|
||||
}
|
||||
};
|
||||
|
||||
const listItems = subTasks.map( ( task ) => ( {
|
||||
...task,
|
||||
onClick: () => onTaskClick( task ),
|
||||
} ) );
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardBody size={ null }>
|
||||
<List items={ listItems } />
|
||||
</CardBody>
|
||||
</Card>
|
||||
{ selectTemplate ? (
|
||||
<ProductTemplateModal
|
||||
onClose={ () => setSelectTemplate( null ) }
|
||||
/>
|
||||
) : null }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-products', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="products">
|
||||
<Products />
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTaskListItem } from '@woocommerce/onboarding';
|
||||
import { useState, useCallback } from '@wordpress/element';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { ONBOARDING_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CartModal from '../../dashboard/components/cart-modal';
|
||||
import { getCategorizedOnboardingProducts } from '../../dashboard/utils';
|
||||
|
||||
const PurchaseTaskItem = () => {
|
||||
const [ cartModalOpen, setCartModalOpen ] = useState( false );
|
||||
|
||||
const { installedPlugins, profileItems } = useSelect( ( select ) => {
|
||||
const { getProfileItems } = select( ONBOARDING_STORE_NAME );
|
||||
const { getInstalledPlugins } = select( PLUGINS_STORE_NAME );
|
||||
|
||||
return {
|
||||
installedPlugins: getInstalledPlugins(),
|
||||
profileItems: getProfileItems(),
|
||||
};
|
||||
} );
|
||||
|
||||
const toggleCartModal = useCallback( () => {
|
||||
if ( ! cartModalOpen ) {
|
||||
recordEvent( 'tasklist_purchase_extensions' );
|
||||
}
|
||||
|
||||
setCartModalOpen( ! cartModalOpen );
|
||||
}, [ cartModalOpen ] );
|
||||
|
||||
const groupedProducts = getCategorizedOnboardingProducts(
|
||||
profileItems,
|
||||
installedPlugins
|
||||
);
|
||||
const { remainingProducts } = groupedProducts;
|
||||
|
||||
return (
|
||||
<WooOnboardingTaskListItem id="purchase">
|
||||
{ ( { defaultTaskItem: DefaultTaskItem } ) => (
|
||||
<>
|
||||
<DefaultTaskItem
|
||||
onClick={ () => {
|
||||
if ( remainingProducts.length ) {
|
||||
toggleCartModal();
|
||||
}
|
||||
} }
|
||||
/>
|
||||
{ cartModalOpen && (
|
||||
<CartModal
|
||||
onClose={ () => toggleCartModal() }
|
||||
onClickPurchaseLater={ () => toggleCartModal() }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
) }
|
||||
</WooOnboardingTaskListItem>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'woocommerce-admin-task-purchase', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: PurchaseTaskItem,
|
||||
} );
|
|
@ -0,0 +1,383 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { difference, filter } from 'lodash';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { Link, Stepper, Plugins } from '@woocommerce/components';
|
||||
import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings';
|
||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||
import { SETTINGS_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Connect from '../../../dashboard/components/connect';
|
||||
import { getCountryCode } from '../../../dashboard/utils';
|
||||
import StoreLocation from '../steps/location';
|
||||
import ShippingRates from './rates';
|
||||
import { createNoticesFromResponse } from '../../../lib/notices';
|
||||
|
||||
export class Shipping extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.initialState = {
|
||||
isPending: false,
|
||||
step: 'store_location',
|
||||
shippingZones: [],
|
||||
};
|
||||
|
||||
// Cache active plugins to prevent removal mid-step.
|
||||
this.activePlugins = props.activePlugins;
|
||||
this.state = this.initialState;
|
||||
this.completeStep = this.completeStep.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState( this.initialState );
|
||||
}
|
||||
|
||||
async fetchShippingZones() {
|
||||
this.setState( { isPending: true } );
|
||||
const { countryCode, countryName } = this.props;
|
||||
|
||||
// @todo The following fetches for shipping information should be moved into
|
||||
// @woocommerce/data to make these methods and states more readily available.
|
||||
const shippingZones = [];
|
||||
const zones = await apiFetch( { path: '/wc/v3/shipping/zones' } );
|
||||
let hasCountryZone = false;
|
||||
|
||||
await Promise.all(
|
||||
zones.map( async ( zone ) => {
|
||||
// "Rest of the world zone"
|
||||
if ( zone.id === 0 ) {
|
||||
zone.methods = await apiFetch( {
|
||||
path: `/wc/v3/shipping/zones/${ zone.id }/methods`,
|
||||
} );
|
||||
zone.name = __( 'Rest of the world', 'woocommerce-admin' );
|
||||
zone.toggleable = true;
|
||||
shippingZones.push( zone );
|
||||
return;
|
||||
}
|
||||
|
||||
// Return any zone with a single location matching the country zone.
|
||||
zone.locations = await apiFetch( {
|
||||
path: `/wc/v3/shipping/zones/${ zone.id }/locations`,
|
||||
} );
|
||||
const countryLocation = zone.locations.find(
|
||||
( location ) => countryCode === location.code
|
||||
);
|
||||
if ( countryLocation ) {
|
||||
zone.methods = await apiFetch( {
|
||||
path: `/wc/v3/shipping/zones/${ zone.id }/methods`,
|
||||
} );
|
||||
shippingZones.push( zone );
|
||||
hasCountryZone = true;
|
||||
}
|
||||
} )
|
||||
);
|
||||
|
||||
// Create the default store country zone if it doesn't exist.
|
||||
if ( ! hasCountryZone ) {
|
||||
const zone = await apiFetch( {
|
||||
method: 'POST',
|
||||
path: '/wc/v3/shipping/zones',
|
||||
data: { name: countryName },
|
||||
} );
|
||||
zone.locations = await apiFetch( {
|
||||
method: 'POST',
|
||||
path: `/wc/v3/shipping/zones/${ zone.id }/locations`,
|
||||
data: [ { code: countryCode, type: 'country' } ],
|
||||
} );
|
||||
shippingZones.push( zone );
|
||||
}
|
||||
|
||||
shippingZones.reverse();
|
||||
|
||||
this.setState( { isPending: false, shippingZones } );
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps, prevState ) {
|
||||
const { countryCode, settings } = this.props;
|
||||
const {
|
||||
woocommerce_store_address: storeAddress,
|
||||
woocommerce_default_country: defaultCountry,
|
||||
woocommerce_store_postcode: storePostCode,
|
||||
} = settings;
|
||||
const { step } = this.state;
|
||||
|
||||
if (
|
||||
step === 'rates' &&
|
||||
( prevProps.countryCode !== countryCode ||
|
||||
prevState.step !== 'rates' )
|
||||
) {
|
||||
this.fetchShippingZones();
|
||||
}
|
||||
|
||||
const isCompleteAddress = Boolean(
|
||||
storeAddress && defaultCountry && storePostCode
|
||||
);
|
||||
|
||||
if ( step === 'store_location' && isCompleteAddress ) {
|
||||
this.completeStep();
|
||||
}
|
||||
}
|
||||
|
||||
completeStep() {
|
||||
const { createNotice } = this.props;
|
||||
const { step } = this.state;
|
||||
const steps = this.getSteps();
|
||||
const currentStepIndex = steps.findIndex( ( s ) => s.key === step );
|
||||
const nextStep = steps[ currentStepIndex + 1 ];
|
||||
|
||||
if ( nextStep ) {
|
||||
this.setState( { step: nextStep.key } );
|
||||
} else {
|
||||
createNotice(
|
||||
'success',
|
||||
__(
|
||||
"📦 Shipping is done! Don't worry, you can always change it later",
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
getHistory().push( getNewPath( {}, '/', {} ) );
|
||||
}
|
||||
}
|
||||
|
||||
getPluginsToActivate() {
|
||||
const { countryCode } = this.props;
|
||||
|
||||
const plugins = [];
|
||||
if ( [ 'GB', 'CA', 'AU' ].includes( countryCode ) ) {
|
||||
plugins.push( 'woocommerce-shipstation-integration' );
|
||||
} else if ( countryCode === 'US' ) {
|
||||
plugins.push( 'woocommerce-services' );
|
||||
plugins.push( 'jetpack' );
|
||||
}
|
||||
return difference( plugins, this.activePlugins );
|
||||
}
|
||||
|
||||
getSteps() {
|
||||
const { countryCode, isJetpackConnected, settings } = this.props;
|
||||
const pluginsToActivate = this.getPluginsToActivate();
|
||||
const requiresJetpackConnection =
|
||||
! isJetpackConnected && countryCode === 'US';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: 'store_location',
|
||||
label: __( 'Set store location', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'The address from which your business operates',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<StoreLocation
|
||||
{ ...this.props }
|
||||
onComplete={ ( values ) => {
|
||||
const country = getCountryCode(
|
||||
values.countryState
|
||||
);
|
||||
recordEvent( 'tasklist_shipping_set_location', {
|
||||
country,
|
||||
} );
|
||||
this.completeStep();
|
||||
} }
|
||||
/>
|
||||
),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'rates',
|
||||
label: __( 'Set shipping costs', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'Define how much customers pay to ship to different destinations',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<ShippingRates
|
||||
buttonText={
|
||||
pluginsToActivate.length ||
|
||||
requiresJetpackConnection
|
||||
? __( 'Proceed', 'woocommerce-admin' )
|
||||
: __( 'Complete task', 'woocommerce-admin' )
|
||||
}
|
||||
shippingZones={ this.state.shippingZones }
|
||||
onComplete={ this.completeStep }
|
||||
{ ...this.props }
|
||||
/>
|
||||
),
|
||||
visible:
|
||||
settings.woocommerce_ship_to_countries === 'disabled'
|
||||
? false
|
||||
: true,
|
||||
},
|
||||
{
|
||||
key: 'label_printing',
|
||||
label: __(
|
||||
'Enable shipping label printing',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
description: pluginsToActivate.includes(
|
||||
'woocommerce-shipstation-integration'
|
||||
)
|
||||
? interpolateComponents( {
|
||||
mixedString: __(
|
||||
'We recommend using ShipStation to save time at the post office by printing your shipping ' +
|
||||
'labels at home. Try ShipStation free for 30 days. {{link}}Learn more{{/link}}.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href="https://woocommerce.com/products/shipstation-integration?utm_medium=product"
|
||||
target="_blank"
|
||||
type="external"
|
||||
/>
|
||||
),
|
||||
},
|
||||
} )
|
||||
: __(
|
||||
'With WooCommerce Shipping you can save time ' +
|
||||
'by printing your USPS and DHL Express shipping labels at home',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<Plugins
|
||||
onComplete={ ( plugins, response ) => {
|
||||
createNoticesFromResponse( response );
|
||||
recordEvent( 'tasklist_shipping_label_printing', {
|
||||
install: true,
|
||||
plugins_to_activate: pluginsToActivate,
|
||||
} );
|
||||
this.completeStep();
|
||||
} }
|
||||
onError={ ( errors, response ) =>
|
||||
createNoticesFromResponse( response )
|
||||
}
|
||||
onSkip={ () => {
|
||||
recordEvent( 'tasklist_shipping_label_printing', {
|
||||
install: false,
|
||||
plugins_to_activate: pluginsToActivate,
|
||||
} );
|
||||
getHistory().push( getNewPath( {}, '/', {} ) );
|
||||
} }
|
||||
pluginSlugs={ pluginsToActivate }
|
||||
{ ...this.props }
|
||||
/>
|
||||
),
|
||||
visible: pluginsToActivate.length,
|
||||
},
|
||||
{
|
||||
key: 'connect',
|
||||
label: __( 'Connect your store', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'Connect your store to WordPress.com to enable label printing',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<Connect
|
||||
redirectUrl={ getAdminLink(
|
||||
'admin.php?page=wc-admin'
|
||||
) }
|
||||
completeStep={ this.completeStep }
|
||||
{ ...this.props }
|
||||
onConnect={ () => {
|
||||
recordEvent( 'tasklist_shipping_connect_store' );
|
||||
} }
|
||||
/>
|
||||
),
|
||||
visible: requiresJetpackConnection,
|
||||
},
|
||||
];
|
||||
|
||||
return filter( steps, ( step ) => step.visible );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isPending, step } = this.state;
|
||||
const { isUpdateSettingsRequesting } = this.props;
|
||||
|
||||
return (
|
||||
<div className="woocommerce-task-shipping">
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardBody>
|
||||
<Stepper
|
||||
isPending={
|
||||
isPending || isUpdateSettingsRequesting
|
||||
}
|
||||
isVertical
|
||||
currentStep={ step }
|
||||
steps={ this.getSteps() }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ShippingWrapper = compose(
|
||||
withSelect( ( select ) => {
|
||||
const { getSettings, isUpdateSettingsRequesting } = select(
|
||||
SETTINGS_STORE_NAME
|
||||
);
|
||||
const { getActivePlugins, isJetpackConnected } = select(
|
||||
PLUGINS_STORE_NAME
|
||||
);
|
||||
|
||||
const { general: settings = {} } = getSettings( 'general' );
|
||||
const countryCode = getCountryCode(
|
||||
settings.woocommerce_default_country
|
||||
);
|
||||
|
||||
const { countries = [] } = getSetting( 'dataEndpoints', {} );
|
||||
const country = countryCode
|
||||
? countries.find( ( c ) => c.code === countryCode )
|
||||
: null;
|
||||
const countryName = country ? country.name : null;
|
||||
const activePlugins = getActivePlugins();
|
||||
|
||||
return {
|
||||
countryCode,
|
||||
countryName,
|
||||
isUpdateSettingsRequesting: isUpdateSettingsRequesting( 'general' ),
|
||||
settings,
|
||||
activePlugins,
|
||||
isJetpackConnected: isJetpackConnected(),
|
||||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
const { updateAndPersistSettingsForGroup } = dispatch(
|
||||
SETTINGS_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
createNotice,
|
||||
updateAndPersistSettingsForGroup,
|
||||
};
|
||||
} )
|
||||
)( Shipping );
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-shipping', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="shipping">
|
||||
<ShippingWrapper />
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,369 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { Button, FormToggle } from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Flag, Form, TextControlWithAffixes } from '@woocommerce/components';
|
||||
import { ONBOARDING_STORE_NAME } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Icon, globe } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CurrencyContext } from '../../../lib/currency-context';
|
||||
|
||||
class ShippingRates extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.updateShippingZones = this.updateShippingZones.bind( this );
|
||||
}
|
||||
|
||||
getShippingMethods( zone, type = null ) {
|
||||
// Sometimes the wc/v3/shipping/zones response does not include a methods attribute, return early if so.
|
||||
if ( ! zone || ! zone.methods || ! Array.isArray( zone.methods ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! type ) {
|
||||
return zone.methods;
|
||||
}
|
||||
|
||||
return zone.methods
|
||||
? zone.methods.filter( ( method ) => method.method_id === type )
|
||||
: [];
|
||||
}
|
||||
|
||||
disableShippingMethods( zone, methods ) {
|
||||
if ( ! methods.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
methods.forEach( ( method ) => {
|
||||
apiFetch( {
|
||||
method: 'POST',
|
||||
path: `/wc/v3/shipping/zones/${ zone.id }/methods/${ method.instance_id }`,
|
||||
data: {
|
||||
enabled: false,
|
||||
},
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
async updateShippingZones( values ) {
|
||||
const {
|
||||
clearTaskStatusCache,
|
||||
createNotice,
|
||||
shippingZones,
|
||||
} = this.props;
|
||||
|
||||
let restOfTheWorld = false;
|
||||
let shippingCost = false;
|
||||
shippingZones.forEach( ( zone ) => {
|
||||
if ( zone.id === 0 ) {
|
||||
restOfTheWorld =
|
||||
zone.toggleable && values[ `${ zone.id }_enabled` ];
|
||||
} else {
|
||||
shippingCost =
|
||||
values[ `${ zone.id }_rate` ] !== '' &&
|
||||
parseFloat( values[ `${ zone.id }_rate` ] ) !==
|
||||
parseFloat( 0 );
|
||||
}
|
||||
|
||||
const shippingMethods = this.getShippingMethods( zone );
|
||||
const methodType =
|
||||
parseFloat( values[ `${ zone.id }_rate` ] ) === parseFloat( 0 )
|
||||
? 'free_shipping'
|
||||
: 'flat_rate';
|
||||
const shippingMethod = this.getShippingMethods( zone, methodType )
|
||||
.length
|
||||
? this.getShippingMethods( zone, methodType )[ 0 ]
|
||||
: null;
|
||||
|
||||
if ( zone.toggleable && ! values[ `${ zone.id }_enabled` ] ) {
|
||||
// Disable any shipping methods that exist if toggled off.
|
||||
this.disableShippingMethods( zone, shippingMethods );
|
||||
return;
|
||||
} else if ( shippingMethod ) {
|
||||
// Disable all methods except the one being updated.
|
||||
const methodsToDisable = shippingMethods.filter(
|
||||
( method ) =>
|
||||
method.instance_id !== shippingMethod.instance_id
|
||||
);
|
||||
this.disableShippingMethods( zone, methodsToDisable );
|
||||
}
|
||||
|
||||
apiFetch( {
|
||||
method: 'POST',
|
||||
path: shippingMethod
|
||||
? // Update the first existing method if one exists, otherwise create a new one.
|
||||
`/wc/v3/shipping/zones/${ zone.id }/methods/${ shippingMethod.instance_id }`
|
||||
: `/wc/v3/shipping/zones/${ zone.id }/methods`,
|
||||
data: {
|
||||
method_id: methodType,
|
||||
enabled: true,
|
||||
settings: { cost: values[ `${ zone.id }_rate` ] },
|
||||
},
|
||||
} );
|
||||
} );
|
||||
|
||||
recordEvent( 'tasklist_shipping_set_costs', {
|
||||
shipping_cost: shippingCost,
|
||||
rest_world: restOfTheWorld,
|
||||
} );
|
||||
|
||||
clearTaskStatusCache();
|
||||
|
||||
createNotice(
|
||||
'success',
|
||||
__( 'Your shipping rates have been updated', 'woocommerce-admin' )
|
||||
);
|
||||
|
||||
this.props.onComplete();
|
||||
}
|
||||
|
||||
renderInputPrefix() {
|
||||
const { symbolPosition, symbol } = this.context.getCurrencyConfig();
|
||||
if ( symbolPosition.indexOf( 'right' ) === 0 ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="woocommerce-shipping-rate__control-prefix">
|
||||
{ symbol }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderInputSuffix( rate ) {
|
||||
const { symbolPosition, symbol } = this.context.getCurrencyConfig();
|
||||
if ( symbolPosition.indexOf( 'right' ) === 0 ) {
|
||||
return (
|
||||
<span className="woocommerce-shipping-rate__control-suffix">
|
||||
{ symbol }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return parseFloat( rate ) === parseFloat( 0 ) ? (
|
||||
<span className="woocommerce-shipping-rate__control-suffix">
|
||||
{ __( 'Free shipping', 'woocommerce-admin' ) }
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
getFormattedRate( value ) {
|
||||
const { formatDecimalString } = this.context;
|
||||
const currencyString = formatDecimalString( value );
|
||||
if ( ! value.length || ! currencyString.length ) {
|
||||
return formatDecimalString( 0 );
|
||||
}
|
||||
|
||||
return formatDecimalString( value );
|
||||
}
|
||||
|
||||
getInitialValues() {
|
||||
const { formatDecimalString } = this.context;
|
||||
const values = {};
|
||||
|
||||
this.props.shippingZones.forEach( ( zone ) => {
|
||||
const shippingMethods = this.getShippingMethods( zone );
|
||||
const rate =
|
||||
shippingMethods.length && shippingMethods[ 0 ].settings.cost
|
||||
? this.getFormattedRate(
|
||||
shippingMethods[ 0 ].settings.cost.value
|
||||
)
|
||||
: formatDecimalString( 0 );
|
||||
values[ `${ zone.id }_rate` ] = rate;
|
||||
|
||||
if ( shippingMethods.length && shippingMethods[ 0 ].enabled ) {
|
||||
values[ `${ zone.id }_enabled` ] = true;
|
||||
} else {
|
||||
values[ `${ zone.id }_enabled` ] = false;
|
||||
}
|
||||
} );
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
validate( values ) {
|
||||
const errors = {};
|
||||
|
||||
const rates = Object.keys( values ).filter( ( field ) =>
|
||||
field.endsWith( '_rate' )
|
||||
);
|
||||
|
||||
rates.forEach( ( rate ) => {
|
||||
if ( values[ rate ] < 0 ) {
|
||||
errors[ rate ] = __(
|
||||
'Shipping rates can not be negative numbers.',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { buttonText, shippingZones } = this.props;
|
||||
|
||||
if ( ! shippingZones.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
initialValues={ this.getInitialValues() }
|
||||
onSubmit={ this.updateShippingZones }
|
||||
validate={ this.validate }
|
||||
>
|
||||
{ ( {
|
||||
getInputProps,
|
||||
handleSubmit,
|
||||
setTouched,
|
||||
setValue,
|
||||
values,
|
||||
} ) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="woocommerce-shipping-rates">
|
||||
{ shippingZones.map( ( zone ) => (
|
||||
<div
|
||||
className="woocommerce-shipping-rate"
|
||||
key={ zone.id }
|
||||
>
|
||||
<div className="woocommerce-shipping-rate__icon">
|
||||
{ zone.locations ? (
|
||||
zone.locations.map(
|
||||
( location ) => (
|
||||
<Flag
|
||||
size={ 24 }
|
||||
code={
|
||||
location.code
|
||||
}
|
||||
key={
|
||||
location.code
|
||||
}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
// Icon used for zones without locations or "Rest of the world".
|
||||
<Icon icon={ globe } />
|
||||
) }
|
||||
</div>
|
||||
<div className="woocommerce-shipping-rate__main">
|
||||
{ zone.toggleable ? (
|
||||
<label
|
||||
htmlFor={ `woocommerce-shipping-rate__toggle-${ zone.id }` }
|
||||
className="woocommerce-shipping-rate__name"
|
||||
>
|
||||
{ zone.name }
|
||||
<FormToggle
|
||||
id={ `woocommerce-shipping-rate__toggle-${ zone.id }` }
|
||||
{ ...getInputProps(
|
||||
`${ zone.id }_enabled`
|
||||
) }
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<div className="woocommerce-shipping-rate__name">
|
||||
{ zone.name }
|
||||
</div>
|
||||
) }
|
||||
{ ( ! zone.toggleable ||
|
||||
values[
|
||||
`${ zone.id }_enabled`
|
||||
] ) && (
|
||||
<TextControlWithAffixes
|
||||
label={ __(
|
||||
'Shipping cost',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
required
|
||||
{ ...getInputProps(
|
||||
`${ zone.id }_rate`
|
||||
) }
|
||||
onBlur={ () => {
|
||||
setTouched(
|
||||
`${ zone.id }_rate`
|
||||
);
|
||||
setValue(
|
||||
`${ zone.id }_rate`,
|
||||
this.getFormattedRate(
|
||||
values[
|
||||
`${ zone.id }_rate`
|
||||
]
|
||||
)
|
||||
);
|
||||
} }
|
||||
prefix={ this.renderInputPrefix() }
|
||||
suffix={ this.renderInputSuffix(
|
||||
values[
|
||||
`${ zone.id }_rate`
|
||||
]
|
||||
) }
|
||||
className="muriel-input-text woocommerce-shipping-rate__control-wrapper"
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
) ) }
|
||||
</div>
|
||||
|
||||
<Button isPrimary onClick={ handleSubmit }>
|
||||
{ buttonText ||
|
||||
__( 'Update', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
);
|
||||
} }
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShippingRates.propTypes = {
|
||||
/**
|
||||
* Text displayed on the primary button.
|
||||
*/
|
||||
buttonText: PropTypes.string,
|
||||
/**
|
||||
* Function used to mark the step complete.
|
||||
*/
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Function to create a transient notice in the store.
|
||||
*/
|
||||
createNotice: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Array of shipping zones returned from the WC REST API with added
|
||||
* `methods` and `locations` properties appended from separate API calls.
|
||||
*/
|
||||
shippingZones: PropTypes.array,
|
||||
};
|
||||
|
||||
ShippingRates.defaultProps = {
|
||||
shippingZones: [],
|
||||
};
|
||||
|
||||
ShippingRates.contextType = CurrencyContext;
|
||||
|
||||
export default compose(
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { invalidateResolutionForStoreSelector } = dispatch(
|
||||
ONBOARDING_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
clearTaskStatusCache: () =>
|
||||
invalidateResolutionForStoreSelector( 'getTasksStatus' ),
|
||||
};
|
||||
} )
|
||||
)( ShippingRates );
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { Form } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
StoreAddress,
|
||||
validateStoreAddress,
|
||||
} from '../../../dashboard/components/settings/general/store-address';
|
||||
|
||||
export default class StoreLocation extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.onSubmit = this.onSubmit.bind( this );
|
||||
}
|
||||
|
||||
async onSubmit( values ) {
|
||||
const {
|
||||
onComplete,
|
||||
createNotice,
|
||||
isSettingsError,
|
||||
updateAndPersistSettingsForGroup,
|
||||
settings,
|
||||
} = this.props;
|
||||
|
||||
await updateAndPersistSettingsForGroup( 'general', {
|
||||
general: {
|
||||
...settings,
|
||||
woocommerce_store_address: values.addressLine1,
|
||||
woocommerce_store_address_2: values.addressLine2,
|
||||
woocommerce_default_country: values.countryState,
|
||||
woocommerce_store_city: values.city,
|
||||
woocommerce_store_postcode: values.postCode,
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! isSettingsError ) {
|
||||
onComplete( values );
|
||||
} else {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was a problem saving your store location',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getInitialValues() {
|
||||
const { settings } = this.props;
|
||||
|
||||
const {
|
||||
woocommerce_store_address: storeAddress,
|
||||
woocommerce_store_address_2: storeAddress2,
|
||||
woocommerce_store_city: storeCity,
|
||||
woocommerce_default_country: defaultCountry,
|
||||
woocommerce_store_postcode: storePostcode,
|
||||
} = settings;
|
||||
|
||||
return {
|
||||
addressLine1: storeAddress || '',
|
||||
addressLine2: storeAddress2 || '',
|
||||
city: storeCity || '',
|
||||
countryState: defaultCountry || '',
|
||||
postCode: storePostcode || '',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSettingsRequesting } = this.props;
|
||||
|
||||
if ( isSettingsRequesting ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
initialValues={ this.getInitialValues() }
|
||||
onSubmit={ this.onSubmit }
|
||||
validate={ validateStoreAddress }
|
||||
>
|
||||
{ ( { getInputProps, handleSubmit, setValue } ) => (
|
||||
<Fragment>
|
||||
<StoreAddress
|
||||
getInputProps={ getInputProps }
|
||||
setValue={ setValue }
|
||||
/>
|
||||
<Button isPrimary onClick={ handleSubmit }>
|
||||
{ __( 'Continue', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,599 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Card, CardBody } from '@wordpress/components';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { difference, filter } from 'lodash';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { H, Link, Stepper, Plugins, Spinner } from '@woocommerce/components';
|
||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||
import {
|
||||
ONBOARDING_STORE_NAME,
|
||||
OPTIONS_STORE_NAME,
|
||||
PLUGINS_STORE_NAME,
|
||||
SETTINGS_STORE_NAME,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent, queueRecordEvent } from '@woocommerce/tracks';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Connect from '../../dashboard/components/connect';
|
||||
import { createNoticesFromResponse } from '../../lib/notices';
|
||||
import { getCountryCode } from '../../dashboard/utils';
|
||||
import StoreLocation from './steps/location';
|
||||
|
||||
class Tax extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
const { hasCompleteAddress, pluginsToActivate } = props;
|
||||
|
||||
this.initialState = {
|
||||
isPending: false,
|
||||
stepIndex: hasCompleteAddress ? 1 : 0,
|
||||
// Cache the value of pluginsToActivate so that we can
|
||||
// show/hide tasks based on it, but not have them update mid task.
|
||||
cachedPluginsToActivate: pluginsToActivate,
|
||||
};
|
||||
this.state = this.initialState;
|
||||
|
||||
this.completeStep = this.completeStep.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { query } = this.props;
|
||||
const { auto } = query;
|
||||
this.reset();
|
||||
|
||||
if ( auto === 'true' ) {
|
||||
this.enableAutomatedTax();
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState( this.initialState );
|
||||
}
|
||||
|
||||
shouldShowSuccessScreen() {
|
||||
const {
|
||||
isJetpackConnected,
|
||||
hasCompleteAddress,
|
||||
pluginsToActivate,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
hasCompleteAddress &&
|
||||
! pluginsToActivate.length &&
|
||||
isJetpackConnected &&
|
||||
this.isTaxJarSupported()
|
||||
);
|
||||
}
|
||||
|
||||
isTaxJarSupported() {
|
||||
const { countryCode, tasksStatus } = this.props;
|
||||
const {
|
||||
automatedTaxSupportedCountries = [],
|
||||
taxJarActivated,
|
||||
} = tasksStatus;
|
||||
|
||||
return (
|
||||
! taxJarActivated && // WCS integration doesn't work with the official TaxJar plugin.
|
||||
automatedTaxSupportedCountries.includes( countryCode )
|
||||
);
|
||||
}
|
||||
|
||||
completeStep() {
|
||||
const { stepIndex } = this.state;
|
||||
const steps = this.getSteps();
|
||||
const nextStep = steps[ stepIndex + 1 ];
|
||||
|
||||
if ( nextStep ) {
|
||||
this.setState( { stepIndex: stepIndex + 1 } );
|
||||
}
|
||||
}
|
||||
|
||||
async manuallyConfigureTaxRates() {
|
||||
const {
|
||||
generalSettings,
|
||||
updateAndPersistSettingsForGroup,
|
||||
} = this.props;
|
||||
|
||||
if ( generalSettings.woocommerce_calc_taxes !== 'yes' ) {
|
||||
this.setState( { isPending: true } );
|
||||
updateAndPersistSettingsForGroup( 'general', {
|
||||
general: {
|
||||
...generalSettings,
|
||||
woocommerce_calc_taxes: 'yes',
|
||||
},
|
||||
} )
|
||||
.then( () => this.redirectToTaxSettings() )
|
||||
.catch( ( error ) => createNoticesFromResponse( error ) );
|
||||
} else {
|
||||
this.redirectToTaxSettings();
|
||||
}
|
||||
}
|
||||
|
||||
updateAutomatedTax( isEnabling ) {
|
||||
const {
|
||||
clearTaskStatusCache,
|
||||
createNotice,
|
||||
updateAndPersistSettingsForGroup,
|
||||
generalSettings,
|
||||
taxSettings,
|
||||
} = this.props;
|
||||
|
||||
Promise.all( [
|
||||
updateAndPersistSettingsForGroup( 'tax', {
|
||||
tax: {
|
||||
...taxSettings,
|
||||
wc_connect_taxes_enabled: isEnabling ? 'yes' : 'no',
|
||||
},
|
||||
} ),
|
||||
updateAndPersistSettingsForGroup( 'general', {
|
||||
general: {
|
||||
...generalSettings,
|
||||
woocommerce_calc_taxes: 'yes',
|
||||
},
|
||||
} ),
|
||||
] )
|
||||
.then( () => {
|
||||
clearTaskStatusCache();
|
||||
|
||||
if ( isEnabling ) {
|
||||
createNotice(
|
||||
'success',
|
||||
__(
|
||||
"You're awesome! One less item on your to-do list ✅",
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
getHistory().push( getNewPath( {}, '/', {} ) );
|
||||
} else {
|
||||
this.redirectToTaxSettings();
|
||||
}
|
||||
} )
|
||||
.catch( () => {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was a problem updating your tax settings',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
redirectToTaxSettings() {
|
||||
window.location = getAdminLink(
|
||||
'admin.php?page=wc-settings&tab=tax§ion=standard&wc_onboarding_active_task=tax'
|
||||
);
|
||||
}
|
||||
|
||||
doNotChargeSalesTax() {
|
||||
const { updateOptions } = this.props;
|
||||
|
||||
queueRecordEvent( 'tasklist_tax_connect_store', {
|
||||
connect: false,
|
||||
no_tax: true,
|
||||
} );
|
||||
|
||||
updateOptions( {
|
||||
woocommerce_no_sales_tax: true,
|
||||
woocommerce_calc_taxes: 'no',
|
||||
} ).then( () => {
|
||||
window.location = getAdminLink( 'admin.php?page=wc-admin' );
|
||||
} );
|
||||
}
|
||||
|
||||
getSteps() {
|
||||
const {
|
||||
generalSettings,
|
||||
isJetpackConnected,
|
||||
isPending,
|
||||
tosAccepted,
|
||||
updateOptions,
|
||||
} = this.props;
|
||||
const { cachedPluginsToActivate } = this.state;
|
||||
let step2Label, agreementText;
|
||||
|
||||
if ( cachedPluginsToActivate.includes( 'woocommerce-services' ) ) {
|
||||
step2Label = __(
|
||||
'Install Jetpack and WooCommerce Tax',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
agreementText = __(
|
||||
'By installing Jetpack and WooCommerce Tax you agree to the {{link}}Terms of Service{{/link}}.',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
} else {
|
||||
step2Label = __( 'Install Jetpack', 'woocommerce-admin' );
|
||||
agreementText = __(
|
||||
'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.',
|
||||
'woocommerce-admin'
|
||||
);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: 'store_location',
|
||||
label: __( 'Set store location', 'woocommerce-admin' ),
|
||||
description: __(
|
||||
'The address from which your business operates',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<StoreLocation
|
||||
{ ...this.props }
|
||||
onComplete={ ( values ) => {
|
||||
const country = getCountryCode(
|
||||
values.countryState
|
||||
);
|
||||
recordEvent( 'tasklist_tax_set_location', {
|
||||
country,
|
||||
} );
|
||||
this.completeStep();
|
||||
} }
|
||||
isSettingsRequesting={ false }
|
||||
settings={ generalSettings }
|
||||
/>
|
||||
),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
label: step2Label,
|
||||
description: __(
|
||||
'Jetpack and WooCommerce Tax allow you to automate sales tax calculations',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
content: (
|
||||
<Fragment>
|
||||
<Plugins
|
||||
onComplete={ ( plugins, response ) => {
|
||||
createNoticesFromResponse( response );
|
||||
recordEvent(
|
||||
'tasklist_tax_install_extensions',
|
||||
{
|
||||
install_extensions: true,
|
||||
}
|
||||
);
|
||||
updateOptions( {
|
||||
woocommerce_setup_jetpack_opted_in: true,
|
||||
} );
|
||||
this.completeStep();
|
||||
} }
|
||||
onError={ ( errors, response ) =>
|
||||
createNoticesFromResponse( response )
|
||||
}
|
||||
onSkip={ () => {
|
||||
queueRecordEvent(
|
||||
'tasklist_tax_install_extensions',
|
||||
{
|
||||
install_extensions: false,
|
||||
}
|
||||
);
|
||||
this.manuallyConfigureTaxRates();
|
||||
} }
|
||||
skipText={ __(
|
||||
'Set up manually',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
onAbort={ () => this.doNotChargeSalesTax() }
|
||||
abortText={ __(
|
||||
"I don't charge sales tax",
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
/>
|
||||
{ ! tosAccepted && (
|
||||
<Text
|
||||
variant="caption"
|
||||
className="woocommerce-task__caption"
|
||||
size="12"
|
||||
lineHeight="16px"
|
||||
>
|
||||
{ interpolateComponents( {
|
||||
mixedString: agreementText,
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={
|
||||
'https://wordpress.com/tos/'
|
||||
}
|
||||
target="_blank"
|
||||
type="external"
|
||||
/>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
</Text>
|
||||
) }
|
||||
</Fragment>
|
||||
),
|
||||
visible:
|
||||
( cachedPluginsToActivate.length || ! tosAccepted ) &&
|
||||
this.isTaxJarSupported(),
|
||||
},
|
||||
{
|
||||
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
|
||||
{ ...this.props }
|
||||
onConnect={ () => {
|
||||
recordEvent( 'tasklist_tax_connect_store', {
|
||||
connect: true,
|
||||
no_tax: false,
|
||||
} );
|
||||
} }
|
||||
onSkip={ () => {
|
||||
queueRecordEvent( 'tasklist_tax_connect_store', {
|
||||
connect: false,
|
||||
no_tax: false,
|
||||
} );
|
||||
this.manuallyConfigureTaxRates();
|
||||
} }
|
||||
skipText={ __(
|
||||
'Set up tax rates manually',
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
onAbort={ () => this.doNotChargeSalesTax() }
|
||||
abortText={ __(
|
||||
"My business doesn't charge sales tax",
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
/>
|
||||
),
|
||||
visible: ! isJetpackConnected && this.isTaxJarSupported(),
|
||||
},
|
||||
{
|
||||
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: (
|
||||
<Fragment>
|
||||
<Button
|
||||
disabled={ isPending }
|
||||
isPrimary
|
||||
isBusy={ isPending }
|
||||
onClick={ () => {
|
||||
recordEvent( 'tasklist_tax_config_rates' );
|
||||
this.manuallyConfigureTaxRates();
|
||||
} }
|
||||
>
|
||||
{ __( 'Configure', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<p>
|
||||
{ generalSettings.woocommerce_calc_taxes !==
|
||||
'yes' &&
|
||||
interpolateComponents( {
|
||||
mixedString: __(
|
||||
/*eslint-disable max-len*/
|
||||
'By clicking "Configure" you\'re enabling tax rates and calculations. More info {{link}}here{{/link}}.',
|
||||
/*eslint-enable max-len*/
|
||||
'woocommerce-admin'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href="https://docs.woocommerce.com/document/setting-up-taxes-in-woocommerce/?utm_medium=product#section-1"
|
||||
target="_blank"
|
||||
type="external"
|
||||
/>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
</p>
|
||||
</Fragment>
|
||||
),
|
||||
visible: ! this.isTaxJarSupported(),
|
||||
},
|
||||
];
|
||||
|
||||
return filter( steps, ( step ) => step.visible );
|
||||
}
|
||||
|
||||
enableAutomatedTax() {
|
||||
recordEvent( 'tasklist_tax_setup_automated_proceed', {
|
||||
setup_automatically: true,
|
||||
} );
|
||||
this.updateAutomatedTax( true );
|
||||
}
|
||||
|
||||
renderSuccessScreen() {
|
||||
const { isPending } = this.props;
|
||||
|
||||
return (
|
||||
<div className="woocommerce-task-tax__success">
|
||||
<span
|
||||
className="woocommerce-task-tax__success-icon"
|
||||
role="img"
|
||||
aria-labelledby="woocommerce-task-tax__success-message"
|
||||
>
|
||||
🎊
|
||||
</span>
|
||||
<H id="woocommerce-task-tax__success-message">
|
||||
{ __( 'Good news!', 'woocommerce-admin' ) }
|
||||
</H>
|
||||
<p>
|
||||
{ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'{{strong}}Jetpack{{/strong}} and {{strong}}WooCommerce Tax{{/strong}} ' +
|
||||
'can automate your sales tax calculations for you.',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
components: {
|
||||
strong: <strong />,
|
||||
},
|
||||
} ) }
|
||||
</p>
|
||||
<Button
|
||||
disabled={ isPending }
|
||||
isPrimary
|
||||
isBusy={ isPending }
|
||||
onClick={ this.enableAutomatedTax.bind( this ) }
|
||||
>
|
||||
{ __( 'Yes please', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
<Button
|
||||
disabled={ isPending }
|
||||
isTertiary
|
||||
onClick={ () => {
|
||||
recordEvent( 'tasklist_tax_setup_automated_proceed', {
|
||||
setup_automatically: false,
|
||||
} );
|
||||
this.updateAutomatedTax( false );
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
"No thanks, I'll set up manually",
|
||||
'woocommerce-admin'
|
||||
) }
|
||||
</Button>
|
||||
<Button
|
||||
disabled={ isPending }
|
||||
isTertiary
|
||||
onClick={ () => this.doNotChargeSalesTax() }
|
||||
>
|
||||
{ __( "I don't charge sales tax", 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stepIndex } = this.state;
|
||||
const { isPending, isResolving } = this.props;
|
||||
if ( isResolving ) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const step = this.getSteps()[ stepIndex ];
|
||||
|
||||
return (
|
||||
<div className="woocommerce-task-tax">
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardBody>
|
||||
{ this.shouldShowSuccessScreen() ? (
|
||||
this.renderSuccessScreen()
|
||||
) : (
|
||||
<Stepper
|
||||
isPending={ isPending || isResolving }
|
||||
isVertical={ true }
|
||||
currentStep={ step.key }
|
||||
steps={ this.getSteps() }
|
||||
/>
|
||||
) }
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TaxWrapper = compose(
|
||||
withSelect( ( select ) => {
|
||||
const { getSettings, isUpdateSettingsRequesting } = select(
|
||||
SETTINGS_STORE_NAME
|
||||
);
|
||||
const { getOption, hasFinishedResolution } = select(
|
||||
OPTIONS_STORE_NAME
|
||||
);
|
||||
const {
|
||||
getActivePlugins,
|
||||
isJetpackConnected,
|
||||
isPluginsRequesting,
|
||||
} = select( PLUGINS_STORE_NAME );
|
||||
const { getTasksStatus } = select( ONBOARDING_STORE_NAME );
|
||||
|
||||
const { general: generalSettings = {} } = getSettings( 'general' );
|
||||
const countryCode = getCountryCode(
|
||||
generalSettings.woocommerce_default_country
|
||||
);
|
||||
const {
|
||||
woocommerce_store_address: storeAddress,
|
||||
woocommerce_default_country: defaultCountry,
|
||||
woocommerce_store_postcode: storePostCode,
|
||||
} = generalSettings;
|
||||
const hasCompleteAddress = Boolean(
|
||||
storeAddress && defaultCountry && storePostCode
|
||||
);
|
||||
|
||||
const { tax: taxSettings = {} } = getSettings( 'tax' );
|
||||
const activePlugins = getActivePlugins();
|
||||
const pluginsToActivate = difference(
|
||||
[ 'jetpack', 'woocommerce-services' ],
|
||||
activePlugins
|
||||
);
|
||||
const connectOptions = getOption( 'wc_connect_options' ) || {};
|
||||
const jetpackOptIn = getOption( 'woocommerce_setup_jetpack_opted_in' );
|
||||
const tosAccepted = connectOptions.tos_accepted || jetpackOptIn === '1';
|
||||
|
||||
const tasksStatus = getTasksStatus();
|
||||
|
||||
const isPending =
|
||||
isUpdateSettingsRequesting( 'tax' ) ||
|
||||
isUpdateSettingsRequesting( 'general' );
|
||||
const isResolving =
|
||||
isPluginsRequesting( 'getJetpackConnectUrl' ) ||
|
||||
! hasFinishedResolution( 'getOption', [
|
||||
'woocommerce_setup_jetpack_opted_in',
|
||||
] ) ||
|
||||
! hasFinishedResolution( 'getOption', [ 'wc_connect_options' ] ) ||
|
||||
jetpackOptIn === undefined ||
|
||||
connectOptions === undefined;
|
||||
|
||||
return {
|
||||
countryCode,
|
||||
generalSettings,
|
||||
hasCompleteAddress,
|
||||
isJetpackConnected: isJetpackConnected(),
|
||||
isPending,
|
||||
isResolving,
|
||||
pluginsToActivate,
|
||||
tasksStatus,
|
||||
taxSettings,
|
||||
tosAccepted,
|
||||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { createNotice } = dispatch( 'core/notices' );
|
||||
const { updateOptions } = dispatch( OPTIONS_STORE_NAME );
|
||||
const { updateAndPersistSettingsForGroup } = dispatch(
|
||||
SETTINGS_STORE_NAME
|
||||
);
|
||||
const { invalidateResolutionForStoreSelector } = dispatch(
|
||||
ONBOARDING_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
clearTaskStatusCache: () =>
|
||||
invalidateResolutionForStoreSelector( 'getTasksStatus' ),
|
||||
createNotice,
|
||||
updateAndPersistSettingsForGroup,
|
||||
updateOptions,
|
||||
};
|
||||
} )
|
||||
)( Tax );
|
||||
|
||||
registerPlugin( 'wc-admin-onboarding-task-tax', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTask id="tax">
|
||||
{ ( { query } ) => <TaxWrapper query={ query } /> }
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { WooOnboardingTaskListItem } from '@woocommerce/onboarding';
|
||||
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { installActivateAndConnectWcpay } from './PaymentGatewaySuggestions/components/WCPay';
|
||||
|
||||
const WoocommercePaymentsTaskItem = () => {
|
||||
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
return (
|
||||
<WooOnboardingTaskListItem id="woocommerce-payments">
|
||||
{ ( { defaultTaskItem: DefaultTaskItem } ) => (
|
||||
<DefaultTaskItem
|
||||
onClick={ () => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
return installActivateAndConnectWcpay(
|
||||
reject,
|
||||
createNotice,
|
||||
installAndActivatePlugins
|
||||
);
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</WooOnboardingTaskListItem>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'woocommerce-admin-task-wcpay', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: WoocommercePaymentsTaskItem,
|
||||
} );
|
|
@ -2,6 +2,7 @@
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { Tasks } from './tasks';
|
||||
import './fills';
|
||||
|
||||
export * from './placeholder';
|
||||
export default Tasks;
|
||||
|
|
|
@ -13,9 +13,10 @@ import {
|
|||
useUserPreferences,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { TaskItem } from '@woocommerce/experimental';
|
||||
import { TaskItem, useSlot } from '@woocommerce/experimental';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { WooOnboardingTaskListItem } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -36,6 +37,7 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( {
|
|||
task,
|
||||
} ) => {
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const {
|
||||
dismissTask,
|
||||
snoozeTask,
|
||||
|
@ -56,6 +58,9 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( {
|
|||
title,
|
||||
} = task;
|
||||
|
||||
const slot = useSlot( `woocommerce_onboarding_task_list_item_${ id }` );
|
||||
const hasFills = Boolean( slot?.fills?.length );
|
||||
|
||||
const onDismiss = useCallback( () => {
|
||||
dismissTask();
|
||||
createNotice( 'success', __( 'Task dismissed' ), {
|
||||
|
@ -107,7 +112,7 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( {
|
|||
} );
|
||||
};
|
||||
|
||||
const onClick = useCallback( () => {
|
||||
const trackClick = () => {
|
||||
recordEvent( 'tasklist_click', {
|
||||
task_name: id,
|
||||
} );
|
||||
|
@ -115,7 +120,9 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( {
|
|||
if ( ! isComplete ) {
|
||||
updateTrackStartedCount();
|
||||
}
|
||||
};
|
||||
|
||||
const onClickDefault = useCallback( () => {
|
||||
if ( actionUrl ) {
|
||||
if ( actionUrl.startsWith( 'http' ) ) {
|
||||
window.location.href = actionUrl;
|
||||
|
@ -129,24 +136,56 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( {
|
|||
updateQueryString( { task: id } );
|
||||
}, [ id, isComplete, actionUrl ] );
|
||||
|
||||
return (
|
||||
<TaskItem
|
||||
key={ id }
|
||||
title={ title }
|
||||
completed={ isComplete }
|
||||
content={ content }
|
||||
onClick={
|
||||
! isExpandable || isComplete
|
||||
? onClick
|
||||
: () => setExpandedTask( id )
|
||||
}
|
||||
expandable={ isExpandable }
|
||||
expanded={ isExpandable && isExpanded }
|
||||
onDismiss={ isDismissable && onDismiss }
|
||||
remindMeLater={ isSnoozable && onSnooze }
|
||||
time={ time }
|
||||
action={ onClick }
|
||||
actionLabel={ actionLabel }
|
||||
const taskItemProps = {
|
||||
expandable: isExpandable,
|
||||
expanded: isExpandable && isExpanded,
|
||||
completed: isComplete,
|
||||
onSnooze: isSnoozable && onSnooze,
|
||||
onDismiss: isDismissable && onDismiss,
|
||||
};
|
||||
|
||||
const DefaultTaskItem = useCallback(
|
||||
( props ) => {
|
||||
const onClickActions = () => {
|
||||
trackClick();
|
||||
|
||||
if ( props.onClick ) {
|
||||
return props.onClick();
|
||||
}
|
||||
|
||||
return onClickDefault();
|
||||
};
|
||||
return (
|
||||
<TaskItem
|
||||
key={ id }
|
||||
title={ title }
|
||||
content={ content }
|
||||
time={ time }
|
||||
action={ onClickActions }
|
||||
actionLabel={ actionLabel }
|
||||
{ ...taskItemProps }
|
||||
{ ...props }
|
||||
onClick={
|
||||
! isExpandable || isComplete
|
||||
? onClickActions
|
||||
: () => setExpandedTask( id )
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[ id, title, content, time, actionLabel, isExpandable, isComplete ]
|
||||
);
|
||||
|
||||
return hasFills ? (
|
||||
<WooOnboardingTaskListItem.Slot
|
||||
id={ id }
|
||||
fillProps={ {
|
||||
defaultTaskItem: DefaultTaskItem,
|
||||
isComplete,
|
||||
...taskItemProps,
|
||||
} }
|
||||
/>
|
||||
) : (
|
||||
<DefaultTaskItem />
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { WooOnboardingTask } from '@woocommerce/onboarding';
|
||||
|
||||
export type TaskProps = {
|
||||
query: { task: string };
|
||||
};
|
||||
|
||||
export const Task: React.FC< TaskProps > = ( { query } ) => (
|
||||
<div>Task: { query.task }</div>
|
||||
<WooOnboardingTask.Slot id={ query.task } fillProps={ { query } } />
|
||||
);
|
||||
|
|
|
@ -70,6 +70,11 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => {
|
|||
hideTaskList();
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
document.body.classList.add( 'woocommerce-onboarding' );
|
||||
document.body.classList.add( 'woocommerce-task-dashboard__body' );
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
// @todo Update this when all task lists have been hidden or completed.
|
||||
const taskListsFinished = false;
|
||||
|
@ -90,7 +95,11 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => {
|
|||
}
|
||||
|
||||
if ( currentTask ) {
|
||||
return <Task query={ query } />;
|
||||
return (
|
||||
<div className="woocommerce-task-dashboard__container">
|
||||
<Task query={ query } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( isLoadingExperiment ) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Unreleased
|
||||
|
||||
- Renaming remindMeLater() to onSnooze() for consistency. #7616
|
||||
|
||||
# 2.0.3
|
||||
|
||||
- Adjust task-item css class to prevent css conflicts. #7593
|
||||
|
|
|
@ -43,8 +43,8 @@ type TaskItemProps = {
|
|||
onCollapse?: () => void;
|
||||
onDelete?: () => void;
|
||||
onDismiss?: () => void;
|
||||
onSnooze?: () => void;
|
||||
onExpand?: () => void;
|
||||
remindMeLater?: () => void;
|
||||
additionalInfo?: string;
|
||||
time?: string;
|
||||
content: string;
|
||||
|
@ -109,8 +109,8 @@ export const TaskItem: React.FC< TaskItemProps > = ( {
|
|||
onDelete,
|
||||
onCollapse,
|
||||
onDismiss,
|
||||
onSnooze,
|
||||
onExpand,
|
||||
remindMeLater,
|
||||
onClick,
|
||||
additionalInfo,
|
||||
time,
|
||||
|
@ -139,7 +139,7 @@ export const TaskItem: React.FC< TaskItemProps > = ( {
|
|||
}
|
||||
|
||||
const showEllipsisMenu =
|
||||
( ( onDismiss || remindMeLater ) && ! completed ) ||
|
||||
( ( onDismiss || onSnooze ) && ! completed ) ||
|
||||
( onDelete && completed );
|
||||
|
||||
const toggleActionVisibility = () => {
|
||||
|
@ -256,11 +256,11 @@ export const TaskItem: React.FC< TaskItemProps > = ( {
|
|||
{ __( 'Dismiss', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
) }
|
||||
{ remindMeLater && ! completed && (
|
||||
{ onSnooze && ! completed && (
|
||||
<Button
|
||||
onClick={ ( e: React.MouseEvent ) => {
|
||||
e.stopPropagation();
|
||||
remindMeLater();
|
||||
onSnooze();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
|
||||
export const WooOnboardingTask = ( { id, ...props } ) => (
|
||||
<Fill name={ 'woocommerce_onboarding_task_' + id } { ...props } />
|
||||
);
|
||||
|
||||
WooOnboardingTask.Slot = ( { id, fillProps } ) => (
|
||||
<Slot
|
||||
name={ 'woocommerce_onboarding_task_' + id }
|
||||
fillProps={ fillProps }
|
||||
/>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export * from './WooOnboardingTask';
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
|
||||
export const WooOnboardingTaskListItem = ( { id, ...props } ) => (
|
||||
<Fill name={ 'woocommerce_onboarding_task_list_item_' + id } { ...props } />
|
||||
);
|
||||
|
||||
WooOnboardingTaskListItem.Slot = ( { id, fillProps } ) => (
|
||||
<Slot
|
||||
name={ 'woocommerce_onboarding_task_list_item_' + id }
|
||||
fillProps={ fillProps }
|
||||
/>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export * from './WooOnboardingTaskListItem';
|
|
@ -10,3 +10,5 @@ export { default as GooglePay } from './images/cards/googlepay';
|
|||
export { default as WCPayLogo } from './images/wcpay-logo';
|
||||
export { WooPaymentGatewaySetup } from './components/WooPaymentGatewaySetup';
|
||||
export { WooPaymentGatewayConfigure } from './components/WooPaymentGatewayConfigure';
|
||||
export { WooOnboardingTask } from './components/WooOnboardingTask';
|
||||
export { WooOnboardingTaskListItem } from './components/WooOnboardingTaskListItem';
|
||||
|
|
|
@ -102,7 +102,9 @@ class TaskLists {
|
|||
$allowed_product_types = Onboarding::get_allowed_product_types();
|
||||
$purchaseable_products = array();
|
||||
$remaining_products = array();
|
||||
|
||||
foreach ( $product_types as $product_type ) {
|
||||
|
||||
if ( ! isset( $allowed_product_types[ $product_type ]['slug'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
@ -113,6 +115,7 @@ class TaskLists {
|
|||
$remaining_products[] = $allowed_product_types[ $product_type ]['label'];
|
||||
}
|
||||
}
|
||||
|
||||
$business_extensions = isset( $profiler_data['business_extensions'] ) ? $profiler_data['business_extensions'] : array();
|
||||
$product_query = new \WC_Product_Query(
|
||||
array(
|
||||
|
|
Loading…
Reference in New Issue