From e4f1771d282da8782f17b23ba9a5aa603782931e Mon Sep 17 00:00:00 2001 From: Paulo Trentin Date: Mon, 31 Aug 2026 17:01:14 -0300 Subject: [PATCH 1/2] Launch site: send the Back button to the page the launch started from The site visibility settings passed `backTo` to steer where the user lands after launching, but `back_to` also drives the flow's Back button, so Back went to the site home instead of the settings page the user came from. Give the post-launch landing its own query arg, `redirect_to`, and leave `back_to` meaning "the page the Back button returns to". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CfPtNr1ULYu5k2NjnKQqm4 --- .../settings-site-visibility/launch-form.tsx | 4 +-- .../sites/site-launch-button/index.tsx | 6 ++-- .../site-launch-button/test/index.test.tsx | 32 +++++++++++++++++++ .../site-launch-button/use-site-launch.tsx | 5 ++- client/signup/config/flows-pure.js | 4 +-- client/signup/config/flows.js | 6 ++++ client/signup/test/flows.js | 29 +++++++++++++++++ 7 files changed, 78 insertions(+), 8 deletions(-) diff --git a/client/dashboard/sites/settings-site-visibility/launch-form.tsx b/client/dashboard/sites/settings-site-visibility/launch-form.tsx index 574cffec10e1..bb68b3164ce2 100644 --- a/client/dashboard/sites/settings-site-visibility/launch-form.tsx +++ b/client/dashboard/sites/settings-site-visibility/launch-form.tsx @@ -10,7 +10,7 @@ import { import { createInterpolateElement } from '@wordpress/element'; import { __, _n, sprintf } from '@wordpress/i18n'; import Notice from '../../components/notice'; -import { a4aLink } from '../../utils/link'; +import { a4aLink, dashboardLinkWithBackport } from '../../utils/link'; import { SiteLaunchButton } from '../site-launch-button'; import AgencyDevelopmentSiteLaunchModal from '../site-launch-button/agency-development-site-launch-modal'; import TrialUpsellNotice from './trial-upsell-notice'; @@ -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..21f53022e728 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, + postLaunchUrl, }: { site: Site; tracksContext: string; launchUrl?: string; LaunchModal?: A4aLaunchModalComponent; - backTo?: string; + postLaunchUrl?: 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, + postLaunchUrl, 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..b22ed3610da3 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,38 @@ 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" + postLaunchUrl="https://my.wordpress.com/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' ) ).toBe( + 'https://my.wordpress.com/sites/kaonashi.wordpress.com' + ); + + window.history.pushState( {}, '', '/' ); + } ); + 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..80b13c403b15 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,9 @@ 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 user lands once the site is live. Defaults to the page they launched from. */ postLaunchUrl?: string; a4aLaunchUrl?: string; a4aLaunchModal?: A4aLaunchModalComponent; @@ -98,9 +100,10 @@ export function useSiteLaunch( back_to: backTo ? dashboardLinkWithBackport( backTo ) : redirectToDashboardLink( { supportBackport: true } ), + ...( postLaunchUrl ? { redirect_to: postLaunchUrl } : {} ), dashboard: getCurrentDashboard(), } ); - }, [ site, backTo ] ); + }, [ site, backTo, postLaunchUrl ] ); 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/test/flows.js b/client/signup/test/flows.js index 5e932a4c8fba..8579c88e9b76 100644 --- a/client/signup/test/flows.js +++ b/client/signup/test/flows.js @@ -55,6 +55,35 @@ 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' ); + } ); + } ); + describe( 'filterDestination with checkout URLs', () => { // Mock the required modules beforeAll( () => { From 9b8c273f50fedc5579a96733403d6b71bb750e32 Mon Sep 17 00:00:00 2001 From: Paulo Trentin Date: Mon, 31 Aug 2026 18:22:47 -0300 Subject: [PATCH 2/2] Launch site: keep the flow destination separate from the immediate launch `postLaunchUrl` already decided where an immediate launch leaves the user, so forwarding it as the flow's `redirect_to` made the site visibility page navigate away on the hosting-trial, staging and pre-launch-modal paths. Split the two: a new `flowDestination` feeds `redirect_to` (normalised through `dashboardLinkWithBackport()` like `backTo`), and `postLaunchUrl` keeps its original meaning. Also re-read `redirect_to` from the query on every launch-site entry, so an abandoned flow's value can't linger in the signup dependency store and decide where the next launch lands. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QS45ELrLYWcFsZz2mefDC9 --- .../settings-site-visibility/launch-form.tsx | 4 +- .../sites/site-launch-button/index.tsx | 6 +- .../site-launch-button/test/index.test.tsx | 57 +++++++++++++++++-- .../site-launch-button/use-site-launch.tsx | 9 ++- client/signup/controller.js | 7 +++ client/signup/test/flows.js | 10 ++++ 6 files changed, 81 insertions(+), 12 deletions(-) diff --git a/client/dashboard/sites/settings-site-visibility/launch-form.tsx b/client/dashboard/sites/settings-site-visibility/launch-form.tsx index bb68b3164ce2..beb30bbed62e 100644 --- a/client/dashboard/sites/settings-site-visibility/launch-form.tsx +++ b/client/dashboard/sites/settings-site-visibility/launch-form.tsx @@ -10,7 +10,7 @@ import { import { createInterpolateElement } from '@wordpress/element'; import { __, _n, sprintf } from '@wordpress/i18n'; import Notice from '../../components/notice'; -import { a4aLink, dashboardLinkWithBackport } from '../../utils/link'; +import { a4aLink } from '../../utils/link'; import { SiteLaunchButton } from '../site-launch-button'; import AgencyDevelopmentSiteLaunchModal from '../site-launch-button/agency-development-site-launch-modal'; import TrialUpsellNotice from './trial-upsell-notice'; @@ -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 21f53022e728..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, - postLaunchUrl, + flowDestination, }: { site: Site; tracksContext: string; launchUrl?: string; LaunchModal?: A4aLaunchModalComponent; - postLaunchUrl?: 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, - postLaunchUrl, + 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 b22ed3610da3..f5290c33eecd 100644 --- a/client/dashboard/sites/site-launch-button/test/index.test.tsx +++ b/client/dashboard/sites/site-launch-button/test/index.test.tsx @@ -187,7 +187,7 @@ describe( '', () => { }, } as Partial< Site > ) } tracksContext="test" - postLaunchUrl="https://my.wordpress.com/sites/kaonashi.wordpress.com" + flowDestination="/sites/kaonashi.wordpress.com" /> ); @@ -198,13 +198,62 @@ describe( '', () => { expect( query.get( 'back_to' ) ).toContain( '/sites/kaonashi.wordpress.com/settings/site-visibility' ); - expect( query.get( 'redirect_to' ) ).toBe( - 'https://my.wordpress.com/sites/kaonashi.wordpress.com' - ); + 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 80b13c403b15..48cf6a17f945 100644 --- a/client/dashboard/sites/site-launch-button/use-site-launch.tsx +++ b/client/dashboard/sites/site-launch-button/use-site-launch.tsx @@ -28,7 +28,9 @@ export interface UseSiteLaunchOptions { tracksContext: string; /** Where the launch flow's Back button returns to. Defaults to the current page. */ backTo?: string; - /** Where the user lands once the site is live. Defaults to the page they launched from. */ + /** 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; @@ -53,6 +55,7 @@ export function useSiteLaunch( { tracksContext, backTo, + flowDestination, postLaunchUrl, a4aLaunchUrl, a4aLaunchModal: A4aLaunchModal, @@ -100,10 +103,10 @@ export function useSiteLaunch( back_to: backTo ? dashboardLinkWithBackport( backTo ) : redirectToDashboardLink( { supportBackport: true } ), - ...( postLaunchUrl ? { redirect_to: postLaunchUrl } : {} ), + ...( flowDestination ? { redirect_to: dashboardLinkWithBackport( flowDestination ) } : {} ), dashboard: getCurrentDashboard(), } ); - }, [ site, backTo, postLaunchUrl ] ); + }, [ site, backTo, flowDestination ] ); const track = () => { recordTracksEvent( 'calypso_dashboard_site_launch_button_click', { context: tracksContext } ); 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 8579c88e9b76..0541953493d4 100644 --- a/client/signup/test/flows.js +++ b/client/signup/test/flows.js @@ -82,6 +82,16 @@ describe( 'Signup Flows Configuration', () => { } ) ).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', () => {