Skip to content

Commit 7cf0c32

Browse files
authored
fix(checkout): merchant-side escape hatch for Dodo overlay deadlock (koala73#3354)
Dodo's hosted overlay can deadlock: X-button click fires GET /api/checkout/sessions/{id}/payment-link, the 404 goes unhandled inside their React, and the resulting Maximum-update-depth render loop prevents the checkout.closed postMessage from ever escaping the iframe. Our onEvent handler never runs, the user is stuck. Add a merchant-side safety net: Escape-key listener on window that calls DodoPayments.Checkout.close() (works via the merchant-mounted iframe node, independent of the frozen inner UI), plus an auto-close from the checkout.error branch so any surfaced error doesn't leave a zombie overlay behind. Cleanup is wired into destroyCheckoutOverlay. SDK 1.8.0 has no onCancel/cancel_url/dismissBehavior option — close() is the only escape hatch Dodo exposes. Observed 2026-04-23 session cks_0NdL3CalSpBDR6vrMFIS3 from the ?embed=pro-preview iframe-in-iframe landing flow.
1 parent 26d4263 commit 7cf0c32

1 file changed

Lines changed: 38 additions & 0 deletions

File tree

src/services/checkout.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,28 @@ let initialized = false;
131131
let onSuccessCallback: (() => void) | null = null;
132132
let _resetOverlaySession: (() => void) | null = null;
133133
let _watchersInitialized = false;
134+
let _escapeHandler: ((e: KeyboardEvent) => void) | null = null;
135+
136+
/**
137+
* Dodo's hosted overlay has been observed to deadlock: the in-iframe X
138+
* button hits `GET /api/checkout/sessions/{id}/payment-link` → 404 →
139+
* unhandled rejection in their React code → Maximum-update-depth render
140+
* loop. When that happens, the `checkout.closed` postMessage never
141+
* escapes their iframe, so our onEvent handler can't clean up and the
142+
* user is trapped on the overlay. `DodoPayments.Checkout.close()`
143+
* removes the iframe at the merchant-SDK level and works even when the
144+
* inner overlay is frozen — it's the only safety net available since
145+
* CheckoutOptions has no onCancel/dismissBehavior hook (SDK 1.8.0).
146+
*/
147+
function safeCloseOverlay(): void {
148+
try {
149+
if (DodoPayments.Checkout.isOpen?.()) {
150+
DodoPayments.Checkout.close();
151+
}
152+
} catch {
153+
// Swallow — the overlay is already gone or the SDK is mid-teardown.
154+
}
155+
}
134156

135157
/**
136158
* Initialize the Dodo overlay SDK. Idempotent -- second+ calls are no-ops.
@@ -229,11 +251,23 @@ export function initCheckoutOverlay(onSuccess?: () => void): void {
229251
case 'checkout.error':
230252
console.error('[checkout] Overlay error:', event.data?.message);
231253
Sentry.captureMessage(`Dodo checkout overlay error: ${event.data?.message || 'unknown'}`, { level: 'error', tags: { component: 'dodo-checkout' } });
254+
// Release the user if their overlay surfaces an error. The
255+
// deadlock bug (payment-link 404 + render loop) never reaches
256+
// this branch — it traps inside their iframe — but any error
257+
// that DOES escape should not leave a broken overlay mounted.
258+
safeCloseOverlay();
232259
break;
233260
}
234261
},
235262
});
236263

264+
_escapeHandler = (e: KeyboardEvent) => {
265+
if (e.key === 'Escape' && DodoPayments.Checkout.isOpen?.()) {
266+
safeCloseOverlay();
267+
}
268+
};
269+
window.addEventListener('keydown', _escapeHandler);
270+
237271
initialized = true;
238272
}
239273

@@ -244,6 +278,10 @@ export function initCheckoutOverlay(onSuccess?: () => void): void {
244278
export function destroyCheckoutOverlay(): void {
245279
initialized = false;
246280
onSuccessCallback = null;
281+
if (_escapeHandler) {
282+
window.removeEventListener('keydown', _escapeHandler);
283+
_escapeHandler = null;
284+
}
247285
}
248286

249287
function loadPendingCheckoutIntent(): PendingCheckoutIntent | null {

0 commit comments

Comments
 (0)