Open Paystack from the Pay click gesture, not after fetch.
Deploy Ladill Merchant / deploy (push) Successful in 56s
Deploy Ladill Merchant / deploy (push) Successful in 56s
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:
+2
-2
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user