Skip to content

🐛 Fixed newsletter reply-to verification links failing after signin#27660

Open
9larsons wants to merge 1 commit intomainfrom
steve-onc-1658-preserve-query-params-after-signin-redirect
Open

🐛 Fixed newsletter reply-to verification links failing after signin#27660
9larsons wants to merge 1 commit intomainfrom
steve-onc-1658-preserve-query-params-after-signin-redirect

Conversation

@9larsons
Copy link
Copy Markdown
Contributor

@9larsons 9larsons commented May 4, 2026

ref https://linear.app/ghost/issue/ONC-1658/

Summary

When a customer clicks a newsletter reply-to verification email in a browser that isn't signed in to admin, the link silently no-ops — they have to click it a second time for the address to actually save. Two customers hit this in the past few weeks (ONC-1618, ONC-1642).

The signin-redirect flow added in #27316 stores the original URL in sessionStorage and replays it via router.transitionTo(redirectUrl) after auth. But react-fallback is a wildcard route with no controller-declared queryParams, so Ember strips ?verifyEmail=<token> on the replay. The verify-on-mount useEffect in newsletters.tsx then short-circuits because the token is missing.

This swaps the soft transitionTo for windowProxy.replaceLocation — already the auth-flow primitive used in authenticated.js:20 and application.js:113. A hard navigation feeds the URL straight to the browser and sidesteps Ember's URL-rebuilding entirely, so the query param survives byte-for-byte.

Test plan — base case

  1. With pnpm dev running, sign out of admin.
  2. Paste into the address bar: http://localhost:2368/ghost/#/settings/newsletters/?verifyEmail=fake-token-xyz
  3. You'll be redirected to signin. Sign in.
  4. Expected: address bar still shows ?verifyEmail=fake-token-xyz, and within a moment an "Error verifying email address" / "Verification link has expired" modal appears (because the token is fake — that's success: it means the round-trip worked and the API was actually called).
  5. Pre-fix behaviour: address bar shows /settings/newsletters/ with no query param, no modal — the silent no-op customers reported.

Test plan — regression checks

  • Deep-link to a no-query-param React route while logged out: /ghost/#/tags → signin → land on Tags. (covered by existing e2e)
  • Deep-link to an Ember route while logged out: /ghost/#/posts → signin → land on Posts. (covered by existing e2e)
  • Normal signin from /ghost/ (no sessionStorage redirect): land on home/dashboard as before.
  • End-to-end: trigger a real reply-to verification email by editing a newsletter's Reply-to address, grab the link from Mailpit (http://localhost:8025), open it in an incognito window, sign in, confirm the "Reply-to address verified" success modal.

Automated coverage

Added an e2e in e2e/tests/admin/signin.test.ts that round-trips a ?verifyEmail= deep link through signin and asserts both that the URL retains the param and that the verify error modal renders (which only happens if newsletters.tsx actually called the API). Verified to fail on the unfixed code.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0588af6f-7cfa-44f6-9df6-5c7f9132e38f

📥 Commits

Reviewing files that changed from the base of the PR and between d58e24a and a62dfc5.

📒 Files selected for processing (2)
  • e2e/tests/admin/signin.test.ts
  • ghost/admin/app/services/session.js
✅ Files skipped from review due to trivial changes (1)
  • e2e/tests/admin/signin.test.ts

Walkthrough

Adds a Playwright e2e test that navigates to a Ghost admin React route with a verifyEmail=fake-token-xyz query parameter, signs in, and asserts the final URL still contains the query parameter and that an “Error verifying email address” heading appears. Updates SessionService.handleAuthentication to use windowProxy.replaceLocation(AuthConfiguration.rootURL + '#' + redirectUrl) for certain redirects instead of router.transitionTo, and imports windowProxy.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: preserving query parameters in newsletter verification links after signin, which is the primary change across both modified files.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the root cause, the solution, and providing detailed test plans and automated coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch steve-onc-1658-preserve-query-params-after-signin-redirect

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
e2e/tests/admin/signin.test.ts (2)

39-39: 💤 Low value

Test name doesn't follow the required 'what is tested - expected outcome' format.

The guideline requires a hyphen-separated format. Consider:

-    test('query params on a deep link survive signin redirect', async ({page, ghostAccountOwner}) => {
+    test('deep link query params - survive signin redirect', async ({page, ghostAccountOwner}) => {

As per coding guidelines: "Test names should follow format: 'what is tested - expected outcome' in lowercase."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/admin/signin.test.ts` at line 39, Rename the test declaration in
signin.test.ts so its description follows the required "what is tested -
expected outcome" lowercase hyphen-separated format; for example, change the
test name used in the test(...) call from "query params on a deep link survive
signin redirect" to "deep link query params - survive signin redirect" (update
the test(...) first argument where the test is declared).

40-45: 💤 Low value

Trim verbose comments — prefer clear names over explanatory prose.

The 6-line preamble (lines 40–45) and the 3-line inline note (lines 55–57) add noise that the test name and assertion message can convey. A single short comment anchoring the ticket reference, or none at all, aligns better with the guideline.

✂️ Suggested trim
     test('deep link query params - survive signin redirect', async ({page, ghostAccountOwner}) => {
-        // Newsletter reply-to verification emails point at
-        // /settings/newsletters/?verifyEmail=<token>. 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');
     });

As per coding guidelines: "Prefer less comments and giving things clear names in E2E tests."

Also applies to: 55-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/admin/signin.test.ts` around lines 40 - 45, Remove the verbose
6-line preamble and the 3-line inline note around the newsletter reply-to
verification in the signin E2E test; instead shorten to a single brief comment
with the ticket reference (e.g., "ONC-1618") or remove it entirely and rely on a
clear test name and assertion message in the test that verifies the
/settings/newsletters/?verifyEmail=<token> round-trip; apply the same trimming
to the comment block at lines 55–57 so the test remains readable and
self-explanatory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/tests/admin/signin.test.ts`:
- Line 39: Rename the test declaration in signin.test.ts so its description
follows the required "what is tested - expected outcome" lowercase
hyphen-separated format; for example, change the test name used in the test(...)
call from "query params on a deep link survive signin redirect" to "deep link
query params - survive signin redirect" (update the test(...) first argument
where the test is declared).
- Around line 40-45: Remove the verbose 6-line preamble and the 3-line inline
note around the newsletter reply-to verification in the signin E2E test; instead
shorten to a single brief comment with the ticket reference (e.g., "ONC-1618")
or remove it entirely and rely on a clear test name and assertion message in the
test that verifies the /settings/newsletters/?verifyEmail=<token> round-trip;
apply the same trimming to the comment block at lines 55–57 so the test remains
readable and self-explanatory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ef12b23-2915-4d72-b4e0-1aa64c91e3de

📥 Commits

Reviewing files that changed from the base of the PR and between aff981f and d58e24a.

📒 Files selected for processing (2)
  • e2e/tests/admin/signin.test.ts
  • ghost/admin/app/services/session.js

Copy link
Copy Markdown
Member

@jonatansberg jonatansberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thank you for fixing this. Tested and verified locally as well.

ref https://linear.app/ghost/issue/ONC-1658/

- when a customer clicks a newsletter reply-to verification email in a
  browser that isn't signed in to admin, the link sends them through the
  signin redirect introduced in #27316 — but the post-signin replay used
  router.transitionTo, which drops query params not declared on the
  resolved route's controller
- react-fallback is a wildcard route with no queryParams config, so the
  ?verifyEmail=<token> param was silently stripped on replay; the React
  verify-on-mount handler in newsletters.tsx then no-ops because the
  token is missing, and the customer thinks the address didn't save
  (root cause for ONC-1618 and ONC-1642)
- swap to windowProxy.replaceLocation, which is already the auth-flow
  primitive used in authenticated.js and application.js — the hard
  navigation feeds the URL straight to the browser and bypasses
  Ember's URL-rebuilding entirely
- e2e covers the round-trip; verified to fail on unfixed code
@rob-ghost rob-ghost force-pushed the steve-onc-1658-preserve-query-params-after-signin-redirect branch from d58e24a to a62dfc5 Compare May 5, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants