Files
ladill-woo-manager/resources/views/partials/paystack-sheet.blade.php
T
isaacclad a50dc46796
Deploy Ladill Woo Manager / deploy (push) Successful in 2m44s
Simplify pay wait copy and scale Paystack to fill the loading area.
Use “Please wait…” while loading, and scale the contained Paystack iframe so
checkout fills the sheet body without a floating second card.
2026-07-23 22:57:46 +00:00

1311 lines
59 KiB
PHP

{{--
Responsive payment checkout shell: bottom sheet on mobile, centered modal on desktop.
Audiences:
- buyer (default): public ticket buyers, donors, shoppers self-serve checkout
- operator: signed-in staff flows such as wallet top-up
Optional overrides: $sheetTitle, $sheetSubtitle, $sheetFooter, $sheetAria, $iframeTitle
Requires Alpine ancestor with:
- showSheet (bool)
- checkoutUrl (string) authorization / waiting URL
- accessCode (string, optional) Paystack initialize access_code for Inline
- publicKey (string, optional) unused by resumeTransaction; kept for callers
- returnUrl (string, optional) where to go after Inline onSuccess (?reference=)
checkout.paystack.com cannot be iframed (X-Frame-Options: SAMEORIGIN).
Primary path: Paystack Inline JS (PaystackPop.resumeTransaction) in-page overlay,
not a separate browser tab. Same-origin URLs (e.g. MoMo waiting) still use iframe.
--}}
@php
$audience = $audience ?? 'buyer';
$defaults = match ($audience) {
'operator' => [
'title' => 'Complete payment',
'subtitle' => null,
'aria' => 'Complete payment',
'iframe' => 'Payment checkout',
'footer' => null,
],
default => [
'title' => 'Complete your payment',
'subtitle' => 'Pay securely. You\'ll get a confirmation when it succeeds.',
'aria' => 'Complete your payment',
'iframe' => 'Secure payment checkout',
'footer' => 'Your payment is processed securely. Do not close this window until finished.',
],
};
$sheetTitle = $sheetTitle ?? $defaults['title'];
$sheetSubtitle = $sheetSubtitle ?? $defaults['subtitle'];
$sheetAria = $sheetAria ?? $defaults['aria'];
$iframeTitle = $iframeTitle ?? $defaults['iframe'];
$sheetFooter = $sheetFooter ?? $defaults['footer'];
@endphp
@once
<script>
(function () {
if (window.__ladillPayCheckoutBootstrapped) return;
window.__ladillPayCheckoutBootstrapped = true;
var INLINE_SRC = 'https://js.paystack.co/v2/inline.js';
var inlineLoading = null;
var activePopup = null;
function isFrameable(url) {
if (!url) return false;
try {
return new URL(url, window.location.href).origin === window.location.origin;
} catch (e) {
return false;
}
}
function isPaystackCheckoutUrl(url) {
if (!url) return false;
try {
var host = new URL(url, window.location.href).hostname.toLowerCase();
return host === 'checkout.paystack.com' || host.endsWith('.paystack.com');
} catch (e) {
return false;
}
}
function accessCodeFromUrl(url) {
if (!url || !isPaystackCheckoutUrl(url)) return '';
try {
var path = new URL(url, window.location.href).pathname.replace(/^\/+/, '');
var code = path.split('/')[0] || '';
return /^[A-Za-z0-9_-]{6,}$/.test(code) ? code : '';
} catch (e) {
return '';
}
}
function resolveAccessCode(accessCode, checkoutUrl) {
var code = (accessCode || '').toString().trim();
if (code) return code;
return accessCodeFromUrl(checkoutUrl);
}
function loadInlineJs() {
if (window.PaystackPop) {
return Promise.resolve(window.PaystackPop);
}
if (inlineLoading) return inlineLoading;
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;
}
var script = document.createElement('script');
script.src = INLINE_SRC;
script.async = true;
script.dataset.ladillPaystackInline = '1';
script.onload = function () { resolve(window.PaystackPop); };
script.onerror = function () {
inlineLoading = null;
reject(new Error('Paystack script failed'));
};
document.head.appendChild(script);
});
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 {
var u = new URL(returnUrl, window.location.href);
u.searchParams.set('reference', reference);
return u.toString();
} catch (e) {
var join = returnUrl.indexOf('?') >= 0 ? '&' : '?';
return returnUrl + join + 'reference=' + encodeURIComponent(reference);
}
}
function cancelInline() {
try {
if (activePopup && typeof activePopup.cancelTransaction === 'function') {
activePopup.cancelTransaction();
}
} catch (e) {}
activePopup = null;
setPaystackContainActive(false);
}
// Paystack Inline always body-appends a full-viewport modal (position:fixed 100%).
// Strategy:
// 1) On FIRST append only, put checkout iframe into our sheet mount (no later reparent
// — reparent after load reloads the frame and breaks Paystack).
// 2) Force position:absolute + fill mount (fixed+100% always paints the whole viewport
// even when the node is nested under the sheet).
// 3) Continuously re-apply styles (Paystack rewrites style on open/animate).
// 4) Hide Paystack's own dimmer; Ladill sheet owns backdrop + chrome.
var PAYSTACK_IFRAME_ALLOW = 'payment *; fullscreen *; clipboard-read *; clipboard-write *; publickey-credentials-get *';
var PAYSTACK_ALLOW_FEATURES = ['payment', 'fullscreen', 'clipboard-read', 'clipboard-write', 'publickey-credentials-get'];
var activeMountEl = null;
var activeBodyEl = null;
var pinRafId = 0;
var lastPinRect = null;
var nativeAppendChild = Node.prototype.appendChild;
var nativeInsertBefore = Node.prototype.insertBefore;
function isElementShown(el) {
if (!el || el.nodeType !== 1) return false;
if (el.getAttribute && el.getAttribute('aria-hidden') === 'true') return false;
var p = el;
while (p && p.nodeType === 1) {
try {
var s = window.getComputedStyle(p);
if (s.display === 'none' || s.visibility === 'hidden') return false;
} catch (e) {}
p = p.parentElement;
}
return true;
}
function resolveOpenSheet() {
var sheets = document.querySelectorAll('[data-ladill-pay-sheet]');
var fallback = null;
for (var i = 0; i < sheets.length; i++) {
var sheet = sheets[i];
if (!fallback) fallback = sheet;
if (sheet.getAttribute('aria-hidden') !== 'true') return sheet;
}
for (var j = 0; j < sheets.length; j++) {
if (isElementShown(sheets[j])) return sheets[j];
}
return fallback;
}
function resolvePaystackMount() {
var sheet = resolveOpenSheet();
if (sheet) {
var mount = sheet.querySelector('[data-ladill-pay-mount]');
if (mount) return mount;
var body = sheet.querySelector('[data-ladill-pay-body]');
if (body) return body;
}
return document.querySelector('[data-ladill-pay-mount]')
|| document.querySelector('[data-ladill-pay-body]');
}
function getPaystackMount() {
if (activeMountEl && activeMountEl.isConnected) return activeMountEl;
return resolvePaystackMount();
}
function getPaystackBody() {
if (activeBodyEl && activeBodyEl.isConnected) return activeBodyEl;
var mount = getPaystackMount();
if (mount) {
var body = mount.closest('[data-ladill-pay-body]');
if (body) return body;
}
var sheet = resolveOpenSheet();
return sheet ? sheet.querySelector('[data-ladill-pay-body]') : document.querySelector('[data-ladill-pay-body]');
}
function armPaystackMount() {
var sheet = resolveOpenSheet();
activeMountEl = sheet
? (sheet.querySelector('[data-ladill-pay-mount]') || sheet.querySelector('[data-ladill-pay-body]'))
: resolvePaystackMount();
activeBodyEl = sheet
? sheet.querySelector('[data-ladill-pay-body]')
: getPaystackBody();
// Keep the mount measurable while we pin (Alpine x-show can set display:none).
try {
if (activeMountEl) {
activeMountEl.style.setProperty('display', 'block', 'important');
activeMountEl.style.setProperty('visibility', 'visible', 'important');
activeMountEl.style.setProperty('pointer-events', 'none', 'important');
}
} catch (e) {}
return activeMountEl;
}
function measurePinTarget() {
var el = getPaystackMount() || getPaystackBody();
if (!el) return null;
var rect = el.getBoundingClientRect();
if (rect.width >= 40 && rect.height >= 40) {
return rect;
}
// Mount may still be display:none — fall back to the white body panel.
var body = getPaystackBody();
if (body && body !== el) {
rect = body.getBoundingClientRect();
if (rect.width >= 40 && rect.height >= 40) return rect;
}
return null;
}
function waitForPinTarget(timeoutMs) {
timeoutMs = timeoutMs || 1200;
return new Promise(function (resolve) {
var start = Date.now();
var tick = function () {
armPaystackMount();
var rect = measurePinTarget();
if (rect) {
syncPaystackMountBox();
resolve(rect);
return;
}
if (Date.now() - start >= timeoutMs) {
syncPaystackMountBox();
resolve(null);
return;
}
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(tick);
} else {
setTimeout(tick, 16);
}
};
tick();
});
}
function isPaystackCheckoutFrame(iframe) {
if (!iframe || !iframe.tagName || iframe.tagName.toUpperCase() !== 'IFRAME') return false;
var id = (iframe.id || '').toLowerCase();
return id.indexOf('inline-checkout') === 0 || id.indexOf('embed-checkout') === 0;
}
function isPaystackBackgroundFrame(iframe) {
if (!iframe || !iframe.tagName || iframe.tagName.toUpperCase() !== 'IFRAME') return false;
var id = (iframe.id || '').toLowerCase();
return id.indexOf('inline-background') === 0;
}
function isPaystackIframe(iframe) {
if (!iframe || !iframe.tagName || iframe.tagName.toUpperCase() !== 'IFRAME') return false;
var id = (iframe.id || '').toLowerCase();
var name = (iframe.getAttribute('name') || iframe.name || '').toLowerCase();
var src = '';
try { src = (iframe.getAttribute('src') || iframe.src || '').toLowerCase(); } catch (e) {}
return src.indexOf('paystack') !== -1
|| src.indexOf('checkout.paystack.com') !== -1
|| id.indexOf('paystack') !== -1
|| id.indexOf('inline-checkout') !== -1
|| id.indexOf('inline-background') !== -1
|| id.indexOf('embed-checkout') !== -1
|| name.indexOf('paystack') !== -1
|| name.indexOf('inline-checkout') !== -1;
}
function mergeIframeAllow(current) {
var parts = current
? current.split(';').map(function (p) { return p.trim(); }).filter(Boolean)
: [];
var lower = parts.map(function (p) { return p.toLowerCase(); });
PAYSTACK_ALLOW_FEATURES.forEach(function (perm) {
if (!lower.some(function (p) { return p === perm || p.indexOf(perm + ' ') === 0; })) {
parts.push(perm + ' *');
}
});
return parts.join('; ') || PAYSTACK_IFRAME_ALLOW;
}
function applyIframePermissions(iframe) {
if (!iframe || !iframe.setAttribute) return;
try {
var next = mergeIframeAllow((iframe.getAttribute('allow') || '').trim());
if ((iframe.getAttribute('allow') || '') !== next) {
iframe.setAttribute('allow', next);
}
if (!iframe.hasAttribute('allowfullscreen')) {
iframe.setAttribute('allowfullscreen', '');
}
try { iframe.allowFullscreen = true; } catch (e) {}
} catch (e) {}
}
function isCheckoutFrame(iframe) {
if (!iframe || !iframe.tagName || iframe.tagName.toUpperCase() !== 'IFRAME') return false;
var id = (iframe.id || '');
return id.indexOf('inline-checkout') === 0
|| id.indexOf('embed-checkout') === 0
|| iframe.hasAttribute('data-ladill-pay-contained');
}
function styleMountAsContainer(mount) {
if (!mount) return;
try {
// Create a fixed containing block so position:fixed descendants still
// cannot escape to the viewport (transform + isolation).
var s = mount.style;
s.setProperty('position', 'absolute', 'important');
s.setProperty('inset', '0', 'important');
s.setProperty('left', '0', 'important');
s.setProperty('top', '0', 'important');
s.setProperty('right', '0', 'important');
s.setProperty('bottom', '0', 'important');
s.setProperty('width', '100%', 'important');
s.setProperty('height', '100%', 'important');
s.setProperty('overflow', 'hidden', 'important');
s.setProperty('display', 'block', 'important');
s.setProperty('visibility', 'visible', 'important');
s.setProperty('background', '#fff', 'important');
s.setProperty('transform', 'translateZ(0)', 'important');
s.setProperty('isolation', 'isolate', 'important');
s.setProperty('contain', 'layout size', 'important');
s.setProperty('z-index', '1', 'important');
} catch (e) {}
}
function styleCheckoutInMount(iframe) {
if (!iframe) return;
try {
iframe.setAttribute('data-ladill-pay-contained', '1');
iframe.setAttribute('data-ladill-pay-mode', 'mount');
var s = iframe.style;
// Absolute fill of mount — NOT position:fixed 100% (that always paints viewport).
// Transparent so we don't paint a second white surface under Paystack's card.
s.setProperty('position', 'absolute', 'important');
s.setProperty('left', '0', 'important');
s.setProperty('top', '0', 'important');
s.setProperty('right', '0', 'important');
s.setProperty('bottom', '0', 'important');
s.setProperty('width', '100%', 'important');
s.setProperty('height', '100%', 'important');
s.setProperty('max-width', 'none', 'important');
s.setProperty('max-height', 'none', 'important');
s.setProperty('margin', '0', 'important');
s.setProperty('padding', '0', 'important');
s.setProperty('border', '0', 'important');
s.setProperty('border-radius', '0', 'important');
s.setProperty('box-shadow', 'none', 'important');
s.setProperty('filter', 'none', 'important');
s.setProperty('z-index', '2', 'important');
s.setProperty('visibility', 'visible', 'important');
s.setProperty('display', 'block', 'important');
s.setProperty('opacity', '1', 'important');
s.setProperty('background', 'transparent', 'important');
s.setProperty('background-color', 'transparent', 'important');
s.setProperty('pointer-events', 'auto', 'important');
s.setProperty('color-scheme', 'light', 'important');
// Scale up so Paystack's centered card fills the loading area; mount clips
// letterbox margins and the card's drop-shadow (cross-origin, can't restyle).
s.setProperty('transform', 'scale(1.32)', 'important');
s.setProperty('transform-origin', 'center center', 'important');
applyIframePermissions(iframe);
} catch (e) {}
}
function styleCheckoutPinnedToRect(iframe, rect) {
if (!iframe || !rect) return;
try {
iframe.setAttribute('data-ladill-pay-contained', '1');
iframe.setAttribute('data-ladill-pay-mode', 'pin');
var s = iframe.style;
var left = Math.max(0, Math.round(rect.left));
var top = Math.max(0, Math.round(rect.top));
var width = Math.max(0, Math.round(rect.width));
var height = Math.max(0, Math.round(rect.height));
// Direct pixels — CSS vars in setProperty are unreliable across browsers.
s.setProperty('position', 'fixed', 'important');
s.setProperty('left', left + 'px', 'important');
s.setProperty('top', top + 'px', 'important');
s.setProperty('right', 'auto', 'important');
s.setProperty('bottom', 'auto', 'important');
s.setProperty('width', width + 'px', 'important');
s.setProperty('height', height + 'px', 'important');
s.setProperty('max-width', 'none', 'important');
s.setProperty('max-height', 'none', 'important');
s.setProperty('margin', '0', 'important');
s.setProperty('padding', '0', 'important');
s.setProperty('border', '0', 'important');
s.setProperty('border-radius', '0', 'important');
s.setProperty('box-shadow', 'none', 'important');
s.setProperty('filter', 'none', 'important');
s.setProperty('z-index', '10050', 'important');
s.setProperty('visibility', 'visible', 'important');
s.setProperty('display', 'block', 'important');
s.setProperty('opacity', '1', 'important');
s.setProperty('background', 'transparent', 'important');
s.setProperty('background-color', 'transparent', 'important');
s.setProperty('pointer-events', 'auto', 'important');
s.setProperty('color-scheme', 'light', 'important');
s.setProperty('transform', 'scale(1.32)', 'important');
s.setProperty('transform-origin', 'center center', 'important');
applyIframePermissions(iframe);
} catch (e) {}
}
function pinCheckoutIframe(iframe) {
if (!iframe || !window.__ladillContainPaystack) return;
var mount = getPaystackMount();
if (mount) {
styleMountAsContainer(mount);
// Prefer living inside the mount (absolute fill). Only first-append
// redirects into mount; if already elsewhere, pin with fixed+pixels.
if (iframe.parentNode === mount) {
styleCheckoutInMount(iframe);
return;
}
}
var rect = measurePinTarget() || lastPinRect;
if (rect) {
lastPinRect = rect;
styleCheckoutPinnedToRect(iframe, rect);
}
}
function hidePaystackBackground(iframe) {
if (!iframe) return;
try {
iframe.setAttribute('data-ladill-pay-bg-hidden', '1');
var s = iframe.style;
s.setProperty('display', 'none', 'important');
s.setProperty('visibility', 'hidden', 'important');
s.setProperty('pointer-events', 'none', 'important');
s.setProperty('opacity', '0', 'important');
s.setProperty('z-index', '-1', 'important');
s.setProperty('width', '0', 'important');
s.setProperty('height', '0', 'important');
s.setProperty('left', '-9999px', 'important');
s.setProperty('top', '-9999px', 'important');
s.setProperty('position', 'fixed', 'important');
} catch (e) {}
}
function containPaystackIframe(iframe) {
if (!iframe || !window.__ladillContainPaystack) return;
if (isPaystackBackgroundFrame(iframe)) {
hidePaystackBackground(iframe);
return;
}
if (!isCheckoutFrame(iframe) && !isPaystackCheckoutFrame(iframe)) {
if (isPaystackIframe(iframe)) applyIframePermissions(iframe);
return;
}
pinCheckoutIframe(iframe);
}
function ensurePaystackContained(root) {
if (!window.__ladillContainPaystack) return;
try {
syncPaystackMountBox();
var bgs = document.querySelectorAll('iframe[id^="inline-background"]');
for (var j = 0; j < bgs.length; j++) hidePaystackBackground(bgs[j]);
var checks = document.querySelectorAll(
'iframe[id^="inline-checkout"], iframe[id^="embed-checkout"], iframe[data-ladill-pay-contained]'
);
for (var k = 0; k < checks.length; k++) pinCheckoutIframe(checks[k]);
if (root && root !== document && root.querySelectorAll) {
var nested = root.querySelectorAll('iframe');
for (var i = 0; i < nested.length; i++) containPaystackIframe(nested[i]);
}
} catch (e) {}
}
function ensurePaystackIframePermissions(root) {
try {
var scope = root || document;
var nodes = scope.querySelectorAll ? scope.querySelectorAll('iframe') : [];
for (var i = 0; i < nodes.length; i++) {
var iframe = nodes[i];
if (isPaystackIframe(iframe) || isCheckoutFrame(iframe)) {
applyIframePermissions(iframe);
}
}
ensurePaystackContained(scope);
} catch (e) {}
}
function syncPaystackMountBox() {
try {
if (!window.__ladillContainPaystack) return;
var rect = measurePinTarget();
var root = document.documentElement;
if (!rect) {
root.style.setProperty('--ladill-pay-left', '0px');
root.style.setProperty('--ladill-pay-top', '0px');
root.style.setProperty('--ladill-pay-width', '0px');
root.style.setProperty('--ladill-pay-height', '0px');
return;
}
lastPinRect = rect;
root.style.setProperty('--ladill-pay-left', Math.max(0, Math.round(rect.left)) + 'px');
root.style.setProperty('--ladill-pay-top', Math.max(0, Math.round(rect.top)) + 'px');
root.style.setProperty('--ladill-pay-width', Math.max(0, Math.round(rect.width)) + 'px');
root.style.setProperty('--ladill-pay-height', Math.max(0, Math.round(rect.height)) + 'px');
} catch (e) {}
}
function startPinLoop() {
stopPinLoop();
var tick = function () {
if (!window.__ladillContainPaystack) {
pinRafId = 0;
return;
}
ensurePaystackContained(document);
pinRafId = typeof requestAnimationFrame === 'function'
? requestAnimationFrame(tick)
: setTimeout(tick, 32);
};
tick();
}
function stopPinLoop() {
if (!pinRafId) return;
if (typeof cancelAnimationFrame === 'function') {
cancelAnimationFrame(pinRafId);
} else {
clearTimeout(pinRafId);
}
pinRafId = 0;
}
function setPaystackContainActive(active) {
window.__ladillContainPaystack = !!active;
try {
document.documentElement.classList.toggle('ladill-pay-inline-active', !!active);
} catch (e) {}
if (active) {
armPaystackMount();
styleMountAsContainer(getPaystackMount());
installIframeAllowHook();
installAppendHook();
ensurePaystackContained(document);
syncPaystackMountBox();
startPinLoop();
if (!window.__ladillPayMountResizeBound) {
window.__ladillPayMountResizeBound = true;
window.addEventListener('resize', function () {
syncPaystackMountBox();
ensurePaystackContained(document);
}, { passive: true });
window.addEventListener('orientationchange', function () {
setTimeout(function () {
syncPaystackMountBox();
ensurePaystackContained(document);
}, 100);
}, { passive: true });
window.addEventListener('scroll', function () {
if (window.__ladillContainPaystack) {
syncPaystackMountBox();
ensurePaystackContained(document);
}
}, { passive: true, capture: true });
}
} else {
stopPinLoop();
lastPinRect = null;
try {
if (activeMountEl) {
activeMountEl.style.removeProperty('display');
activeMountEl.style.removeProperty('visibility');
activeMountEl.style.removeProperty('pointer-events');
activeMountEl.style.removeProperty('transform');
activeMountEl.style.removeProperty('isolation');
activeMountEl.style.removeProperty('contain');
}
} catch (e) {}
activeMountEl = null;
activeBodyEl = null;
try {
var root = document.documentElement;
root.style.removeProperty('--ladill-pay-left');
root.style.removeProperty('--ladill-pay-top');
root.style.removeProperty('--ladill-pay-width');
root.style.removeProperty('--ladill-pay-height');
} catch (e) {}
}
}
function installIframeAllowHook() {
if (window.__ladillIframeAllowHook) return;
window.__ladillIframeAllowHook = true;
try {
var orig = Document.prototype.createElement;
Document.prototype.createElement = function (tagName, options) {
var el = options !== undefined ? orig.call(this, tagName, options) : orig.call(this, tagName);
try {
if (String(tagName).toLowerCase() === 'iframe') {
applyIframePermissions(el);
}
} catch (e) {}
return el;
};
} catch (e) {}
}
function installAppendHook() {
if (window.__ladillAppendHook) return;
window.__ladillAppendHook = true;
try {
Node.prototype.appendChild = function (child) {
try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
if (isPaystackBackgroundFrame(child)) {
var bg = nativeAppendChild.call(this, child);
hidePaystackBackground(child);
return bg;
}
if (isCheckoutFrame(child) || isPaystackCheckoutFrame(child)
|| (child.id || '').indexOf('inline-checkout') === 0
|| (child.id || '').indexOf('embed-checkout') === 0) {
var mount = getPaystackMount();
if (mount) {
styleMountAsContainer(mount);
// FIRST insert goes into mount — avoids later reparent reload.
var mounted = nativeAppendChild.call(mount, child);
styleCheckoutInMount(child);
return mounted;
}
}
}
} catch (e) {}
var result = nativeAppendChild.call(this, child);
try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
containPaystackIframe(child);
}
} catch (e2) {}
return result;
};
Node.prototype.insertBefore = function (child, ref) {
try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
if (isPaystackBackgroundFrame(child)) {
var bg = nativeInsertBefore.call(this, child, ref);
hidePaystackBackground(child);
return bg;
}
if (isCheckoutFrame(child) || isPaystackCheckoutFrame(child)) {
var mount = getPaystackMount();
if (mount) {
styleMountAsContainer(mount);
var mounted = nativeAppendChild.call(mount, child);
styleCheckoutInMount(child);
return mounted;
}
}
}
} catch (e) {}
var result = nativeInsertBefore.call(this, child, ref);
try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
containPaystackIframe(child);
}
} catch (e2) {}
return result;
};
} catch (e) {}
}
function watchPaystackIframes() {
if (window.__ladillPaystackIframeWatch) return;
window.__ladillPaystackIframeWatch = true;
installIframeAllowHook();
installAppendHook();
ensurePaystackIframePermissions(document);
try {
var obs = new MutationObserver(function (mutations) {
var dirty = false;
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (m.type === 'attributes' && m.target) {
if (m.target.tagName && m.target.tagName.toUpperCase() === 'IFRAME') {
if (isPaystackIframe(m.target)) {
applyIframePermissions(m.target);
// Paystack rewrites style when animating the popup open — re-contain.
if (window.__ladillContainPaystack) {
containPaystackIframe(m.target);
}
}
dirty = true;
}
continue;
}
if (!m.addedNodes || !m.addedNodes.length) continue;
for (var n = 0; n < m.addedNodes.length; n++) {
var node = m.addedNodes[n];
if (!node || node.nodeType !== 1) continue;
if (node.tagName && node.tagName.toUpperCase() === 'IFRAME') {
applyIframePermissions(node);
if (window.__ladillContainPaystack) containPaystackIframe(node);
dirty = true;
} else if (node.querySelectorAll) {
var nested = node.querySelectorAll('iframe');
for (var k = 0; k < nested.length; k++) {
applyIframePermissions(nested[k]);
if (window.__ladillContainPaystack) containPaystackIframe(nested[k]);
dirty = true;
}
}
}
}
if (dirty) {
ensurePaystackIframePermissions(document);
}
});
obs.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['allow', 'src', 'id', 'name', 'style', 'class'],
});
} catch (e) {}
var polls = 0;
var pollId = setInterval(function () {
ensurePaystackIframePermissions(document);
polls += 1;
if (polls >= 60) clearInterval(pollId);
}, 200);
}
function createStoreDefinition() {
return {
frameable: false,
inline: false,
ready: false,
launching: false,
error: '',
_launchToken: 0,
_activeCode: '',
reset() {
this.frameable = false;
this.inline = false;
this.ready = false;
this.launching = false;
this.error = '';
this._activeCode = '';
cancelInline();
},
sync(show, url, accessCode, returnUrl) {
url = (url || '').toString();
accessCode = (accessCode || '').toString();
returnUrl = (returnUrl || '').toString();
if (!show) {
this._launchToken += 1;
this.reset();
return;
}
this.ready = !!(url || accessCode);
if (!url && !accessCode) {
this.frameable = false;
this.inline = false;
this.launching = true; // waiting for checkout payload
this.error = '';
this._activeCode = '';
setPaystackContainActive(false);
return;
}
if (isFrameable(url)) {
this.frameable = true;
this.inline = false;
this.launching = false;
this.error = '';
this._activeCode = '';
cancelInline();
return;
}
var code = resolveAccessCode(accessCode, url);
if (code) {
this.frameable = false;
this.inline = true;
if (this._activeCode === code && (this.launching || activePopup)) {
return;
}
this.launchInline(code, returnUrl);
return;
}
// Never navigate to checkout.paystack.com (external tab/window).
this.frameable = false;
this.inline = false;
this.launching = false;
this._activeCode = '';
setPaystackContainActive(false);
this.error = isPaystackCheckoutUrl(url)
? 'Could not start in-app payment. Please try again.'
: 'Secure checkout could not be opened in-page. Please try again.';
},
launchInline(accessCode, returnUrl) {
var store = this;
var token = ++this._launchToken;
this._activeCode = accessCode;
this.launching = true;
this.error = '';
// Wait for the bottomsheet/modal body to lay out, then open Paystack
// and pin its body iframe over that loading area (no reparent).
var start = function () {
if (token !== store._launchToken) return;
armPaystackMount();
setPaystackContainActive(true);
watchPaystackIframes();
waitForPinTarget(1200).then(function () {
if (token !== store._launchToken) return;
return loadInlineJs();
}).then(function (PaystackPop) {
if (token !== store._launchToken) return;
if (!PaystackPop) throw new Error('Paystack unavailable');
try {
if (activePopup && typeof activePopup.cancelTransaction === 'function') {
activePopup.cancelTransaction();
}
} catch (e) {}
activePopup = null;
armPaystackMount();
setPaystackContainActive(true);
syncPaystackMountBox();
// PaystackPop constructor appends iframes to document.body.
// We leave them there and pin checkout over our sheet body.
var popup = new PaystackPop();
activePopup = popup;
ensurePaystackContained(document);
popup.resumeTransaction(accessCode, {
onLoad: function () {
if (token !== store._launchToken) return;
store.launching = false;
ensurePaystackIframePermissions(document);
ensurePaystackContained(document);
// Alpine swaps header/spinner; re-measure then re-pin.
setTimeout(function () { ensurePaystackContained(document); }, 0);
setTimeout(function () { ensurePaystackContained(document); }, 50);
setTimeout(function () { ensurePaystackContained(document); }, 150);
setTimeout(function () { ensurePaystackContained(document); }, 400);
},
onSuccess: function (transaction) {
if (token !== store._launchToken) return;
store.launching = false;
setPaystackContainActive(false);
var reference = (transaction && (transaction.reference || transaction.trxref)) || '';
var dest = buildReturnUrl(returnUrl, reference);
if (dest) {
window.location.assign(dest);
return;
}
window.location.reload();
},
onCancel: function () {
if (token !== store._launchToken) return;
store.launching = false;
store._activeCode = '';
activePopup = null;
setPaystackContainActive(false);
window.dispatchEvent(new CustomEvent('ladill-pay-cancelled'));
},
onError: function (err) {
if (token !== store._launchToken) return;
store.launching = false;
store._activeCode = '';
setPaystackContainActive(false);
store.error = (err && err.message) ? String(err.message) : 'Could not open secure payment.';
},
});
setTimeout(function () { ensurePaystackContained(document); }, 0);
setTimeout(function () { ensurePaystackContained(document); }, 50);
setTimeout(function () { ensurePaystackContained(document); }, 200);
setTimeout(function () { ensurePaystackContained(document); }, 500);
}).catch(function (err) {
if (token !== store._launchToken) return;
store.launching = false;
store._activeCode = '';
setPaystackContainActive(false);
store.error = (err && err.message) ? String(err.message) : 'Could not load payment script.';
});
};
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(function () { requestAnimationFrame(start); });
} else {
setTimeout(start, 32);
}
},
};
}
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');
}
document.addEventListener('alpine:init', function () {
ensureStore();
});
// If Alpine already started (late include), register immediately.
if (window.Alpine && window.Alpine.version) {
ensureStore();
}
watchPaystackIframes();
window.LadillPayCheckout = {
isFrameable: isFrameable,
isPaystackCheckoutUrl: isPaystackCheckoutUrl,
accessCodeFromUrl: accessCodeFromUrl,
resolveAccessCode: resolveAccessCode,
prepare: function () { loadInlineJs().catch(function () {}); return null; },
cancel: function () {
cancelInline();
var store = ensureStore();
if (store) store.reset();
},
open: function () { return null; },
ensureStore: ensureStore,
};
})();
</script>
@endonce
{{-- Keep Paystack checkout flush inside the Ladill sheet loading area (no double card). --}}
@once('ladill-paystack-contain-css')
<style>
/* Hide Paystack's own full-screen dimmer — Ladill backdrop owns that. */
html.ladill-pay-inline-active iframe[id^="inline-background"],
iframe[data-ladill-pay-bg-hidden] {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
z-index: -1 !important;
left: -9999px !important;
top: -9999px !important;
position: fixed !important;
box-shadow: none !important;
}
[data-ladill-pay-mount] {
position: absolute;
inset: 0;
overflow: hidden;
background: #fff;
min-height: 12rem;
transform: translateZ(0);
isolation: isolate;
}
html.ladill-pay-inline-active [data-ladill-pay-mount] {
display: block !important;
visibility: visible !important;
pointer-events: auto !important;
background: transparent !important;
/* Clip Paystack's internal card shadow so it doesn't float over empty chrome. */
overflow: hidden !important;
}
html.ladill-pay-inline-active [data-ladill-pay-sheet] {
z-index: 9999 !important;
}
/* Live: one flat surface — no second elevated white stage under the iframe. */
html.ladill-pay-inline-active [data-ladill-pay-panel][data-paystack-live="1"] {
box-shadow: none !important;
background: #fff !important;
}
html.ladill-pay-inline-active [data-ladill-pay-panel][data-paystack-live="1"] [data-ladill-pay-body] {
background: transparent !important;
min-height: 0 !important;
}
html.ladill-pay-inline-active [data-ladill-pay-panel][data-paystack-live="1"] [data-ladill-pay-header-minimal] {
border-bottom-color: transparent !important;
background: #fff !important;
}
/*
Scale Paystack's centered card so it fills the loading area.
Mount overflow:hidden clips the letterbox margins + Paystack's own card shadow.
*/
html.ladill-pay-inline-active [data-ladill-pay-mount] iframe[id^="inline-checkout"],
html.ladill-pay-inline-active [data-ladill-pay-mount] iframe[id^="embed-checkout"],
html.ladill-pay-inline-active [data-ladill-pay-mount] iframe[data-ladill-pay-contained],
html.ladill-pay-inline-active iframe[data-ladill-pay-mode="mount"] {
position: absolute !important;
left: 0 !important;
top: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
max-width: none !important;
max-height: none !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
filter: none !important;
z-index: 2 !important;
visibility: visible !important;
display: block !important;
opacity: 1 !important;
background: transparent !important;
background-color: transparent !important;
color-scheme: light !important;
pointer-events: auto !important;
transform: scale(1.32) !important;
transform-origin: center center !important;
}
html.ladill-pay-inline-active body > iframe[id^="inline-checkout"],
html.ladill-pay-inline-active body > iframe[id^="embed-checkout"],
html.ladill-pay-inline-active iframe[data-ladill-pay-mode="pin"] {
position: fixed !important;
left: var(--ladill-pay-left, 0px) !important;
top: var(--ladill-pay-top, 0px) !important;
right: auto !important;
bottom: auto !important;
width: var(--ladill-pay-width, 0px) !important;
height: var(--ladill-pay-height, 0px) !important;
max-width: none !important;
max-height: none !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
filter: none !important;
z-index: 10050 !important;
visibility: visible !important;
display: block !important;
opacity: 1 !important;
background: transparent !important;
background-color: transparent !important;
color-scheme: light !important;
pointer-events: auto !important;
transform: scale(1.32) !important;
transform-origin: center center !important;
}
</style>
@endonce
<template x-teleport="body">
{{-- 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' : ''"
@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"
role="dialog"
aria-modal="true"
:aria-hidden="(!showSheet).toString()"
aria-label="{{ $sheetAria }}">
<div class="absolute inset-0 bg-black/60"
data-ladill-pay-backdrop
x-show="showSheet"
x-transition:enter="transition-opacity duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showSheet = false; window.LadillPayCheckout?.cancel?.()">
</div>
{{--
ONE surface everywhere (POS / Merchant / Mini / Give / Invoice / Events):
- Mobile: bottom sheet
- Desktop: centered modal
- Paystack Inline fills the sheet body loading area (not a second full-screen modal)
--}}
<div data-ladill-pay-panel
x-show="showSheet"
x-transition:enter="transition duration-300 ease-out md:duration-200"
x-transition:enter-start="translate-y-full opacity-0 md:translate-y-0 md:scale-95"
x-transition:enter-end="translate-y-0 opacity-100 md:scale-100"
x-transition:leave="transition duration-200 ease-in md:duration-150"
x-transition:leave-start="translate-y-0 opacity-100 md:scale-100"
x-transition:leave-end="translate-y-full opacity-0 md:translate-y-0 md:scale-95"
class="relative flex w-full flex-col overflow-hidden rounded-t-2xl bg-white md:rounded-2xl"
:data-paystack-live="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable) ? '1' : '0'"
:class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable)
? 'shadow-none ring-1 ring-black/10 md:max-w-[min(100vw-1.5rem,760px)]'
: 'shadow-2xl md:max-w-lg'"
:style="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable)
? 'height: min(90dvh, 700px); max-height: 90dvh; padding-bottom: 0;'
: 'height: min(90dvh, 720px); max-height: 90dvh; padding-bottom: env(safe-area-inset-bottom, 0px);'"
@click.stop>
{{-- Full header while loading / error / same-origin; compact bar once Paystack fills the body --}}
<div x-show="!$store.ladillPayCheckout
|| $store.ladillPayCheckout.frameable
|| $store.ladillPayCheckout.launching
|| $store.ladillPayCheckout.error
|| !$store.ladillPayCheckout.inline"
class="relative flex shrink-0 flex-col gap-0.5 border-b border-slate-100 px-4 pb-3 pt-5 md:px-5 md:pt-3"
style="padding-top: max(1.25rem, env(safe-area-inset-top, 0px))"
data-ladill-pay-header>
<div class="absolute left-1/2 top-2 h-1 w-10 -translate-x-1/2 rounded-full bg-slate-200 md:hidden" data-ladill-pay-handle aria-hidden="true"></div>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="text-sm font-semibold text-slate-800">{{ $sheetTitle }}</p>
@if (! empty($sheetSubtitle))
<p class="mt-0.5 text-xs leading-snug text-slate-500">{{ $sheetSubtitle }}</p>
@endif
</div>
<button type="button"
x-ref="checkoutClose"
x-init="$watch('showSheet', value => { if (value) { $nextTick(() => $refs.checkoutClose?.focus()) } })"
@click="showSheet = false; window.LadillPayCheckout?.cancel?.()"
class="shrink-0 rounded-full p-1.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
aria-label="Close">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div x-show="$store.ladillPayCheckout
&& $store.ladillPayCheckout.inline
&& !$store.ladillPayCheckout.launching
&& !$store.ladillPayCheckout.error
&& !$store.ladillPayCheckout.frameable"
class="relative z-20 flex shrink-0 items-center justify-between px-3 pb-2 pt-3 md:px-4 md:pt-2"
style="padding-top: max(0.75rem, env(safe-area-inset-top, 0px))"
data-ladill-pay-header-minimal>
<div class="absolute left-1/2 top-2 h-1 w-10 -translate-x-1/2 rounded-full bg-slate-200 md:hidden" data-ladill-pay-handle aria-hidden="true"></div>
<p class="pl-1 text-xs font-medium text-slate-500">{{ $sheetTitle }}</p>
<button type="button"
data-ladill-pay-close-live
@click="showSheet = false; window.LadillPayCheckout?.cancel?.()"
class="shrink-0 rounded-full p-1.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
aria-label="Close">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- Body: spinner while starting; Paystack fills this flush when ready (no second card stage) --}}
<div class="relative min-h-0 flex-1 overflow-hidden overscroll-contain"
data-ladill-pay-body
:class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable)
? 'bg-transparent'
: 'bg-white'"
:style="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable)
? 'min-height: 0;'
: 'min-height: 22rem;'">
<iframe x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.frameable"
:src="showSheet && $store.ladillPayCheckout && $store.ladillPayCheckout.frameable ? checkoutUrl : ''"
class="absolute inset-0 h-full w-full border-0"
allow="payment *; fullscreen *; clipboard-read *; clipboard-write *"
title="{{ $iframeTitle }}"></iframe>
{{-- Loading-area mount: Paystack fills this edge-to-edge when live. --}}
<div x-show="!$store.ladillPayCheckout || !$store.ladillPayCheckout.frameable"
data-ladill-pay-mount
class="absolute inset-0"
:class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error)
? 'bg-transparent'
: 'bg-white'"
style="min-height: 12rem"
aria-hidden="true"></div>
<div x-show="(!$store.ladillPayCheckout || !$store.ladillPayCheckout.frameable)
&& (!$store.ladillPayCheckout
|| $store.ladillPayCheckout.launching
|| $store.ladillPayCheckout.error
|| !$store.ladillPayCheckout.inline
|| !$store.ladillPayCheckout.ready)"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 z-10 flex flex-col items-center justify-center gap-4 bg-white px-6 py-10 text-center 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.error"
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>
</svg>
<svg x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.error"
class="h-6 w-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"/>
</svg>
</div>
<div class="max-w-xs space-y-1 md:max-w-sm">
<p class="text-sm font-semibold text-slate-800"
data-ladill-pay-status-title
x-text="$store.ladillPayCheckout && $store.ladillPayCheckout.error
? 'Checkout unavailable'
: 'Please wait…'"></p>
<p class="text-xs leading-snug text-slate-500"
x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.error"
x-text="$store.ladillPayCheckout && $store.ladillPayCheckout.error
? $store.ladillPayCheckout.error
: ''"></p>
</div>
<button type="button"
data-ladill-pay-retry
x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.error"
@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>
<button type="button"
@click="showSheet = false; window.LadillPayCheckout?.cancel?.()"
class="text-xs font-medium text-slate-500 hover:text-slate-700">
Cancel
</button>
</div>
</div>
@if (! empty($sheetFooter))
<p x-show="!$store.ladillPayCheckout
|| $store.ladillPayCheckout.frameable
|| $store.ladillPayCheckout.launching
|| $store.ladillPayCheckout.error
|| !$store.ladillPayCheckout.inline"
class="shrink-0 border-t border-slate-100 bg-slate-50 px-4 py-2 text-center text-[11px] text-slate-500 md:px-5"
data-ladill-pay-footer>
{{ $sheetFooter }}
</p>
@endif
</div>
</div>
</template>