* Add onboarding plugin installation (step 2)

* Handle PR Feedback: Fix vertical label display on mobile, fix Jetpack connect URL generation, so we make sure the plugin is active first, add new constant for local calypso development, fix typo, refactor activatePlugin/installPlugin on the JS side, sanitize plugin name input.
This commit is contained in:
Justin Shreve 2019-05-23 08:10:42 -04:00 committed by GitHub
parent 539407bc85
commit 0e1db89f67
5 changed files with 506 additions and 26 deletions

View File

@ -2,50 +2,215 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { Component, Fragment } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { forEach } from 'lodash';
import { compose } from '@wordpress/compose';
import { withDispatch } from '@wordpress/data';
/**
* Internal depdencies
*/
import { H, Stepper } from '@woocommerce/components';
import { H, Stepper, Card } from '@woocommerce/components';
import ProfileWizardHeader from '../header';
export default class Start extends Component {
const plugins = [ 'jetpack', 'woocommerce-services' ];
class Plugins extends Component {
constructor() {
super( ...arguments );
this.state = {
step: 'install',
isPending: true,
isError: false,
pluginsInstalled: 0,
pluginsActivated: 0,
connectUrl: '',
};
this.activatePlugins = this.activatePlugins.bind( this );
}
componentDidMount() {
this.installPlugins();
}
componentDidUpdate( prevProps, prevState ) {
if (
this.state.pluginsInstalled !== prevState.pluginsInstalled &&
this.state.pluginsInstalled === plugins.length
) {
/* eslint-disable react/no-did-update-set-state */
this.setState( {
step: 'activate',
isPending: false,
} );
/* 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 ) {
event.preventDefault();
// Avoid double activating.
const { isPending } = this.state;
if ( isPending ) {
return false;
}
this.setState( {
isPending: true,
} );
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: `/wc-admin/v1/onboarding/plugins/${ action }`,
method: 'POST',
data: {
plugin,
},
} );
return pluginResponse;
} catch ( err ) {
this.props.addNotice( {
status: 'error',
message: this.getErrorMessage( action, plugin ),
} );
this.setState( {
isPending: false,
isError: true,
} );
}
}
async connectJetpack() {
try {
const connectResponse = await apiFetch( {
path: '/wc-admin/v1/onboarding/plugins/connect-jetpack',
} );
if ( connectResponse && connectResponse.connectAction ) {
window.location = connectResponse.connectAction;
return;
}
throw new Error();
} catch ( err ) {
this.props.addNotice( {
status: 'error',
message: 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' );
}
}
render() {
const { step, isPending, isError } = this.state;
return (
<Fragment>
<ProfileWizardHeader />
<div className="woocommerce-profile-wizard__container">
<H className="woocommerce-profile-wizard__header-title">
{ __( 'Installing plugins', 'woocommerce-admin' ) }
{ __( 'Install plugins', 'woocommerce-admin' ) }
</H>
<p>
{ __(
'Once Jetpack and WooCommerce Services are installed you will create or log in to a Jetpack account' +
' and connect your site to WordPress.com to enable the features on your store.',
'woocommerce-admin'
) }
</p>
<Card className="woocommerce-profile-wizard__plugins-card">
<Stepper
direction="vertical"
currentStep={ step }
isPending={ isPending }
steps={ [
{
label: __( 'Install Jetpack and WooCommerce Services', 'woocommerce-admin' ),
key: 'install',
},
{
label: __( 'Activate Jetpack and WooCommerce Services', 'woocommerce-admin' ),
key: 'activate',
},
] }
/>
<Stepper
direction="vertical"
currentStep="install"
isPending
steps={ [
{
label: __( 'Install Jetpack and WooCommerce Services', 'woocommerce-admin' ),
key: 'install',
},
{
label: __( 'Activate Jetpack and WooCommerce Services', 'woocommerce-admin' ),
key: 'activate',
},
] }
/>
<div className="woocommerce-profile-wizard__plugins-actions">
{ isError && (
<Button isPrimary onClick={ () => location.reload() }>
{ __( 'Retry', 'woocommerce-admin' ) }
</Button>
) }
{ ! isError &&
'activate' === step && (
<Button isPrimary isBusy={ isPending } onClick={ this.activatePlugins }>
{ __( 'Activate & continue', 'woocommerce-admin' ) }
</Button>
) }
</div>
</Card>
</div>
</Fragment>
);
}
}
export default compose(
withDispatch( dispatch => {
const { addNotice } = dispatch( 'wc-admin' );
return {
addNotice,
};
} )
)( Plugins );

View File

@ -102,3 +102,18 @@
padding-top: 0;
}
}
.woocommerce-profile-wizard__plugins-card {
.woocommerce-stepper {
box-shadow: none;
}
.woocommerce-profile-wizard__plugins-actions {
text-align: left;
margin-left: 64px;
button {
display: initial;
}
}
}

View File

@ -0,0 +1,288 @@
<?php
/**
* REST API Onboarding Plugins Controller
*
* Handles requests to install and activate depedent plugins.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* Onboarding Plugins Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class WC_Admin_REST_Onboarding_Plugins_Controller extends WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'onboarding/plugins';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/install',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'install_plugin' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/activate',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'activate_plugin' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/connect-jetpack',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'connect_jetpack' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_connect_schema' ),
)
);
}
/**
* Check if a given request has access to manage plugins.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage plugins.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
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(
'jetpack' => 'jetpack/jetpack.php',
'woocommerce-services' => 'woocommerce-services/woocommerce-services.php',
)
);
}
/**
* Installs the requested plugin.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Plugin Status
*/
public function install_plugin( $request ) {
$allowed_plugins = $this->get_allowed_plugins();
$plugin = sanitize_title_with_dashes( $request['plugin'] );
if ( ! in_array( $plugin, array_keys( $allowed_plugins ), true ) ) {
return new WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$slug = $plugin;
$path = $allowed_plugins[ $slug ];
$installed_plugins = get_plugins();
if ( in_array( $path, array_keys( $installed_plugins ), true ) ) {
return( array(
'slug' => $slug,
'name' => $installed_plugins[ $path ]['Name'],
'status' => 'success',
) );
}
include_once ABSPATH . '/wp-admin/includes/admin.php';
include_once ABSPATH . '/wp-admin/includes/plugin-install.php';
include_once ABSPATH . '/wp-admin/includes/plugin.php';
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';
$api = plugins_api(
'plugin_information',
array(
'slug' => sanitize_key( $slug ),
'fields' => array(
'sections' => false,
),
)
);
if ( is_wp_error( $api ) ) {
return new WP_Error( 'woocommerce_rest_plugin_install', __( 'The requested plugin could not be installed.', 'woocommerce-admin' ), 500 );
}
$upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
$result = $upgrader->install( $api->download_link );
if ( is_wp_error( $result ) || is_null( $result ) ) {
return new WP_Error( 'woocommerce_rest_plugin_install', __( 'The requested plugin could not be installed.', 'woocommerce-admin' ), 500 );
}
return array(
'slug' => $slug,
'name' => $api->name,
'status' => 'success',
);
}
/**
* Activate the requested plugin.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Plugin Status
*/
public function activate_plugin( $request ) {
$allowed_plugins = $this->get_allowed_plugins();
$plugin = sanitize_title_with_dashes( $request['plugin'] );
if ( ! in_array( $plugin, array_keys( $allowed_plugins ), true ) ) {
return new WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$slug = $plugin;
$path = $allowed_plugins[ $slug ];
$installed_plugins = get_plugins();
if ( ! in_array( $path, array_keys( $installed_plugins ), true ) ) {
return new WP_Error( 'woocommerce_rest_invalid_plugin', __( 'Invalid plugin.', 'woocommerce-admin' ), 404 );
}
$result = activate_plugin( $path );
if ( ! is_null( $result ) ) {
return new WP_Error( 'woocommerce_rest_invalid_plugin', __( 'The requested plugin could not be activated.', 'woocommerce-admin' ), 500 );
}
return( array(
'slug' => $slug,
'name' => $installed_plugins[ $path ]['Name'],
'status' => 'success',
) );
}
/**
* Generates a Jetpack Connect URL.
*
* @return array Connection URL for Jetpack
*/
public function connect_jetpack() {
if ( ! class_exists( 'Jetpack' ) ) {
return new WP_Error( 'woocommerce_rest_jetpack_not_active', __( 'Jetpack is not installed or active.', 'woocommerce-admin' ), 404 );
}
$redirect_url = esc_url_raw(
add_query_arg(
array(
'page' => 'wc-admin',
),
admin_url( 'admin.php' )
) . '#/?step=details'
);
$connect_url = Jetpack::init()->build_connect_url( true, $redirect_url, 'woocommerce-setup-wizard' );
// Redirect to local calypso instead of production.
if ( defined( 'WOOCOMMERCE_CALYPSO_LOCAL' ) && WOOCOMMERCE_CALYPSO_LOCAL ) {
$connect_url = add_query_arg(
array(
'calypso_env' => 'development',
),
$connect_url
);
}
return( array(
'slug' => $slug,
'name' => __( 'Jetpack', 'woocommerce-admin' ),
'connectAction' => $connect_url,
) );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'onboarding_plugin',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Plugin slug.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Plugin name.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Plugin status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_connect_schema() {
$schema = $this->get_item_schema();
unset( $schema['properties']['status'] );
$schema['properties']['connectAction'] = array(
'description' => __( 'Action that should be completed to connect Jetpack.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
);
return $schema;
}
}

View File

@ -110,6 +110,7 @@ class WC_Admin_Api_Init {
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-leaderboards-controller.php';
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-onboarding-levels-controller.php';
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-onboarding-profile-controller.php';
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-onboarding-plugins-controller.php';
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-orders-controller.php';
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-products-controller.php';
require_once WC_ADMIN_ABSPATH . 'includes/api/class-wc-admin-rest-product-categories-controller.php';
@ -153,6 +154,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Leaderboards_Controller',
'WC_Admin_REST_Onboarding_Levels_Controller',
'WC_Admin_REST_Onboarding_Profile_Controller',
'WC_Admin_REST_Onboarding_Plugins_Controller',
'WC_Admin_REST_Orders_Controller',
'WC_Admin_REST_Products_Controller',
'WC_Admin_REST_Product_Categories_Controller',

View File

@ -56,6 +56,7 @@
justify-content: center;
width: 24px;
height: 24px;
min-width: 24px;
margin-right: $gap-small;
background: $muriel-gray-300;
color: #fff;
@ -78,6 +79,7 @@
@include breakpoint( '<782px' ) {
.woocommerce-stepper_step-label {
display: none;
padding-top: 24px;
}
.woocommerce-stepper__step-icon {
margin-right: 0;
@ -94,5 +96,13 @@
height: 50px;
margin-left: 24px;
}
.woocommerce-stepper_step-label {
display: initial;
}
.woocommerce-stepper__step-icon {
margin-right: $gap-small;
}
}
}