From d7ce7459a549c0b6f0001d283f6663be1f5178a3 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Tue, 21 Jul 2026 18:13:33 +0000 Subject: [PATCH] Open Paystack from the Pay click gesture, not after fetch. Paystack blocks iframes, so checkout uses a popup beside the sheet/modal chrome. Opening about:blank on the user tap keeps that window from being blocked after the async initiate response. --- .../user/service-topup-modal.blade.php | 4 +- .../views/partials/paystack-sheet.blade.php | 79 +++++++++++++++---- resources/views/public/qr/landing.blade.php | 18 ++--- tests/Feature/ResponsivePaystackSheetTest.php | 1 + 4 files changed, 74 insertions(+), 28 deletions(-) diff --git a/resources/views/components/user/service-topup-modal.blade.php b/resources/views/components/user/service-topup-modal.blade.php index 5d85dac..7d41d93 100644 --- a/resources/views/components/user/service-topup-modal.blade.php +++ b/resources/views/components/user/service-topup-modal.blade.php @@ -39,7 +39,7 @@ if (this.method !== 'paystack') return; event.preventDefault(); - this.loading = true; + this.loading = true; window.LadillPayCheckout?.prepare?.(); this.errorMsg = ''; try { @@ -59,7 +59,7 @@ this.showSheet = true; this.loading = false; } catch (error) { - this.errorMsg = 'Network error. Please try again.'; + window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.'; this.loading = false; } }, diff --git a/resources/views/partials/paystack-sheet.blade.php b/resources/views/partials/paystack-sheet.blade.php index a437868..17afe3c 100644 --- a/resources/views/partials/paystack-sheet.blade.php +++ b/resources/views/partials/paystack-sheet.blade.php @@ -10,8 +10,9 @@ Paystack (checkout.paystack.com) sends X-Frame-Options: SAMEORIGIN and cannot be embedded. Same-origin URLs (e.g. MoMo waiting pages) still load in an iframe. - External checkouts open in a named window, with an in-sheet Continue CTA as a - user-gesture fallback when the popup is blocked after an async fetch. + External checkouts open in a named window. Call LadillPayCheckout.prepare() + synchronously in the Pay click handler (before await fetch) so the popup is not + blocked after the async response. An in-sheet Continue CTA remains as fallback. --}} @php $audience = $audience ?? 'buyer'; @@ -42,6 +43,9 @@ (function () { if (window.LadillPayCheckout) return; + var PENDING_NAME = 'ladill_pay_checkout'; + var pendingWindow = null; + function isFrameable(url) { if (!url) return false; try { @@ -51,12 +55,50 @@ } } + function writePendingPlaceholder(win) { + if (!win) return; + try { + win.document.open(); + win.document.write('Payment

Opening secure checkout…

'); + win.document.close(); + } catch (e) {} + } + + function prepare() { + cancel(); + try { + var win = window.open('about:blank', PENDING_NAME, 'width=480,height=720'); + if (win && !win.closed) { + pendingWindow = win; + writePendingPlaceholder(win); + return win; + } + } catch (e) {} + pendingWindow = null; + return null; + } + + function cancel() { + if (pendingWindow && !pendingWindow.closed) { + try { pendingWindow.close(); } catch (e) {} + } + pendingWindow = null; + } + function openCheckout(url) { if (!url) return null; try { + if (pendingWindow && !pendingWindow.closed) { + pendingWindow.location = url; + var win = pendingWindow; + pendingWindow = null; + return win; + } + pendingWindow = null; // Named window (no noopener) so payment-return can redirect window.opener. - return window.open(url, 'ladill_pay_checkout', 'width=480,height=720'); + return window.open(url, PENDING_NAME, 'width=480,height=720'); } catch (e) { + pendingWindow = null; return null; } } @@ -83,6 +125,7 @@ if (!url) return; this.frameable = isFrameable(url); if (this.frameable) { + cancel(); this.popupBlocked = false; return; } @@ -114,6 +157,8 @@ window.LadillPayCheckout = { isFrameable: isFrameable, + prepare: prepare, + cancel: cancel, open: openCheckout, ensureStore: ensureStore, }; @@ -183,12 +228,12 @@
- -

+ x-text="$store.ladillPayCheckout.popupBlocked ? 'Continue in a secure payment window' : 'Complete payment in the secure window'">

- A Paystack window should be open. If you do not see it, tap continue below. - Your browser blocked the popup. Tap continue to open Paystack. + A Paystack window should be open. If you do not see it, tap continue below. + Your browser blocked the popup. Tap continue to open Paystack.

@@ -251,13 +296,13 @@
- -

+ x-text="$store.ladillPayCheckout.popupBlocked ? 'Continue in a secure payment window' : 'Complete payment in the secure window'">

- A Paystack window should be open. If you do not see it, click continue below. - Your browser blocked the popup. Click continue to open Paystack. + A Paystack window should be open. If you do not see it, click continue below. + Your browser blocked the popup. Click continue to open Paystack.

diff --git a/resources/views/public/qr/landing.blade.php b/resources/views/public/qr/landing.blade.php index 6e8454b..714bb65 100644 --- a/resources/views/public/qr/landing.blade.php +++ b/resources/views/public/qr/landing.blade.php @@ -343,7 +343,7 @@ if (!this.amount || parseFloat(this.amount) <= 0) { this.errorMsg = 'Enter a valid amount.'; return; } if (!this.name.trim()) { this.errorMsg = 'Enter your name.'; return; } if (!this.phone.trim()) { this.errorMsg = 'Enter your phone number.'; return; } - this.errorMsg = ''; this.loading = true; + this.errorMsg = ''; this.loading = true; window.LadillPayCheckout?.prepare?.(); const labels = @js($churchTypeLabels); try { const res = await fetch(@js($churchOrderRoute), { @@ -357,12 +357,12 @@ }), }); const data = await res.json(); - if (data.error) { this.errorMsg = data.error; this.loading = false; return; } + if (data.error) { window.LadillPayCheckout?.cancel?.(); this.errorMsg = data.error; this.loading = false; return; } this.checkoutUrl = data.checkout_url; this.showSheet = true; this.loading = false; } catch(e) { - this.errorMsg = 'Network error. Please try again.'; + window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.'; this.loading = false; } } @@ -662,7 +662,7 @@ if (!this.name.trim()) { this.errorMsg = 'Enter your full name.'; return; } if (!this.email.trim() || !this.email.includes('@')) { this.errorMsg = 'Enter a valid email address.'; return; } if (!this.phone.trim()) { this.errorMsg = 'Enter your phone number.'; return; } - this.errorMsg = ''; this.loading = true; + this.errorMsg = ''; this.loading = true; window.LadillPayCheckout?.prepare?.(); try { const res = await fetch(@js($evRegRoute), { method: 'POST', @@ -675,7 +675,7 @@ this.loading = false; return; } - if (data.error) { this.errorMsg = data.error; this.loading = false; return; } + if (data.error) { window.LadillPayCheckout?.cancel?.(); this.errorMsg = data.error; this.loading = false; return; } if (data.paid) { if (!data.checkout_url) { this.errorMsg = 'Could not start checkout. Please try again.'; this.loading = false; return; } this.checkoutUrl = data.checkout_url; this.showSheet = true; this.loading = false; @@ -684,7 +684,7 @@ if (data.success_url) { window.location.href = data.success_url; return; } this.errorMsg = 'Registration failed. Please try again.'; this.loading = false; - } catch(e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; } + } catch(e) { window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.'; this.loading = false; } } }" class="min-h-screen bg-slate-50 pb-12"> @@ -1241,7 +1241,7 @@ if (!this.name.trim()) { this.errorMsg = 'Enter your name.'; return } if (!this.phone.trim()) { this.errorMsg = 'Enter your phone number.'; return } this.errorMsg = ''; - this.loading = true; + this.loading = true; window.LadillPayCheckout?.prepare?.(); const payload = { customer_name: this.name, customer_email: this.email, @@ -1256,12 +1256,12 @@ body: JSON.stringify(payload), }); const data = await res.json(); - if (data.error) { this.errorMsg = data.error; this.loading = false; return; } + if (data.error) { window.LadillPayCheckout?.cancel?.(); this.errorMsg = data.error; this.loading = false; return; } this.checkoutUrl = data.checkout_url; this.showSheet = true; this.loading = false; } catch(e) { - this.errorMsg = 'Network error. Please try again.'; + window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.'; this.loading = false; } } diff --git a/tests/Feature/ResponsivePaystackSheetTest.php b/tests/Feature/ResponsivePaystackSheetTest.php index 814687e..8da1d1b 100644 --- a/tests/Feature/ResponsivePaystackSheetTest.php +++ b/tests/Feature/ResponsivePaystackSheetTest.php @@ -22,6 +22,7 @@ class ResponsivePaystackSheetTest extends TestCase $this->assertStringContainsString('LadillPayCheckout', $html); $this->assertStringContainsString('Continue to Paystack', $html); $this->assertStringContainsString('ladill_pay_checkout', $html); + $this->assertStringContainsString('prepare:', $html); $this->assertStringNotContainsString('Paystack checkout', $html); }