From af21617da942ba57d38c4f47416deb363efc783c Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Wed, 5 Feb 2025 16:05:28 +0100 Subject: [PATCH 1/7] Prelaod the step after the user step --- client/landing/stepper/declarative-flow/internals/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx index 06fc89767b1b46..d42160632ee58c 100644 --- a/client/landing/stepper/declarative-flow/internals/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/index.tsx @@ -112,11 +112,7 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] // potentially slow that down by having the CPU busy initialising future steps. return; } - if ( - // Don't load anything on user step because the user step will hard-navigate anyways. - currentStepRoute !== 'user' && - 'asyncComponent' in nextStep - ) { + if ( 'asyncComponent' in nextStep ) { nextStep.asyncComponent(); } // Most flows sadly instantiate a new steps array on every call to `flow.useSteps()`, From 815125b23c36ddc7efd6a3dba74e506b91a4626c Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Wed, 5 Feb 2025 22:28:35 +0100 Subject: [PATCH 2/7] More performance fine-tuning --- .../components/help-center/async.tsx | 12 +- .../internals/hooks/use-preload-steps.ts | 67 ++++ .../declarative-flow/internals/index.tsx | 40 +- .../declarative-flow/internals/steps.tsx | 347 ++++++++++++++---- .../stepper/declarative-flow/onboarding.ts | 25 +- .../landing/stepper/hooks/use-first-step.ts | 5 +- 6 files changed, 368 insertions(+), 128 deletions(-) create mode 100644 client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts diff --git a/client/landing/stepper/declarative-flow/internals/components/help-center/async.tsx b/client/landing/stepper/declarative-flow/internals/components/help-center/async.tsx index 56142895c1720b..2099134c6ff2b1 100644 --- a/client/landing/stepper/declarative-flow/internals/components/help-center/async.tsx +++ b/client/landing/stepper/declarative-flow/internals/components/help-center/async.tsx @@ -1,17 +1,27 @@ -import { HelpCenter, User as UserStore } from '@automattic/data-stores'; +import { HelpCenter } from '@automattic/data-stores'; import { useDispatch } from '@wordpress/data'; import { useCallback } from 'react'; import AsyncLoad from 'calypso/components/async-load'; +import { useSelector } from 'calypso/state'; +import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; +import type { User as UserStore } from '@automattic/data-stores'; const HELP_CENTER_STORE = HelpCenter.register(); const AsyncHelpCenter: React.FC< { user: UserStore.CurrentUser | undefined } > = ( { user } ) => { + const isLoggedIn = useSelector( isUserLoggedIn ); + const { setShowHelpCenter } = useDispatch( HELP_CENTER_STORE ); const handleClose = useCallback( () => { setShowHelpCenter( false ); }, [ setShowHelpCenter ] ); + // The Help Center only works if you're logged in. Don't waste time loading it if you're not. + if ( ! isLoggedIn ) { + return null; + } + /** * The stepper query parameter ensures Webpack treats this Help Center as separate from the one in the main client app. * Without it, Webpack would create one shared chunk, loaded in both apps. Since Stepper is smaller, more CSS would diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts new file mode 100644 index 00000000000000..0a0ffe95085210 --- /dev/null +++ b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts @@ -0,0 +1,67 @@ +import { SiteDetails } from '@automattic/data-stores'; +import debugFactory from 'debug'; +import { useEffect } from 'react'; +import { Flow, StepperStep } from '../types'; + +const debug = debugFactory( 'calypso:stepper:flow-initialization' ); + +async function tryPreload( step?: StepperStep, followingStep?: StepperStep ) { + if ( step && 'asyncComponent' in step ) { + debug( 'Preloading next step:', step.slug ); + + await step.asyncComponent(); + + // Flows are indeterminate, they often pick one of the two next steps based on user input, so load two steps ahead. + tryPreload( followingStep ); + } +} + +/** + * Preload the next step in the flow, if it's an async component. + * + * @param siteSlugOrId The site slug or ID. + * @param selectedSite The selected site. + * @param currentStepRoute The current step route. + * @param flowSteps The flow steps. + * @param flow The flow. + */ +export function usePreloadSteps( + siteSlugOrId: string | number, + selectedSite: SiteDetails | undefined | null, + currentStepRoute: string, + flowSteps: readonly StepperStep[], + flow: Flow +) { + useEffect( () => { + if ( siteSlugOrId && ! selectedSite ) { + // If this step depends on a selected site, only preload after we have the data. + // Otherwise, we're still waiting to render something meaningful, and we don't want to + // potentially slow that down by having the CPU busy initialising future steps. + return; + } + if ( currentStepRoute ) { + // The user step is a special case, as it's not part of the flow steps. It always comes in the end of the steps array. + if ( currentStepRoute === 'user' ) { + const nextStep = flowSteps[ 0 ]; + const nextNextStep = flowSteps[ 1 ]; + tryPreload( nextStep, nextNextStep ); + } else { + const nextStepIndex = flowSteps.findIndex( ( step ) => step.slug === currentStepRoute ) + 1; + const nextNextStepIndex = nextStepIndex + 1; + + const nextStep = flowSteps[ nextStepIndex ]; + const nextNextStep = flowSteps[ nextNextStepIndex ]; + + tryPreload( nextStep, nextNextStep ); + } + } + // Most flows sadly instantiate a new steps array on every call to `flow.useSteps()`, + // which means that we don't want to depend on `flowSteps` here, or this would end up + // running on every render. We thus depend on `flow` instead. + // + // This should be safe, because flows shouldn't return different lists of steps at + // different points. But even if they do, worst case scenario we only fail to preload + // some steps, and they'll simply be loaded later. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ siteSlugOrId, selectedSite, currentStepRoute, flow ] ); +} diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx index d42160632ee58c..b95b853498ee10 100644 --- a/client/landing/stepper/declarative-flow/internals/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/index.tsx @@ -21,6 +21,7 @@ import { Boot } from './components/boot'; import { RedirectToStep } from './components/redirect-to-step'; import { useFlowAnalytics } from './hooks/use-flow-analytics'; import { useFlowNavigation } from './hooks/use-flow-navigation'; +import { usePreloadSteps } from './hooks/use-preload-steps'; import { useSignUpStartTracking } from './hooks/use-sign-up-start-tracking'; import { useStepNavigationWithTracking } from './hooks/use-step-navigation-with-tracking'; import { PRIVATE_STEPS } from './steps'; @@ -97,33 +98,7 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] const selectedSite = useSelector( ( state ) => site && getSite( state, siteSlugOrId ) ); // this pre-loads the next step in the flow. - useEffect( () => { - const nextStepIndex = flowSteps.findIndex( ( step ) => step.slug === currentStepRoute ) + 1; - const nextStep = flowSteps[ nextStepIndex ]; - - // 0 implies the findIndex returned -1. - if ( nextStepIndex === 0 || ! nextStep ) { - return; - } - - if ( siteSlugOrId && ! selectedSite ) { - // If this step depends on a selected site, only preload after we have the data. - // Otherwise, we're still waiting to render something meaningful, and we don't want to - // potentially slow that down by having the CPU busy initialising future steps. - return; - } - if ( 'asyncComponent' in nextStep ) { - nextStep.asyncComponent(); - } - // Most flows sadly instantiate a new steps array on every call to `flow.useSteps()`, - // which means that we don't want to depend on `flowSteps` here, or this would end up - // running on every render. We thus depend on `flow` instead. - // - // This should be safe, because flows shouldn't return different lists of steps at - // different points. But even if they do, worst case scenario we only fail to preload - // some steps, and they'll simply be loaded later. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ siteSlugOrId, selectedSite, currentStepRoute, flow ] ); + usePreloadSteps( siteSlugOrId, selectedSite, currentStepRoute, flowSteps, flow ); const stepNavigation = useStepNavigationWithTracking( { flow, @@ -164,8 +139,8 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] } // The `nextStep` is available only when logged-out users go to the step that requires auth - // and are redirected to the user step. - const postAuthStepSlug = stepData?.nextStep ?? ''; + // and are redirected to the user step. When it's not available, go to the first step. + const postAuthStepSlug = stepData?.nextStep ?? stepPaths[ 0 ]; if ( step.slug === PRIVATE_STEPS.USER.slug && postAuthStepSlug ) { const previousAuthStepSlug = stepData?.previousStep; const postAuthStepPath = generatePath( '/setup/:flow/:step/:lang?', { @@ -201,13 +176,6 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] ); } - if ( step.slug === PRIVATE_STEPS.USER.slug ) { - // eslint-disable-next-line no-console - console.warn( - 'Please define the next step after auth explicitly as we cannot find the user step automatically.' - ); - } - return ( import( './steps-repository/blogger-starting-point' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-blogger-starting-point' */ './steps-repository/blogger-starting-point' + ), }, BUSINESS_INFO: { slug: 'businessInfo', - asyncComponent: () => import( './steps-repository/business-info' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-business-info' */ './steps-repository/business-info' + ), }, CELEBRATION: { slug: 'celebration-step', - asyncComponent: () => import( './steps-repository/celebration-step' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-celebration-step' */ './steps-repository/celebration-step' + ), }, CHECK_SITES: { slug: 'check-sites', - asyncComponent: () => import( './steps-repository/sites-checker' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-sites-checker' */ './steps-repository/sites-checker' + ), }, - COURSES: { slug: 'courses', asyncComponent: () => import( './steps-repository/courses' ) }, + COURSES: { + slug: 'courses', + asyncComponent: () => + import( /* webpackChunkName: 'async-step-courses' */ './steps-repository/courses' ), + }, DESIGN_CHOICES: { slug: 'design-choices', - asyncComponent: () => import( './steps-repository/design-choices' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-design-choices' */ './steps-repository/design-choices' + ), }, DESIGN_SETUP: { slug: 'designSetup', - asyncComponent: () => import( './steps-repository/design-setup' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-design-setup' */ './steps-repository/design-setup' ), }, DIFM_STARTING_POINT: { slug: 'difmStartingPoint', - asyncComponent: () => import( './steps-repository/difm-starting-point' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-difm-starting-point' */ './steps-repository/difm-starting-point' + ), }, DOMAINS: { slug: 'domains', - asyncComponent: () => import( './steps-repository/domains' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-domains' */ './steps-repository/domains' ), }, - ERROR: { slug: 'error', asyncComponent: () => import( './steps-repository/error-step' ) }, + ERROR: { + slug: 'error', + asyncComponent: () => + import( /* webpackChunkName: 'async-step-error-step' */ './steps-repository/error-step' ), + }, MIGRATION_ERROR: { slug: 'error', - asyncComponent: () => import( './steps-repository/migration-error' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-migration-error' */ './steps-repository/migration-error' + ), }, NEWSLETTER_SETUP: { slug: 'newsletterSetup', - asyncComponent: () => import( './steps-repository/newsletter-setup' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-newsletter-setup' */ './steps-repository/newsletter-setup' + ), }, NEWSLETTER_GOALS: { slug: 'newsletterGoals', - asyncComponent: () => import( './steps-repository/newsletter-goals' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-newsletter-goals' */ './steps-repository/newsletter-goals' + ), }, SUBSCRIBERS: { slug: 'subscribers', - asyncComponent: () => import( './steps-repository/subscribers' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-subscribers' */ './steps-repository/subscribers' ), }, FREE_POST_SETUP: { slug: 'freePostSetup', - asyncComponent: () => import( './steps-repository/free-post-setup' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-free-post-setup' */ './steps-repository/free-post-setup' + ), }, - GOALS: { slug: 'goals', asyncComponent: () => import( './steps-repository/goals' ) }, + GOALS: { + slug: 'goals', + asyncComponent: () => + import( /* webpackChunkName: 'async-step-goals' */ './steps-repository/goals' ), + }, GENERATE_CONTENT: { slug: 'generateContent', - asyncComponent: () => import( './steps-repository/readymade-template-generate-content' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-readymade-template-generate-content' */ './steps-repository/readymade-template-generate-content' + ), }, - IMPORT: { slug: 'import', asyncComponent: () => import( './steps-repository/import' ) }, + IMPORT: { + slug: 'import', + asyncComponent: () => + import( /* webpackChunkName: 'async-step-import' */ './steps-repository/import' ), + }, IMPORT_LIGHT: { slug: 'importLight', - asyncComponent: () => import( './steps-repository/import-light' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-import-light' */ './steps-repository/import-light' ), }, IMPORT_LIST: { slug: 'importList', - asyncComponent: () => import( './steps-repository/import-list' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-import-list' */ './steps-repository/import-list' ), }, IMPORT_READY: { slug: 'importReady', - asyncComponent: () => import( './steps-repository/import-ready' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-import-ready' */ './steps-repository/import-ready' ), }, IMPORT_READY_NOT: { slug: 'importReadyNot', - asyncComponent: () => import( './steps-repository/import-ready-not' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-import-ready-not' */ './steps-repository/import-ready-not' + ), }, IMPORT_READY_PREVIEW: { slug: 'importReadyPreview', - asyncComponent: () => import( './steps-repository/import-ready-preview' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-import-ready-preview' */ './steps-repository/import-ready-preview' + ), }, IMPORT_READY_WPCOM: { slug: 'importReadyWpcom', - asyncComponent: () => import( './steps-repository/import-ready-wpcom' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-import-ready-wpcom' */ './steps-repository/import-ready-wpcom' + ), }, IMPORTER_BLOGGER: { slug: 'importerBlogger', - asyncComponent: () => import( './steps-repository/importer-blogger' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-importer-blogger' */ './steps-repository/importer-blogger' + ), }, IMPORTER_MEDIUM: { slug: 'importerMedium', - asyncComponent: () => import( './steps-repository/importer-medium' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-importer-medium' */ './steps-repository/importer-medium' + ), }, IMPORTER_SQUARESPACE: { slug: 'importerSquarespace', - asyncComponent: () => import( './steps-repository/importer-squarespace' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-importer-squarespace' */ './steps-repository/importer-squarespace' + ), }, IMPORTER_WIX: { slug: 'importerWix', - asyncComponent: () => import( './steps-repository/importer-wix' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-importer-wix' */ './steps-repository/importer-wix' ), }, IMPORTER_WORDPRESS: { slug: 'importerWordpress', - asyncComponent: () => import( './steps-repository/importer-wordpress' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-importer-wordpress' */ './steps-repository/importer-wordpress' + ), }, INTENT: { slug: 'intent', - asyncComponent: () => import( './steps-repository/intent-step' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-intent-step' */ './steps-repository/intent-step' ), }, INTRO: { slug: 'intro', - asyncComponent: () => import( './steps-repository/intro' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-intro' */ './steps-repository/intro' ), }, NEW_OR_EXISTING_SITE: { slug: 'new-or-existing-site', - asyncComponent: () => import( './steps-repository/new-or-existing-site' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-new-or-existing-site' */ './steps-repository/new-or-existing-site' + ), }, LAUNCH_BIG_SKY: { slug: 'launch-big-sky', - asyncComponent: () => import( './steps-repository/launch-big-sky' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-launch-big-sky' */ './steps-repository/launch-big-sky' + ), }, - LAUNCHPAD: { slug: 'launchpad', asyncComponent: () => import( './steps-repository/launchpad' ) }, + LAUNCHPAD: { + slug: 'launchpad', + asyncComponent: () => + import( /* webpackChunkName: 'async-step-launchpad' */ './steps-repository/launchpad' ), + }, MIGRATION_HANDLER: { slug: 'migrationHandler', - asyncComponent: () => import( './steps-repository/migration-handler' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-migration-handler' */ './steps-repository/migration-handler' + ), }, OPTIONS: { slug: 'options', - asyncComponent: () => import( './steps-repository/site-options' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-site-options' */ './steps-repository/site-options' ), }, - PLANS: { slug: 'plans', asyncComponent: () => import( './steps-repository/plans' ) }, + PLANS: { + slug: 'plans', + asyncComponent: () => + import( /* webpackChunkName: 'async-step-plans' */ './steps-repository/plans' ), + }, PROCESSING: { slug: 'processing', - asyncComponent: () => import( './steps-repository/processing-step' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-processing-step' */ './steps-repository/processing-step' + ), }, SITE_CREATION_STEP: { slug: 'create-site', - asyncComponent: () => import( './steps-repository/create-site' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-create-site' */ './steps-repository/create-site' ), }, SITE_LAUNCH: { slug: 'site-launch', - asyncComponent: () => import( './steps-repository/site-launch' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-site-launch' */ './steps-repository/site-launch' ), }, SITE_PICKER: { slug: 'site-picker', - asyncComponent: () => import( './steps-repository/site-picker-list' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-picker-list' */ './steps-repository/site-picker-list' + ), }, STORE_ADDRESS: { slug: 'storeAddress', - asyncComponent: () => import( './steps-repository/store-address' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-store-address' */ './steps-repository/store-address' + ), }, TRIAL_ACKNOWLEDGE: { slug: 'trialAcknowledge', - asyncComponent: () => import( './steps-repository/trial-acknowledge' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-trial-acknowledge' */ './steps-repository/trial-acknowledge' + ), }, VERIFY_EMAIL: { slug: 'verifyEmail', - asyncComponent: () => import( './steps-repository/import-verify-email' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-import-verify-email' */ './steps-repository/import-verify-email' + ), }, BUNDLE_CONFIRM: { slug: 'bundleConfirm', - asyncComponent: () => import( './steps-repository/bundle-confirm' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-bundle-confirm' */ './steps-repository/bundle-confirm' + ), }, BUNDLE_INSTALL_PLUGINS: { slug: 'bundleInstallPlugins', - asyncComponent: () => import( './steps-repository/bundle-install-plugins' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-bundle-install-plugins' */ './steps-repository/bundle-install-plugins' + ), }, BUNDLE_TRANSFER: { slug: 'bundleTransfer', - asyncComponent: () => import( './steps-repository/bundle-transfer' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-bundle-transfer' */ './steps-repository/bundle-transfer' + ), }, WAIT_FOR_ATOMIC: { slug: 'waitForAtomic', - asyncComponent: () => import( './steps-repository/wait-for-atomic' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-wait-for-atomic' */ './steps-repository/wait-for-atomic' + ), }, WAIT_FOR_PLUGIN_INSTALL: { slug: 'waitForPluginInstall', - asyncComponent: () => import( './steps-repository/wait-for-plugin-install' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-wait-for-plugin-install' */ './steps-repository/wait-for-plugin-install' + ), }, ASSIGN_TRIAL_PLAN: { slug: 'assignTrialPlan', - asyncComponent: () => import( './steps-repository/assign-trial-plan' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-assign-trial-plan' */ './steps-repository/assign-trial-plan' + ), }, SITE_MIGRATION_ASSIGN_TRIAL_PLAN: { slug: 'site-migration-assign-trial-plan', - asyncComponent: () => import( './steps-repository/site-migration-assign-trial-plan' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-assign-trial-plan' */ './steps-repository/site-migration-assign-trial-plan' + ), }, SITE_MIGRATION_INSTRUCTIONS: { slug: 'site-migration-instructions', - asyncComponent: () => import( './steps-repository/site-migration-instructions' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-instructions' */ './steps-repository/site-migration-instructions' + ), }, SITE_MIGRATION_STARTED: { slug: 'site-migration-started', - asyncComponent: () => import( './steps-repository/site-migration-started' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-started' */ './steps-repository/site-migration-started' + ), }, SITE_MIGRATION_ASSISTED_MIGRATION: { slug: 'migrateMessage', - asyncComponent: () => import( './steps-repository/importer-migrate-message' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-importer-migrate-message' */ './steps-repository/importer-migrate-message' + ), }, SITE_MIGRATION_CREDENTIALS: { slug: 'site-migration-credentials', - asyncComponent: () => import( './steps-repository/site-migration-credentials' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-credentials' */ './steps-repository/site-migration-credentials' + ), }, SITE_MIGRATION_FALLBACK_CREDENTIALS: { slug: 'site-migration-fallback-credentials', - asyncComponent: () => import( './steps-repository/site-migration-fallback-credentials' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-fallback-credentials' */ './steps-repository/site-migration-fallback-credentials' + ), }, SITE_MIGRATION_APPLICATION_PASSWORD_AUTHORIZATION: { slug: 'site-migration-application-password-authorization', asyncComponent: () => - import( './steps-repository/site-migration-application-password-authorization' ), + import( + /* webpackChunkName: 'async-step-site-migration-application-password-authorization' */ './steps-repository/site-migration-application-password-authorization' + ), }, SITE_MIGRATION_IDENTIFY: { slug: 'site-migration-identify', - asyncComponent: () => import( './steps-repository/site-migration-identify' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-identify' */ './steps-repository/site-migration-identify' + ), }, SITE_MIGRATION_IMPORT_OR_MIGRATE: { slug: 'site-migration-import-or-migrate', - asyncComponent: () => import( './steps-repository/site-migration-import-or-migrate' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-import-or-migrate' */ './steps-repository/site-migration-import-or-migrate' + ), }, SITE_MIGRATION_OTHER_PLATFORM_DETECTED_IMPORT: { slug: 'other-platform-detected', asyncComponent: () => - import( './steps-repository/site-migration-other-platform-detected-import' ), + import( + /* webpackChunkName: 'async-step-site-migration-other-platform-detected-import' */ './steps-repository/site-migration-other-platform-detected-import' + ), }, SITE_MIGRATION_HOW_TO_MIGRATE: { slug: 'site-migration-how-to-migrate', - asyncComponent: () => import( './steps-repository/site-migration-how-to-migrate' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-how-to-migrate' */ './steps-repository/site-migration-how-to-migrate' + ), }, SITE_MIGRATION_SOURCE_URL: { slug: 'site-migration-source-url', - asyncComponent: () => import( './steps-repository/site-migration-source-url' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-source-url' */ './steps-repository/site-migration-source-url' + ), }, SITE_MIGRATION_UPGRADE_PLAN: { slug: 'site-migration-upgrade-plan', - asyncComponent: () => import( './steps-repository/site-migration-upgrade-plan' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-upgrade-plan' */ './steps-repository/site-migration-upgrade-plan' + ), }, SITE_MIGRATION_PLUGIN_INSTALL: { slug: 'site-migration-plugin-install', - asyncComponent: () => import( './steps-repository/site-migration-plugin-install' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-plugin-install' */ './steps-repository/site-migration-plugin-install' + ), }, SITE_MIGRATION_ALREADY_WPCOM: { slug: 'already-wpcom', - asyncComponent: () => import( './steps-repository/site-migration-already-wpcom' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-already-wpcom' */ './steps-repository/site-migration-already-wpcom' + ), }, SITE_MIGRATION_SUPPORT_INSTRUCTIONS: { slug: 'migration-support-instructions', - asyncComponent: () => import( './steps-repository/site-migration-support-instructions' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-site-migration-support-instructions' */ './steps-repository/site-migration-support-instructions' + ), + }, + + UNIFIED_DOMAINS: { + slug: 'domains', + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-unified-domains' */ './steps-repository/unified-domains' + ), + }, + + UNIFIED_PLANS: { + slug: 'plans', + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-unified-plans' */ './steps-repository/unified-plans' + ), + }, + + USE_MY_DOMAIN: { + slug: 'use-my-domain', + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-use-my-domain' */ './steps-repository/use-my-domain' + ), }, PICK_SITE: { slug: 'sitePicker', - asyncComponent: () => import( './steps-repository/site-picker' ), + asyncComponent: () => + import( /* webpackChunkName: 'async-step-site-picker' */ './steps-repository/site-picker' ), }, SEGMENTATION_SURVEY: { slug: 'segmentation-survey', - asyncComponent: () => import( './steps-repository/segmentation-survey' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-segmentation-survey' */ './steps-repository/segmentation-survey' + ), }, PLATFORM_IDENTIFICATION: { slug: 'platform-identification', - asyncComponent: () => import( './steps-repository/platform-identification' ), + asyncComponent: () => + import( + /* webpackChunkName: 'async-step-platform-identification' */ './steps-repository/platform-identification' + ), }, } satisfies Record< string, StepperStep >; diff --git a/client/landing/stepper/declarative-flow/onboarding.ts b/client/landing/stepper/declarative-flow/onboarding.ts index be99b60b44c97b..4b858bb29d273c 100644 --- a/client/landing/stepper/declarative-flow/onboarding.ts +++ b/client/landing/stepper/declarative-flow/onboarding.ts @@ -99,26 +99,11 @@ const onboarding: Flow = { const [ , isGoalsAtFrontExperiment ] = useGoalsFirstExperiment(); const steps = stepsWithRequiredLogin( [ - { - slug: 'domains', - asyncComponent: () => import( './internals/steps-repository/unified-domains' ), - }, - { - slug: 'use-my-domain', - asyncComponent: () => import( './internals/steps-repository/use-my-domain' ), - }, - { - slug: 'plans', - asyncComponent: () => import( './internals/steps-repository/unified-plans' ), - }, - { - slug: 'create-site', - asyncComponent: () => import( './internals/steps-repository/create-site' ), - }, - { - slug: 'processing', - asyncComponent: () => import( './internals/steps-repository/processing-step' ), - }, + STEPS.UNIFIED_DOMAINS, + STEPS.USE_MY_DOMAIN, + STEPS.UNIFIED_PLANS, + STEPS.SITE_CREATION_STEP, + STEPS.PROCESSING, ] ); if ( isGoalsAtFrontExperiment ) { diff --git a/client/landing/stepper/hooks/use-first-step.ts b/client/landing/stepper/hooks/use-first-step.ts index 47b36573faea2a..9e5eb388fcbd61 100644 --- a/client/landing/stepper/hooks/use-first-step.ts +++ b/client/landing/stepper/hooks/use-first-step.ts @@ -9,8 +9,9 @@ import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; export const useFirstStep = ( stepPaths: string[] ) => { const isLoggedIn = useSelector( isUserLoggedIn ); - if ( stepPaths[ 0 ] === 'user' && isLoggedIn ) { - return stepPaths[ 1 ]; + // If the flow has the user step, which always falls in the end, and the user is not logged in, then the user step should be the first step. + if ( stepPaths[ stepPaths.length - 1 ] === 'user' && ! isLoggedIn ) { + return stepPaths[ stepPaths.length - 1 ]; } return stepPaths[ 0 ]; From 4489aa8fe3b1681494515f12f973abcf070751b5 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Wed, 5 Feb 2025 22:36:45 +0100 Subject: [PATCH 3/7] Change debug tag --- .../declarative-flow/internals/hooks/use-preload-steps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts index 0a0ffe95085210..efbb93afebd883 100644 --- a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts +++ b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts @@ -3,7 +3,7 @@ import debugFactory from 'debug'; import { useEffect } from 'react'; import { Flow, StepperStep } from '../types'; -const debug = debugFactory( 'calypso:stepper:flow-initialization' ); +const debug = debugFactory( 'calypso:stepper:preloading' ); async function tryPreload( step?: StepperStep, followingStep?: StepperStep ) { if ( step && 'asyncComponent' in step ) { From 452caae549f7aa12dedceb966776ea534868dc61 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 6 Feb 2025 17:08:33 +0100 Subject: [PATCH 4/7] Revert first step hook changes - it assumes auth will always come first --- client/landing/stepper/hooks/use-first-step.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/landing/stepper/hooks/use-first-step.ts b/client/landing/stepper/hooks/use-first-step.ts index 9e5eb388fcbd61..47b36573faea2a 100644 --- a/client/landing/stepper/hooks/use-first-step.ts +++ b/client/landing/stepper/hooks/use-first-step.ts @@ -9,9 +9,8 @@ import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; export const useFirstStep = ( stepPaths: string[] ) => { const isLoggedIn = useSelector( isUserLoggedIn ); - // If the flow has the user step, which always falls in the end, and the user is not logged in, then the user step should be the first step. - if ( stepPaths[ stepPaths.length - 1 ] === 'user' && ! isLoggedIn ) { - return stepPaths[ stepPaths.length - 1 ]; + if ( stepPaths[ 0 ] === 'user' && isLoggedIn ) { + return stepPaths[ 1 ]; } return stepPaths[ 0 ]; From 449a737840e165786db484e38c89c1c84afb08c2 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 6 Feb 2025 17:39:02 +0100 Subject: [PATCH 5/7] Further tuning --- .../internals/hooks/use-preload-steps.ts | 20 +++++++++++++++---- .../declarative-flow/internals/index.tsx | 9 ++++++++- .../declarative-flow/internals/steps.tsx | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts index efbb93afebd883..1cab492465e7e0 100644 --- a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts +++ b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts @@ -2,6 +2,8 @@ import { SiteDetails } from '@automattic/data-stores'; import debugFactory from 'debug'; import { useEffect } from 'react'; import { Flow, StepperStep } from '../types'; +import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; +import { useSelector } from 'calypso/state'; const debug = debugFactory( 'calypso:stepper:preloading' ); @@ -32,6 +34,8 @@ export function usePreloadSteps( flowSteps: readonly StepperStep[], flow: Flow ) { + const isLoggedIn = useSelector( isUserLoggedIn ); + useEffect( () => { if ( siteSlugOrId && ! selectedSite ) { // If this step depends on a selected site, only preload after we have the data. @@ -42,13 +46,21 @@ export function usePreloadSteps( if ( currentStepRoute ) { // The user step is a special case, as it's not part of the flow steps. It always comes in the end of the steps array. if ( currentStepRoute === 'user' ) { - const nextStep = flowSteps[ 0 ]; - const nextNextStep = flowSteps[ 1 ]; + // Load the first steps that requires authentication. + const nextStepIndex = flowSteps.findIndex( ( step ) => step.requiresLoggedInUser ); + const nextStep = flowSteps[ nextStepIndex ]; + const nextNextStep = flowSteps[ nextStepIndex + 1 ]; tryPreload( nextStep, nextNextStep ); } else { - const nextStepIndex = flowSteps.findIndex( ( step ) => step.slug === currentStepRoute ) + 1; - const nextNextStepIndex = nextStepIndex + 1; + // If any step requires authentication, preload the user step. + if ( ! isLoggedIn && flowSteps.some( ( step ) => step.requiresLoggedInUser ) ) { + const userStep = flowSteps.find( ( step ) => step.slug === 'user' ); + tryPreload( userStep ); + } + const nextStepIndex = flowSteps.findIndex( ( step ) => step.slug === currentStepRoute ) + 1; + const nextNextStepIndex = + flowSteps.findIndex( ( step ) => step.slug === currentStepRoute ) + 2; const nextStep = flowSteps[ nextStepIndex ]; const nextNextStep = flowSteps[ nextNextStepIndex ]; diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx index b95b853498ee10..cc1d7c310e3229 100644 --- a/client/landing/stepper/declarative-flow/internals/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/index.tsx @@ -140,7 +140,7 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] // The `nextStep` is available only when logged-out users go to the step that requires auth // and are redirected to the user step. When it's not available, go to the first step. - const postAuthStepSlug = stepData?.nextStep ?? stepPaths[ 0 ]; + const postAuthStepSlug = stepData?.nextStep ?? ''; if ( step.slug === PRIVATE_STEPS.USER.slug && postAuthStepSlug ) { const previousAuthStepSlug = stepData?.previousStep; const postAuthStepPath = generatePath( '/setup/:flow/:step/:lang?', { @@ -176,6 +176,13 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] ); } + if ( step.slug === PRIVATE_STEPS.USER.slug ) { + // eslint-disable-next-line no-console + console.warn( + 'Please define the next step after auth explicitly as we cannot find the user step automatically.' + ); + } + return ( - import( /* webpackChunkName: "stepper-user-step" */ './steps-repository/__user' ), + import( /* webpackChunkName: "async-step-user-step" */ './steps-repository/__user' ), }, } satisfies Record< string, StepperStep >; From 2dea1341c03c7ff815e71a0bd8b6b6523c1bfbcf Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 6 Feb 2025 17:39:14 +0100 Subject: [PATCH 6/7] Further tuning --- .../declarative-flow/internals/hooks/use-preload-steps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts index 1cab492465e7e0..4dd06797ba0b94 100644 --- a/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts +++ b/client/landing/stepper/declarative-flow/internals/hooks/use-preload-steps.ts @@ -1,9 +1,9 @@ import { SiteDetails } from '@automattic/data-stores'; import debugFactory from 'debug'; import { useEffect } from 'react'; -import { Flow, StepperStep } from '../types'; -import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; import { useSelector } from 'calypso/state'; +import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; +import type { Flow, StepperStep } from '../types'; const debug = debugFactory( 'calypso:stepper:preloading' ); From 999bda153415d84666e9fe12c9c5dd3f3036989c Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 6 Feb 2025 17:40:57 +0100 Subject: [PATCH 7/7] Code comment --- client/landing/stepper/declarative-flow/internals/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx index cc1d7c310e3229..9e9a8043cefe05 100644 --- a/client/landing/stepper/declarative-flow/internals/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/index.tsx @@ -139,7 +139,7 @@ export const FlowRenderer: React.FC< { flow: Flow; steps: readonly StepperStep[] } // The `nextStep` is available only when logged-out users go to the step that requires auth - // and are redirected to the user step. When it's not available, go to the first step. + // and are redirected to the user step. const postAuthStepSlug = stepData?.nextStep ?? ''; if ( step.slug === PRIVATE_STEPS.USER.slug && postAuthStepSlug ) { const previousAuthStepSlug = stepData?.previousStep;