From 7167242dfbcb039d3a0f0bbd7bd24c2bdca4e5ab Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 29 Jul 2021 12:10:53 -0400 Subject: [PATCH] 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 --- plugins/woocommerce-admin/changelogs/add-7313 | 4 + .../client/task-list/tasks.js | 23 +++ .../task-list/tasks/Marketing/Marketing.scss | 15 ++ .../task-list/tasks/Marketing/Plugin.scss | 42 +++++ .../task-list/tasks/Marketing/Plugin.tsx | 93 ++++++++++ .../task-list/tasks/Marketing/PluginList.scss | 12 ++ .../task-list/tasks/Marketing/PluginList.tsx | 63 +++++++ .../task-list/tasks/Marketing/index.tsx | 175 ++++++++++++++++++ .../packages/data/src/plugins/selectors.ts | 1 + .../RemoteFreeExtensions/DataSourcePoller.php | 2 +- 10 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/changelogs/add-7313 create mode 100644 plugins/woocommerce-admin/client/task-list/tasks/Marketing/Marketing.scss create mode 100644 plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.scss create mode 100644 plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.tsx create mode 100644 plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.scss create mode 100644 plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.tsx create mode 100644 plugins/woocommerce-admin/client/task-list/tasks/Marketing/index.tsx diff --git a/plugins/woocommerce-admin/changelogs/add-7313 b/plugins/woocommerce-admin/changelogs/add-7313 new file mode 100644 index 00000000000..9cd4a97dde7 --- /dev/null +++ b/plugins/woocommerce-admin/changelogs/add-7313 @@ -0,0 +1,4 @@ +Significance: minor +Type: Add + +Add marketing extensions task to task list diff --git a/plugins/woocommerce-admin/client/task-list/tasks.js b/plugins/woocommerce-admin/client/task-list/tasks.js index 9d6668c10d3..3d783bb7f2e 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks.js +++ b/plugins/woocommerce-admin/client/task-list/tasks.js @@ -17,6 +17,7 @@ import { recordEvent } from '@woocommerce/tracks'; */ import Appearance from './tasks/appearance'; import { getCategorizedOnboardingProducts } from '../dashboard/utils'; +import { Marketing } from './tasks/Marketing'; import { Products } from './tasks/products'; import Shipping from './tasks/shipping'; import Tax from './tasks/tax'; @@ -328,6 +329,28 @@ export function getAllTasks( { time: __( '1 minute', 'woocommerce-admin' ), 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: , + 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', title: __( 'Personalize my store', 'woocommerce-admin' ), diff --git a/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Marketing.scss b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Marketing.scss new file mode 100644 index 00000000000..d6c7b2234f4 --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Marketing.scss @@ -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; + } + } +} diff --git a/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.scss b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.scss new file mode 100644 index 00000000000..2954b3812ac --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.scss @@ -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; +} diff --git a/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.tsx b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.tsx new file mode 100644 index 00000000000..3ac0461f4a6 --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/Plugin.tsx @@ -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 ( +
+ { imageUrl && ( +
+ { +
+ ) } +
+ + { name } + + { description } +
+
+ { isActive && manageUrl && ( + + ) } + { isInstalled && ! isActive && ( + + ) } + { ! isInstalled && ( + + ) } +
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.scss b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.scss new file mode 100644 index 00000000000..6c854eef9ee --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.scss @@ -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; + } +} diff --git a/plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.tsx b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.tsx new file mode 100644 index 00000000000..c4a7e04a090 --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/PluginList.tsx @@ -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 ( +
+ { title && ( +
+ + { title } + +
+ ) } + { plugins.map( ( plugin ) => { + const { + description, + imageUrl, + isActive, + isInstalled, + manageUrl, + slug, + name, + } = plugin; + return ( + + ); + } ) } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/task-list/tasks/Marketing/index.tsx b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/index.tsx new file mode 100644 index 00000000000..f5a5bc968dc --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/Marketing/index.tsx @@ -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 ; + } + + return ( +
+ + + + { __( + 'Recommended marketing extensions', + 'woocommerce-admin' + ) } + + + { __( + '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' + ) } + + + { pluginLists.map( ( list ) => { + const { key, title, plugins } = list; + return ( + + ); + } ) } + +
+ ); +}; diff --git a/plugins/woocommerce-admin/packages/data/src/plugins/selectors.ts b/plugins/woocommerce-admin/packages/data/src/plugins/selectors.ts index ff0d13aa0e0..c9f678c455c 100644 --- a/plugins/woocommerce-admin/packages/data/src/plugins/selectors.ts +++ b/plugins/woocommerce-admin/packages/data/src/plugins/selectors.ts @@ -69,4 +69,5 @@ export type PluginSelectors = { getInstalledPlugins: WPDataSelector< typeof getInstalledPlugins >; getRecommendedPlugins: WPDataSelector< typeof getRecommendedPlugins >; isJetpackConnected: WPDataSelector< typeof isJetpackConnected >; + isPluginsRequesting: WPDataSelector< typeof isPluginsRequesting >; } & WPDataSelectors; diff --git a/plugins/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php b/plugins/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php index 96c46c71b8e..dbf350afd04 100644 --- a/plugins/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php +++ b/plugins/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php @@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit; */ class DataSourcePoller { 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', ); /**