Skip to content

🐛 Fixed paid checkout returning to homepage instead of originating page#28485

Merged
9larsons merged 1 commit into
mainfrom
claude/pensive-bose-89dcb8
Jun 18, 2026
Merged

🐛 Fixed paid checkout returning to homepage instead of originating page#28485
9larsons merged 1 commit into
mainfrom
claude/pensive-bose-89dcb8

Conversation

@9larsons

Copy link
Copy Markdown
Contributor

Summary

When a member starts a paid signup or upgrade from a page other than the homepage, Portal returned them to the site root after a successful Stripe Checkout (e.g. https://example.com/?stripe=success) instead of the page where checkout began. Cancelling checkout already returned to the originating page, so the behaviour was inconsistent — and especially disruptive for paywall flows, where a member who just paid to unlock an article was bounced to the homepage instead of back to the article.

Root cause

checkoutPlan() in apps/portal/src/utils/api.js already derived a contextual cancelUrl from window.location when none was supplied, but had no equivalent fallback for successUrl. The two standard callers (signup and upgrade in apps/portal/src/actions.js) don't pass one, so successUrl arrived at the backend as undefined and Ghost fell back to the configured site-root default (stripe-api.jscheckoutSessionSuccessUrl).

Fix

Derive a contextual successUrl from window.location when none is supplied, mirroring the existing cancelUrl logic. This is the same approach continueGiftCheckout() already uses for gift flows (and matches #27643 / #27827).

No backend change is needed: sanitizeReturnUrl() already enforces that return URLs are same-origin / within the site subpath, so the contextual URL flows through safely.

Behaviour preserved

  • Welcome-page tiers — the server still overrides the success URL with a configured tier welcome page (_generateSuccessUrl); the contextual URL only applies when no welcome page is set.
  • New-email signup — the contextual URL becomes the post-signup magic-link referrer, so the member returns to the originating page after confirming (an improvement over the previous site-root referrer).

Tests

Added unit tests in apps/portal/test/api.test.js covering:

  • contextual successUrl/cancelUrl derived from the current page when none supplied
  • fallback to the site root when the current location is off-site (open-redirect guard)
  • explicitly-supplied successUrl/cancelUrl are preserved

ref https://linear.app/ghost/issue/PLA-86
fixes #28369

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

checkoutPlan in api.js gains a conditional block that initializes successUrl when it is not supplied: if the current window.location.href shares the same origin as siteUrl, that href is used as the base; otherwise siteUrl is used, and stripe=success is set as a query parameter on the resulting URL. This mirrors the existing cancelUrl fallback. A new Portal API plan checkout test suite is added in api.test.js with setup that overrides window.location and stubs window.fetch, covering the same-origin default, off-site fallback, and explicit URL passthrough scenarios. The package version is bumped to 2.69.5.

Possibly related issues

Possibly related PRs

  • TryGhost/Ghost#27845: Both PRs modify Stripe checkout session URL derivation in checkoutPlan, with overlapping logic for same-origin/subpath detection when building successUrl/cancelUrl defaults.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: returning users to their originating page after successful checkout instead of the homepage.
Description check ✅ Passed The description provides comprehensive context about the bug, root cause, fix, behavior preservation, and test coverage—all directly related to the changeset.
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 claude/pensive-bose-89dcb8

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.

@9larsons 9larsons force-pushed the claude/pensive-bose-89dcb8 branch from 79475f1 to 15fb105 Compare June 18, 2026 14:13
@nx-cloud

nx-cloud Bot commented Jun 18, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 15fb105

Command Status Duration Result
nx build @tryghost/announcement-bar ✅ Succeeded <1s View ↗
nx build @tryghost/signup-form ✅ Succeeded <1s View ↗
nx build @tryghost/activitypub ✅ Succeeded 1s View ↗
nx build @tryghost/comments-ui ✅ Succeeded <1s View ↗
nx build @tryghost/sodo-search ✅ Succeeded <1s View ↗
nx build @tryghost/portal ✅ Succeeded <1s View ↗
nx build @tryghost/admin-toolbar ✅ Succeeded <1s View ↗
nx run-many -t test:unit -p @tryghost/portal ✅ Succeeded 58s View ↗
Additional runs (5) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-18 14:39:08 UTC

ref https://linear.app/ghost/issue/PLA-86
ref #28369

- checkoutPlan() already derived a contextual cancelUrl from window.location but had no equivalent fallback for successUrl, so the standard signup/upgrade callers omitted it and the backend fell back to the site-root default — landing paid members on the homepage after Stripe Checkout instead of the article they just unlocked
- mirrors the existing cancelUrl logic and continueGiftCheckout(), which already does this for gift flows
- backend sanitizeReturnUrl() already enforces same-origin, so the contextual URL flows through safely with no server change needed
@9larsons 9larsons force-pushed the claude/pensive-bose-89dcb8 branch from 15fb105 to 6b90006 Compare June 18, 2026 14:33
@9larsons 9larsons marked this pull request as ready for review June 18, 2026 14:38
@9larsons 9larsons enabled auto-merge (squash) June 18, 2026 14:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/portal/test/api.test.js (1)

231-317: ⚡ Quick win

Well-structured test coverage for the new successUrl derivation logic.

The three test cases cover the primary scenarios: same-origin default, off-site fallback, and explicit URL preservation. The mock setup correctly stubs both the session and checkout endpoints.

Optional: Consider additional edge-case coverage

While the current tests cover the main branches well, you might consider adding tests for:

  1. Current page with existing query parameters:

    window.location.href = 'https://example.com/article?utm_source=newsletter';
    // Expected: https://example.com/article?utm_source=newsletter&stripe=success

    This would verify that existing params are preserved when stripe=success is added.

  2. Site URL with a path component:

    const ghostApi = setupGhostApi({siteUrl: 'https://example.com/blog'});
    window.location.href = 'https://example.com/blog/post';

    This would verify the startsWith same-origin check works correctly with path-based site URLs.

These scenarios should work correctly with the current implementation but explicit coverage would increase confidence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/portal/test/api.test.js` around lines 231 - 317, Add two additional test
cases to the Portal API plan checkout test suite to improve edge-case coverage.
First, add a test that verifies when the current page URL contains existing
query parameters (like utm_source), those parameters are preserved when the
stripe=success and stripe=cancel parameters are appended to the successUrl and
cancelUrl respectively. Second, add a test that verifies the same-origin check
in the successUrl derivation logic works correctly when the siteUrl has a path
component (e.g., https://example.com/blog), ensuring that both the siteUrl and
window.location.href with matching path prefixes are correctly identified as
same-origin. Both tests should follow the existing pattern of setting up a
ghostApi with setupGhostApi, calling mockCheckoutFetch, invoking
ghostApi.member.checkoutPlan with appropriate parameters, and asserting the
correct body values using lastCheckoutBody.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/portal/test/api.test.js`:
- Around line 231-317: Add two additional test cases to the Portal API plan
checkout test suite to improve edge-case coverage. First, add a test that
verifies when the current page URL contains existing query parameters (like
utm_source), those parameters are preserved when the stripe=success and
stripe=cancel parameters are appended to the successUrl and cancelUrl
respectively. Second, add a test that verifies the same-origin check in the
successUrl derivation logic works correctly when the siteUrl has a path
component (e.g., https://example.com/blog), ensuring that both the siteUrl and
window.location.href with matching path prefixes are correctly identified as
same-origin. Both tests should follow the existing pattern of setting up a
ghostApi with setupGhostApi, calling mockCheckoutFetch, invoking
ghostApi.member.checkoutPlan with appropriate parameters, and asserting the
correct body values using lastCheckoutBody.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b9b1eca1-d1c8-4ec9-95f4-befb719626c2

📥 Commits

Reviewing files that changed from the base of the PR and between 0aeb5a5 and 6b90006.

📒 Files selected for processing (3)
  • apps/portal/package.json
  • apps/portal/src/utils/api.js
  • apps/portal/test/api.test.js

@9larsons 9larsons merged commit 3f953ef into main Jun 18, 2026
47 checks passed
@9larsons 9larsons deleted the claude/pensive-bose-89dcb8 branch June 18, 2026 14:49
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.

Portal paid checkout returns to site root instead of originating page

1 participant