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 (
}>