diff --git a/client/landing/stepper/declarative-flow/helpers/use-goals-first-experiment.ts b/client/landing/stepper/declarative-flow/helpers/use-goals-first-experiment.ts new file mode 100644 index 0000000000000..d8fcdbe31c6fc --- /dev/null +++ b/client/landing/stepper/declarative-flow/helpers/use-goals-first-experiment.ts @@ -0,0 +1,32 @@ +import { isEnabled } from '@automattic/calypso-config'; +import { useEffect, useState } from 'react'; + +/** + * Check whether the user should have the "goals first" onboarding experience. + * Returns [ isLoading, isGoalsAtFrontExperiment ] + * + * The experience is currently gated behind a feature flag which is loaded immediately, + * but we want to code as if loading time might be involved. So this hook has a fake + * timer. + * Note: It's important that there be no timer in the production experience + * i.e. when the feature flag is disabled. + */ +export function useGoalsFirstExperiment(): [ boolean, boolean ] { + const [ isLoading, setIsLoading ] = useState( true ); + useEffect( () => { + if ( ! isEnabled( 'onboarding/goals-first' ) ) { + return; + } + + const id = setTimeout( () => setIsLoading( false ), 700 ); + return () => { + clearTimeout( id ); + }; + }, [] ); + + if ( ! isEnabled( 'onboarding/goals-first' ) ) { + return [ false, false ]; + } + + return [ isLoading, true ]; +} diff --git a/client/landing/stepper/declarative-flow/internals/global.scss b/client/landing/stepper/declarative-flow/internals/global.scss index d75fc2068d4b8..bfd36f391dbbb 100644 --- a/client/landing/stepper/declarative-flow/internals/global.scss +++ b/client/landing/stepper/declarative-flow/internals/global.scss @@ -73,6 +73,7 @@ button { .new-hosted-site, .import-hosted-site, .site-setup, +.onboarding.goals, .site-migration, .migration, .migration-signup, diff --git a/client/landing/stepper/declarative-flow/onboarding.ts b/client/landing/stepper/declarative-flow/onboarding.ts index f0cb1a221868a..e7cb9dc9c8a6f 100644 --- a/client/landing/stepper/declarative-flow/onboarding.ts +++ b/client/landing/stepper/declarative-flow/onboarding.ts @@ -1,4 +1,5 @@ -import { OnboardSelect } from '@automattic/data-stores'; +import { isEnabled } from '@automattic/calypso-config'; +import { OnboardSelect, Onboard } from '@automattic/data-stores'; import { ONBOARDING_FLOW } from '@automattic/onboarding'; import { useDispatch, useSelect } from '@wordpress/data'; import { addQueryArgs, getQueryArg, getQueryArgs, removeQueryArgs } from '@wordpress/url'; @@ -12,8 +13,12 @@ import { import { STEPPER_TRACKS_EVENT_STEP_NAV_SUBMIT } from '../constants'; import { ONBOARD_STORE } from '../stores'; import { stepsWithRequiredLogin } from '../utils/steps-with-required-login'; +import { useGoalsFirstExperiment } from './helpers/use-goals-first-experiment'; import { recordStepNavigation } from './internals/analytics/record-step-navigation'; -import { Flow, ProvidedDependencies } from './internals/types'; +import { STEPS } from './internals/steps'; +import { AssertConditionState, Flow, ProvidedDependencies } from './internals/types'; + +const SiteIntent = Onboard.SiteIntent; const clearUseMyDomainsQueryParams = ( currentStepSlug: string | undefined ) => { const isDomainsStep = currentStepSlug === 'domains'; @@ -32,7 +37,10 @@ const onboarding: Flow = { isSignupFlow: true, __experimentalUseBuiltinAuth: true, useSteps() { - return stepsWithRequiredLogin( [ + // We have already checked the value has loaded in useAssertConditions + const [ , isGoalsAtFrontExperiment ] = useGoalsFirstExperiment(); + + const steps = stepsWithRequiredLogin( [ { slug: 'domains', asyncComponent: () => import( './internals/steps-repository/unified-domains' ), @@ -54,6 +62,13 @@ const onboarding: Flow = { asyncComponent: () => import( './internals/steps-repository/processing-step' ), }, ] ); + + if ( isGoalsAtFrontExperiment ) { + // This step is not wrapped in `stepsWithRequiredLogin` + steps.unshift( STEPS.GOALS ); + } + + return steps; }, useStepNavigation( currentStepSlug, navigate ) { @@ -84,6 +99,29 @@ const onboarding: Flow = { const submit = async ( providedDependencies: ProvidedDependencies = {} ) => { switch ( currentStepSlug ) { + case 'goals': { + const { intent, skip } = providedDependencies; + + if ( skip ) { + // TODO Implement skipping to dashboard + return; + } + + switch ( intent ) { + case SiteIntent.Import: + // TODO Implement exit to site migration + return; + + case SiteIntent.DIFM: + // TODO Implement exit to DIFM + return; + + default: { + return navigate( 'domains' ); + } + } + } + case 'domains': setSiteUrl( providedDependencies.siteUrl ); setDomain( providedDependencies.suggestion ); @@ -164,8 +202,9 @@ const onboarding: Flow = { case 'create-site': return navigate( 'processing', undefined, true ); case 'processing': { - const destination = addQueryArgs( '/setup/site-setup/goals', { + const destination = addQueryArgs( '/setup/site-setup', { siteSlug: providedDependencies.siteSlug, + ...( isEnabled( 'onboarding/goals-first' ) && { flags: 'onboarding/goals-first' } ), } ); persistSignupDestination( destination ); setSignupCompleteFlowName( flowName ); @@ -224,6 +263,13 @@ const onboarding: Flow = { return { goBack, submit }; }, + useAssertConditions() { + const [ isLoading ] = useGoalsFirstExperiment(); + + return { + state: isLoading ? AssertConditionState.CHECKING : AssertConditionState.SUCCESS, + }; + }, }; export default onboarding; diff --git a/client/landing/stepper/declarative-flow/site-setup-flow.ts b/client/landing/stepper/declarative-flow/site-setup-flow.ts index 4a8569c4706b5..a1fd3d2c4fc83 100644 --- a/client/landing/stepper/declarative-flow/site-setup-flow.ts +++ b/client/landing/stepper/declarative-flow/site-setup-flow.ts @@ -20,6 +20,7 @@ import { useSiteData } from '../hooks/use-site-data'; import { useCanUserManageOptions } from '../hooks/use-user-can-manage-options'; import { ONBOARD_STORE, SITE_STORE, USER_STORE, STEPPER_INTERNAL_STORE } from '../stores'; import { shouldRedirectToSiteMigration } from './helpers'; +import { useGoalsFirstExperiment } from './helpers/use-goals-first-experiment'; import { useLaunchpadDecider } from './internals/hooks/use-launchpad-decider'; import { STEPS } from './internals/steps'; import { redirect } from './internals/steps-repository/import/util'; @@ -66,7 +67,10 @@ const siteSetupFlow: Flow = { }, useSteps() { - return [ + // We have already checked the value has loaded in useAssertConditions + const [ , isGoalsAtFrontExperiment ] = useGoalsFirstExperiment(); + + const steps = [ STEPS.GOALS, STEPS.INTENT, STEPS.OPTIONS, @@ -94,6 +98,14 @@ const siteSetupFlow: Flow = { STEPS.ERROR, STEPS.DIFM_STARTING_POINT, ]; + + if ( isGoalsAtFrontExperiment ) { + // The user has already seen the goals step in the `onboarding` flow + // TODO Ensure that DESIGN_CHOICES is at the front if the user is Big Sky eligible + steps.splice( 0, 4 ); + } + + return steps; }, useStepNavigation( currentStep, navigate ) { const isGoalsHoldout = useIsGoalsHoldout( currentStep ); @@ -719,6 +731,13 @@ const siteSetupFlow: Flow = { }; } + const [ isLoadingGoalsFirstExp ] = useGoalsFirstExperiment(); + if ( isLoadingGoalsFirstExp ) { + result = { + state: AssertConditionState.CHECKING, + }; + } + return result; }, }; diff --git a/client/landing/stepper/declarative-flow/test/site-setup-flow.tsx b/client/landing/stepper/declarative-flow/test/site-setup-flow.tsx index e5692c96b9d8f..fce970aeaa5c8 100644 --- a/client/landing/stepper/declarative-flow/test/site-setup-flow.tsx +++ b/client/landing/stepper/declarative-flow/test/site-setup-flow.tsx @@ -1,6 +1,7 @@ /** * @jest-environment jsdom */ +import { renderHook } from '@testing-library/react'; import { STEPS } from '../internals/steps'; import siteSetupFlow from '../site-setup-flow'; import { getFlowLocation, renderFlow } from './helpers'; @@ -27,9 +28,9 @@ describe( 'Site Setup Flow', () => { * It's totally fine to change this test if the flow changes. But please make sure to update and test the site-setup-wg accordingly. */ describe( 'First steps should be goals and intent capture', () => { - const steps = siteSetupFlow.useSteps(); - const firstStep = steps[ 0 ]; - const secondStep = steps[ 1 ]; + const { result } = renderHook( () => siteSetupFlow.useSteps() ); + const firstStep = result.current[ 0 ]; + const secondStep = result.current[ 1 ]; it( 'should be goals', () => { expect( firstStep.slug ).toBe( STEPS.GOALS.slug ); diff --git a/config/development.json b/config/development.json index 223bcf1b48356..5fd5523795259 100644 --- a/config/development.json +++ b/config/development.json @@ -170,6 +170,7 @@ "onboarding/trail-map-feature-grid-structure": false, "onboarding/trail-map-feature-grid": false, "onboarding/user-on-stepper-hosting": true, + "onboarding/goals-first": true, "p2/p2-plus": true, "p2-enabled": false, "page/export": true, diff --git a/config/stage.json b/config/stage.json index 9c7274294c7f4..87f92e6a6da08 100644 --- a/config/stage.json +++ b/config/stage.json @@ -138,6 +138,7 @@ "onboarding/trail-map-feature-grid-copy": false, "onboarding/trail-map-feature-grid-structure": false, "onboarding/trail-map-feature-grid": false, + "onboarding/goals-first": true, "p2/p2-plus": true, "p2-enabled": true, "page/export": true,