Open Paystack from the Pay click gesture, not after fetch.
Deploy Ladill QR Plus / deploy (push) Successful in 47s

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.
This commit is contained in:
isaacclad
2026-07-21 18:14:04 +00:00
parent 6b81644c32
commit f88d50f24c
4 changed files with 74 additions and 28 deletions
@@ -91,7 +91,7 @@
if (this.method !== 'paystack') return;
event.preventDefault();
this.loading = true;
this.loading = true; window.LadillPayCheckout?.prepare?.();
this.errorMsg = '';
try {
@@ -111,7 +111,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;
}
},
@@ -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('<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Payment</title></head><body style="margin:0;min-height:100vh;display:grid;place-items:center;font-family:system-ui,sans-serif;background:#f8fafc;color:#475569"><p style="font-size:0.95rem">Opening secure checkout…</p></body></html>');
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 @@
</div>
</div>
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain">
<iframe x-show="$store.ladillPayCheckout?.frameable"
:src="showSheet && $store.ladillPayCheckout?.frameable ? checkoutUrl : ''"
<iframe x-show="$store.ladillPayCheckout.frameable"
:src="showSheet && $store.ladillPayCheckout.frameable ? checkoutUrl : ''"
class="h-full min-h-[60dvh] w-full border-0"
allow="payment *"
title="{{ $iframeTitle }}"></iframe>
<div x-show="!$store.ladillPayCheckout?.frameable"
<div x-show="!$store.ladillPayCheckout.frameable"
class="flex min-h-[60dvh] flex-col items-center justify-center gap-4 px-6 py-10 text-center">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-indigo-50 text-indigo-600" aria-hidden="true">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor">
@@ -197,14 +242,14 @@
</div>
<div class="max-w-xs space-y-1">
<p class="text-sm font-semibold text-slate-800"
x-text="$store.ladillPayCheckout?.popupBlocked ? 'Continue in a secure payment window' : 'Complete payment in the secure window'"></p>
x-text="$store.ladillPayCheckout.popupBlocked ? 'Continue in a secure payment window' : 'Complete payment in the secure window'"></p>
<p class="text-xs leading-snug text-slate-500">
<span x-show="!$store.ladillPayCheckout?.popupBlocked">A Paystack window should be open. If you do not see it, tap continue below.</span>
<span x-show="$store.ladillPayCheckout?.popupBlocked">Your browser blocked the popup. Tap continue to open Paystack.</span>
<span x-show="!$store.ladillPayCheckout.popupBlocked">A Paystack window should be open. If you do not see it, tap continue below.</span>
<span x-show="$store.ladillPayCheckout.popupBlocked">Your browser blocked the popup. Tap continue to open Paystack.</span>
</p>
</div>
<button type="button"
@click="$store.ladillPayCheckout?.continueTo(checkoutUrl)"
@click="$store.ladillPayCheckout.continueTo(checkoutUrl)"
class="inline-flex w-full max-w-xs items-center justify-center rounded-xl bg-indigo-600 px-4 py-3 text-sm font-semibold text-white hover:bg-indigo-700">
Continue to Paystack
</button>
@@ -251,13 +296,13 @@
</button>
</div>
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain">
<iframe x-show="$store.ladillPayCheckout?.frameable"
:src="showSheet && $store.ladillPayCheckout?.frameable ? checkoutUrl : ''"
<iframe x-show="$store.ladillPayCheckout.frameable"
:src="showSheet && $store.ladillPayCheckout.frameable ? checkoutUrl : ''"
class="h-full min-h-0 w-full border-0"
style="min-height: 28rem"
allow="payment *"
title="{{ $iframeTitle }}"></iframe>
<div x-show="!$store.ladillPayCheckout?.frameable"
<div x-show="!$store.ladillPayCheckout.frameable"
class="flex h-full min-h-[28rem] flex-col items-center justify-center gap-4 px-8 py-12 text-center">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-indigo-50 text-indigo-600" aria-hidden="true">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor">
@@ -266,14 +311,14 @@
</div>
<div class="max-w-sm space-y-1">
<p class="text-sm font-semibold text-slate-800"
x-text="$store.ladillPayCheckout?.popupBlocked ? 'Continue in a secure payment window' : 'Complete payment in the secure window'"></p>
x-text="$store.ladillPayCheckout.popupBlocked ? 'Continue in a secure payment window' : 'Complete payment in the secure window'"></p>
<p class="text-xs leading-snug text-slate-500">
<span x-show="!$store.ladillPayCheckout?.popupBlocked">A Paystack window should be open. If you do not see it, click continue below.</span>
<span x-show="$store.ladillPayCheckout?.popupBlocked">Your browser blocked the popup. Click continue to open Paystack.</span>
<span x-show="!$store.ladillPayCheckout.popupBlocked">A Paystack window should be open. If you do not see it, click continue below.</span>
<span x-show="$store.ladillPayCheckout.popupBlocked">Your browser blocked the popup. Click continue to open Paystack.</span>
</p>
</div>
<button type="button"
@click="$store.ladillPayCheckout?.continueTo(checkoutUrl)"
@click="$store.ladillPayCheckout.continueTo(checkoutUrl)"
class="inline-flex w-full max-w-xs items-center justify-center rounded-xl bg-indigo-600 px-4 py-3 text-sm font-semibold text-white hover:bg-indigo-700">
Continue to Paystack
</button>
+9 -9
View File
@@ -343,7 +343,7 @@
if (!this.name.trim()) { this.errorMsg = 'Enter your name.'; return; }
if (!this.email.trim() || !this.email.includes('@')) { this.errorMsg = 'Enter a valid email.'; 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,11 +357,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;
}
}
@@ -656,7 +656,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.email.trim() || !this.email.includes('@')) { this.errorMsg = 'Enter a valid email.'; return; }
this.errorMsg = ''; this.loading = true;
this.errorMsg = ''; this.loading = true; window.LadillPayCheckout?.prepare?.();
try {
const res = await fetch(@js($evRegRoute), {
method: 'POST',
@@ -664,11 +664,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">
@@ -1395,7 +1395,7 @@
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.loading = true; window.LadillPayCheckout?.prepare?.();
const payload = {
customer_name: this.name,
customer_email: this.email,
@@ -1410,11 +1410,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;
}
}
@@ -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);
}