mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(checkout): implement checkout.redirect_requested — the Dodo handler we were missing (#3346)
* fix(checkout): implement checkout.redirect_requested — the Dodo handler we were missing (revert #3298) Buyers got stuck on /pro after successful Dodo payment because NEITHER pro-test nor dashboard checkout services handled `checkout.redirect_requested` — the event Dodo's SDK fires under `manualRedirect: true` carrying the URL the MERCHANT must navigate to. We were only listening for `checkout.status`, so navigation never happened for Safari users (saw the orphaned about:blank tab). PR #3298 chased the wrong theory (flipped /pro to `manualRedirect: false`, hoping the SDK would auto-navigate). Dodo docs explicitly say that mode disables BOTH `checkout.status` AND `checkout.redirect_requested` ("only when manualRedirect is enabled"), and the SDK's internal redirect is where Safari breaks. Fix: - Revert /pro to `manualRedirect: true` - Add `checkout.redirect_requested` handler in BOTH /pro and dashboard: `window.location.href = event.data.message.redirect_to` - Align `status` read to docs-documented `event.data.message.status` only (drop legacy top-level `.status` guess) - `checkout.link_expired` logged to Sentry (follow-up if volume warrants UX) - Rebuilt public/pro/ bundle on Node 22 (new hash: index-CiMZEtgt.js) Docs: https://docs.dodopayments.com/developer-resources/overlay-checkout ## Test plan - [ ] Vercel preview: complete Dodo test-mode checkout on /pro with 4242 card. Verify console shows `[checkout] dodo event checkout.status {status: "succeeded"}` followed by `checkout.redirect_requested`, and the tab navigates to worldmonitor.app/?wm_checkout=success&... WITHOUT an about:blank second tab. - [ ] Vercel preview: same flow with a 3DS-required test card. - [ ] Vercel preview: dashboard in-app upgrade click → overlay → success → same-origin navigation lands on worldmonitor.app with Dodo's appended ?payment_id=...&status=succeeded&... - [ ] Post-deploy: Sentry breadcrumbs show full event sequence on every success; no new "stuck after paying" user reports in 24h. ## Rollback Single `git revert` + bundle rebuild. Fallback state is PR #3298's broken-for-Safari `manualRedirect: false`. * chore(ci): retrigger Vercel preview build — initial push skipped via ignore-step
This commit is contained in:
@@ -136,20 +136,41 @@ export function initOverlay(onSuccess?: () => void): void {
|
||||
// payment_id) depending on event type, and anything logged here
|
||||
// lands in Sentry breadcrumbs via the console integration.
|
||||
const data = event.data as Record<string, unknown> | undefined;
|
||||
const status = data?.status
|
||||
?? (data?.message as Record<string, unknown> | undefined)?.status;
|
||||
const msg = data?.message as Record<string, unknown> | undefined;
|
||||
const status = msg?.status as string | undefined;
|
||||
console.info('[checkout] dodo event', event.event_type,
|
||||
status !== undefined ? { status } : undefined);
|
||||
|
||||
// Dodo's documented `manualRedirect: true` flow emits TWO events
|
||||
// on terminal success: `checkout.status` for UI updates, and
|
||||
// `checkout.redirect_requested` carrying the URL WE must navigate
|
||||
// to. The SDK explicitly hands navigation to the merchant in this
|
||||
// mode — ignoring `checkout.redirect_requested` is what stranded
|
||||
// users after paying (docs: overlay-checkout.mdx, inline-checkout.mdx).
|
||||
//
|
||||
// Status shape is ONLY `event.data.message.status` per docs — the
|
||||
// legacy top-level `event.data.status` read was a guess against
|
||||
// an older SDK version and most likely never matched.
|
||||
if (event.event_type === 'checkout.status' && status === 'succeeded') {
|
||||
// Best-effort: with `manualRedirect: false` the SDK performs
|
||||
// `window.location.href = redirect_to` on a sibling
|
||||
// `checkout.redirect` event, and that navigation can race
|
||||
// with this callback. Callers should treat any side effects
|
||||
// here as a bonus, not a guarantee. The authoritative success
|
||||
// path is the `?wm_checkout=success` bridge on
|
||||
// worldmonitor.app that the SDK's redirect lands on.
|
||||
onSuccess?.();
|
||||
}
|
||||
if (event.event_type === 'checkout.redirect_requested') {
|
||||
const redirectTo = msg?.redirect_to as string | undefined;
|
||||
// Dodo builds redirect_to from the return_url we sent, appending
|
||||
// payment_id/subscription_id/status/license_key/email per
|
||||
// changelog v1.84.0. Our return_url carries `?wm_checkout=success`
|
||||
// so the dashboard bridge (src/services/checkout-return.ts) fires
|
||||
// regardless of Dodo's appended params.
|
||||
window.location.href = redirectTo || 'https://worldmonitor.app/?wm_checkout=success';
|
||||
}
|
||||
if (event.event_type === 'checkout.link_expired') {
|
||||
// Not user-blocking — log-only for now; follow-up if Sentry
|
||||
// shows volume.
|
||||
Sentry.captureMessage('Dodo checkout link expired', {
|
||||
level: 'info',
|
||||
tags: { surface: 'pro-marketing', code: 'link_expired' },
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}).catch((err) => {
|
||||
@@ -320,15 +341,16 @@ async function doCheckout(
|
||||
DodoPayments.Checkout.open({
|
||||
checkoutUrl: result.checkout_url,
|
||||
options: {
|
||||
// manualRedirect: false — Dodo performs the parent-window
|
||||
// redirect on success, landing at the `returnUrl` we sent to
|
||||
// /api/create-checkout (which carries ?wm_checkout=success).
|
||||
// Relying on the SDK's own redirect avoids a class of bugs
|
||||
// where `checkout.status=succeeded` never reaches our onEvent
|
||||
// (iframe internally navigates to wallet-return, postMessage
|
||||
// gets lost) and the user is stuck on /pro#pricing with the
|
||||
// overlay open.
|
||||
manualRedirect: false,
|
||||
// manualRedirect: true — Dodo emits `checkout.redirect_requested`
|
||||
// with the final redirect URL and the MERCHANT performs the
|
||||
// navigation. Reverting PR #3298's `false`: that mode disables
|
||||
// both `checkout.status` and `checkout.redirect_requested` events
|
||||
// (docs: "only when manualRedirect is enabled") and depends on
|
||||
// the SDK's internal redirect, which fails for Safari users
|
||||
// (stuck on a spinner with an orphaned about:blank tab). The
|
||||
// correct flow per docs is manualRedirect:true + a
|
||||
// checkout.redirect_requested handler — see onEvent above.
|
||||
manualRedirect: true,
|
||||
themeConfig: {
|
||||
dark: {
|
||||
bgPrimary: '#0d0d0d',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -144,7 +144,7 @@
|
||||
}
|
||||
</script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
||||
<script type="module" crossorigin src="/pro/assets/index-B7bmlZIg.js"></script>
|
||||
<script type="module" crossorigin src="/pro/assets/index-CiMZEtgt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/pro/assets/index-xSEP0-ib.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -164,14 +164,12 @@ export function initCheckoutOverlay(onSuccess?: () => void): void {
|
||||
onEvent: (event: CheckoutEvent) => {
|
||||
switch (event.event_type) {
|
||||
case 'checkout.status': {
|
||||
// Dodo SDK has emitted `event.data.status` in some versions and
|
||||
// `event.data.message.status` in others (the /pro build reads both
|
||||
// already; main app was only reading the first, so successes went
|
||||
// unnoticed whenever the SDK used the nested shape). Read both.
|
||||
// Docs-documented shape is ONLY `event.data.message.status` —
|
||||
// the prior top-level `event.data.status` read was a guess
|
||||
// against an older SDK version and most likely never matched.
|
||||
// (overlay-checkout.mdx / inline-checkout.mdx, SDK >= 0.109.2).
|
||||
const rawData = event.data as Record<string, unknown> | undefined;
|
||||
const status = typeof rawData?.status === 'string'
|
||||
? rawData.status
|
||||
: (rawData?.message as Record<string, unknown> | undefined)?.status;
|
||||
const status = (rawData?.message as Record<string, unknown> | undefined)?.status;
|
||||
if (status === 'succeeded') {
|
||||
successFired = true;
|
||||
onSuccessCallback?.();
|
||||
@@ -215,6 +213,19 @@ export function initCheckoutOverlay(onSuccess?: () => void): void {
|
||||
clearPendingCheckoutIntent();
|
||||
}
|
||||
break;
|
||||
case 'checkout.redirect_requested': {
|
||||
// With `manualRedirect: true` (below), Dodo's SDK hands the
|
||||
// final navigation to the merchant via this event. Dodo's own
|
||||
// redirect path (manualRedirect:false) has been observed to
|
||||
// fail on Safari with an orphaned about:blank tab; we follow
|
||||
// the docs-prescribed handler instead.
|
||||
// (overlay-checkout.mdx: "Redirect the customer manually".)
|
||||
const redirectTo = (event.data?.message as Record<string, unknown> | undefined)?.redirect_to as string | undefined;
|
||||
if (redirectTo) {
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'checkout.error':
|
||||
console.error('[checkout] Overlay error:', event.data?.message);
|
||||
Sentry.captureMessage(`Dodo checkout overlay error: ${event.data?.message || 'unknown'}`, { level: 'error', tags: { component: 'dodo-checkout' } });
|
||||
|
||||
Reference in New Issue
Block a user