Add marketing extensions task to task list (https://github.com/woocommerce/woocommerce-admin/pull/7383)
* Add initial plugin list components and marketing task * Add marketing task styles * Fix action button alignment * Only allow specific plugin lists * Add button to activate already installed plugins * Record event when marketing plugin is installed * Update plugin list when plugins are installed or activated * Disable and set buttons as busy when installing/activating * Update data source to use v2 controller * Add changelog entry
This commit is contained in:
parent
f5de7ef892
commit
7167242dfb
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: Add
|
||||||
|
|
||||||
|
Add marketing extensions task to task list
|
|
@ -17,6 +17,7 @@ import { recordEvent } from '@woocommerce/tracks';
|
||||||
*/
|
*/
|
||||||
import Appearance from './tasks/appearance';
|
import Appearance from './tasks/appearance';
|
||||||
import { getCategorizedOnboardingProducts } from '../dashboard/utils';
|
import { getCategorizedOnboardingProducts } from '../dashboard/utils';
|
||||||
|
import { Marketing } from './tasks/Marketing';
|
||||||
import { Products } from './tasks/products';
|
import { Products } from './tasks/products';
|
||||||
import Shipping from './tasks/shipping';
|
import Shipping from './tasks/shipping';
|
||||||
import Tax from './tasks/tax';
|
import Tax from './tasks/tax';
|
||||||
|
@ -328,6 +329,28 @@ export function getAllTasks( {
|
||||||
time: __( '1 minute', 'woocommerce-admin' ),
|
time: __( '1 minute', 'woocommerce-admin' ),
|
||||||
type: 'setup',
|
type: 'setup',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'marketing',
|
||||||
|
title: __( 'Set up marketing tools', 'woocommerce-admin' ),
|
||||||
|
content: __(
|
||||||
|
'Add recommended marketing tools to reach new customers and grow your business',
|
||||||
|
'woocommerce-admin'
|
||||||
|
),
|
||||||
|
container: <Marketing />,
|
||||||
|
onClick: () => {
|
||||||
|
onTaskSelect( 'marketing' );
|
||||||
|
updateQueryString( { task: 'marketing' } );
|
||||||
|
},
|
||||||
|
// @todo This should use the free extensions data store.
|
||||||
|
completed:
|
||||||
|
[].filter( ( plugin ) => installedPlugins.includes( plugin ) )
|
||||||
|
.length > 0,
|
||||||
|
visible:
|
||||||
|
window.wcAdminFeatures &&
|
||||||
|
window.wcAdminFeatures[ 'remote-extensions-list' ],
|
||||||
|
time: __( '1 minute', 'woocommerce-admin' ),
|
||||||
|
type: 'setup',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'appearance',
|
key: 'appearance',
|
||||||
title: __( 'Personalize my store', 'woocommerce-admin' ),
|
title: __( 'Personalize my store', 'woocommerce-admin' ),
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
.woocommerce-task-marketing {
|
||||||
|
.components-card__header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: $gray-900;
|
||||||
|
margin-bottom: $gap-smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
.woocommerce-plugin-list__plugin {
|
||||||
|
display: flex;
|
||||||
|
padding: $gap-large;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-bottom: $gap-small;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $gray-700;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #3c434a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,93 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||||
|
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 ) }
|
||||||
|
>
|
||||||
|
{ __( '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 ) }
|
||||||
|
>
|
||||||
|
{ __( 'Install', 'woocommmerce-admin' ) }
|
||||||
|
</Button>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
.woocommerce-plugin-list__title {
|
||||||
|
padding: $gap-small 30px;
|
||||||
|
background: $gray-200;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
margin-top: 1px;
|
||||||
|
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,175 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
|
import { Card, CardHeader, Spinner } from '@wordpress/components';
|
||||||
|
import { PLUGINS_STORE_NAME, WCDataSelector } from '@woocommerce/data';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { Text } from '@woocommerce/experimental';
|
||||||
|
import { useEffect, useMemo, useState } from '@wordpress/element';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './Marketing.scss';
|
||||||
|
import { createNoticesFromResponse } from '~/lib/notices';
|
||||||
|
import { PluginList, PluginListProps } from './PluginList';
|
||||||
|
import { PluginProps } from './Plugin';
|
||||||
|
|
||||||
|
type ExtensionList = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
plugins: Extension[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Extension = {
|
||||||
|
description: string;
|
||||||
|
key: string;
|
||||||
|
image_url: string;
|
||||||
|
manage_url: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOWED_PLUGIN_LISTS = [ 'reach', 'grow' ];
|
||||||
|
|
||||||
|
export const Marketing: React.FC = () => {
|
||||||
|
const [ fetchedExtensions, setFetchedExtensions ] = useState<
|
||||||
|
ExtensionList[]
|
||||||
|
>( [] );
|
||||||
|
const [ currentPlugin, setCurrentPlugin ] = useState< string | null >(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [ isFetching, setIsFetching ] = useState( true );
|
||||||
|
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
|
||||||
|
const { activePlugins, installedPlugins } = useSelect(
|
||||||
|
( select: WCDataSelector ) => {
|
||||||
|
const { getActivePlugins, getInstalledPlugins } = select(
|
||||||
|
PLUGINS_STORE_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activePlugins: getActivePlugins(),
|
||||||
|
installedPlugins: getInstalledPlugins(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const transformExtensionToPlugin = (
|
||||||
|
extension: Extension
|
||||||
|
): 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
apiFetch( {
|
||||||
|
path: '/wc-admin/onboarding/free-extensions',
|
||||||
|
} )
|
||||||
|
.then( ( results: ExtensionList[] ) => {
|
||||||
|
if ( results?.length ) {
|
||||||
|
setFetchedExtensions( results );
|
||||||
|
}
|
||||||
|
setIsFetching( false );
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
// @todo Handle error checking.
|
||||||
|
setIsFetching( false );
|
||||||
|
} );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
const pluginLists: PluginListProps[] = useMemo( () => {
|
||||||
|
return fetchedExtensions
|
||||||
|
.map( ( list ) => {
|
||||||
|
return {
|
||||||
|
...list,
|
||||||
|
plugins: list.plugins.map( ( extension ) =>
|
||||||
|
transformExtensionToPlugin( extension )
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} )
|
||||||
|
.filter( ( list ) => ALLOWED_PLUGIN_LISTS.includes( list.key ) );
|
||||||
|
}, [ installedPlugins, activePlugins, fetchedExtensions ] );
|
||||||
|
|
||||||
|
const getInstalledMarketingPlugins = () => {
|
||||||
|
const installed: string[] = [];
|
||||||
|
pluginLists.forEach( ( list: PluginListProps ) => {
|
||||||
|
return list.plugins?.forEach( ( plugin ) => {
|
||||||
|
if ( plugin.isInstalled ) {
|
||||||
|
installed.push( plugin.slug );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return installed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installAndActivate = ( slug: string ) => {
|
||||||
|
setCurrentPlugin( slug );
|
||||||
|
installAndActivatePlugins( [ slug ] )
|
||||||
|
.then( ( response: { errors: Record< string, string > } ) => {
|
||||||
|
recordEvent( 'tasklist_marketing_install', {
|
||||||
|
selected_extension: slug,
|
||||||
|
installed_extensions: getInstalledMarketingPlugins(),
|
||||||
|
} );
|
||||||
|
createNoticesFromResponse( response );
|
||||||
|
setCurrentPlugin( null );
|
||||||
|
} )
|
||||||
|
.catch( ( response: { errors: Record< string, string > } ) => {
|
||||||
|
createNoticesFromResponse( response );
|
||||||
|
setCurrentPlugin( null );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( isFetching ) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-task-marketing">
|
||||||
|
<Card className="woocommerce-task-card">
|
||||||
|
<CardHeader>
|
||||||
|
<Text
|
||||||
|
variant="title.small"
|
||||||
|
as="h2"
|
||||||
|
className="woocommerce-task-card__title"
|
||||||
|
>
|
||||||
|
{ __(
|
||||||
|
'Recommended marketing extensions',
|
||||||
|
'woocommerce-admin'
|
||||||
|
) }
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
{ __(
|
||||||
|
'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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -69,4 +69,5 @@ export type PluginSelectors = {
|
||||||
getInstalledPlugins: WPDataSelector< typeof getInstalledPlugins >;
|
getInstalledPlugins: WPDataSelector< typeof getInstalledPlugins >;
|
||||||
getRecommendedPlugins: WPDataSelector< typeof getRecommendedPlugins >;
|
getRecommendedPlugins: WPDataSelector< typeof getRecommendedPlugins >;
|
||||||
isJetpackConnected: WPDataSelector< typeof isJetpackConnected >;
|
isJetpackConnected: WPDataSelector< typeof isJetpackConnected >;
|
||||||
|
isPluginsRequesting: WPDataSelector< typeof isPluginsRequesting >;
|
||||||
} & WPDataSelectors;
|
} & WPDataSelectors;
|
||||||
|
|
|
@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit;
|
||||||
*/
|
*/
|
||||||
class DataSourcePoller {
|
class DataSourcePoller {
|
||||||
const DATA_SOURCES = array(
|
const DATA_SOURCES = array(
|
||||||
'https://woocommerce.com/wp-json/wccom/obw-free-extensions/1.0/extensions.json',
|
'https://woocommerce.com/wp-json/wccom/obw-free-extensions/2.0/extensions.json',
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue