🐛 Fixed paid checkout returning to homepage instead of originating page#28485
Conversation
Walkthrough
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
79475f1 to
15fb105
Compare
|
| 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
15fb105 to
6b90006
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/portal/test/api.test.js (1)
231-317: ⚡ Quick winWell-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:
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=successThis would verify that existing params are preserved when
stripe=successis added.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
startsWithsame-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
📒 Files selected for processing (3)
apps/portal/package.jsonapps/portal/src/utils/api.jsapps/portal/test/api.test.js

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()inapps/portal/src/utils/api.jsalready derived a contextualcancelUrlfromwindow.locationwhen none was supplied, but had no equivalent fallback forsuccessUrl. The two standard callers (signup and upgrade inapps/portal/src/actions.js) don't pass one, sosuccessUrlarrived at the backend asundefinedand Ghost fell back to the configured site-root default (stripe-api.js→checkoutSessionSuccessUrl).Fix
Derive a contextual
successUrlfromwindow.locationwhen none is supplied, mirroring the existingcancelUrllogic. This is the same approachcontinueGiftCheckout()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
_generateSuccessUrl); the contextual URL only applies when no welcome page is set.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.jscovering:successUrl/cancelUrlderived from the current page when none suppliedsuccessUrl/cancelUrlare preservedref https://linear.app/ghost/issue/PLA-86
fixes #28369