Skip to content

Commit 72484c8

Browse files
test
1 parent d8fd937 commit 72484c8

3 files changed

Lines changed: 88 additions & 7 deletions

File tree

src/apm/references.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
/// <reference path="views/NextSteps.ts" />
3535
/// <reference path="views/Pending.ts" />
3636
/// <reference path="views/Redirect.ts" />
37+
/// <reference path="views/PopupBlockedFallback.ts" />
3738
/// <reference path="views/CancelRequest.ts" />
3839
/// <reference path="views/utils/render-elements.ts" />
3940
/// <reference path="views/utils/form.ts" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module ProcessOut {
2+
const { div } = elements
3+
4+
interface PopupBlockedFallbackProps {
5+
config: APIRedirectBase & Partial<PaymentContext>
6+
onRetry: () => void
7+
}
8+
9+
/**
10+
* Renders the Pay button UI inside the popup-blocked overlay modal.
11+
* Used when a headless redirect popup is blocked by the browser — this view
12+
* is mounted into a fresh APMPageImpl overlaid on document.body so it is
13+
* always visible regardless of the merchant's container visibility.
14+
*/
15+
export class APMViewPopupBlockedFallback extends APMViewImpl<PopupBlockedFallbackProps> {
16+
render() {
17+
const redirectLabel = `Pay ${formatCurrency(this.props.config.invoice.amount, this.props.config.invoice.currency)}`
18+
return (
19+
Main({
20+
config: this.props.config,
21+
className: 'redirect-page',
22+
hideAmount: true,
23+
buttons: [
24+
Button({ onclick: this.props.onRetry }, redirectLabel),
25+
ContextImpl.context.confirmation.allowCancelation
26+
? CancelButton({ config: this.props.config })
27+
: null
28+
]
29+
},
30+
div({ className: 'heading-container' },
31+
Header('Continue to payment'),
32+
SubHeader('Click the button below to complete your payment'),
33+
)
34+
)
35+
)
36+
}
37+
}
38+
}

src/apm/views/Redirect.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)