add: core profiler business info page (#38412)
* add: core profiler business info page
This commit is contained in:
parent
953a8f5c30
commit
ab18828e84
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add ability to make geolocation call to WPCOM API
|
|
@ -3,6 +3,8 @@ export enum TYPES {
|
|||
GET_LOCALES_SUCCESS = 'GET_LOCALES_SUCCESS',
|
||||
GET_COUNTRIES_ERROR = 'GET_COUNTRIES_ERROR',
|
||||
GET_COUNTRIES_SUCCESS = 'GET_COUNTRIES_SUCCESS',
|
||||
GEOLOCATION_SUCCESS = 'GEOLOCATION_SUCCESS',
|
||||
GEOLOCATION_ERROR = 'GEOLOCATION_ERROR',
|
||||
}
|
||||
|
||||
export default TYPES;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Internal dependencies
|
||||
*/
|
||||
import TYPES from './action-types';
|
||||
import { Locales, Country } from './types';
|
||||
import { Locales, Country, GeolocationResponse } from './types';
|
||||
|
||||
export function getLocalesSuccess( locales: Locales ) {
|
||||
return {
|
||||
|
@ -32,9 +32,25 @@ export function getCountriesError( error: unknown ) {
|
|||
};
|
||||
}
|
||||
|
||||
export function geolocationSuccess( geolocation: GeolocationResponse ) {
|
||||
return {
|
||||
type: TYPES.GEOLOCATION_SUCCESS as const,
|
||||
geolocation,
|
||||
};
|
||||
}
|
||||
|
||||
export function geolocationError( error: unknown ) {
|
||||
return {
|
||||
type: TYPES.GEOLOCATION_ERROR as const,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export type Action = ReturnType<
|
||||
| typeof getLocalesSuccess
|
||||
| typeof getLocalesError
|
||||
| typeof getCountriesSuccess
|
||||
| typeof getCountriesError
|
||||
| typeof geolocationSuccess
|
||||
| typeof geolocationError
|
||||
>;
|
||||
|
|
|
@ -16,6 +16,7 @@ const reducer: Reducer< CountriesState, Action > = (
|
|||
errors: {},
|
||||
locales: {},
|
||||
countries: [],
|
||||
geolocation: undefined,
|
||||
},
|
||||
action
|
||||
) => {
|
||||
|
@ -50,6 +51,21 @@ const reducer: Reducer< CountriesState, Action > = (
|
|||
},
|
||||
};
|
||||
break;
|
||||
case TYPES.GEOLOCATION_SUCCESS:
|
||||
state = {
|
||||
...state,
|
||||
geolocation: action.geolocation,
|
||||
};
|
||||
break;
|
||||
case TYPES.GEOLOCATION_ERROR:
|
||||
state = {
|
||||
...state,
|
||||
errors: {
|
||||
...state.errors,
|
||||
geolocation: action.error,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
*/
|
||||
import { apiFetch, select } from '@wordpress/data-controls';
|
||||
import { controls } from '@wordpress/data';
|
||||
import { DispatchFromMap } from '@automattic/data-stores';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import * as actions from './actions';
|
||||
import {
|
||||
getLocalesSuccess,
|
||||
getLocalesError,
|
||||
|
@ -14,7 +16,7 @@ import {
|
|||
getCountriesError,
|
||||
} from './actions';
|
||||
import { NAMESPACE } from '../constants';
|
||||
import { Locales, Country } from './types';
|
||||
import { Locales, Country, GeolocationResponse } from './types';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
const resolveSelect =
|
||||
|
@ -55,3 +57,18 @@ export function* getCountries() {
|
|||
return getCountriesError( error );
|
||||
}
|
||||
}
|
||||
|
||||
export const geolocate =
|
||||
() =>
|
||||
async ( { dispatch }: { dispatch: DispatchFromMap< typeof actions > } ) => {
|
||||
try {
|
||||
const url = `https://public-api.wordpress.com/geo/?v=${ new Date().getTime() }`;
|
||||
const response = await fetch( url, {
|
||||
method: 'GET',
|
||||
} );
|
||||
const result: GeolocationResponse = await response.json();
|
||||
dispatch.geolocationSuccess( result );
|
||||
} catch ( error ) {
|
||||
dispatch.geolocationError( error );
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,3 +19,7 @@ export const getCountries = ( state: CountriesState ) => {
|
|||
export const getCountry = ( state: CountriesState, code: string ) => {
|
||||
return state.countries.find( ( country ) => country.code === code );
|
||||
};
|
||||
|
||||
export const geolocate = ( state: CountriesState ) => {
|
||||
return state.geolocation;
|
||||
};
|
||||
|
|
|
@ -40,4 +40,26 @@ export type CountriesState = {
|
|||
};
|
||||
locales: Locales;
|
||||
countries: Country[];
|
||||
geolocation: GeolocationResponse | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Geolocation response from the WPCOM API which geolocates using ip2location.
|
||||
* Example response:
|
||||
* {
|
||||
* "latitude":"-38.23476",
|
||||
* "longitude":"146.39499",
|
||||
* "country_short":"AU",
|
||||
* "country_long":"Australia",
|
||||
* "region":"Victoria",
|
||||
* "city":"Morwell"
|
||||
* }
|
||||
*/
|
||||
export type GeolocationResponse = {
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
country_short: string;
|
||||
country_long: string;
|
||||
region: string;
|
||||
city: string;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Added getCountry utility for splitting colon delimited country:state strings
|
|
@ -77,3 +77,12 @@ export const findCountryOption = (
|
|||
}
|
||||
return match;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns just the country portion of a country:state string that is delimited with a colon.
|
||||
*
|
||||
* @param countryPossiblyWithState Country string that may or may not have a state. e.g 'US:CA', 'UG:UG312'
|
||||
* @return Just the country portion of the string, or undefined if input is undefined. e.g 'US', 'UG'
|
||||
*/
|
||||
export const getCountry = ( countryPossiblyWithState: string ) =>
|
||||
countryPossiblyWithState?.split( ':' )[ 0 ] ?? undefined;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { findCountryOption } from '../../';
|
||||
import { findCountryOption, getCountry } from '../../';
|
||||
import { countryStateOptions } from './country-options';
|
||||
import { locations } from './locations';
|
||||
|
||||
|
@ -94,3 +94,18 @@ describe( 'findCountryOption', () => {
|
|||
expect( matchCount / locations.length ).toBeGreaterThan( 0.98 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getCountry', () => {
|
||||
it( 'should return null on undefined location', () => {
|
||||
// @ts-expect-error -- tests undefined
|
||||
expect( getCountry( undefined ) ).toEqual( undefined );
|
||||
} );
|
||||
|
||||
it( 'should return country when country string without state is passed', () => {
|
||||
expect( getCountry( 'SG' ) ).toEqual( 'SG' );
|
||||
} );
|
||||
|
||||
it( 'should return country when country string with state is passed', () => {
|
||||
expect( getCountry( 'AU:VIC' ) ).toEqual( 'AU' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
|
||||
@media (max-width: #{ ($break-mobile) }) {
|
||||
margin-top: 52px 0 40px;
|
||||
margin: 52px 0 40px;
|
||||
|
||||
.woocommerce-profiler-heading__title {
|
||||
font-size: 32px;
|
||||
|
|
|
@ -13,10 +13,12 @@ import {
|
|||
Country,
|
||||
ONBOARDING_STORE_NAME,
|
||||
Extension,
|
||||
GeolocationResponse,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { initializeExPlat } from '@woocommerce/explat';
|
||||
import { CountryStateOption } from '@woocommerce/onboarding';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -28,7 +30,11 @@ import {
|
|||
SellingOnlineAnswer,
|
||||
SellingPlatform,
|
||||
} from './pages/UserProfile';
|
||||
import { BusinessInfo } from './pages/BusinessInfo';
|
||||
import {
|
||||
BusinessInfo,
|
||||
IndustryChoice,
|
||||
POSSIBLY_DEFAULT_STORE_NAMES,
|
||||
} from './pages/BusinessInfo';
|
||||
import { BusinessLocation } from './pages/BusinessLocation';
|
||||
import { getCountryStateOptions } from './services/country';
|
||||
import { Loader } from './pages/Loader';
|
||||
|
@ -42,9 +48,6 @@ import {
|
|||
PluginInstallError,
|
||||
} from './services/installAndActivatePlugins';
|
||||
|
||||
// TODO: Typescript support can be improved, but for now lets write the types ourselves
|
||||
// https://stately.ai/blog/introducing-typescript-typegen-for-xstate
|
||||
|
||||
export type InitializationCompleteEvent = {
|
||||
type: 'INITIALIZATION_COMPLETE';
|
||||
payload: { optInDataSharing: boolean };
|
||||
|
@ -69,14 +72,17 @@ export type UserProfileEvent =
|
|||
export type BusinessInfoEvent = {
|
||||
type: 'BUSINESS_INFO_COMPLETED';
|
||||
payload: {
|
||||
businessInfo: CoreProfilerStateMachineContext[ 'businessInfo' ];
|
||||
storeName?: string;
|
||||
industry?: IndustryChoice;
|
||||
storeLocation: CountryStateOption[ 'key' ];
|
||||
geolocationOverruled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type BusinessLocationEvent = {
|
||||
type: 'BUSINESS_LOCATION_COMPLETED';
|
||||
payload: {
|
||||
businessInfo: CoreProfilerStateMachineContext[ 'businessInfo' ];
|
||||
storeLocation: CountryStateOption[ 'key' ];
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -90,9 +96,11 @@ export type PluginsInstallationRequestedEvent = {
|
|||
// TODO: add types as we develop the pages
|
||||
export type OnboardingProfile = {
|
||||
business_choice: BusinessChoice;
|
||||
industry: Array< IndustryChoice >;
|
||||
selling_online_answer: SellingOnlineAnswer | null;
|
||||
selling_platforms: SellingPlatform[] | null;
|
||||
skip?: boolean;
|
||||
is_store_country_set: boolean | null;
|
||||
};
|
||||
|
||||
export type PluginsPageSkippedEvent = {
|
||||
|
@ -128,20 +136,23 @@ export type CoreProfilerStateMachineContext = {
|
|||
sellingPlatforms?: SellingPlatform[] | null;
|
||||
skipped?: boolean;
|
||||
};
|
||||
geolocatedLocation: {
|
||||
location: string;
|
||||
};
|
||||
pluginsAvailable: ExtensionList[ 'plugins' ] | [];
|
||||
pluginsSelected: string[]; // extension slugs
|
||||
pluginsInstallationErrors: PluginInstallError[];
|
||||
businessInfo: { foo?: { bar: 'qux' }; location: string };
|
||||
countries: { [ key: string ]: string };
|
||||
geolocatedLocation: GeolocationResponse | undefined;
|
||||
businessInfo: {
|
||||
storeName?: string | undefined;
|
||||
industry?: string | undefined;
|
||||
location?: string;
|
||||
};
|
||||
countries: CountryStateOption[];
|
||||
loader: {
|
||||
progress?: number;
|
||||
className?: string;
|
||||
useStages?: string;
|
||||
stageIndex?: number;
|
||||
};
|
||||
onboardingProfile: OnboardingProfile;
|
||||
};
|
||||
|
||||
const getAllowTrackingOption = async () =>
|
||||
|
@ -156,6 +167,40 @@ const handleTrackingOption = assign( {
|
|||
) => event.data !== 'no',
|
||||
} );
|
||||
|
||||
const getStoreNameOption = async () =>
|
||||
resolveSelect( OPTIONS_STORE_NAME ).getOption( 'blogname' );
|
||||
|
||||
const handleStoreNameOption = assign( {
|
||||
businessInfo: (
|
||||
context: CoreProfilerStateMachineContext,
|
||||
event: DoneInvokeEvent< string | undefined >
|
||||
) => {
|
||||
return {
|
||||
...context.businessInfo,
|
||||
storeName: POSSIBLY_DEFAULT_STORE_NAMES.includes( event.data ) // if its empty or the default, show empty to the user
|
||||
? undefined
|
||||
: event.data,
|
||||
};
|
||||
},
|
||||
} );
|
||||
|
||||
const getStoreCountryOption = async () =>
|
||||
resolveSelect( OPTIONS_STORE_NAME ).getOption(
|
||||
'woocommerce_default_country'
|
||||
);
|
||||
|
||||
const handleStoreCountryOption = assign( {
|
||||
businessInfo: (
|
||||
context: CoreProfilerStateMachineContext,
|
||||
event: DoneInvokeEvent< string | undefined >
|
||||
) => {
|
||||
return {
|
||||
...context.businessInfo,
|
||||
location: event.data,
|
||||
};
|
||||
},
|
||||
} );
|
||||
|
||||
/**
|
||||
* Prefetch it so that @wp/data caches it and there won't be a loading delay when its used
|
||||
*/
|
||||
|
@ -167,6 +212,20 @@ const preFetchGetCountries = assign( {
|
|||
),
|
||||
} );
|
||||
|
||||
const preFetchOptions = assign( {
|
||||
spawnPrefetchOptionsRef: ( _ctx, _evt, { action } ) => {
|
||||
spawn(
|
||||
Promise.all( [
|
||||
// @ts-expect-error -- not sure its possible to type this yet, maybe in xstate v5
|
||||
action.options.map( ( optionName: string ) =>
|
||||
resolveSelect( OPTIONS_STORE_NAME ).getOption( optionName )
|
||||
),
|
||||
] ),
|
||||
'core-profiler-prefetch-options'
|
||||
);
|
||||
},
|
||||
} );
|
||||
|
||||
const getCountries = async () =>
|
||||
resolveSelect( COUNTRIES_STORE_NAME ).getCountries();
|
||||
|
||||
|
@ -196,7 +255,6 @@ const handleOnboardingProfileOption = assign( {
|
|||
selling_platforms: sellingPlatforms,
|
||||
...rest
|
||||
} = event.data;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
businessChoice,
|
||||
|
@ -206,6 +264,37 @@ const handleOnboardingProfileOption = assign( {
|
|||
},
|
||||
} );
|
||||
|
||||
const assignOnboardingProfile = assign( {
|
||||
onboardingProfile: (
|
||||
_context,
|
||||
event: DoneInvokeEvent< OnboardingProfile | undefined >
|
||||
) => event.data,
|
||||
} );
|
||||
|
||||
const getGeolocation = async ( context: CoreProfilerStateMachineContext ) => {
|
||||
if ( context.optInDataSharing ) {
|
||||
return resolveSelect( COUNTRIES_STORE_NAME ).geolocate();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const preFetchGeolocation = assign( {
|
||||
spawnGeolocationRef: ( context: CoreProfilerStateMachineContext ) =>
|
||||
spawn(
|
||||
getGeolocation( context ),
|
||||
'core-profiler-prefetch-geolocation'
|
||||
),
|
||||
} );
|
||||
|
||||
const handleGeolocation = assign( {
|
||||
geolocatedLocation: (
|
||||
_context,
|
||||
event: DoneInvokeEvent< GeolocationResponse >
|
||||
) => {
|
||||
return event.data;
|
||||
},
|
||||
} );
|
||||
|
||||
const redirectToWooHome = () => {
|
||||
/**
|
||||
* @todo replace with navigateTo
|
||||
|
@ -271,6 +360,35 @@ const recordTracksPluginsSkipped = () => {
|
|||
recordEvent( 'storeprofiler_plugins_skip' );
|
||||
};
|
||||
|
||||
const recordTracksBusinessInfoViewed = () => {
|
||||
recordEvent( 'storeprofiler_step_view', {
|
||||
step: 'business_info',
|
||||
wc_version: getSetting( 'wcVersion' ),
|
||||
} );
|
||||
};
|
||||
|
||||
const recordTracksBusinessInfoCompleted = (
|
||||
_context: CoreProfilerStateMachineContext,
|
||||
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >
|
||||
) => {
|
||||
recordEvent( 'storeprofiler_step_complete', {
|
||||
step: 'business_info',
|
||||
wc_version: getSetting( 'wcVersion' ),
|
||||
} );
|
||||
|
||||
recordEvent( 'storeprofiler_business_info', {
|
||||
business_name_filled:
|
||||
POSSIBLY_DEFAULT_STORE_NAMES.findIndex(
|
||||
( name ) => name === event.payload.storeName
|
||||
) === -1,
|
||||
industry: event.payload.industry,
|
||||
store_location_previously_set:
|
||||
_context.onboardingProfile.is_store_country_set || false,
|
||||
geolocation_success: _context.geolocatedLocation !== undefined,
|
||||
geolocation_overruled: event.payload.geolocationOverruled,
|
||||
} );
|
||||
};
|
||||
|
||||
const recordTracksSkipBusinessLocationViewed = () => {
|
||||
recordEvent( 'storeprofiler_step_view', {
|
||||
step: 'skip_business_location',
|
||||
|
@ -280,7 +398,7 @@ const recordTracksSkipBusinessLocationViewed = () => {
|
|||
|
||||
const recordTracksSkipBusinessLocationCompleted = () => {
|
||||
recordEvent( 'storeprofiler_step_complete', {
|
||||
step: 'skp_business_location',
|
||||
step: 'skip_business_location',
|
||||
wc_version: getSetting( 'wcVersion' ),
|
||||
} );
|
||||
};
|
||||
|
@ -306,15 +424,16 @@ const updateTrackingOption = (
|
|||
} );
|
||||
};
|
||||
|
||||
// TODO: move the data references over to the context.onboardingProfile object which stores the entire woocommerce_onboarding_profile contents
|
||||
const updateOnboardingProfileOption = (
|
||||
context: CoreProfilerStateMachineContext
|
||||
) => {
|
||||
const { businessChoice, sellingOnlineAnswer, sellingPlatforms, ...rest } =
|
||||
const { businessChoice, sellingOnlineAnswer, sellingPlatforms } =
|
||||
context.userProfile;
|
||||
|
||||
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
|
||||
woocommerce_onboarding_profile: {
|
||||
...rest,
|
||||
...context.onboardingProfile,
|
||||
business_choice: businessChoice,
|
||||
selling_online_answer: sellingOnlineAnswer,
|
||||
selling_platforms: sellingPlatforms,
|
||||
|
@ -328,6 +447,35 @@ const updateBusinessLocation = ( countryAndState: string ) => {
|
|||
} );
|
||||
};
|
||||
|
||||
const updateBusinessInfo = async (
|
||||
_ctx: CoreProfilerStateMachineContext,
|
||||
event: BusinessInfoEvent
|
||||
) => {
|
||||
const refreshedOnboardingProfile = ( await resolveSelect(
|
||||
OPTIONS_STORE_NAME
|
||||
).getOption( 'woocommerce_onboarding_profile' ) ) as OnboardingProfile;
|
||||
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
|
||||
blogname: event.payload.storeName,
|
||||
woocommerce_default_country: event.payload.storeLocation,
|
||||
woocommerce_onboarding_profile: {
|
||||
...refreshedOnboardingProfile,
|
||||
is_store_country_set: true,
|
||||
industry: [ event.payload.industry ],
|
||||
},
|
||||
} );
|
||||
};
|
||||
|
||||
const persistBusinessInfo = assign( {
|
||||
persistBusinessInfoRef: (
|
||||
_ctx: CoreProfilerStateMachineContext,
|
||||
event: BusinessInfoEvent
|
||||
) =>
|
||||
spawn(
|
||||
updateBusinessInfo( _ctx, event ),
|
||||
'core-profiler-update-business-info'
|
||||
),
|
||||
} );
|
||||
|
||||
const promiseDelay = ( milliseconds: number ) => {
|
||||
return new Promise( ( resolve ) => {
|
||||
setTimeout( resolve, milliseconds );
|
||||
|
@ -372,12 +520,14 @@ const handlePlugins = assign( {
|
|||
event.data,
|
||||
} );
|
||||
|
||||
const coreProfilerMachineActions = {
|
||||
updateTrackingOption,
|
||||
export const preFetchActions = {
|
||||
preFetchGetPlugins,
|
||||
preFetchGetCountries,
|
||||
handleTrackingOption,
|
||||
handlePlugins,
|
||||
preFetchGeolocation,
|
||||
preFetchOptions,
|
||||
};
|
||||
|
||||
export const recordTracksActions = {
|
||||
recordTracksIntroCompleted,
|
||||
recordTracksIntroSkipped,
|
||||
recordTracksIntroViewed,
|
||||
|
@ -388,15 +538,33 @@ const coreProfilerMachineActions = {
|
|||
recordTracksPluginsSkipped,
|
||||
recordTracksSkipBusinessLocationViewed,
|
||||
recordTracksSkipBusinessLocationCompleted,
|
||||
recordTracksBusinessInfoViewed,
|
||||
recordTracksBusinessInfoCompleted,
|
||||
};
|
||||
|
||||
const coreProfilerMachineActions = {
|
||||
...preFetchActions,
|
||||
...recordTracksActions,
|
||||
handlePlugins,
|
||||
updateTrackingOption,
|
||||
handleTrackingOption,
|
||||
handleGeolocation,
|
||||
handleStoreNameOption,
|
||||
handleStoreCountryOption,
|
||||
assignOptInDataSharing,
|
||||
handleCountries,
|
||||
handleOnboardingProfileOption,
|
||||
assignOnboardingProfile,
|
||||
persistBusinessInfo,
|
||||
redirectToWooHome,
|
||||
};
|
||||
|
||||
const coreProfilerMachineServices = {
|
||||
getAllowTrackingOption,
|
||||
getStoreNameOption,
|
||||
getStoreCountryOption,
|
||||
getCountries,
|
||||
getGeolocation,
|
||||
getOnboardingProfileOption,
|
||||
getPlugins,
|
||||
};
|
||||
|
@ -409,37 +577,66 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
|||
// actual defaults displayed to the user should be handled in the steps themselves
|
||||
optInDataSharing: false,
|
||||
userProfile: { skipped: true },
|
||||
geolocatedLocation: { location: 'US:CA' },
|
||||
businessInfo: { location: 'US:CA' },
|
||||
geolocatedLocation: undefined,
|
||||
businessInfo: {
|
||||
storeName: undefined,
|
||||
industry: undefined,
|
||||
storeCountryPreviouslySet: false,
|
||||
location: 'US:CA',
|
||||
},
|
||||
countries: [] as CountryStateOption[],
|
||||
pluginsAvailable: [],
|
||||
pluginsSelected: [],
|
||||
pluginsInstallationErrors: [],
|
||||
countries: {},
|
||||
pluginsSelected: [],
|
||||
loader: {},
|
||||
onboardingProfile: {} as OnboardingProfile,
|
||||
} as CoreProfilerStateMachineContext,
|
||||
states: {
|
||||
initializing: {
|
||||
on: {
|
||||
INITIALIZATION_COMPLETE: {
|
||||
target: 'introOptIn',
|
||||
},
|
||||
},
|
||||
entry: [ 'preFetchGetPlugins', 'preFetchGetCountries' ],
|
||||
invoke: [
|
||||
entry: [
|
||||
// these prefetch tasks are spawned actors in the background and do not block progression of the state machine
|
||||
'preFetchGetPlugins',
|
||||
'preFetchGetCountries',
|
||||
{
|
||||
src: 'getAllowTrackingOption',
|
||||
// eslint-disable-next-line xstate/no-ondone-outside-compound-state -- The invoke.onDone property refers to the invocation (invoke.src) being done, not the onDone property on a state node.
|
||||
onDone: [
|
||||
{
|
||||
actions: [ 'handleTrackingOption' ],
|
||||
target: 'introOptIn',
|
||||
},
|
||||
type: 'preFetchOptions',
|
||||
options: [
|
||||
'blogname',
|
||||
'woocommerce_onboarding_profile',
|
||||
'woocommerce_default_country',
|
||||
],
|
||||
onError: {
|
||||
target: 'introOptIn', // leave it as initialised default on error
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'parallel',
|
||||
states: {
|
||||
// if we have any other init tasks to do in parallel, add them as a parallel state here.
|
||||
// this blocks the introOptIn UI from loading keep that in mind when adding new tasks here
|
||||
trackingOption: {
|
||||
initial: 'fetching',
|
||||
states: {
|
||||
fetching: {
|
||||
invoke: {
|
||||
src: 'getAllowTrackingOption',
|
||||
onDone: [
|
||||
{
|
||||
actions: [ 'handleTrackingOption' ],
|
||||
target: 'done',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'done', // leave it as initialised default on error
|
||||
},
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
onDone: {
|
||||
target: 'introOptIn',
|
||||
// TODO: at this point, we can handle the URL path param if any and jump to the correct page
|
||||
},
|
||||
meta: {
|
||||
progress: 0,
|
||||
},
|
||||
|
@ -484,7 +681,10 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
|||
src: 'getOnboardingProfileOption',
|
||||
onDone: [
|
||||
{
|
||||
actions: [ 'handleOnboardingProfileOption' ],
|
||||
actions: [
|
||||
'handleOnboardingProfileOption',
|
||||
'assignOnboardingProfile',
|
||||
],
|
||||
target: 'userProfile',
|
||||
},
|
||||
],
|
||||
|
@ -494,7 +694,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
|||
},
|
||||
},
|
||||
userProfile: {
|
||||
entry: [ 'recordTracksUserProfileViewed' ],
|
||||
entry: [ 'recordTracksUserProfileViewed', 'preFetchGeolocation' ],
|
||||
on: {
|
||||
USER_PROFILE_COMPLETED: {
|
||||
target: 'postUserProfile',
|
||||
|
@ -546,28 +746,117 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
|||
},
|
||||
},
|
||||
preBusinessInfo: {
|
||||
always: [
|
||||
// immediately transition to businessInfo without any events as long as geolocation parallel has completed
|
||||
{
|
||||
target: 'businessInfo',
|
||||
cond: () => true, // TODO: use a custom function to check on the parallel state using meta when we implement that. https://xstate.js.org/docs/guides/guards.html#guards-condition-functions
|
||||
type: 'parallel',
|
||||
states: {
|
||||
geolocation: {
|
||||
initial: 'checkDataOptIn',
|
||||
states: {
|
||||
checkDataOptIn: {
|
||||
// if the user has opted out of data sharing, we skip the geolocation step
|
||||
always: [
|
||||
{
|
||||
cond: ( context ) =>
|
||||
context.optInDataSharing,
|
||||
target: 'fetching',
|
||||
},
|
||||
{
|
||||
target: 'done',
|
||||
},
|
||||
],
|
||||
},
|
||||
fetching: {
|
||||
invoke: {
|
||||
src: 'getGeolocation',
|
||||
onDone: {
|
||||
target: 'done',
|
||||
actions: 'handleGeolocation',
|
||||
},
|
||||
// onError TODO: handle error
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
storeCountryOption: {
|
||||
initial: 'fetching',
|
||||
states: {
|
||||
fetching: {
|
||||
invoke: {
|
||||
src: 'getStoreCountryOption',
|
||||
onDone: [
|
||||
{
|
||||
actions: [ 'handleStoreCountryOption' ],
|
||||
target: 'done',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'done',
|
||||
},
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
},
|
||||
storeNameOption: {
|
||||
initial: 'fetching',
|
||||
states: {
|
||||
fetching: {
|
||||
invoke: {
|
||||
src: 'getStoreNameOption',
|
||||
onDone: [
|
||||
{
|
||||
actions: [ 'handleStoreNameOption' ],
|
||||
target: 'done',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'done', // leave it as initialised default on error
|
||||
},
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
},
|
||||
countries: {
|
||||
initial: 'fetching',
|
||||
states: {
|
||||
fetching: {
|
||||
invoke: {
|
||||
src: 'getCountries',
|
||||
onDone: {
|
||||
target: 'done',
|
||||
actions: 'handleCountries',
|
||||
},
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// onDone is reached when child parallel states are all at their final states
|
||||
onDone: {
|
||||
target: 'businessInfo',
|
||||
},
|
||||
meta: {
|
||||
progress: 50,
|
||||
},
|
||||
},
|
||||
businessInfo: {
|
||||
entry: [ 'recordTracksBusinessInfoViewed' ],
|
||||
on: {
|
||||
BUSINESS_INFO_COMPLETED: {
|
||||
target: 'prePlugins',
|
||||
actions: [
|
||||
assign( {
|
||||
businessInfo: (
|
||||
_context,
|
||||
event: BusinessInfoEvent
|
||||
) => event.payload.businessInfo, // assign context.businessInfo to the payload of the event
|
||||
} ),
|
||||
'persistBusinessInfo',
|
||||
'recordTracksBusinessInfoCompleted',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -599,7 +888,12 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
|||
businessInfo: (
|
||||
_context,
|
||||
event: BusinessLocationEvent
|
||||
) => event.payload.businessInfo, // assign context.businessInfo to the payload of the event
|
||||
) => {
|
||||
return {
|
||||
..._context.businessInfo,
|
||||
location: event.payload.storeLocation,
|
||||
};
|
||||
},
|
||||
} ),
|
||||
'recordTracksSkipBusinessLocationCompleted',
|
||||
],
|
||||
|
@ -623,7 +917,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
|||
invoke: {
|
||||
src: ( context ) => {
|
||||
return updateBusinessLocation(
|
||||
context.businessInfo.location
|
||||
context.businessInfo.location as string
|
||||
);
|
||||
},
|
||||
onDone: {
|
||||
|
@ -899,7 +1193,9 @@ export const CoreProfilerController = ( {
|
|||
} );
|
||||
}, [ actionOverrides, servicesOverrides ] );
|
||||
|
||||
const [ state, send ] = useMachine( augmentedStateMachine );
|
||||
const [ state, send ] = useMachine( augmentedStateMachine, {
|
||||
devTools: process.env.NODE_ENV === 'development',
|
||||
} );
|
||||
const stateValue =
|
||||
typeof state.value === 'object'
|
||||
? Object.keys( state.value )[ 0 ]
|
||||
|
|
|
@ -1,34 +1,353 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, TextControl, Notice } from '@wordpress/components';
|
||||
import { SelectControl } from '@woocommerce/components';
|
||||
import { Icon, chevronDown } from '@wordpress/icons';
|
||||
import {
|
||||
createInterpolateElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { findCountryOption, getCountry } from '@woocommerce/onboarding';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CoreProfilerStateMachineContext, BusinessInfoEvent } from '../index';
|
||||
import { CountryStateOption } from '../services/country';
|
||||
import { Heading } from '../components/heading/heading';
|
||||
import { Navigation } from '../components/navigation/navigation';
|
||||
|
||||
/** These are some store names that are known to be set by default and not likely to be used as actual names */
|
||||
export const POSSIBLY_DEFAULT_STORE_NAMES = [
|
||||
undefined,
|
||||
'woocommerce',
|
||||
'Site Title',
|
||||
'',
|
||||
];
|
||||
export type IndustryChoice = typeof industryChoices[ number ][ 'key' ];
|
||||
export const industryChoices = [
|
||||
{
|
||||
label: __( 'Clothing and accessories', 'woocommerce' ),
|
||||
key: 'clothing_and_accessories' as const,
|
||||
},
|
||||
{
|
||||
label: __( 'Health and beauty', 'woocommerce' ),
|
||||
key: 'health_and_beauty' as const,
|
||||
},
|
||||
{
|
||||
label: __( 'Food and drink', 'woocommerce' ),
|
||||
key: 'food_and_drink' as const,
|
||||
},
|
||||
{
|
||||
label: __( 'Home, furniture and garden', 'woocommerce' ),
|
||||
key: 'home_furniture_and_garden' as const,
|
||||
},
|
||||
{
|
||||
label: __( 'Education and learning', 'woocommerce' ),
|
||||
key: 'education_and_learning' as const,
|
||||
},
|
||||
{
|
||||
label: __( 'Electronics and computers', 'woocommerce' ),
|
||||
key: 'electronics_and_computers' as const,
|
||||
},
|
||||
{
|
||||
label: __( 'Other', 'woocommerce' ),
|
||||
key: 'other' as const,
|
||||
},
|
||||
];
|
||||
|
||||
export type IndustryChoiceOption = typeof industryChoices[ number ];
|
||||
|
||||
export const selectIndustryMapping = {
|
||||
im_just_starting_my_business: __(
|
||||
'What type of products or services do you plan to sell?',
|
||||
'woocommerce'
|
||||
),
|
||||
im_already_selling: __(
|
||||
'Which industry is your business in?',
|
||||
'woocommerce'
|
||||
),
|
||||
im_setting_up_a_store_for_a_client: __(
|
||||
"Which industry is your client's business in?",
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
export const BusinessInfo = ( {
|
||||
context,
|
||||
navigationProgress,
|
||||
sendEvent,
|
||||
}: {
|
||||
context: CoreProfilerStateMachineContext;
|
||||
navigationProgress: number;
|
||||
sendEvent: ( event: BusinessInfoEvent ) => void;
|
||||
} ) => {
|
||||
return (
|
||||
<>
|
||||
<div>Business Info</div>
|
||||
<div>{ context.geolocatedLocation.location }</div>
|
||||
<button
|
||||
onClick={ () =>
|
||||
sendEvent( {
|
||||
type: 'BUSINESS_INFO_COMPLETED',
|
||||
payload: {
|
||||
businessInfo: {
|
||||
foo: { bar: 'qux' },
|
||||
location: 'US:CA',
|
||||
},
|
||||
},
|
||||
} )
|
||||
const {
|
||||
geolocatedLocation,
|
||||
userProfile: { businessChoice },
|
||||
businessInfo,
|
||||
countries,
|
||||
onboardingProfile: {
|
||||
is_store_country_set: isStoreCountrySet,
|
||||
industry: industryFromOnboardingProfile,
|
||||
},
|
||||
} = context;
|
||||
|
||||
const [ storeName, setStoreName ] = useState(
|
||||
businessInfo.storeName || ''
|
||||
);
|
||||
|
||||
const [ storeCountry, setStoreCountry ] = useState< CountryStateOption >( {
|
||||
key: '',
|
||||
label: '',
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
if ( isStoreCountrySet ) {
|
||||
const previouslyStoredCountryOption = countries.find(
|
||||
( country ) => country.key === businessInfo.location
|
||||
);
|
||||
setStoreCountry(
|
||||
previouslyStoredCountryOption || { key: '', label: '' }
|
||||
);
|
||||
}
|
||||
}, [ businessInfo.location, countries, isStoreCountrySet ] );
|
||||
|
||||
const [ geolocationMatch, setGeolocationMatch ] = useState( {
|
||||
key: '',
|
||||
label: '',
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
if ( geolocatedLocation ) {
|
||||
const foundCountryOption = findCountryOption(
|
||||
countries,
|
||||
geolocatedLocation
|
||||
);
|
||||
if ( foundCountryOption ) {
|
||||
setGeolocationMatch( foundCountryOption );
|
||||
if ( ! isStoreCountrySet ) {
|
||||
setStoreCountry( foundCountryOption );
|
||||
}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}, [ countries, isStoreCountrySet, geolocatedLocation ] );
|
||||
|
||||
const geolocationOverruled =
|
||||
geolocatedLocation &&
|
||||
getCountry( storeCountry.key ) !== getCountry( geolocationMatch.key );
|
||||
|
||||
const [ industry, setIndustry ] = useState<
|
||||
IndustryChoiceOption | undefined
|
||||
>(
|
||||
industryFromOnboardingProfile
|
||||
? industryChoices.find(
|
||||
( choice ) =>
|
||||
choice.key === industryFromOnboardingProfile[ 0 ]
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
|
||||
const selectCountryLabel = __( 'Select country/region', 'woocommerce' );
|
||||
const selectIndustryQuestionLabel =
|
||||
selectIndustryMapping[
|
||||
businessChoice || 'im_just_starting_my_business'
|
||||
];
|
||||
|
||||
const [ dismissedGeolocationNotice, setDismissedGeolocationNotice ] =
|
||||
useState( false );
|
||||
|
||||
return (
|
||||
<div
|
||||
className="woocommerce-profiler-business-information"
|
||||
data-testid="core-profiler-business-information"
|
||||
>
|
||||
<Navigation percentage={ navigationProgress } />
|
||||
<div className="woocommerce-profiler-page__content woocommerce-profiler-business-information__content">
|
||||
<Heading
|
||||
className="woocommerce-profiler__stepper-heading"
|
||||
title={ __(
|
||||
'Tell us a bit about your store',
|
||||
'woocommerce'
|
||||
) }
|
||||
subTitle={ __(
|
||||
"We'll use this information to help you set up payments, shipping, and taxes, as well as recommending the best theme for your store.",
|
||||
'woocommerce'
|
||||
) }
|
||||
/>
|
||||
|
||||
<form
|
||||
className="woocommerce-profiler-business-information-form"
|
||||
autoComplete="off"
|
||||
>
|
||||
<TextControl
|
||||
className="woocommerce-profiler-business-info-store-name"
|
||||
onChange={ ( value ) => {
|
||||
setStoreName( value );
|
||||
} }
|
||||
value={ storeName }
|
||||
label={
|
||||
<>
|
||||
{ __(
|
||||
'Give your store a name',
|
||||
'woocommerce'
|
||||
) }
|
||||
</>
|
||||
}
|
||||
placeholder={ __(
|
||||
'Ex. My awesome store',
|
||||
'woocommerce'
|
||||
) }
|
||||
/>
|
||||
<p className="woocommerce-profiler-question-subtext">
|
||||
{ __(
|
||||
"Don't worry — you can always change it later!",
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<p className="woocommerce-profiler-question-label">
|
||||
{ selectIndustryQuestionLabel }
|
||||
</p>
|
||||
<SelectControl
|
||||
className="woocommerce-profiler-select-control__industry"
|
||||
instanceId={ 1 }
|
||||
placeholder={ __(
|
||||
'Select an industry',
|
||||
'woocommerce'
|
||||
) }
|
||||
label={ __( 'Select an industry', 'woocommerce' ) }
|
||||
options={ industryChoices }
|
||||
excludeSelectedOptions={ false }
|
||||
help={ <Icon icon={ chevronDown } /> }
|
||||
onChange={ (
|
||||
results: Array< typeof industryChoices[ number ] >
|
||||
) => {
|
||||
if ( results.length ) {
|
||||
setIndustry( results[ 0 ] );
|
||||
}
|
||||
} }
|
||||
selected={ industry ? [ industry ] : [] }
|
||||
showAllOnFocus
|
||||
isSearchable
|
||||
/>
|
||||
<p className="woocommerce-profiler-question-label">
|
||||
{ __( 'Where is your store located?', 'woocommerce' ) }
|
||||
<span className="woocommerce-profiler-question-required">
|
||||
{ '*' }
|
||||
</span>
|
||||
</p>
|
||||
<SelectControl
|
||||
className="woocommerce-profiler-select-control__country"
|
||||
instanceId={ 2 }
|
||||
placeholder={ selectCountryLabel }
|
||||
label={
|
||||
storeCountry.key === '' ? selectCountryLabel : ''
|
||||
}
|
||||
getSearchExpression={ ( query: string ) => {
|
||||
return new RegExp(
|
||||
'(^' + query + '| — (' + query + '))',
|
||||
'i'
|
||||
);
|
||||
} }
|
||||
options={ countries }
|
||||
excludeSelectedOptions={ false }
|
||||
help={ <Icon icon={ chevronDown } /> }
|
||||
onChange={ ( results: Array< CountryStateOption > ) => {
|
||||
if ( results.length ) {
|
||||
setStoreCountry( results[ 0 ] );
|
||||
}
|
||||
} }
|
||||
selected={ storeCountry ? [ storeCountry ] : [] }
|
||||
showAllOnFocus
|
||||
isSearchable
|
||||
/>
|
||||
{ /* woocommerce-profiler-select-control__country-spacer exists purely because the select-control above has an unremovable and unstyleable div and that's preventing margin collapse */ }
|
||||
<div className="woocommerce-profiler-select-control__country-spacer" />
|
||||
{ geolocationOverruled && ! dismissedGeolocationNotice && (
|
||||
<Notice
|
||||
className="woocommerce-profiler-geolocation-notice"
|
||||
onRemove={ () =>
|
||||
setDismissedGeolocationNotice( true )
|
||||
}
|
||||
status="warning"
|
||||
>
|
||||
<p>
|
||||
{ createInterpolateElement(
|
||||
__(
|
||||
// translators: first tag is filled with the country name detected by geolocation, second tag is the country name selected by the user
|
||||
"It looks like you're located in <geolocatedCountry></geolocatedCountry>. Are you sure you want to create a store in <selectedCountry></selectedCountry>?",
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
geolocatedCountry: (
|
||||
<Button
|
||||
className="geolocation-notice-geolocated-country"
|
||||
variant="link"
|
||||
onClick={ () =>
|
||||
setStoreCountry(
|
||||
geolocationMatch
|
||||
)
|
||||
}
|
||||
>
|
||||
{
|
||||
geolocatedLocation?.country_long
|
||||
}
|
||||
</Button>
|
||||
),
|
||||
selectedCountry: (
|
||||
<span className="geolocation-notice-selected-country">
|
||||
{ storeCountry.label }
|
||||
</span>
|
||||
),
|
||||
}
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ __(
|
||||
'Setting up your store in the wrong country may lead to the following issues: ',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<ul className="woocommerce-profiler-geolocation-notice__list">
|
||||
<li>
|
||||
{ __(
|
||||
'Tax and duty obligations',
|
||||
'woocommerce'
|
||||
) }
|
||||
</li>
|
||||
<li>
|
||||
{ __( 'Payment issues', 'woocommerce' ) }
|
||||
</li>
|
||||
<li>
|
||||
{ __( 'Shipping issues', 'woocommerce' ) }
|
||||
</li>
|
||||
</ul>
|
||||
</Notice>
|
||||
) }
|
||||
</form>
|
||||
<div className="woocommerce-profiler-button-container">
|
||||
<Button
|
||||
className="woocommerce-profiler-button"
|
||||
variant="primary"
|
||||
disabled={ ! storeCountry.key }
|
||||
onClick={ () => {
|
||||
sendEvent( {
|
||||
type: 'BUSINESS_INFO_COMPLETED',
|
||||
payload: {
|
||||
storeName,
|
||||
industry: industry?.key,
|
||||
storeLocation: storeCountry.key,
|
||||
geolocationOverruled:
|
||||
geolocationOverruled || false,
|
||||
},
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ __( 'Continue', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -84,9 +84,7 @@ export const BusinessLocation = ( {
|
|||
sendEvent( {
|
||||
type: 'BUSINESS_LOCATION_COMPLETED',
|
||||
payload: {
|
||||
businessInfo: {
|
||||
location: storeCountry.key,
|
||||
},
|
||||
storeLocation: storeCountry.key,
|
||||
},
|
||||
} );
|
||||
} }
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { BusinessInfo } from '../BusinessInfo';
|
||||
import { CoreProfilerStateMachineContext } from '../..';
|
||||
|
||||
describe( 'BusinessInfo', () => {
|
||||
let props: {
|
||||
sendEvent: jest.Mock;
|
||||
navigationProgress: number;
|
||||
context: CoreProfilerStateMachineContext;
|
||||
};
|
||||
|
||||
beforeEach( () => {
|
||||
props = {
|
||||
sendEvent: jest.fn(),
|
||||
navigationProgress: 0,
|
||||
context: {
|
||||
geolocatedLocation: undefined,
|
||||
userProfile: {
|
||||
businessChoice: 'im_just_starting_my_business',
|
||||
},
|
||||
businessInfo: {
|
||||
storeName: '',
|
||||
location: '',
|
||||
},
|
||||
countries: [
|
||||
{
|
||||
key: 'AU:VIC',
|
||||
label: 'Australia — Victoria',
|
||||
},
|
||||
{
|
||||
key: 'AW',
|
||||
label: 'Aruba',
|
||||
},
|
||||
],
|
||||
// @ts-expect-error -- we don't need the other props in the tests
|
||||
onboardingProfile: {
|
||||
is_store_country_set: false,
|
||||
industry: [ 'other' ],
|
||||
},
|
||||
},
|
||||
};
|
||||
} );
|
||||
|
||||
it( 'should render business info page', () => {
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByText( /Tell us a bit about your store/i, {
|
||||
selector: 'h1',
|
||||
} )
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText( /Give your store a name/i )
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText( /Where is your store located?/i )
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole( 'button', {
|
||||
name: /Continue/i,
|
||||
} )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should show the correct label for each businessChoice', () => {
|
||||
props.context.userProfile.businessChoice =
|
||||
'im_just_starting_my_business';
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByText(
|
||||
/What type of products or services do you plan to sell?/i
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
props.context.userProfile.businessChoice = 'im_already_selling';
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByText( /Which industry is your business in?/i )
|
||||
).toBeInTheDocument();
|
||||
|
||||
props.context.userProfile.businessChoice =
|
||||
'im_setting_up_a_store_for_a_client';
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByText( /Which industry is your client's business in?/i )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should prepopulate the store country selector if geolocation is available', () => {
|
||||
props.context.geolocatedLocation = {
|
||||
latitude: '-37.83961',
|
||||
longitude: '144.94228',
|
||||
country_short: 'AU',
|
||||
country_long: 'Australia',
|
||||
region: 'Victoria',
|
||||
city: 'Port Melbourne',
|
||||
};
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByRole( 'combobox', {
|
||||
name: '', // unfortunately the SelectControl loses its name when its populated and there's no other unique identifier
|
||||
} )
|
||||
).toHaveValue( 'Australia — Victoria' );
|
||||
} );
|
||||
|
||||
it( 'should not prepopulate the store country selector if geolocation is not available', () => {
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByRole( 'combobox', {
|
||||
name: /Select country\/region/i,
|
||||
} )
|
||||
).toHaveValue( '' );
|
||||
} );
|
||||
|
||||
it( 'should correctly prepopulate the store name if it is passed in', () => {
|
||||
props.context.businessInfo.storeName = 'Test Store Name';
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByRole( 'textbox', {
|
||||
name: /Give your store a name/i,
|
||||
} )
|
||||
).toHaveValue( 'Test Store Name' );
|
||||
} );
|
||||
|
||||
it( 'should correctly prepopulate the industry if it is passed in', () => {
|
||||
props.context.onboardingProfile.industry = [ 'food_and_drink' ];
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByRole( 'combobox', {
|
||||
name: /Select an industry/i,
|
||||
} )
|
||||
).toHaveValue( 'Food and drink' );
|
||||
} );
|
||||
|
||||
it( 'should correctly prepopulate the store location if it is passed in', () => {
|
||||
props.context.businessInfo.location = 'AW';
|
||||
props.context.onboardingProfile.is_store_country_set = true;
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
expect(
|
||||
screen.getByRole( 'combobox', {
|
||||
name: '',
|
||||
} )
|
||||
).toHaveValue( 'Aruba' );
|
||||
} );
|
||||
|
||||
it( 'should correctly send event with empty form inputs when continue is clicked', () => {
|
||||
props.context.geolocatedLocation = {
|
||||
latitude: '-37.83961',
|
||||
longitude: '144.94228',
|
||||
country_short: 'AU',
|
||||
country_long: 'Australia',
|
||||
region: 'Victoria',
|
||||
city: 'Port Melbourne',
|
||||
};
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
const continueButton = screen.getByRole( 'button', {
|
||||
name: /Continue/i,
|
||||
} );
|
||||
userEvent.click( continueButton );
|
||||
expect( props.sendEvent ).toHaveBeenCalledWith( {
|
||||
payload: {
|
||||
geolocationOverruled: false,
|
||||
industry: 'other',
|
||||
storeLocation: 'AU:VIC',
|
||||
storeName: '',
|
||||
},
|
||||
type: 'BUSINESS_INFO_COMPLETED',
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should show the warning tooltip if geolocation is available and the user has manually changed the country, and send the event with geolocationOverruled', async () => {
|
||||
props.context.geolocatedLocation = {
|
||||
latitude: '-37.83961',
|
||||
longitude: '144.94228',
|
||||
country_short: 'AU',
|
||||
country_long: 'Australia',
|
||||
region: 'Victoria',
|
||||
city: 'Port Melbourne',
|
||||
};
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
const countrySelector = screen.getByRole( 'combobox', {
|
||||
name: '',
|
||||
} );
|
||||
|
||||
countrySelector.focus();
|
||||
|
||||
await waitFor( () => {
|
||||
expect(
|
||||
screen.getByRole( 'option', {
|
||||
name: /Aruba/i,
|
||||
} )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole( 'option', {
|
||||
name: /Aruba/i,
|
||||
} )
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Setting up your store in the wrong country may lead to the following issues: /i
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
const continueButton = screen.getByRole( 'button', {
|
||||
name: /Continue/i,
|
||||
} );
|
||||
userEvent.click( continueButton );
|
||||
expect( props.sendEvent ).toHaveBeenCalledWith( {
|
||||
payload: {
|
||||
geolocationOverruled: true,
|
||||
industry: 'other',
|
||||
storeLocation: 'AW',
|
||||
storeName: '',
|
||||
},
|
||||
type: 'BUSINESS_INFO_COMPLETED',
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should correctly send event with inputs filled in a fresh form when continue is clicked', async () => {
|
||||
props.context.geolocatedLocation = {
|
||||
latitude: '-37.83961',
|
||||
longitude: '144.94228',
|
||||
country_short: 'AU',
|
||||
country_long: 'Australia',
|
||||
region: 'Victoria',
|
||||
city: 'Port Melbourne',
|
||||
};
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
const storeNameInput = screen.getByRole( 'textbox', {
|
||||
name: /Give your store a name/i,
|
||||
} );
|
||||
userEvent.type( storeNameInput, 'Test Store Name' );
|
||||
const industrySelector = screen.getByRole( 'combobox', {
|
||||
name: /Select an industry/i,
|
||||
} );
|
||||
|
||||
industrySelector.focus();
|
||||
|
||||
await waitFor( () => {
|
||||
expect(
|
||||
screen.getByRole( 'option', {
|
||||
name: /Food and drink/i,
|
||||
} )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole( 'option', {
|
||||
name: /Food and drink/i,
|
||||
} )
|
||||
);
|
||||
|
||||
const continueButton = screen.getByRole( 'button', {
|
||||
name: /Continue/i,
|
||||
} );
|
||||
userEvent.click( continueButton );
|
||||
expect( props.sendEvent ).toHaveBeenCalledWith( {
|
||||
payload: {
|
||||
geolocationOverruled: false,
|
||||
industry: 'food_and_drink',
|
||||
storeLocation: 'AU:VIC',
|
||||
storeName: 'Test Store Name',
|
||||
},
|
||||
type: 'BUSINESS_INFO_COMPLETED',
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should send the event with the correct values if the form has been pre-filled from context', () => {
|
||||
props.context.geolocatedLocation = {
|
||||
latitude: '-37.83961',
|
||||
longitude: '144.94228',
|
||||
country_short: 'AU',
|
||||
country_long: 'Australia',
|
||||
region: 'Victoria',
|
||||
city: 'Port Melbourne',
|
||||
};
|
||||
props.context.onboardingProfile.industry = [ 'food_and_drink' ];
|
||||
props.context.businessInfo.storeName = 'Test Store Name';
|
||||
props.context.businessInfo.location = 'AU:VIC';
|
||||
render( <BusinessInfo { ...props } /> );
|
||||
const continueButton = screen.getByRole( 'button', {
|
||||
name: /Continue/i,
|
||||
} );
|
||||
userEvent.click( continueButton );
|
||||
expect( props.sendEvent ).toHaveBeenCalledWith( {
|
||||
payload: {
|
||||
geolocationOverruled: false,
|
||||
industry: 'food_and_drink',
|
||||
storeLocation: 'AU:VIC',
|
||||
storeName: 'Test Store Name',
|
||||
},
|
||||
type: 'BUSINESS_INFO_COMPLETED',
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -12,15 +12,6 @@ export type CountryStateOption = {
|
|||
label: string;
|
||||
};
|
||||
|
||||
type UserLocation = {
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
country_short: string;
|
||||
country_long: string;
|
||||
region: string;
|
||||
city: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all country and state combinations used for select dropdowns.
|
||||
*
|
||||
|
@ -58,33 +49,3 @@ export function getCountryStateOptions(
|
|||
|
||||
return countryStateOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's location
|
||||
*
|
||||
* @return {Object} {
|
||||
* latitude: '39.039474',
|
||||
* longitude: '-77.491809',
|
||||
* country_short: 'US',
|
||||
* country_long: 'United States of America',
|
||||
* region: 'Virginia',
|
||||
* city: 'Ashburn'
|
||||
* }
|
||||
*/
|
||||
export const getUserLocation = (): Promise< UserLocation | null > => {
|
||||
// cache buster
|
||||
const v = new Date().getTime();
|
||||
return fetch( 'https://public-api.wordpress.com/geo/?v=' + v )
|
||||
.then( ( res ) => {
|
||||
if ( ! res.ok ) {
|
||||
// @ts-expect-error tmp
|
||||
return res.body().then( ( body ) => {
|
||||
throw new Error( body );
|
||||
} );
|
||||
}
|
||||
return res.json();
|
||||
} )
|
||||
.catch( () => {
|
||||
return null;
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
position: absolute;
|
||||
bottom: 20px;
|
||||
padding: 0 20px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -457,3 +458,141 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Business Info Page
|
||||
|
||||
.woocommerce-profiler-business-information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.woocommerce-profiler-business-information__content {
|
||||
max-width: 615px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-business-information-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
max-width: 404px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-question-label,
|
||||
.woocommerce-profiler-business-info-store-name
|
||||
.components-base-control__label {
|
||||
text-transform: uppercase;
|
||||
color: $gray-900;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-question-label
|
||||
.woocommerce-profiler-question-required {
|
||||
color: #cc1818;
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-business-info-store-name
|
||||
.components-text-control__input {
|
||||
height: 40px;
|
||||
border-color: #bbb;
|
||||
border-radius: 2px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-select-control__industry {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-question-subtext {
|
||||
color: $gray-700;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-select-control__country {
|
||||
max-width: 404px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-select-control__country-spacer {
|
||||
margin: auto auto 12px auto;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-button-container {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.woocommerce-select-control__control-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.woocommerce-select-control__control.is-active {
|
||||
.components-base-control__label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-select-control.is-searchable
|
||||
.woocommerce-select-control__control-input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.woocommerce-select-control.is-searchable
|
||||
.components-base-control__label {
|
||||
left: 13px;
|
||||
}
|
||||
|
||||
.woocommerce-profiler-geolocation-notice {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
padding-right: 0;
|
||||
color: $gray-900;
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.components-notice__dismiss > svg {
|
||||
height: 16px;
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
.components-notice__dismiss {
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
|
||||
.geolocation-notice-geolocated-country {
|
||||
color: #3858e9;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-profiler-geolocation-notice__list {
|
||||
list-style-type: unset;
|
||||
padding-left: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,20 +12,29 @@ import { createMachine } from 'xstate';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CoreProfilerController } from '../';
|
||||
import {
|
||||
preFetchActions,
|
||||
recordTracksActions,
|
||||
CoreProfilerController,
|
||||
} from '../';
|
||||
|
||||
const preFetchActionsMocks = Object.fromEntries(
|
||||
Object.entries( preFetchActions ).map( ( [ key ] ) => [ key, jest.fn() ] )
|
||||
);
|
||||
|
||||
const recordTracksActionsMocks = Object.fromEntries(
|
||||
Object.entries( recordTracksActions ).map( ( [ key ] ) => [
|
||||
key,
|
||||
jest.fn(),
|
||||
] )
|
||||
);
|
||||
|
||||
// mock out the external dependencies which we don't want to test here
|
||||
const actionOverrides = {
|
||||
...preFetchActionsMocks,
|
||||
...recordTracksActionsMocks,
|
||||
updateTrackingOption: jest.fn(),
|
||||
updateOnboardingProfileOption: jest.fn(),
|
||||
recordTracksIntroCompleted: jest.fn(),
|
||||
recordTracksIntroSkipped: jest.fn(),
|
||||
recordTracksIntroViewed: jest.fn(),
|
||||
recordTracksUserProfileCompleted: jest.fn(),
|
||||
recordTracksUserProfileSkipped: jest.fn(),
|
||||
recordTracksUserProfileViewed: jest.fn(),
|
||||
recordTracksSkipBusinessLocationViewed: jest.fn(),
|
||||
recordTracksSkipBusinessLocationCompleted: jest.fn(),
|
||||
redirectToWooHome: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -36,6 +45,7 @@ const servicesOverrides = {
|
|||
.mockResolvedValue( [
|
||||
{ code: 'US', name: 'United States', states: [] },
|
||||
] ),
|
||||
getGeolocation: jest.fn().mockResolvedValue( {} ),
|
||||
getOnboardingProfileOption: jest.fn().mockResolvedValue( {} ),
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Added business info page to new core profiler
|
|
@ -206,6 +206,9 @@ class Options extends \WC_REST_Data_Controller {
|
|||
'woocommerce_date_type',
|
||||
'date_format',
|
||||
'time_format',
|
||||
'woocommerce_onboarding_profile',
|
||||
'woocommerce_default_country',
|
||||
'blogname',
|
||||
// WC Test helper options.
|
||||
'wc-admin-test-helper-rest-api-filters',
|
||||
'wc_admin_helper_feature_values',
|
||||
|
|
Loading…
Reference in New Issue