Open Paystack from the Pay click gesture, not after fetch.
Deploy Ladill Link / deploy (push) Successful in 59s
Deploy Ladill Link / deploy (push) Successful in 59s
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:
@@ -91,7 +91,7 @@
|
|||||||
if (this.method !== 'paystack') return;
|
if (this.method !== 'paystack') return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.loading = true;
|
this.loading = true; window.LadillPayCheckout?.prepare?.();
|
||||||
this.errorMsg = '';
|
this.errorMsg = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
this.showSheet = true;
|
this.showSheet = true;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.errorMsg = 'Network error. Please try again.';
|
window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.';
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
|
|
||||||
Paystack (checkout.paystack.com) sends X-Frame-Options: SAMEORIGIN and cannot be
|
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.
|
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
|
External checkouts open in a named window. Call LadillPayCheckout.prepare()
|
||||||
user-gesture fallback when the popup is blocked after an async fetch.
|
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
|
@php
|
||||||
$audience = $audience ?? 'buyer';
|
$audience = $audience ?? 'buyer';
|
||||||
@@ -42,6 +43,9 @@
|
|||||||
(function () {
|
(function () {
|
||||||
if (window.LadillPayCheckout) return;
|
if (window.LadillPayCheckout) return;
|
||||||
|
|
||||||
|
var PENDING_NAME = 'ladill_pay_checkout';
|
||||||
|
var pendingWindow = null;
|
||||||
|
|
||||||
function isFrameable(url) {
|
function isFrameable(url) {
|
||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
try {
|
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) {
|
function openCheckout(url) {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
try {
|
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.
|
// 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) {
|
} catch (e) {
|
||||||
|
pendingWindow = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +125,7 @@
|
|||||||
if (!url) return;
|
if (!url) return;
|
||||||
this.frameable = isFrameable(url);
|
this.frameable = isFrameable(url);
|
||||||
if (this.frameable) {
|
if (this.frameable) {
|
||||||
|
cancel();
|
||||||
this.popupBlocked = false;
|
this.popupBlocked = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,6 +157,8 @@
|
|||||||
|
|
||||||
window.LadillPayCheckout = {
|
window.LadillPayCheckout = {
|
||||||
isFrameable: isFrameable,
|
isFrameable: isFrameable,
|
||||||
|
prepare: prepare,
|
||||||
|
cancel: cancel,
|
||||||
open: openCheckout,
|
open: openCheckout,
|
||||||
ensureStore: ensureStore,
|
ensureStore: ensureStore,
|
||||||
};
|
};
|
||||||
@@ -183,12 +228,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||||
<iframe x-show="$store.ladillPayCheckout?.frameable"
|
<iframe x-show="$store.ladillPayCheckout.frameable"
|
||||||
:src="showSheet && $store.ladillPayCheckout?.frameable ? checkoutUrl : ''"
|
:src="showSheet && $store.ladillPayCheckout.frameable ? checkoutUrl : ''"
|
||||||
class="h-full min-h-[60dvh] w-full border-0"
|
class="h-full min-h-[60dvh] w-full border-0"
|
||||||
allow="payment *"
|
allow="payment *"
|
||||||
title="{{ $iframeTitle }}"></iframe>
|
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">
|
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">
|
<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">
|
<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>
|
||||||
<div class="max-w-xs space-y-1">
|
<div class="max-w-xs space-y-1">
|
||||||
<p class="text-sm font-semibold text-slate-800"
|
<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">
|
<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">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">Your browser blocked the popup. Tap continue to open Paystack.</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<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">
|
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
|
Continue to Paystack
|
||||||
</button>
|
</button>
|
||||||
@@ -251,13 +296,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||||
<iframe x-show="$store.ladillPayCheckout?.frameable"
|
<iframe x-show="$store.ladillPayCheckout.frameable"
|
||||||
:src="showSheet && $store.ladillPayCheckout?.frameable ? checkoutUrl : ''"
|
:src="showSheet && $store.ladillPayCheckout.frameable ? checkoutUrl : ''"
|
||||||
class="h-full min-h-0 w-full border-0"
|
class="h-full min-h-0 w-full border-0"
|
||||||
style="min-height: 28rem"
|
style="min-height: 28rem"
|
||||||
allow="payment *"
|
allow="payment *"
|
||||||
title="{{ $iframeTitle }}"></iframe>
|
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">
|
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">
|
<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">
|
<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>
|
||||||
<div class="max-w-sm space-y-1">
|
<div class="max-w-sm space-y-1">
|
||||||
<p class="text-sm font-semibold text-slate-800"
|
<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">
|
<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">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">Your browser blocked the popup. Click continue to open Paystack.</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<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">
|
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
|
Continue to Paystack
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -343,7 +343,7 @@
|
|||||||
if (!this.name.trim()) { this.errorMsg = 'Enter your name.'; return; }
|
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.email.trim() || !this.email.includes('@')) { this.errorMsg = 'Enter a valid email.'; return; }
|
||||||
if (!this.phone.trim()) { this.errorMsg = 'Enter your phone number.'; 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);
|
const labels = @js($churchTypeLabels);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(@js($churchOrderRoute), {
|
const res = await fetch(@js($churchOrderRoute), {
|
||||||
@@ -357,11 +357,11 @@
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
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.checkoutUrl = data.checkout_url;
|
||||||
this.showSheet = true;
|
this.showSheet = true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.errorMsg = 'Network error. Please try again.';
|
window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.';
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -657,7 +657,7 @@
|
|||||||
if (this.mode === 'contributions' && !(parseFloat(this.amount) > 0)) { this.errorMsg = 'Enter a contribution amount.'; return; }
|
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.name.trim()) { this.errorMsg = 'Enter your full name.'; return; }
|
||||||
if (!this.email.trim() || !this.email.includes('@')) { this.errorMsg = 'Enter a valid email.'; 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 {
|
try {
|
||||||
const res = await fetch(@js($evRegRoute), {
|
const res = await fetch(@js($evRegRoute), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -665,11 +665,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 }),
|
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();
|
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; }
|
if (!data.paid) { window.location.href = data.success_url; return; }
|
||||||
this.checkoutUrl = data.checkout_url;
|
this.checkoutUrl = data.checkout_url;
|
||||||
this.showSheet = true;
|
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">
|
}" class="min-h-screen bg-slate-50 pb-12">
|
||||||
|
|
||||||
@@ -1185,7 +1185,7 @@
|
|||||||
if (!this.email.trim() || !this.email.includes('@')) { this.errorMsg = 'Enter a valid email address.'; 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 }
|
if (!this.phone.trim()) { this.errorMsg = 'Enter your phone number.'; return }
|
||||||
this.errorMsg = '';
|
this.errorMsg = '';
|
||||||
this.loading = true;
|
this.loading = true; window.LadillPayCheckout?.prepare?.();
|
||||||
const payload = {
|
const payload = {
|
||||||
customer_name: this.name,
|
customer_name: this.name,
|
||||||
customer_email: this.email,
|
customer_email: this.email,
|
||||||
@@ -1200,11 +1200,11 @@
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
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.checkoutUrl = data.checkout_url;
|
||||||
this.showSheet = true;
|
this.showSheet = true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.errorMsg = 'Network error. Please try again.';
|
window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.';
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ResponsivePaystackSheetTest extends TestCase
|
|||||||
$this->assertStringContainsString('LadillPayCheckout', $html);
|
$this->assertStringContainsString('LadillPayCheckout', $html);
|
||||||
$this->assertStringContainsString('Continue to Paystack', $html);
|
$this->assertStringContainsString('Continue to Paystack', $html);
|
||||||
$this->assertStringContainsString('ladill_pay_checkout', $html);
|
$this->assertStringContainsString('ladill_pay_checkout', $html);
|
||||||
|
$this->assertStringContainsString('prepare:', $html);
|
||||||
$this->assertStringNotContainsString('Paystack checkout', $html);
|
$this->assertStringNotContainsString('Paystack checkout', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user