diff --git a/e2e/tests/admin/signin.test.ts b/e2e/tests/admin/signin.test.ts index 0233c08524a..bc4d3ce0552 100644 --- a/e2e/tests/admin/signin.test.ts +++ b/e2e/tests/admin/signin.test.ts @@ -35,4 +35,27 @@ test.describe('Ghost Admin - Signin Redirect', () => { await postsPage.waitForPageToFullyLoad(); }); + + test('query params on a deep link survive signin redirect', async ({page, ghostAccountOwner}) => { + // Newsletter reply-to verification emails point at + // /settings/newsletters/?verifyEmail=. If the user clicks the + // link in a browser that isn't signed in, the param needs to survive + // the signin round-trip, otherwise the verify-on-mount handler in + // newsletters.tsx no-ops and the customer thinks their reply-to + // address didn't save (ONC-1618 / ONC-1642). + await logout(page); + + await page.goto('/ghost/#/settings/newsletters/?verifyEmail=fake-token-xyz'); + + const loginPage = new LoginPage(page); + await expect(loginPage.signInButton).toBeVisible(); + + await loginPage.signIn(ghostAccountOwner.email, ghostAccountOwner.password); + + // The error modal is the signal that the React verify handler ran: + // it only renders when newsletters.tsx attempted to redeem a token + // and the API rejected it (expected, since the token is fake). + await expect(page.getByRole('heading', {name: 'Error verifying email address'})).toBeVisible(); + expect(page.url()).toContain('verifyEmail=fake-token-xyz'); + }); }); diff --git a/ghost/admin/app/services/session.js b/ghost/admin/app/services/session.js index 5ff594b8a65..6939cc26612 100644 --- a/ghost/admin/app/services/session.js +++ b/ghost/admin/app/services/session.js @@ -1,5 +1,7 @@ +import AuthConfiguration from 'ember-simple-auth/configuration'; import ESASessionService from 'ember-simple-auth/services/session'; import RSVP from 'rsvp'; +import windowProxy from 'ghost-admin/utils/window-proxy'; import {configureScope} from '@sentry/ember'; import {getOwner} from '@ember/application'; import {inject} from 'ghost-admin/decorators/inject'; @@ -91,7 +93,11 @@ export default class SessionService extends ESASessionService { const redirectUrl = window.sessionStorage.getItem('ghost-signin-redirect'); window.sessionStorage.removeItem('ghost-signin-redirect'); if (redirectUrl && !redirectUrl.startsWith('/signin') && !redirectUrl.startsWith('/signup') && !redirectUrl.startsWith('/setup')) { - this.router.transitionTo(redirectUrl); + // Hard navigate rather than router.transitionTo: the catch-all + // react-fallback route has no controller-declared queryParams, + // so transitionTo strips params like ?verifyEmail= used + // by newsletter reply-to confirmation links. + windowProxy.replaceLocation(`${AuthConfiguration.rootURL}#${redirectUrl}`); return; }