Add Jetpack connection to plugin benefits step (https://github.com/woocommerce/woocommerce-admin/pull/4374)

* Allow updateActivePlugins to add new plugins instead of replace

* Add autoConnect prop to Jetpack Connect component

* Add connect component to plugin benefits screen

* Add onError and missing prop types for Connect

* Update redirect URL after Jetpack connection

* Add tests for added active plugins

* Skip install if plugin error exists

* Update active and installed plugins to use replace flag

* Update tests to handle replace flag

* Refactor plugin install flow pending state
This commit is contained in:
Joshua T Flowers 2020-05-19 03:47:25 +03:00 committed by GitHub
parent 35616d5f22
commit 65145bf92c
7 changed files with 173 additions and 46 deletions

View File

@ -21,20 +21,44 @@ class Connect extends Component {
props.setIsPending( true ); props.setIsPending( true );
} }
componentDidMount() {
const { autoConnect, jetpackConnectUrl } = this.props;
if ( autoConnect && jetpackConnectUrl ) {
this.connectJetpack();
}
}
componentDidUpdate( prevProps ) { componentDidUpdate( prevProps ) {
const { createNotice, error, isRequesting, setIsPending } = this.props; const {
autoConnect,
createNotice,
error,
isRequesting,
jetpackConnectUrl,
onError,
setIsPending,
} = this.props;
if ( prevProps.isRequesting && ! isRequesting ) { if ( prevProps.isRequesting && ! isRequesting ) {
setIsPending( false ); setIsPending( false );
} }
if ( error && error !== prevProps.error ) { if ( error && error !== prevProps.error ) {
if ( onError ) {
onError();
}
createNotice( 'error', error ); createNotice( 'error', error );
} }
if ( autoConnect && jetpackConnectUrl ) {
this.connectJetpack();
}
} }
async connectJetpack() { async connectJetpack() {
const { jetpackConnectUrl, onConnect } = this.props; const { jetpackConnectUrl, onConnect } = this.props;
if ( onConnect ) { if ( onConnect ) {
onConnect(); onConnect();
} }
@ -42,7 +66,17 @@ class Connect extends Component {
} }
render() { render() {
const { hasErrors, isRequesting, onSkip, skipText } = this.props; const {
autoConnect,
hasErrors,
isRequesting,
onSkip,
skipText,
} = this.props;
if ( autoConnect ) {
return null;
}
return ( return (
<Fragment> <Fragment>
@ -73,6 +107,10 @@ class Connect extends Component {
} }
Connect.propTypes = { Connect.propTypes = {
/**
* If connection should happen automatically, or requires user confirmation.
*/
autoConnect: PropTypes.bool,
/** /**
* Method to create a displayed notice. * Method to create a displayed notice.
*/ */
@ -93,6 +131,14 @@ Connect.propTypes = {
* Generated Jetpack connection URL. * Generated Jetpack connection URL.
*/ */
jetpackConnectUrl: PropTypes.string, jetpackConnectUrl: PropTypes.string,
/**
* Called before the redirect to Jetpack.
*/
onConnect: PropTypes.func,
/**
* Called when the plugin has an error retrieving the jetpackConnectUrl.
*/
onError: PropTypes.func,
/** /**
* Called when the plugin connection is skipped. * Called when the plugin connection is skipped.
*/ */
@ -112,6 +158,7 @@ Connect.propTypes = {
}; };
Connect.defaultProps = { Connect.defaultProps = {
autoConnect: false,
setIsPending: () => {}, setIsPending: () => {},
}; };

View File

@ -12,11 +12,13 @@ import { filter } from 'lodash';
* WooCommerce dependencies * WooCommerce dependencies
*/ */
import { Card, H, Plugins } from '@woocommerce/components'; import { Card, H, Plugins } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/wc-admin-settings';
import { PLUGINS_STORE_NAME } from '@woocommerce/data'; import { PLUGINS_STORE_NAME } from '@woocommerce/data';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import Connect from 'dashboard/components/connect';
import Logo from './logo'; import Logo from './logo';
import ManagementIcon from './images/management'; import ManagementIcon from './images/management';
import SalesTaxIcon from './images/sales_tax'; import SalesTaxIcon from './images/sales_tax';
@ -30,8 +32,9 @@ class Benefits extends Component {
constructor( props ) { constructor( props ) {
super( props ); super( props );
this.state = { this.state = {
isConnecting: false,
isInstalling: false, isInstalling: false,
isPending: false, isActioned: false,
}; };
this.isJetpackActive = props.activePlugins.includes( 'jetpack' ); this.isJetpackActive = props.activePlugins.includes( 'jetpack' );
@ -55,19 +58,27 @@ class Benefits extends Component {
} }
componentDidUpdate( prevProps, prevState ) { componentDidUpdate( prevProps, prevState ) {
const { goToNextStep, isRequesting } = this.props; const { goToNextStep } = this.props;
const { isInstalling, isPending } = this.state; const { isActioned } = this.state;
// No longer pending or updating profile items, go to next step.
if ( if (
isPending && isActioned &&
! isRequesting && ! this.isPending() &&
! isInstalling && ( prevProps.isRequesting ||
( prevProps.isRequesting || prevState.isInstalling ) prevState.isConnecting ||
prevState.isInstalling )
) { ) {
goToNextStep(); goToNextStep();
} }
} }
isPending() {
const { isActioned, isConnecting, isInstalling } = this.state;
const { isRequesting } = this.props;
return isActioned && ( isConnecting || isInstalling || isRequesting );
}
async skipPluginInstall() { async skipPluginInstall() {
const { const {
createNotice, createNotice,
@ -75,10 +86,9 @@ class Benefits extends Component {
updateProfileItems, updateProfileItems,
} = this.props; } = this.props;
this.setState( { isPending: true } );
const plugins = this.isJetpackActive ? 'skipped-wcs' : 'skipped'; const plugins = this.isJetpackActive ? 'skipped-wcs' : 'skipped';
await updateProfileItems( { plugins } ); await updateProfileItems( { plugins } );
this.setState( { isActioned: true } );
if ( isProfileItemsError ) { if ( isProfileItemsError ) {
createNotice( createNotice(
@ -99,10 +109,7 @@ class Benefits extends Component {
async startPluginInstall() { async startPluginInstall() {
const { updateProfileItems, updateOptions } = this.props; const { updateProfileItems, updateOptions } = this.props;
this.setState( { this.setState( { isActioned: true, isInstalling: true } );
isInstalling: true,
isPending: true,
} );
await updateOptions( { await updateOptions( {
woocommerce_setup_jetpack_opted_in: true, woocommerce_setup_jetpack_opted_in: true,
@ -191,7 +198,8 @@ class Benefits extends Component {
} }
render() { render() {
const { isInstalling, isPending } = this.state; const { isConnecting, isInstalling } = this.state;
const { isJetpackConnected, isRequesting } = this.props;
const pluginNamesString = this.pluginsToInstall const pluginNamesString = this.pluginsToInstall
.map( ( pluginSlug ) => pluginNames[ pluginSlug ] ) .map( ( pluginSlug ) => pluginNames[ pluginSlug ] )
@ -212,8 +220,10 @@ class Benefits extends Component {
<div className="woocommerce-profile-wizard__card-actions"> <div className="woocommerce-profile-wizard__card-actions">
<Button <Button
isPrimary isPrimary
isBusy={ isPending && isInstalling } isBusy={
disabled={ isPending } this.isPending() && ( isInstalling || isConnecting )
}
disabled={ this.isPending() }
onClick={ this.startPluginInstall } onClick={ this.startPluginInstall }
className="woocommerce-profile-wizard__continue" className="woocommerce-profile-wizard__continue"
> >
@ -221,8 +231,10 @@ class Benefits extends Component {
</Button> </Button>
<Button <Button
isDefault isDefault
isBusy={ isPending && ! isInstalling } isBusy={
disabled={ isPending } this.isPending() && ! isInstalling && ! isConnecting
}
disabled={ this.isPending() }
className="woocommerce-profile-wizard__skip" className="woocommerce-profile-wizard__skip"
onClick={ this.skipPluginInstall } onClick={ this.skipPluginInstall }
> >
@ -233,14 +245,37 @@ class Benefits extends Component {
<Plugins <Plugins
autoInstall autoInstall
onComplete={ () => onComplete={ () =>
this.setState( { isInstalling: false } ) this.setState( {
isInstalling: false,
isConnecting: ! isJetpackConnected,
} )
} }
onError={ () => onError={ () =>
this.setState( { isInstalling: false } ) this.setState( {
isInstalling: false,
} )
} }
pluginSlugs={ this.pluginsToInstall } pluginSlugs={ this.pluginsToInstall }
/> />
) } ) }
{ /* Make sure we're finished requesting since this will auto redirect us. */ }
{ isConnecting && ! isJetpackConnected && ! isRequesting && (
<Connect
autoConnect
onConnect={ () => {
recordEvent(
'storeprofiler_jetpack_connect_redirect'
);
} }
onError={ () =>
this.setState( { isConnecting: false } )
}
redirectUrl={ getAdminLink(
'admin.php?page=wc-admin&reset_profiler=0'
) }
/>
) }
</div> </div>
<p className="woocommerce-profile-wizard__benefits-install-notice"> <p className="woocommerce-profile-wizard__benefits-install-notice">
@ -271,7 +306,9 @@ export default compose(
isGetProfileItemsRequesting, isGetProfileItemsRequesting,
} = select( 'wc-api' ); } = select( 'wc-api' );
const { getActivePlugins } = select( PLUGINS_STORE_NAME ); const { getActivePlugins, isJetpackConnected } = select(
PLUGINS_STORE_NAME
);
const isProfileItemsError = Boolean( getProfileItemsError() ); const isProfileItemsError = Boolean( getProfileItemsError() );
const activePlugins = getActivePlugins(); const activePlugins = getActivePlugins();
@ -281,6 +318,7 @@ export default compose(
activePlugins, activePlugins,
isProfileItemsError, isProfileItemsError,
profileItems, profileItems,
isJetpackConnected: isJetpackConnected(),
isRequesting: isGetProfileItemsRequesting(), isRequesting: isGetProfileItemsRequesting(),
}; };
} ), } ),

View File

@ -12,18 +12,19 @@ import TYPES from './action-types';
import { WC_ADMIN_NAMESPACE } from '../constants'; import { WC_ADMIN_NAMESPACE } from '../constants';
import { pluginNames } from './constants'; import { pluginNames } from './constants';
export function updateActivePlugins( active ) { export function updateActivePlugins( active, replace = false ) {
return { return {
type: TYPES.UPDATE_ACTIVE_PLUGINS, type: TYPES.UPDATE_ACTIVE_PLUGINS,
active, active,
replace,
}; };
} }
export function updateInstalledPlugins( installed, added ) { export function updateInstalledPlugins( installed, replace = false ) {
return { return {
type: TYPES.UPDATE_INSTALLED_PLUGINS, type: TYPES.UPDATE_INSTALLED_PLUGINS,
installed, installed,
added, replace,
}; };
} }

View File

@ -21,20 +21,20 @@ const plugins = (
type, type,
active, active,
installed, installed,
added,
selector, selector,
isRequesting, isRequesting,
error, error,
jetpackConnection, jetpackConnection,
redirectUrl, redirectUrl,
jetpackConnectUrl, jetpackConnectUrl,
replace,
} }
) => { ) => {
switch ( type ) { switch ( type ) {
case TYPES.UPDATE_ACTIVE_PLUGINS: case TYPES.UPDATE_ACTIVE_PLUGINS:
state = { state = {
...state, ...state,
active, active: replace ? active : concat( state.active, active ),
requesting: { requesting: {
...state.requesting, ...state.requesting,
getActivePlugins: false, getActivePlugins: false,
@ -50,7 +50,9 @@ const plugins = (
case TYPES.UPDATE_INSTALLED_PLUGINS: case TYPES.UPDATE_INSTALLED_PLUGINS:
state = { state = {
...state, ...state,
installed: added ? concat( state.installed, added ) : installed, installed: replace
? installed
: concat( state.installed, installed ),
requesting: { requesting: {
...state.requesting, ...state.requesting,
getInstalledPlugins: false, getInstalledPlugins: false,

View File

@ -27,7 +27,7 @@ export function* getActivePlugins() {
method: 'GET', method: 'GET',
} ); } );
yield updateActivePlugins( results.plugins ); yield updateActivePlugins( results.plugins, true );
} catch ( error ) { } catch ( error ) {
yield setError( 'getActivePlugins', error ); yield setError( 'getActivePlugins', error );
} }
@ -43,7 +43,7 @@ export function* getInstalledPlugins() {
method: 'GET', method: 'GET',
} ); } );
yield updateInstalledPlugins( results ); yield updateInstalledPlugins( results, true );
} catch ( error ) { } catch ( error ) {
yield setError( 'getInstalledPlugins', error ); yield setError( 'getInstalledPlugins', error );
} }

View File

@ -19,11 +19,17 @@ describe( 'plugins reducer', () => {
expect( state ).not.toBe( defaultState ); expect( state ).not.toBe( defaultState );
} ); } );
it( 'should handle UPDATE_ACTIVE_PLUGINS', () => { it( 'should handle UPDATE_ACTIVE_PLUGINS with replace', () => {
const state = reducer( defaultState, { const state = reducer(
{
active: [ 'plugins', 'to', 'overwrite' ],
},
{
type: TYPES.UPDATE_ACTIVE_PLUGINS, type: TYPES.UPDATE_ACTIVE_PLUGINS,
active: [ 'jetpack' ], active: [ 'jetpack' ],
} ); replace: true,
}
);
/* eslint-disable dot-notation */ /* eslint-disable dot-notation */
@ -35,11 +41,42 @@ describe( 'plugins reducer', () => {
expect( state.active[ 0 ] ).toBe( 'jetpack' ); expect( state.active[ 0 ] ).toBe( 'jetpack' );
} ); } );
it( 'should handle UPDATE_INSTALLED_PLUGINS', () => { it( 'should handle UPDATE_ACTIVE_PLUGINS with active plugins', () => {
const state = reducer( defaultState, { const state = reducer(
{
active: [ 'jetpack' ],
installed: [ 'jetpack' ],
requesting: {},
errors: {},
},
{
type: TYPES.UPDATE_ACTIVE_PLUGINS,
installed: null,
active: [ 'woocommerce-services' ],
}
);
/* eslint-disable dot-notation */
expect( state.requesting[ 'getActivePlugins' ] ).toBe( false );
expect( state.errors[ 'getActivePlugins' ] ).toBe( false );
/* eslint-enable dot-notation */
expect( state.active ).toHaveLength( 2 );
expect( state.active[ 1 ] ).toBe( 'woocommerce-services' );
} );
it( 'should handle UPDATE_INSTALLED_PLUGINS with replace', () => {
const state = reducer(
{
active: [ 'plugins', 'to', 'overwrite' ],
},
{
type: TYPES.UPDATE_INSTALLED_PLUGINS, type: TYPES.UPDATE_INSTALLED_PLUGINS,
installed: [ 'jetpack' ], installed: [ 'jetpack' ],
} ); replace: true,
}
);
/* eslint-disable dot-notation */ /* eslint-disable dot-notation */
@ -51,7 +88,7 @@ describe( 'plugins reducer', () => {
expect( state.installed[ 0 ] ).toBe( 'jetpack' ); expect( state.installed[ 0 ] ).toBe( 'jetpack' );
} ); } );
it( 'should handle UPDATE_INSTALLED_PLUGINS with added plugins', () => { it( 'should handle UPDATE_INSTALLED_PLUGINS with installed plugins', () => {
const state = reducer( const state = reducer(
{ {
active: [ 'jetpack' ], active: [ 'jetpack' ],
@ -61,8 +98,7 @@ describe( 'plugins reducer', () => {
}, },
{ {
type: TYPES.UPDATE_INSTALLED_PLUGINS, type: TYPES.UPDATE_INSTALLED_PLUGINS,
installed: null, installed: [ 'woocommerce-services' ],
added: [ 'woocommerce-services' ],
} }
); );

View File

@ -34,8 +34,11 @@ export const withPluginsHydration = ( data ) => ( OriginalComponent ) => {
startResolution( 'getActivePlugins', [] ); startResolution( 'getActivePlugins', [] );
startResolution( 'getInstalledPlugins', [] ); startResolution( 'getInstalledPlugins', [] );
startResolution( 'isJetpackConnected', [] ); startResolution( 'isJetpackConnected', [] );
updateActivePlugins( dataRef.current.activePlugins ); updateActivePlugins( dataRef.current.activePlugins, true );
updateInstalledPlugins( dataRef.current.installedPlugins ); updateInstalledPlugins(
dataRef.current.installedPlugins,
true
);
updateIsJetpackConnected( updateIsJetpackConnected(
dataRef.current.jetpackStatus && dataRef.current.jetpackStatus &&
dataRef.current.jetpackStatus.isActive dataRef.current.jetpackStatus.isActive