diff --git a/client/dashboard/sites/settings-site-visibility/launch-form.tsx b/client/dashboard/sites/settings-site-visibility/launch-form.tsx
index 574cffec10e1..beb30bbed62e 100644
--- a/client/dashboard/sites/settings-site-visibility/launch-form.tsx
+++ b/client/dashboard/sites/settings-site-visibility/launch-form.tsx
@@ -132,7 +132,7 @@ export function LaunchForm( { site }: { site: Site } ) {
}
>
diff --git a/client/dashboard/sites/site-launch-button/index.tsx b/client/dashboard/sites/site-launch-button/index.tsx
index 752d0459bdc5..fcfa4ad73dd5 100644
--- a/client/dashboard/sites/site-launch-button/index.tsx
+++ b/client/dashboard/sites/site-launch-button/index.tsx
@@ -10,13 +10,13 @@ export function SiteLaunchButton( {
tracksContext,
launchUrl,
LaunchModal,
- backTo,
+ flowDestination,
}: {
site: Site;
tracksContext: string;
launchUrl?: string;
LaunchModal?: A4aLaunchModalComponent;
- backTo?: string;
+ flowDestination?: string;
} ) {
const { queries } = useAppContext();
const { recordTracksEvent } = useAnalytics();
@@ -24,7 +24,7 @@ export function SiteLaunchButton( {
const { isLoading, isExperimentLoading, isHidden, isDisabled, isBusy, href, onClick, modal } =
useSiteLaunch( site, {
tracksContext,
- backTo,
+ flowDestination,
a4aLaunchUrl: launchUrl,
a4aLaunchModal: LaunchModal,
domainsOptions: queries.domainsQuery(),
diff --git a/client/dashboard/sites/site-launch-button/test/index.test.tsx b/client/dashboard/sites/site-launch-button/test/index.test.tsx
index 12ed79c7acd0..f5290c33eecd 100644
--- a/client/dashboard/sites/site-launch-button/test/index.test.tsx
+++ b/client/dashboard/sites/site-launch-button/test/index.test.tsx
@@ -173,6 +173,87 @@ describe( '', () => {
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
} );
+ test( 'points Back at the page the launch started from, and post-launch elsewhere', async () => {
+ mockDomainsApi( [ createMockDomain( 'kaonashi.wordpress.com', false ) ] );
+ window.history.pushState( {}, '', '/sites/kaonashi.wordpress.com/settings/site-visibility' );
+
+ render(
+ ) }
+ tracksContext="test"
+ flowDestination="/sites/kaonashi.wordpress.com"
+ />
+ );
+
+ const launchLink = await screen.findByRole( 'link', { name: 'Launch your site' } );
+ const query = new URL( launchLink.getAttribute( 'href' ) ?? '', window.location.origin )
+ .searchParams;
+
+ expect( query.get( 'back_to' ) ).toContain(
+ '/sites/kaonashi.wordpress.com/settings/site-visibility'
+ );
+ expect( query.get( 'redirect_to' ) ).toContain( '/sites/kaonashi.wordpress.com' );
+ expect( query.get( 'redirect_to' ) ).not.toContain( 'site-visibility' );
+
+ window.history.pushState( {}, '', '/' );
+ } );
+
+ test( 'an immediate launch stays on the page even when a flow destination is set', async () => {
+ const user = userEvent.setup();
+ mockDomainsApi( [ createMockDomain( 'kaonashi.wordpress.com', false ) ] );
+ const launchScope = mockLaunchApi();
+
+ const originalLocation = window.location;
+ const assign = jest.fn();
+ Object.defineProperty( window, 'location', {
+ writable: true,
+ value: {
+ href: 'http://localhost/sites/kaonashi.wordpress.com/settings/site-visibility',
+ origin: 'http://localhost',
+ pathname: '/sites/kaonashi.wordpress.com/settings/site-visibility',
+ search: '',
+ assign,
+ },
+ } );
+ const replaceState = jest
+ .spyOn( window.history, 'replaceState' )
+ .mockImplementation( () => {} );
+
+ try {
+ render(
+ ) }
+ tracksContext="test"
+ flowDestination="/sites/kaonashi.wordpress.com"
+ />
+ );
+
+ const launchButton = await screen.findByRole( 'button', { name: 'Launch your site' } );
+ replaceState.mockClear();
+
+ await user.click( launchButton );
+
+ await waitFor( () => expect( launchScope.isDone() ).toBe( true ) );
+ await waitFor( () => expect( replaceState ).toHaveBeenCalled() );
+ expect( assign ).not.toHaveBeenCalled();
+ } finally {
+ replaceState.mockRestore();
+ Object.defineProperty( window, 'location', { writable: true, value: originalLocation } );
+ }
+ } );
+
test( 'renders a link to the launch flow for a free site without an immediate launch', async () => {
mockDomainsApi( [ createMockDomain( 'kaonashi.wordpress.com', false ) ] );
diff --git a/client/dashboard/sites/site-launch-button/use-site-launch.tsx b/client/dashboard/sites/site-launch-button/use-site-launch.tsx
index 243fa4fed0f7..48cf6a17f945 100644
--- a/client/dashboard/sites/site-launch-button/use-site-launch.tsx
+++ b/client/dashboard/sites/site-launch-button/use-site-launch.tsx
@@ -26,7 +26,11 @@ type RecordTracksEvent = ( eventName: string, properties?: Record< string, unkno
export interface UseSiteLaunchOptions {
tracksContext: string;
+ /** Where the launch flow's Back button returns to. Defaults to the current page. */
backTo?: string;
+ /** Where the launch flow leaves the user once the site is live. Defaults to `backTo`. */
+ flowDestination?: string;
+ /** Where an immediate launch leaves the user. Defaults to staying on the current page. */
postLaunchUrl?: string;
a4aLaunchUrl?: string;
a4aLaunchModal?: A4aLaunchModalComponent;
@@ -51,6 +55,7 @@ export function useSiteLaunch(
{
tracksContext,
backTo,
+ flowDestination,
postLaunchUrl,
a4aLaunchUrl,
a4aLaunchModal: A4aLaunchModal,
@@ -98,9 +103,10 @@ export function useSiteLaunch(
back_to: backTo
? dashboardLinkWithBackport( backTo )
: redirectToDashboardLink( { supportBackport: true } ),
+ ...( flowDestination ? { redirect_to: dashboardLinkWithBackport( flowDestination ) } : {} ),
dashboard: getCurrentDashboard(),
} );
- }, [ site, backTo ] );
+ }, [ site, backTo, flowDestination ] );
const track = () => {
recordTracksEvent( 'calypso_dashboard_site_launch_button_click', { context: tracksContext } );
diff --git a/client/signup/config/flows-pure.js b/client/signup/config/flows-pure.js
index 51c3fec9b964..bdf74116806e 100644
--- a/client/signup/config/flows-pure.js
+++ b/client/signup/config/flows-pure.js
@@ -310,8 +310,8 @@ export function generateFlows( {
steps: [ 'domains-launch', 'plans-launch', 'launch' ],
destination: getLaunchDestination,
description: 'A flow to launch a private site.',
- providesDependenciesInQuery: [ 'siteSlug', 'back_to' ],
- optionalDependenciesInQuery: [ 'back_to' ],
+ providesDependenciesInQuery: [ 'siteSlug', 'back_to', 'redirect_to' ],
+ optionalDependenciesInQuery: [ 'back_to', 'redirect_to' ],
hideProgressIndicator: true,
lastModified: '2019-11-22',
excludeFromManageSiteFlows: true,
diff --git a/client/signup/config/flows.js b/client/signup/config/flows.js
index 710fa8123b70..9259733b0d4d 100644
--- a/client/signup/config/flows.js
+++ b/client/signup/config/flows.js
@@ -157,6 +157,12 @@ export function getLaunchReturnUrl( dependencies ) {
}
function getLaunchDestination( dependencies ) {
+ // `redirect_to` lands the user somewhere other than where they came from once the site is live,
+ // so `back_to` is free to keep meaning "the page the Back button returns to".
+ if ( dependencies.redirect_to ) {
+ return addQueryArgs( { celebrateLaunch: 'true' }, dependencies.redirect_to );
+ }
+
const { url, celebrateArgs } = getLaunchReturnTarget( dependencies );
return addQueryArgs( celebrateArgs, url );
diff --git a/client/signup/controller.js b/client/signup/controller.js
index d6eb6708d3f5..4f197c407eb8 100644
--- a/client/signup/controller.js
+++ b/client/signup/controller.js
@@ -306,6 +306,13 @@ export default {
context.store.dispatch( updateDependencies( additionalDependencies ) );
}
+ if ( 'launch-site' === flowName ) {
+ // The dependency store persists between visits, and `redirect_to` is optional, so an
+ // abandoned launch would otherwise decide where the next one leaves the user. Re-read it
+ // from the query the flow was entered with.
+ context.store.dispatch( updateDependencies( { redirect_to: query?.redirect_to ?? null } ) );
+ }
+
context.primary = createElement( SignupComponent, {
store: context.store,
path: context.path,
diff --git a/client/signup/test/flows.js b/client/signup/test/flows.js
index 5e932a4c8fba..0541953493d4 100644
--- a/client/signup/test/flows.js
+++ b/client/signup/test/flows.js
@@ -55,6 +55,45 @@ describe( 'Signup Flows Configuration', () => {
} );
} );
+ describe( 'launch-site destination', () => {
+ beforeAll( () => {
+ // The suites above stub `getFlows` with fixtures; these assertions need the real config.
+ jest.restoreAllMocks();
+ } );
+
+ const getDestination = ( dependencies ) =>
+ flows.getFlows()[ 'launch-site' ].destination( dependencies );
+
+ test( 'returns the user to where the flow was started from', () => {
+ expect(
+ getDestination( {
+ siteSlug: 'test-site',
+ back_to: '/sites/test-site/settings/site-visibility',
+ } )
+ ).toBe( '/sites/test-site/settings/site-visibility?celebrateLaunch=true' );
+ } );
+
+ test( 'prefers redirect_to, so the post-launch landing can differ from where Back goes', () => {
+ expect(
+ getDestination( {
+ siteSlug: 'test-site',
+ back_to: '/sites/test-site/settings/site-visibility',
+ redirect_to: '/sites/test-site',
+ } )
+ ).toBe( '/sites/test-site?celebrateLaunch=true' );
+ } );
+
+ test( 'falls back to back_to when redirect_to was cleared on flow entry', () => {
+ expect(
+ getDestination( {
+ siteSlug: 'test-site',
+ back_to: '/sites/test-site/settings/site-visibility',
+ redirect_to: null,
+ } )
+ ).toBe( '/sites/test-site/settings/site-visibility?celebrateLaunch=true' );
+ } );
+ } );
+
describe( 'filterDestination with checkout URLs', () => {
// Mock the required modules
beforeAll( () => {