diff --git a/resources/js/app.js b/resources/js/app.js index bb6c591..46e8987 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -215,7 +215,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ } this.errorMsg = ''; - this.loading = true; + this.loading = true; window.LadillPayCheckout?.prepare?.(); try { const res = await fetch(config.payUrl, { @@ -242,7 +242,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ this.checkoutUrl = data.checkout_url; this.showSheet = true; } 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/resources/views/components/user/service-topup-modal.blade.php b/resources/views/components/user/service-topup-modal.blade.php index acdbd94..65d6938 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/book-landing.blade.php b/resources/views/public/qr/book-landing.blade.php index ba9bcb0..96c1e33 100644 --- a/resources/views/public/qr/book-landing.blade.php +++ b/resources/views/public/qr/book-landing.blade.php @@ -36,7 +36,7 @@ async submitOrder() { if (!this.customerName.trim()) { this.errorMsg = 'Enter your full name.'; return; } if (!this.customerPhone.trim()) { this.errorMsg = 'Enter your phone number.'; return; } - this.loading = true; this.errorMsg = ''; + this.loading = true; window.LadillPayCheckout?.prepare?.(); this.errorMsg = ''; try { const res = await fetch(@js($orderUrl), { method: 'POST', @@ -44,13 +44,13 @@ body: JSON.stringify({ customer_name: this.customerName, customer_email: this.customerEmail, customer_phone: this.customerPhone }), }); 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.showModal = false; 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/resources/views/public/qr/booking-landing.blade.php b/resources/views/public/qr/booking-landing.blade.php index b5a9aef..2a2eb1d 100644 --- a/resources/views/public/qr/booking-landing.blade.php +++ b/resources/views/public/qr/booking-landing.blade.php @@ -48,7 +48,7 @@ const data = await res.json(); this.slots = data.slots || []; } catch (e) { - this.errorMsg = 'Could not load times. Try again.'; + window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Could not load times. Try again.'; } this.loadingSlots = false; }, @@ -56,7 +56,7 @@ if (!this.customerName.trim()) { this.errorMsg = 'Enter your full name.'; return; } if (!this.customerPhone.trim()) { this.errorMsg = 'Enter your phone number.'; return; } if (!this.selectedSlot) { this.errorMsg = 'Pick a time slot.'; return; } - this.loading = true; this.errorMsg = ''; + this.loading = true; window.LadillPayCheckout?.prepare?.(); this.errorMsg = ''; try { const res = await fetch(@js($bookUrl), { method: 'POST', @@ -70,7 +70,7 @@ }), }); 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; } if (data.confirmed && data.redirect_url) { window.location.href = data.redirect_url; return; @@ -79,7 +79,7 @@ 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/resources/views/public/qr/landing.blade.php b/resources/views/public/qr/landing.blade.php index bb7322c..fdbb6df 100644 --- a/resources/views/public/qr/landing.blade.php +++ b/resources/views/public/qr/landing.blade.php @@ -342,7 +342,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), { @@ -356,11 +356,11 @@ }), }); 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; } catch(e) { - this.errorMsg = 'Network error. Please try again.'; + window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.'; this.loading = false; } } @@ -652,7 +652,7 @@ if (this.mode === 'contributions' && !(parseFloat(this.amount) > 0)) { this.errorMsg = 'Enter a contribution amount.'; return; } if (!this.name.trim()) { this.errorMsg = 'Enter your full 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?.(); try { const res = await fetch(@js($evRegRoute), { method: 'POST', @@ -660,11 +660,11 @@ body: JSON.stringify({ tier: this.tier, amount: this.mode === 'contributions' ? this.amount : null, attendee_name: this.name, attendee_email: this.email, attendee_phone: this.phone, badge_fields: this.fields }), }); 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; } if (!data.paid) { window.location.href = data.success_url; return; } this.checkoutUrl = data.checkout_url; this.showSheet = true; - } 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"> @@ -1177,7 +1177,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, @@ -1192,11 +1192,11 @@ 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; } 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/resources/views/public/qr/storefront.blade.php b/resources/views/public/qr/storefront.blade.php index 2d043bd..1363fd2 100644 --- a/resources/views/public/qr/storefront.blade.php +++ b/resources/views/public/qr/storefront.blade.php @@ -184,7 +184,7 @@ function storefront(cfg) { this.errorMsg = ''; if (!this.name.trim() || !this.phone.trim()) { this.errorMsg = 'Please fill in your name and phone.'; return; } if (this.count === 0) { this.errorMsg = 'Your cart is empty.'; return; } - this.loading = true; + this.loading = true; window.LadillPayCheckout?.prepare?.(); try { const items = Object.values(this.cart).map(l => ({ name: l.name, price: l.price, qty: l.qty })); const res = await fetch(this.orderUrl, { @@ -200,7 +200,7 @@ function storefront(cfg) { 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); }