Skip to content

Commit

Permalink
Replace the launchpad-first flag with the goals-first cumulative ex…
Browse files Browse the repository at this point in the history
…periment (#99914)

* Replace the launchpad-first flag with the goals-first cumulative experiment

* Update makeTestSite so it doesn't overwrite default site options

* Add test cases for hiding launchpad for control variants of experiment

* Fixed experiment test

---------

Co-authored-by: Paulo Trentin <[email protected]>
  • Loading branch information
p-jackson and paulopmt1 authored Feb 19, 2025
1 parent 2f5e505 commit ee3c4b1
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { urlToSlug } from 'calypso/lib/url';
import { useSelector, useDispatch } from 'calypso/state';
import { isUserLoggedIn } from 'calypso/state/current-user/selectors';
import { successNotice } from 'calypso/state/notices/actions';
import shouldShowLaunchpadFirst from 'calypso/state/selectors/should-show-launchpad-first';
import { useShouldShowLaunchpadFirst } from 'calypso/state/selectors/should-show-launchpad-first';
import { useQuery } from '../../../../hooks/use-query';
import StepContent from './step-content';
import { areLaunchpadTasksCompleted } from './task-helper';
Expand Down Expand Up @@ -92,8 +92,11 @@ const Launchpad: Step = ( { navigation, flow }: LaunchpadProps ) => {
}
}, [ verifiedParam, translate, dispatch ] );

const [ loadingShouldShowLaunchpadFirst, shouldShowLaunchpadFirst ] =
useShouldShowLaunchpadFirst( site );

// Avoid screen flickering when redirecting to other paths
if ( ! site?.options ) {
if ( ! site?.options || loadingShouldShowLaunchpadFirst ) {
return null;
}

Expand All @@ -102,7 +105,7 @@ const Launchpad: Step = ( { navigation, flow }: LaunchpadProps ) => {
return null;
}

if ( shouldShowLaunchpadFirst( site ) ) {
if ( shouldShowLaunchpadFirst ) {
window.location.replace( `/home/${ siteSlug }` );
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion client/my-sites/customer-home/controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export async function maybeRedirect( context, next ) {

const site = getSelectedSite( state );

if ( shouldShowLaunchpadFirst( site ) ) {
if ( await shouldShowLaunchpadFirst( site ) ) {
return next();
}

Expand Down
54 changes: 29 additions & 25 deletions client/my-sites/customer-home/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import DocumentHead from 'calypso/components/data/document-head';
import Main from 'calypso/components/main';
import { useGetDomainsQuery } from 'calypso/data/domains/use-get-domains-query';
import PageViewTracker from 'calypso/lib/analytics/page-view-tracker';
import shouldShowLaunchpadFirst from 'calypso/state/selectors/should-show-launchpad-first';
import { useShouldShowLaunchpadFirst } from 'calypso/state/selectors/should-show-launchpad-first';
import CelebrateLaunchModal from './components/celebrate-launch-modal';
import { FullScreenLaunchpad } from './components/full-screen-launchpad';
import HomeContent from './components/home-content';
import type { SiteDetails } from '@automattic/data-stores';

export default function CustomerHome( { site }: { site: SiteDetails } ) {
const showLaunchpadFirst = shouldShowLaunchpadFirst( site );
const [ isLoadingShouldShowLaunchpadFirst, shouldShowLaunchpadFirst ] =
useShouldShowLaunchpadFirst( site );

const isSiteLaunched = site?.launch_status === 'launched' || false;

const [ isShowingLaunchpad, setIsShowingLaunchpad ] = useState(
showLaunchpadFirst &&
site.options?.launchpad_screen !== undefined &&
site.options?.launchpad_screen !== 'skipped' &&
! isSiteLaunched
const [ isFullLaunchpadDismissed, setIsFullLaunchpadDismissed ] = useState(
site.options?.launchpad_screen === undefined ||
site.options.launchpad_screen === 'skipped' ||
isSiteLaunched
);

const translate = useTranslate();
Expand All @@ -35,25 +35,29 @@ export default function CustomerHome( { site }: { site: SiteDetails } ) {
<Main wideLayout>
<PageViewTracker path="/home/:site" title={ translate( 'My Home' ) } />
<DocumentHead title={ translate( 'My Home' ) } />
{ isShowingLaunchpad ? (
<FullScreenLaunchpad
onClose={ () => setIsShowingLaunchpad( false ) }
onSiteLaunch={ () => {
setIsShowingLaunchpad( false );
setShowSiteLaunchedModal( true );
} }
/>
) : (
<HomeContent />
) }
{ showSiteLaunchedModal && (
{ ! isLoadingShouldShowLaunchpadFirst && (
<>
<ConfettiAnimation />
<CelebrateLaunchModal
setModalIsOpen={ setShowSiteLaunchedModal }
site={ site }
allDomains={ allDomains }
/>
{ shouldShowLaunchpadFirst && ! isFullLaunchpadDismissed ? (
<FullScreenLaunchpad
onClose={ () => setIsFullLaunchpadDismissed( true ) }
onSiteLaunch={ () => {
setIsFullLaunchpadDismissed( true );
setShowSiteLaunchedModal( true );
} }
/>
) : (
<HomeContent />
) }
{ showSiteLaunchedModal && (
<>
<ConfettiAnimation />
<CelebrateLaunchModal
setModalIsOpen={ setShowSiteLaunchedModal }
site={ site }
allDomains={ allDomains }
/>
</>
) }
</>
) }
</Main>
Expand Down
64 changes: 52 additions & 12 deletions client/my-sites/customer-home/test/customer-home.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
/**
* @jest-environment jsdom
*/
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import apiFetch from '@wordpress/api-fetch';
import nock from 'nock';
import React, { act } from 'react';
import { loadExperimentAssignment } from 'calypso/lib/explat';
import { reducer as ui } from 'calypso/state/ui/reducer';
import { renderWithProvider } from 'calypso/test-helpers/testing-library';
import CustomerHome from '../main';
import type { SiteDetails } from '@automattic/data-stores';

jest.mock( '@wordpress/api-fetch' );

jest.mock( '@automattic/calypso-config', () => {
const config = () => 'development';
config.isEnabled = ( property: string ) => property === 'home/launchpad-first';
return config;
} );
let mockUseExperimentResult = [ false, true ];

jest.mock( 'calypso/lib/explat', () => ( {
loadExperimentAssignment: jest.fn( ( slug ) =>
slug === 'calypso_signup_onboarding_goals_first_flow_holdout_v2_20250131'
? Promise.resolve( { variationName: 'treatment_cumulative' } )
: Promise.reject( new Error( `Unmocked experiment slug: ${ slug }` ) )
),
useExperiment: jest.fn( () => mockUseExperimentResult ),
} ) );

jest.mock( '../components/home-content', () => () => (
<div data-testid="home-content">Home Content</div>
Expand All @@ -41,8 +47,13 @@ function makeTestSite( site: Partial< SiteDetails > = {} ): SiteDetails {
URL: 'https://example.com',
domain: 'example.com',
launch_status: 'launched',
options: { site_creation_flow: 'onboarding', launchpad_screen: false, ...site.options },
...site,
options: {
site_creation_flow: 'onboarding',
launchpad_screen: false,
created_at: '2025-02-17T00:00:00+00:00',
...site.options,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any; // This partial site object should be good enough for testing purposes
}
Expand All @@ -56,24 +67,24 @@ describe( 'CustomerHome', () => {
nock.cleanAll();
} );

it( 'should show HomeContent for launched site', () => {
it( 'should show HomeContent for launched site', async () => {
const testSite = makeTestSite( { launch_status: 'launched' } );

renderWithProvider( <CustomerHome site={ testSite } /> );

expect( screen.getByTestId( 'home-content' ) ).toBeInTheDocument();
await waitFor( () => expect( screen.getByTestId( 'home-content' ) ).toBeInTheDocument() );
expect( screen.queryByTestId( 'launchpad-first' ) ).not.toBeInTheDocument();
} );

it( 'should show HomeContent for unlaunched site when launchpad is skipped', () => {
it( 'should show HomeContent for unlaunched site when launchpad is skipped', async () => {
const testSite = makeTestSite( {
launch_status: 'unlaunched',
options: { launchpad_screen: 'skipped' },
} );

renderWithProvider( <CustomerHome site={ testSite } /> );

expect( screen.getByTestId( 'home-content' ) ).toBeInTheDocument();
await waitFor( () => expect( screen.getByTestId( 'home-content' ) ).toBeInTheDocument() );
expect( screen.queryByTestId( 'launchpad-first' ) ).not.toBeInTheDocument();
} );

Expand All @@ -85,7 +96,7 @@ describe( 'CustomerHome', () => {

renderWithProvider( <CustomerHome site={ testSite } /> );

expect( screen.getByTestId( 'launchpad-first' ) ).toBeInTheDocument();
await waitFor( () => expect( screen.getByTestId( 'launchpad-first' ) ).toBeInTheDocument() );
expect( screen.queryByTestId( 'home-content' ) ).not.toBeInTheDocument();

// Click the close button
Expand Down Expand Up @@ -151,4 +162,33 @@ describe( 'CustomerHome', () => {

expect( await screen.findByText( 'Congrats, your site is live!' ) ).toBeInTheDocument();
} );

it( 'shows home content when site would be eligible to show launchpad, but user is in control group', async () => {
const testSite = makeTestSite( {
launch_status: 'unlaunched',
options: { site_creation_flow: 'onboarding', launchpad_screen: false },
} );
( loadExperimentAssignment as jest.Mock ).mockResolvedValue( { variationName: 'control' } );

renderWithProvider( <CustomerHome site={ testSite } /> );

await waitFor( () => expect( screen.getByTestId( 'home-content' ) ).toBeInTheDocument() );
expect( screen.queryByTestId( 'launchpad-first' ) ).not.toBeInTheDocument();
} );

it( 'shows home content when site would be eligible to show launchpad, but user is in treatment_frozen group', async () => {
mockUseExperimentResult = [ false, false ];
const testSite = makeTestSite( {
launch_status: 'unlaunched',
options: { site_creation_flow: 'onboarding', launchpad_screen: false },
} );
( loadExperimentAssignment as jest.Mock ).mockResolvedValue( {
variationName: 'treatment_frozen',
} );

renderWithProvider( <CustomerHome site={ testSite } /> );

await waitFor( () => expect( screen.getByTestId( 'home-content' ) ).toBeInTheDocument() );
expect( screen.queryByTestId( 'launchpad-first' ) ).not.toBeInTheDocument();
} );
} );
65 changes: 56 additions & 9 deletions client/state/selectors/should-show-launchpad-first.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,66 @@
import config from '@automattic/calypso-config';
import { SiteDetails, Onboard } from '@automattic/data-stores';
import { useEffect, useState } from 'react';
import { loadExperimentAssignment } from 'calypso/lib/explat';

const SiteIntent = Onboard.SiteIntent;

/**
* Determines if the launchpad should be shown first based on site createion flow
* @param {SiteDetails|undefined} site Site object
* @returns {boolean} Whether launchpad should be shown first
* @param site Site object
* @returns Whether launchpad should be shown first
*/
export const shouldShowLaunchpadFirst = ( site: SiteDetails ) => {
const isLaunchpadFirstEnabled = config.isEnabled( 'home/launchpad-first' );
const wasSiteCreatedOnboardingFlow = site?.options?.site_creation_flow === 'onboarding';
const isNotBigSkyIntent = site?.options?.site_intent !== SiteIntent.AIAssembler;
export const shouldShowLaunchpadFirst = async ( site: SiteDetails ): Promise< boolean > => {
const wasSiteCreatedOnboardingFlow = site.options?.site_creation_flow === 'onboarding';
const createdAfterExperimentStart =
( site.options?.created_at ?? '' ) > '2025-02-03T10:22:45+00:00'; // If created_at is null then this expression is false
const isBigSkyIntent = site?.options?.site_intent === SiteIntent.AIAssembler;

return isLaunchpadFirstEnabled && wasSiteCreatedOnboardingFlow && isNotBigSkyIntent;
if ( isBigSkyIntent || ! wasSiteCreatedOnboardingFlow || ! createdAfterExperimentStart ) {
return false;
}

const assignment = await loadExperimentAssignment(
'calypso_signup_onboarding_goals_first_flow_holdout_v2_20250131'
);

return assignment?.variationName === 'treatment_cumulative';
};

export default shouldShowLaunchpadFirst;
export const useShouldShowLaunchpadFirst = ( site?: SiteDetails | null ): [ boolean, boolean ] => {
const [ state, setState ] = useState< boolean | 'loading' >( 'loading' );

useEffect( () => {
let cancel = false;

const getResponse = async () => {
if ( ! site ) {
return;
}

try {
setState( 'loading' );
const result = await shouldShowLaunchpadFirst( site );
if ( ! cancel ) {
setState( result );
}
} catch ( err ) {
if ( ! cancel ) {
setState( false );
}
}
};

getResponse();

return () => {
cancel = true;
};
}, [ site ] );

if ( ! site ) {
// If the site isn't available yet we'll assume we're still loading
return [ true, false ];
}

return [ state === 'loading', state === 'loading' ? false : state ];
};
Loading

0 comments on commit ee3c4b1

Please sign in to comment.