add: core profiler email marketing opt in (#40869)

* add: core profiler email marketing opt in

* tests

* changed mailchimp feature flag

* fix: made experiment name static

* lint
This commit is contained in:
RJ 2023-10-24 23:07:26 +08:00 committed by GitHub
parent eafc87b453
commit e01e6f8b2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 377 additions and 11 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Export WCUser type for consumption in wcadmin

View File

@ -108,6 +108,7 @@ export {
} from './product-categories/types';
export { TaxClass } from './tax-classes/types';
export { ProductTag, Query } from './product-tags/types';
export { WCUser } from './user/types';
/**
* Internal dependencies

View File

@ -76,12 +76,58 @@ const recordTracksSkipBusinessLocationCompleted = () => {
} );
};
// Temporarily expand the step viewed track for BusinessInfo so that we can include the experiment assignment
// Remove this and change the action back to recordTracksStepViewed when the experiment is over
const recordTracksStepViewedBusinessInfo = (
context: CoreProfilerStateMachineContext,
_event: unknown,
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( 'coreprofiler_step_view', {
step,
email_marketing_experiment_assignment:
context.emailMarketingExperimentAssignment,
wc_version: getSetting( 'wcVersion' ),
} );
};
const recordTracksIsEmailChanged = (
context: CoreProfilerStateMachineContext,
event: BusinessInfoEvent
) => {
if ( context.emailMarketingExperimentAssignment === 'treatment' ) {
let emailSource, isEmailChanged;
if ( context.onboardingProfile.store_email ) {
emailSource = 'onboarding_profile_store_email'; // from previous entry
isEmailChanged =
event.payload.storeEmailAddress !==
context.onboardingProfile.store_email;
} else if ( context.currentUserEmail ) {
emailSource = 'current_user_email'; // from currentUser
isEmailChanged =
event.payload.storeEmailAddress !== context.currentUserEmail;
} else {
emailSource = 'was_empty';
isEmailChanged = event.payload.storeEmailAddress?.length > 0;
}
recordEvent( 'coreprofiler_email_marketing', {
opt_in: event.payload.isOptInMarketing,
email_field_prefilled_source: emailSource,
email_field_modified: isEmailChanged,
} );
}
};
const recordTracksBusinessInfoCompleted = (
_context: CoreProfilerStateMachineContext,
context: CoreProfilerStateMachineContext,
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >
) => {
recordEvent( 'coreprofiler_step_complete', {
step: 'business_info',
email_marketing_experiment_assignment:
context.emailMarketingExperimentAssignment,
wc_version: getSetting( 'wcVersion' ),
} );
@ -92,8 +138,8 @@ const recordTracksBusinessInfoCompleted = (
) === -1,
industry: event.payload.industry,
store_location_previously_set:
_context.onboardingProfile.is_store_country_set || false,
geolocation_success: _context.geolocatedLocation !== undefined,
context.onboardingProfile.is_store_country_set || false,
geolocation_success: context.geolocatedLocation !== undefined,
geolocation_overruled: event.payload.geolocationOverruled,
} );
};
@ -180,4 +226,6 @@ export default {
recordFailedPluginInstallations,
recordSuccessfulPluginInstallation,
recordTracksPluginsInstallationRequest,
recordTracksIsEmailChanged,
recordTracksStepViewedBusinessInfo,
};

View File

@ -31,8 +31,13 @@ import {
GeolocationResponse,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME,
USER_STORE_NAME,
WCUser,
} from '@woocommerce/data';
import { initializeExPlat } from '@woocommerce/explat';
import {
initializeExPlat,
loadExperimentAssignment,
} from '@woocommerce/explat';
import { CountryStateOption } from '@woocommerce/onboarding';
import { getAdminLink } from '@woocommerce/settings';
import CurrencyFactory from '@woocommerce/currency';
@ -99,6 +104,8 @@ export type BusinessInfoEvent = {
industry?: IndustryChoice;
storeLocation: CountryStateOption[ 'key' ];
geolocationOverruled: boolean;
isOptInMarketing: boolean;
storeEmailAddress: string;
};
};
@ -139,6 +146,8 @@ export type OnboardingProfile = {
selling_platforms: SellingPlatform[] | null;
skip?: boolean;
is_store_country_set: boolean | null;
store_email?: string;
is_agree_marketing?: boolean;
};
export type PluginsPageSkippedEvent = {
@ -195,6 +204,8 @@ export type CoreProfilerStateMachineContext = {
persistBusinessInfoRef?: ReturnType< typeof spawn >;
spawnUpdateOnboardingProfileOptionRef?: ReturnType< typeof spawn >;
spawnGeolocationRef?: ReturnType< typeof spawn >;
emailMarketingExperimentAssignment: 'treatment' | 'control';
currentUserEmail: string | undefined;
};
const getAllowTrackingOption = async () =>
@ -309,6 +320,35 @@ const handleOnboardingProfileOption = assign( {
},
} );
const getMarketingOptInExperimentAssignment = async () => {
return loadExperimentAssignment(
`woocommerce_core_profiler_email_marketing_opt_in_2023_Q4_V1`
);
};
const getCurrentUserEmail = async () => {
const currentUser: WCUser< 'email' > = await resolveSelect(
USER_STORE_NAME
).getCurrentUser();
return currentUser?.email;
};
const assignCurrentUserEmail = assign( {
currentUserEmail: (
_context,
event: DoneInvokeEvent< string | undefined >
) => {
if (
event.data &&
event.data.length > 0 &&
event.data !== 'wordpress@example.com' // wordpress default prefilled email address
) {
return event.data;
}
return undefined;
},
} );
const assignOnboardingProfile = assign( {
onboardingProfile: (
_context,
@ -316,6 +356,17 @@ const assignOnboardingProfile = assign( {
) => event.data,
} );
const assignMarketingOptInExperimentAssignment = assign( {
emailMarketingExperimentAssignment: (
_context,
event: DoneInvokeEvent<
Awaited<
ReturnType< typeof getMarketingOptInExperimentAssignment >
>
>
) => event.data.variationName ?? 'control',
} );
const getGeolocation = async ( context: CoreProfilerStateMachineContext ) => {
if ( context.optInDataSharing ) {
return resolveSelect( COUNTRIES_STORE_NAME ).geolocate();
@ -499,6 +550,11 @@ const updateBusinessInfo = async (
...refreshedOnboardingProfile,
is_store_country_set: true,
industry: [ event.payload.industry ],
is_agree_marketing: event.payload.isOptInMarketing,
store_email:
event.payload.storeEmailAddress.length > 0
? event.payload.storeEmailAddress
: null,
},
} );
};
@ -644,6 +700,8 @@ const coreProfilerMachineActions = {
handleCountries,
handleOnboardingProfileOption,
assignOnboardingProfile,
assignMarketingOptInExperimentAssignment,
assignCurrentUserEmail,
persistBusinessInfo,
spawnUpdateOnboardingProfileOption,
redirectToWooHome,
@ -657,6 +715,8 @@ const coreProfilerMachineServices = {
getCountries,
getGeolocation,
getOnboardingProfileOption,
getMarketingOptInExperimentAssignment,
getCurrentUserEmail,
getPlugins,
browserPopstateHandler,
updateBusinessInfo,
@ -693,6 +753,8 @@ export const coreProfilerStateMachineDefinition = createMachine( {
loader: {},
onboardingProfile: {} as OnboardingProfile,
jetpackAuthUrl: undefined,
emailMarketingExperimentAssignment: 'control',
currentUserEmail: undefined,
} as CoreProfilerStateMachineContext,
states: {
navigate: {
@ -1026,6 +1088,45 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
},
marketingOptInExperiment: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'getMarketingOptInExperimentAssignment',
onDone: {
target: 'done',
actions: [
'assignMarketingOptInExperimentAssignment',
],
},
},
},
done: { type: 'final' },
},
},
currentUserEmail: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'getCurrentUserEmail',
onDone: {
target: 'done',
actions: [
'assignCurrentUserEmail',
],
},
onError: {
target: 'done',
},
},
},
done: {
type: 'final',
},
},
},
},
// onDone is reached when child parallel states fo fetching are resolved (reached final states)
onDone: {
@ -1039,14 +1140,17 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
entry: [
{
type: 'recordTracksStepViewed',
type: 'recordTracksStepViewedBusinessInfo',
step: 'business_info',
},
],
on: {
BUSINESS_INFO_COMPLETED: {
target: 'postBusinessInfo',
actions: [ 'recordTracksBusinessInfoCompleted' ],
actions: [
'recordTracksBusinessInfoCompleted',
'recordTracksIsEmailChanged',
],
},
},
},

View File

@ -2,7 +2,13 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, TextControl, Notice, Spinner } from '@wordpress/components';
import {
Button,
TextControl,
Notice,
Spinner,
CheckboxControl,
} from '@wordpress/components';
import { SelectControl } from '@woocommerce/components';
import { Icon, chevronDown } from '@wordpress/icons';
import {
@ -83,9 +89,18 @@ export type BusinessInfoContextProps = Pick<
> & {
onboardingProfile: Pick<
CoreProfilerStateMachineContext[ 'onboardingProfile' ],
'industry' | 'business_choice' | 'is_store_country_set'
| 'industry'
| 'business_choice'
| 'is_store_country_set'
| 'is_agree_marketing'
| 'store_email'
>;
} & Partial<
Pick<
CoreProfilerStateMachineContext,
'emailMarketingExperimentAssignment' | 'currentUserEmail'
>
>;
};
export const BusinessInfo = ( {
context,
@ -105,7 +120,11 @@ export const BusinessInfo = ( {
is_store_country_set: isStoreCountrySet,
industry: industryFromOnboardingProfile,
business_choice: businessChoiceFromOnboardingProfile,
is_agree_marketing: isOptInMarketingFromOnboardingProfile,
store_email: storeEmailAddressFromOnboardingProfile,
},
emailMarketingExperimentAssignment,
currentUserEmail,
} = context;
const [ storeName, setStoreName ] = useState(
@ -176,6 +195,14 @@ export const BusinessInfo = ( {
const [ hasSubmitted, setHasSubmitted ] = useState( false );
const [ storeEmailAddress, setEmailAddress ] = useState(
storeEmailAddressFromOnboardingProfile || currentUserEmail || ''
);
const [ isOptInMarketing, setIsOptInMarketing ] = useState< boolean >(
isOptInMarketingFromOnboardingProfile || false
);
return (
<div
className="woocommerce-profiler-business-information"
@ -345,12 +372,55 @@ export const BusinessInfo = ( {
</ul>
</Notice>
) }
{ emailMarketingExperimentAssignment === 'treatment' && (
<>
<TextControl
className="woocommerce-profiler-business-info-email-adddress"
onChange={ ( value ) => {
setEmailAddress( value );
} }
value={ decodeEntities( storeEmailAddress ) }
label={
<>
{ __(
'Your email address',
'woocommerce'
) }
{ isOptInMarketing && (
<span className="woocommerce-profiler-question-required">
{ '*' }
</span>
) }
</>
}
placeholder={ __(
'wordpress@example.com',
'woocommerce'
) }
/>
<CheckboxControl
className="core-profiler__checkbox"
label={ __(
'Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox.',
'woocommerce'
) }
checked={ isOptInMarketing }
onChange={ setIsOptInMarketing }
/>
</>
) }
</form>
<div className="woocommerce-profiler-button-container">
<Button
className="woocommerce-profiler-button"
variant="primary"
disabled={ ! storeCountry.key }
disabled={
! storeCountry.key ||
( emailMarketingExperimentAssignment ===
'treatment' &&
isOptInMarketing &&
storeEmailAddress.length === 0 )
}
onClick={ () => {
sendEvent( {
type: 'BUSINESS_INFO_COMPLETED',
@ -360,6 +430,8 @@ export const BusinessInfo = ( {
storeLocation: storeCountry.key,
geolocationOverruled:
geolocationOverruled || false,
isOptInMarketing,
storeEmailAddress,
},
} );
setHasSubmitted( true );

View File

@ -173,6 +173,8 @@ describe( 'BusinessInfo', () => {
industry: 'other',
storeLocation: 'AU:VIC',
storeName: '',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
@ -224,6 +226,8 @@ describe( 'BusinessInfo', () => {
industry: 'other',
storeLocation: 'AW',
storeName: '',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
@ -273,6 +277,8 @@ describe( 'BusinessInfo', () => {
industry: 'food_and_drink',
storeLocation: 'AU:VIC',
storeName: 'Test Store Name',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
@ -301,8 +307,106 @@ describe( 'BusinessInfo', () => {
industry: 'food_and_drink',
storeLocation: 'AU:VIC',
storeName: 'Test Store Name',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
} );
describe( 'business info page, email marketing variant', () => {
beforeEach( () => {
props.context.emailMarketingExperimentAssignment = 'treatment';
} );
it( 'should correctly render the experiment variant with the email field', () => {
render( <BusinessInfo { ...props } /> );
expect(
screen.getByText( /Your email address/i )
).toBeInTheDocument();
} );
it( 'should not disable the continue field when experiment variant is shown, opt in checkbox is not checked and email field is empty', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
expect( continueButton ).not.toBeDisabled();
} );
it( 'should disable the continue field when experiment variant is shown, opt in checkbox is checked and email field is empty', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const checkbox = screen.getByRole( 'checkbox', {
name: /Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox./i,
} );
userEvent.click( checkbox );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
expect( continueButton ).toBeDisabled();
} );
it( 'should correctly send event with opt-in true when experiment variant is shown, opt in checkbox is checked and email field is filled', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const checkbox = screen.getByRole( 'checkbox', {
name: /Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox./i,
} );
userEvent.click( checkbox );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
userEvent.type( emailInput, 'wordpress@automattic.com' );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
userEvent.click( continueButton );
expect( props.sendEvent ).toHaveBeenCalledWith( {
payload: {
geolocationOverruled: false,
industry: 'other',
storeLocation: 'AW',
storeName: '',
isOptInMarketing: true,
storeEmailAddress: 'wordpress@automattic.com',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
} );
it( 'should correctly prepopulate the email field if populated in the onboarding profile', () => {
props.context.onboardingProfile.store_email =
'wordpress@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'wordpress@automattic.com' );
} );
it( 'should correctly prepopulate the email field if populated in the current user', () => {
props.context.currentUserEmail = 'currentUser@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'currentUser@automattic.com' );
} );
it( 'should correctly favor the onboarding profile email over the current user email', () => {
props.context.currentUserEmail = 'currentUser@automattic.com';
props.context.onboardingProfile.store_email =
'wordpress@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'wordpress@automattic.com' );
} );
} );
} );

View File

@ -419,6 +419,8 @@
}
.woocommerce-profiler-question-label,
.woocommerce-profiler-business-info-email-adddress
.components-base-control__label,
.woocommerce-profiler-business-info-store-name
.components-base-control__label {
text-transform: uppercase;
@ -430,11 +432,15 @@
}
.woocommerce-profiler-question-label
.woocommerce-profiler-question-required,
.woocommerce-profiler-business-info-email-adddress
.woocommerce-profiler-question-required {
color: #cc1818;
padding-left: 3px;
}
.woocommerce-profiler-business-info-email-adddress
.components-text-control__input,
.woocommerce-profiler-business-info-store-name
.components-text-control__input {
height: 40px;
@ -448,6 +454,29 @@
}
}
.woocommerce-profiler-select-control__country-spacer + .woocommerce-profiler-business-info-email-adddress {
margin-top: 8px;
}
.woocommerce-profiler-business-info-email-adddress {
margin-top: 20px;
}
.core-profiler__checkbox {
margin-top: 4px;
.components-checkbox-control__input-container {
margin-right: 16px;
}
.components-checkbox-control__label {
color: $gray-700;
font-size: 12px;
line-height: 16px;
font-weight: 400;
}
}
.woocommerce-profiler-select-control__industry {
margin-bottom: 20px;
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Added A/B test setup for email marketing opt in for core profiler

View File

@ -156,7 +156,7 @@ class Events {
MerchantEmailNotifications::run();
}
if ( Features::is_enabled( 'onboarding' ) ) {
if ( Features::is_enabled( 'core-profiler' ) ) {
( new MailchimpScheduler() )->run();
}
}