woocommerce/plugins/woocommerce-admin/client/profile-wizard/steps/business-details.js

954 lines
23 KiB
JavaScript
Raw Normal View History

/**
* External dependencies
*/
import { __, _n, _x, sprintf } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import {
Button,
CheckboxControl,
FormToggle,
Popover,
} from '@wordpress/components';
import interpolateComponents from 'interpolate-components';
wp.data Settings refactor add data store for settings using wp.data add use-select-with-refresh example replace fresh-data usage with new settings data store for settings page Add data package move to packages Fix isDirty after save Add isBusy to primary button when saving update Readme remove comment readme to use useSelect Revert "update Readme" This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. Data Layer: Settings page to use Settings store (https://github.com/woocommerce/woocommerce-admin/pull/3430) * Data Layer: Settings store as source of truth for settings page This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. * fixup * save on reset * non mutable constants * add set/getSettings * save using setSettings * separate HOC * cleanup * remove settingsToData * withHydration * remove withSettings HOC * renmove useSettins for now * withSettingsHydration updates * Revert "withSettingsHydration updates" This reverts commit f2adf108fbe19b574978fea5925a1a18e7ed3007. * rename withSettingsHydration * redo withSettingsHydration simplification * restore * useSettings * render using useSettings * handleInputChange working * get setIsDirty working * saving works * reset and cleanup * cleanup * use snake case on hook files * use clearIsDirty * Avoid mutation on setting update * remove @todo * persiting -> isPersisting * better reducer ternaries * add wcSettings as arg to withSettingsHydration reset package-lock Settings: split out mutable wcAdminSettings (https://github.com/woocommerce/woocommerce-admin/pull/3675) Settings: handle async settings groups (https://github.com/woocommerce/woocommerce-admin/pull/3707)
2020-03-25 03:20:17 +00:00
import { withDispatch, withSelect } from '@wordpress/data';
import { keys, get, pickBy } from 'lodash';
/**
* WooCommerce dependencies
*/
import { formatValue } from '@woocommerce/number';
import { getSetting } from '@woocommerce/wc-admin-settings';
import {
ONBOARDING_STORE_NAME,
PLUGINS_STORE_NAME,
pluginNames,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
/**
* Internal dependencies
*/
import {
H,
Card,
SelectControl,
Form,
TextControl,
} from '@woocommerce/components';
import { recordEvent } from 'lib/tracks';
import { getCountryCode, getCurrencyRegion } from 'dashboard/utils';
import { CurrencyContext } from 'lib/currency-context';
import { createNoticesFromResponse } from 'lib/notices';
const wcAdminAssetUrl = getSetting( 'wcAdminAssetUrl', '' );
class BusinessDetails extends Component {
constructor( props ) {
super();
const settings = get( props, 'settings', {} );
const profileItems = get( props, 'profileItems', {} );
const industrySlugs = get( profileItems, 'industry', [] ).map(
( industry ) => industry.slug
);
const businessExtensions = get(
profileItems,
'business_extensions',
false
);
this.state = {
isPopoverVisible: false,
};
this.initialValues = {
other_platform: profileItems.other_platform || '',
other_platform_name: profileItems.other_platform_name || '',
product_count: profileItems.product_count || '',
selling_venues: profileItems.selling_venues || '',
revenue: profileItems.revenue || '',
'facebook-for-woocommerce': businessExtensions
? businessExtensions.includes( 'facebook-for-woocommerce' )
: true,
'mailchimp-for-woocommerce': businessExtensions
? businessExtensions.includes( 'mailchimp-for-woocommerce' )
: true,
'kliken-marketing-for-google': businessExtensions
? businessExtensions.includes( 'kliken-marketing-for-google' )
: true,
install_extensions: true,
};
this.extensions = [
'facebook-for-woocommerce',
'mailchimp-for-woocommerce',
'kliken-marketing-for-google',
];
this.bundleInstall =
getCountryCode( settings.woocommerce_default_country ) === 'US' &&
( industrySlugs.includes( 'fashion-apparel-accessories' ) ||
industrySlugs.includes( 'health-beauty' ) );
this.onContinue = this.onContinue.bind( this );
this.validate = this.validate.bind( this );
this.getNumberRangeString = this.getNumberRangeString.bind( this );
this.numberFormat = this.numberFormat.bind( this );
}
async onContinue( values ) {
const {
createNotice,
goToNextStep,
installAndActivatePlugins,
updateProfileItems,
} = this.props;
const {
install_extensions: installExtensions,
other_platform: otherPlatform,
other_platform_name: otherPlatformName,
product_count: productCount,
revenue,
selling_venues: sellingVenues,
} = values;
const businessExtensions = this.getBusinessExtensions( values );
const { getCurrencyConfig } = this.context;
recordEvent( 'storeprofiler_store_business_details_continue', {
product_number: productCount,
already_selling: sellingVenues,
currency: getCurrencyConfig().code,
revenue,
used_platform: otherPlatform,
used_platform_name: otherPlatformName,
install_woocommerce_services: businessExtensions.includes(
'facebook-for-woocommerce'
),
install_jetpack: businessExtensions.includes( 'jetpack' ),
install_facebook: businessExtensions.includes(
'facebook-for-woocommerce'
),
install_mailchimp: businessExtensions.includes(
'mailchimp-for-woocommerce'
),
install_google_ads: businessExtensions.includes(
'kliken-marketing-for-google'
),
install_extensions: installExtensions,
bundle_install: this.bundleInstall,
} );
const _updates = {
other_platform: otherPlatform,
other_platform_name:
otherPlatform === 'other' ? otherPlatformName : '',
product_count: productCount,
revenue,
selling_venues: sellingVenues,
business_extensions: businessExtensions,
};
// Remove possible empty values like `revenue` and `other_platform`.
const updates = {};
Object.keys( _updates ).forEach( ( key ) => {
if ( _updates[ key ] !== '' ) {
updates[ key ] = _updates[ key ];
}
} );
const promises = [
updateProfileItems( updates ).catch( () => {
throw new Error();
} ),
];
if ( businessExtensions.length ) {
promises.push(
installAndActivatePlugins( businessExtensions )
.then( ( response ) => {
createNoticesFromResponse( response );
} )
.catch( ( error ) => {
createNoticesFromResponse( error );
throw new Error();
} )
);
}
Promise.all( promises )
.then( () => {
goToNextStep();
} )
.catch( () => {
createNotice(
'error',
__(
'There was a problem updating your business details.',
'woocommerce-admin'
)
);
} );
}
validate( values ) {
const errors = {};
if ( ! values.product_count.length ) {
errors.product_count = __(
'This field is required',
'woocommerce-admin'
);
}
if ( ! values.selling_venues.length ) {
errors.selling_venues = __(
'This field is required',
'woocommerce-admin'
);
}
if (
! values.other_platform.length &&
[ 'other', 'brick-mortar-other' ].includes( values.selling_venues )
) {
errors.other_platform = __(
'This field is required',
'woocommerce-admin'
);
}
if (
! values.other_platform_name &&
values.other_platform === 'other' &&
[ 'other', 'brick-mortar-other' ].includes( values.selling_venues )
) {
errors.other_platform_name = __(
'This field is required',
'woocommerce-admin'
);
}
if (
! values.revenue.length &&
[
'other',
'brick-mortar',
'brick-mortar-other',
'other-woocommerce',
].includes( values.selling_venues )
) {
errors.revenue = __(
'This field is required',
'woocommerce-admin'
);
}
return errors;
}
getBusinessExtensions( values ) {
if ( this.bundleInstall ) {
return values.install_extensions
? [
'jetpack',
'woocommerce-services',
'woocommerce-payments',
...this.extensions,
]
: [];
}
if ( values.selling_venues === '' ) {
return [];
}
return keys( pickBy( values ) ).filter( ( name ) =>
this.extensions.includes( name )
);
}
convertCurrency( value ) {
const region = getCurrencyRegion(
this.props.settings.woocommerce_default_country
);
if ( region === 'US' ) {
return value;
}
// These are rough exchange rates from USD. Precision is not paramount.
// The keys here should match the keys in `getCurrencyData`.
const exchangeRates = {
US: 1,
EU: 0.9,
IN: 71.24,
GB: 0.76,
BR: 4.19,
VN: 23172.5,
ID: 14031.0,
BD: 84.87,
PK: 154.8,
RU: 63.74,
TR: 5.75,
MX: 19.37,
CA: 1.32,
};
const exchangeRate = exchangeRates[ region ] || exchangeRates.US;
const digits = exchangeRate.toString().split( '.' )[ 0 ].length;
const multiplier = Math.pow( 10, 2 + digits );
return Math.round( ( value * exchangeRate ) / multiplier ) * multiplier;
}
numberFormat( value ) {
const { getCurrencyConfig } = this.context;
return formatValue( getCurrencyConfig(), 'number', value );
}
getNumberRangeString( min, max = false, format = this.numberFormat ) {
if ( ! max ) {
return sprintf(
_x(
'%s+',
'store product count or revenue',
'woocommerce-admin'
),
format( min )
);
}
return sprintf(
_x(
'%1$s - %2$s',
'store product count or revenue range',
'woocommerce-admin'
),
format( min ),
format( max )
);
}
renderBusinessExtensionHelpText( values ) {
const { isInstallingActivating } = this.props;
const extensions = this.getBusinessExtensions( values );
if ( extensions.length === 0 ) {
return null;
}
const extensionsList = extensions
.map( ( extension ) => {
return pluginNames[ extension ];
} )
.join( ', ' );
if ( isInstallingActivating ) {
return (
<p>
{ sprintf(
_n(
'Installing the following plugin: %s',
'Installing the following plugins: %s',
extensions.length,
'woocommerce-admin'
),
extensionsList
) }
</p>
);
}
const accountRequiredText = this.bundleInstall
? __(
'User accounts are required to use these features.',
'woocommerce-admin'
)
: '';
return (
<Fragment>
<p>
{ sprintf(
_n(
'The following plugin will be installed for free: %s. %s',
'The following plugins will be installed for free: %s. %s',
extensions.length,
'woocommerce-admin'
),
extensionsList,
accountRequiredText
) }
</p>
</Fragment>
);
}
renderBusinessExtensions( values, getInputProps ) {
// Show extensions when the currently selling elsewhere checkbox has been answered.
if ( values.selling_venues === '' ) {
return null;
}
const extensionBenefits = [
{
slug: 'facebook-for-woocommerce',
title: __( 'Market on Facebook', 'woocommerce-admin' ),
icon: 'onboarding/fb-woocommerce.png',
description: __(
'Grow your business by targeting the right people and driving sales with Facebook.',
'woocommerce-admin'
),
},
{
slug: 'mailchimp-for-woocommerce',
title: __(
'Contact customers with Mailchimp',
'woocommerce-admin'
),
icon: 'onboarding/mailchimp.png',
description: __(
'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.',
'woocommerce-admin'
),
},
{
slug: 'kliken-marketing-for-google',
title: __( 'Drive sales with Google Ads', 'woocommerce-admin' ),
icon: 'onboarding/g-shopping.png',
description: __(
'Get in front of new customers on Google and secure $150 in ads credit with Klikens integration.',
'woocommerce-admin'
),
},
];
return (
<Fragment>
{ extensionBenefits.map( ( benefit ) => (
<div
className="woocommerce-profile-wizard__benefit"
key={ benefit.title }
>
<div className="woocommerce-profile-wizard__business-extension">
<img
src={ wcAdminAssetUrl + benefit.icon }
alt=""
/>
</div>
<div className="woocommerce-profile-wizard__benefit-content">
<H className="woocommerce-profile-wizard__benefit-title">
{ benefit.title }
</H>
<p>{ benefit.description }</p>
</div>
<div className="woocommerce-profile-wizard__benefit-toggle">
<FormToggle
checked={ values[ benefit.slug ] }
{ ...getInputProps( benefit.slug ) }
/>
</div>
</div>
) ) }
</Fragment>
);
}
renderBusinessExtensionsBundle( values, getInputProps ) {
const { isPopoverVisible } = this.state;
return (
<div className="woocommerce-business-extensions">
<label htmlFor="woocommerce-business-extensions__checkbox">
<CheckboxControl
id="woocommerce-business-extensions__checkbox"
{ ...getInputProps( 'install_extensions' ) }
/>
<span className="woocommerce-business-extensions__label-text">
{ interpolateComponents( {
mixedString: __(
'Install recommended {{strong}}free{{/strong}} business features',
'woocommerce-admin'
),
components: {
strong: <strong />,
},
} ) }
<span className="woocommerce-business-extensions__label-subtext">
{ __( 'Requires an account', 'woocommerce-admin' ) }
</span>
</span>
</label>
<div className="woocommerce-business-extensions__popover-wrapper">
<Button
isTertiary
label={ __(
'Learn more about recommended free business features',
'woocommerce-admin'
) }
onClick={ () => {
recordEvent(
'storeprofiler_store_business_details_popover'
);
this.setState( { isPopoverVisible: true } );
} }
>
<i
className="material-icons-outlined"
aria-hidden="true"
>
info
</i>
</Button>
{ isPopoverVisible && (
<Popover
className="woocommerce-business-extensions__popover"
focusOnMount="container"
position="top center"
onClose={ () =>
this.setState( { isPopoverVisible: false } )
}
>
<div className="woocommerce-business-extensions__benefits">
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Manage your store on the go with the WooCommerce mobile app',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Accept credit cards with WooCommerce Payments',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Speed & security enhancements',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Automatic sales taxes',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Market on Facebook',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Contact customers with Mailchimp',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Drive sales with Google Ads',
'woocommerce-admin'
) }
</div>
<div className="woocommerce-business-extensions__benefit">
<i
className="material-icons-outlined"
aria-hidden="true"
>
check
</i>
{ __(
'Print shipping labels at home',
'woocommerce-admin'
) }
</div>
</div>
</Popover>
) }
</div>
</div>
);
}
render() {
const {
goToNextStep,
isInstallingActivating,
hasInstallActivateError,
} = this.props;
const { formatAmount } = this.context;
const productCountOptions = [
{
key: '0',
label: __(
"I don't have any products yet.",
'woocommerce-admin'
),
},
{
key: '1-10',
label: this.getNumberRangeString( 1, 10 ),
},
{
key: '11-100',
label: this.getNumberRangeString( 11, 100 ),
},
{
key: '101-1000',
label: this.getNumberRangeString( 101, 1000 ),
},
{
key: '1000+',
label: this.getNumberRangeString( 1000 ),
},
];
const revenueOptions = [
{
key: 'none',
label: sprintf(
/* translators: %s: $0 revenue amount */
__( "%s (I'm just getting started)", 'woocommerce-admin' ),
formatAmount( 0 )
),
},
{
key: 'up-to-2500',
label: sprintf(
/* translators: %s: A given revenue amount, e.g., $2500 */
__( 'Up to %s', 'woocommerce-admin' ),
formatAmount( this.convertCurrency( 2500 ) )
),
},
{
key: '2500-10000',
label: this.getNumberRangeString(
this.convertCurrency( 2500 ),
this.convertCurrency( 10000 ),
formatAmount
),
},
{
key: '10000-50000',
label: this.getNumberRangeString(
this.convertCurrency( 10000 ),
this.convertCurrency( 50000 ),
formatAmount
),
},
{
key: '50000-250000',
label: this.getNumberRangeString(
this.convertCurrency( 50000 ),
this.convertCurrency( 250000 ),
formatAmount
),
},
{
key: 'more-than-250000',
label: sprintf(
/* translators: %s: A given revenue amount, e.g., $250000 */
__( 'More than %s', 'woocommerce-admin' ),
formatAmount( this.convertCurrency( 250000 ) )
),
},
];
const sellingVenueOptions = [
{
key: 'no',
label: __( 'No', 'woocommerce-admin' ),
},
{
key: 'other',
label: __( 'Yes, on another platform', 'woocommerce-admin' ),
},
{
key: 'other-woocommerce',
label: __(
'Yes, I own a different store powered by WooCommerce',
'woocommerce-admin'
),
},
{
key: 'brick-mortar',
label: __(
'Yes, in person at physical stores and/or events',
'woocommerce-admin'
),
},
{
key: 'brick-mortar-other',
label: __(
'Yes, on another platform and in person at physical stores and/or events',
'woocommerce-admin'
),
},
];
const otherPlatformOptions = [
{
key: 'shopify',
label: __( 'Shopify', 'woocommerce-admin' ),
},
{
key: 'bigcommerce',
label: __( 'BigCommerce', 'woocommerce-admin' ),
},
{
key: 'magento',
label: __( 'Magento', 'woocommerce-admin' ),
},
{
key: 'wix',
label: __( 'Wix', 'woocommerce-admin' ),
},
{
key: 'amazon',
label: __( 'Amazon', 'woocommerce-admin' ),
},
{
key: 'ebay',
label: __( 'eBay', 'woocommerce-admin' ),
},
{
key: 'etsy',
label: __( 'Etsy', 'woocommerce-admin' ),
},
{
key: 'squarespace',
label: __( 'Squarespace', 'woocommerce-admin' ),
},
{
key: 'other',
label: __( 'Other', 'woocommerce-admin' ),
},
];
return (
<Form
initialValues={ this.initialValues }
onSubmitCallback={ this.onContinue }
validate={ this.validate }
>
{ ( { getInputProps, handleSubmit, values, isValidForm } ) => {
return (
<Fragment>
<H className="woocommerce-profile-wizard__header-title">
{ __(
'Tell us about your business',
'woocommerce-admin'
) }
</H>
<H className="woocommerce-profile-wizard__header-subtitle">
{ __(
"We'd love to know if you are just getting started or you already have a business in place.",
'woocommerce-admin'
) }
</H>
<Card>
<Fragment>
<SelectControl
label={ __(
'How many products do you plan to display?',
'woocommerce-admin'
) }
options={ productCountOptions }
required
{ ...getInputProps( 'product_count' ) }
/>
<SelectControl
label={ __(
'Currently selling elsewhere?',
'woocommerce-admin'
) }
options={ sellingVenueOptions }
required
{ ...getInputProps( 'selling_venues' ) }
/>
{ [
'other',
'brick-mortar',
'brick-mortar-other',
'other-woocommerce',
].includes( values.selling_venues ) && (
<SelectControl
label={ __(
"What's your current annual revenue?",
'woocommerce-admin'
) }
options={ revenueOptions }
required
{ ...getInputProps( 'revenue' ) }
/>
) }
{ [
'other',
'brick-mortar-other',
].includes( values.selling_venues ) && (
<Fragment>
<div className="business-competitors">
<SelectControl
label={ __(
'Which platform is the store using?',
'woocommerce-admin'
) }
options={
otherPlatformOptions
}
required
{ ...getInputProps(
'other_platform'
) }
/>
{ values.other_platform ===
'other' && (
<TextControl
label={ __(
'What is the platform name?',
'woocommerce-admin'
) }
required
{ ...getInputProps(
'other_platform_name'
) }
/>
) }
</div>
</Fragment>
) }
{ this.bundleInstall
? this.renderBusinessExtensionsBundle(
values,
getInputProps
)
: this.renderBusinessExtensions(
values,
getInputProps
) }
<div className="woocommerce-profile-wizard__card-actions">
<Button
isPrimary
onClick={ handleSubmit }
disabled={ ! isValidForm }
isBusy={ isInstallingActivating }
>
{ ! hasInstallActivateError
? __(
'Continue',
'woocommerce-admin'
)
: __(
'Retry',
'woocommerce-admin'
) }
</Button>
{ hasInstallActivateError && (
<Button
onClick={ () => goToNextStep() }
>
{ __(
'Continue without installing',
'woocommerce-admin'
) }
</Button>
) }
</div>
</Fragment>
</Card>
{ this.renderBusinessExtensionHelpText( values ) }
</Fragment>
);
} }
</Form>
);
}
}
BusinessDetails.contextType = CurrencyContext;
export default compose(
withSelect( ( select ) => {
const { getSettings, getSettingsError } = select( SETTINGS_STORE_NAME );
const { getProfileItems, getOnboardingError } = select(
ONBOARDING_STORE_NAME
);
const { getPluginsError, isPluginsRequesting } = select(
PLUGINS_STORE_NAME
);
wp.data Settings refactor add data store for settings using wp.data add use-select-with-refresh example replace fresh-data usage with new settings data store for settings page Add data package move to packages Fix isDirty after save Add isBusy to primary button when saving update Readme remove comment readme to use useSelect Revert "update Readme" This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. Data Layer: Settings page to use Settings store (https://github.com/woocommerce/woocommerce-admin/pull/3430) * Data Layer: Settings store as source of truth for settings page This reverts commit 7402fd49b8f384fde5878e0bee0616f0a87bb4f6. * fixup * save on reset * non mutable constants * add set/getSettings * save using setSettings * separate HOC * cleanup * remove settingsToData * withHydration * remove withSettings HOC * renmove useSettins for now * withSettingsHydration updates * Revert "withSettingsHydration updates" This reverts commit f2adf108fbe19b574978fea5925a1a18e7ed3007. * rename withSettingsHydration * redo withSettingsHydration simplification * restore * useSettings * render using useSettings * handleInputChange working * get setIsDirty working * saving works * reset and cleanup * cleanup * use snake case on hook files * use clearIsDirty * Avoid mutation on setting update * remove @todo * persiting -> isPersisting * better reducer ternaries * add wcSettings as arg to withSettingsHydration reset package-lock Settings: split out mutable wcAdminSettings (https://github.com/woocommerce/woocommerce-admin/pull/3675) Settings: handle async settings groups (https://github.com/woocommerce/woocommerce-admin/pull/3707)
2020-03-25 03:20:17 +00:00
const { general: settings = {} } = getSettings( 'general' );
return {
hasInstallActivateError:
getPluginsError( 'installPlugins' ) ||
getPluginsError( 'activatePlugins' ),
isError: Boolean( getOnboardingError( 'updateProfileItems' ) ),
profileItems: getProfileItems(),
isSettingsError: Boolean( getSettingsError( 'general' ) ),
settings,
isInstallingActivating:
isPluginsRequesting( 'installPlugins' ) ||
isPluginsRequesting( 'activatePlugins' ) ||
isPluginsRequesting( 'getJetpackConnectUrl' ),
};
} ),
withDispatch( ( dispatch ) => {
const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME );
const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME );
const { createNotice } = dispatch( 'core/notices' );
return {
createNotice,
installAndActivatePlugins,
updateProfileItems,
};
} )
)( BusinessDetails );