Harden Mini Paystack sheet so payment modals open reliably.
Deploy Ladill Mini / deploy (push) Successful in 49s

Use Alpine watches for checkout sync, prewarm the Paystack script, and
preserve bottomsheet wrapping around in-page Inline.
This commit is contained in:
isaacclad
2026-07-21 21:55:37 +00:00
parent de6e6f5331
commit e03a5dc30f
2 changed files with 73 additions and 43 deletions
@@ -44,7 +44,8 @@
@once
<script>
(function () {
if (window.LadillPayCheckout) return;
if (window.__ladillPayCheckoutBootstrapped) return;
window.__ladillPayCheckoutBootstrapped = true;
var INLINE_SRC = 'https://js.paystack.co/v2/inline.js';
var inlineLoading = null;
@@ -94,6 +95,10 @@
inlineLoading = new Promise(function (resolve, reject) {
var existing = document.querySelector('script[data-ladill-paystack-inline]');
if (existing) {
if (window.PaystackPop) {
resolve(window.PaystackPop);
return;
}
existing.addEventListener('load', function () { resolve(window.PaystackPop); });
existing.addEventListener('error', function () { reject(new Error('Paystack script failed')); });
return;
@@ -112,6 +117,15 @@
return inlineLoading;
}
// Warm the Paystack script early so resumeTransaction is ready on Pay click.
if (document.readyState === 'complete' || document.readyState === 'interactive') {
loadInlineJs().catch(function () {});
} else {
document.addEventListener('DOMContentLoaded', function () {
loadInlineJs().catch(function () {});
});
}
function buildReturnUrl(returnUrl, reference) {
if (!returnUrl || !reference) return '';
try {
@@ -133,12 +147,8 @@
activePopup = null;
}
function ensureStore() {
if (!window.Alpine || typeof window.Alpine.store !== 'function') return null;
if (window.Alpine.store('ladillPayCheckout')) {
return window.Alpine.store('ladillPayCheckout');
}
window.Alpine.store('ladillPayCheckout', {
function createStoreDefinition() {
return {
frameable: false,
inline: false,
ready: false,
@@ -156,6 +166,10 @@
cancelInline();
},
sync(show, url, accessCode, returnUrl) {
url = (url || '').toString();
accessCode = (accessCode || '').toString();
returnUrl = (returnUrl || '').toString();
if (!show) {
this._launchToken += 1;
this.reset();
@@ -165,7 +179,8 @@
if (!url && !accessCode) {
this.frameable = false;
this.inline = false;
this.launching = false;
this.launching = true; // waiting for checkout payload
this.error = '';
this._activeCode = '';
return;
}
@@ -173,6 +188,7 @@
this.frameable = true;
this.inline = false;
this.launching = false;
this.error = '';
this._activeCode = '';
cancelInline();
return;
@@ -188,7 +204,6 @@
return;
}
// Never navigate to checkout.paystack.com (external tab/window).
// Without access_code we cannot open Inline in-page.
this.frameable = false;
this.inline = false;
this.launching = false;
@@ -223,7 +238,6 @@
window.location.assign(dest);
return;
}
// Fallback: reload so server can reconcile via webhook.
window.location.reload();
},
onCancel: function () {
@@ -247,25 +261,34 @@
store.error = (err && err.message) ? String(err.message) : 'Could not load payment script.';
});
},
});
};
}
function ensureStore() {
if (!window.Alpine || typeof window.Alpine.store !== 'function') return null;
try {
var existing = window.Alpine.store('ladillPayCheckout');
if (existing) return existing;
} catch (e) {}
window.Alpine.store('ladillPayCheckout', createStoreDefinition());
return window.Alpine.store('ladillPayCheckout');
}
function onAlpineReady() {
document.addEventListener('alpine:init', function () {
ensureStore();
});
// If Alpine already started (late include), register immediately.
if (window.Alpine && window.Alpine.version) {
ensureStore();
}
if (window.Alpine) {
onAlpineReady();
}
document.addEventListener('alpine:init', onAlpineReady);
window.LadillPayCheckout = {
isFrameable: isFrameable,
isPaystackCheckoutUrl: isPaystackCheckoutUrl,
accessCodeFromUrl: accessCodeFromUrl,
resolveAccessCode: resolveAccessCode,
prepare: function () { return null; },
prepare: function () { loadInlineJs().catch(function () {}); return null; },
cancel: function () {
cancelInline();
var store = ensureStore();
@@ -278,24 +301,30 @@
</script>
@endonce
<template x-teleport="body">
<div x-show="showSheet"
{{-- No local x-data: inherit showSheet/checkoutUrl/accessCode from the including scope. --}}
<div
x-init="
window.LadillPayCheckout && window.LadillPayCheckout.ensureStore();
const runSync = () => {
const store = $store.ladillPayCheckout;
if (!store) return;
store.sync(
!!showSheet,
(typeof checkoutUrl === 'undefined' || checkoutUrl === null) ? '' : String(checkoutUrl),
(typeof accessCode === 'undefined' || accessCode === null) ? '' : String(accessCode),
(typeof returnUrl === 'undefined' || returnUrl === null) ? '' : String(returnUrl)
);
};
$watch('showSheet', () => runSync());
$watch('checkoutUrl', () => runSync());
$watch('accessCode', () => runSync());
$watch('returnUrl', () => runSync());
$nextTick(() => runSync());
"
x-show="showSheet"
x-cloak
data-ladill-pay-sheet
x-effect="
document.documentElement.style.overflow = showSheet ? 'hidden' : '';
if (typeof window.LadillPayCheckout !== 'undefined') {
window.LadillPayCheckout.ensureStore();
}
const store = $store.ladillPayCheckout;
if (store) {
store.sync(
showSheet,
typeof checkoutUrl !== 'undefined' ? checkoutUrl : '',
typeof accessCode !== 'undefined' ? accessCode : '',
typeof returnUrl !== 'undefined' ? returnUrl : ''
);
}
"
x-effect="document.documentElement.style.overflow = showSheet ? 'hidden' : ''"
@keydown.escape.window="if (showSheet) showSheet = false"
@ladill-pay-cancelled.window="showSheet = false"
class="fixed inset-0 z-[9999] flex items-end justify-center md:items-center md:p-6"
@@ -362,7 +391,7 @@
class="flex min-h-[60dvh] flex-col items-center justify-center gap-4 px-6 py-10 text-center md:min-h-[28rem] md:px-8 md:py-12"
data-ladill-pay-inline-status>
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-indigo-50 text-indigo-600" aria-hidden="true">
<svg x-show="!$store.ladillPayCheckout || $store.ladillPayCheckout.launching || $store.ladillPayCheckout && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.inline"
<svg x-show="!$store.ladillPayCheckout || $store.ladillPayCheckout.launching || ($store.ladillPayCheckout && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.inline)"
class="h-6 w-6 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
@@ -392,7 +421,7 @@
<button type="button"
data-ladill-pay-retry
x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.error"
@click="$store.ladillPayCheckout.sync(true, typeof checkoutUrl !== 'undefined' ? checkoutUrl : '', typeof accessCode !== 'undefined' ? accessCode : '', typeof returnUrl !== 'undefined' ? returnUrl : '')"
@click="window.LadillPayCheckout?.ensureStore()?.sync(true, (typeof checkoutUrl==='undefined'||checkoutUrl===null)?'':String(checkoutUrl), (typeof accessCode==='undefined'||accessCode===null)?'':String(accessCode), (typeof returnUrl==='undefined'||returnUrl===null)?'':String(returnUrl))"
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">
Try again
</button>
@@ -27,6 +27,7 @@ class ResponsivePaystackSheetTest extends TestCase
$this->assertStringContainsString('safe-area-inset-bottom', $html);
$this->assertStringContainsString('LadillPayCheckout', $html);
$this->assertStringContainsString('resumeTransaction', $html);
$this->assertStringContainsString("\$watch('showSheet'", $html);
$this->assertStringContainsString('js.paystack.co/v2/inline.js', $html);
$this->assertStringContainsString('Starting secure checkout', $html);
$this->assertStringContainsString('Complete payment on this screen', $html);