Fix POS payment sheet host and harden Inline launch.
Deploy Ladill POS / deploy (push) Successful in 42s

Use this.\$store in the host component so the bottomsheet opens, and share
the hardened Paystack sheet sync path used across apps.
This commit is contained in:
isaacclad
2026-07-21 21:55:45 +00:00
parent 26b3201822
commit dc65bc44ac
3 changed files with 82 additions and 50 deletions
@@ -5,26 +5,27 @@
accessCode: '', accessCode: '',
returnUrl: '', returnUrl: '',
syncFromStore() { syncFromStore() {
const store = $store.paymentCheckout; const store = this.$store.paymentCheckout;
if (!store) return;
this.showSheet = !!store.isOpen; this.showSheet = !!store.isOpen;
this.checkoutUrl = store.url || ''; this.checkoutUrl = store.url || '';
this.accessCode = store.accessCode || ''; this.accessCode = store.accessCode || '';
}, },
init() { init() {
// Keep local reactive mirrors so the teleported sheet always re-renders. // Keep local reactive mirrors so the teleported sheet always re-renders.
this.$watch(() => $store.paymentCheckout.isOpen, () => this.syncFromStore()); this.$watch(() => this.$store.paymentCheckout.isOpen, () => this.syncFromStore());
this.$watch(() => $store.paymentCheckout.url, () => this.syncFromStore()); this.$watch(() => this.$store.paymentCheckout.url, () => this.syncFromStore());
this.$watch(() => $store.paymentCheckout.accessCode, () => this.syncFromStore()); this.$watch(() => this.$store.paymentCheckout.accessCode, () => this.syncFromStore());
this.$watch('showSheet', (open) => { this.$watch('showSheet', (open) => {
if (!open && $store.paymentCheckout.isOpen) { if (!open && this.$store.paymentCheckout?.isOpen) {
$store.paymentCheckout.close(); this.$store.paymentCheckout.close();
} }
}); });
const url = @js(session('checkout_url')); const url = @js(session('checkout_url'));
const code = @js(session('access_code')); const code = @js(session('access_code'));
if (url || code) { if (url || code) {
$store.paymentCheckout.open(url || '', code || ''); this.$store.paymentCheckout.open(url || '', code || '');
this.syncFromStore(); this.syncFromStore();
} }
}, },
@@ -44,7 +44,8 @@
@once @once
<script> <script>
(function () { (function () {
if (window.LadillPayCheckout) return; if (window.__ladillPayCheckoutBootstrapped) return;
window.__ladillPayCheckoutBootstrapped = true;
var INLINE_SRC = 'https://js.paystack.co/v2/inline.js'; var INLINE_SRC = 'https://js.paystack.co/v2/inline.js';
var inlineLoading = null; var inlineLoading = null;
@@ -94,6 +95,10 @@
inlineLoading = new Promise(function (resolve, reject) { inlineLoading = new Promise(function (resolve, reject) {
var existing = document.querySelector('script[data-ladill-paystack-inline]'); var existing = document.querySelector('script[data-ladill-paystack-inline]');
if (existing) { if (existing) {
if (window.PaystackPop) {
resolve(window.PaystackPop);
return;
}
existing.addEventListener('load', function () { resolve(window.PaystackPop); }); existing.addEventListener('load', function () { resolve(window.PaystackPop); });
existing.addEventListener('error', function () { reject(new Error('Paystack script failed')); }); existing.addEventListener('error', function () { reject(new Error('Paystack script failed')); });
return; return;
@@ -112,6 +117,15 @@
return inlineLoading; 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) { function buildReturnUrl(returnUrl, reference) {
if (!returnUrl || !reference) return ''; if (!returnUrl || !reference) return '';
try { try {
@@ -133,12 +147,8 @@
activePopup = null; activePopup = null;
} }
function ensureStore() { function createStoreDefinition() {
if (!window.Alpine || typeof window.Alpine.store !== 'function') return null; return {
if (window.Alpine.store('ladillPayCheckout')) {
return window.Alpine.store('ladillPayCheckout');
}
window.Alpine.store('ladillPayCheckout', {
frameable: false, frameable: false,
inline: false, inline: false,
ready: false, ready: false,
@@ -156,6 +166,10 @@
cancelInline(); cancelInline();
}, },
sync(show, url, accessCode, returnUrl) { sync(show, url, accessCode, returnUrl) {
url = (url || '').toString();
accessCode = (accessCode || '').toString();
returnUrl = (returnUrl || '').toString();
if (!show) { if (!show) {
this._launchToken += 1; this._launchToken += 1;
this.reset(); this.reset();
@@ -165,7 +179,8 @@
if (!url && !accessCode) { if (!url && !accessCode) {
this.frameable = false; this.frameable = false;
this.inline = false; this.inline = false;
this.launching = false; this.launching = true; // waiting for checkout payload
this.error = '';
this._activeCode = ''; this._activeCode = '';
return; return;
} }
@@ -173,6 +188,7 @@
this.frameable = true; this.frameable = true;
this.inline = false; this.inline = false;
this.launching = false; this.launching = false;
this.error = '';
this._activeCode = ''; this._activeCode = '';
cancelInline(); cancelInline();
return; return;
@@ -187,7 +203,7 @@
this.launchInline(code, returnUrl); this.launchInline(code, returnUrl);
return; return;
} }
// Never open checkout.paystack.com in an external tab. // Never navigate to checkout.paystack.com (external tab/window).
this.frameable = false; this.frameable = false;
this.inline = false; this.inline = false;
this.launching = false; this.launching = false;
@@ -222,7 +238,6 @@
window.location.assign(dest); window.location.assign(dest);
return; return;
} }
// Fallback: reload so server can reconcile via webhook.
window.location.reload(); window.location.reload();
}, },
onCancel: function () { onCancel: function () {
@@ -246,25 +261,34 @@
store.error = (err && err.message) ? String(err.message) : 'Could not load payment script.'; 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'); 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(); ensureStore();
} }
if (window.Alpine) {
onAlpineReady();
}
document.addEventListener('alpine:init', onAlpineReady);
window.LadillPayCheckout = { window.LadillPayCheckout = {
isFrameable: isFrameable, isFrameable: isFrameable,
isPaystackCheckoutUrl: isPaystackCheckoutUrl, isPaystackCheckoutUrl: isPaystackCheckoutUrl,
accessCodeFromUrl: accessCodeFromUrl, accessCodeFromUrl: accessCodeFromUrl,
resolveAccessCode: resolveAccessCode, resolveAccessCode: resolveAccessCode,
prepare: function () { return null; }, prepare: function () { loadInlineJs().catch(function () {}); return null; },
cancel: function () { cancel: function () {
cancelInline(); cancelInline();
var store = ensureStore(); var store = ensureStore();
@@ -277,24 +301,30 @@
</script> </script>
@endonce @endonce
<template x-teleport="body"> <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 x-cloak
data-ladill-pay-sheet data-ladill-pay-sheet
x-effect=" x-effect="document.documentElement.style.overflow = showSheet ? 'hidden' : ''"
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 : ''
);
}
"
@keydown.escape.window="if (showSheet) showSheet = false" @keydown.escape.window="if (showSheet) showSheet = false"
@ladill-pay-cancelled.window="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" class="fixed inset-0 z-[9999] flex items-end justify-center md:items-center md:p-6"
@@ -361,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" 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> 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"> <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"> 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> <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> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
@@ -391,7 +421,7 @@
<button type="button" <button type="button"
data-ladill-pay-retry data-ladill-pay-retry
x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.error" 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"> 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 Try again
</button> </button>
@@ -27,6 +27,7 @@ class ResponsivePaystackSheetTest extends TestCase
$this->assertStringContainsString('safe-area-inset-bottom', $html); $this->assertStringContainsString('safe-area-inset-bottom', $html);
$this->assertStringContainsString('LadillPayCheckout', $html); $this->assertStringContainsString('LadillPayCheckout', $html);
$this->assertStringContainsString('resumeTransaction', $html); $this->assertStringContainsString('resumeTransaction', $html);
$this->assertStringContainsString("\$watch('showSheet'", $html);
$this->assertStringContainsString('js.paystack.co/v2/inline.js', $html); $this->assertStringContainsString('js.paystack.co/v2/inline.js', $html);
$this->assertStringContainsString('Starting secure checkout', $html); $this->assertStringContainsString('Starting secure checkout', $html);
$this->assertStringContainsString('Complete payment on this screen', $html); $this->assertStringContainsString('Complete payment on this screen', $html);