Onboarding - Profile Wizard: Update plugin installation step to deal with previously installed plugins (https://github.com/woocommerce/woocommerce-admin/pull/2825)

* Handle previously installed plugins during the onboarding wizard

* Allow the activate endpoint to activate multiple plugins at once, avoiding a race condition.

* Handle PR feedback

* Add the ability to fetch active plugins via wc-api
This commit is contained in:
Justin Shreve 2019-08-23 08:56:57 -04:00 committed by GitHub
parent 2b188369b3
commit fe585aa2ee
9 changed files with 453 additions and 139 deletions

View File

@ -2,21 +2,32 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { Button } from 'newspack-components'; import { Button } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose'; import { compose } from '@wordpress/compose';
import { difference } from 'lodash'; import { difference, get } from 'lodash';
import { withDispatch } from '@wordpress/data'; import { withDispatch } from '@wordpress/data';
/**
* WooCommerce depdencies
*/
import { H, Stepper, Card } from '@woocommerce/components';
import { updateQueryString } from '@woocommerce/navigation';
/** /**
* Internal depdencies * Internal depdencies
*/ */
import { H, Stepper, Card } from '@woocommerce/components';
import { recordEvent } from 'lib/tracks'; import { recordEvent } from 'lib/tracks';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
import { pluginNames } from 'wc-api/onboarding/constants';
const plugins = [ 'jetpack', 'woocommerce-services' ]; const pluginsToInstall = [ 'jetpack', 'woocommerce-services' ];
// We want to use the cached version of activePlugins here, otherwise the list we are dealing with could update as plugins are activated.
const plugins = difference(
pluginsToInstall,
get( wcSettings, [ 'onboarding', 'activePlugins' ], [] )
);
class Plugins extends Component { class Plugins extends Component {
constructor() { constructor() {
@ -30,11 +41,20 @@ class Plugins extends Component {
} }
componentDidMount() { componentDidMount() {
if ( 0 === plugins.length ) {
return updateQueryString( { step: 'store-details' } );
}
this.props.installPlugins( plugins ); this.props.installPlugins( plugins );
} }
componentDidUpdate( prevProps ) { componentDidUpdate( prevProps ) {
const { createNotice, errors, installedPlugins, jetpackConnectUrl } = this.props; const {
createNotice,
errors,
installedPlugins,
activatedPlugins,
jetpackConnectUrl,
} = this.props;
if ( jetpackConnectUrl ) { if ( jetpackConnectUrl ) {
window.location = jetpackConnectUrl; window.location = jetpackConnectUrl;
@ -51,6 +71,17 @@ class Plugins extends Component {
this.setState( { step: 'activate' } ); this.setState( { step: 'activate' } );
/* eslint-enable react/no-did-update-set-state */ /* eslint-enable react/no-did-update-set-state */
} }
// If Jetpack was already connected, we can go to store details after WCS is activated.
if (
! plugins.includes( 'jetpack' ) &&
prevProps.activatedPlugins.length !== plugins.length &&
activatedPlugins.length === plugins.length
) {
/* eslint-disable react/no-did-update-set-state */
return updateQueryString( { step: 'store-details' } );
/* eslint-enable react/no-did-update-set-state */
}
} }
async activatePlugins( event ) { async activatePlugins( event ) {
@ -71,6 +102,10 @@ class Plugins extends Component {
const { hasErrors, isRequesting } = this.props; const { hasErrors, isRequesting } = this.props;
const { step } = this.state; const { step } = this.state;
const pluginLabel = plugins.includes( 'jetpack' )
? Object.values( pluginNames ).join( ' & ' )
: pluginNames[ 'woocommerce-services' ];
return ( return (
<Fragment> <Fragment>
<H className="woocommerce-profile-wizard__header-title"> <H className="woocommerce-profile-wizard__header-title">
@ -84,11 +119,11 @@ class Plugins extends Component {
isPending={ isRequesting && ! hasErrors } isPending={ isRequesting && ! hasErrors }
steps={ [ steps={ [
{ {
label: __( 'Install Jetpack and WooCommerce Services', 'woocommerce-admin' ), label: sprintf( __( 'Install %s', 'woocommerce-admin' ), pluginLabel ),
key: 'install', key: 'install',
}, },
{ {
label: __( 'Activate Jetpack and WooCommerce Services', 'woocommerce-admin' ), label: sprintf( __( 'Activate %s', 'woocommerce-admin' ), pluginLabel ),
key: 'activate', key: 'activate',
}, },
] } ] }
@ -150,7 +185,7 @@ export default compose(
errors.push( installationErrors[ plugin ].message ) errors.push( installationErrors[ plugin ].message )
); );
if ( jetpackConnectUrlError ) { if ( jetpackConnectUrlError ) {
errors.push( jetpackConnectUrlError ); errors.push( jetpackConnectUrlError.message );
} }
const hasErrors = Boolean( errors.length ); const hasErrors = Boolean( errors.length );

View File

@ -0,0 +1,33 @@
/** @format */
/*eslint-disable max-len*/
export default () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<mask
id="card_mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="2"
y="4"
width="20"
height="16"
>
<path
id="icon/action/payment_24px_2"
fillRule="evenodd"
clipRule="evenodd"
d="M20 4H4C2.89 4 2.01 4.89 2.01 6L2 18C2 19.11 2.89 20 4 20H20C21.11 20 22 19.11 22 18V6C22 4.89 21.11 4 20 4ZM20 18H4V12H20V18ZM4 8H20V6H4V8Z"
fill="white"
/>
</mask>
<g mask="url(#card_mask)">
<g>
<rect width="24" height="24" fill="#50575D" />
</g>
</g>
</g>
</svg>
);
/*eslint-enable max-len*/

View File

@ -0,0 +1,32 @@
/** @format */
/*eslint-disable max-len*/
export default () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<mask
id="print_mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="2"
y="3"
width="20"
height="18"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19 8H18V3H6V8H5C3.34 8 2 9.34 2 11V17H6V21H18V17H22V11C22 9.34 20.66 8 19 8ZM8 5H16V8H8V5ZM16 19V17V15H8V19H16ZM18 15V13H6V15H4V11C4 10.45 4.45 10 5 10H19C19.55 10 20 10.45 20 11V15H18ZM17 11.5C17 10.9477 17.4477 10.5 18 10.5C18.5523 10.5 19 10.9477 19 11.5C19 12.0523 18.5523 12.5 18 12.5C17.4477 12.5 17 12.0523 17 11.5Z"
fill="white"
/>
</mask>
<g mask="url(#print_mask)">
<g>
<rect width="24" height="24" fill="#50575D" />
</g>
</g>
</g>
</svg>
);
/*eslint-enable max-len*/

View File

@ -2,60 +2,33 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { FormToggle } from '@wordpress/components'; import { FormToggle } from '@wordpress/components';
import { Button, CheckboxControl } from 'newspack-components'; import { Button, CheckboxControl } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose'; import { compose } from '@wordpress/compose';
import interpolateComponents from 'interpolate-components'; import interpolateComponents from 'interpolate-components';
import { withDispatch } from '@wordpress/data'; import { withDispatch } from '@wordpress/data';
import { filter } from 'lodash';
/**
* WooCommerce depdencies
*/
import { Card, H, Link } from '@woocommerce/components';
import { updateQueryString } from '@woocommerce/navigation';
/** /**
* Internal depdencies * Internal depdencies
*/ */
import { Card, H, Link } from '@woocommerce/components'; import CardIcon from './images/card';
import SecurityIcon from './images/security'; import SecurityIcon from './images/security';
import SalesTaxIcon from './images/local_atm'; import SalesTaxIcon from './images/local_atm';
import SpeedIcon from './images/flash_on'; import SpeedIcon from './images/flash_on';
import MobileAppIcon from './images/phone_android'; import MobileAppIcon from './images/phone_android';
import PrintIcon from './images/print';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
import { recordEvent } from 'lib/tracks'; import { recordEvent } from 'lib/tracks';
const benefits = [
{
title: __( 'Security', 'woocommerce-admin' ),
icon: <SecurityIcon />,
description: __(
'Jetpack automatically blocks brute force attacks to protect your store from unauthorized access.',
'woocommerce-admin'
),
},
{
title: __( 'Sales Tax', 'woocommerce-admin' ),
icon: <SalesTaxIcon />,
description: __(
'With WooCommerce Services we ensure that the correct rate of tax is charged on all of your orders.',
'woocommerce-admin'
),
},
{
title: __( 'Speed', 'woocommerce-admin' ),
icon: <SpeedIcon />,
description: __(
'Cache your images and static files on our own powerful global network of servers and speed up your site.',
'woocommerce-admin'
),
},
{
title: __( 'Mobile App', 'woocommerce-admin' ),
icon: <MobileAppIcon />,
description: __(
'Your store in your pocket. Manage orders, receive sales notifications, and more. Only with a Jetpack connection.',
'woocommerce-admin'
),
},
];
class Start extends Component { class Start extends Component {
constructor() { constructor() {
super( ...arguments ); super( ...arguments );
@ -69,6 +42,15 @@ class Start extends Component {
this.skipWizard = this.skipWizard.bind( this ); this.skipWizard = this.skipWizard.bind( this );
} }
componentDidMount() {
if (
this.props.activePlugins.includes( 'jetpack' ) &&
this.props.activePlugins.includes( 'woocommerce-services' )
) {
return updateQueryString( { step: 'store-details' } );
}
}
async updateTracking() { async updateTracking() {
const { updateSettings } = this.props; const { updateSettings } = this.props;
const allowTracking = this.state.allowTracking ? 'yes' : 'no'; const allowTracking = this.state.allowTracking ? 'yes' : 'no';
@ -128,8 +110,84 @@ class Start extends Component {
); );
} }
getBenefits() {
const { activePlugins } = this.props;
return [
{
title: __( 'Security', 'woocommerce-admin' ),
icon: <SecurityIcon />,
description: __(
'Jetpack automatically blocks brute force attacks to protect your store from unauthorized access.',
'woocommerce-admin'
),
visible: ! activePlugins.includes( 'jetpack' ),
},
{
title: __( 'Sales Tax', 'woocommerce-admin' ),
icon: <SalesTaxIcon />,
description: __(
'With WooCommerce Services we ensure that the correct rate of tax is charged on all of your orders.',
'woocommerce-admin'
),
visible: true,
},
{
title: __( 'Speed', 'woocommerce-admin' ),
icon: <SpeedIcon />,
description: __(
'Cache your images and static files on our own powerful global network of servers and speed up your site.',
'woocommerce-admin'
),
visible: ! activePlugins.includes( 'jetpack' ),
},
{
title: __( 'Mobile App', 'woocommerce-admin' ),
icon: <MobileAppIcon />,
description: __(
'Your store in your pocket. Manage orders, receive sales notifications, and more. Only with a Jetpack connection.',
'woocommerce-admin'
),
visible: ! activePlugins.includes( 'jetpack' ),
},
{
title: __( 'Print your own shipping labels', 'woocommerce-admin' ),
icon: <PrintIcon />,
description: __(
'Save time at the Post Office by printing USPS shipping labels at home.',
'woocommerce-admin'
),
visible:
activePlugins.includes( 'jetpack' ) && ! activePlugins.includes( 'woocommerce-services' ),
},
{
title: __( 'Simple payment setup', 'woocommerce-admin' ),
icon: <CardIcon />,
description: __(
'WooCommerce Services enables us to provision Stripe and Paypal accounts quickly and easily for you.',
'woocommerce-admin'
),
visible:
activePlugins.includes( 'jetpack' ) && ! activePlugins.includes( 'woocommerce-services' ),
},
];
}
renderBenefits() {
return (
<div className="woocommerce-profile-wizard__benefits">
{ filter( this.getBenefits(), benefit => benefit.visible ).map( benefit =>
this.renderBenefit( benefit )
) }
</div>
);
}
render() { render() {
const { allowTracking } = this.state; const { allowTracking } = this.state;
const { activePlugins } = this.props;
const pluginNames = activePlugins.includes( 'jetpack' )
? __( 'WooCommerce Services', 'woocommerce-admin' )
: __( 'Jetpack & WooCommerce Services', 'woocommerce-admin' );
const trackingLabel = interpolateComponents( { const trackingLabel = interpolateComponents( {
mixedString: __( mixedString: __(
@ -151,10 +209,13 @@ class Start extends Component {
<p> <p>
{ interpolateComponents( { { interpolateComponents( {
mixedString: __( mixedString: sprintf(
'Simplify and enhance the setup of your store with the free features and benefits offered by ' + __(
'{{strong}}Jetpack & WooCommerce Services{{/strong}}.', 'Simplify and enhance the setup of your store with the free features and benefits offered by ' +
'woocommerce-admin' '{{strong}}%s{{/strong}}.',
'woocommerce-admin'
),
pluginNames
), ),
components: { components: {
strong: <strong />, strong: <strong />,
@ -163,9 +224,7 @@ class Start extends Component {
</p> </p>
<Card> <Card>
<div className="woocommerce-profile-wizard__benefits"> { this.renderBenefits() }
{ benefits.map( benefit => this.renderBenefit( benefit ) ) }
</div>
<div className="woocommerce-profile-wizard__tracking"> <div className="woocommerce-profile-wizard__tracking">
<CheckboxControl <CheckboxControl
@ -195,7 +254,7 @@ class Start extends Component {
<p> <p>
<Button isLink className="woocommerce-profile-wizard__skip" onClick={ this.skipWizard }> <Button isLink className="woocommerce-profile-wizard__skip" onClick={ this.skipWizard }>
{ __( 'Proceed without Jetpack or WooCommerce Services', 'woocommerce-admin' ) } { sprintf( __( 'Proceed without %s', 'woocommerce-admin' ), pluginNames ) }
</Button> </Button>
</p> </p>
</Fragment> </Fragment>
@ -205,15 +264,27 @@ class Start extends Component {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getProfileItemsError, getSettings, getSettingsError, isGetSettingsRequesting } = select( const {
'wc-api' getProfileItemsError,
); getSettings,
getSettingsError,
isGetSettingsRequesting,
getActivePlugins,
} = select( 'wc-api' );
const isSettingsError = Boolean( getSettingsError( 'advanced' ) ); const isSettingsError = Boolean( getSettingsError( 'advanced' ) );
const isSettingsRequesting = isGetSettingsRequesting( 'advanced' ); const isSettingsRequesting = isGetSettingsRequesting( 'advanced' );
const isProfileItemsError = Boolean( getProfileItemsError() ); const isProfileItemsError = Boolean( getProfileItemsError() );
return { getSettings, isSettingsError, isProfileItemsError, isSettingsRequesting }; const activePlugins = getActivePlugins();
return {
getSettings,
isSettingsError,
isProfileItemsError,
isSettingsRequesting,
activePlugins,
};
} ), } ),
withDispatch( dispatch => { withDispatch( dispatch => {
const { updateProfileItems, updateSettings } = dispatch( 'wc-api' ); const { updateProfileItems, updateSettings } = dispatch( 'wc-api' );

View File

@ -263,7 +263,7 @@
.woocommerce-profile-wizard__plugins-card { .woocommerce-profile-wizard__plugins-card {
.woocommerce-profile-wizard__plugins-actions { .woocommerce-profile-wizard__plugins-actions {
text-align: left; text-align: left;
margin-left: 64px; margin-left: 44px;
min-height: 28px; min-height: 28px;
button.muriel-button { button.muriel-button {
@ -271,6 +271,7 @@
height: 40px; height: 40px;
min-width: auto; min-width: auto;
display: initial; display: initial;
margin-right: $gap-small;
} }
} }
} }

View File

@ -14,6 +14,7 @@ import { NAMESPACE, pluginNames } from './constants';
function read( resourceNames, fetch = apiFetch ) { function read( resourceNames, fetch = apiFetch ) {
return [ return [
...readActivePlugins( resourceNames, fetch ),
...readProfileItems( resourceNames, fetch ), ...readProfileItems( resourceNames, fetch ),
...readJetpackConnectUrl( resourceNames, fetch ), ...readJetpackConnectUrl( resourceNames, fetch ),
]; ];
@ -98,6 +99,77 @@ function profileItemToResource( items ) {
return resources; return resources;
} }
function readActivePlugins( resourceNames, fetch ) {
const resourceName = 'active-plugins';
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/onboarding/plugins/active';
return [
fetch( { path: url } )
.then( activePluginsToResources )
.catch( error => {
return { [ resourceName ]: { error: String( error.message ) } };
} ),
];
}
return [];
}
function activePluginsToResources( items ) {
const { plugins } = items;
const resourceName = 'active-plugins';
return {
[ resourceName ]: {
data: plugins,
},
};
}
function activatePlugins( resourceNames, data, fetch ) {
const resourceName = 'plugin-activate';
if ( resourceNames.includes( resourceName ) ) {
const plugins = data[ resourceName ];
const url = NAMESPACE + '/onboarding/plugins/activate';
return [
fetch( {
path: url,
method: 'POST',
data: {
plugins: plugins.join( ',' ),
},
} )
.then( response => activatePluginToResource( response, plugins ) )
.catch( error => {
const resources = { [ resourceName ]: { error } };
Object.keys( plugins ).forEach( key => {
const pluginError = { ...error };
const item = plugins[ key ];
pluginError.message = getPluginErrorMessage( 'activate', item );
resources[ getResourceName( resourceName, item ) ] = { error: pluginError };
} );
return resources;
} ),
];
}
return [];
}
function activatePluginToResource( response, items ) {
const resourceName = 'plugin-activate';
const resources = {
[ resourceName ]: { data: items },
[ 'active-plugins' ]: { data: response.active },
};
Object.keys( items ).forEach( key => {
const item = items[ key ];
resources[ getResourceName( resourceName, item ) ] = { data: item };
} );
return resources;
}
function readJetpackConnectUrl( resourceNames, fetch ) { function readJetpackConnectUrl( resourceNames, fetch ) {
const resourceName = 'jetpack-connect-url'; const resourceName = 'jetpack-connect-url';
@ -112,7 +184,7 @@ function readJetpackConnectUrl( resourceNames, fetch ) {
return { [ resourceName ]: { data: response.connectAction } }; return { [ resourceName ]: { data: response.connectAction } };
} ) } )
.catch( error => { .catch( error => {
error.message = getPluginErrorMessage( 'activate', 'jetpack' ); error.message = getPluginErrorMessage( 'connect', 'jetpack' );
return { [ resourceName ]: { error } }; return { [ resourceName ]: { error } };
} ), } ),
]; ];
@ -121,64 +193,55 @@ function readJetpackConnectUrl( resourceNames, fetch ) {
return []; 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 ) { function getPluginErrorMessage( action, plugin ) {
const pluginName = pluginNames[ plugin ] || plugin; const pluginName = pluginNames[ plugin ] || plugin;
switch ( action ) {
return 'install' === action case 'install':
? sprintf( return sprintf(
__( 'There was an error installing %s. Please try again.', 'woocommerce-admin' ), __( 'There was an error installing %s. Please try again.', 'woocommerce-admin' ),
pluginName pluginName
) );
: sprintf( case 'connect':
return sprintf(
__( 'There was an error connecting to %s. Please try again.', 'woocommerce-admin' ),
pluginName
);
case 'activate':
default:
return sprintf(
__( 'There was an error activating %s. Please try again.', 'woocommerce-admin' ), __( 'There was an error activating %s. Please try again.', 'woocommerce-admin' ),
pluginName pluginName
); );
}
} }
function installPlugins( resourceNames, data, fetch ) { function installPlugins( resourceNames, data, fetch ) {
const resourceName = 'plugin-install'; const resourceName = 'plugin-install';
if ( resourceNames.includes( resourceName ) ) { if ( resourceNames.includes( resourceName ) ) {
const plugins = data[ resourceName ]; const plugins = data[ resourceName ];
return doPluginActions( fetch, 'install', plugins );
}
return []; return plugins.map( async plugin => {
} return fetch( {
path: `${ NAMESPACE }/onboarding/plugins/install`,
function activatePlugins( resourceNames, data, fetch ) { method: 'POST',
const resourceName = 'plugin-activate'; data: {
plugin,
if ( resourceNames.includes( resourceName ) ) { },
const plugins = data[ resourceName ]; } )
return doPluginActions( fetch, 'activate', plugins ); .then( response => {
return {
[ resourceName ]: { data: plugins },
[ getResourceName( resourceName, plugin ) ]: { data: response },
};
} )
.catch( error => {
error.message = getPluginErrorMessage( 'install', pluginNames[ plugin ] || plugin );
return {
[ resourceName ]: { data: plugins },
[ getResourceName( resourceName, plugin ) ]: { error },
};
} );
} );
} }
return []; return [];

View File

@ -77,6 +77,32 @@ const getPluginInstallations = getResource => plugins => {
return installations; return installations;
}; };
const getActivePlugins = ( getResource, requireResource ) => (
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = 'active-plugins';
const data = requireResource( requirement, resourceName ).data || [];
if ( ! data.length ) {
return wcSettings.onboarding.activePlugins;
}
return data;
};
const getActivePluginsError = getResource => () => {
return getResource( 'active-plugins' ).error;
};
const isGetActivePluginsRequesting = getResource => () => {
const { lastReceived, lastRequested } = getResource( 'active-plugins' );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
const getPluginActivations = getResource => plugins => { const getPluginActivations = getResource => plugins => {
const resourceName = 'plugin-activate'; const resourceName = 'plugin-activate';
@ -146,6 +172,9 @@ export default {
getJetpackConnectUrl, getJetpackConnectUrl,
getJetpackConnectUrlError, getJetpackConnectUrlError,
isGetJetpackConnectUrlRequesting, isGetJetpackConnectUrlRequesting,
getActivePlugins,
getActivePluginsError,
isGetActivePluginsRequesting,
getPluginActivations, getPluginActivations,
getPluginInstallations, getPluginInstallations,
getPluginInstallationErrors, getPluginInstallationErrors,

View File

@ -8,6 +8,7 @@
*/ */
namespace Automattic\WooCommerce\Admin\API; namespace Automattic\WooCommerce\Admin\API;
use Automattic\WooCommerce\Admin\Features\Onboarding;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
@ -49,13 +50,26 @@ class OnboardingPlugins extends \WC_REST_Data_Controller {
) )
); );
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/active',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'active_plugins' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route( register_rest_route(
$this->namespace, $this->namespace,
'/' . $this->rest_base . '/activate', '/' . $this->rest_base . '/activate',
array( array(
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'activate_plugin' ), 'callback' => array( $this, 'activate_plugins' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ),
), ),
'schema' => array( $this, 'get_item_schema' ), 'schema' => array( $this, 'get_item_schema' ),
@ -115,20 +129,6 @@ class OnboardingPlugins extends \WC_REST_Data_Controller {
return true; return true;
} }
/**
* Get an array of plugins that can be installed & activated via the endpoints.
*/
public function get_allowed_plugins() {
return apply_filters(
'woocommerce_onboarding_plugins_whitelist',
array(
'facebook-for-woocommerce' => 'facebook-for-woocommerce/facebook-for-woocommerce.php',
'jetpack' => 'jetpack/jetpack.php',
'woocommerce-services' => 'woocommerce-services/woocommerce-services.php',
)
);
}
/** /**
* Installs the requested plugin. * Installs the requested plugin.
* *
@ -136,7 +136,7 @@ class OnboardingPlugins extends \WC_REST_Data_Controller {
* @return array Plugin Status * @return array Plugin Status
*/ */
public function install_plugin( $request ) { public function install_plugin( $request ) {
$allowed_plugins = $this->get_allowed_plugins(); $allowed_plugins = Onboarding::get_allowed_plugins();
$plugin = sanitize_title_with_dashes( $request['plugin'] ); $plugin = sanitize_title_with_dashes( $request['plugin'] );
if ( ! in_array( $plugin, array_keys( $allowed_plugins ), true ) ) { if ( ! in_array( $plugin, array_keys( $allowed_plugins ), true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 ); return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
@ -190,37 +190,54 @@ class OnboardingPlugins extends \WC_REST_Data_Controller {
); );
} }
/**
* Returns a list of active plugins.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Active plugins
*/
public function active_plugins( $request ) {
$plugins = Onboarding::get_active_plugins();
return( array(
'plugins' => array_values( $plugins ),
) );
}
/** /**
* Activate the requested plugin. * Activate the requested plugin.
* *
* @param WP_REST_Request $request Full details about the request. * @param WP_REST_Request $request Full details about the request.
* @return array Plugin Status * @return array Plugin Status
*/ */
public function activate_plugin( $request ) { public function activate_plugins( $request ) {
$allowed_plugins = $this->get_allowed_plugins(); $allowed_plugins = Onboarding::get_allowed_plugins();
$plugin = sanitize_title_with_dashes( $request['plugin'] ); $_plugins = explode( ',', $request['plugins'] );
if ( ! in_array( $plugin, array_keys( $allowed_plugins ), true ) ) { $plugins = array_intersect( array_keys( $allowed_plugins ), $_plugins );
return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
if ( empty( $plugins ) || ! is_array( $plugins ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Invalid plugins.', 'woocommerce-admin' ), 404 );
} }
require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-admin/includes/plugin.php';
$slug = $plugin; foreach( $plugins as $plugin ) {
$path = $allowed_plugins[ $slug ]; $slug = $plugin;
$installed_plugins = get_plugins(); $path = $allowed_plugins[ $slug ];
$installed_plugins = get_plugins();
if ( ! in_array( $path, array_keys( $installed_plugins ), true ) ) { if ( ! in_array( $path, array_keys( $installed_plugins ), true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 ); return new \WP_Error( 'woocommerce_rest_invalid_plugin', sprintf( __( 'Invalid plugin %s.', 'woocommerce-admin' ), $slug ), 404 );
} }
$result = activate_plugin( $path ); $result = activate_plugin( $path );
if ( ! is_null( $result ) ) { if ( ! is_null( $result ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_plugin', __( 'The requested plugin could not be activated.', 'woocommerce-admin' ), 500 ); return new \WP_Error( 'woocommerce_rest_invalid_plugin', sprintf( __( 'The requested plugins could not be activated.', 'woocommerce-admin' ), $slug ), 500 );
}
} }
return( array( return( array(
'slug' => $slug, 'activatedPlugins' => array_values( $plugins ),
'name' => $installed_plugins[ $path ]['Name'], 'active' => Onboarding::get_active_plugins(),
'status' => 'success', 'status' => 'success',
) ); ) );
} }
@ -231,7 +248,7 @@ class OnboardingPlugins extends \WC_REST_Data_Controller {
* @return array Connection URL for Jetpack * @return array Connection URL for Jetpack
*/ */
public function connect_jetpack() { public function connect_jetpack() {
if ( ! class_exists( 'Jetpack' ) ) { if ( ! class_exists( '\Jetpack' ) ) {
return new \WP_Error( 'woocommerce_rest_jetpack_not_active', __( 'Jetpack is not installed or active.', 'woocommerce-admin' ), 404 ); return new \WP_Error( 'woocommerce_rest_jetpack_not_active', __( 'Jetpack is not installed or active.', 'woocommerce-admin' ), 404 );
} }

View File

@ -311,14 +311,47 @@ class Onboarding {
// Only fetch if the onboarding wizard is incomplete. // Only fetch if the onboarding wizard is incomplete.
if ( $this->should_show_profiler() ) { if ( $this->should_show_profiler() ) {
$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' );
$settings['onboarding']['activePlugins'] = self::get_active_plugins();
} }
return $settings; return $settings;
} }
/**
* Gets an array of plugins that can be installed & activated via the onboarding wizard.
*
* @todo Handle edgecase of where installed plugins may have versioned folder names (i.e. `jetpack-master/jetpack.php`).
*/
public static function get_allowed_plugins() {
return apply_filters(
'woocommerce_onboarding_plugins_whitelist',
array(
'jetpack' => 'jetpack/jetpack.php',
'woocommerce-services' => 'woocommerce-services/woocommerce-services.php',
)
);
}
/**
* Get a list of active plugins, relevent to the onboarding wizard.
*
* @return array
*/
public static function get_active_plugins() {
$all_active_plugins = get_option( 'active_plugins', array() );
$allowed_plugins = self::get_allowed_plugins();
$active_plugin_files = array_intersect( $all_active_plugins, $allowed_plugins );
$allowed_plugin_slugs = array_flip( $allowed_plugins );
$active_plugins = array();
foreach ( $active_plugin_files as $file ) {
$slug = $allowed_plugin_slugs[ $file ];
$active_plugins[] = $slug;
}
return $active_plugins;
}
/** /**
* Let the app know that we will be showing the onboarding route, so wp-admin elements should be hidden while loading. * Let the app know that we will be showing the onboarding route, so wp-admin elements should be hidden while loading.
* *