mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(checkout): merchant-side escape hatch for Dodo overlay deadlock (#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.
This commit is contained in:
@@ -131,6 +131,28 @@ let initialized = false;
|
||||
let onSuccessCallback: (() => void) | null = null;
|
||||
let _resetOverlaySession: (() => void) | null = null;
|
||||
let _watchersInitialized = false;
|
||||
let _escapeHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Dodo's hosted overlay has been observed to deadlock: the in-iframe X
|
||||
* button hits `GET /api/checkout/sessions/{id}/payment-link` → 404 →
|
||||
* unhandled rejection in their React code → Maximum-update-depth render
|
||||
* loop. When that happens, the `checkout.closed` postMessage never
|
||||
* escapes their iframe, so our onEvent handler can't clean up and the
|
||||
* user is trapped on the overlay. `DodoPayments.Checkout.close()`
|
||||
* removes the iframe at the merchant-SDK level and works even when the
|
||||
* inner overlay is frozen — it's the only safety net available since
|
||||
* CheckoutOptions has no onCancel/dismissBehavior hook (SDK 1.8.0).
|
||||
*/
|
||||
function safeCloseOverlay(): void {
|
||||
try {
|
||||
if (DodoPayments.Checkout.isOpen?.()) {
|
||||
DodoPayments.Checkout.close();
|
||||
}
|
||||
} catch {
|
||||
// Swallow — the overlay is already gone or the SDK is mid-teardown.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Dodo overlay SDK. Idempotent -- second+ calls are no-ops.
|
||||
@@ -229,11 +251,23 @@ export function initCheckoutOverlay(onSuccess?: () => void): void {
|
||||
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' } });
|
||||
// Release the user if their overlay surfaces an error. The
|
||||
// deadlock bug (payment-link 404 + render loop) never reaches
|
||||
// this branch — it traps inside their iframe — but any error
|
||||
// that DOES escape should not leave a broken overlay mounted.
|
||||
safeCloseOverlay();
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
_escapeHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && DodoPayments.Checkout.isOpen?.()) {
|
||||
safeCloseOverlay();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', _escapeHandler);
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@@ -244,6 +278,10 @@ export function initCheckoutOverlay(onSuccess?: () => void): void {
|
||||
export function destroyCheckoutOverlay(): void {
|
||||
initialized = false;
|
||||
onSuccessCallback = null;
|
||||
if (_escapeHandler) {
|
||||
window.removeEventListener('keydown', _escapeHandler);
|
||||
_escapeHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadPendingCheckoutIntent(): PendingCheckoutIntent | null {
|
||||
|
||||
Reference in New Issue
Block a user