diff --git a/client/landing/stepper/README.md b/client/landing/stepper/README.md
index 8c26e646c0048d..d2ac8561c787cf 100644
--- a/client/landing/stepper/README.md
+++ b/client/landing/stepper/README.md
@@ -6,7 +6,7 @@ The stepper framework is a new framework for quickly spinning up sign-up flows.
## Non-linearity
-It has been tricky for us to create flows with input-driven steps configuration. Stepper makes it easy by allowing flows to create their own two hooks `useSteps`, and `useStepNavigation`. These hooks have access to the state of the flow so they can make decisions based on that.
+It has been tricky for us to create flows with input-driven steps configuration. Stepper makes it easy by using `useStepNavigation`. This hook has access to the state of the flow so it makes navigation decisions based on it at every stage of the flow.
### Example flow
@@ -15,8 +15,8 @@ import type { StepPath } from './internals/steps-repository';
import type { Flow } from './internals/types';
export const exampleFlow: Flow = {
- useSteps(): Array< StepPath > {
- return [ 'domain', 'design' ];
+ initialize() {
+ return [ STEPS.DOMAINS, STEPS.DESIGN ];
},
useStepNavigation( currentStep, navigate ) {
const goBack = () => {
@@ -34,14 +34,13 @@ export const exampleFlow: Flow = {
## The API
-To create a flow, you only have to implement `useSteps` and `useStepNavigation`. `useSteps` just returns an array of step keys, `useStepNavigation` is the engine where you make navigation decisions. This hook returns an object of type [`NavigationControls`](./declarative-flow/internals/types.ts):
+To create a flow, you only have to implement `initialize` and `useStepNavigation`. `initialize` can do any checks you need and it should finally return an array of step objects, `useStepNavigation` is the engine where you make navigation decisions. This hook returns an object of type [`NavigationControls`](./declarative-flow/internals/types.ts):
There is also an optional `useSideEffect` hook. You can implement this hook to run any side-effects to the flow. You can prefetch information, send track events when something changes, etc...
There is a required `isSignupFlow` flag that _MUST be `true` for signup flows_ (generally where a new site may be created), and should be `false` for other flows. The `isSignupFlow` flag controls whether we'll trigger a `calypso_signup_start` Tracks event when the flow starts. For signup flows, you can also supply additional event props to the `calypso_signup_start` event by implementing the optional `useTracksEventProps()` hook on the flow.
```tsx
-// prettier-ignore
/**
* This is the return type of useStepNavigation hook
*/
@@ -68,12 +67,14 @@ export type NavigationControls = {
Since this is a hook, it can access any state from any store, so you can make dynamic navigation decisions based on the state. [Here](./declarative-flow/site-setup-flow.ts) is a developed example of this hook.
```ts
-import type { StepPath } from './internals/steps-repository';
import type { Flow } from './internals/types';
export const exampleFlow: Flow = {
- useSteps(): Array< StepPath > {
- return [];
+ initialize() {
+ return [
+ STEPS.INTRO,
+ STEPS.DOMAINS
+ ];
},
useStepNavigation( currentStep, navigate ) {
return { goNext, goBack };
@@ -81,31 +82,6 @@ export const exampleFlow: Flow = {
};
```
-### Assert Conditions
-
-Optionally, you could also define a `useAssertConditions` function in the flow. This function can be used to add some conditions to check, and maybe return an error if those are not met.
-
-```ts
-import type { StepPath } from './internals/steps-repository';
-import type { Flow } from './internals/types';
-
-export const exampleFlow: Flow = {
- useSteps(): Array< StepPath > {
- return [];
- },
- useStepNavigation( currentStep, navigate ) {
- return { goNext, goBack };
- },
- useAssertConditions() {
- const siteSlug = useSiteSlugParam();
-
- if ( ! siteSlug ) {
- throw new Error( 'site-setup did not provide the site slug it is configured to.' );
- }
- },
-};
-```
-
## Reusability
Stepper aims to create a big `steps-repository` that contains the steps and allows them to be recycled and reused. Every step you create is inherently reusable by any future flow. Because steps are like components, they're not parts of the flows, flows just happen to use them.
diff --git a/client/landing/stepper/declarative-flow/example.ts b/client/landing/stepper/declarative-flow/example.ts
new file mode 100644
index 00000000000000..9f3fbad2fae1d4
--- /dev/null
+++ b/client/landing/stepper/declarative-flow/example.ts
@@ -0,0 +1,175 @@
+import { Onboard, OnboardActions, updateLaunchpadSettings } from '@automattic/data-stores';
+import { EXAMPLE_FLOW } from '@automattic/onboarding';
+import { dispatch } from '@wordpress/data';
+import { addQueryArgs } from '@wordpress/url';
+import { translate } from 'i18n-calypso';
+import { useLaunchpadDecider } from 'calypso/landing/stepper/declarative-flow/internals/hooks/use-launchpad-decider';
+import { useQuery } from 'calypso/landing/stepper/hooks/use-query';
+import { skipLaunchpad } from 'calypso/landing/stepper/utils/skip-launchpad';
+import { triggerGuidesForStep } from 'calypso/lib/guides/trigger-guides-for-step';
+import {
+ clearSignupDestinationCookie,
+ setSignupCompleteSlug,
+ persistSignupDestination,
+ setSignupCompleteFlowName,
+} from 'calypso/signup/storageUtils';
+import { useExitFlow } from '../hooks/use-exit-flow';
+import { useSiteIdParam } from '../hooks/use-site-id-param';
+import { useSiteSlug } from '../hooks/use-site-slug';
+import { ONBOARD_STORE } from '../stores';
+import { getQuery } from '../utils/get-query';
+import { stepsWithRequiredLogin } from '../utils/steps-with-required-login';
+import { STEPS } from './internals/steps';
+import { ProvidedDependencies } from './internals/types';
+import type { Flow } from './internals/types';
+
+const newsletter: Flow = {
+ name: EXAMPLE_FLOW,
+ get title() {
+ return translate( 'Newsletter Example Flow' );
+ },
+ isSignupFlow: true,
+ initialize() {
+ const query = getQuery();
+ const isComingFromMarketingPage = query[ 'ref' ] === 'newsletter-lp';
+
+ const { setHidePlansFeatureComparison, setIntent } = dispatch(
+ ONBOARD_STORE
+ ) as OnboardActions;
+
+ // We can just call these. They're guaranteed to run once.
+ setHidePlansFeatureComparison( true );
+ clearSignupDestinationCookie();
+ setIntent( Onboard.SiteIntent.Newsletter );
+
+ const privateSteps = stepsWithRequiredLogin( [
+ STEPS.NEWSLETTER_SETUP,
+ STEPS.NEWSLETTER_GOALS,
+ STEPS.DOMAINS,
+ STEPS.PLANS,
+ STEPS.PROCESSING,
+ STEPS.SUBSCRIBERS,
+ STEPS.SITE_CREATION_STEP,
+ STEPS.LAUNCHPAD,
+ ] );
+
+ if ( ! isComingFromMarketingPage ) {
+ return [ STEPS.INTRO, ...privateSteps ];
+ }
+
+ return privateSteps;
+ },
+
+ useStepNavigation( _currentStep, navigate ) {
+ const flowName = this.name;
+ const siteId = useSiteIdParam();
+ const siteSlug = useSiteSlug();
+ const query = useQuery();
+ const { exitFlow } = useExitFlow();
+ const isComingFromMarketingPage = query.get( 'ref' ) === 'newsletter-lp';
+
+ const { getPostFlowUrl, initializeLaunchpadState } = useLaunchpadDecider( {
+ exitFlow,
+ navigate,
+ } );
+
+ const completeSubscribersTask = async () => {
+ if ( siteSlug ) {
+ await updateLaunchpadSettings( siteSlug, {
+ checklist_statuses: { subscribers_added: true },
+ } );
+ }
+ };
+
+ triggerGuidesForStep( flowName, _currentStep );
+
+ function submit( providedDependencies: ProvidedDependencies = {} ) {
+ const launchpadUrl = `/setup/${ flowName }/launchpad?siteSlug=${ providedDependencies.siteSlug }`;
+
+ switch ( _currentStep ) {
+ case 'intro':
+ return navigate( 'newsletterSetup' );
+
+ case 'newsletterSetup':
+ return navigate( 'newsletterGoals' );
+
+ case 'newsletterGoals':
+ return navigate( 'domains' );
+
+ case 'domains':
+ return navigate( 'plans' );
+
+ case 'plans':
+ return navigate( 'createSite' );
+
+ case 'createSite':
+ return navigate( 'processing' );
+
+ case 'processing':
+ if ( providedDependencies?.goToHome && providedDependencies?.siteSlug ) {
+ return window.location.replace(
+ addQueryArgs( `/home/${ siteId ?? providedDependencies?.siteSlug }`, {
+ celebrateLaunch: true,
+ launchpadComplete: true,
+ } )
+ );
+ }
+
+ if ( providedDependencies?.goToCheckout && providedDependencies?.siteSlug ) {
+ persistSignupDestination( launchpadUrl );
+ setSignupCompleteSlug( providedDependencies?.siteSlug );
+ setSignupCompleteFlowName( flowName );
+
+ return window.location.assign(
+ `/checkout/${ encodeURIComponent(
+ providedDependencies?.siteSlug as string
+ ) }?redirect_to=${ encodeURIComponent( launchpadUrl ) }&signup=1`
+ );
+ }
+
+ initializeLaunchpadState( {
+ siteId: providedDependencies?.siteId as number,
+ siteSlug: providedDependencies?.siteSlug as string,
+ } );
+
+ return window.location.assign(
+ getPostFlowUrl( {
+ flow: flowName,
+ siteId: providedDependencies?.siteId as number,
+ siteSlug: providedDependencies?.siteSlug as string,
+ } )
+ );
+
+ case 'subscribers':
+ completeSubscribersTask();
+ return navigate( 'launchpad' );
+ }
+ }
+
+ const goBack = () => {
+ return;
+ };
+
+ const goNext = async () => {
+ switch ( _currentStep ) {
+ case 'launchpad':
+ skipLaunchpad( {
+ siteId,
+ siteSlug,
+ } );
+ return;
+
+ default:
+ return navigate( isComingFromMarketingPage ? 'newsletterSetup' : 'intro' );
+ }
+ };
+
+ const goToStep = ( step: string ) => {
+ navigate( step );
+ };
+
+ return { goNext, goBack, goToStep, submit };
+ },
+};
+
+export default newsletter;
diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-flow-navigation/index.tsx b/client/landing/stepper/declarative-flow/internals/hooks/use-flow-navigation/index.tsx
index 72e1c83ca0a179..d89ebda89658b0 100644
--- a/client/landing/stepper/declarative-flow/internals/hooks/use-flow-navigation/index.tsx
+++ b/client/landing/stepper/declarative-flow/internals/hooks/use-flow-navigation/index.tsx
@@ -46,8 +46,8 @@ export const useFlowNavigation = ( flow: Flow ): FlowNavigation => {
const match = useMatch( '/:flow/:step?/:lang?' );
const { step: currentStepSlug = null, lang = null } = match?.params || {};
const [ currentSearchParams ] = useSearchParams();
+ const steps = 'useSteps' in flow ? flow.useSteps() : flow.__flowSteps ?? [];
const flowName = flow.variantSlug ?? flow.name;
- const steps = flow.useSteps();
const isLoggedIn = useSelector( isUserLoggedIn );
const stepsSlugs = steps.map( ( step ) => step.slug );
const locale = useFlowLocale();
diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-step-navigation-with-tracking/index.ts b/client/landing/stepper/declarative-flow/internals/hooks/use-step-navigation-with-tracking/index.ts
index c761a7cc0790b6..385054639abc7b 100644
--- a/client/landing/stepper/declarative-flow/internals/hooks/use-step-navigation-with-tracking/index.ts
+++ b/client/landing/stepper/declarative-flow/internals/hooks/use-step-navigation-with-tracking/index.ts
@@ -25,7 +25,7 @@ export const useStepNavigationWithTracking = ( {
flow,
currentStepRoute,
navigate,
-}: Params< ReturnType< Flow[ 'useSteps' ] > > ) => {
+}: Params< StepperStep[] > ) => {
const stepNavigation = flow.useStepNavigation( currentStepRoute, navigate );
const { intent, goals } = useSelect( ( select ) => {
const onboardStore = select( ONBOARD_STORE ) as OnboardSelect;
diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx
index d3db2f074d1584..06fc89767b1b46 100644
--- a/client/landing/stepper/declarative-flow/internals/index.tsx
+++ b/client/landing/stepper/declarative-flow/internals/index.tsx
@@ -60,12 +60,18 @@ function flowStepComponent( flowStep: StepperStep | undefined ) {
* 3. It's responsive to the dynamic changes in side the flow's hooks (useSteps and useStepsNavigation)
* @param props
* @param props.flow the flow you want to render
+ * @param props.steps the steps of the flow.
* @returns A React router switch will all the routes
*/
-export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
+export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] | null } > = ( {
+ flow,
+ steps,
+} ) => {
// Configure app element that React Modal will aria-hide when modal is open
Modal.setAppElement( '#wpcom' );
- const flowSteps = flow.useSteps();
+ const deprecatedFlowSteps = 'useSteps' in flow ? flow.useSteps() : null;
+ const flowSteps = steps ?? deprecatedFlowSteps ?? [];
+
const stepPaths = flowSteps.map( ( step ) => step.slug );
const firstStepSlug = useFirstStep( stepPaths );
const { navigate, params } = useFlowNavigation( flow );
diff --git a/client/landing/stepper/declarative-flow/internals/steps.tsx b/client/landing/stepper/declarative-flow/internals/steps.tsx
index 7e5ab273be2dbc..e6a85e41a8ba83 100644
--- a/client/landing/stepper/declarative-flow/internals/steps.tsx
+++ b/client/landing/stepper/declarative-flow/internals/steps.tsx
@@ -50,6 +50,21 @@ export const STEPS = {
asyncComponent: () => import( './steps-repository/migration-error' ),
},
+ NEWSLETTER_SETUP: {
+ slug: 'newsletterSetup',
+ asyncComponent: () => import( './steps-repository/newsletter-setup' ),
+ },
+
+ NEWSLETTER_GOALS: {
+ slug: 'newsletterGoals',
+ asyncComponent: () => import( './steps-repository/newsletter-goals' ),
+ },
+
+ SUBSCRIBERS: {
+ slug: 'subscribers',
+ asyncComponent: () => import( './steps-repository/subscribers' ),
+ },
+
FREE_POST_SETUP: {
slug: 'freePostSetup',
asyncComponent: () => import( './steps-repository/free-post-setup' ),
@@ -124,6 +139,11 @@ export const STEPS = {
asyncComponent: () => import( './steps-repository/intent-step' ),
},
+ INTRO: {
+ slug: 'intro',
+ asyncComponent: () => import( './steps-repository/intro' ),
+ },
+
NEW_OR_EXISTING_SITE: {
slug: 'new-or-existing-site',
asyncComponent: () => import( './steps-repository/new-or-existing-site' ),
diff --git a/client/landing/stepper/declarative-flow/internals/types.ts b/client/landing/stepper/declarative-flow/internals/types.ts
index 3c39e23d6126e2..1d39d78bba71ce 100644
--- a/client/landing/stepper/declarative-flow/internals/types.ts
+++ b/client/landing/stepper/declarative-flow/internals/types.ts
@@ -85,7 +85,7 @@ export interface AsyncUserStep extends AsyncStepperStep {
export type StepperStep = DeprecatedStepperStep | AsyncStepperStep | AsyncUserStep;
-export type Navigate< FlowSteps extends StepperStep[] > = (
+export type Navigate< FlowSteps extends readonly StepperStep[] > = (
stepName: FlowSteps[ number ][ 'slug' ] | `${ FlowSteps[ number ][ 'slug' ] }?${ string }`,
extraData?: any,
/**
@@ -104,11 +104,11 @@ export type UseStepNavigationHook< FlowSteps extends StepperStep[] > = (
navigate: Navigate< FlowSteps >
) => NavigationControls;
-export type UseAssertConditionsHook< FlowSteps extends StepperStep[] > = (
+export type UseAssertConditionsHook< FlowSteps extends readonly StepperStep[] > = (
navigate?: Navigate< FlowSteps >
) => AssertConditionResult;
-export type UseSideEffectHook< FlowSteps extends StepperStep[] > = (
+export type UseSideEffectHook< FlowSteps extends readonly StepperStep[] > = (
currentStepSlug: FlowSteps[ number ][ 'slug' ],
navigate: Navigate< FlowSteps >
) => void;
@@ -127,7 +127,10 @@ export type UseTracksEventPropsHook = () => {
>;
};
-export type Flow = {
+/**
+ * @deprecated Use FlowV2 instead.
+ */
+export type FlowV1 = {
/**
* If this flag is set to true, the flow will login the user without leaving Stepper.
*/
@@ -155,13 +158,23 @@ export type Flow = {
customLoginPath?: string;
extraQueryParams?: Record< string, string | number >;
};
+ /**
+ * @deprecated use `initialize` method instead.
+ */
useSteps: UseStepsHook;
- useStepNavigation: UseStepNavigationHook< ReturnType< Flow[ 'useSteps' ] > >;
- useAssertConditions?: UseAssertConditionsHook< ReturnType< Flow[ 'useSteps' ] > >;
+ /**
+ * Use this method to define the steps of the flow and do any actions that need to run before the flow starts.
+ * This hook is called only once when the flow is mounted. It can be asynchronous if you would like to load an experiment or other data.
+ */
+ useStepNavigation: UseStepNavigationHook< StepperStep[] >;
+ /**
+ * @deprecated Use `initialize` instead. `initialize` will run before the flow is rendered and you can make any decisions there.
+ */
+ useAssertConditions?: UseAssertConditionsHook< ReturnType< FlowV1[ 'useSteps' ] > >;
/**
* A hook that is called in the flow's root at every render. You can use this hook to setup side-effects, call other hooks, etc..
*/
- useSideEffect?: UseSideEffectHook< ReturnType< Flow[ 'useSteps' ] > >;
+ useSideEffect?: UseSideEffectHook< ReturnType< FlowV1[ 'useSteps' ] > >;
useTracksEventProps?: UseTracksEventPropsHook;
/**
* Temporary hook to allow gradual migration of flows to the globalised/default event tracking.
@@ -170,6 +183,76 @@ export type Flow = {
use__Temporary__ShouldTrackEvent?: ( event: keyof NavigationControls ) => boolean;
};
+export type FlowV2 = {
+ /**
+ * If this flag is set to true, the flow will login the user without leaving Stepper.
+ */
+ __experimentalUseBuiltinAuth?: boolean;
+
+ /**
+ * The steps of the flow. **Please don't use this variable unless absolutely necessary**. It's meant to be used internally by the Stepper.
+ * Use `getSteps` instead.
+ */
+ __flowSteps?: readonly StepperStep[];
+
+ /**
+ * Use this method to retrieve the steps of the flow.
+ */
+ getSteps?(): readonly StepperStep[];
+
+ name: string;
+ /**
+ * If this flow extends another flow, the variant slug will be added as a class name to the root element of the flow.
+ */
+ variantSlug?: string;
+ title?: string;
+ classnames?: string | [ string ];
+ /**
+ * Required flag to indicate if the flow is a signup flow.
+ */
+ isSignupFlow: boolean;
+ /**
+ * You can use this hook to configure the login url.
+ * @returns An object describing the configuration.
+ * For now only extraQueryParams is supported.
+ */
+ useLoginParams?: () => {
+ /**
+ * A custom login path to use instead of the default login path.
+ */
+ customLoginPath?: string;
+ extraQueryParams?: Record< string, string | number >;
+ };
+ /**
+ * Use this method to define the steps of the flow and do any actions that need to run before the flow starts.
+ * This hook is called only once when the flow is mounted. It can be asynchronous if you would like to load an experiment or other data.
+ *
+ * Returning false will kill the app.
+ */
+ initialize():
+ | false
+ | Promise< false >
+ | Promise< readonly StepperStep[] >
+ | readonly StepperStep[];
+ useStepNavigation: UseStepNavigationHook< StepperStep[] >;
+ /**
+ * A hook that is called in the flow's root at every render. You can use this hook to setup side-effects, call other hooks, etc..
+ */
+ useSideEffect?: UseSideEffectHook< StepperStep[] >;
+ useTracksEventProps?: UseTracksEventPropsHook;
+ /**
+ * Temporary hook to allow gradual migration of flows to the globalised/default event tracking.
+ * IMPORTANT: This hook will be removed in the future.
+ */
+ use__Temporary__ShouldTrackEvent?: ( event: keyof NavigationControls ) => boolean;
+ /**
+ * @deprecated Avoid this. Assert your conditions in `initialize` instead unless you're 100% sure you need this.
+ */
+ useAssertConditions?: UseAssertConditionsHook< ReturnType< FlowV1[ 'useSteps' ] > >;
+};
+
+export type Flow = FlowV1 | FlowV2;
+
export type StepProps = {
navigation: NavigationControls;
stepName: string;
diff --git a/client/landing/stepper/declarative-flow/plugin-bundle-flow.ts b/client/landing/stepper/declarative-flow/plugin-bundle-flow.ts
index 11eef6d0ac8f04..7554eeec77293a 100644
--- a/client/landing/stepper/declarative-flow/plugin-bundle-flow.ts
+++ b/client/landing/stepper/declarative-flow/plugin-bundle-flow.ts
@@ -14,7 +14,7 @@ import { ProcessingResult } from './internals/steps-repository/processing-step/c
import {
AssertConditionResult,
AssertConditionState,
- Flow,
+ FlowV1,
ProvidedDependencies,
StepperStep,
} from './internals/types';
@@ -36,7 +36,7 @@ const getNextStep = ( currentStep: string, steps: StepperStep[] ): string | unde
const SiteIntent = Onboard.SiteIntent;
-const pluginBundleFlow: Flow = {
+const pluginBundleFlow: FlowV1 = {
name: 'plugin-bundle',
isSignupFlow: false,
diff --git a/client/landing/stepper/declarative-flow/registered-flows.ts b/client/landing/stepper/declarative-flow/registered-flows.ts
index 371eadd1971a4d..fdd6212ed964c9 100644
--- a/client/landing/stepper/declarative-flow/registered-flows.ts
+++ b/client/landing/stepper/declarative-flow/registered-flows.ts
@@ -18,6 +18,7 @@ import {
NEW_HOSTED_SITE_FLOW_USER_INCLUDED,
ONBOARDING_FLOW,
HUNDRED_YEAR_DOMAIN_FLOW,
+ EXAMPLE_FLOW,
} from '@automattic/onboarding';
import type { Flow } from '../declarative-flow/internals/types';
@@ -135,6 +136,8 @@ const availableFlows: Record< string, () => Promise< { default: Flow } > > = {
),
[ MIGRATION_FLOW ]: () =>
import( /* webpackChunkName: "migration-flow" */ '../declarative-flow/migration' ),
+ [ EXAMPLE_FLOW ]: () =>
+ import( /* webpackChunkName: "example-flow" */ '../declarative-flow/example' ),
};
const hostedSiteMigrationFlow: Record< string, () => Promise< { default: Flow } > > = {
diff --git a/client/landing/stepper/declarative-flow/site-setup-flow.ts b/client/landing/stepper/declarative-flow/site-setup-flow.ts
index e5c1ef093134f7..7b008406796393 100644
--- a/client/landing/stepper/declarative-flow/site-setup-flow.ts
+++ b/client/landing/stepper/declarative-flow/site-setup-flow.ts
@@ -30,7 +30,7 @@ import { ProcessingResult } from './internals/steps-repository/processing-step/c
import {
AssertConditionResult,
AssertConditionState,
- Flow,
+ FlowV1,
ProvidedDependencies,
} from './internals/types';
import type { OnboardSelect, SiteSelect, UserSelect } from '@automattic/data-stores';
@@ -52,7 +52,7 @@ function useGoalsAtFrontExperimentQueryParam() {
return Boolean( useSelector( getInitialQueryArguments )?.[ 'goals-at-front-experiment' ] );
}
-const siteSetupFlow: Flow = {
+const siteSetupFlow: FlowV1 = {
name: 'site-setup',
isSignupFlow: false,
diff --git a/client/landing/stepper/declarative-flow/site-setup-wg-flow.ts b/client/landing/stepper/declarative-flow/site-setup-wg-flow.ts
index 8062fede5e7dcd..ea1869757897af 100644
--- a/client/landing/stepper/declarative-flow/site-setup-wg-flow.ts
+++ b/client/landing/stepper/declarative-flow/site-setup-wg-flow.ts
@@ -3,7 +3,7 @@ import { useDispatch } from '@wordpress/data';
import { useEffect } from 'react';
import { useSiteData } from '../hooks/use-site-data';
import { ONBOARD_STORE } from '../stores';
-import { Flow } from './internals/types';
+import { FlowV1 } from './internals/types';
import siteSetup from './site-setup-flow';
const { goalsToIntent } = Onboard.utils;
@@ -11,7 +11,7 @@ const { goalsToIntent } = Onboard.utils;
/**
* A variant of site-setup flow without goals step.
*/
-const siteSetupWithoutGoalsFlow: Flow = {
+const siteSetupWithoutGoalsFlow: FlowV1 = {
...siteSetup,
variantSlug: 'site-setup-wg',
useSteps() {
diff --git a/client/landing/stepper/index.tsx b/client/landing/stepper/index.tsx
index 942da0f7bf6c8c..8502ca17978e68 100644
--- a/client/landing/stepper/index.tsx
+++ b/client/landing/stepper/index.tsx
@@ -3,7 +3,7 @@ import accessibleFocus from '@automattic/accessible-focus';
import { initializeAnalytics } from '@automattic/calypso-analytics';
import { CurrentUser } from '@automattic/calypso-analytics/dist/types/utils/current-user';
import config from '@automattic/calypso-config';
-import { User as UserStore } from '@automattic/data-stores';
+import { UserActions, User as UserStore } from '@automattic/data-stores';
import { geolocateCurrencySymbol } from '@automattic/format-currency';
import {
HOSTED_SITE_MIGRATION_FLOW,
@@ -12,7 +12,7 @@ import {
SITE_MIGRATION_FLOW,
} from '@automattic/onboarding';
import { QueryClientProvider } from '@tanstack/react-query';
-import { useDispatch } from '@wordpress/data';
+import { dispatch } from '@wordpress/data';
import defaultCalypsoI18n from 'i18n-calypso';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
@@ -41,12 +41,12 @@ import 'calypso/assets/stylesheets/style.scss';
import availableFlows from './declarative-flow/registered-flows';
import { USER_STORE } from './stores';
import { setupWpDataDebug } from './utils/devtools';
-import { enhanceFlowWithAuth } from './utils/enhanceFlowWithAuth';
+import { enhanceFlowWithUtilityFunctions } from './utils/enhance-flow-with-utils';
+import { enhanceFlowWithAuth, injectUserStepInSteps } from './utils/enhanceFlowWithAuth';
import redirectPathIfNecessary from './utils/flow-redirect-handler';
import { getFlowFromURL } from './utils/get-flow-from-url';
import { startStepperPerformanceTracking } from './utils/performance-tracking';
import { WindowLocaleEffectManager } from './utils/window-locale-effect-manager';
-import type { Flow } from './declarative-flow/internals/types';
import type { AnyAction } from 'redux';
declare const window: AppWindow;
@@ -61,19 +61,6 @@ function determineFlow() {
return availableFlows[ flowNameFromPathName ] || availableFlows[ 'site-setup' ];
}
-
-/**
- * TODO: this is no longer a switch and should be removed
- */
-const FlowSwitch: React.FC< { user: UserStore.CurrentUser | undefined; flow: Flow } > = ( {
- user,
- flow,
-} ) => {
- const { receiveCurrentUser } = useDispatch( USER_STORE );
- user && receiveCurrentUser( user as UserStore.CurrentUser );
-
- return ;
-};
interface AppWindow extends Window {
BUILD_TARGET: string;
}
@@ -141,15 +128,39 @@ window.AppBoot = async () => {
setStore( reduxStore, getStateFromCache( userId ) );
onDisablePersistence( persistOnChange( reduxStore, userId ) );
setupLocale( user, reduxStore );
+ const { receiveCurrentUser } = dispatch( USER_STORE ) as UserActions;
- user && initializeCalypsoUserStore( reduxStore, user as CurrentUser );
+ if ( user ) {
+ initializeCalypsoUserStore( reduxStore, user as CurrentUser );
+ receiveCurrentUser( user as UserStore.CurrentUser );
+ }
initializeAnalytics( user, getSuperProps( reduxStore ) );
setupErrorLogger( reduxStore );
- const { default: rawFlow } = await flowPromise;
- const flow = rawFlow.__experimentalUseBuiltinAuth ? enhanceFlowWithAuth( rawFlow ) : rawFlow;
+ let { default: flow } = await flowPromise;
+ let flowSteps = 'initialize' in flow ? await flow.initialize() : null;
+
+ /**
+ * When `initialize` returns false, it means the app should be killed (the user probably issued a redirect).
+ */
+ if ( flowSteps === false ) {
+ return;
+ }
+
+ // Checking for initialize implies this is a V2 flow.
+ // CLEAN UP: once the `onboarding` flow is migrated to V2, this can be cleaned up to only support V2
+ // The `onboarding` flow is the only flow that uses in-stepper auth so far, so all the auth logic catering V1 can be deleted.
+ if ( 'initialize' in flow && flowSteps ) {
+ // Cache the flow steps for later internal usage. We need to cache them because we promise to call `initialize` only once.
+ flowSteps = injectUserStepInSteps( flowSteps );
+ flow.__flowSteps = flowSteps;
+ enhanceFlowWithUtilityFunctions( flow );
+ } else if ( 'useSteps' in flow ) {
+ // V1 flows have to be enhanced by changing their `useSteps` hook.
+ flow = enhanceFlowWithAuth( flow );
+ }
// When re-using steps from /start, we need to set the current flow name in the redux store, since some depend on it.
reduxStore.dispatch( setCurrentFlowName( flow.name ) );
@@ -166,7 +177,7 @@ window.AppBoot = async () => {
-
+
{ config.isEnabled( 'cookie-banner' ) && (
) }
diff --git a/client/landing/stepper/utils/enhance-flow-with-utils.ts b/client/landing/stepper/utils/enhance-flow-with-utils.ts
new file mode 100644
index 00000000000000..0c5a73e454ad58
--- /dev/null
+++ b/client/landing/stepper/utils/enhance-flow-with-utils.ts
@@ -0,0 +1,13 @@
+import type { FlowV2 } from '../declarative-flow/internals/types';
+
+/**
+ * Add utility functions to the flow object. This frees the API consumers from making these functions themselves.
+ * @param flow the flow.
+ * @returns the enhanced flow.
+ */
+export function enhanceFlowWithUtilityFunctions( flow: FlowV2 ): FlowV2 {
+ flow.getSteps = () => {
+ return flow.__flowSteps ?? [];
+ };
+ return flow;
+}
diff --git a/client/landing/stepper/utils/enhanceFlowWithAuth.ts b/client/landing/stepper/utils/enhanceFlowWithAuth.ts
index e34d63d970f3fa..95670aa65c3ab3 100644
--- a/client/landing/stepper/utils/enhanceFlowWithAuth.ts
+++ b/client/landing/stepper/utils/enhanceFlowWithAuth.ts
@@ -1,8 +1,12 @@
import { PRIVATE_STEPS } from '../declarative-flow/internals/steps';
-import type { Flow, StepperStep } from '../declarative-flow/internals/types';
+import type { FlowV1, StepperStep } from '../declarative-flow/internals/types';
-function useInjectUserStepIfNeeded( flow: Flow ): StepperStep[] {
+function useInjectUserStepIfNeededForV1( flow: FlowV1 ): readonly StepperStep[] {
const steps = flow.useSteps();
+ return injectUserStepInSteps( steps );
+}
+
+export function injectUserStepInSteps( steps: readonly StepperStep[] ) {
const firstAuthWalledStep = steps.findIndex( ( step ) => step.requiresLoggedInUser );
if ( firstAuthWalledStep === -1 ) {
@@ -16,9 +20,12 @@ function useInjectUserStepIfNeeded( flow: Flow ): StepperStep[] {
return [ ...steps, PRIVATE_STEPS.USER ];
}
-export function enhanceFlowWithAuth( flow: Flow ): Flow {
+/**
+ * @deprecated should be removed once #97999 is merged and all flows are migrated to V2.
+ */
+export function enhanceFlowWithAuth( flow: FlowV1 ): FlowV1 {
return {
...flow,
- useSteps: () => useInjectUserStepIfNeeded( flow ),
+ useSteps: () => useInjectUserStepIfNeededForV1( flow ) as StepperStep[],
};
}
diff --git a/client/landing/stepper/utils/get-query.ts b/client/landing/stepper/utils/get-query.ts
new file mode 100644
index 00000000000000..54c9b4c3bb628c
--- /dev/null
+++ b/client/landing/stepper/utils/get-query.ts
@@ -0,0 +1,7 @@
+/**
+ * Parses and returns the query parameters from the URL
+ * @returns An object with the query parameters
+ */
+export const getQuery = () => {
+ return Object.fromEntries( new URLSearchParams( window.location.search ) );
+};
diff --git a/packages/onboarding/src/utils/flows.ts b/packages/onboarding/src/utils/flows.ts
index b1d906d8494029..0cbfce57d87a72 100644
--- a/packages/onboarding/src/utils/flows.ts
+++ b/packages/onboarding/src/utils/flows.ts
@@ -40,6 +40,7 @@ export const BLOG_FLOW = 'blog';
export const REBLOGGING_FLOW = 'reblogging';
export const DOMAIN_FOR_GRAVATAR_FLOW = 'domain-for-gravatar';
export const ONBOARDING_FLOW = 'onboarding';
+export const EXAMPLE_FLOW = 'example';
export const ONBOARDING_GUIDED_FLOW = '__disabled_onboarding';
export const isLinkInBioFlow = ( flowName: string | null | undefined ) => {