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

View File

@ -41,7 +41,7 @@
color: $studio-gray-50;
}
.woocommerce-list__item-description {
.woocommerce-list__item-content {
display: none;
}
}
@ -62,7 +62,7 @@
color: $studio-gray-80;
}
.woocommerce-list__item-description {
.woocommerce-list__item-content {
color: $studio-gray-50;
}
@ -194,3 +194,34 @@
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 = [
{
title: __( 'Add manually (recommended)', 'woocommerce-admin' ),
description: __(
'For small stores we recommend adding products manually',
'woocommerce-admin'
),
content: __( 'For small stores we recommend adding products manually', 'woocommerce-admin' ),
before: <i className="material-icons-outlined">add_box</i>,
after: <i className="material-icons-outlined">chevron_right</i>,
href: getAdminLink( 'post-new.php?post_type=product&wc_onboarding_active_task=products' ),
},
{
title: __( 'Import', 'woocommerce-admin' ),
description: __(
content: __(
'For larger stores we recommend importing all products at once via CSV file',
'woocommerce-admin'
),
@ -36,7 +33,7 @@ const subTasks = [
},
{
title: __( 'Migrate', 'woocommerce-admin' ),
description: __(
content: __(
'For stores currently selling elsewhere we suggest using a product migration service',
'woocommerce-admin'
),

View File

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

View File

@ -10,12 +10,13 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { NAMESPACE, pluginNames } from './constants';
import { JETPACK_NAMESPACE, NAMESPACE, pluginNames } from './constants';
function read( resourceNames, fetch = apiFetch ) {
return [
...readActivePlugins( resourceNames, fetch ),
...readProfileItems( resourceNames, fetch ),
...readJetpackStatus( resourceNames, fetch ),
...readJetpackConnectUrl( resourceNames, fetch ),
];
}
@ -170,6 +171,28 @@ function activatePluginToResource( response, items ) {
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 ) {
const resourceName = 'jetpack-connect-url';

View File

@ -77,6 +77,24 @@ const getPluginInstallations = getResource => plugins => {
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 ) => (
requirement = DEFAULT_REQUIREMENT
) => {
@ -181,4 +199,5 @@ export default {
getPluginActivationErrors,
isPluginActivateRequesting,
isPluginInstallRequesting,
isJetpackConnected,
};

View File

@ -20,7 +20,7 @@ Additional class name to style the component.
- after: ReactNode - Content displayed after the list item text.
- before: ReactNode - Content displayed before the list item text.
- 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.
- onClick: Function - Content displayed after the list item text.
- 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
- AdvancedFilters component: fire `onAdvancedFilterAction` for match changes.
- TableCard component: add `onSearch` and `onSort` function props.

View File

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

View File

@ -29,7 +29,7 @@ class List extends Component {
return (
<ul className={ listClassName } role="menu">
{ 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 itemClassName = classnames( 'woocommerce-list__item', itemClasses, {
'has-action': hasAction,
@ -63,9 +63,9 @@ class List extends Component {
<span className="woocommerce-list__item-title">
{ title }
</span>
{ description &&
<span className="woocommerce-list__item-description">
{ description }
{ content &&
<span className="woocommerce-list__item-content">
{ content }
</span>
}
</div>
@ -106,9 +106,12 @@ List.propTypes = {
*/
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.
*/

View File

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

View File

@ -57,6 +57,7 @@ class Onboarding {
OnboardingTasks::get_instance();
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( 'after_switch_theme', array( $this, 'delete_themes_transient' ) );
add_action( 'current_screen', array( $this, 'update_help_tab' ), 60 );
@ -81,6 +82,16 @@ class Onboarding {
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.
*
@ -314,12 +325,30 @@ class Onboarding {
$settings['onboarding']['productTypes'] = self::get_allowed_product_types();
$settings['onboarding']['themes'] = self::get_themes();
$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();
}
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.
*