fix(checkout): return Dodo 3DS flows to dashboard#4447
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| if (FAILED_STATUSES.has(status)) return { kind: 'failed', rawStatus: status }; | ||
| } | ||
| if (hasWmSuccess) return { kind: 'success' }; | ||
| if (hasWmReturn && !status && loadCheckoutAttempt()) return { kind: 'success' }; |
There was a problem hiding this comment.
Untested combination: Dodo ID + no status +
wm_checkout=return + saved attempt
When Dodo appends a subscription/payment ID to the return URL but omits status (e.g. /dashboard?wm_checkout=return&subscription_id=sub_X), !status is true (because status defaults to ''), so this line fires and returns { kind: 'success' } if a checkout attempt is saved. Before this PR, that same URL would have fallen through to { kind: 'none' }. The existing test at the "checkout params present but status missing" case doesn't include wm_checkout=return, so this combination is uncovered. Dodo reliably includes a status in its redirects, making this a very unlikely path — but a defensive test for ?wm_checkout=return&subscription_id=sub_X (no status, attempt saved) would lock in the intended behaviour.
2c591c0 to
2654bee
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
2654bee to
daa379f
Compare
daa379f to
a6df7f1
Compare
The 'wm_checkout'/'return' literals were defined independently in the producer (checkout-return-url.ts) and the consumer/parser (checkout-return.ts). A rename on one side would silently break 3DS return detection while each file's own tests kept passing, since each hardcodes its expected string. Export CHECKOUT_RETURN_PARAM / CHECKOUT_RETURN_MARKER from the producer (which owns the URL contract) and import them into the parser, collapsing the duplicate literals into one source of truth so a rename becomes a compile-time break. No behavior change: handleCheckoutReturn still reads the same param and honors the same marker value; WM_MARKER_SUCCESS stays local (the /pro overlay-success value no producer module writes). Addresses the P2 maintainability finding from code review. Claude-Session: https://claude.ai/code/session_017L8nACBdvQ5Sj7mTBfEBhy
…e redirect guard #1 (P1) /pro pre-marked success: /pro sent Dodo `?wm_checkout=success` as the return_url. With hosted redirect now the primary flow Dodo sends the buyer there for EVERY outcome — a failed/cancelled/pending/no-ID return false-succeeded via the bare success marker (checkout-return.ts:113). Switch /pro to the GUARDED `?wm_checkout=return` contract (mirrors the dashboard / #4447): success reconciles only against authoritative Dodo evidence (subscription_id/payment_id + success status); a non-success return can no longer succeed. #3 (P2) untested open-redirect guard: extract safeHostedCheckoutUrl + HOSTED_CHECKOUT_HOSTS to src/services/hosted-checkout-url.ts (dependency-free, imported by checkout.ts) and add tests/hosted-checkout-url.test.mts — http downgrade, third-party host, look-alike suffix host, unlisted subdomain, javascript: URL, unparseable/empty strings, non-string inputs, plus both valid hosts. 10 cases. #4 (P2) stale comments: update the checkout.ts and pro-test file headers + the dormant redirect_requested comment so they describe redirect as the active flow and the overlay machinery as dormant. #2 (P2 /pro test coverage): not addressed — pro-test has no test runner; adding one is a separate infra task. The shared validator (#3) and the dashboard navigation test cover the redirect logic, which /pro mirrors. Rebuilt public/pro bundle. Refs #4449 #4454.
…y iframe (#4449) (#4454) * fix(checkout): redirect to hosted Dodo checkout instead of the overlay iframe (#4449) The overlay-checkout iframe cannot host Dodo's nested 3DS/fraud stack (Hyperswitch → Airwallex → Sardine): the device sensors it fingerprints with are blocked two frames deep by BOTH our Permissions-Policy AND the Dodo SDK's own iframe `allow` attribute — HAR-confirmed, loosening our header to `*` changed nothing (#4450). Card payments requiring 3DS hung at "Processing…" forever. Switch both checkout surfaces to navigate the top window to Dodo's HOSTED checkout (their documented primary flow): 3DS/fraud run unconstrained top-level, and #4447 returns the customer to /dashboard?wm_checkout=return to reconcile. - src/services/checkout.ts (dashboard) + pro-test/src/services/checkout.ts (/pro): replace DodoPayments.Checkout.open(checkout_url) with a validated window.location.assign. New safeHostedCheckoutUrl() guards open-redirect (https + checkout.dodopayments.com origin only); a missing/untrusted URL falls through to the existing contract-violation handler. - Overlay machinery (Initialize/onEvent/watchdog/openCheckout) left dormant pending removal — the overlay SDK is now never loaded on the dashboard. - Test: checkout-overlay-lifecycle asserts startCheckout navigates (assignedUrls) and does NOT open the overlay (openedUrls empty). Red→green. Refs #4449 #4450 * fix(checkout): address #4454 review — guard /pro return URL + test the redirect guard #1 (P1) /pro pre-marked success: /pro sent Dodo `?wm_checkout=success` as the return_url. With hosted redirect now the primary flow Dodo sends the buyer there for EVERY outcome — a failed/cancelled/pending/no-ID return false-succeeded via the bare success marker (checkout-return.ts:113). Switch /pro to the GUARDED `?wm_checkout=return` contract (mirrors the dashboard / #4447): success reconciles only against authoritative Dodo evidence (subscription_id/payment_id + success status); a non-success return can no longer succeed. #3 (P2) untested open-redirect guard: extract safeHostedCheckoutUrl + HOSTED_CHECKOUT_HOSTS to src/services/hosted-checkout-url.ts (dependency-free, imported by checkout.ts) and add tests/hosted-checkout-url.test.mts — http downgrade, third-party host, look-alike suffix host, unlisted subdomain, javascript: URL, unparseable/empty strings, non-string inputs, plus both valid hosts. 10 cases. #4 (P2) stale comments: update the checkout.ts and pro-test file headers + the dormant redirect_requested comment so they describe redirect as the active flow and the overlay machinery as dormant. #2 (P2 /pro test coverage): not addressed — pro-test has no test runner; adding one is a separate infra task. The shared validator (#3) and the dashboard navigation test cover the redirect logic, which /pro mirrors. Rebuilt public/pro bundle. Refs #4449 #4454. * fix(checkout): #4454 review round 2 — drop dead /pro overlay SDK load + Sentry parity + docs-stats CI docs-stats: the new src/services/hosted-checkout-url.ts bumped serviceTopLevelEntries 187→188 — regenerate docs/generated/stats.json and update the AGENTS.md count. Greptile (a) dead overlay SDK load on /pro: App.tsx called initOverlay() on mount, which dynamically imported the heavy `dodopayments-checkout` SDK and registered a success banner that can never fire under redirect mode (the buyer lands on the dashboard, not /pro). Remove the call (+ now-unused imports). Symmetric with the dashboard, which simply stopped invoking its overlay init. Bonus: initOverlay is now an unused export, so the bundler tree-shakes the overlay SDK chunk out of the /pro bundle entirely (index.esm-*.js removed). Greptile (b) Sentry parity: the /pro missing/untrusted checkout_url path was console.error-only; add a Sentry.captureMessage so the server-contract violation is observable, matching the dashboard's missing-checkout-url path. Rebuilt public/pro. Refs #4449 #4454.
Summary
/dashboard?wm_checkout=returninstead of bare origin/Closes #4434.
Validation
./node_modules/.bin/tsx --test tests/checkout-return-url.test.mts tests/checkout-return-discriminant.test.mts tests/checkout-overlay-lifecycle.test.mtsnpm run typechecknode --test tests/deploy-config.test.mjsgit diff --checkReview Notes
/dashboard?wm_checkout=returnas a possible gap.wm-last-checkout-attemptsession record, and by explicitly testing the no-attempt/no-Dodo-ID spoof cases.