Add plugin and jetpack wc-api methods to shipping task (https://github.com/woocommerce/woocommerce-admin/pull/2778)

* Add shipping labels step

* Add jetpack connection selectors to wc-api

* Add plugin install and activation methods to wc-api

* Add shipping connect step

* Add busy cursor CSS to buttons
This commit is contained in:
Joshua T Flowers 2019-08-21 14:34:21 +08:00 committed by GitHub
parent f4d7936b17
commit 234e4d513c
10 changed files with 575 additions and 127 deletions

View File

@ -2,20 +2,19 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { Button } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { forEach } from 'lodash';
import { compose } from '@wordpress/compose';
import { difference } from 'lodash';
import { withDispatch } from '@wordpress/data';
/**
* Internal depdencies
*/
import { H, Stepper, Card } from '@woocommerce/components';
import { NAMESPACE } from 'wc-api/onboarding/constants';
import { recordEvent } from 'lib/tracks';
import withSelect from 'wc-api/with-select';
const plugins = [ 'jetpack', 'woocommerce-services' ];
@ -25,138 +24,53 @@ class Plugins extends Component {
this.state = {
step: 'install',
isPending: true,
isError: false,
pluginsInstalled: 0,
pluginsActivated: 0,
connectUrl: '',
};
this.activatePlugins = this.activatePlugins.bind( this );
}
componentDidMount() {
this.installPlugins();
this.props.installPlugins( plugins );
}
componentDidUpdate( prevProps, prevState ) {
componentDidUpdate( prevProps ) {
const { createNotice, errors, installedPlugins, jetpackConnectUrl } = this.props;
if ( jetpackConnectUrl ) {
window.location = jetpackConnectUrl;
}
const newErrors = difference( errors, prevProps.errors );
newErrors.map( error => createNotice( 'error', error ) );
if (
this.state.pluginsInstalled !== prevState.pluginsInstalled &&
this.state.pluginsInstalled === plugins.length
prevProps.installedPlugins.length !== plugins.length &&
installedPlugins.length === plugins.length
) {
/* eslint-disable react/no-did-update-set-state */
this.setState( {
step: 'activate',
isPending: false,
} );
this.setState( { step: 'activate' } );
/* eslint-enable react/no-did-update-set-state */
}
if (
this.state.pluginsActivated !== prevState.pluginsActivated &&
this.state.pluginsActivated === plugins.length
) {
this.connectJetpack();
}
}
installPlugins() {
forEach( plugins, async plugin => {
const response = await this.doPluginAction( 'install', plugin );
if ( 'success' === response.status ) {
this.setState( state => ( {
pluginsInstalled: state.pluginsInstalled + 1,
} ) );
}
} );
}
activatePlugins( event ) {
async activatePlugins( event ) {
event.preventDefault();
// Avoid double activating.
const { isPending } = this.state;
if ( isPending ) {
const { isRequesting } = this.props;
if ( isRequesting ) {
return false;
}
this.setState( {
isPending: true,
} );
recordEvent( 'storeprofiler_install_plugin' );
forEach( plugins, async plugin => {
const response = await this.doPluginAction( 'activate', plugin );
if ( 'success' === response.status ) {
this.setState( state => ( {
pluginsActivated: state.pluginsActivated + 1,
} ) );
}
} );
}
getErrorMessage( action, plugin ) {
return 'install' === action
? sprintf(
__( 'There was an error installing %s. Please try again.', 'woocommerce-admin' ),
this.getPluginName( plugin )
)
: sprintf(
__( 'There was an error activating %s. Please try again.', 'woocommerce-admin' ),
this.getPluginName( plugin )
);
}
async doPluginAction( action, plugin ) {
try {
const pluginResponse = await apiFetch( {
path: `${ NAMESPACE }/onboarding/plugins/${ action }`,
method: 'POST',
data: {
plugin,
},
} );
return pluginResponse;
} catch ( err ) {
this.props.createNotice( 'error', this.getErrorMessage( action, plugin ) );
this.setState( {
isPending: false,
isError: true,
} );
}
}
async connectJetpack() {
try {
const connectResponse = await apiFetch( {
path: `${ NAMESPACE }/onboarding/plugins/connect-jetpack`,
} );
if ( connectResponse && connectResponse.connectAction ) {
window.location = connectResponse.connectAction;
return;
}
throw new Error();
} catch ( err ) {
this.props.createNotice( 'error', this.getErrorMessage( 'activate', 'jetpack' ) );
this.setState( {
isPending: false,
isError: true,
} );
}
}
getPluginName( plugin ) {
switch ( plugin ) {
case 'jetpack':
return __( 'Jetpack', 'woocommerce-admin' );
case 'woocommerce-services':
return __( 'WooCommerce Services', 'woocommerce-admin' );
}
this.props.activatePlugins( plugins );
}
render() {
const { step, isPending, isError } = this.state;
const { hasErrors, isRequesting } = this.props;
const { step } = this.state;
return (
<Fragment>
<H className="woocommerce-profile-wizard__header-title">
@ -167,7 +81,7 @@ class Plugins extends Component {
<Stepper
isVertical={ true }
currentStep={ step }
isPending={ isPending }
isPending={ isRequesting && ! hasErrors }
steps={ [
{
label: __( 'Install Jetpack and WooCommerce Services', 'woocommerce-admin' ),
@ -181,18 +95,17 @@ class Plugins extends Component {
/>
<div className="woocommerce-profile-wizard__plugins-actions">
{ isError && (
{ hasErrors && (
<Button isPrimary onClick={ () => location.reload() }>
{ __( 'Retry', 'woocommerce-admin' ) }
</Button>
) }
{ ! isError &&
'activate' === step && (
<Button isPrimary isBusy={ isPending } onClick={ this.activatePlugins }>
{ __( 'Activate & continue', 'woocommerce-admin' ) }
</Button>
) }
{ ! ( hasErrors && 'activate' === step ) && (
<Button isPrimary isBusy={ isRequesting } onClick={ this.activatePlugins }>
{ __( 'Activate & continue', 'woocommerce-admin' ) }
</Button>
) }
</div>
</Card>
</Fragment>
@ -201,10 +114,64 @@ class Plugins extends Component {
}
export default compose(
withSelect( select => {
const {
getJetpackConnectUrl,
isGetJetpackConnectUrlRequesting,
getJetpackConnectUrlError,
getPluginInstallations,
getPluginInstallationErrors,
getPluginActivations,
getPluginActivationErrors,
isPluginActivateRequesting,
isPluginInstallRequesting,
} = select( 'wc-api' );
const isRequesting =
isPluginActivateRequesting() || isPluginInstallRequesting() || getJetpackConnectUrlError();
const activationErrors = getPluginActivationErrors( plugins );
const activatedPlugins = Object.keys( getPluginActivations( plugins ) );
const installationErrors = getPluginInstallationErrors( plugins );
const installedPlugins = Object.keys( getPluginInstallations( plugins ) );
const isJetpackConnectUrlRequesting = isGetJetpackConnectUrlRequesting();
const jetpackConnectUrlError = getJetpackConnectUrlError();
let jetpackConnectUrl = null;
if ( activatedPlugins.includes( 'jetpack' ) ) {
jetpackConnectUrl = getJetpackConnectUrl();
}
const errors = [];
Object.keys( activationErrors ).map( plugin =>
errors.push( activationErrors[ plugin ].message )
);
Object.keys( installationErrors ).map( plugin =>
errors.push( installationErrors[ plugin ].message )
);
if ( jetpackConnectUrlError ) {
errors.push( jetpackConnectUrlError );
}
const hasErrors = Boolean( errors.length );
return {
activatedPlugins,
installedPlugins,
jetpackConnectUrl,
isJetpackConnectUrlRequesting,
errors,
hasErrors,
isRequesting,
};
} ),
withDispatch( dispatch => {
const { createNotice } = dispatch( 'core/notices' );
const { activatePlugins, installPlugins } = dispatch( 'wc-api' );
return {
activatePlugins,
createNotice,
installPlugins,
};
} )
)( Plugins );

View File

@ -194,6 +194,10 @@
.muriel-button.is-button {
height: 48px;
&.is-busy {
cursor: progress;
}
}
.muriel-checkbox input[type='checkbox']:checked {

View File

@ -16,9 +16,15 @@
margin-right: auto;
}
.muriel-button.is-button {
.muriel-button.components-button {
height: 40px;
margin: 0;
min-width: 106px;
margin: $gap $gap-smaller 0 0;
justify-content: center;
&:not(.is-primary) {
color: $muriel-hot-pink-500;
}
}
.woocommerce-list {
@ -65,10 +71,6 @@
}
}
.woocommerce-shipping-rates {
margin-bottom: $gap;
}
.woocommerce-shipping-rate {
display: flex;
padding-top: $gap-small;

View File

@ -0,0 +1,78 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import withSelect from 'wc-api/with-select';
class Connect extends Component {
constructor() {
super( ...arguments );
this.connectJetpack = this.connectJetpack.bind( this );
}
componentDidUpdate( prevProps ) {
const { createNotice, error } = this.props;
if ( error && error !== prevProps.error ) {
createNotice( 'error', error );
}
}
async connectJetpack() {
const { jetpackConnectUrl } = this.props;
window.location = jetpackConnectUrl;
}
render() {
const { hasErrors, isRequesting } = this.props;
return hasErrors ? (
<Button isPrimary onClick={ () => location.reload() }>
{ __( 'Retry', 'woocommerce-admin' ) }
</Button>
) : (
<Fragment>
<Button isBusy={ isRequesting } isPrimary onClick={ this.connectJetpack }>
{ __( 'Connect', 'woocommerce-admin' ) }
</Button>
</Fragment>
);
}
}
export default compose(
withSelect( select => {
const {
getJetpackConnectUrl,
isGetJetpackConnectUrlRequesting,
getJetpackConnectUrlError,
} = select( 'wc-api' );
const isRequesting = isGetJetpackConnectUrlRequesting();
const error = getJetpackConnectUrlError();
const jetpackConnectUrl = getJetpackConnectUrl();
return {
error,
isRequesting,
jetpackConnectUrl,
};
} ),
withDispatch( dispatch => {
const { createNotice } = dispatch( 'core/notices' );
return {
createNotice,
};
} )
)( Connect );

View File

@ -18,7 +18,9 @@ import { getHistory, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import Connect from './connect';
import StoreLocation from './location';
import ShippingLabels from './labels';
import ShippingRates from './rates';
import withSelect from 'wc-api/with-select';
@ -137,6 +139,8 @@ class Shipping extends Component {
}
getSteps() {
const { countryCode } = this.props;
const steps = [
{
key: 'store_location',
@ -161,6 +165,27 @@ class Shipping extends Component {
),
visible: true,
},
{
key: 'label_printing',
label: __( 'Enable shipping label printing', 'woocommerce-admin' ),
description: __(
'With WooCommerce Services and Jetpack you can save time at the' +
'Post Office by printing your shipping labels at home',
'woocommerce-admin'
),
content: <ShippingLabels completeStep={ this.completeStep } { ...this.props } />,
visible: [ 'US', 'GB', 'CA', 'AU' ].includes( countryCode ),
},
{
key: 'connect',
label: __( 'Connect your store', 'woocommerce-admin' ),
description: __(
'Connect your store to WordPress.com to enable label printing',
'woocommerce-admin'
),
content: <Connect completeStep={ this.completeStep } { ...this.props } />,
visible: 'US' === countryCode,
},
];
return filter( steps, step => step.visible );

View File

@ -0,0 +1,142 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { difference } from 'lodash';
import { withDispatch } from '@wordpress/data';
/**
* WooCommerce dependencies
*/
import { getHistory, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import withSelect from 'wc-api/with-select';
const plugins = [ 'jetpack', 'woocommerce-services' ];
class ShippingLabels extends Component {
constructor() {
super( ...arguments );
this.installAndActivatePlugins = this.installAndActivatePlugins.bind( this );
}
componentDidUpdate( prevProps ) {
const {
activatedPlugins,
activatePlugins,
completeStep,
createNotice,
errors,
installedPlugins,
isRequesting,
} = this.props;
const newErrors = difference( errors, prevProps.errors );
newErrors.map( error => createNotice( 'error', error ) );
if (
! isRequesting &&
installedPlugins.length === plugins.length &&
activatedPlugins.length !== plugins.length &&
prevProps.installedPlugins.length !== installedPlugins.length
) {
activatePlugins( plugins );
}
if ( activatedPlugins.length === plugins.length ) {
createNotice(
'success',
__( 'Plugins were successfully installed and activated.', 'woocommerce-admin' )
);
completeStep();
}
}
async installAndActivatePlugins( event ) {
event.preventDefault();
// Avoid double activating.
const { isRequesting, installPlugins } = this.props;
if ( isRequesting ) {
return false;
}
installPlugins( plugins );
}
skipInstaller() {
getHistory().push( getNewPath( {}, '/', {} ) );
}
render() {
const { hasErrors, isRequesting } = this.props;
return hasErrors ? (
<Button isPrimary onClick={ () => location.reload() }>
{ __( 'Retry', 'woocommerce-admin' ) }
</Button>
) : (
<Fragment>
<Button isBusy={ isRequesting } isPrimary onClick={ this.installAndActivatePlugins }>
{ __( 'Install & enable', 'woocommerce-admin' ) }
</Button>
<Button onClick={ this.skipInstaller }>{ __( 'No thanks', 'woocommerce-admin' ) }</Button>
</Fragment>
);
}
}
export default compose(
withSelect( select => {
const {
getPluginInstallations,
getPluginInstallationErrors,
getPluginActivations,
getPluginActivationErrors,
isPluginActivateRequesting,
isPluginInstallRequesting,
} = select( 'wc-api' );
const isRequesting = isPluginActivateRequesting() || isPluginInstallRequesting();
const activationErrors = getPluginActivationErrors( plugins );
const activatedPlugins = Object.keys( getPluginActivations( plugins ) );
const installationErrors = getPluginInstallationErrors( plugins );
const installedPlugins = Object.keys( getPluginInstallations( plugins ) );
const errors = [];
Object.keys( activationErrors ).map( plugin =>
errors.push( activationErrors[ plugin ].message )
);
Object.keys( installationErrors ).map( plugin =>
errors.push( installationErrors[ plugin ].message )
);
const hasErrors = Boolean( errors.length );
return {
activatedPlugins,
installedPlugins,
errors,
hasErrors,
isRequesting,
};
} ),
withDispatch( dispatch => {
const { createNotice } = dispatch( 'core/notices' );
const { activatePlugins, installPlugins } = dispatch( 'wc-api' );
return {
activatePlugins,
createNotice,
installPlugins,
};
} )
)( ShippingLabels );

View File

@ -1,3 +1,18 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Onboarding namespace.
*/
export const NAMESPACE = '/wc-admin/v1';
/**
* Plugin slugs and names as key/value pairs.
*/
export const pluginNames = {
jetpack: __( 'Jetpack', 'woocommerce-admin' ),
'woocommerce-services': __( 'WooCommerce Services', 'woocommerce-admin' ),
};

View File

@ -7,6 +7,22 @@ const updateProfileItems = operations => fields => {
} );
};
const installPlugins = operations => plugins => {
const resourceKey = 'plugin-install';
operations.update( [ resourceKey ], {
[ resourceKey ]: plugins,
} );
};
const activatePlugins = operations => plugins => {
const resourceKey = 'plugin-activate';
operations.update( [ resourceKey ], {
[ resourceKey ]: plugins,
} );
};
export default {
activatePlugins,
installPlugins,
updateProfileItems,
};

View File

@ -3,20 +3,28 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { NAMESPACE } from './constants';
import { NAMESPACE, pluginNames } from './constants';
function read( resourceNames, fetch = apiFetch ) {
return [ ...readProfileItems( resourceNames, fetch ) ];
return [
...readProfileItems( resourceNames, fetch ),
...readJetpackConnectUrl( resourceNames, fetch ),
];
}
function update( resourceNames, data, fetch = apiFetch ) {
return [ ...updateProfileItems( resourceNames, data, fetch ) ];
return [
...activatePlugins( resourceNames, data, fetch ),
...installPlugins( resourceNames, data, fetch ),
...updateProfileItems( resourceNames, data, fetch ),
];
}
function readProfileItems( resourceNames, fetch ) {
@ -90,6 +98,92 @@ function profileItemToResource( items ) {
return resources;
}
function readJetpackConnectUrl( resourceNames, fetch ) {
const resourceName = 'jetpack-connect-url';
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/onboarding/plugins/connect-jetpack';
return [
fetch( {
path: url,
} )
.then( response => {
return { [ resourceName ]: { data: response.connectAction } };
} )
.catch( error => {
error.message = getPluginErrorMessage( 'activate', 'jetpack' );
return { [ resourceName ]: { error } };
} ),
];
}
return [];
}
function doPluginActions( fetch, action, plugins ) {
const resourceName = [ `plugin-${ action }` ];
return plugins.map( async plugin => {
return fetch( {
path: `${ NAMESPACE }/onboarding/plugins/${ action }`,
method: 'POST',
data: {
plugin,
},
} )
.then( response => {
return {
[ resourceName ]: { data: plugins },
[ getResourceName( resourceName, plugin ) ]: { data: response },
};
} )
.catch( error => {
error.message = getPluginErrorMessage( action, plugin );
return {
[ resourceName ]: { data: plugins },
[ getResourceName( resourceName, plugin ) ]: { error },
};
} );
} );
}
function getPluginErrorMessage( action, plugin ) {
const pluginName = pluginNames[ plugin ] || plugin;
return 'install' === action
? sprintf(
__( 'There was an error installing %s. Please try again.', 'woocommerce-admin' ),
pluginName
)
: sprintf(
__( 'There was an error activating %s. Please try again.', 'woocommerce-admin' ),
pluginName
);
}
function installPlugins( resourceNames, data, fetch ) {
const resourceName = 'plugin-install';
if ( resourceNames.includes( resourceName ) ) {
const plugins = data[ resourceName ];
return doPluginActions( fetch, 'install', plugins );
}
return [];
}
function activatePlugins( resourceNames, data, fetch ) {
const resourceName = 'plugin-activate';
if ( resourceNames.includes( resourceName ) ) {
const plugins = data[ resourceName ];
return doPluginActions( fetch, 'activate', plugins );
}
return [];
}
export default {
read,
update,

View File

@ -43,8 +43,113 @@ const isGetProfileItemsRequesting = getResource => () => {
return lastRequested > lastReceived;
};
const getJetpackConnectUrl = ( getResource, requireResource ) => (
requirement = DEFAULT_REQUIREMENT
) => {
return requireResource( requirement, 'jetpack-connect-url' ).data;
};
const getJetpackConnectUrlError = getResource => () => {
return getResource( 'jetpack-connect-url' ).error;
};
const isGetJetpackConnectUrlRequesting = getResource => () => {
const { lastReceived, lastRequested } = getResource( 'jetpack-connect-url' );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
const getPluginInstallations = getResource => plugins => {
const resourceName = 'plugin-install';
const installations = {};
plugins.forEach( plugin => {
const data = getResource( getResourceName( resourceName, plugin ) ).data;
if ( data ) {
installations[ plugin ] = data;
}
} );
return installations;
};
const getPluginActivations = getResource => plugins => {
const resourceName = 'plugin-activate';
const activations = {};
plugins.forEach( plugin => {
const data = getResource( getResourceName( resourceName, plugin ) ).data;
if ( data ) {
activations[ plugin ] = data;
}
} );
return activations;
};
const getPluginActivationErrors = getResource => plugins => {
const resourceName = 'plugin-activate';
const errors = {};
plugins.forEach( plugin => {
const error = getResource( getResourceName( resourceName, plugin ) ).error;
if ( error ) {
errors[ plugin ] = error;
}
} );
return errors;
};
const getPluginInstallationErrors = getResource => plugins => {
const resourceName = 'plugin-install';
const errors = {};
plugins.forEach( plugin => {
const error = getResource( getResourceName( resourceName, plugin ) ).error;
if ( error ) {
errors[ plugin ] = error;
}
} );
return errors;
};
const isPluginActivateRequesting = getResource => () => {
const { lastReceived, lastRequested } = getResource( 'plugin-activate' );
if ( ! isNil( lastRequested ) && isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
const isPluginInstallRequesting = getResource => () => {
const { lastReceived, lastRequested } = getResource( 'plugin-install' );
if ( ! isNil( lastRequested ) && isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getProfileItems,
getProfileItemsError,
isGetProfileItemsRequesting,
getJetpackConnectUrl,
getJetpackConnectUrlError,
isGetJetpackConnectUrlRequesting,
getPluginActivations,
getPluginInstallations,
getPluginInstallationErrors,
getPluginActivationErrors,
isPluginActivateRequesting,
isPluginInstallRequesting,
};