Add newsletter signup to profiler (https://github.com/woocommerce/woocommerce-admin/pull/7601)
* Add email address field to store details step in OBW (https://github.com/woocommerce/woocommerce-admin/pull/7552) * Subscribe store_email to MailChimp (https://github.com/woocommerce/woocommerce-admin/pull/7579) * Add prefill for email field in OBW (https://github.com/woocommerce/woocommerce-admin/pull/7570) * Add error handling for email validation errors from backend (https://github.com/woocommerce/woocommerce-admin/pull/7590) * Remove OnboardingEmailMarketing note class (https://github.com/woocommerce/woocommerce-admin/pull/7595) Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com> Co-authored-by: Moon <moon.kyong@automattic.com>
This commit is contained in:
parent
ccdd32282d
commit
6d23ab7ea1
|
@ -2,6 +2,18 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Add Newsletter Signup #7601
|
||||||
|
|
||||||
|
- Start OBW and set up your browser console to monitor tracks. To do this, run `localStorage.setItem( 'debug', 'wc-admin:*' );`
|
||||||
|
- Observe "Get tips, product updates and inspiration straight to your mailbox" checkbox and "Email address" field in the Store Details step.
|
||||||
|
- Checking the checkbox should make the email field required, you should not be able to continue if it's not filled.
|
||||||
|
- Fill in the email address field with a valid email and click on continue.
|
||||||
|
- Observe in the track `wcadmin_storeprofiler_store_details_continue` with prop `email_signup` that appropriately flags if the user agreed to receive marketing emails.
|
||||||
|
- Continue until Business Features step.
|
||||||
|
- Observe the "I'm setting up a store for a client" checkbox in the step.
|
||||||
|
- Click on continue.
|
||||||
|
- Observe in the track `wcadmin_storeprofiler_store_business_details_continue_variant` with prop `setup_client` that appropriately flags if the user is setting up store for a client.
|
||||||
|
|
||||||
### Making business details sticky in OBW #7426
|
### Making business details sticky in OBW #7426
|
||||||
|
|
||||||
1. Start out with a fresh store
|
1. Start out with a fresh store
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: Dev
|
||||||
|
|
||||||
|
Add email address field to OBW #7552
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: Add
|
||||||
|
|
||||||
|
Added MailchimpScheduler that runs daily to subscribe store_email in the profile data #7579
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: Update
|
||||||
|
|
||||||
|
Deleted OnboardingEmailMarketing note class #7595
|
|
@ -11,6 +11,8 @@ import {
|
||||||
CardFooter,
|
CardFooter,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
__experimentalText as Text,
|
__experimentalText as Text,
|
||||||
|
FlexItem,
|
||||||
|
CheckboxControl,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
import { withDispatch, withSelect } from '@wordpress/data';
|
import { withDispatch, withSelect } from '@wordpress/data';
|
||||||
import { SelectControl, Form, TextControl } from '@woocommerce/components';
|
import { SelectControl, Form, TextControl } from '@woocommerce/components';
|
||||||
|
@ -138,6 +140,7 @@ class BusinessDetails extends Component {
|
||||||
product_count: productCount,
|
product_count: productCount,
|
||||||
revenue,
|
revenue,
|
||||||
selling_venues: sellingVenues,
|
selling_venues: sellingVenues,
|
||||||
|
setup_client: isSetupClient,
|
||||||
} = this.state.savedValues;
|
} = this.state.savedValues;
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
|
@ -147,6 +150,7 @@ class BusinessDetails extends Component {
|
||||||
product_count: productCount,
|
product_count: productCount,
|
||||||
revenue,
|
revenue,
|
||||||
selling_venues: sellingVenues,
|
selling_venues: sellingVenues,
|
||||||
|
setup_client: isSetupClient,
|
||||||
...additions,
|
...additions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -242,6 +246,7 @@ class BusinessDetails extends Component {
|
||||||
product_count: productCount,
|
product_count: productCount,
|
||||||
selling_venues: sellingVenues,
|
selling_venues: sellingVenues,
|
||||||
revenue,
|
revenue,
|
||||||
|
setup_client: isSetupClient,
|
||||||
} ) {
|
} ) {
|
||||||
const { getCurrencyConfig } = this.context;
|
const { getCurrencyConfig } = this.context;
|
||||||
|
|
||||||
|
@ -252,6 +257,7 @@ class BusinessDetails extends Component {
|
||||||
revenue,
|
revenue,
|
||||||
used_platform: otherPlatform,
|
used_platform: otherPlatform,
|
||||||
used_platform_name: otherPlatformName,
|
used_platform_name: otherPlatformName,
|
||||||
|
setup_client: isSetupClient,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,7 +398,22 @@ class BusinessDetails extends Component {
|
||||||
</>
|
</>
|
||||||
) }
|
) }
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter isBorderless justify="center">
|
<CardFooter isBorderless>
|
||||||
|
<FlexItem>
|
||||||
|
<div className="woocommerce-profile-wizard__client">
|
||||||
|
<CheckboxControl
|
||||||
|
label={ __(
|
||||||
|
"I'm setting up a store for a client",
|
||||||
|
'woocommerce-admin'
|
||||||
|
) }
|
||||||
|
{ ...getInputProps(
|
||||||
|
'setup_client'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FlexItem>
|
||||||
|
</CardFooter>
|
||||||
|
<CardFooter justify="center">
|
||||||
<Button
|
<Button
|
||||||
isPrimary
|
isPrimary
|
||||||
onClick={ async () => {
|
onClick={ async () => {
|
||||||
|
|
|
@ -18,8 +18,5 @@
|
||||||
&__body {
|
&__body {
|
||||||
padding: $gap $gap 0;
|
padding: $gap $gap 0;
|
||||||
}
|
}
|
||||||
&__footer {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const BusinessDetailsStep = ( props ) => {
|
||||||
product_count: profileItems.product_count || '',
|
product_count: profileItems.product_count || '',
|
||||||
selling_venues: profileItems.selling_venues || '',
|
selling_venues: profileItems.selling_venues || '',
|
||||||
revenue: profileItems.revenue || '',
|
revenue: profileItems.revenue || '',
|
||||||
|
setup_client: profileItems.setup_client || false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,12 +9,13 @@ import {
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CheckboxControl,
|
CheckboxControl,
|
||||||
FlexItem as MaybeFlexItem,
|
FlexItem as MaybeFlexItem,
|
||||||
|
Spinner,
|
||||||
Popover,
|
Popover,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
import { Component } from '@wordpress/element';
|
import { Component, useRef } from '@wordpress/element';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import { withDispatch, withSelect } from '@wordpress/data';
|
import { withDispatch, withSelect } from '@wordpress/data';
|
||||||
import { Form } from '@woocommerce/components';
|
import { Form, TextControl } from '@woocommerce/components';
|
||||||
import { getSetting } from '@woocommerce/wc-admin-settings';
|
import { getSetting } from '@woocommerce/wc-admin-settings';
|
||||||
import {
|
import {
|
||||||
ONBOARDING_STORE_NAME,
|
ONBOARDING_STORE_NAME,
|
||||||
|
@ -48,10 +49,15 @@ const FlextItemSubstitute = ( { children, align } ) => {
|
||||||
};
|
};
|
||||||
const FlexItem = MaybeFlexItem || FlextItemSubstitute;
|
const FlexItem = MaybeFlexItem || FlextItemSubstitute;
|
||||||
|
|
||||||
|
const LoadingPlaceholder = () => (
|
||||||
|
<div className="woocommerce-admin__store-details__spinner">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
class StoreDetails extends Component {
|
class StoreDetails extends Component {
|
||||||
constructor( props ) {
|
constructor( props ) {
|
||||||
super( props );
|
super( props );
|
||||||
const { profileItems, settings } = props;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showUsageModal: false,
|
showUsageModal: false,
|
||||||
|
@ -60,22 +66,6 @@ class StoreDetails extends Component {
|
||||||
isSkipSetupPopoverVisible: false,
|
isSkipSetupPopoverVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if a store address is set so that we don't default
|
|
||||||
// to WooCommerce's default country of the UK.
|
|
||||||
const countryState =
|
|
||||||
( settings.woocommerce_store_address &&
|
|
||||||
settings.woocommerce_default_country ) ||
|
|
||||||
'';
|
|
||||||
|
|
||||||
this.initialValues = {
|
|
||||||
addressLine1: settings.woocommerce_store_address || '',
|
|
||||||
addressLine2: settings.woocommerce_store_address_2 || '',
|
|
||||||
city: settings.woocommerce_store_city || '',
|
|
||||||
countryState,
|
|
||||||
postCode: settings.woocommerce_store_postcode || '',
|
|
||||||
isClient: profileItems.setup_client || false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onContinue = this.onContinue.bind( this );
|
this.onContinue = this.onContinue.bind( this );
|
||||||
this.onSubmit = this.onSubmit.bind( this );
|
this.onSubmit = this.onSubmit.bind( this );
|
||||||
}
|
}
|
||||||
|
@ -109,12 +99,11 @@ class StoreDetails extends Component {
|
||||||
const {
|
const {
|
||||||
createNotice,
|
createNotice,
|
||||||
goToNextStep,
|
goToNextStep,
|
||||||
isSettingsError,
|
|
||||||
updateProfileItems,
|
updateProfileItems,
|
||||||
isProfileItemsError,
|
|
||||||
updateAndPersistSettingsForGroup,
|
updateAndPersistSettingsForGroup,
|
||||||
profileItems,
|
profileItems,
|
||||||
settings,
|
settings,
|
||||||
|
errorsRef,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const currencySettings = this.deriveCurrencySettings(
|
const currencySettings = this.deriveCurrencySettings(
|
||||||
|
@ -126,7 +115,7 @@ class StoreDetails extends Component {
|
||||||
recordEvent( 'storeprofiler_store_details_continue', {
|
recordEvent( 'storeprofiler_store_details_continue', {
|
||||||
store_country: getCountryCode( values.countryState ),
|
store_country: getCountryCode( values.countryState ),
|
||||||
derived_currency: currencySettings.currency_code,
|
derived_currency: currencySettings.currency_code,
|
||||||
setup_client: values.isClient,
|
email_signup: values.isAgreeMarketing,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await updateAndPersistSettingsForGroup( 'general', {
|
await updateAndPersistSettingsForGroup( 'general', {
|
||||||
|
@ -147,7 +136,11 @@ class StoreDetails extends Component {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const profileItemsToUpdate = { setup_client: values.isClient };
|
const profileItemsToUpdate = {
|
||||||
|
is_agree_marketing: values.isAgreeMarketing,
|
||||||
|
store_email: values.storeEmail,
|
||||||
|
};
|
||||||
|
|
||||||
const region = getCurrencyRegion( values.countryState );
|
const region = getCurrencyRegion( values.countryState );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -174,9 +167,20 @@ class StoreDetails extends Component {
|
||||||
profileItemsToUpdate.industry = trimmedIndustries;
|
profileItemsToUpdate.industry = trimmedIndustries;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateProfileItems( profileItemsToUpdate );
|
let errorMessages = [];
|
||||||
|
try {
|
||||||
|
await updateProfileItems( profileItemsToUpdate );
|
||||||
|
} catch ( error ) {
|
||||||
|
// Array of error messages obtained from API response.
|
||||||
|
if ( error?.data?.params ) {
|
||||||
|
errorMessages = Object.values( error.data.params );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! isSettingsError && ! isProfileItemsError ) {
|
if (
|
||||||
|
! Boolean( errorsRef.current.settings ) &&
|
||||||
|
! errorMessages.length
|
||||||
|
) {
|
||||||
goToNextStep();
|
goToNextStep();
|
||||||
} else {
|
} else {
|
||||||
createNotice(
|
createNotice(
|
||||||
|
@ -186,9 +190,39 @@ class StoreDetails extends Component {
|
||||||
'woocommerce-admin'
|
'woocommerce-admin'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
errorMessages.forEach( ( message ) =>
|
||||||
|
createNotice( 'error', message )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateStoreDetails( values ) {
|
||||||
|
const errors = validateStoreAddress( values );
|
||||||
|
|
||||||
|
if (
|
||||||
|
values.isAgreeMarketing &&
|
||||||
|
( ! values.storeEmail || ! values.storeEmail.trim().length )
|
||||||
|
) {
|
||||||
|
errors.storeEmail = __(
|
||||||
|
'Please add an email address',
|
||||||
|
'woocommerce-admin'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
values.storeEmail &&
|
||||||
|
values.storeEmail.trim().length &&
|
||||||
|
values.storeEmail.indexOf( '@' ) === -1
|
||||||
|
) {
|
||||||
|
errors.storeEmail = __(
|
||||||
|
'Invalid email address',
|
||||||
|
'woocommerce-admin'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
showUsageModal,
|
showUsageModal,
|
||||||
|
@ -196,7 +230,7 @@ class StoreDetails extends Component {
|
||||||
isStoreDetailsPopoverVisible,
|
isStoreDetailsPopoverVisible,
|
||||||
isSkipSetupPopoverVisible,
|
isSkipSetupPopoverVisible,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { skipProfiler, isBusy } = this.props;
|
const { skipProfiler, isLoading, isBusy, initialValues } = this.props;
|
||||||
|
|
||||||
/* eslint-disable @wordpress/i18n-no-collapsible-whitespace */
|
/* eslint-disable @wordpress/i18n-no-collapsible-whitespace */
|
||||||
const skipSetupText = __(
|
const skipSetupText = __(
|
||||||
|
@ -210,6 +244,14 @@ class StoreDetails extends Component {
|
||||||
);
|
);
|
||||||
/* eslint-enable @wordpress/i18n-no-collapsible-whitespace */
|
/* eslint-enable @wordpress/i18n-no-collapsible-whitespace */
|
||||||
|
|
||||||
|
if ( isLoading ) {
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-profile-wizard__store-details">
|
||||||
|
<LoadingPlaceholder />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-profile-wizard__store-details">
|
<div className="woocommerce-profile-wizard__store-details">
|
||||||
<div className="woocommerce-profile-wizard__step-header">
|
<div className="woocommerce-profile-wizard__step-header">
|
||||||
|
@ -258,9 +300,9 @@ class StoreDetails extends Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
initialValues={ this.initialValues }
|
initialValues={ initialValues }
|
||||||
onSubmit={ this.onSubmit }
|
onSubmit={ this.onSubmit }
|
||||||
validate={ validateStoreAddress }
|
validate={ this.validateStoreDetails }
|
||||||
>
|
>
|
||||||
{ ( {
|
{ ( {
|
||||||
getInputProps,
|
getInputProps,
|
||||||
|
@ -292,17 +334,29 @@ class StoreDetails extends Component {
|
||||||
getInputProps={ getInputProps }
|
getInputProps={ getInputProps }
|
||||||
setValue={ setValue }
|
setValue={ setValue }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextControl
|
||||||
|
label={ __(
|
||||||
|
'Email address',
|
||||||
|
'woocommerce-admin'
|
||||||
|
) }
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
{ ...getInputProps( 'storeEmail' ) }
|
||||||
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<FlexItem>
|
<FlexItem>
|
||||||
<div className="woocommerce-profile-wizard__client">
|
<div>
|
||||||
<CheckboxControl
|
<CheckboxControl
|
||||||
label={ __(
|
label={ __(
|
||||||
"I'm setting up a store for a client",
|
'Get tips, product updates and inspiration straight to your mailbox',
|
||||||
'woocommerce-admin'
|
'woocommerce-admin'
|
||||||
) }
|
) }
|
||||||
{ ...getInputProps( 'isClient' ) }
|
{ ...getInputProps(
|
||||||
|
'isAgreeMarketing'
|
||||||
|
) }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
|
@ -376,30 +430,59 @@ export default compose(
|
||||||
isUpdateSettingsRequesting,
|
isUpdateSettingsRequesting,
|
||||||
} = select( SETTINGS_STORE_NAME );
|
} = select( SETTINGS_STORE_NAME );
|
||||||
const {
|
const {
|
||||||
getOnboardingError,
|
|
||||||
getProfileItems,
|
getProfileItems,
|
||||||
isOnboardingRequesting,
|
isOnboardingRequesting,
|
||||||
|
getEmailPrefill,
|
||||||
|
hasFinishedResolution: hasFinishedResolutionOnboarding,
|
||||||
} = select( ONBOARDING_STORE_NAME );
|
} = select( ONBOARDING_STORE_NAME );
|
||||||
const { isResolving } = select( OPTIONS_STORE_NAME );
|
const { isResolving } = select( OPTIONS_STORE_NAME );
|
||||||
|
|
||||||
const profileItems = getProfileItems();
|
const profileItems = getProfileItems();
|
||||||
const isProfileItemsError = Boolean(
|
|
||||||
getOnboardingError( 'updateProfileItems' )
|
|
||||||
);
|
|
||||||
|
|
||||||
const { general: settings = {} } = getSettings( 'general' );
|
const { general: settings = {} } = getSettings( 'general' );
|
||||||
const isSettingsError = Boolean( getSettingsError( 'general' ) );
|
|
||||||
const isBusy =
|
const isBusy =
|
||||||
isOnboardingRequesting( 'updateProfileItems' ) ||
|
isOnboardingRequesting( 'updateProfileItems' ) ||
|
||||||
isUpdateSettingsRequesting( 'general' ) ||
|
isUpdateSettingsRequesting( 'general' ) ||
|
||||||
isResolving( 'getOption', [ 'woocommerce_allow_tracking' ] );
|
isResolving( 'getOption', [ 'woocommerce_allow_tracking' ] );
|
||||||
|
const isLoading =
|
||||||
|
! hasFinishedResolutionOnboarding( 'getProfileItems' ) ||
|
||||||
|
! hasFinishedResolutionOnboarding( 'getEmailPrefill' );
|
||||||
|
const errorsRef = useRef( {
|
||||||
|
settings: null,
|
||||||
|
} );
|
||||||
|
errorsRef.current = {
|
||||||
|
settings: getSettingsError( 'general' ),
|
||||||
|
};
|
||||||
|
// Check if a store address is set so that we don't default
|
||||||
|
// to WooCommerce's default country of the UK.
|
||||||
|
const countryState =
|
||||||
|
( settings.woocommerce_store_address &&
|
||||||
|
settings.woocommerce_default_country ) ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
addressLine1: settings.woocommerce_store_address || '',
|
||||||
|
addressLine2: settings.woocommerce_store_address_2 || '',
|
||||||
|
city: settings.woocommerce_store_city || '',
|
||||||
|
countryState,
|
||||||
|
postCode: settings.woocommerce_store_postcode || '',
|
||||||
|
isAgreeMarketing:
|
||||||
|
typeof profileItems.is_agree_marketing === 'boolean'
|
||||||
|
? profileItems.is_agree_marketing
|
||||||
|
: true,
|
||||||
|
storeEmail:
|
||||||
|
typeof profileItems.store_email === 'string'
|
||||||
|
? profileItems.store_email
|
||||||
|
: getEmailPrefill(),
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isProfileItemsError,
|
initialValues,
|
||||||
isSettingsError,
|
isLoading,
|
||||||
profileItems,
|
profileItems,
|
||||||
isBusy,
|
isBusy,
|
||||||
settings,
|
settings,
|
||||||
|
errorsRef,
|
||||||
};
|
};
|
||||||
} ),
|
} ),
|
||||||
withDispatch( ( dispatch ) => {
|
withDispatch( ( dispatch ) => {
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
.woocommerce-profile-wizard__store-details {
|
.woocommerce-profile-wizard__store-details {
|
||||||
|
.woocommerce-admin__store-details__spinner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.components-popover .components-popover__content {
|
.components-popover .components-popover__content {
|
||||||
min-width: 360px;
|
min-width: 360px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,10 +193,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-profile-wizard__client {
|
|
||||||
margin: $gap-smaller 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-profile-wizard__checkbox {
|
.woocommerce-profile-wizard__checkbox {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -5,7 +5,12 @@ import { BasePage } from '../../pages/BasePage';
|
||||||
import { waitForElementByText } from '../../utils/actions';
|
import { waitForElementByText } from '../../utils/actions';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const { setCheckbox, unsetCheckbox } = require( '@woocommerce/e2e-utils' );
|
const {
|
||||||
|
setCheckbox,
|
||||||
|
unsetCheckbox,
|
||||||
|
verifyCheckboxIsSet,
|
||||||
|
verifyCheckboxIsUnset,
|
||||||
|
} = require( '@woocommerce/e2e-utils' );
|
||||||
/* eslint-enable @typescript-eslint/no-var-requires */
|
/* eslint-enable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
export class BusinessSection extends BasePage {
|
export class BusinessSection extends BasePage {
|
||||||
|
@ -66,4 +71,18 @@ export class BusinessSection extends BasePage {
|
||||||
'.woocommerce-profile-wizard__benefit .components-form-toggle__input'
|
'.woocommerce-profile-wizard__benefit .components-form-toggle__input'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async selectSetupForClient() {
|
||||||
|
await setCheckbox( '.components-checkbox-control__input' );
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkClientSetupCheckbox( selected: boolean ) {
|
||||||
|
if ( selected ) {
|
||||||
|
await verifyCheckboxIsSet( '.components-checkbox-control__input' );
|
||||||
|
} else {
|
||||||
|
await verifyCheckboxIsUnset(
|
||||||
|
'.components-checkbox-control__input'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
*/
|
*/
|
||||||
import { DropdownTypeaheadField } from '../../elements/DropdownTypeaheadField';
|
import { DropdownTypeaheadField } from '../../elements/DropdownTypeaheadField';
|
||||||
import { BasePage } from '../../pages/BasePage';
|
import { BasePage } from '../../pages/BasePage';
|
||||||
|
import { waitForElementByText } from '../../utils/actions';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const {
|
const {
|
||||||
setCheckbox,
|
|
||||||
clearAndFillInput,
|
clearAndFillInput,
|
||||||
verifyCheckboxIsSet,
|
verifyCheckboxIsSet,
|
||||||
verifyCheckboxIsUnset,
|
verifyCheckboxIsUnset,
|
||||||
|
@ -22,6 +22,7 @@ interface StoreDetails {
|
||||||
countryRegion?: string;
|
countryRegion?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
postcode?: string;
|
postcode?: string;
|
||||||
|
storeEmail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StoreDetailsSection extends BasePage {
|
export class StoreDetailsSection extends BasePage {
|
||||||
|
@ -29,6 +30,10 @@ export class StoreDetailsSection extends BasePage {
|
||||||
return this.getDropdownTypeahead( '#woocommerce-select-control' );
|
return this.getDropdownTypeahead( '#woocommerce-select-control' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isDisplayed() {
|
||||||
|
await waitForElementByText( 'h2', 'Welcome to WooCommerce' );
|
||||||
|
}
|
||||||
|
|
||||||
async completeStoreDetailsSection( storeDetails: StoreDetails = {} ) {
|
async completeStoreDetailsSection( storeDetails: StoreDetails = {} ) {
|
||||||
// const onboardingWizard = new OnboardingWizard( page );
|
// const onboardingWizard = new OnboardingWizard( page );
|
||||||
// Fill store's address - first line
|
// Fill store's address - first line
|
||||||
|
@ -66,8 +71,14 @@ export class StoreDetailsSection extends BasePage {
|
||||||
config.get( 'addresses.admin.store.postcode' )
|
config.get( 'addresses.admin.store.postcode' )
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify that checkbox next to "I'm setting up a store for a client" is not selected
|
// Fill store's email address
|
||||||
await this.checkClientSetupCheckbox( false );
|
await this.fillEmailAddress(
|
||||||
|
storeDetails.storeEmail ||
|
||||||
|
config.get( 'addresses.admin.store.email' )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that checkbox next to "Get tips, product updates and inspiration straight to your mailbox" is selected
|
||||||
|
await this.checkMarketingCheckbox( true );
|
||||||
}
|
}
|
||||||
|
|
||||||
async fillAddress( address: string ) {
|
async fillAddress( address: string ) {
|
||||||
|
@ -95,11 +106,11 @@ export class StoreDetailsSection extends BasePage {
|
||||||
await clearAndFillInput( '#inspector-text-control-3', postalCode );
|
await clearAndFillInput( '#inspector-text-control-3', postalCode );
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectSetupForClient() {
|
async fillEmailAddress( email: string ) {
|
||||||
await setCheckbox( '.components-checkbox-control__input' );
|
await clearAndFillInput( '#inspector-text-control-4', email );
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkClientSetupCheckbox( selected: boolean ) {
|
async checkMarketingCheckbox( selected: boolean ) {
|
||||||
if ( selected ) {
|
if ( selected ) {
|
||||||
await verifyCheckboxIsSet( '.components-checkbox-control__input' );
|
await verifyCheckboxIsSet( '.components-checkbox-control__input' );
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -37,6 +37,7 @@ const testAdminOnboardingWizard = () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'can complete the store details section', async () => {
|
it( 'can complete the store details section', async () => {
|
||||||
|
await profileWizard.storeDetails.isDisplayed();
|
||||||
await profileWizard.storeDetails.completeStoreDetailsSection();
|
await profileWizard.storeDetails.completeStoreDetailsSection();
|
||||||
// Wait for "Continue" button to become active
|
// Wait for "Continue" button to become active
|
||||||
await profileWizard.continue();
|
await profileWizard.continue();
|
||||||
|
@ -81,7 +82,7 @@ const testAdminOnboardingWizard = () => {
|
||||||
await profileWizard.business.selectCurrentlySelling(
|
await profileWizard.business.selectCurrentlySelling(
|
||||||
config.get( 'onboardingwizard.sellingelsewhere' )
|
config.get( 'onboardingwizard.sellingelsewhere' )
|
||||||
);
|
);
|
||||||
|
await profileWizard.business.checkClientSetupCheckbox( false );
|
||||||
await profileWizard.continue();
|
await profileWizard.continue();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ const TYPES = {
|
||||||
SET_ERROR: 'SET_ERROR',
|
SET_ERROR: 'SET_ERROR',
|
||||||
SET_IS_REQUESTING: 'SET_IS_REQUESTING',
|
SET_IS_REQUESTING: 'SET_IS_REQUESTING',
|
||||||
SET_PROFILE_ITEMS: 'SET_PROFILE_ITEMS',
|
SET_PROFILE_ITEMS: 'SET_PROFILE_ITEMS',
|
||||||
|
SET_EMAIL_PREFILL: 'SET_EMAIL_PREFILL',
|
||||||
SET_TASKS_STATUS: 'SET_TASKS_STATUS',
|
SET_TASKS_STATUS: 'SET_TASKS_STATUS',
|
||||||
GET_PAYMENT_METHODS_SUCCESS: 'GET_PAYMENT_METHODS_SUCCESS',
|
GET_PAYMENT_METHODS_SUCCESS: 'GET_PAYMENT_METHODS_SUCCESS',
|
||||||
GET_FREE_EXTENSIONS_ERROR: 'GET_FREE_EXTENSIONS_ERROR',
|
GET_FREE_EXTENSIONS_ERROR: 'GET_FREE_EXTENSIONS_ERROR',
|
||||||
|
|
|
@ -163,8 +163,16 @@ export function setPaymentMethods( paymentMethods ) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setEmailPrefill( email ) {
|
||||||
|
return {
|
||||||
|
type: TYPES.SET_EMAIL_PREFILL,
|
||||||
|
emailPrefill: email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function* updateProfileItems( items ) {
|
export function* updateProfileItems( items ) {
|
||||||
yield setIsRequesting( 'updateProfileItems', true );
|
yield setIsRequesting( 'updateProfileItems', true );
|
||||||
|
yield setError( 'updateProfileItems', null );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = yield apiFetch( {
|
const results = yield apiFetch( {
|
||||||
|
@ -183,7 +191,7 @@ export function* updateProfileItems( items ) {
|
||||||
} catch ( error ) {
|
} catch ( error ) {
|
||||||
yield setError( 'updateProfileItems', error );
|
yield setError( 'updateProfileItems', error );
|
||||||
yield setIsRequesting( 'updateProfileItems', false );
|
yield setIsRequesting( 'updateProfileItems', false );
|
||||||
throw new Error();
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,10 @@ export const defaultState = {
|
||||||
skipped: null,
|
skipped: null,
|
||||||
theme: null,
|
theme: null,
|
||||||
wccom_connected: null,
|
wccom_connected: null,
|
||||||
|
is_agree_marketing: null,
|
||||||
|
store_email: null,
|
||||||
},
|
},
|
||||||
|
emailPrefill: '',
|
||||||
paymentMethods: [],
|
paymentMethods: [],
|
||||||
requesting: {},
|
requesting: {},
|
||||||
taskLists: [],
|
taskLists: [],
|
||||||
|
@ -50,6 +53,7 @@ const onboarding = (
|
||||||
freeExtensions,
|
freeExtensions,
|
||||||
type,
|
type,
|
||||||
profileItems,
|
profileItems,
|
||||||
|
emailPrefill,
|
||||||
paymentMethods,
|
paymentMethods,
|
||||||
replace,
|
replace,
|
||||||
error,
|
error,
|
||||||
|
@ -69,6 +73,11 @@ const onboarding = (
|
||||||
? profileItems
|
? profileItems
|
||||||
: { ...state.profileItems, ...profileItems },
|
: { ...state.profileItems, ...profileItems },
|
||||||
};
|
};
|
||||||
|
case TYPES.SET_EMAIL_PREFILL:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
emailPrefill,
|
||||||
|
};
|
||||||
case TYPES.SET_TASKS_STATUS:
|
case TYPES.SET_TASKS_STATUS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
setError,
|
setError,
|
||||||
setTasksStatus,
|
setTasksStatus,
|
||||||
setPaymentMethods,
|
setPaymentMethods,
|
||||||
|
setEmailPrefill,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
export function* getProfileItems() {
|
export function* getProfileItems() {
|
||||||
|
@ -31,6 +32,19 @@ export function* getProfileItems() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* getEmailPrefill() {
|
||||||
|
try {
|
||||||
|
const results = yield apiFetch( {
|
||||||
|
path: WC_ADMIN_NAMESPACE + '/onboarding/profile/get_email_prefill',
|
||||||
|
method: 'GET',
|
||||||
|
} );
|
||||||
|
|
||||||
|
yield setEmailPrefill( results.email );
|
||||||
|
} catch ( error ) {
|
||||||
|
yield setError( 'getEmailPrefill', error );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function* getTasksStatus() {
|
export function* getTasksStatus() {
|
||||||
try {
|
try {
|
||||||
const results = yield apiFetch( {
|
const results = yield apiFetch( {
|
||||||
|
|
|
@ -47,6 +47,10 @@ export const isOnboardingRequesting = (
|
||||||
return state.requesting[ selector ] || false;
|
return state.requesting[ selector ] || false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEmailPrefill = ( state: OnboardingState ): string => {
|
||||||
|
return state.emailPrefill || '';
|
||||||
|
};
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type OnboardingSelectors = {
|
export type OnboardingSelectors = {
|
||||||
getProfileItems: () => ReturnType< typeof getProfileItems >;
|
getProfileItems: () => ReturnType< typeof getProfileItems >;
|
||||||
|
@ -64,6 +68,7 @@ export type OnboardingState = {
|
||||||
taskLists: TaskList[];
|
taskLists: TaskList[];
|
||||||
tasksStatus: TasksStatusState;
|
tasksStatus: TasksStatusState;
|
||||||
paymentMethods: PaymentMethodsState[];
|
paymentMethods: PaymentMethodsState[];
|
||||||
|
emailPrefill: string;
|
||||||
// TODO clarify what the error record's type is
|
// TODO clarify what the error record's type is
|
||||||
errors: Record< string, unknown >;
|
errors: Record< string, unknown >;
|
||||||
requesting: Record< string, boolean >;
|
requesting: Record< string, boolean >;
|
||||||
|
@ -135,6 +140,8 @@ export type ProfileItemsState = {
|
||||||
skipped: boolean | null;
|
skipped: boolean | null;
|
||||||
theme: string | null;
|
theme: string | null;
|
||||||
wccom_connected: boolean | null;
|
wccom_connected: boolean | null;
|
||||||
|
is_agree_marketing: boolean | null;
|
||||||
|
store_email: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldLocale = {
|
export type FieldLocale = {
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\Admin\API;
|
||||||
defined( 'ABSPATH' ) || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
use Automattic\WooCommerce\Admin\Features\Onboarding;
|
use Automattic\WooCommerce\Admin\Features\Onboarding;
|
||||||
|
use \Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Onboarding Profile controller.
|
* Onboarding Profile controller.
|
||||||
|
@ -60,6 +61,18 @@ class OnboardingProfile extends \WC_REST_Data_Controller {
|
||||||
'schema' => array( $this, 'get_public_item_schema' ),
|
'schema' => array( $this, 'get_public_item_schema' ),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/get_email_prefill',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_email_prefill' ),
|
||||||
|
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||||
|
),
|
||||||
|
'schema' => array( $this, 'get_public_item_schema' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,7 +152,9 @@ class OnboardingProfile extends \WC_REST_Data_Controller {
|
||||||
$params = $request->get_json_params();
|
$params = $request->get_json_params();
|
||||||
$query_args = $this->prepare_objects_query( $params );
|
$query_args = $this->prepare_objects_query( $params );
|
||||||
$onboarding_data = (array) get_option( Onboarding::PROFILE_DATA_OPTION, array() );
|
$onboarding_data = (array) get_option( Onboarding::PROFILE_DATA_OPTION, array() );
|
||||||
update_option( Onboarding::PROFILE_DATA_OPTION, array_merge( $onboarding_data, $query_args ) );
|
$profile_data = array_merge( $onboarding_data, $query_args );
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
do_action( 'woocommerce_onboarding_profile_data_updated', $onboarding_data, $query_args );
|
||||||
|
|
||||||
$result = array(
|
$result = array(
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
|
@ -152,6 +167,36 @@ class OnboardingProfile extends \WC_REST_Data_Controller {
|
||||||
return rest_ensure_response( $data );
|
return rest_ensure_response( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a default email to be pre-filled in OBW. Prioritizes Jetpack if connected,
|
||||||
|
* otherwise will default to WordPress general settings.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request data.
|
||||||
|
* @return WP_Error|WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_email_prefill( $request ) {
|
||||||
|
$result = array(
|
||||||
|
'email' => '',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attempt to get email from Jetpack.
|
||||||
|
if ( class_exists( Jetpack_Connection_Manager::class ) ) {
|
||||||
|
$jetpack_connection_manager = new Jetpack_Connection_Manager();
|
||||||
|
if ( $jetpack_connection_manager->is_active() ) {
|
||||||
|
$jetpack_user = $jetpack_connection_manager->get_connected_user_data();
|
||||||
|
|
||||||
|
$result['email'] = $jetpack_user['email'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get email from WordPress general settings.
|
||||||
|
if ( empty( $result['email'] ) ) {
|
||||||
|
$result['email'] = get_option( 'admin_email' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_ensure_response( $result );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare objects query.
|
* Prepare objects query.
|
||||||
*
|
*
|
||||||
|
@ -360,11 +405,43 @@ class OnboardingProfile extends \WC_REST_Data_Controller {
|
||||||
'readonly' => true,
|
'readonly' => true,
|
||||||
'validate_callback' => 'rest_validate_request_arg',
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
),
|
),
|
||||||
|
'is_agree_marketing' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => __( 'Whether or not this store agreed to receiving marketing contents from WooCommerce.com.', 'woocommerce-admin' ),
|
||||||
|
'context' => array( 'view' ),
|
||||||
|
'readonly' => true,
|
||||||
|
'validate_callback' => 'rest_validate_request_arg',
|
||||||
|
),
|
||||||
|
'store_email' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __( 'Store email address.', 'woocommerce-admin' ),
|
||||||
|
'context' => array( 'view' ),
|
||||||
|
'readonly' => true,
|
||||||
|
'validate_callback' => array( __CLASS__, 'rest_validate_marketing_email' ),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return apply_filters( 'woocommerce_rest_onboarding_profile_properties', $properties );
|
return apply_filters( 'woocommerce_rest_onboarding_profile_properties', $properties );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally validates email if user agreed to marketing or if email is not empty.
|
||||||
|
*
|
||||||
|
* @param mixed $value Email value.
|
||||||
|
* @param WP_REST_Request $request Request object.
|
||||||
|
* @param string $param Parameter name.
|
||||||
|
* @return true|WP_Error
|
||||||
|
*/
|
||||||
|
public static function rest_validate_marketing_email( $value, $request, $param ) {
|
||||||
|
$is_agree_marketing = $request->get_param( 'is_agree_marketing' );
|
||||||
|
if (
|
||||||
|
( $is_agree_marketing || ! empty( $value ) ) &&
|
||||||
|
! is_email( $value ) ) {
|
||||||
|
return new \WP_Error( 'rest_invalid_email', __( 'Invalid email address', 'woocommerce-admin' ) );
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the schema, conforming to JSON Schema.
|
* Get the schema, conforming to JSON Schema.
|
||||||
*
|
*
|
||||||
|
|
|
@ -49,6 +49,7 @@ use \Automattic\WooCommerce\Admin\Notes\AddFirstProduct;
|
||||||
use \Automattic\WooCommerce\Admin\Notes\DrawAttention;
|
use \Automattic\WooCommerce\Admin\Notes\DrawAttention;
|
||||||
use \Automattic\WooCommerce\Admin\Notes\GettingStartedInEcommerceWebinar;
|
use \Automattic\WooCommerce\Admin\Notes\GettingStartedInEcommerceWebinar;
|
||||||
use \Automattic\WooCommerce\Admin\Notes\NavigationNudge;
|
use \Automattic\WooCommerce\Admin\Notes\NavigationNudge;
|
||||||
|
use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events Class.
|
* Events Class.
|
||||||
|
@ -103,6 +104,10 @@ class Events {
|
||||||
if ( $this->is_merchant_email_notifications_enabled() ) {
|
if ( $this->is_merchant_email_notifications_enabled() ) {
|
||||||
MerchantEmailNotifications::run();
|
MerchantEmailNotifications::run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( Features::is_enabled( 'onboarding' ) ) {
|
||||||
|
( new MailchimpScheduler() )->run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,7 +117,6 @@ class Events {
|
||||||
NewSalesRecord::possibly_add_note();
|
NewSalesRecord::possibly_add_note();
|
||||||
MobileApp::possibly_add_note();
|
MobileApp::possibly_add_note();
|
||||||
TrackingOptIn::possibly_add_note();
|
TrackingOptIn::possibly_add_note();
|
||||||
OnboardingEmailMarketing::possibly_add_note();
|
|
||||||
OnboardingPayments::possibly_add_note();
|
OnboardingPayments::possibly_add_note();
|
||||||
PersonalizeStore::possibly_add_note();
|
PersonalizeStore::possibly_add_note();
|
||||||
WooCommercePayments::possibly_add_note();
|
WooCommercePayments::possibly_add_note();
|
||||||
|
|
|
@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Admin\Features;
|
||||||
use \Automattic\WooCommerce\Admin\Loader;
|
use \Automattic\WooCommerce\Admin\Loader;
|
||||||
use Automattic\WooCommerce\Admin\PageController;
|
use Automattic\WooCommerce\Admin\PageController;
|
||||||
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||||
|
use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains backend logic for the onboarding profile and checklist feature.
|
* Contains backend logic for the onboarding profile and checklist feature.
|
||||||
|
@ -97,6 +98,8 @@ class Onboarding {
|
||||||
// Always hook into Jetpack connection even if outside of admin.
|
// Always hook into Jetpack connection even if outside of admin.
|
||||||
add_action( 'jetpack_site_registered', array( $this, 'set_woocommerce_setup_jetpack_opted_in' ) );
|
add_action( 'jetpack_site_registered', array( $this, 'set_woocommerce_setup_jetpack_opted_in' ) );
|
||||||
|
|
||||||
|
add_action( 'woocommerce_onboarding_profile_data_updated', array( $this, 'on_profile_data_updated' ), 10, 2 );
|
||||||
|
|
||||||
if ( ! is_admin() ) {
|
if ( ! is_admin() ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -977,4 +980,20 @@ class Onboarding {
|
||||||
}
|
}
|
||||||
return $plugins;
|
return $plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete MailchimpScheduler::SUBSCRIBED_OPTION_NAME option if profile data is being updated with a new email.
|
||||||
|
*
|
||||||
|
* @param array $existing_data Existing option data.
|
||||||
|
* @param array $updating_data Updating option data.
|
||||||
|
*/
|
||||||
|
public function on_profile_data_updated( $existing_data, $updating_data ) {
|
||||||
|
if (
|
||||||
|
isset( $existing_data['store_email'] ) &&
|
||||||
|
isset( $updating_data['store_email'] ) &&
|
||||||
|
$existing_data['store_email'] !== $updating_data['store_email']
|
||||||
|
) {
|
||||||
|
delete_option( MailchimpScheduler::SUBSCRIBED_OPTION_NAME );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* WooCommerce Admin Onboarding Email Marketing Note Provider.
|
|
||||||
*
|
|
||||||
* Adds a note to sign up to email marketing after completing the profiler.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\Admin\Notes;
|
|
||||||
|
|
||||||
defined( 'ABSPATH' ) || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Onboarding_Email_Marketing
|
|
||||||
*/
|
|
||||||
class OnboardingEmailMarketing {
|
|
||||||
/**
|
|
||||||
* Note traits.
|
|
||||||
*/
|
|
||||||
use NoteTraits;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the note for use in the database.
|
|
||||||
*/
|
|
||||||
const NOTE_NAME = 'wc-admin-onboarding-email-marketing';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the note.
|
|
||||||
*
|
|
||||||
* @return Note
|
|
||||||
*/
|
|
||||||
public static function get_note() {
|
|
||||||
$content = __( 'We\'re here for you - get tips, product updates and inspiration straight to your email box', 'woocommerce-admin' );
|
|
||||||
|
|
||||||
$note = new Note();
|
|
||||||
$note->set_title( __( 'Sign up for tips, product updates, and inspiration', 'woocommerce-admin' ) );
|
|
||||||
$note->set_content( $content );
|
|
||||||
$note->set_content_data( (object) array() );
|
|
||||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
|
||||||
$note->set_name( self::NOTE_NAME );
|
|
||||||
$note->set_source( 'woocommerce-admin' );
|
|
||||||
$note->add_action( 'yes-please', __( 'Yes please!', 'woocommerce-admin' ), 'https://woocommerce.us8.list-manage.com/subscribe/post?u=2c1434dc56f9506bf3c3ecd21&id=13860df971&SIGNUPPAGE=plugin' );
|
|
||||||
return $note;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MailchimpScheduler
|
||||||
|
*
|
||||||
|
* @package Automattic\WooCommerce\Admin\Schedulers
|
||||||
|
*/
|
||||||
|
class MailchimpScheduler {
|
||||||
|
|
||||||
|
const SUBSCRIBE_ENDPOINT = 'https://woocommerce.com/wp-json/wccom/v1/subscribe';
|
||||||
|
const SUBSCRIBE_ENDPOINT_DEV = 'http://woocommerce.test/wp-json/wccom/v1/subscribe';
|
||||||
|
|
||||||
|
const SUBSCRIBED_OPTION_NAME = 'woocommerce_onboarding_subscribed_to_mailchimp';
|
||||||
|
|
||||||
|
const LOGGER_CONTEXT = 'mailchimp_scheduler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logger instance.
|
||||||
|
*
|
||||||
|
* @var \WC_Logger_Interface|null
|
||||||
|
*/
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MailchimpScheduler constructor.
|
||||||
|
*
|
||||||
|
* @param \WC_Logger_Interface|null $logger Logger instance.
|
||||||
|
*/
|
||||||
|
public function __construct( \WC_Logger_Interface $logger = null ) {
|
||||||
|
if ( null === $logger ) {
|
||||||
|
$logger = wc_get_logger();
|
||||||
|
}
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to subscribe store_email to MailChimp.
|
||||||
|
*/
|
||||||
|
public function run() {
|
||||||
|
// Abort if we've already subscribed to MailChimp.
|
||||||
|
if ( 'yes' === get_option( self::SUBSCRIBED_OPTION_NAME ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile_data = get_option( 'woocommerce_onboarding_profile' );
|
||||||
|
|
||||||
|
if ( ! isset( $profile_data['is_agree_marketing'] ) || false === $profile_data['is_agree_marketing'] ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort if store_email doesn't exist.
|
||||||
|
if ( ! isset( $profile_data['store_email'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->make_request( $profile_data['store_email'] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
|
||||||
|
$this->logger->error(
|
||||||
|
'Error getting a response from Mailchimp API.',
|
||||||
|
array( 'source' => self::LOGGER_CONTEXT )
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
$body = json_decode( $response['body'] );
|
||||||
|
if ( isset( $body->success ) && true === $body->success ) {
|
||||||
|
update_option( self::SUBSCRIBED_OPTION_NAME, 'yes' );
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$this->logger->error(
|
||||||
|
// phpcs:ignore
|
||||||
|
'Incorrect response from Mailchimp API with: ' . print_r( $body, true ),
|
||||||
|
array( 'source' => self::LOGGER_CONTEXT )
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an HTTP request to the API.
|
||||||
|
*
|
||||||
|
* @param string $store_email Email address to subscribe.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function make_request( $store_email ) {
|
||||||
|
if ( 'development' === constant( 'WP_ENVIRONMENT_TYPE' ) ) {
|
||||||
|
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT_DEV;
|
||||||
|
} else {
|
||||||
|
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wp_remote_post(
|
||||||
|
$subscribe_endpoint,
|
||||||
|
array(
|
||||||
|
'method' => 'POST',
|
||||||
|
'body' => array(
|
||||||
|
'email' => $store_email,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use \Automattic\WooCommerce\Admin\API\OnboardingProfile;
|
use \Automattic\WooCommerce\Admin\API\OnboardingProfile;
|
||||||
|
use Automattic\WooCommerce\Admin\Features\Onboarding;
|
||||||
|
use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WC Tests API Onboarding Profile
|
* WC Tests API Onboarding Profile
|
||||||
|
@ -104,7 +106,7 @@ class WC_Tests_API_Onboarding_Profiles extends WC_REST_Unit_Test_Case {
|
||||||
$data = $response->get_data();
|
$data = $response->get_data();
|
||||||
$properties = $data['schema']['properties'];
|
$properties = $data['schema']['properties'];
|
||||||
|
|
||||||
$this->assertCount( 13, $properties );
|
$this->assertCount( 15, $properties );
|
||||||
$this->assertArrayHasKey( 'completed', $properties );
|
$this->assertArrayHasKey( 'completed', $properties );
|
||||||
$this->assertArrayHasKey( 'skipped', $properties );
|
$this->assertArrayHasKey( 'skipped', $properties );
|
||||||
$this->assertArrayHasKey( 'industry', $properties );
|
$this->assertArrayHasKey( 'industry', $properties );
|
||||||
|
@ -118,6 +120,8 @@ class WC_Tests_API_Onboarding_Profiles extends WC_REST_Unit_Test_Case {
|
||||||
$this->assertArrayHasKey( 'theme', $properties );
|
$this->assertArrayHasKey( 'theme', $properties );
|
||||||
$this->assertArrayHasKey( 'wccom_connected', $properties );
|
$this->assertArrayHasKey( 'wccom_connected', $properties );
|
||||||
$this->assertArrayHasKey( 'setup_client', $properties );
|
$this->assertArrayHasKey( 'setup_client', $properties );
|
||||||
|
$this->assertArrayHasKey( 'is_agree_marketing', $properties );
|
||||||
|
$this->assertArrayHasKey( 'store_email', $properties );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,4 +183,24 @@ class WC_Tests_API_Onboarding_Profiles extends WC_REST_Unit_Test_Case {
|
||||||
$this->assertTrue( is_array( $response->get_data() ) );
|
$this->assertTrue( is_array( $response->get_data() ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a profile data update API request
|
||||||
|
* When the payload has a different store_email value that the existing store_email
|
||||||
|
* Then self::SUBSCRIBED_OPTION_NAME option should be deleted.
|
||||||
|
*/
|
||||||
|
public function test_it_deletes_the_option_when_a_different_email_gets_updated() {
|
||||||
|
wp_set_current_user( $this->user );
|
||||||
|
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, array( 'store_email' => 'first@test.com' ) );
|
||||||
|
update_option( MailchimpScheduler::SUBSCRIBED_OPTION_NAME, 'yes' );
|
||||||
|
|
||||||
|
$request = new WP_REST_Request( 'POST', '/wc-admin/onboarding/profile' );
|
||||||
|
$request->set_headers( array( 'content-type' => 'application/json' ) );
|
||||||
|
$request->set_body( wp_json_encode( array( 'store_email' => 'second@test.com' ) ) );
|
||||||
|
|
||||||
|
$this->server->dispatch( $request );
|
||||||
|
|
||||||
|
$this->assertFalse( get_option( MailchimpScheduler::SUBSCRIBED_OPTION_NAME, false ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,8 @@
|
||||||
"countryandstate": "United States (US) -- California",
|
"countryandstate": "United States (US) -- California",
|
||||||
"city": "San Francisco",
|
"city": "San Francisco",
|
||||||
"state": "CA",
|
"state": "CA",
|
||||||
"postcode": "94107"
|
"postcode": "94107",
|
||||||
|
"email": "john.doe@example.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"customer": {
|
"customer": {
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MailchimpScheduler tests
|
||||||
|
*
|
||||||
|
* @package Automattic\WooCommerce\Admin\Schedulers
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Admin\Features\Onboarding;
|
||||||
|
use Automattic\WooCommerce\Admin\Schedulers\MailchimpScheduler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class WC_Tests_Mailchimp_Scheduler
|
||||||
|
*/
|
||||||
|
class WC_Tests_Mailchimp_Scheduler extends WC_Unit_Test_Case {
|
||||||
|
/**
|
||||||
|
* @var MailchimpScheduler MailchimpScheduler instance to test
|
||||||
|
*/
|
||||||
|
private $instance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \PHPUnit\Framework\MockObject\MockObject WC_Logger_Interface mock
|
||||||
|
*/
|
||||||
|
private $logger_mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden setUp() method.
|
||||||
|
*/
|
||||||
|
public function setUp() {
|
||||||
|
$this->logger_mock = $this->getMockBuilder( \WC_Logger_Interface::class )->getMock();
|
||||||
|
|
||||||
|
$this->instance = $this->getMockBuilder( MailchimpScheduler::class )
|
||||||
|
->setConstructorArgs( array( $this->logger_mock ) )
|
||||||
|
->setMethods( array( 'make_request' ) )
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
delete_option( MailchimpScheduler::SUBSCRIBED_OPTION_NAME );
|
||||||
|
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When woocommerce_onboarding_subscribed_to_mailchimp value us 'yes'.
|
||||||
|
* Then it should abort.
|
||||||
|
*/
|
||||||
|
public function test_it_aborts_if_already_subscribed() {
|
||||||
|
// When.
|
||||||
|
update_option( MailchimpScheduler::SUBSCRIBED_OPTION_NAME, 'yes' );
|
||||||
|
|
||||||
|
// Then.
|
||||||
|
$this->assertFalse( $this->instance->run() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When is_agree_marketing is missing or value is false.
|
||||||
|
* Then it should abort.
|
||||||
|
*/
|
||||||
|
public function test_it_aborts_if_is_agree_marketing_is_false_or_missing() {
|
||||||
|
// When.
|
||||||
|
$profile_data = array( 'store_email' => 'test@test.com' );
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
$this->assertFalse( $this->instance->run() );
|
||||||
|
|
||||||
|
$profile_data = array(
|
||||||
|
'is_agree_marketing' => false,
|
||||||
|
'store_email' => 'test@test.com',
|
||||||
|
);
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
|
||||||
|
// Then.
|
||||||
|
$this->assertFalse( $this->instance->run() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When store_email is missing.
|
||||||
|
* Then it should abort.
|
||||||
|
*/
|
||||||
|
public function test_it_aborts_if_store_email_is_missing() {
|
||||||
|
// When.
|
||||||
|
$profile_data = array( 'is_agree_marketing' => true );
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
|
||||||
|
// Then.
|
||||||
|
$this->assertFalse( $this->instance->run() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given is_agree_marketing and store_email values are set.
|
||||||
|
* When the request to the API returns WP_Error.
|
||||||
|
* Then it should log an error message.
|
||||||
|
*/
|
||||||
|
public function test_it_logs_error_when_wp_error_is_returned() {
|
||||||
|
// Given.
|
||||||
|
$profile_data = array(
|
||||||
|
'is_agree_marketing' => true,
|
||||||
|
'store_email' => 'test@test.com',
|
||||||
|
);
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
|
||||||
|
// When.
|
||||||
|
$wp_error = new WP_Error();
|
||||||
|
$this->instance->method( 'make_request' )->willReturn( $wp_error );
|
||||||
|
|
||||||
|
// Then.
|
||||||
|
$this->logger_mock->expects( $this->exactly( 2 ) )
|
||||||
|
->method( 'error' )
|
||||||
|
->with( 'Error getting a response from Mailchimp API.', array( 'source' => MailchimpScheduler::LOGGER_CONTEXT ) );
|
||||||
|
|
||||||
|
$this->instance->run();
|
||||||
|
|
||||||
|
// Check for the missing 'body'.
|
||||||
|
$this->instance->method( 'make_request' )->willReturn( array() );
|
||||||
|
$this->instance->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given is_agree_marketing and store_email values are set.
|
||||||
|
* When the request to the API returns body without 'success'.
|
||||||
|
* Then it should log an error message.
|
||||||
|
*/
|
||||||
|
public function test_it_logs_error_when_response_data_is_incorrect() {
|
||||||
|
// Given.
|
||||||
|
$profile_data = array(
|
||||||
|
'is_agree_marketing' => true,
|
||||||
|
'store_email' => 'test@test.com',
|
||||||
|
);
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
|
||||||
|
// When.
|
||||||
|
$body = wp_json_encode( array() );
|
||||||
|
$this->instance->method( 'make_request' )->willReturn( array( 'body' => $body ) );
|
||||||
|
|
||||||
|
// Then.
|
||||||
|
$this->logger_mock->expects( $this->once() )
|
||||||
|
->method( 'error' )
|
||||||
|
->with(
|
||||||
|
// phpcs:ignore
|
||||||
|
'Incorrect response from Mailchimp API with: ' . print_r( array(), true ),
|
||||||
|
array( 'source' => MailchimpScheduler::LOGGER_CONTEXT )
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->instance->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given is_agree_marketing and store_email values are set
|
||||||
|
* When the request to the API returns success: true
|
||||||
|
* Then woocommerce_onboarding_subscribed_to_mailchimp should be updated to 'yes'
|
||||||
|
*/
|
||||||
|
public function test_it_updates_option_value_when_everything_went_well() {
|
||||||
|
// Given.
|
||||||
|
$profile_data = array(
|
||||||
|
'is_agree_marketing' => true,
|
||||||
|
'store_email' => 'test@test.com',
|
||||||
|
);
|
||||||
|
update_option( Onboarding::PROFILE_DATA_OPTION, $profile_data );
|
||||||
|
|
||||||
|
// When.
|
||||||
|
$body = wp_json_encode( array( 'success' => true ) );
|
||||||
|
$this->instance->method( 'make_request' )->willReturn( array( 'body' => $body ) );
|
||||||
|
|
||||||
|
$this->instance->run();
|
||||||
|
|
||||||
|
// Then.
|
||||||
|
$this->assertEquals( 'yes', get_option( 'woocommerce_onboarding_subscribed_to_mailchimp' ) );
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue