@@ -9,6 +9,10 @@ module ProcessOut {
99 /** After a retryable headless error (e.g. pop-up blocked), show the normal Pay / Cancel UI. */
1010 private headlessManualFallback = false
1111
12+ /** Overlay mounted on document.body when popup is blocked in headless mode. */
13+ private popupBlockedOverlayEl : HTMLElement | null = null
14+ private popupBlockedOverlayPage : APMPageImpl | null = null
15+
1216 styles = css `
1317 .redirect-headless-loading {
1418 justify-content : center;
@@ -34,6 +38,44 @@ module ProcessOut {
3438 }
3539 }
3640
41+ private showPopupBlockedFallbackOverlay ( ) {
42+ // Full-screen backdrop with centered content card
43+ const overlay = document . createElement ( 'div' )
44+ overlay . style . cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;'
45+ document . body . appendChild ( overlay )
46+ this . popupBlockedOverlayEl = overlay
47+
48+ // Inner wrapper gives APMPageImpl a bounded container to render into
49+ const content = document . createElement ( 'div' )
50+ content . style . cssText = 'width:100%;max-width:400px;'
51+ overlay . appendChild ( content )
52+
53+ const overlayPage = new APMPageImpl ( content )
54+ this . popupBlockedOverlayPage = overlayPage
55+
56+ overlayPage . render ( APMViewPopupBlockedFallback , {
57+ config : this . props . config ,
58+ onRetry : ( ) => {
59+ this . removePopupBlockedFallbackOverlay ( )
60+ this . handleRedirectClick ( )
61+ } ,
62+ } )
63+
64+ // Auto-clean up when the payment reaches a terminal state
65+ const remove = ( ) => this . removePopupBlockedFallbackOverlay ( )
66+ ContextImpl . context . events . on ( 'success' , remove )
67+ ContextImpl . context . events . on ( 'failure' , remove )
68+ ContextImpl . context . events . on ( 'payment-cancelled' , remove )
69+ }
70+
71+ private removePopupBlockedFallbackOverlay ( ) {
72+ if ( this . popupBlockedOverlayEl ) {
73+ this . popupBlockedOverlayEl . remove ( )
74+ this . popupBlockedOverlayEl = null
75+ this . popupBlockedOverlayPage = null
76+ }
77+ }
78+
3779 handleRedirectClick ( ) {
3880 ContextImpl . context . events . emit ( 'redirect-initiated' )
3981 const pm = this . props . config . payment_method
@@ -64,16 +106,16 @@ module ProcessOut {
64106 if ( ! this . headlessManualFallback && failure . code === 'customer.popup-blocked' ) {
65107 console . warn ( '[PO] Redirect: popup blocked on headless first attempt — activating manual fallback UI' )
66108 this . headlessManualFallback = true
67- // Emit before forceUpdate so the merchant can make their container
68- // visible (e.g. un-hide an invisible host) before the SDK paints
69- // the Pay button inside it. retry() is a bound handleRedirectClick
70- // — the merchant must call it from a real user-gesture handler so
71- // the browser grants popup permission.
109+ // Show the SDK's built-in Pay button in an overlay on document.body so it
110+ // is visible even when the merchant's container is hidden (invisible host flow).
111+ this . showPopupBlockedFallbackOverlay ( )
112+ // Also emit the event so merchants who want custom UI can intercept it.
113+ // retry() is a bound handleRedirectClick — must be called from a real
114+ // user-gesture handler so the browser grants popup permission.
72115 ContextImpl . context . events . emit ( 'redirect-popup-blocked' , {
73116 retry : this . handleRedirectClick . bind ( this ) ,
74117 } )
75- console . warn ( '[PO] Redirect: emitted redirect-popup-blocked, calling forceUpdate' )
76- this . forceUpdate ( )
118+ console . warn ( '[PO] Redirect: emitted redirect-popup-blocked, overlay mounted' )
77119 return
78120 }
79121 if ( this . headlessManualFallback ) {
0 commit comments