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

[WIP] Try to rethink Stepper and logins #91074

Closed
wants to merge 3 commits into from
Closed
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
117 changes: 103 additions & 14 deletions client/landing/stepper/declarative-flow/internals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import React, { useEffect, useCallback, useMemo, Suspense, lazy } from 'react';
import Modal from 'react-modal';
import { Navigate, Route, Routes, generatePath, useNavigate, useLocation } from 'react-router-dom';
import DocumentHead from 'calypso/components/data/document-head';
import { recordSubmitStep } from 'calypso/landing/stepper/declarative-flow/internals/analytics/record-submit-step';
import { STEPS } from 'calypso/landing/stepper/declarative-flow/internals/steps';
import { STEPPER_INTERNAL_STORE } from 'calypso/landing/stepper/stores';
import { recordPageView } from 'calypso/lib/analytics/page-view';
import { recordSignupStart } from 'calypso/lib/analytics/signup';
Expand All @@ -20,6 +22,7 @@ import {
getSignupCompleteStepNameAndClear,
} from 'calypso/signup/storageUtils';
import { useSelector } from 'calypso/state';
import { isUserLoggedIn } from 'calypso/state/current-user/selectors';
import { getSite, isRequestingSite } from 'calypso/state/sites/selectors';
import { useQuery } from '../../hooks/use-query';
import { useSaveQueryParams } from '../../hooks/use-save-query-params';
Expand All @@ -30,8 +33,9 @@ import kebabCase from '../../utils/kebabCase';
import { getAssemblerSource } from './analytics/record-design';
import recordStepStart from './analytics/record-step-start';
import { StepRoute, StepperLoader } from './components';
import { AssertConditionState, Flow, StepperStep, StepProps } from './types';
import { AssertConditionState } from './types';
import './global.scss';
import type { Flow, Navigate as StepNavigate, StepperStep, StepProps } from './types';
import type { OnboardSelect, StepperInternalSelect } from '@automattic/data-stores';

/**
Expand All @@ -45,6 +49,69 @@ export const getStepOldSlug = ( stepSlug: string ): string | undefined => {
return stepSlugMap[ stepSlug ];
};

const useFlowStepsWithLoginHandling = ( flow: Flow ) => {
const steps = flow.useSteps();
const isLoggedInUser = useSelector( isUserLoggedIn );

if ( ! flow.usesLocalLogin || isLoggedInUser ) {
return steps;
}

const userStepIndex = steps.findIndex( ( step ) => step.slug === STEPS.USER.slug );
if ( userStepIndex === 0 ) {
return steps;
}

if ( userStepIndex === -1 ) {
return [ STEPS.USER, ...steps ];
}

steps.splice( userStepIndex, 1 );

return [ STEPS.USER, ...steps ];
};

const useStepNavigationWithLogin = (
flow: Flow,
currentStepRoute: string,
stepNavigate: StepNavigate< StepperStep[] >,
stepPaths?: string[]
): ReturnType< Flow[ 'useStepNavigation' ] > => {
const isLoggedInUser = useSelector( isUserLoggedIn );

const stepNavigation = flow.useStepNavigation( currentStepRoute, stepNavigate, stepPaths );

// TODO: General gap: flows shouldn't be calling recordSubmitStep().
// That logic should be somewhere at this level, and not in the flows, as I strongly suspect we
// have flows that don't call that function.

if (
! flow.usesLocalLogin ||
isLoggedInUser ||
currentStepRoute !== STEPS.USER.slug ||
! stepPaths
) {
return stepNavigation;
}

// If we can work out what the next step in the flow is, ensure the submit from the USER step
// navigates there.
const userStepIndex = stepPaths.indexOf( STEPS.USER.slug );
if ( userStepIndex > -1 && userStepIndex < stepPaths.length - 1 ) {
const nextStep = stepPaths[ userStepIndex + 1 ];

return {
...stepNavigation,
submit: () => {
recordSubmitStep( {}, '', flow.name, currentStepRoute, flow.variantSlug );
return stepNavigate( nextStep );
},
};
}

return stepNavigation;
};

/**
* This component accepts a single flow property. It does the following:
*
Expand All @@ -58,7 +125,7 @@ export const getStepOldSlug = ( stepSlug: string ): string | undefined => {
export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
// Configure app element that React Modal will aria-hide when modal is open
Modal.setAppElement( '#wpcom' );
const flowSteps = flow.useSteps();
const flowSteps = useFlowStepsWithLoginHandling( flow );
const stepPaths = flowSteps.map( ( step ) => step.slug );
const stepComponents: Record< string, React.FC< StepProps > > = useMemo(
() =>
Expand All @@ -73,8 +140,17 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
[ flowSteps ]
);

const handleLoggedOutUsers = flow.usesLocalLogin ?? false;
const isLoggedInUser = useSelector( isUserLoggedIn );
const flowPath = flow.variantSlug ?? flow.name;
const location = useLocation();
const currentStepRoute = location.pathname.split( '/' )[ 2 ]?.replace( /\/+$/, '' );

const currentStepRouteFromLocation = location.pathname.split( '/' )[ 2 ]?.replace( /\/+$/, '' );
// TODO: Should we even do this? It feels like a HACK.
// Also, should we use USER.slug explicitly, or just use stepPaths[0] to match the later logic for flow starts?
const currentStepRoute =
handleLoggedOutUsers && ! isLoggedInUser ? STEPS.USER.slug : currentStepRouteFromLocation;

const stepOldSlug = getStepOldSlug( currentStepRoute );
const { __ } = useI18n();
const navigate = useNavigate();
Expand Down Expand Up @@ -133,6 +209,11 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
return currentStepRoute === stepPaths[ 1 ];
}

if ( flow.usesLocalLogin && ! isLoggedInUser ) {
// TODO: this feels like it could be more nuanced
return true;
}

return currentStepRoute === stepPaths[ 0 ];
}, [ flow, currentStepRoute, ...stepPaths ] );

Expand All @@ -146,17 +227,13 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
} );

const _path = path.includes( '?' ) // does path contain search params
? generatePath( `/${ flow.variantSlug ?? flow.name }/${ path }` )
: generatePath( `/${ flow.variantSlug ?? flow.name }/${ path }${ window.location.search }` );
? generatePath( `/${ flowPath }/${ path }` )
: generatePath( `/${ flowPath }/${ path }${ window.location.search }` );

navigate( _path, { state: stepPaths } );
};

const stepNavigation = flow.useStepNavigation(
currentStepRoute,
_navigate,
flowSteps.map( ( step ) => step.slug )
);
const stepNavigation = useStepNavigationWithLogin( flow, currentStepRoute, _navigate, stepPaths );

// Retrieve any extra step data from the stepper-internal store. This will be passed as a prop to the current step.
const stepData = useSelect(
Expand All @@ -172,6 +249,20 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
window.scrollTo( 0, 0 );
}, [ location ] );

useEffect( () => {
if (
isLoggedInUser ||
! flow.usesLocalLogin ||
[ '', STEPS.USER.slug ].includes( currentStepRouteFromLocation )
) {
return;
}

// If we're on some other step, and we need to log in, navigate to the user step.
// We could also navigate to the flow root, but that's less explicit as to what we want to happen.
_navigate( STEPS.USER.slug );
}, [ currentStepRouteFromLocation, flow.usesLocalLogin, isLoggedInUser ] );

// Get any flow-specific event props to include in the
// `calypso_signup_start` Tracks event triggerd in the effect below.
const signupStartEventProps = flow.useSignupStartEventProps?.() ?? {};
Expand Down Expand Up @@ -260,7 +351,7 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
{ flowSteps.map( ( step ) => (
<Route
key={ step.slug }
path={ `/${ flow.variantSlug ?? flow.name }/${ step.slug }` }
path={ `/${ flowPath }/${ step.slug }` }
element={
<StepRoute
step={ step }
Expand All @@ -275,9 +366,7 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => {
path="*"
element={
<Navigate
to={ `/${ flow.variantSlug ?? flow.name }/${ stepPaths[ 0 ] }${
window.location.search
}` }
to={ `/${ flowPath }/${ stepPaths[ 0 ] }${ window.location.search }` }
replace
/>
}
Expand Down
5 changes: 5 additions & 0 deletions client/landing/stepper/declarative-flow/internals/steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ export const STEPS = {
asyncComponent: () => import( './steps-repository/trial-acknowledge' ),
},

USER: {
slug: 'user',
asyncComponent: () => import( './steps-repository/user' ),
},

VERIFY_EMAIL: {
slug: 'verifyEmail',
asyncComponent: () => import( './steps-repository/import-verify-email' ),
Expand Down
1 change: 1 addition & 0 deletions client/landing/stepper/declarative-flow/internals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type UseSideEffectHook< FlowSteps extends StepperStep[] > = (

export type Flow = {
name: string;
usesLocalLogin?: boolean;
/**
* If this flow extends another flow, the variant slug will be added as a class name to the root element of the flow.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NEW_HOSTED_SITE_FLOW_USER_INCLUDED } from '@automattic/onboarding';
import { useDispatch, useSelect } from '@wordpress/data';
import { addQueryArgs } from '@wordpress/url';
import { useEffect, useLayoutEffect } from 'react';
import { STEPS } from 'calypso/landing/stepper/declarative-flow/internals/steps';
import { recordFreeHostingTrialStarted } from 'calypso/lib/analytics/ad-tracking/ad-track-trial-start';
import {
setSignupCompleteSlug,
Expand All @@ -15,30 +16,22 @@ import { isUserEligibleForFreeHostingTrial } from 'calypso/state/selectors/is-us
import { useQuery } from '../hooks/use-query';
import { ONBOARD_STORE, USER_STORE } from '../stores';
import { recordSubmitStep } from './internals/analytics/record-submit-step';
import { Flow, ProvidedDependencies } from './internals/types';
import type { Flow, ProvidedDependencies } from './internals/types';
import type { OnboardSelect, UserSelect } from '@automattic/data-stores';
import type { MinimalRequestCartProduct } from '@automattic/shopping-cart';
import './internals/new-hosted-site-flow.scss';

const hosting: Flow = {
name: NEW_HOSTED_SITE_FLOW_USER_INCLUDED,
isSignupFlow: true,
usesLocalLogin: true,
useSteps() {
return [
{ slug: 'user', asyncComponent: () => import( './internals/steps-repository/user' ) },
{ slug: 'plans', asyncComponent: () => import( './internals/steps-repository/plans' ) },
{
slug: 'trialAcknowledge',
asyncComponent: () => import( './internals/steps-repository/trial-acknowledge' ),
},
{
slug: 'createSite',
asyncComponent: () => import( './internals/steps-repository/create-site' ),
},
{
slug: 'processing',
asyncComponent: () => import( './internals/steps-repository/processing-step' ),
},
//STEPS.USER,
STEPS.PLANS,
STEPS.TRIAL_ACKNOWLEDGE,
STEPS.SITE_CREATION_STEP,
STEPS.PROCESSING,
];
},
useStepNavigation( _currentStepSlug, navigate ) {
Expand All @@ -62,9 +55,11 @@ const hosting: Flow = {
recordSubmitStep( providedDependencies, '', flowName, _currentStepSlug );

switch ( _currentStepSlug ) {
/*
case 'user': {
return navigate( 'plans' );
}
*/
case 'plans': {
const productSlug = ( providedDependencies.plan as MinimalRequestCartProduct )
.product_slug;
Expand Down
Loading