Summary
Our checkout.redirect_requested handler runs terminal success side-effects before the payment is authenticated. With manualRedirect: true, SDK 1.8.0 routes all redirects to the merchant via this event — including the 3DS bank-escape hop. So when a card needs a full-page 3DS redirect, we fire onSuccess and clear the retry context before the user has even reached the bank.
Evidence
src/services/checkout.ts:379-399 — case 'checkout.redirect_requested': if (!successFired) runTerminalSuccessSideEffects('event-redirect') runs first, then window.location.href = redirectTo.
runTerminalSuccessSideEffects fires onSuccessCallback, clearCheckoutAttempt('success'), clearPendingCheckoutIntent(), markPostCheckout() — i.e. it tells the app the user is Pro and wipes the retry intent.
- SDK 1.8.0 (
index.esm.js, handler v): the parent SDK auto-navigates only on checkout.redirect; checkout.redirect_requested is forwarded untouched. With manualRedirect:true forwarded into the iframe URL, the iframe emits redirect_requested for redirects it would otherwise perform — which includes the 3DS escape, not just the final return-URL hop.
Fix direction (small)
Only treat redirect_requested as terminal success when redirect_to resolves to our return_url origin (the final hop). If redirect_to is an external/bank origin (intermediate 3DS), navigate without marking success or clearing retry context. Better still: don't infer payment success from a redirect at all — rely on the return_url status params + the payment.succeeded/subscription.active webhook.
Test first
Emit redirect_requested with redirect_to = an external bank origin and assert success side-effects do NOT fire (retry context preserved); emit it with redirect_to = our return_url and assert they do.
Part of the DodoPayments 3DS stuck-payments incident.
Summary
Our
checkout.redirect_requestedhandler runs terminal success side-effects before the payment is authenticated. WithmanualRedirect: true, SDK 1.8.0 routes all redirects to the merchant via this event — including the 3DS bank-escape hop. So when a card needs a full-page 3DS redirect, we fireonSuccessand clear the retry context before the user has even reached the bank.Evidence
src/services/checkout.ts:379-399—case 'checkout.redirect_requested':if (!successFired) runTerminalSuccessSideEffects('event-redirect')runs first, thenwindow.location.href = redirectTo.runTerminalSuccessSideEffectsfiresonSuccessCallback,clearCheckoutAttempt('success'),clearPendingCheckoutIntent(),markPostCheckout()— i.e. it tells the app the user is Pro and wipes the retry intent.index.esm.js, handlerv): the parent SDK auto-navigates only oncheckout.redirect;checkout.redirect_requestedis forwarded untouched. WithmanualRedirect:trueforwarded into the iframe URL, the iframe emitsredirect_requestedfor redirects it would otherwise perform — which includes the 3DS escape, not just the final return-URL hop.Fix direction (small)
Only treat
redirect_requestedas terminal success whenredirect_toresolves to ourreturn_urlorigin (the final hop). Ifredirect_tois an external/bank origin (intermediate 3DS), navigate without marking success or clearing retry context. Better still: don't infer payment success from a redirect at all — rely on thereturn_urlstatus params + thepayment.succeeded/subscription.activewebhook.Test first
Emit
redirect_requestedwithredirect_to= an external bank origin and assert success side-effects do NOT fire (retry context preserved); emit it withredirect_to= our return_url and assert they do.Part of the DodoPayments 3DS stuck-payments incident.