diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-sign-up-start-tracking/index.tsx b/client/landing/stepper/declarative-flow/internals/hooks/use-sign-up-start-tracking/index.tsx new file mode 100644 index 00000000000000..338d92a0ae08e5 --- /dev/null +++ b/client/landing/stepper/declarative-flow/internals/hooks/use-sign-up-start-tracking/index.tsx @@ -0,0 +1,35 @@ +import { SENSEI_FLOW } from '@automattic/onboarding'; +import { useEffect, useMemo } from 'react'; +import { useQuery } from 'calypso/landing/stepper/hooks/use-query'; +import { recordSignupStart } from 'calypso/lib/analytics/signup'; +import { type Flow } from '../../types'; + +/** + * Hook to track the start of a signup flow. + */ +interface Props { + flow: Flow; + currentStepRoute: string; +} + +export const useSignUpStartTracking = ( { flow, currentStepRoute }: Props ) => { + const steps = flow.useSteps(); + const queryParams = useQuery(); + const ref = queryParams.get( 'ref' ) || ''; + const signedUp = queryParams.has( 'signed_up' ); + + // TODO: Check if we can remove the sensei flow reference from here. + const firstStepSlug = ( flow.name === SENSEI_FLOW ? steps[ 1 ] : steps[ 0 ] ).slug; + const isFirstStep = firstStepSlug === currentStepRoute; + const extraProps = useMemo( () => flow.useSignupStartEventProps?.() || {}, [ flow ] ); + const flowName = flow.name; + const shouldTrack = flow.isSignupFlow && isFirstStep && ! signedUp; + + useEffect( () => { + if ( ! shouldTrack ) { + return; + } + + recordSignupStart( flowName, ref, extraProps ); + }, [ extraProps, flowName, ref, shouldTrack ] ); +}; diff --git a/client/landing/stepper/declarative-flow/internals/hooks/use-sign-up-start-tracking/test/index.tsx b/client/landing/stepper/declarative-flow/internals/hooks/use-sign-up-start-tracking/test/index.tsx new file mode 100644 index 00000000000000..5c0700aa99fb4f --- /dev/null +++ b/client/landing/stepper/declarative-flow/internals/hooks/use-sign-up-start-tracking/test/index.tsx @@ -0,0 +1,147 @@ +/** + * @jest-environment jsdom + */ + +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { SENSEI_FLOW } from '@automattic/onboarding'; +import { addQueryArgs } from '@wordpress/url'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { renderHookWithProvider } from 'calypso/test-helpers/testing-library'; +import { useSignUpStartTracking } from '../'; +import type { Flow, StepperStep } from '../../../types'; + +const steps = [ { slug: 'step-1' }, { slug: 'step-2' } ] as StepperStep[]; + +const regularFlow: Flow = { + name: 'regular-flow', + useSteps: () => steps, + useStepNavigation: () => ( { + submit: () => {}, + } ), + isSignupFlow: false, +}; + +const signUpFlow: Flow = { + ...regularFlow, + name: 'sign-up-flow', + isSignupFlow: true, +}; + +const senseiFlow: Flow = { + ...regularFlow, + name: SENSEI_FLOW, + // The original sensei flow is missing the isSignupFlow flag as true, it will be addressed by wp-calypso/pull/91593 + isSignupFlow: true, +}; + +jest.mock( '@automattic/calypso-analytics' ); + +const render = ( { flow, currentStepRoute, queryParams = {} } ) => { + return renderHookWithProvider( + () => + useSignUpStartTracking( { + flow, + currentStepRoute, + } ), + { + wrapper: ( { children } ) => ( + + { children } + + ), + } + ); +}; + +describe( 'useSignUpTracking', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'does not track event when the flow is not a isSignupFlow', () => { + render( { flow: regularFlow, currentStepRoute: 'step-1' } ); + + expect( recordTracksEvent ).not.toHaveBeenCalled(); + } ); + + describe( 'sign-up-flow', () => { + it( 'tracks the event current step is first step', () => { + render( { + flow: signUpFlow, + currentStepRoute: 'step-1', + queryParams: { ref: 'another-flow-or-cta' }, + } ); + + expect( recordTracksEvent ).toHaveBeenCalledWith( 'calypso_signup_start', { + flow: 'sign-up-flow', + ref: 'another-flow-or-cta', + } ); + } ); + + it( 'skips the tracking when the signed up flag is set', () => { + render( { + flow: signUpFlow, + currentStepRoute: 'step-1', + queryParams: { signed_up: 1 }, + } ); + + expect( recordTracksEvent ).not.toHaveBeenCalled(); + } ); + + it( 'tracks the event with extra props from useSighupStartEventProps', () => { + render( { + flow: { + ...signUpFlow, + useSignupStartEventProps: () => ( { extra: 'props' } ), + } satisfies Flow, + currentStepRoute: 'step-1', + queryParams: { ref: 'another-flow-or-cta' }, + } ); + + expect( recordTracksEvent ).toHaveBeenCalledWith( 'calypso_signup_start', { + flow: 'sign-up-flow', + ref: 'another-flow-or-cta', + extra: 'props', + } ); + } ); + + it( 'does not track events current step is NOT the first step', () => { + render( { flow: signUpFlow, currentStepRoute: 'step-2' } ); + + expect( recordTracksEvent ).not.toHaveBeenCalled(); + } ); + + it( 'does not track events current step is the first step AND the user is returning from the sign in flow', () => { + render( { flow: signUpFlow, currentStepRoute: 'step-1', queryParams: { signed_up: true } } ); + + expect( recordTracksEvent ).not.toHaveBeenCalled(); + } ); + + // Check if sensei is a sign-up flow; + it( "tracks when the user is on the sensei's flow second step", () => { + render( { flow: senseiFlow, currentStepRoute: 'step-2' } ); + + expect( recordTracksEvent ).toHaveBeenCalledWith( 'calypso_signup_start', { + flow: SENSEI_FLOW, + ref: '', + } ); + } ); + + it( 'does not trigger the event on rerender', () => { + const { rerender } = render( { + flow: { ...signUpFlow, useSignupStartEventProps: () => ( { extra: 'props' } ) }, + currentStepRoute: 'step-1', + queryParams: { ref: 'another-flow-or-cta' }, + } ); + + rerender(); + + expect( recordTracksEvent ).toHaveBeenNthCalledWith( 1, 'calypso_signup_start', { + flow: 'sign-up-flow', + ref: 'another-flow-or-cta', + extra: 'props', + } ); + } ); + } ); +} ); diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx index eba982856913d5..fd26e3f9485185 100644 --- a/client/landing/stepper/declarative-flow/internals/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/index.tsx @@ -1,28 +1,27 @@ import { - SENSEI_FLOW, isNewsletterOrLinkInBioFlow, isSenseiFlow, isWooExpressFlow, } from '@automattic/onboarding'; import { useSelect, useDispatch } from '@wordpress/data'; import { useI18n } from '@wordpress/react-i18n'; -import React, { useEffect, useCallback, useMemo, Suspense, lazy } from 'react'; +import React, { useEffect, 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 { STEPPER_INTERNAL_STORE } from 'calypso/landing/stepper/stores'; -import { recordSignupStart } from 'calypso/lib/analytics/signup'; import AsyncCheckoutModal from 'calypso/my-sites/checkout/modal/async'; import { useSelector } from 'calypso/state'; import { getSite } from 'calypso/state/sites/selectors'; -import { useQuery } from '../../hooks/use-query'; import { useSaveQueryParams } from '../../hooks/use-save-query-params'; import { useSiteData } from '../../hooks/use-site-data'; import useSyncRoute from '../../hooks/use-sync-route'; import { ONBOARD_STORE } from '../../stores'; import { StepRoute, StepperLoader } from './components'; +import { useSignUpStartTracking } from './hooks/use-sign-up-start-tracking'; import { AssertConditionState, type Flow, type StepperStep, type StepProps } from './types'; import type { OnboardSelect, StepperInternalSelect } from '@automattic/data-stores'; + import './global.scss'; /** @@ -40,6 +39,7 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => { Modal.setAppElement( '#wpcom' ); const flowSteps = flow.useSteps(); const stepPaths = flowSteps.map( ( step ) => step.slug ); + const stepComponents: Record< string, React.FC< StepProps > > = useMemo( () => flowSteps.reduce( @@ -64,7 +64,6 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => { ); useSaveQueryParams(); - const ref = useQuery().get( 'ref' ) || ''; const { site, siteSlugOrId } = useSiteData(); @@ -91,18 +90,6 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [ flow, siteSlugOrId, selectedSite ] ); - const isFlowStart = useCallback( () => { - if ( ! flow || ! stepPaths.length ) { - return false; - } - - if ( flow.name === SENSEI_FLOW ) { - return currentStepRoute === stepPaths[ 1 ]; - } - - return currentStepRoute === stepPaths[ 0 ]; - }, [ flow, currentStepRoute, ...stepPaths ] ); - const _navigate = async ( path: string, extraData = {} ) => { // If any extra data is passed to the navigate() function, store it to the stepper-internal store. setStepData( { @@ -139,16 +126,6 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => { window.scrollTo( 0, 0 ); }, [ location ] ); - // Get any flow-specific event props to include in the - // `calypso_signup_start` Tracks event triggerd in the effect below. - const signupStartEventProps = flow.useSignupStartEventProps?.() ?? {}; - - useEffect( () => { - if ( flow.isSignupFlow && isFlowStart() ) { - recordSignupStart( flow.name, ref, signupStartEventProps ); - } - }, [ flow, ref, isFlowStart ] ); - const assertCondition = flow.useAssertConditions?.( _navigate ) ?? { state: AssertConditionState.SUCCESS, }; @@ -184,6 +161,8 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => { } }; + useSignUpStartTracking( { flow, currentStepRoute: currentStepRoute } ); + return ( }>