diff --git a/packages/js/data/changelog/add-geolocation-api-call b/packages/js/data/changelog/add-geolocation-api-call new file mode 100644 index 00000000000..3aa3715e854 --- /dev/null +++ b/packages/js/data/changelog/add-geolocation-api-call @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add ability to make geolocation call to WPCOM API diff --git a/packages/js/data/src/countries/action-types.ts b/packages/js/data/src/countries/action-types.ts index 68547991c51..25a739f1c17 100644 --- a/packages/js/data/src/countries/action-types.ts +++ b/packages/js/data/src/countries/action-types.ts @@ -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; diff --git a/packages/js/data/src/countries/actions.ts b/packages/js/data/src/countries/actions.ts index 7970d6bfd2e..af2b808eff5 100644 --- a/packages/js/data/src/countries/actions.ts +++ b/packages/js/data/src/countries/actions.ts @@ -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 >; diff --git a/packages/js/data/src/countries/reducer.ts b/packages/js/data/src/countries/reducer.ts index 48ab31815bb..f95e1dffeb4 100644 --- a/packages/js/data/src/countries/reducer.ts +++ b/packages/js/data/src/countries/reducer.ts @@ -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; }; diff --git a/packages/js/data/src/countries/resolvers.ts b/packages/js/data/src/countries/resolvers.ts index ccdb261276e..8f8afde66c9 100644 --- a/packages/js/data/src/countries/resolvers.ts +++ b/packages/js/data/src/countries/resolvers.ts @@ -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 ); + } + }; diff --git a/packages/js/data/src/countries/selectors.ts b/packages/js/data/src/countries/selectors.ts index 13372f8e5b5..e9a81e4a7cc 100644 --- a/packages/js/data/src/countries/selectors.ts +++ b/packages/js/data/src/countries/selectors.ts @@ -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; +}; diff --git a/packages/js/data/src/countries/types.ts b/packages/js/data/src/countries/types.ts index 4d6eb2b849a..a107c4ac94a 100644 --- a/packages/js/data/src/countries/types.ts +++ b/packages/js/data/src/countries/types.ts @@ -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; }; diff --git a/packages/js/onboarding/changelog/add-get-country-util b/packages/js/onboarding/changelog/add-get-country-util new file mode 100644 index 00000000000..435f5738ec0 --- /dev/null +++ b/packages/js/onboarding/changelog/add-get-country-util @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added getCountry utility for splitting colon delimited country:state strings \ No newline at end of file diff --git a/packages/js/onboarding/src/utils/countries/index.ts b/packages/js/onboarding/src/utils/countries/index.ts index 387b0ee6d95..edc8664e883 100644 --- a/packages/js/onboarding/src/utils/countries/index.ts +++ b/packages/js/onboarding/src/utils/countries/index.ts @@ -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; diff --git a/packages/js/onboarding/src/utils/countries/tests/utils/index.test.ts b/packages/js/onboarding/src/utils/countries/tests/utils/index.test.ts index cb3546d99ec..688a100abff 100644 --- a/packages/js/onboarding/src/utils/countries/tests/utils/index.test.ts +++ b/packages/js/onboarding/src/utils/countries/tests/utils/index.test.ts @@ -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' ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss b/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss index c1cf617130f..2a06768136c 100644 --- a/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss @@ -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; diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 186ffe9b54e..d0b9f8eb8ca 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -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 ] diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx index 103b97b852f..44539ef9a9d 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx @@ -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 ( - <> -