Skip to content

Commit 090d09b

Browse files
paulopmt1claude
andcommitted
Launch site: resolve back_to on click, not once per memo
The launch URL was built inside a useMemo keyed on [ site, postLaunchUrl ], but back_to now comes from redirectToDashboardLink(), which reads window.location. Neither dep changes on a client-side navigation within the same site, so back_to froze on the page the button first rendered on — and the two omnibars that render it sit outside the router and can outlive several navigations without re-rendering at all. Drop the memo so the href is fresh per render, and re-resolve the URL on click, following the fresh one when the rendered href has gone stale. Unmodified primary clicks only, so cmd/ctrl-click still opens the href in a new tab. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Su9dfy7uw3VtuXzxnC8wRo
1 parent 5bf1406 commit 090d09b

2 files changed

Lines changed: 89 additions & 6 deletions

File tree

client/dashboard/sites/site-launch-button/test/index.test.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const mockLaunchApi = () =>
4545
.reply( 200, {} );
4646

4747
describe( '<SiteLaunchButton>', () => {
48+
afterEach( () => {
49+
window.history.pushState( {}, '', '/' );
50+
} );
51+
4852
test( 'opens the pre-launch modal instead of launching immediately for a paid site with a custom domain', async () => {
4953
const user = userEvent.setup();
5054
mockDomainsApi( [
@@ -201,8 +205,62 @@ describe( '<SiteLaunchButton>', () => {
201205
expect( query.get( 'redirect_to' ) ).toBe(
202206
'https://my.wordpress.com/sites/kaonashi.wordpress.com'
203207
);
208+
} );
204209

205-
window.history.pushState( {}, '', '/' );
210+
test( 'resolves the Back target again on click, in case the page changed after render', async () => {
211+
const user = userEvent.setup();
212+
mockDomainsApi( [ createMockDomain( 'kaonashi.wordpress.com', false ) ] );
213+
window.history.pushState( {}, '', '/sites/kaonashi.wordpress.com/settings/site-visibility' );
214+
215+
render(
216+
<SiteLaunchButton
217+
site={ createMockSite( {
218+
plan: {
219+
product_slug: 'free_plan',
220+
product_name: 'Free',
221+
is_free: true,
222+
},
223+
} as Partial< Site > ) }
224+
tracksContext="test"
225+
/>
226+
);
227+
228+
const launchLink = await screen.findByRole( 'link', { name: 'Launch your site' } );
229+
230+
// The omnibars rendering this button sit outside the router and can outlive a navigation
231+
// without re-rendering, which leaves a stale `back_to` baked into the href.
232+
window.history.pushState( {}, '', '/sites/kaonashi.wordpress.com/plugins' );
233+
234+
// `Location.assign` isn't writable, so read through to the real location and swap the
235+
// object out only for the click.
236+
const realLocation = window.location;
237+
const assign = jest.fn();
238+
Object.defineProperty( window, 'location', {
239+
configurable: true,
240+
value: {
241+
get href() {
242+
return realLocation.href;
243+
},
244+
get origin() {
245+
return realLocation.origin;
246+
},
247+
get hostname() {
248+
return realLocation.hostname;
249+
},
250+
assign,
251+
},
252+
} );
253+
254+
try {
255+
await user.click( launchLink );
256+
} finally {
257+
Object.defineProperty( window, 'location', { configurable: true, value: realLocation } );
258+
}
259+
260+
expect( assign ).toHaveBeenCalledTimes( 1 );
261+
const query = new URL( assign.mock.calls[ 0 ][ 0 ] as string, window.location.origin )
262+
.searchParams;
263+
expect( query.get( 'back_to' ) ).toContain( '/sites/kaonashi.wordpress.com/plugins' );
206264
} );
207265

208266
test( 'renders a link to the launch flow for a free site without an immediate launch', async () => {

client/dashboard/sites/site-launch-button/use-site-launch.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { domainsQuery, siteLaunchMutation } from '@automattic/api-queries';
33
import { useQuery, useMutation } from '@tanstack/react-query';
44
import { __ } from '@wordpress/i18n';
55
import { addQueryArgs } from '@wordpress/url';
6-
import { useMemo, useState, type ComponentType, type ReactElement } from 'react';
6+
import { useState, type ComponentType, type MouseEvent, type ReactElement } from 'react';
77
import { useSiteLaunchGatingVariant } from 'calypso/lib/use-site-launch-gating-variant';
88
import { getCurrentDashboard } from '../../app/routing';
99
import { withSnackbar } from '../../app/snackbars/with-snackbar';
@@ -41,7 +41,7 @@ export interface UseSiteLaunchResult {
4141
isDisabled: boolean;
4242
isBusy: boolean;
4343
href?: string;
44-
onClick: () => void;
44+
onClick: ( event?: MouseEvent ) => void;
4545
modal: ReactElement | null;
4646
}
4747

@@ -78,7 +78,10 @@ export function useSiteLaunch(
7878
const isDisabled = ! getIsSitePlanLaunchable( site );
7979
const shouldImmediatelyLaunch = isSitePlanHostingTrial || site.is_wpcom_staging_site;
8080

81-
const launchUrl = useMemo( () => {
81+
// Reads `window.location`, so it must not be memoized: the omnibars that render this button
82+
// live outside the router and can outlast several navigations, which would freeze `back_to`
83+
// on the page the button first rendered on.
84+
const getLaunchUrl = () => {
8285
if ( isSitePlanBigSkyTrial( site ) ) {
8386
return addQueryArgs( wpcomLink( '/setup/ai-site-builder/domains' ), {
8487
siteId: site.ID,
@@ -97,12 +100,34 @@ export function useSiteLaunch(
97100
...( postLaunchUrl ? { redirect_to: postLaunchUrl } : {} ),
98101
dashboard: getCurrentDashboard(),
99102
} );
100-
}, [ site, postLaunchUrl ] );
103+
};
104+
105+
const launchUrl = getLaunchUrl();
101106

102107
const track = () => {
103108
recordTracksEvent( 'calypso_dashboard_site_launch_button_click', { context: tracksContext } );
104109
};
105110

111+
// Those same omnibars may not have re-rendered since the user navigated, so resolve `back_to`
112+
// again on click and follow the fresh URL when the rendered `href` has gone stale.
113+
const openLaunchUrl = ( event?: MouseEvent ) => {
114+
track();
115+
116+
if ( ! event || event.defaultPrevented ) {
117+
return;
118+
}
119+
120+
if ( event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) {
121+
return;
122+
}
123+
124+
const currentLaunchUrl = getLaunchUrl();
125+
if ( currentLaunchUrl !== launchUrl ) {
126+
event.preventDefault();
127+
window.location.assign( currentLaunchUrl );
128+
}
129+
};
130+
106131
const redirectAfterLaunch = ( options: { celebrate?: boolean } = {} ) => {
107132
const targetUrl = addQueryArgs( postLaunchUrl ?? window.location.href, {
108133
...( options.celebrate ? { celebrateLaunch: 'true' } : {} ),
@@ -212,7 +237,7 @@ export function useSiteLaunch(
212237
...baseResult,
213238
isHidden: false,
214239
href: launchUrl,
215-
onClick: track,
240+
onClick: openLaunchUrl,
216241
};
217242
}
218243
}

0 commit comments

Comments
 (0)