Co-authored-by: David Stone <david@nnucomputerwhiz.com>
Co-authored-by: Chris Shultz <chris.shultz@automattic.com>
Co-authored-by: Harris Wong <harris.wong@a8c.com>
This commit is contained in:
Fernando Espinosa 2020-03-27 21:42:58 +01:00 committed by GitHub
parent b50ad35d63
commit b71bc861fb
19 changed files with 2260 additions and 5 deletions

View File

@ -21,9 +21,9 @@ output 2 "Creating archive... 🎁"
ZIP_FILE=$1
build_files=$(ls dist/*/*.{js,css})
build_files=$(find dist/ \( -name '*.js' -o -name '*.css' \))
zip -r ${ZIP_FILE} \
zip -r "${ZIP_FILE}" \
woocommerce-admin.php \
uninstall.php \
includes/ \

View File

@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Button, Modal } from '@wordpress/components';
import { withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
/**
* Internal dependencies
*/
import '../style.scss';
export class DismissModal extends Component {
setDismissed = ( timestamp ) => {
this.props.updateOptions( {
woocommerce_shipping_dismissed_timestamp: timestamp,
} );
};
hideBanner = () => {
document.getElementById(
'woocommerce-admin-print-label'
).style.display = 'none';
};
remindMeLaterClicked = () => {
const { onCloseAll, trackElementClicked } = this.props;
this.setDismissed( Date.now() );
onCloseAll();
this.hideBanner();
trackElementClicked( 'shipping_banner_dismiss_modal_remind_me_later' );
};
closeForeverClicked = () => {
const { onCloseAll, trackElementClicked } = this.props;
this.setDismissed( -1 );
onCloseAll();
this.hideBanner();
trackElementClicked( 'shipping_banner_dismiss_modal_close_forever' );
};
render() {
const { onClose, visible } = this.props;
if ( ! visible ) {
return null;
}
return (
<Modal
title={ __( 'Are you sure?', 'woocommerce-admin' ) }
onRequestClose={ onClose }
className="wc-admin-shipping-banner__dismiss-modal"
>
<p className="wc-admin-shipping-banner__dismiss-modal-help-text">
{ __(
'With WooCommerce Shipping you can Print shipping labels from your WooCommerce dashboard at the lowest USPS rates.',
'woocommerce-admin'
) }
</p>
<div className="wc-admin-shipping-banner__dismiss-modal-actions">
<Button isDefault onClick={ this.remindMeLaterClicked }>
{ __( 'Remind me later', 'woocommerce-admin' ) }
</Button>
<Button isPrimary onClick={ this.closeForeverClicked }>
{ __( "I don't need this", 'woocommerce-admin' ) }
</Button>
</div>
</Modal>
);
}
}
export default compose(
withDispatch( ( dispatch ) => {
const { updateOptions } = dispatch( 'wc-api' );
return { updateOptions };
} )
)( DismissModal );

View File

@ -0,0 +1,141 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import { DismissModal } from '../index.js';
describe( 'Option Save events in DismissModal', () => {
const spyUpdateOptions = jest.fn();
let dismissModalWrapper;
beforeEach( () => {
document.body.innerHTML =
document.body.innerHTML +
'<div id="woocommerce-admin-print-label">';
dismissModalWrapper = shallow(
<DismissModal
visible={ true }
onClose={ jest.fn() }
onCloseAll={ jest.fn() }
trackElementClicked={ jest.fn() }
updateOptions={ spyUpdateOptions }
/>
);
} );
test( 'Should save permanent dismissal', () => {
const permanentDismissTimestamp = -1;
const actionButtons = dismissModalWrapper.find( Button );
expect( actionButtons.length ).toBe( 2 );
const permanenttDismissButton = actionButtons.last();
permanenttDismissButton.simulate( 'click' );
expect( spyUpdateOptions ).toHaveBeenCalledWith( {
woocommerce_shipping_dismissed_timestamp: permanentDismissTimestamp,
} );
} );
test( 'Should save temporary dismissal', () => {
// Mock Date.now() so a known timestamp will be saved.
const mockDate = 123456;
const realDateNow = Date.now.bind( global.Date );
global.Date.now = jest.fn( () => mockDate );
const actionButtons = dismissModalWrapper.find( Button );
expect( actionButtons.length ).toBe( 2 );
const remindMeLaterButton = actionButtons.first();
remindMeLaterButton.simulate( 'click' );
expect( spyUpdateOptions ).toHaveBeenCalledWith( {
woocommerce_shipping_dismissed_timestamp: mockDate,
} );
// Restore Date.now().
global.Date.now = realDateNow;
} );
} );
describe( 'Tracking events in DismissModal', () => {
const trackElementClicked = jest.fn();
let dismissModalWrapper;
beforeEach( () => {
dismissModalWrapper = shallow(
<DismissModal
visible={ true }
onClose={ jest.fn() }
onCloseAll={ jest.fn() }
trackElementClicked={ trackElementClicked }
updateOptions={ jest.fn() }
/>
);
} );
it( 'should record an event when user clicks "I don\'t need this"', () => {
const actionButtons = dismissModalWrapper.find( Button );
expect( actionButtons.length ).toBe( 2 );
const iDoNotNeedThisButton = actionButtons.last();
iDoNotNeedThisButton.simulate( 'click' );
expect( trackElementClicked ).toHaveBeenCalledWith(
'shipping_banner_dismiss_modal_close_forever'
);
} );
it( 'should record an event when user clicks "Remind me later"', () => {
const actionButtons = dismissModalWrapper.find( Button );
expect( actionButtons.length ).toBe( 2 );
const remindMeLaterButton = actionButtons.first();
remindMeLaterButton.simulate( 'click' );
expect( trackElementClicked ).toHaveBeenCalledWith(
'shipping_banner_dismiss_modal_remind_me_later'
);
} );
} );
describe( 'Dismissing modal', () => {
let dismissModalWrapper;
beforeEach( () => {
document.body.innerHTML =
document.body.innerHTML +
'<div id="woocommerce-admin-print-label">';
dismissModalWrapper = shallow(
<DismissModal
visible={ true }
onClose={ jest.fn() }
onCloseAll={ jest.fn() }
trackElementClicked={ jest.fn() }
updateOptions={ jest.fn() }
/>
);
} );
test( 'Should hide the banner by clicking permanent dismissal', () => {
const actionButtons = dismissModalWrapper.find( Button );
expect( actionButtons.length ).toBe( 2 );
const permanenttDismissButton = actionButtons.last();
permanenttDismissButton.simulate( 'click' );
const bannerStyle = document.getElementById(
'woocommerce-admin-print-label'
).style;
expect( bannerStyle.display ).toBe( 'none' );
} );
test( 'Should hide the banner by clicking temporary dismissal', () => {
const actionButtons = dismissModalWrapper.find( Button );
expect( actionButtons.length ).toBe( 2 );
const remindMeLaterButton = actionButtons.first();
remindMeLaterButton.simulate( 'click' );
const bannerStyle = document.getElementById(
'woocommerce-admin-print-label'
).style;
expect( bannerStyle.display ).toBe( 'none' );
} );
} );

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import ShippingBanner from './shipping-banner';
const metaBox = document.getElementById( 'wc-admin-shipping-banner-root' );
const args =
( metaBox.dataset.args && JSON.parse( metaBox.dataset.args ) ) || {};
// Render the header.
render( <ShippingBanner itemsCount={ args.shippable_items_count } />, metaBox );

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Dashicon } from '@wordpress/components';
export const setupErrorTypes = {
DOWNLOAD: 'download',
INSTALL: 'install',
ACTIVATE: 'activate',
SETUP: 'setup',
START: 'start',
};
const setupErrorDescriptions = {
[ setupErrorTypes.DOWNLOAD ]: __( 'download', 'woocommerce-admin' ),
[ setupErrorTypes.INSTALL ]: __( 'install', 'woocommerce-admin' ),
[ setupErrorTypes.ACTIVATE ]: __( 'activate', 'woocommerce-admin' ),
[ setupErrorTypes.SETUP ]: __( 'set up', 'woocommerce-admin' ),
[ setupErrorTypes.START ]: __( 'start', 'woocommerce-admin' ),
};
export default function SetupNotice( { isSetupError, errorReason } ) {
const getErrorMessage = ( errorType ) => {
// Default to 'set up' description if the error type somehow doesn't exist.
const description =
errorType in setupErrorDescriptions
? setupErrorDescriptions[ errorType ]
: setupErrorDescriptions[ setupErrorTypes.SETUP ];
return sprintf(
__(
'Unable to %s the plugin. Refresh the page and try again.',
'woocommerce-admin'
),
description
);
};
if ( ! isSetupError ) {
return null;
}
return (
<div className="wc-admin-shipping-banner-install-error">
<Dashicon icon="warning" />
{ getErrorMessage( errorReason ) }
</div>
);
}

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { mount } from 'enzyme';
/**
* Internal dependencies
*/
import SetupNotice, { setupErrorTypes } from '../';
describe( 'SetupNotice', () => {
it( 'should be hidden for no error', () => {
const notice = mount( <SetupNotice isSetupError={ false } /> );
expect( notice.isEmptyRender() ).toBe( true );
} );
it( 'should show div if there is an error', () => {
const notice = mount( <SetupNotice isSetupError={ true } /> );
const contents = notice.find(
'.wc-admin-shipping-banner-install-error'
);
expect( contents.length ).toBe( 1 );
} );
it( 'should show download message for download error', () => {
const notice = mount(
<SetupNotice
isSetupError={ true }
errorReason={ setupErrorTypes.DOWNLOAD }
/>
);
expect(
notice.text().includes( 'Unable to download the plugin' )
).toBe( true );
} );
it( 'should show default message for unset error', () => {
const notice = mount( <SetupNotice isSetupError={ true } /> );
expect( notice.text().includes( 'Unable to set up the plugin' ) ).toBe(
true
);
} );
} );

View File

@ -0,0 +1,556 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Button, ExternalLink } from '@wordpress/components';
import { compose } from '@wordpress/compose';
import { recordEvent } from 'lib/tracks';
import interpolateComponents from 'interpolate-components';
import PropTypes from 'prop-types';
import { get, isArray } from 'lodash';
/**
* Internal dependencies
*/
import '../style.scss';
import DismissModal from '../dismiss-modal';
import { getSetting } from '@woocommerce/wc-admin-settings';
import withSelect from 'wc-api/with-select';
import SetupNotice, { setupErrorTypes } from '../setup-notice';
import { withDispatch } from '@wordpress/data';
import { getWcsAssets, acceptWcsTos } from '../wcs-api';
const wcAdminAssetUrl = getSetting( 'wcAdminAssetUrl', '' );
export class ShippingBanner extends Component {
constructor( props ) {
super( props );
const orderId = new URL( window.location.href ).searchParams.get(
'post'
);
this.state = {
showShippingBanner: true,
isDismissModalOpen: false,
setupErrorReason: setupErrorTypes.SETUP,
orderId: parseInt( orderId, 10 ),
wcsAssetsLoaded: false,
wcsAssetsLoading: false,
wcsSetupError: false,
isShippingLabelButtonBusy: false,
installText: this.getInstallText(),
isWcsModalOpen: false,
};
}
componentDidMount() {
const { showShippingBanner } = this.state;
if ( showShippingBanner ) {
this.trackImpression();
}
}
componentDidUpdate( prevProps ) {
const { activatePlugins, wcsPluginSlug } = this.props;
if ( this.justInstalledWcs( prevProps ) ) {
activatePlugins( [ wcsPluginSlug ] );
}
if ( this.justActivatedWcs( prevProps ) ) {
this.acceptTosAndGetWCSAssets();
}
}
justInstalledWcs( prevProps ) {
const { installedPlugins, wcsPluginSlug } = this.props;
const wcsNowInstalled = installedPlugins.includes( wcsPluginSlug );
const wcsPrevInstalled = prevProps.installedPlugins.includes(
wcsPluginSlug
);
return wcsNowInstalled && ! wcsPrevInstalled;
}
justActivatedWcs( prevProps ) {
const { activatedPlugins, wcsPluginSlug } = this.props;
const wcsNowActivated = activatedPlugins.includes( wcsPluginSlug );
const wcsPrevActivated = prevProps.activatedPlugins.includes(
wcsPluginSlug
);
return wcsNowActivated && ! wcsPrevActivated;
}
hasActivationError = () => {
return Boolean( this.props.activationErrors.length );
};
hasInstallationError = () => {
return Boolean( this.props.installationErrors.length );
};
isSetupError = () => {
return (
this.hasActivationError() ||
this.hasInstallationError() ||
this.state.wcsSetupError
);
};
setupErrorReason = () => {
if ( this.hasInstallationError() ) {
return setupErrorTypes.INSTALL;
}
if ( this.hasActivationError() ) {
return setupErrorTypes.ACTIVATE;
}
return setupErrorTypes.SETUP;
};
closeDismissModal = () => {
this.setState( { isDismissModalOpen: false } );
this.trackElementClicked(
'shipping_banner_dismiss_modal_close_button'
);
};
openDismissModal = () => {
this.setState( { isDismissModalOpen: true } );
this.trackElementClicked( 'shipping_banner_dimiss' );
};
hideBanner = () => {
this.setState( { showShippingBanner: false } );
};
createShippingLabelClicked = () => {
const { wcsPluginSlug, activePlugins } = this.props;
this.setState( { isShippingLabelButtonBusy: true } );
this.trackElementClicked( 'shipping_banner_create_label' );
if ( ! activePlugins.includes( wcsPluginSlug ) ) {
this.installAndActivatePlugins( wcsPluginSlug );
} else {
this.acceptTosAndGetWCSAssets();
}
};
installAndActivatePlugins( pluginSlug ) {
// Avoid double activating.
const { installPlugins, isRequesting } = this.props;
if ( isRequesting ) {
return false;
}
installPlugins( [ pluginSlug ] );
}
woocommerceServiceLinkClicked = () => {
this.trackElementClicked( 'shipping_banner_woocommerce_service_link' );
};
trackBannerEvent = ( eventName, customProps = {} ) => {
const { activePlugins, isJetpackConnected, wcsPluginSlug } = this.props;
recordEvent( eventName, {
banner_name: 'wcadmin_install_wcs_prompt',
jetpack_installed: activePlugins.includes( 'jetpack' ),
jetpack_connected: isJetpackConnected,
wcs_installed: activePlugins.includes( wcsPluginSlug ),
...customProps,
} );
};
trackImpression = () => {
this.trackBannerEvent( 'banner_impression' );
};
trackElementClicked = ( element ) => {
this.trackBannerEvent( 'banner_element_clicked', {
element,
} );
};
acceptTosAndGetWCSAssets() {
return acceptWcsTos()
.then( () => getWcsAssets() )
.then( ( wcsAssets ) => this.loadWcsAssets( wcsAssets ) )
.catch( () => this.setState( { wcsSetupError: true } ) );
}
generateMetaBoxHtml( nodeId, title, args ) {
const argsJsonString = JSON.stringify( args ).replace( /"/g, '&quot;' ); // JS has no native html_entities so we just replace.
const togglePanelText = __( 'Toggle panel:', 'woocommerce-admin' );
return `
<div id="${ nodeId }" class="postbox">
<button type="button" class="handlediv" aria-expanded="true">
<span class="screen-reader-text">${ togglePanelText } ${ title }</span>
<span class="toggle-indicator" aria-hidden="true"></span>
</button>
<h2 class="hndle"><span>${ title }</span></h2>
<div class="inside">
<div class="wcc-root woocommerce wc-connect-create-shipping-label" data-args="${ argsJsonString }">
</div>
</div>
</div>
`;
}
loadWcsAssets( { assets } ) {
if ( this.state.wcsAssetsLoaded || this.state.wcsAssetsLoading ) {
this.openWcsModal();
return;
}
this.setState( { wcsAssetsLoading: true } );
const js = assets.wc_connect_admin_script;
const styles = assets.wc_connect_admin_style;
const { orderId } = this.state;
const { itemsCount } = this.props;
const shippingLabelContainerHtml = this.generateMetaBoxHtml(
'woocommerce-order-label',
__( 'Shipping Label', 'woocommerce-admin' ),
{
orderId,
context: 'shipping_label',
items: itemsCount,
}
);
// Insert shipping label metabox just above main order details box.
document
.getElementById( 'woocommerce-order-data' )
.insertAdjacentHTML( 'beforebegin', shippingLabelContainerHtml );
const shipmentTrackingHtml = this.generateMetaBoxHtml(
'woocommerce-order-shipment-tracking',
__( 'Shipment Tracking', 'woocommerce-admin' ),
{
orderId,
context: 'shipment_tracking',
items: itemsCount,
}
);
// Insert tracking metabox in the side after the order actions.
document
.getElementById( 'woocommerce-order-actions' )
.insertAdjacentHTML( 'afterend', shipmentTrackingHtml );
if ( window.jQuery ) {
// Need to refresh so the new metaboxes are sortable.
window.jQuery( '#normal-sortables' ).sortable( 'refresh' );
window.jQuery( '#side-sortables' ).sortable( 'refresh' );
window.jQuery( '#woocommerce-order-label' ).hide();
}
Promise.all( [
new Promise( ( resolve, reject ) => {
const script = document.createElement( 'script' );
script.src = js;
script.async = true;
script.onload = resolve;
script.onerror = reject;
document.body.appendChild( script );
} ),
new Promise( ( resolve, reject ) => {
const head = document.getElementsByTagName( 'head' )[ 0 ];
const link = document.createElement( 'link' );
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = styles;
link.media = 'all';
link.onload = resolve;
link.onerror = reject;
head.appendChild( link );
} ),
] ).then( () => {
this.setState( {
wcsAssetsLoaded: true,
wcsAssetsLoading: false,
isShippingLabelButtonBusy: false,
} );
this.openWcsModal();
} );
}
getInstallText = () => {
const { activePlugins, wcsPluginSlug } = this.props;
if ( activePlugins.includes( wcsPluginSlug ) ) {
// If WCS is active, then the only remaining step is to agree to the ToS.
return __(
'You\'ve already installed WooCommerce Shipping. By clicking "Create shipping label", you agree to its {{tosLink}}Terms of Service{{/tosLink}}.',
'woocommerce-admin'
);
}
return __(
'By clicking "Create shipping label", {{wcsLink}}WooCommerce Shipping{{/wcsLink}} will be installed and you agree to its {{tosLink}}Terms of Service{{/tosLink}}.',
'woocommerce-admin'
);
};
openWcsModal() {
if ( window.wcsGetAppStore ) {
const wcsStore = window.wcsGetAppStore(
'wc-connect-create-shipping-label'
);
const state = wcsStore.getState();
const { orderId } = this.state;
const siteId = state.ui.selectedSiteId;
const wcsStoreUnsubscribe = wcsStore.subscribe( () => {
const latestState = wcsStore.getState();
const shippingLabelState = get(
latestState,
[
'extensions',
'woocommerce',
'woocommerceServices',
siteId,
'shippingLabel',
orderId,
],
null
);
const labelSettingsState = get(
latestState,
[
'extensions',
'woocommerce',
'woocommerceServices',
siteId,
'labelSettings',
],
null
);
const packageState = get(
latestState,
[
'extensions',
'woocommerce',
'woocommerceServices',
siteId,
'packages',
],
null
);
const locationsState = get( latestState, [
'extensions',
'woocommerce',
'sites',
siteId,
'data',
'locations',
] );
if (
shippingLabelState &&
labelSettingsState &&
labelSettingsState.meta &&
packageState &&
locationsState
) {
if (
shippingLabelState.loaded &&
labelSettingsState.meta.isLoaded &&
packageState.isLoaded &&
isArray( locationsState ) &&
! this.state.isWcsModalOpen
) {
if ( window.jQuery ) {
this.setState( { isWcsModalOpen: true } );
window
.jQuery( '.shipping-label__new-label-button' )
.click();
}
wcsStore.dispatch( {
type: 'NOTICE_CREATE',
notice: {
duration: 10000,
status: 'is-success',
text: __(
'Plugin installed and activated',
'woocommerce-admin'
),
},
} );
} else if ( shippingLabelState.showPurchaseDialog ) {
wcsStoreUnsubscribe();
if ( window.jQuery ) {
window.jQuery( '#woocommerce-order-label' ).show();
}
}
}
} );
document.getElementById(
'woocommerce-admin-print-label'
).style.display = 'none';
}
}
render() {
const {
isDismissModalOpen,
showShippingBanner,
isShippingLabelButtonBusy,
} = this.state;
if ( ! showShippingBanner ) {
return null;
}
return (
<div>
<div className="wc-admin-shipping-banner-container">
<img
className="wc-admin-shipping-banner-illustration"
src={ wcAdminAssetUrl + 'shippingillustration.svg' }
alt={ __( 'Shipping ', 'woocommerce-admin' ) }
/>
<div className="wc-admin-shipping-banner-blob">
<h3>
{ __(
'Print discounted shipping labels with a click.',
'woocommerce-admin'
) }
</h3>
<p>
{ interpolateComponents( {
mixedString: this.state.installText,
components: {
tosLink: (
<ExternalLink
href="https://wordpress.com/tos"
target="_blank"
type="external"
/>
),
wcsLink: (
<ExternalLink
href="https://woocommerce.com/products/shipping/"
target="_blank"
type="external"
onClick={
this
.woocommerceServiceLinkClicked
}
/>
),
},
} ) }
</p>
<SetupNotice
isSetupError={ this.isSetupError() }
errorReason={ this.setupErrorReason() }
/>
</div>
<Button
disabled={ isShippingLabelButtonBusy }
isPrimary
isBusy={ isShippingLabelButtonBusy }
onClick={ this.createShippingLabelClicked }
>
{ __( 'Create shipping label', 'woocommerce-admin' ) }
</Button>
<button
onClick={ this.openDismissModal }
type="button"
className="notice-dismiss"
disabled={ this.state.isShippingLabelButtonBusy }
>
<span className="screen-reader-text">
{ __(
'Close Print Label Banner.',
'woocommerce-admin'
) }
</span>
</button>
</div>
<DismissModal
visible={ isDismissModalOpen }
onClose={ this.closeDismissModal }
onCloseAll={ this.hideBanner }
trackElementClicked={ this.trackElementClicked }
/>
</div>
);
}
}
ShippingBanner.propTypes = {
itemsCount: PropTypes.number.isRequired,
isJetpackConnected: PropTypes.bool.isRequired,
activatedPlugins: PropTypes.array.isRequired,
activePlugins: PropTypes.array.isRequired,
installedPlugins: PropTypes.array.isRequired,
wcsPluginSlug: PropTypes.string.isRequired,
activationErrors: PropTypes.array.isRequired,
installationErrors: PropTypes.array.isRequired,
activatePlugins: PropTypes.func.isRequired,
installPlugins: PropTypes.func.isRequired,
isRequesting: PropTypes.bool.isRequired,
};
export default compose(
withSelect( ( select ) => {
const wcsPluginSlug = 'woocommerce-services';
const {
getActivePlugins,
getPluginInstallations,
getPluginActivations,
getPluginActivationErrors,
getPluginInstallationErrors,
isJetpackConnected,
isPluginActivateRequesting,
isPluginInstallRequesting,
} = select( 'wc-api' );
const isRequesting =
isPluginActivateRequesting() || isPluginInstallRequesting();
const allInstallationErrors = getPluginInstallationErrors( [
wcsPluginSlug,
] );
const installedPlugins = Object.keys(
getPluginInstallations( [ wcsPluginSlug ] )
);
const allActivationErrors = getPluginActivationErrors( [
wcsPluginSlug,
] );
const activatedPlugins = Object.keys(
getPluginActivations( [ wcsPluginSlug ] )
);
const activationErrors = [];
const installationErrors = [];
Object.keys( allActivationErrors ).map( ( plugin ) =>
activationErrors.push( allActivationErrors[ plugin ].message )
);
Object.keys( allInstallationErrors ).map( ( plugin ) =>
installationErrors.push( allInstallationErrors[ plugin ].message )
);
return {
activePlugins: getActivePlugins(),
isJetpackConnected: isJetpackConnected(),
isRequesting,
installedPlugins,
activatedPlugins,
wcsPluginSlug,
activationErrors,
installationErrors,
};
} ),
withDispatch( ( dispatch ) => {
const { activatePlugins, installPlugins } = dispatch( 'wc-api' );
return {
activatePlugins,
installPlugins,
};
} )
)( ShippingBanner );

View File

@ -0,0 +1,459 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';
import { recordEvent } from 'lib/tracks';
import { ExternalLink, Button } from '@wordpress/components';
/**
* Internal dependencies
*/
jest.mock( '../../wcs-api.js' );
import { acceptWcsTos, getWcsAssets } from '../../wcs-api.js';
acceptWcsTos.mockReturnValue( Promise.resolve() );
const wcsAssetsMock = {};
getWcsAssets.mockReturnValue( Promise.resolve( wcsAssetsMock ) );
import { ShippingBanner } from '../index.js';
jest.mock( 'lib/tracks' );
jest.mock( '@woocommerce/wc-admin-settings' );
describe( 'Tracking impression in shippingBanner', () => {
const expectedTrackingData = {
banner_name: 'wcadmin_install_wcs_prompt',
jetpack_connected: true,
jetpack_installed: true,
wcs_installed: true,
};
const wcsPluginSlug = 'it-does-n-t-matter-since-its-a-prop';
beforeEach( () => {
shallow(
<ShippingBanner
isJetpackConnected={ true }
activatedPlugins={ [] }
activePlugins={ [ wcsPluginSlug, 'jetpack' ] }
installedPlugins={ [ wcsPluginSlug, 'jetpack' ] }
activationErrors={ [] }
installationErrors={ [] }
itemsCount={ 1 }
wcsPluginSlug={ wcsPluginSlug }
activatePlugins={ jest.fn() }
installPlugins={ jest.fn() }
isRequesting={ false }
/>
);
} );
it( 'should record an event when user sees banner loaded', () => {
expect( recordEvent ).toHaveBeenCalledTimes( 1 );
expect( recordEvent ).toHaveBeenCalledWith(
'banner_impression',
expectedTrackingData
);
} );
} );
describe( 'Tracking clicks in shippingBanner', () => {
const isJetpackConnected = true;
let shippingBannerWrapper;
const getExpectedTrackingData = ( element ) => {
return {
banner_name: 'wcadmin_install_wcs_prompt',
jetpack_connected: true,
jetpack_installed: true,
wcs_installed: true,
element,
};
};
const wcsPluginSlug = 'it-does-n-t-matter-since-its-a-prop';
beforeEach( () => {
shippingBannerWrapper = shallow(
<ShippingBanner
isJetpackConnected={ isJetpackConnected }
activatedPlugins={ [] }
activePlugins={ [ wcsPluginSlug, 'jetpack' ] }
installedPlugins={ [ wcsPluginSlug, 'jetpack' ] }
installPlugins={ jest.fn() }
activatePlugins={ jest.fn() }
wcsPluginSlug={ wcsPluginSlug }
activationErrors={ [] }
installationErrors={ [] }
isRequesting={ false }
itemsCount={ 1 }
/>
);
} );
it( 'should record an event when user clicks "Create shipping label"', () => {
const createShippingLabelButton = shippingBannerWrapper.find( Button );
expect( createShippingLabelButton.length ).toBe( 1 );
createShippingLabelButton.simulate( 'click' );
expect( recordEvent ).toHaveBeenCalledWith(
'banner_element_clicked',
getExpectedTrackingData( 'shipping_banner_create_label' )
);
} );
it( 'should record an event when user clicks "WooCommerce Service"', () => {
const links = shippingBannerWrapper.find( ExternalLink );
expect( links.length ).toBe( 1 );
const wcsLink = links.first();
wcsLink.simulate( 'click' );
expect( recordEvent ).toHaveBeenCalledWith(
'banner_impression',
getExpectedTrackingData()
);
} );
it( 'should record an event when user clicks "x" to dismiss the banner', () => {
const noticeDimissButton = shippingBannerWrapper.find(
'.notice-dismiss'
);
expect( noticeDimissButton.length ).toBe( 1 );
noticeDimissButton.simulate( 'click' );
expect( recordEvent ).toHaveBeenCalledWith(
'banner_element_clicked',
getExpectedTrackingData( 'shipping_banner_dimiss' )
);
} );
} );
describe( 'Create shipping label button', () => {
let shippingBannerWrapper;
const installPlugins = jest.fn();
const activatePlugins = jest.fn();
delete window.location; // jsdom won't allow to rewrite window.location unless deleted first
window.location = {
href: 'http://wcship.test/wp-admin/post.php?post=1000&action=edit',
};
beforeEach( () => {
shippingBannerWrapper = shallow(
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ activatePlugins }
activePlugins={ [] }
activatedPlugins={ [] }
installPlugins={ installPlugins }
installedPlugins={ [] }
wcsPluginSlug={ 'woocommerce-services' }
activationErrors={ [] }
installationErrors={ [] }
isRequesting={ false }
itemsCount={ 1 }
/>
);
} );
it( 'should install WooCommerce Shipping when button is clicked', () => {
const createShippingLabelButton = shippingBannerWrapper.find( Button );
expect( createShippingLabelButton.length ).toBe( 1 );
createShippingLabelButton.simulate( 'click' );
expect( installPlugins ).toHaveBeenCalledWith( [
'woocommerce-services',
] );
} );
it( 'should activate WooCommerce Shipping when installation finishes', () => {
// Cause a 'componentDidUpdate' by changing the props.
shippingBannerWrapper.setProps( {
installedPlugins: [ 'woocommerce-services' ],
} );
expect( activatePlugins ).toHaveBeenCalledWith( [
'woocommerce-services',
] );
} );
it( 'should perform a request to accept the TOS and get WCS assets to load', async () => {
const loadWcsAssetsMock = jest.fn();
shippingBannerWrapper.instance().loadWcsAssets = loadWcsAssetsMock;
await shippingBannerWrapper.instance().acceptTosAndGetWCSAssets();
expect( acceptWcsTos ).toHaveBeenCalled();
expect( getWcsAssets ).toHaveBeenCalled();
expect( loadWcsAssetsMock ).toHaveBeenCalledWith( wcsAssetsMock );
} );
it( 'should load WCS assets when a path is provided', () => {
const scriptMock = {};
const linkMock = {};
const divMock = { dataset: { args: null } };
const createElementMockReturn = {
div: divMock,
script: scriptMock,
link: linkMock,
};
window.jQuery = jest.fn();
window.jQuery.mockReturnValue( {
sortable: jest.fn(),
hide: jest.fn(),
} );
const createElementMock = jest.fn( ( tagName ) => {
return createElementMockReturn[ tagName ];
} );
const createElement = document.createElement;
document.createElement = createElementMock;
const getElementsByTagNameMock = jest.fn();
const headMock = {
appendChild: jest.fn(),
};
getElementsByTagNameMock.mockReturnValueOnce( [ headMock ] );
const getElementsByTagName = document.getElementsByTagName;
document.getElementsByTagName = getElementsByTagNameMock;
const getElementByIdMock = jest.fn();
getElementByIdMock.mockReturnValue( {
insertAdjacentHTML: jest.fn(),
} );
const getElementById = document.getElementById;
document.getElementById = getElementByIdMock;
const appendChildMock = jest.fn();
const appendChild = document.body.appendChild;
document.body.appendChild = appendChildMock;
const openWcsModalMock = jest.fn();
shippingBannerWrapper.instance().openWcsModal = openWcsModalMock;
shippingBannerWrapper.instance().loadWcsAssets( {
assets: {
wc_connect_admin_script: '/path/to/wcs.js',
wc_connect_admin_style: '/path/to/wcs.css',
},
} );
expect( createElementMock ).toHaveBeenCalledWith( 'script' );
expect( createElementMock ).toHaveNthReturnedWith( 1, scriptMock );
expect( scriptMock.async ).toEqual( true );
expect( scriptMock.src ).toEqual( '/path/to/wcs.js' );
expect( appendChildMock ).toHaveBeenCalledWith( scriptMock );
expect( getElementsByTagNameMock ).toHaveBeenCalledWith( 'head' );
expect( getElementsByTagNameMock ).toHaveReturnedWith( [ headMock ] );
expect( createElementMock ).toHaveBeenCalledWith( 'link' );
expect( createElementMock ).toHaveNthReturnedWith( 2, linkMock );
expect( linkMock.rel ).toEqual( 'stylesheet' );
expect( linkMock.type ).toEqual( 'text/css' );
expect( linkMock.href ).toEqual( '/path/to/wcs.css' );
expect( linkMock.media ).toEqual( 'all' );
document.createElement = createElement;
document.getElementById = getElementById;
document.body.appendChild = appendChild;
document.getElementsByTagName = getElementsByTagName;
} );
it( 'should open WCS modal', () => {
window.wcsGetAppStore = jest.fn();
const getState = jest.fn();
const dispatch = jest.fn();
const subscribe = jest.fn();
window.wcsGetAppStore.mockReturnValueOnce( {
getState,
dispatch,
subscribe,
} );
getState.mockReturnValueOnce( {
ui: {
selectedSiteId: 'SITE_ID',
},
} );
const getElementByIdMock = jest.fn();
getElementByIdMock.mockReturnValue( {
style: { display: null },
} );
const getElementById = document.getElementById;
document.getElementById = getElementByIdMock;
shippingBannerWrapper.instance().openWcsModal();
expect( window.wcsGetAppStore ).toHaveBeenCalledWith(
'wc-connect-create-shipping-label'
);
expect( getState ).toHaveBeenCalledTimes( 1 );
expect( subscribe ).toHaveBeenCalledTimes( 1 );
expect( getElementByIdMock ).toHaveBeenCalledWith(
'woocommerce-admin-print-label'
);
document.getElementById = getElementById;
} );
} );
describe( 'In the process of installing, activating, loading assets for WooCommerce Service', () => {
let shippingBannerWrapper;
beforeEach( () => {
shippingBannerWrapper = shallow(
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ jest.fn() }
activePlugins={ [ 'jetpack', 'woocommerce-services' ] }
activatedPlugins={ [] }
installPlugins={ jest.fn() }
installedPlugins={ [] }
wcsPluginSlug={ 'woocommerce-services' }
activationErrors={ [] }
installationErrors={ [] }
isRequesting={ true }
itemsCount={ 1 }
/>
);
} );
it( 'should show a busy loading state on "Create shipping label"', () => {
shippingBannerWrapper.setState( { isShippingLabelButtonBusy: true } );
const createShippingLabelButton = shippingBannerWrapper.find( Button );
expect( createShippingLabelButton.length ).toBe( 1 );
expect( createShippingLabelButton.prop( 'disabled' ) ).toBe( true );
expect( createShippingLabelButton.prop( 'isBusy' ) ).toBe( true );
} );
it( 'should disable the dismiss button ', () => {
shippingBannerWrapper.setState( { isShippingLabelButtonBusy: true } );
const dismissButton = shippingBannerWrapper.find( '.notice-dismiss' );
expect( dismissButton.length ).toBe( 1 );
expect( dismissButton.prop( 'disabled' ) ).toBe( true );
} );
} );
describe( 'Setup error message', () => {
let shippingBannerWrapper;
beforeEach( () => {
shippingBannerWrapper = shallow(
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ jest.fn() }
activePlugins={ [ 'jetpack', 'woocommerce-services' ] }
activatedPlugins={ [] }
installPlugins={ jest.fn() }
installedPlugins={ [] }
wcsPluginSlug={ 'woocommerce-services' }
activationErrors={ [] }
installationErrors={ [] }
itemsCount={ 1 }
isRequesting={ false }
/>
);
} );
it( 'should not show if there is no error', () => {
expect( shippingBannerWrapper.instance().isSetupError() ).toBe( false );
expect( shippingBannerWrapper.instance().hasActivationError() ).toBe(
false
);
expect( shippingBannerWrapper.instance().hasInstallationError() ).toBe(
false
);
} );
it( 'should show if there is activation error', () => {
shippingBannerWrapper.setProps( {
activationErrors: [ 'Can not activate' ],
} );
expect( shippingBannerWrapper.instance().isSetupError() ).toBe( true );
expect( shippingBannerWrapper.instance().hasActivationError() ).toBe(
true
);
expect( shippingBannerWrapper.instance().hasInstallationError() ).toBe(
false
);
} );
it( 'should show if there is installation error', () => {
shippingBannerWrapper.setProps( {
installationErrors: [ 'Can not activate' ],
} );
expect( shippingBannerWrapper.instance().isSetupError() ).toBe( true );
expect( shippingBannerWrapper.instance().hasActivationError() ).toBe(
false
);
expect( shippingBannerWrapper.instance().hasInstallationError() ).toBe(
true
);
} );
} );
describe( 'The message in the banner', () => {
const createShippingBannerWrapper = ( {
activePlugins,
installedPlugins,
} ) =>
shallow(
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ jest.fn() }
activePlugins={ activePlugins }
activatedPlugins={ [] }
installPlugins={ jest.fn() }
installedPlugins={ installedPlugins }
wcsPluginSlug={ 'woocommerce-services' }
activationErrors={ [] }
installationErrors={ [] }
isRequesting={ true }
itemsCount={ 1 }
/>
);
const notActivatedMessage =
'By clicking "Create shipping label", WooCommerce Shipping will be installed and you agree to its Terms of Service.';
const activatedMessage =
'You\'ve already installed WooCommerce Shipping. By clicking "Create shipping label", you agree to its Terms of Service.';
it( 'should show install text "By clicking "Create shipping label"..." when first loaded.', () => {
const shippingBannerWrapper = createShippingBannerWrapper( {
activePlugins: [],
installedPlugins: [],
} );
const createShippingLabelText = shippingBannerWrapper.find( 'p' );
expect( createShippingLabelText.text() ).toBe( notActivatedMessage );
} );
it( 'should continue to show the initial message "By clicking "Create shipping label"..." after WooCommerce Service is installed successfully.', () => {
const shippingBannerWrapper = createShippingBannerWrapper( {
activePlugins: [],
installedPlugins: [ 'woocommerce-services' ],
} );
// Mock installation and activation successful
shippingBannerWrapper.setProps( {
activePlugins: [ 'woocommerce-services' ],
} );
const createShippingLabelText = shippingBannerWrapper.find( 'p' );
expect( createShippingLabelText.text() ).toBe( notActivatedMessage );
} );
it( 'should continue to show the initial message "By clicking "Create shipping label"..." after WooCommerce Service is installed and activated successfully.', () => {
const shippingBannerWrapper = createShippingBannerWrapper( {
activePlugins: [],
installedPlugins: [],
} );
// Mock installation and activation successful
shippingBannerWrapper.setProps( {
activePlugins: [ 'woocommerce-services' ],
installedPlugins: [ 'woocommerce-services' ],
} );
const createShippingLabelText = shippingBannerWrapper.find( 'p' );
expect( createShippingLabelText.text() ).toBe( notActivatedMessage );
} );
it( 'should show install text "By clicking "You\'ve already installed WooCommerce Shipping."..." when WooCommerce Service is already installed.', () => {
const shippingBannerWrapper = createShippingBannerWrapper( {
activePlugins: [ 'woocommerce-services' ],
installedPlugins: [ 'woocommerce-services' ],
} );
const createShippingLabelText = shippingBannerWrapper.find( 'p' );
expect( createShippingLabelText.text() ).toBe( activatedMessage );
} );
} );

View File

@ -0,0 +1,153 @@
// This is needed so ExternalLinks appear correctly. Should be part of 'wp-components' style but for some reason it's not.
@import '@wordpress/components/src/visually-hidden/style';
#woocommerce-admin-print-label {
min-height: 100px;
.hndle,
.handlediv {
display: none;
}
.wc-admin-shipping-banner-container {
display: flex;
align-items: center;
margin-top: 18px;
margin-bottom: 6px;
}
.wc-admin-shipping-banner-illustration {
margin-right: 8px;
}
h3 {
margin: 0;
font-size: 1.15em;
}
p {
margin: 2px 0 11px;
font-size: 15px;
line-height: 19px;
}
.components-button.is-button {
margin: 10px 61px 10px auto;
padding: 0 30px;
font-size: 14px;
font-weight: 500;
line-height: 25px;
text-decoration: none;
text-shadow: none;
}
.notice-dismiss {
top: 14px;
right: 18px;
}
.wc-admin-shipping-banner-install-error {
color: #d63638;
> .dashicon {
width: 16px;
height: 16px;
margin-right: 3px;
vertical-align: middle;
margin-top: -2px;
path {
fill: #d63638;
}
}
}
.components-external-link__icon {
display: none;
}
}
.components-modal__frame.wc-admin-shipping-banner__dismiss-modal {
width: 500px;
max-width: 100%;
.components-modal__header {
border-bottom: 0;
margin: 4px 0;
padding: 0;
height: 50px;
.components-button.components-icon-button {
left: 15px;
svg.dashicon {
width: 30px;
height: 30px;
}
}
}
.components-modal__header-heading {
font-style: normal;
font-weight: 500;
font-size: 20px;
line-height: 32px;
}
.wc-admin-shipping-banner__dismiss-modal-help-text {
font-size: 16px;
line-height: 24px;
margin: 0 60px 1em 0;
}
.wc-admin-shipping-banner__dismiss-modal-actions {
text-align: right;
.components-button.is-button {
height: 30px;
padding-left: 15px;
padding-right: 15px;
text-align: center;
font-size: 14px;
line-height: 15px;
font-weight: 500;
align-items: center;
margin-left: 10px;
}
button.is-primary {
align-self: flex-end;
}
}
}
@media (max-width: 1080px) {
#woocommerce-admin-print-label {
text-align: center;
.wc-admin-shipping-banner-container {
flex-wrap: wrap;
justify-content: center;
}
.components-button.is-button {
margin: 10px 0 0 0;
}
p {
margin: 5px 15px;
max-width: initial;
}
h3 {
margin-top: 10px;
}
}
}
.notice.wcs-nux__notice {
display: none;
}
.wc-admin-shipping-banner__dismiss-modal {
.components-button {
span {
display: none;
}
}
}

View File

@ -0,0 +1,21 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
export function acceptWcsTos() {
const path = '/wc/v1/connect/tos';
return apiFetch( {
path,
method: 'POST',
data: { accepted: true },
} );
}
export function getWcsAssets() {
const path = '/wc/v1/connect/assets';
return apiFetch( {
path,
method: 'GET',
} );
}

View File

@ -6,6 +6,7 @@
"analytics-dashboard/customizable": true,
"devdocs": false,
"onboarding": true,
"store-alerts": true
"store-alerts": true,
"shipping-label-banner": true
}
}

View File

@ -6,6 +6,7 @@
"analytics-dashboard/customizable": true,
"devdocs": true,
"onboarding": true,
"store-alerts": true
"store-alerts": true,
"shipping-label-banner": true
}
}

View File

@ -6,6 +6,7 @@
"analytics-dashboard/customizable": true,
"devdocs": false,
"onboarding": true,
"store-alerts": true
"store-alerts": true,
"shipping-label-banner": true
}
}

View File

@ -0,0 +1 @@
<svg width="96" height="68" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M0 0h96v68H0z"/><g clip-path="url(#clip0)"><path d="M0 0h96v68H0V0z" fill="#fff"/><g clip-path="url(#clip1)"><path d="M96 63.366H3v.224h93v-.224z" fill="#7F54B3"/><path d="M90.549 29.889h-31.26v33.523h31.26V29.89z" fill="#F7EDF7"/><path d="M60.736 46.595V31.122h1.78l8.732.28-.39 2.299v12.894H60.737zM77.645 53.434h-5.451V63.3h5.45v-9.866z" fill="#DCDCDE"/><path d="M35.595 45.361h-22.36v18.163h22.36V45.361z" fill="#F7EDF7"/><path d="M25.137 58.255h-3.782v5.157h3.782v-5.157z" fill="#DCDCDE"/><path d="M67.4 35.041c-.843 1.486-6.78-.99-6.78-.99l-11.344-4.605-2.742-2.046-1.747-1.303 3.88-1.095.109.053 2.704 1.3 10.37 6.243s7.75-1.432 5.55 2.443z" fill="#A0616A"/><path d="M38.807 17.784s-1.46.591-1.629 2.25c-.169 1.658-.183 11.012-.183 11.012s3.814-2.213 5.865-2.258c2.05-.046 6.815-3.532 6.815-3.532s-8.976-8.17-10.868-7.472z" fill="#D0CDE1"/><path opacity=".1" d="M38.807 17.784s-1.46.591-1.629 2.25c-.169 1.658-.183 11.012-.183 11.012s3.814-2.213 5.865-2.258c2.05-.046 6.815-3.532 6.815-3.532s-8.976-8.17-10.868-7.472z" fill="#000"/><path d="M53.115 48.388l-3.114.225s-.096 5.89-.501 7.4l5.34 4.26-1.113-3.588-.612-8.297z" fill="#A0616A"/><path d="M55.142 60.395c-.221-.079-.191-.122-.191-.122v-.224l-5.442-4.402c-.053.197-.112.326-.176.365-.2.121-.485.516-.769.973a4.393 4.393 0 00-.455 3.624l-.222 3.812h.89l.222-3.476h1.113s1.891 2.467 2.447 3.364c.556.897 3.004 1.121 5.451-.56 2.068-1.422-1.662-2.923-2.868-3.354z" fill="#2F2E41"/><path d="M25.75 35.102s-5.452 6.615-.223 10.651c2.3 1.776 4.321 5.137 5.863 8.344a19.152 19.152 0 005.114 6.534 18.967 18.967 0 007.378 3.734S62.46 42.502 56.12 36.335c-6.341-6.166-23.472-.785-23.472-.785l-6.898-.448z" fill="#2F2E41"/><path d="M41.182 29.524c.025 1.477-.256 3.098-1.527 3.448-2.447.673-2.67.336-2.67.336s-.445.785-.334 1.682c.112.897-1.056.168-1.056.168s-11.848 1.514-11.403-.168c.445-1.682 4.672-7.512 4.672-7.512s6.897-9.754 8.344-9.418c.049.011.099.025.15.04 1.474.432 3.973 2.342 4.188 3.1.223.784-.667 5.718-.667 5.718.18.857.282 1.73.303 2.606z" fill="#D0CDE1"/><path d="M58.678 43.287c-1.447.897-2.336-5.494-2.336-5.494l-10.457-6.39-2.379-2.466-1.515-1.571 4.005-.448.099.069 2.46 1.724 9.233 7.849s2.336 5.83.89 6.727z" fill="#A0616A"/><path opacity=".1" d="M40.879 26.917c.18.858.282 1.73.303 2.607-.777.143-1.48.194-1.972.084-2.002-.448-6.23.785-6.23.785s2.225-9.082 2.781-10.651a2.747 2.747 0 011.598-1.643c1.473.433 3.972 2.342 4.187 3.1.223.785-.667 5.718-.667 5.718z" fill="#000"/><path d="M40.828 16.68c-2.403-1.15-3.428-4.052-2.29-6.481 1.137-2.43 4.007-3.466 6.41-2.316 2.403 1.15 3.428 4.052 2.29 6.482-1.137 2.43-4.007 3.466-6.41 2.315z" fill="#2F2E41"/><path opacity=".1" d="M46.663 27.814s-1.466.604-3.157 1.123l-1.515-1.571 4.005-.448.099.069c.358.515.568.827.568.827z" fill="#000"/><path d="M44.065 15.589l-1.77 7.578-4.269-3.989s2.186-3.058 2.186-3.855c0-.798 3.853.266 3.853.266z" fill="#A0616A"/><path d="M38.209 17.387s-1.558.225-2.114 1.794c-.556 1.57-2.781 10.652-2.781 10.652s4.227-1.234 6.23-.785c2.002.448 7.453-1.794 7.453-1.794s-6.786-10.09-8.788-9.867z" fill="#D0CDE1"/><path d="M42.025 17.743c-1.857-.889-2.65-3.131-1.77-5.008.88-1.878 3.097-2.679 4.954-1.79 1.856.889 2.649 3.131 1.77 5.009-.88 1.877-3.097 2.678-4.954 1.79z" fill="#9F616A"/><path d="M39.829 15.713c-1.748-.837-2.494-2.947-1.666-4.714a3.484 3.484 0 014.662-1.684c1.747.836 2.493 2.947 1.666 4.713a3.484 3.484 0 01-4.662 1.685z" fill="#2F2E41"/><path d="M37.569 9.31c-1.614-.773-2.303-2.723-1.539-4.355A3.219 3.219 0 0140.337 3.4c1.614.773 2.303 2.723 1.539 4.355a3.219 3.219 0 01-4.307 1.556z" fill="#2F2E41"/><path d="M35.392 7.67a3.294 3.294 0 002.476 1.675 3.22 3.22 0 001.517-.182 3.19 3.19 0 001.257-.86 3.19 3.19 0 01-2.153 1.44c-.44.076-.892.06-1.328-.047a3.274 3.274 0 01-2.08-1.587 3.28 3.28 0 01-.401-1.274 3.24 3.24 0 01.14-1.323c.139-.425.363-.816.66-1.149A3.222 3.222 0 0034.985 6a3.275 3.275 0 00.408 1.67zM43.534 15.039c-1.53-.732-2.308-2.31-1.739-3.525.569-1.215 2.27-1.606 3.799-.874 1.529.732 2.307 2.31 1.738 3.525-.569 1.215-2.27 1.606-3.798.874z" fill="#2F2E41"/><path d="M43.752 16.123c-.327-.157-.383-.732-.125-1.284.259-.552.734-.873 1.062-.716.328.157.383.732.125 1.284-.259.552-.734.872-1.062.716z" fill="#A0616A"/><path opacity=".5" d="M70.904 32H61v.576h9.904V32zM70.904 34.591H61v.576h9.904v-.576zM70.904 37.182h-5.598v.576h5.598v-.576zM63.727 36.318H61v2.303h2.727v-2.303zM70.904 45.674H65.02v2.303h5.885v-2.303zM70.904 39.773H61v.575h9.904v-.575zM70.904 42.364H61v.575h9.904v-.575z" fill="#9A69C7"/></g></g><defs><clipPath id="clip0"><path d="M0 0h96v68H0V0z" fill="#fff"/></clipPath><clipPath id="clip1"><path fill="#fff" d="M3-4h88v86H3z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -84,6 +84,14 @@ class Init {
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
)
);
} elseif ( Loader::is_feature_enabled( 'shipping-label-banner' ) ) {
// Shipping Banner needs to use /active /install and /activate endpoints.
$controllers = array_merge(
$controllers,
array(
\Automattic\WooCommerce\Admin\API\OnboardingPlugins::class,
)
);
}
// The performance indicators controller must be registered last, after other /stats endpoints have been registered.

View File

@ -0,0 +1,187 @@
<?php
/**
* WooCommerce Shipping Label banner.
* NOTE: DO NOT edit this file in WooCommerce core, this is generated from woocommerce-admin.
*
* @package Woocommerce Admin
*/
namespace Automattic\WooCommerce\Admin\Features;
use \Automattic\WooCommerce\Admin\Loader;
/**
* Shows print shipping label banner on edit order page.
*/
class ShippingLabelBanner {
/**
* Singleton for the display rules class
*
* @var ShippingLabelBannerDisplayRules
*/
private $shipping_label_banner_display_rules;
/**
* Constructor
*/
public function __construct() {
if ( ! is_admin() ) {
return;
}
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 6, 2 );
add_filter( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 );
add_filter( 'woocommerce_shared_settings', array( $this, 'component_settings' ), 20 );
}
/**
* Check if WooCommerce Shipping makes sense for this merchant.
*
* @return bool
*/
private function should_show_meta_box() {
if ( ! $this->shipping_label_banner_display_rules ) {
$jetpack_version = null;
$jetpack_connected = null;
$wcs_version = null;
$wcs_tos_accepted = null;
if ( class_exists( '\Jetpack_Data' ) ) {
$user_token = \Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
$jetpack_connected = isset( $user_token->external_user_id );
$jetpack_version = JETPACK__VERSION;
}
if ( class_exists( '\WC_Connect_Loader' ) ) {
$wcs_version = \WC_Connect_Loader::get_wcs_version();
}
if ( class_exists( '\WC_Connect_Options' ) ) {
$wcs_tos_accepted = \WC_Connect_Options::get_option( 'tos_accepted' );
}
$incompatible_plugins = class_exists( '\WC_Shipping_Fedex_Init' ) ||
class_exists( '\WC_Shipping_UPS_Init' ) ||
class_exists( '\WC_Integration_ShippingEasy' ) ||
class_exists( '\WC_ShipStation_Integration' );
$this->shipping_label_banner_display_rules =
new ShippingLabelBannerDisplayRules(
$jetpack_version,
$jetpack_connected,
$wcs_version,
$wcs_tos_accepted,
$incompatible_plugins
);
}
return $this->shipping_label_banner_display_rules->should_display_banner();
}
/**
* Add metabox to order page.
*
* @param string $post_type current post type.
* @param \WP_Post $post Current post object.
*/
public function add_meta_boxes( $post_type, $post ) {
$order = wc_get_order( $post );
if ( $this->should_show_meta_box() ) {
add_meta_box(
'woocommerce-admin-print-label',
__( 'Shipping Label', 'woocommerce-admin' ),
array( $this, 'meta_box' ),
null,
'normal',
'high',
array(
'context' => 'shipping_label',
'order_id' => $post->ID,
'shippable_items_count' => $this->count_shippable_items( $order ),
)
);
add_action( 'admin_enqueue_scripts', array( $this, 'add_print_shipping_label_script' ) );
}
}
/**
* Count shippable items
*
* @param \WC_Order $order Current order.
* @return int
*/
private function count_shippable_items( \WC_Order $order ) {
$count = 0;
foreach ( $order->get_items() as $item ) {
if ( $item instanceof \WC_Order_Item_Product ) {
$product = $item->get_product();
if ( $product && $product->needs_shipping() ) {
$count += $item->get_quantity();
}
}
}
return $count;
}
/**
* Adds JS to order page to render shipping banner.
*
* @param string $hook current page hook.
*/
public function add_print_shipping_label_script( $hook ) {
$rtl = is_rtl() ? '-rtl' : '';
wp_enqueue_style(
'print-shipping-label-banner-style',
Loader::get_url( "print-shipping-label-banner/style{$rtl}.css" ),
array( 'wp-components' ),
Loader::get_file_version( 'print-shipping-label-banner/style.css' )
);
wp_enqueue_script(
'print-shipping-label-banner',
Loader::get_url( 'wp-admin-scripts/print-shipping-label-banner.js' ),
array( 'wp-i18n', 'wp-data', 'wp-element', 'moment', 'wp-api-fetch', WC_ADMIN_APP ),
Loader::get_file_version( 'wp-admin-scripts/print-shipping-label-banner.js' ),
true
);
$payload = array(
'nonce' => wp_create_nonce( 'wp_rest' ),
'baseURL' => get_rest_url(),
'wcs_server_connection' => true,
);
wp_localize_script( 'print-shipping-label-banner', 'wcConnectData', $payload );
}
/**
* Render placeholder metabox.
*
* @param \WP_Post $post current post.
* @param array $args empty args.
*/
public function meta_box( $post, $args ) {
?>
<div id="wc-admin-shipping-banner-root" class="woocommerce <?php echo esc_attr( 'wc-admin-shipping-banner' ); ?>" data-args="<?php echo esc_attr( wp_json_encode( $args['args'] ) ); ?>">
</div>
<?php
}
/**
* Return the settings for the component for wc-api to use. If onboarding
* is active, return its settings. Otherwise, loads "activePlugins" since
* that's the ones we need to get installation status for WCS and Jetpack.
*
* @param array $settings Component settings.
* @return array
*/
public function component_settings( $settings ) {
if ( ! isset( $settings['onboarding'] ) ) {
$settings['onboarding'] = array();
}
if ( ! isset( $settings['onboarding']['activePlugins'] ) ) {
$settings['onboarding']['activePlugins'] = Onboarding::get_active_plugins();
}
return $settings;
}
}

View File

@ -0,0 +1,229 @@
<?php
/**
* WooCommerce Shipping Label Banner Display Rules.
* NOTE: DO NOT edit this file in WooCommerce core, this is generated from woocommerce-admin.
*
* @package Woocommerce Admin
*/
namespace Automattic\WooCommerce\Admin\Features;
/**
* Determines whether or not the Shipping Label Banner should be displayed
*/
class ShippingLabelBannerDisplayRules {
/**
* Holds the installed Jetpack version.
*
* @var string
*/
private $jetpack_version;
/**
* Whether or not the installed Jetpack is connected.
*
* @var bool
*/
private $jetpack_connected;
/**
* Holds the installed WooCommerce Services version.
*
* @var string
*/
private $wcs_version;
/**
* Whether or not there're plugins installed incompatible with the banner.
*
* @var bool
*/
private $no_incompatible_plugins_installed;
/**
* Whether or not the WooCommerce Services ToS has been accepted.
*
* @var bool
*/
private $wcs_tos_accepted;
/**
* Minimum supported Jetpack version.
*
* @var string
*/
private $min_jetpack_version = '4.4';
/**
* Minimum supported WooCommerce Services version.
*
* @var string
*/
private $min_wcs_version = '1.22.5';
/**
* Supported countries by USPS, see: https://webpmt.usps.gov/pmt010.cfm
*
* @var array
*/
private $supported_countries = array( 'US', 'AS', 'PR', 'VI', 'GU', 'MP', 'UM', 'FM', 'MH' );
/**
* Array of supported currency codes.
*
* @var array
*/
private $supported_currencies = array( 'USD' );
/**
* Constructor.
*
* @param string $jetpack_version Installed Jetpack version to check.
* @param bool $jetpack_connected Is Jetpack connected?.
* @param string $wcs_version Installed WooCommerce Services version to check.
* @param bool $wcs_tos_accepted WooCommerce Services Terms of Service accepted?.
* @param bool $incompatible_plugins_installed Are there any incompatible plugins installed?.
*/
public function __construct( $jetpack_version, $jetpack_connected, $wcs_version, $wcs_tos_accepted, $incompatible_plugins_installed ) {
$this->jetpack_version = $jetpack_version;
$this->jetpack_connected = $jetpack_connected;
$this->wcs_version = $wcs_version;
$this->wcs_tos_accepted = $wcs_tos_accepted;
$this->no_incompatible_plugins_installed = ! $incompatible_plugins_installed;
}
/**
* Determines whether or not the banner should be displayed.
*/
public function should_display_banner() {
if ( ! $this->should_allow_banner() ) {
return false;
}
$ab_test = get_option( 'woocommerce_shipping_prompt_ab' );
// If it doesn't exist yet, generate it for later use and save it, so we always show the same to this user.
if ( ! $ab_test ) {
$ab_test = 1 !== wp_rand( 1, 4 ) ? 'a' : 'b'; // 25% of users. b gets the prompt.
update_option( 'woocommerce_shipping_prompt_ab', $ab_test, false );
}
return 'b' === $ab_test;
}
/**
* Determines whether banner is eligible for display (does not include a/b logic).
*/
public function should_allow_banner() {
return $this->banner_not_dismissed() &&
$this->jetpack_installed_and_active() &&
$this->jetpack_up_to_date() &&
$this->jetpack_connected &&
$this->no_incompatible_plugins_installed &&
$this->order_has_shippable_products() &&
$this->store_in_us_and_usd() &&
( $this->wcs_not_installed() || (
$this->wcs_up_to_date() && ! $this->wcs_tos_accepted
) );
}
/**
* Checks if the banner was not dismissed by the user.
*
* @return bool
*/
private function banner_not_dismissed() {
$dismissed_timestamp_ms = get_option( 'woocommerce_shipping_dismissed_timestamp' );
if ( ! is_numeric( $dismissed_timestamp_ms ) ) {
return true;
}
$dismissed_timestamp_ms = intval( $dismissed_timestamp_ms );
$dismissed_timestamp = intval( round( $dismissed_timestamp_ms / 1000 ) );
$expired_timestamp = $dismissed_timestamp + 24 * 60 * 60; // 24 hours from click time
$dismissed_for_good = -1 === $dismissed_timestamp_ms;
$dismissed_24h = time() < $expired_timestamp;
return ! $dismissed_for_good && ! $dismissed_24h;
}
/**
* Checks if jetpack is installed and active.
*
* @return bool
*/
private function jetpack_installed_and_active() {
return ! ! $this->jetpack_version;
}
/**
* Checks if Jetpack version is supported.
*
* @return bool
*/
private function jetpack_up_to_date() {
return version_compare( $this->jetpack_version, $this->min_jetpack_version, '>=' );
}
/**
* Checks if there's a shippable product in the current order.
*
* @return bool
*/
private function order_has_shippable_products() {
$post = get_post();
if ( ! $post ) {
return false;
}
$order = wc_get_order( get_post()->ID );
if ( ! $order ) {
return false;
}
// At this point (no packaging data), only show if there's at least one existing and shippable product.
foreach ( $order->get_items() as $item ) {
if ( $item instanceof \WC_Order_Item_Product ) {
$product = $item->get_product();
if ( $product && $product->needs_shipping() ) {
return true;
}
}
}
return false;
}
/**
* Checks if the store is in the US and has its default currency set to USD.
*
* @return bool
*/
private function store_in_us_and_usd() {
$base_currency = get_woocommerce_currency();
$base_location = wc_get_base_location();
return in_array( $base_currency, $this->supported_currencies, true ) && in_array( $base_location['country'], $this->supported_countries, true );
}
/**
* Checks if WooCommerce Services is not installed.
*
* @return bool
*/
private function wcs_not_installed() {
return ! $this->wcs_version;
}
/**
* Checks if WooCommerce Services is up to date.
*/
private function wcs_up_to_date() {
return $this->wcs_version && version_compare( $this->wcs_version, $this->min_wcs_version, '>=' );
}
}

View File

@ -0,0 +1,306 @@
<?php
/**
* Shipping Label Banner Display Rules tests.
*
* @package WooCommerce\Tests\Shipping-label-banner-display-rules
*/
use \Automattic\WooCommerce\Admin\Features\ShippingLabelBannerDisplayRules;
/**
* Class WC_Tests_Shipping_Label_Banner_Display_Rules
*/
class WC_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Case {
/**
* Jetpack version to test the display manager.
*
* @var string
*/
private $valid_jetpack_version = '4.4';
/**
* Stores the default WordPress options stored in teh database.
*
* @var array
*/
private static $modified_options = array(
'woocommerce_default_country' => null,
'woocommerce_currency' => null,
'woocommerce_shipping_prompt_ab' => null,
'woocommerce_shipping_dismissed_timestamp' => null,
);
/**
* Setup for every single test.
*/
public function setUp() {
parent::setup();
update_option( 'woocommerce_default_country', 'US' );
update_option( 'woocommerce_currency', 'USD' );
}
/**
* Setup for the whole test class.
*/
public static function setUpBeforeClass() {
parent::setUpBeforeClass();
foreach ( self::$modified_options as $option_name => $option_value ) {
self::$modified_options[ $option_name ] = $option_value;
}
}
/**
* Cleans up test data once all test have run.
*/
public static function tearDownAfterClass() {
parent::tearDownAfterClass();
foreach ( self::$modified_options as $option_name => $option_value ) {
update_option( $option_name, $option_value );
}
}
/**
* Test if the banner is displayed when all conditions are satisfied:
* - Banner NOT dismissed
* - Jetpack >= 4.4 installed and active
* - Jetpack Connected
* - No incompatible extensions installed:
* - Shipstation not installed
* - UPS not Installed
* - Fedex not installed
* - ShippingEasy not installed
* - Order contains physical products which need to be shipped (we should check that the order status is not set to complete)
* - Store is located in US
* - Store currency is set to USD
* - WCS plugin not installed OR WCS is installed *AND* ToS have NOT been accepted *AND* WCS version is 1.22.5 or greater
* (The 1.22.5 or greater requirement is so we can launch the shipping modal from the banner)
*/
public function test_display_banner_if_all_conditions_are_met() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), true );
}
);
}
/**
* Test if the banner is hidden when Jetpack is not active.
*/
public function test_if_banner_hidden_when_jetpack_disconnected() {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( null, null, null, null, null );
$this->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
/**
* Test if the banner is hidden when a dismiss banner option is checked.
*/
public function test_if_banner_hidden_when_dismiss_option_enabled() {
update_option( 'woocommerce_shipping_dismissed_timestamp', -1 );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$this->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
/**
* Banner should not show if it was dismissed 2 hours ago.
*/
public function test_if_banner_hidden_when_dismiss_was_clicked_2_hrs_ago() {
$two_hours_from_ago = ( time() - 2 * 60 * 60 ) * 1000;
update_option( 'woocommerce_shipping_dismissed_timestamp', $two_hours_from_ago );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$this->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
/**
* Banner should show if it was dismissed 24 hours and 1 second ago.
*/
public function test_if_banner_hidden_when_dismiss_was_clicked_24_hrs_1s_ago() {
$twenty_four_hours_one_sec_ago = ( time() - 24 * 60 * 60 - 1 ) * 1000;
update_option( 'woocommerce_shipping_dismissed_timestamp', $twenty_four_hours_one_sec_ago );
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), true );
}
);
}
/**
* Test if the banner is hidden when no shippable product available.
*/
public function test_if_banner_hidden_when_no_shippable_product() {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$this->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
/**
* Test if the banner is displayed when the store is in the US.
*/
public function test_if_banner_hidden_when_store_is_not_in_us() {
update_option( 'woocommerce_default_country', 'ES' );
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
);
}
/**
* Test if the banner is displayed when the store's currency is USD.
*/
public function test_if_banner_hidden_when_currency_is_not_usd() {
update_option( 'woocommerce_currency', 'EUR' );
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
);
}
/**
* Test if the banner is hidden when an incompatible plugin is installed
*/
public function test_if_banner_hidden_when_incompatible_plugin_installed() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, true );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
);
}
/**
* Test if the banner is hidden when Jetpack version is not at least 4.4.
*/
public function test_if_banner_hidden_when_jetpack_version_is_old() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.3', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
);
}
/**
* Test if the banner is hidden when the WooCommerce Services Terms of Service has been already accepted.
*/
public function test_if_banner_hidden_when_wcs_tos_accepted() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', true, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
);
}
/**
* Test if the banner is hidden when WooCommerce Services is installed but not up to date.
*/
public function test_if_banner_hidden_when_wcs_not_installed() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.4', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_allow_banner(), false );
}
);
}
/**
* Test if the banner is displayed when site is in 'b' group.
*/
public function test_display_banner_if_b_flag() {
$this->with_order(
function( $that ) {
update_option( 'woocommerce_shipping_prompt_ab', 'b' );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), true );
}
);
}
/**
* Test if the banner is displayed when site is in 'a' group.
*/
public function test_no_display_banner_if_a_flag() {
$this->with_order(
function( $that ) {
update_option( 'woocommerce_shipping_prompt_ab', 'a' );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
);
}
/**
* Creates a test order.
*/
private function create_order() {
$product = WC_Helper_Product::create_simple_product();
$product->set_props( array( 'virtual' => true ) );
$order = new WC_Order();
$order_item = new WC_Order_Item_Product();
$order_item->set_props( array( 'product' => $product ) );
$order->add_item( $order_item );
$order->save();
global $post;
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
$post = new \stdClass();
$post->ID = $order->get_id();
return $order;
}
/**
* Destroys the test order.
*
* @param object $order to destroy.
*/
private function destroy_order( $order ) {
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
$product->delete( true );
$item->delete( true );
}
$order->delete( true );
}
/**
* Wraps a function call within an order creation/deletion lifecycle.
*
* @param function $callback to wrap.
*/
private function with_order( $callback ) {
$order = $this->create_order();
$callback( $this );
$this->destroy_order( $order );
}
}

View File

@ -77,6 +77,7 @@ const wpAdminScripts = [
'onboarding-product-notice',
'onboarding-product-import-notice',
'onboarding-tax-notice',
'print-shipping-label-banner',
'onboarding-menu-experience',
];
wpAdminScripts.forEach( ( name ) => {