Add base payments task & jetpack connection status method (https://github.com/woocommerce/woocommerce-admin/pull/2853)

* Add a base for the payments task, with the ability to choose methods. Also adds Jetpack connection status.

* Handle PR feedback
This commit is contained in:
Justin Shreve 2019-08-29 12:41:04 -04:00 committed by GitHub
parent e67b556ac9
commit e5b4606047
13 changed files with 398 additions and 31 deletions

View File

@ -21,6 +21,7 @@ import Connect from './tasks/connect';
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';
import Payments from './tasks/payments';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
class TaskDashboard extends Component { class TaskDashboard extends Component {
@ -42,7 +43,7 @@ class TaskDashboard extends Component {
{ {
key: 'connect', key: 'connect',
title: __( 'Connect your store to WooCommerce.com', 'woocommerce-admin' ), title: __( 'Connect your store to WooCommerce.com', 'woocommerce-admin' ),
description: __( content: __(
'Install and manage your extensions directly from your Dashboard', 'Install and manage your extensions directly from your Dashboard',
'wooocommerce-admin' 'wooocommerce-admin'
), ),
@ -55,7 +56,7 @@ class TaskDashboard extends Component {
{ {
key: 'products', key: 'products',
title: __( 'Add your first product', 'woocommerce-admin' ), title: __( 'Add your first product', 'woocommerce-admin' ),
description: __( content: __(
'Add products manually, import from a sheet or migrate from another platform', 'Add products manually, import from a sheet or migrate from another platform',
'wooocommerce-admin' 'wooocommerce-admin'
), ),
@ -73,7 +74,7 @@ class TaskDashboard extends Component {
{ {
key: 'personalize-store', key: 'personalize-store',
title: __( 'Personalize your store', 'woocommerce-admin' ), title: __( 'Personalize your store', 'woocommerce-admin' ),
description: __( 'Create a custom homepage and upload your logo', 'wooocommerce-admin' ), content: __( 'Create a custom homepage and upload your logo', 'wooocommerce-admin' ),
before: <i className="material-icons-outlined">palette</i>, before: <i className="material-icons-outlined">palette</i>,
after: <i className="material-icons-outlined">chevron_right</i>, after: <i className="material-icons-outlined">chevron_right</i>,
onClick: noop, onClick: noop,
@ -82,10 +83,7 @@ class TaskDashboard extends Component {
{ {
key: 'shipping', key: 'shipping',
title: __( 'Set up shipping', 'woocommerce-admin' ), title: __( 'Set up shipping', 'woocommerce-admin' ),
description: __( content: __( 'Configure some basic shipping rates to get started', 'wooocommerce-admin' ),
'Configure some basic shipping rates to get started',
'wooocommerce-admin'
),
before: before:
shippingZonesCount > 0 ? ( shippingZonesCount > 0 ? (
<i className="material-icons-outlined">check_circle</i> <i className="material-icons-outlined">check_circle</i>
@ -101,7 +99,7 @@ class TaskDashboard extends Component {
{ {
key: 'tax', key: 'tax',
title: __( 'Set up tax', 'woocommerce-admin' ), title: __( 'Set up tax', 'woocommerce-admin' ),
description: __( content: __(
'Choose how to configure tax rates - manually or automatically', 'Choose how to configure tax rates - manually or automatically',
'wooocommerce-admin' 'wooocommerce-admin'
), ),
@ -114,13 +112,14 @@ class TaskDashboard extends Component {
{ {
key: 'payments', key: 'payments',
title: __( 'Set up payments', 'woocommerce-admin' ), title: __( 'Set up payments', 'woocommerce-admin' ),
description: __( content: __(
'Select which payment providers youd like to use and configure them', 'Select which payment providers youd like to use and configure them',
'wooocommerce-admin' 'wooocommerce-admin'
), ),
before: <i className="material-icons-outlined">payment</i>, before: <i className="material-icons-outlined">payment</i>,
after: <i className="material-icons-outlined">chevron_right</i>, after: <i className="material-icons-outlined">chevron_right</i>,
onClick: noop, onClick: () => updateQueryString( { task: 'payments' } ),
container: <Payments />,
visible: true, visible: true,
}, },
]; ];

View File

@ -41,7 +41,7 @@
color: $studio-gray-50; color: $studio-gray-50;
} }
.woocommerce-list__item-description { .woocommerce-list__item-content {
display: none; display: none;
} }
} }
@ -62,7 +62,7 @@
color: $studio-gray-80; color: $studio-gray-80;
} }
.woocommerce-list__item-description { .woocommerce-list__item-content {
color: $studio-gray-50; color: $studio-gray-50;
} }
@ -194,3 +194,34 @@
font-size: 16px; font-size: 16px;
} }
} }
.woocommerce-task-payments {
.woocommerce-list__item .woocommerce-list__item-after {
align-self: start;
margin-left: $gap;
margin-top: $gap-large;
}
.woocommerce-list__item-title {
border-top: 1px solid $studio-gray-5;
padding-top: $gap;
}
.woocommerce-task-payments__woocommerce-services-options {
border-top: 1px solid $studio-gray-5;
margin-top: $gap;
margin-left: $gap-larger;
padding-top: $gap;
.components-checkbox-control__input {
margin-left: -$gap-larger;
}
.components-checkbox-control__label {
font-size: 16px;
line-height: 22px;
padding-left: $gap-small;
color: #1a1a1a;
}
}
}

View File

@ -0,0 +1,262 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Fragment, Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { filter, noop } from 'lodash';
import { FormToggle, CheckboxControl } from '@wordpress/components';
import { TextControl } from 'newspack-components';
/**
* WooCommerce dependencies
*/
import { getCountryCode } from 'dashboard/utils';
import { Form, Card, Stepper, List } from '@woocommerce/components';
import { getHistory, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import withSelect from 'wc-api/with-select';
class Payments extends Component {
constructor() {
super( ...arguments );
this.state = {
step: 'choose',
};
this.completeStep = this.completeStep.bind( this );
}
getInitialValues() {
const values = {
stripe: false,
paypal: false,
klarna_checkout: false,
klarna_payments: false,
create_stripe: false,
create_paypal: false,
stripe_email: '',
paypal_email: '',
};
return values;
}
validate() {
const errors = {};
return errors;
}
completeStep() {
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 {
getHistory().push( getNewPath( {}, '/', {} ) );
}
}
// If Jetpack is connected and WCS is enabled, we will offer a streamlined option.
renderWooCommerceServicesStripeConnect( { getInputProps, values } ) {
if ( ! values.stripe ) {
return null;
}
const { isJetpackConnected, activePlugins } = this.props;
if ( ! isJetpackConnected || ! activePlugins.includes( 'woocommerce-services' ) ) {
return null;
}
return (
<div className="woocommerce-task-payments__woocommerce-services-options">
<CheckboxControl
label={ __( 'Create a Stripe account for me', 'woocommerce-admin' ) }
{ ...getInputProps( 'create_stripe' ) }
/>
{ values.create_stripe && (
<TextControl
label={ __( 'Email address', 'woocommerce-admin' ) }
{ ...getInputProps( 'stripe_email' ) }
/>
) }
</div>
);
}
renderWooCommerceServicesPayPalConnect( { getInputProps, values } ) {
if ( ! values.paypal ) {
return null;
}
const { isJetpackConnected, activePlugins } = this.props;
if ( ! isJetpackConnected || ! activePlugins.includes( 'woocommerce-services' ) ) {
return null;
}
return (
<div className="woocommerce-task-payments__woocommerce-services-options">
<CheckboxControl
label={ __( 'Create a Paypal account for me', 'woocommerce-admin' ) }
{ ...getInputProps( 'create_paypal' ) }
/>
{ values.create_paypal && (
<TextControl
label={ __( 'Email address', 'woocommerce-admin' ) }
{ ...getInputProps( 'paypal_email' ) }
/>
) }
</div>
);
}
getMethodOptions( formData ) {
const { getInputProps } = formData;
const { countryCode, profileItems } = this.props;
const methods = [
{
title: __( 'Credit cards - powered by Stripe', 'woocommerce-admin' ),
content: (
<Fragment>
{ __(
'Accept debit and credit cards in 135+ currencies, methods such as Alipay, ' +
'and one-touch checkout with Apple Pay.',
'woocommerce-admin'
) }
{ this.renderWooCommerceServicesStripeConnect( formData ) }
</Fragment>
),
before: <div />, // @todo Logo
after: <FormToggle { ...getInputProps( 'stripe' ) } />,
visible: true,
},
{
title: __( 'PayPal Checkout', 'woocommerce-admin' ),
content: (
<Fragment>
{ __(
"Safe and secure payments using credit cards or your customer's PayPal account.",
'woocommerce-admin'
) }
{ this.renderWooCommerceServicesPayPalConnect( formData ) }
</Fragment>
),
before: <div />, // @todo Logo
after: <FormToggle { ...getInputProps( 'paypal' ) } />,
visible: true,
},
{
title: __( 'Klarna Checkout', 'woocommerce-admin' ),
content: __(
'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.',
'woocommerce-admin'
),
before: <div />, // @todo Logo
after: <FormToggle { ...getInputProps( 'klarna_checkout' ) } />,
visible: [ 'SE', 'FI', 'NO', 'NL' ].includes( countryCode ),
},
{
title: __( 'Klarna Payments', 'woocommerce-admin' ),
content: __(
'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.',
'woocommerce-admin'
),
before: <div />, // @todo Logo
after: <FormToggle { ...getInputProps( 'klarna_payments' ) } />,
visible: [ 'DK', 'DE', 'AT' ].includes( countryCode ),
},
{
title: __( 'Square', 'woocommerce-admin' ),
content: __(
'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). ' +
'Sell online and in store and track sales and inventory in one place.',
'woocommerce-admin'
),
before: <div />, // @todo Logo
after: <FormToggle { ...getInputProps( 'square' ) } />,
visible:
[ 'brick-mortar', 'brick-mortar-other' ].includes( profileItems.selling_venues ) &&
[ 'US', 'CA', 'JP', 'GB', 'AU' ].includes( countryCode ),
},
];
return filter( methods, method => method.visible );
}
getSteps( formData ) {
const steps = [
{
key: 'choose',
label: __( 'Choose payment methods', 'woocommerce-admin' ),
description: __( "Select which payment methods you'd like to use", 'woocommerce-admin' ),
content: <List items={ this.getMethodOptions( formData ) } />,
visible: true,
},
];
return filter( steps, step => step.visible );
}
render() {
const { step } = this.state;
const { isSettingsRequesting } = this.props;
return (
<Form
initialValues={ this.getInitialValues() }
onSubmitCallback={ noop }
validate={ this.validate }
>
{ ( { getInputProps, values } ) => {
return (
<div className="woocommerce-task-payments">
<Card className="is-narrow">
<Stepper
isVertical
isPending={ isSettingsRequesting }
currentStep={ step }
steps={ this.getSteps( { getInputProps, values } ) }
/>
</Card>
</div>
);
} }
</Form>
);
}
}
export default compose(
withSelect( select => {
const {
getSettings,
getSettingsError,
isGetSettingsRequesting,
getProfileItems,
isJetpackConnected,
getActivePlugins,
} = select( 'wc-api' );
const settings = getSettings( 'general' );
const isSettingsError = Boolean( getSettingsError( 'general' ) );
const isSettingsRequesting = isGetSettingsRequesting( 'general' );
const countryCode = getCountryCode( settings.woocommerce_default_country );
return {
countryCode,
isSettingsError,
isSettingsRequesting,
settings,
profileItems: getProfileItems(),
activePlugins: getActivePlugins(),
isJetpackConnected: isJetpackConnected(),
};
} )
)( Payments );

View File

@ -14,17 +14,14 @@ import { getAdminLink } from '@woocommerce/navigation';
const subTasks = [ const subTasks = [
{ {
title: __( 'Add manually (recommended)', 'woocommerce-admin' ), title: __( 'Add manually (recommended)', 'woocommerce-admin' ),
description: __( content: __( 'For small stores we recommend adding products manually', 'woocommerce-admin' ),
'For small stores we recommend adding products manually',
'woocommerce-admin'
),
before: <i className="material-icons-outlined">add_box</i>, before: <i className="material-icons-outlined">add_box</i>,
after: <i className="material-icons-outlined">chevron_right</i>, after: <i className="material-icons-outlined">chevron_right</i>,
href: getAdminLink( 'post-new.php?post_type=product&wc_onboarding_active_task=products' ), href: getAdminLink( 'post-new.php?post_type=product&wc_onboarding_active_task=products' ),
}, },
{ {
title: __( 'Import', 'woocommerce-admin' ), title: __( 'Import', 'woocommerce-admin' ),
description: __( content: __(
'For larger stores we recommend importing all products at once via CSV file', 'For larger stores we recommend importing all products at once via CSV file',
'woocommerce-admin' 'woocommerce-admin'
), ),
@ -36,7 +33,7 @@ const subTasks = [
}, },
{ {
title: __( 'Migrate', 'woocommerce-admin' ), title: __( 'Migrate', 'woocommerce-admin' ),
description: __( content: __(
'For stores currently selling elsewhere we suggest using a product migration service', 'For stores currently selling elsewhere we suggest using a product migration service',
'woocommerce-admin' 'woocommerce-admin'
), ),

View File

@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n';
* Onboarding namespace. * Onboarding namespace.
*/ */
export const NAMESPACE = '/wc-admin/v1'; export const NAMESPACE = '/wc-admin/v1';
export const JETPACK_NAMESPACE = '/jetpack/v4';
/** /**
* Plugin slugs and names as key/value pairs. * Plugin slugs and names as key/value pairs.

View File

@ -10,12 +10,13 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies * Internal dependencies
*/ */
import { getResourceName } from '../utils'; import { getResourceName } from '../utils';
import { NAMESPACE, pluginNames } from './constants'; import { JETPACK_NAMESPACE, NAMESPACE, pluginNames } from './constants';
function read( resourceNames, fetch = apiFetch ) { function read( resourceNames, fetch = apiFetch ) {
return [ return [
...readActivePlugins( resourceNames, fetch ), ...readActivePlugins( resourceNames, fetch ),
...readProfileItems( resourceNames, fetch ), ...readProfileItems( resourceNames, fetch ),
...readJetpackStatus( resourceNames, fetch ),
...readJetpackConnectUrl( resourceNames, fetch ), ...readJetpackConnectUrl( resourceNames, fetch ),
]; ];
} }
@ -170,6 +171,28 @@ function activatePluginToResource( response, items ) {
return resources; return resources;
} }
function readJetpackStatus( resourceNames, fetch ) {
const resourceName = 'jetpack-status';
if ( resourceNames.includes( resourceName ) ) {
const url = JETPACK_NAMESPACE + '/connection';
return [
fetch( {
path: url,
} )
.then( response => {
return { [ resourceName ]: { data: response } };
} )
.catch( error => {
return { [ resourceName ]: { error: String( error.message ) } };
} ),
];
}
return [];
}
function readJetpackConnectUrl( resourceNames, fetch ) { function readJetpackConnectUrl( resourceNames, fetch ) {
const resourceName = 'jetpack-connect-url'; const resourceName = 'jetpack-connect-url';

View File

@ -77,6 +77,24 @@ const getPluginInstallations = getResource => plugins => {
return installations; return installations;
}; };
const isJetpackConnected = ( getResource, requireResource ) => (
requirement = DEFAULT_REQUIREMENT
) => {
const activePluginsData = requireResource( requirement, 'active-plugins' ).data || undefined;
const activePlugins = ! activePluginsData
? wcSettings.onboarding.activePlugins
: activePluginsData;
// Avoid issuing API calls, since Jetpack is obviously not connected.
if ( ! activePlugins.includes( 'jetpack' ) ) {
return false;
}
const data =
requireResource( requirement, 'jetpack-status' ).data || wcSettings.dataEndpoints.jetpackStatus;
return ( data && data.isActive ) || false;
};
const getActivePlugins = ( getResource, requireResource ) => ( const getActivePlugins = ( getResource, requireResource ) => (
requirement = DEFAULT_REQUIREMENT requirement = DEFAULT_REQUIREMENT
) => { ) => {
@ -181,4 +199,5 @@ export default {
getPluginActivationErrors, getPluginActivationErrors,
isPluginActivateRequesting, isPluginActivateRequesting,
isPluginInstallRequesting, isPluginInstallRequesting,
isJetpackConnected,
}; };

View File

@ -20,7 +20,7 @@ Additional class name to style the component.
- after: ReactNode - Content displayed after the list item text. - after: ReactNode - Content displayed after the list item text.
- before: ReactNode - Content displayed before the list item text. - before: ReactNode - Content displayed before the list item text.
- className: String - Additional class name to style the list item. - className: String - Additional class name to style the list item.
- description: String - Description displayed beneath the list item title. - content: One of type: string, node
- href: String - Href attribute used in a Link wrapped around the item. - href: String - Href attribute used in a Link wrapped around the item.
- onClick: Function - Content displayed after the list item text. - onClick: Function - Content displayed after the list item text.
- target: String - Target attribute used for Link wrapper. - target: String - Target attribute used for Link wrapper.

View File

@ -1,3 +1,6 @@
# 4.0.0
- Changed the <List /> `description` prop to `content` and allowed content nodes to be passed in addition to strings.
# 3.2.0 # 3.2.0
- AdvancedFilters component: fire `onAdvancedFilterAction` for match changes. - AdvancedFilters component: fire `onAdvancedFilterAction` for match changes.
- TableCard component: add `onSearch` and `onSort` function props. - TableCard component: add `onSearch` and `onSort` function props.

View File

@ -5,22 +5,22 @@ import Gridicon from 'gridicons';
const listItems = [ const listItems = [
{ {
title: 'List item title', title: 'List item title',
description: 'List item description text', content: 'List item description text',
}, },
{ {
before: <Gridicon icon="star" />, before: <Gridicon icon="star" />,
title: 'List item with before icon', title: 'List item with before icon',
description: 'List item description text', content: 'List item description text',
}, },
{ {
before: <Gridicon icon="star" />, before: <Gridicon icon="star" />,
after: <Gridicon icon="chevron-right" />, after: <Gridicon icon="chevron-right" />,
title: 'List item with before and after icons', title: 'List item with before and after icons',
description: 'List item description text', content: 'List item description text',
}, },
{ {
title: 'Clickable list item', title: 'Clickable list item',
description: 'List item description text', content: 'List item description text',
onClick: () => alert( 'List item clicked' ), onClick: () => alert( 'List item clicked' ),
}, },
]; ];

View File

@ -29,7 +29,7 @@ class List extends Component {
return ( return (
<ul className={ listClassName } role="menu"> <ul className={ listClassName } role="menu">
{ items.map( ( item, i ) => { { items.map( ( item, i ) => {
const { after, before, className: itemClasses, description, href, onClick, target, title } = item; const { after, before, className: itemClasses, content, href, onClick, target, title } = item;
const hasAction = 'function' === typeof onClick || href; const hasAction = 'function' === typeof onClick || href;
const itemClassName = classnames( 'woocommerce-list__item', itemClasses, { const itemClassName = classnames( 'woocommerce-list__item', itemClasses, {
'has-action': hasAction, 'has-action': hasAction,
@ -63,9 +63,9 @@ class List extends Component {
<span className="woocommerce-list__item-title"> <span className="woocommerce-list__item-title">
{ title } { title }
</span> </span>
{ description && { content &&
<span className="woocommerce-list__item-description"> <span className="woocommerce-list__item-content">
{ description } { content }
</span> </span>
} }
</div> </div>
@ -106,9 +106,12 @@ List.propTypes = {
*/ */
className: PropTypes.string, className: PropTypes.string,
/** /**
* Description displayed beneath the list item title. * Content displayed beneath the list item title.
*/ */
description: PropTypes.string, content: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.node,
] ),
/** /**
* Href attribute used in a Link wrapped around the item. * Href attribute used in a Link wrapped around the item.
*/ */

View File

@ -26,7 +26,7 @@
color: $studio-gray-90; color: $studio-gray-90;
} }
.woocommerce-list__item-description { .woocommerce-list__item-content {
margin-top: $gap-smallest; margin-top: $gap-smallest;
display: block; display: block;
font-size: 14px; font-size: 14px;

View File

@ -57,6 +57,7 @@ class Onboarding {
OnboardingTasks::get_instance(); OnboardingTasks::get_instance();
add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // Run after Automattic\WooCommerce\Admin\Loader. add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // Run after Automattic\WooCommerce\Admin\Loader.
add_filter( 'woocommerce_component_settings_preload_endpoints', array( $this, 'add_preload_endpoints' ) );
add_action( 'woocommerce_theme_installed', array( $this, 'delete_themes_transient' ) ); add_action( 'woocommerce_theme_installed', array( $this, 'delete_themes_transient' ) );
add_action( 'after_switch_theme', array( $this, 'delete_themes_transient' ) ); add_action( 'after_switch_theme', array( $this, 'delete_themes_transient' ) );
add_action( 'current_screen', array( $this, 'update_help_tab' ), 60 ); add_action( 'current_screen', array( $this, 'update_help_tab' ), 60 );
@ -81,6 +82,16 @@ class Onboarding {
return $is_completed || $is_skipped ? false : true; return $is_completed || $is_skipped ? false : true;
} }
/**
* Returns true if the task list should be displayed (not completed or hidden off the dashboard.
*
* @return bool
*/
public function should_show_tasks() {
// @todo Implement logic for this.
return true;
}
/** /**
* Get a list of allowed industries for the onboarding wizard. * Get a list of allowed industries for the onboarding wizard.
* *
@ -314,12 +325,30 @@ class Onboarding {
$settings['onboarding']['productTypes'] = self::get_allowed_product_types(); $settings['onboarding']['productTypes'] = self::get_allowed_product_types();
$settings['onboarding']['themes'] = self::get_themes(); $settings['onboarding']['themes'] = self::get_themes();
$settings['onboarding']['activeTheme'] = get_option( 'stylesheet' ); $settings['onboarding']['activeTheme'] = get_option( 'stylesheet' );
}
// Only fetch if the onboarding wizard OR the task list is incomplete.
if ( $this->should_show_profiler() || $this->should_show_tasks() ) {
$settings['onboarding']['activePlugins'] = self::get_active_plugins(); $settings['onboarding']['activePlugins'] = self::get_active_plugins();
} }
return $settings; return $settings;
} }
/**
* Preload data from API endpoints.
*
* @param array $endpoints Array of preloaded endpoints.
* @return array
*/
public function add_preload_endpoints( $endpoints ) {
if ( ! class_exists( 'Jetpack' ) ) {
return $endpoints;
}
$endpoints['jetpackStatus'] = '/jetpack/v4/connection';
return $endpoints;
}
/** /**
* Gets an array of plugins that can be installed & activated via the onboarding wizard. * Gets an array of plugins that can be installed & activated via the onboarding wizard.
* *