Summary
When a Dodo checkout session/payment-link expires mid-flow, the SDK emits checkout.link_expired and does not self-close the iframe. Our overlay event handler has no case for it, so the customer is stranded on a frozen "Processing… / Payment Link Expires in 00:00" overlay with no recovery — the literal symptom in the incident screenshots.
Evidence
- SDK 1.8.0 (
node_modules/dodopayments-checkout/dist/index.esm.js) emits checkout.link_expired and only self-removes the iframe when the iframe itself posts checkout.closed.
- Our handler (
src/services/checkout.ts:331-411) switches on checkout.opened, checkout.status, checkout.closed, checkout.redirect_requested, checkout.error — no checkout.link_expired branch.
- The "Expires in 00:00" countdown is Dodo's
showTimer UI bound to the session's server-side expires_at. We do not set confirm=true, so sessions use Dodo's 24h default — these frozen overlays are abandoned-then-reopened expired links with no client-side escape.
Fix direction (small)
Add a case 'checkout.link_expired' that: (a) safeCloseOverlay() to clear the frozen iframe, (b) surfaces an actionable "payment link expired — retry" surface (reuse the existing failed-return banner/error taxonomy), and (c) preserves retry context so the user can re-initiate without hunting.
Test first
Drive the lifecycle stub to emit checkout.link_expired and assert the overlay is closed + a retry surface is shown (revert-check: removing the case leaves the overlay open).
Part of the DodoPayments 3DS stuck-payments incident.
Summary
When a Dodo checkout session/payment-link expires mid-flow, the SDK emits
checkout.link_expiredand does not self-close the iframe. Our overlay event handler has no case for it, so the customer is stranded on a frozen "Processing… / Payment Link Expires in 00:00" overlay with no recovery — the literal symptom in the incident screenshots.Evidence
node_modules/dodopayments-checkout/dist/index.esm.js) emitscheckout.link_expiredand only self-removes the iframe when the iframe itself postscheckout.closed.src/services/checkout.ts:331-411) switches oncheckout.opened,checkout.status,checkout.closed,checkout.redirect_requested,checkout.error— nocheckout.link_expiredbranch.showTimerUI bound to the session's server-sideexpires_at. We do not setconfirm=true, so sessions use Dodo's 24h default — these frozen overlays are abandoned-then-reopened expired links with no client-side escape.Fix direction (small)
Add a
case 'checkout.link_expired'that: (a)safeCloseOverlay()to clear the frozen iframe, (b) surfaces an actionable "payment link expired — retry" surface (reuse the existing failed-return banner/error taxonomy), and (c) preserves retry context so the user can re-initiate without hunting.Test first
Drive the lifecycle stub to emit
checkout.link_expiredand assert the overlay is closed + a retry surface is shown (revert-check: removing the case leaves the overlay open).Part of the DodoPayments 3DS stuck-payments incident.