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

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:13:56 +00:00
parent c9c8278500
commit 497cdfbe8e
3 changed files with 65 additions and 19 deletions
+2 -2
View File
@@ -231,7 +231,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({
}
this.errorMsg = '';
this.loading = true;
this.loading = true; window.LadillPayCheckout?.prepare?.();
try {
const res = await fetch(config.payUrl, {
@@ -259,7 +259,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({
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;
}
},
@@ -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>
@@ -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);
}