Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stepper: Fix navigating to a gated step #98083

Merged
merged 6 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const StepRoute = ( { step, flow, showWooLogo, renderStep, navigate }: StepRoute
<>
<SignupHeader pageTitle={ flow.title } showWooLogo={ showWooLogo } />
{ stepContent }
<SurveyManager disabled />
<SurveyManager disabled flow={ flow } />
<StepperPerformanceTrackerStop flow={ flow.name } step={ step.slug } />
</>
) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { Suspense } from 'react';
import { useFlowNavigation } from '../../hooks/use-flow-navigation';
import AsyncMigrationSurvey from '../../steps-repository/components/migration-survey/async';
import { Flow } from '../../types';
import { DeferredRender } from '../deferred-render';

const MIGRATION_SURVEY_FLOWS = [
Expand All @@ -17,8 +18,8 @@ const MIGRATION_SURVEY_FLOWS = [
MIGRATION_SIGNUP_FLOW,
];

const SurveyManager = ( { disabled }: { disabled: boolean } ) => {
const { params } = useFlowNavigation();
const SurveyManager = ( { disabled, flow }: { disabled: boolean; flow: Flow } ) => {
const { params } = useFlowNavigation( flow );
const isEnLocale = useIsEnglishLocale();

if ( ! params.flow || disabled ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { OnboardSelect } from '@automattic/data-stores';
import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback } from 'react';
import { generatePath, useMatch, useNavigate } from 'react-router';
import { generatePath, createPath, useMatch, useNavigate } from 'react-router';
import { useSearchParams } from 'react-router-dom';
import { useFlowLocale } from 'calypso/landing/stepper/hooks/use-flow-locale';
import { getLoginUrlForFlow } from 'calypso/landing/stepper/hooks/use-login-url-for-flow';
import { useSiteData } from 'calypso/landing/stepper/hooks/use-site-data';
import { ONBOARD_STORE, STEPPER_INTERNAL_STORE } from 'calypso/landing/stepper/stores';
import type { Navigate, StepperStep } from '../../types';
import { useSelector } from 'calypso/state';
import { isUserLoggedIn } from 'calypso/state/current-user/selectors';
import { PRIVATE_STEPS } from '../../steps';
import type { Flow, Navigate, StepperStep } from '../../types';

const useOnboardingIntent = () => {
const intent = useSelect(
Expand Down Expand Up @@ -33,16 +39,65 @@ interface FlowNavigation {
/**
* Hook to manage the navigation between steps in the flow
*/
export const useFlowNavigation = (): FlowNavigation => {
export const useFlowNavigation = ( flow: Flow ): FlowNavigation => {
const intent = useOnboardingIntent();
const { setStepData } = useDispatch( STEPPER_INTERNAL_STORE );
const navigate = useNavigate();
const match = useMatch( '/:flow/:step?/:lang?' );
const { flow = null, step: currentStepSlug = null, lang = null } = match?.params || {};
const { step: currentStepSlug = null, lang = null } = match?.params || {};
const [ currentSearchParams ] = useSearchParams();
const steps = flow.useSteps();
const isLoggedIn = useSelector( isUserLoggedIn );
const stepsSlugs = steps.map( ( step ) => step.slug );
const locale = useFlowLocale();
const { siteId, siteSlug } = useSiteData();

const customNavigate = useCallback< Navigate< StepperStep[] > >(
( nextStep: string, extraData = {}, replace = false ) => {
// If the user is not logged in, and the next step requires a logged in user, redirect to the login step.
if (
! isLoggedIn &&
steps.find( ( step ) => step.slug === nextStep )?.requiresLoggedInUser
) {
// In-stepper auth.
if ( flow.__experimentalUseBuiltinAuth ) {
const signInPath = generatePath( `/:flow/:step/:lang?`, {
flow: flow.name,
lang,
step: PRIVATE_STEPS.USER.slug,
} );

// Inform the user step where to go after the user is authenticated.
setStepData( {
previousStep: currentStepSlug,
nextStep,
} );

return navigate( signInPath );
}
// Classic /login auth.
const nextStepPath = createPath( {
// We have to include /setup, as this URL should be absolute and we can't use `useHref`.
pathname: generatePath( `/setup/:flow/:step/:lang?`, {
flow: flow.name,
lang,
step: nextStep,
} ),
search: currentSearchParams.toString(),
hash: window.location.hash,
} );

const loginUrl = getLoginUrlForFlow( {
flow,
locale,
path: nextStepPath,
siteId,
siteSlug,
} );

return window.location.assign( loginUrl );
}

const hasQueryParams = nextStep.includes( '?' );

// Get the latest search params from the current location
Expand All @@ -56,20 +111,33 @@ export const useFlowNavigation = (): FlowNavigation => {
} );

const newPath = generatePath( `/:flow/:step/:lang?`, {
flow,
flow: flow.name,
lang,
step: nextStep,
} );

navigate( addQueryParams( newPath, queryParams ), { replace } );
},
[ flow, intent, lang, navigate, setStepData, currentStepSlug ]
// eslint-disable-next-line react-hooks/exhaustive-deps -- steps array is recreated on every render, use stepsSlugs instead.
[
stepsSlugs,
isLoggedIn,
locale,
siteId,
siteSlug,
flow,
intent,
lang,
navigate,
setStepData,
currentStepSlug,
]
);

return {
navigate: customNavigate,
params: {
flow,
flow: flow.name,
step: currentStepSlug,
},
search: currentSearchParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@ import React from 'react';
import { MemoryRouter, useLocation } from 'react-router';
import { renderHookWithProvider } from 'calypso/test-helpers/testing-library';
import { STEPPER_INTERNAL_STORE } from '../../../../../stores';
import { Flow } from '../../../types';
import { useFlowNavigation } from '../index';

const mockFlow: Flow = {
name: 'some-flow',
isSignupFlow: false,
useSteps() {
return [ { slug: 'some-step', component: () => <div>Step 1</div> } ];
},
useStepNavigation() {
return {};
},
};

const LocationDisplay = () => {
const location = useLocation();
const stepData = useSelect(
Expand All @@ -35,7 +47,7 @@ const Wrapper =

const render = ( { initialEntry = '/setup/some-flow/some-step' } = {} ) => {
window.history.replaceState( null, '', initialEntry );
return renderHookWithProvider( () => useFlowNavigation(), {
return renderHookWithProvider( () => useFlowNavigation( mockFlow ), {
wrapper: Wrapper( initialEntry ),
} );
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
const flowSteps = flow.useSteps();
const stepPaths = flowSteps.map( ( step ) => step.slug );
const firstStepSlug = useFirstStep( stepPaths );
const { navigate, params } = useFlowNavigation();
const { navigate, params } = useFlowNavigation( flow );
const currentStepRoute = params.step || '';
const isLoggedIn = useSelector( isUserLoggedIn );
const { lang = null } = useParams();
Expand Down
39 changes: 36 additions & 3 deletions client/landing/stepper/hooks/use-login-url-for-flow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { addQueryArgs } from '@wordpress/url';
import { useHref, useLocation } from 'react-router';
import { useLoginUrl } from '../utils/path';
import { getLoginUrl } from '../utils/path';
import { useFlowLocale } from './use-flow-locale';
import { useSiteData } from './use-site-data';
import type { Flow } from 'calypso/landing/stepper/declarative-flow/internals/types';
Expand All @@ -16,14 +16,47 @@ export function useLoginUrlForFlow( { flow }: UseLoginUrlForFlowProps ): string
const path = useHref( location.pathname );
const { extraQueryParams, customLoginPath } = flow.useLoginParams?.() ?? {};

return getLoginUrlForFlow( {
flow,
path,
locale,
siteId,
siteSlug,
search: location.search,
extraQueryParams,
customLoginPath,
} );
}

type GetLoginUrlForFlowProps = {
flow: Flow;
path: string;
locale: string;
siteId: string | number;
siteSlug: string;
search?: string;
extraQueryParams?: Record< string, string | number >;
customLoginPath?: string;
};

export function getLoginUrlForFlow( {
flow,
path,
locale,
siteId,
siteSlug,
search = window.location.search,
extraQueryParams = {},
customLoginPath,
}: GetLoginUrlForFlowProps ): string {
const redirectTo = addQueryArgs( path, {
...( locale && locale !== 'en' ? { locale } : {} ),
...( siteId ? { siteId } : {} ),
...( siteSlug ? { siteSlug } : {} ),
...Object.fromEntries( new URLSearchParams( location.search ).entries() ),
...Object.fromEntries( new URLSearchParams( search ).entries() ),
} );

return useLoginUrl( {
return getLoginUrl( {
variationName: flow.variantSlug ?? flow.name,
pageTitle: flow.title,
locale,
Expand Down
Loading