diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js
index 8f50af94ce5..043b71f6968 100644
--- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js
+++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js
@@ -22,6 +22,7 @@ import { Card, H } from '@woocommerce/components';
import withSelect from 'wc-api/with-select';
import './style.scss';
import { recordEvent } from 'lib/tracks';
+import ThemeUploader from './uploader';
class Theme extends Component {
constructor() {
@@ -29,8 +30,10 @@ class Theme extends Component {
this.state = {
activeTab: 'all',
+ uploadedThemes: [],
};
+ this.handleUploadComplete = this.handleUploadComplete.bind( this );
this.onChoose = this.onChoose.bind( this );
this.onSelectTab = this.onSelectTab.bind( this );
this.openDemo = this.openDemo.bind( this );
@@ -128,17 +131,27 @@ class Theme extends Component {
}
getThemes() {
+ const { activeTab, uploadedThemes } = this.state;
const { themes } = wcSettings.onboarding;
- const { activeTab } = this.state;
+ themes.concat( uploadedThemes );
+ const allThemes = [ ...themes, ...uploadedThemes ];
switch ( activeTab ) {
case 'paid':
- return themes.filter( theme => this.getPriceValue( theme.price ) > 0 );
+ return allThemes.filter( theme => this.getPriceValue( theme.price ) > 0 );
case 'free':
- return themes.filter( theme => this.getPriceValue( theme.price ) <= 0 );
+ return allThemes.filter( theme => this.getPriceValue( theme.price ) <= 0 );
case 'all':
default:
- return themes;
+ return allThemes;
+ }
+ }
+
+ handleUploadComplete( upload ) {
+ if ( 'success' === upload.status && upload.theme_data ) {
+ this.setState( {
+ uploadedThemes: [ ...this.state.uploadedThemes, upload.theme_data ],
+ } );
}
}
@@ -175,6 +188,7 @@ class Theme extends Component {
{ () => (
{ themes && themes.map( theme => this.renderTheme( theme ) ) }
+
) }
diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss
index ffcb07f7e36..9686dfe0ecb 100644
--- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss
+++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss
@@ -116,3 +116,64 @@
}
}
}
+
+.woocommerce-profile-wizard__body .woocommerce-theme-uploader.woocommerce-card {
+ margin: 0;
+ position: relative;
+
+ .woocommerce-card__body {
+ height: 100%;
+ }
+
+ &.is-uploading .components-drop-zone__provider {
+ min-height: 382px;
+ }
+
+ .components-drop-zone__provider {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ background: #f6f6f6;
+ border: 1px dashed #b0b5b8;
+ }
+
+ .components-form-file-upload {
+ flex: 1;
+ width: 100%;
+ }
+
+ .components-form-file-upload > .components-button {
+ flex: 1;
+ flex-direction: column;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ min-height: 380px;
+
+ > .gridicon {
+ width: 48px;
+ height: 48px;
+
+ path {
+ fill: #50575d;
+ }
+ }
+
+ .dashicons-upload {
+ display: none;
+ }
+ }
+
+ .woocommerce-theme-uploader__title {
+ margin: $gap-smaller 0;
+ @include font-size(24);
+ font-weight: 400;
+ }
+
+ p {
+ @include font-size(14);
+ margin: 0;
+ }
+}
diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js
new file mode 100644
index 00000000000..a7f0446d5c7
--- /dev/null
+++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js
@@ -0,0 +1,124 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import apiFetch from '@wordpress/api-fetch';
+import classnames from 'classnames';
+import { Component, Fragment } from '@wordpress/element';
+import { compose } from '@wordpress/compose';
+import { DropZoneProvider, DropZone, FormFileUpload } from '@wordpress/components';
+import Gridicon from 'gridicons';
+import { noop } from 'lodash';
+import PropTypes from 'prop-types';
+import { withDispatch } from '@wordpress/data';
+
+/**
+ * WooCommerce dependencies
+ */
+import { Card, H, Spinner } from '@woocommerce/components';
+
+class ThemeUploader extends Component {
+ constructor() {
+ super();
+
+ this.state = {
+ isUploading: false,
+ };
+
+ this.handleFilesUpload = this.handleFilesUpload.bind( this );
+ this.handleFilesDrop = this.handleFilesDrop.bind( this );
+ }
+
+ handleFilesDrop( files ) {
+ const file = files[ 0 ];
+ this.uploadTheme( file );
+ }
+
+ handleFilesUpload( e ) {
+ const file = e.target.files[ 0 ];
+ this.uploadTheme( file );
+ }
+
+ uploadTheme( file ) {
+ const { addNotice, onUploadComplete } = this.props;
+ this.setState( { isUploading: true } );
+
+ const body = new FormData();
+ body.append( 'pluginzip', file );
+
+ return apiFetch( { path: '/wc-admin/v1/themes', method: 'POST', body } )
+ .then( response => {
+ onUploadComplete( response );
+ this.setState( { isUploading: false } );
+ addNotice( { status: response.status, message: response.message } );
+ } )
+ .catch( error => {
+ this.setState( { isUploading: false } );
+ if ( error && error.message ) {
+ addNotice( { status: 'error', message: error.message } );
+ }
+ } );
+ }
+
+ render() {
+ const { className } = this.props;
+ const { isUploading } = this.state;
+
+ const classes = classnames( 'woocommerce-theme-uploader', className, {
+ 'is-uploading': isUploading,
+ } );
+
+ return (
+
+
+ { ! isUploading ? (
+
+
+
+
+ { __( 'Upload a theme', 'woocommerce-admin' ) }
+
+ { __( 'Drop a theme zip file here to upload', 'woocommerce-admin' ) }
+
+
+
+ ) : (
+
+
+
+ { __( 'Uploading theme', 'woocommerce-admin' ) }
+
+ { __( 'Your theme is being uploaded', 'woocommerce-admin' ) }
+
+ ) }
+
+
+ );
+ }
+}
+
+ThemeUploader.propTypes = {
+ /**
+ * Additional class name to style the component.
+ */
+ className: PropTypes.string,
+ /**
+ * Function called when an upload has finished.
+ */
+ onUploadComplete: PropTypes.func,
+};
+
+ThemeUploader.defaultProps = {
+ onUploadComplete: noop,
+};
+
+export default compose(
+ withDispatch( dispatch => {
+ const { addNotice } = dispatch( 'wc-admin' );
+ return { addNotice };
+ } )
+)( ThemeUploader );
diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php
index 8584c48ff6c..df7782cfcb7 100644
--- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php
+++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php
@@ -89,11 +89,19 @@ class WC_Admin_REST_Themes_Controller extends WC_REST_Data_Controller {
}
if ( ! is_wp_error( $install ) && isset( $install['destination_name'] ) ) {
+ $theme = $install['destination_name'];
$result = array(
'status' => 'success',
'message' => $upgrader->strings['process_success'],
- 'theme' => $install['destination_name'],
+ 'theme' => $theme,
);
+
+ /**
+ * Fires when a theme is successfully installed.
+ *
+ * @param string $theme The theme name.
+ */
+ do_action( 'woocommerce_theme_installed', $theme );
} else {
if ( is_wp_error( $install ) && $install->get_error_code() ) {
$error_message = isset( $upgrader->strings[ $install->get_error_code() ] ) ? $upgrader->strings[ $install->get_error_code() ] : $install->get_error_data();
diff --git a/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php b/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php
index 64aa6548900..56de15f8395 100644
--- a/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php
+++ b/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php
@@ -17,6 +17,20 @@ class WC_Admin_Onboarding {
*/
protected static $instance = null;
+ /**
+ * Name of themes transient.
+ *
+ * @var string
+ */
+ const THEMES_TRANSIENT = 'wc_onboarding_themes';
+
+ /**
+ * Name of product data transient.
+ *
+ * @var string
+ */
+ const PRODUCT_DATA_TRANSIENT = 'wc_onboarding_product_data';
+
/**
* Get class instance.
*/
@@ -32,6 +46,8 @@ class WC_Admin_Onboarding {
*/
public function __construct() {
add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // Run after WC_Admin_Loader.
+ add_action( 'woocommerce_theme_installed', array( $this, 'delete_themes_transient' ) );
+ add_action( 'after_switch_theme', array( $this, 'delete_themes_transient' ) );
add_filter( 'woocommerce_admin_is_loading', array( $this, 'is_loading' ) );
add_filter( 'woocommerce_rest_prepare_themes', array( $this, 'add_uploaded_theme_data' ) );
}
@@ -116,8 +132,7 @@ class WC_Admin_Onboarding {
* @return array
*/
public static function get_themes() {
- $themes_transient_name = 'wc_onboarding_themes';
- $themes = get_transient( $themes_transient_name );
+ $themes = get_transient( self::THEMES_TRANSIENT );
if ( false === $themes ) {
$theme_data = wp_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search?category=themes' );
$themes = array();
@@ -152,7 +167,7 @@ class WC_Admin_Onboarding {
$themes = array( $active_theme => $themes[ $active_theme ] ) + $themes;
- set_transient( $themes_transient_name, $themes, DAY_IN_SECONDS );
+ set_transient( self::THEMES_TRANSIENT, $themes, DAY_IN_SECONDS );
}
$themes = apply_filters( 'woocommerce_admin_onboarding_themes', $themes );
@@ -221,15 +236,14 @@ class WC_Admin_Onboarding {
* @return array
*/
public static function append_product_data( $product_types ) {
- $product_data_transient_name = 'wc_onboarding_product_data';
- $woocommerce_products = get_transient( $product_data_transient_name );
+ $woocommerce_products = get_transient( self::PRODUCT_DATA_TRANSIENT );
if ( false === $woocommerce_products ) {
$woocommerce_products = wp_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search?category=product-type' );
if ( is_wp_error( $woocommerce_products ) ) {
return $product_types;
}
- set_transient( $product_data_transient_name, $woocommerce_products, DAY_IN_SECONDS );
+ set_transient( self::PRODUCT_DATA_TRANSIENT, $woocommerce_products, DAY_IN_SECONDS );
}
$product_data = json_decode( $woocommerce_products['body'] );
@@ -253,6 +267,13 @@ class WC_Admin_Onboarding {
return $product_types;
}
+ /**
+ * Delete the stored themes transient.
+ */
+ public static function delete_themes_transient() {
+ delete_transient( self::THEMES_TRANSIENT );
+ }
+
/**
* Add profiler items to component settings.
*