Force Paystack checkout into the sheet mount on first insert.
Deploy Ladill Mini / deploy (push) Successful in 53s

Redirect first iframe append into the Ladill sheet body and keep absolute
fill styles applied so checkout is not a second full-screen modal.
This commit is contained in:
isaacclad
2026-07-23 22:36:21 +00:00
parent cf5cc4d50d
commit 98a17a6207
+523 -182
View File
@@ -13,7 +13,7 @@
- publicKey (string, optional) unused by resumeTransaction; kept for callers - publicKey (string, optional) unused by resumeTransaction; kept for callers
- returnUrl (string, optional) where to go after Inline onSuccess (?reference=) - returnUrl (string, optional) where to go after Inline onSuccess (?reference=)
Paystack checkout.paystack.com cannot be iframed (X-Frame-Options: SAMEORIGIN). checkout.paystack.com cannot be iframed (X-Frame-Options: SAMEORIGIN).
Primary path: Paystack Inline JS (PaystackPop.resumeTransaction) in-page overlay, 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. not a separate browser tab. Same-origin URLs (e.g. MoMo waiting) still use iframe.
--}} --}}
@@ -148,14 +148,139 @@
setPaystackContainActive(false); setPaystackContainActive(false);
} }
// Paystack Inline defaults to a full-viewport modal (body append + position:fixed). // Paystack Inline always body-appends a full-viewport modal (position:fixed 100%).
// We contain it inside our bottomsheet/modal mount so checkout loads where the // Strategy:
// spinner is — not as a second modal on top of Ladill chrome. // 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_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 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() { function getPaystackMount() {
return document.querySelector('[data-ladill-pay-mount]'); 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) { function isPaystackCheckoutFrame(iframe) {
@@ -213,12 +338,46 @@
} catch (e) {} } catch (e) {}
} }
function styleContainedCheckout(iframe) { 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; if (!iframe) return;
try { try {
iframe.setAttribute('data-ladill-pay-contained', '1'); iframe.setAttribute('data-ladill-pay-contained', '1');
// Force embed layout; Paystack sets position:fixed inline without !important. iframe.setAttribute('data-ladill-pay-mode', 'mount');
var s = iframe.style; var s = iframe.style;
// Absolute fill of mount — NOT position:fixed 100% (that always paints viewport).
s.setProperty('position', 'absolute', 'important'); s.setProperty('position', 'absolute', 'important');
s.setProperty('left', '0', 'important'); s.setProperty('left', '0', 'important');
s.setProperty('top', '0', 'important'); s.setProperty('top', '0', 'important');
@@ -226,20 +385,81 @@
s.setProperty('bottom', '0', 'important'); s.setProperty('bottom', '0', 'important');
s.setProperty('width', '100%', 'important'); s.setProperty('width', '100%', 'important');
s.setProperty('height', '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('margin', '0', 'important');
s.setProperty('padding', '0', 'important'); s.setProperty('padding', '0', 'important');
s.setProperty('border', '0', 'important'); s.setProperty('border', '0', 'important');
s.setProperty('border-radius', '0', 'important');
s.setProperty('z-index', '2', 'important'); s.setProperty('z-index', '2', 'important');
s.setProperty('visibility', 'visible', 'important'); s.setProperty('visibility', 'visible', 'important');
s.setProperty('display', 'block', 'important'); s.setProperty('display', 'block', 'important');
s.setProperty('background', 'transparent', 'important'); s.setProperty('opacity', '1', 'important');
s.setProperty('background-color', 'transparent', 'important'); s.setProperty('background', '#fff', 'important');
s.setProperty('max-width', 'none', 'important'); s.setProperty('background-color', '#fff', 'important');
s.setProperty('max-height', 'none', 'important'); s.setProperty('pointer-events', 'auto', 'important');
s.setProperty('color-scheme', 'light', 'important');
s.setProperty('transform', 'none', 'important');
applyIframePermissions(iframe); applyIframePermissions(iframe);
} catch (e) {} } 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('z-index', '10050', 'important');
s.setProperty('visibility', 'visible', 'important');
s.setProperty('display', 'block', 'important');
s.setProperty('opacity', '1', 'important');
s.setProperty('background', '#fff', 'important');
s.setProperty('background-color', '#fff', 'important');
s.setProperty('pointer-events', 'auto', 'important');
s.setProperty('color-scheme', 'light', 'important');
s.setProperty('transform', 'none', '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) { function hidePaystackBackground(iframe) {
if (!iframe) return; if (!iframe) return;
try { try {
@@ -249,6 +469,12 @@
s.setProperty('visibility', 'hidden', 'important'); s.setProperty('visibility', 'hidden', 'important');
s.setProperty('pointer-events', 'none', 'important'); s.setProperty('pointer-events', 'none', 'important');
s.setProperty('opacity', '0', '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) {} } catch (e) {}
} }
@@ -258,41 +484,28 @@
hidePaystackBackground(iframe); hidePaystackBackground(iframe);
return; return;
} }
if (!isPaystackCheckoutFrame(iframe) && !isPaystackIframe(iframe)) return; if (!isCheckoutFrame(iframe) && !isPaystackCheckoutFrame(iframe)) {
if (isPaystackBackgroundFrame(iframe)) return; if (isPaystackIframe(iframe)) applyIframePermissions(iframe);
var mount = getPaystackMount();
if (!mount) return;
// Prefer checkout frames; skip stray non-checkout paystack frames if unsure.
if (!isPaystackCheckoutFrame(iframe) && (iframe.id || '').indexOf('inline-') !== 0) {
applyIframePermissions(iframe);
return; return;
} }
pinCheckoutIframe(iframe);
try {
if (iframe.parentNode !== mount) {
mount.appendChild(iframe);
}
styleContainedCheckout(iframe);
} catch (e) {
// If reparent fails mid-load, still try absolute restyle in place.
styleContainedCheckout(iframe);
}
} }
function ensurePaystackContained(root) { function ensurePaystackContained(root) {
if (!window.__ladillContainPaystack) return; if (!window.__ladillContainPaystack) return;
try { try {
var scope = root || document; syncPaystackMountBox();
var nodes = scope.querySelectorAll ? scope.querySelectorAll('iframe') : [];
for (var i = 0; i < nodes.length; i++) {
containPaystackIframe(nodes[i]);
}
// Paystack may leave backgrounds on body outside scope walks.
var bgs = document.querySelectorAll('iframe[id^="inline-background"]'); var bgs = document.querySelectorAll('iframe[id^="inline-background"]');
for (var j = 0; j < bgs.length; j++) { for (var j = 0; j < bgs.length; j++) hidePaystackBackground(bgs[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) {} } catch (e) {}
} }
@@ -303,9 +516,7 @@
var nodes = scope.querySelectorAll ? scope.querySelectorAll('iframe') : []; var nodes = scope.querySelectorAll ? scope.querySelectorAll('iframe') : [];
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
var iframe = nodes[i]; var iframe = nodes[i];
var src = ''; if (isPaystackIframe(iframe) || isCheckoutFrame(iframe)) {
try { src = (iframe.getAttribute('src') || '').trim(); } catch (e) {}
if (isPaystackIframe(iframe) || !src) {
applyIframePermissions(iframe); applyIframePermissions(iframe);
} }
} }
@@ -315,10 +526,17 @@
function syncPaystackMountBox() { function syncPaystackMountBox() {
try { try {
var mount = getPaystackMount(); if (!window.__ladillContainPaystack) return;
if (!mount || !window.__ladillContainPaystack) return; var rect = measurePinTarget();
var rect = mount.getBoundingClientRect();
var root = document.documentElement; 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-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-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-width', Math.max(0, Math.round(rect.width)) + 'px');
@@ -326,22 +544,78 @@
} catch (e) {} } 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) { function setPaystackContainActive(active) {
window.__ladillContainPaystack = !!active; window.__ladillContainPaystack = !!active;
try { try {
document.documentElement.classList.toggle('ladill-pay-inline-active', !!active); document.documentElement.classList.toggle('ladill-pay-inline-active', !!active);
} catch (e) {} } catch (e) {}
if (active) { if (active) {
armPaystackMount();
styleMountAsContainer(getPaystackMount());
installIframeAllowHook(); installIframeAllowHook();
installAppendHook(); installAppendHook();
ensurePaystackContained(document); ensurePaystackContained(document);
syncPaystackMountBox(); syncPaystackMountBox();
startPinLoop();
if (!window.__ladillPayMountResizeBound) { if (!window.__ladillPayMountResizeBound) {
window.__ladillPayMountResizeBound = true; window.__ladillPayMountResizeBound = true;
window.addEventListener('resize', syncPaystackMountBox, { passive: true }); window.addEventListener('resize', function () {
window.addEventListener('orientationchange', syncPaystackMountBox, { passive: true }); 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 { } 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 { try {
var root = document.documentElement; var root = document.documentElement;
root.style.removeProperty('--ladill-pay-left'); root.style.removeProperty('--ladill-pay-left');
@@ -373,48 +647,63 @@
if (window.__ladillAppendHook) return; if (window.__ladillAppendHook) return;
window.__ladillAppendHook = true; window.__ladillAppendHook = true;
try { try {
var origAppend = Node.prototype.appendChild;
Node.prototype.appendChild = function (child) { Node.prototype.appendChild = function (child) {
try { try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') { if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
if (isPaystackBackgroundFrame(child)) { if (isPaystackBackgroundFrame(child)) {
var appendedBg = origAppend.call(this, child); var bg = nativeAppendChild.call(this, child);
hidePaystackBackground(child); hidePaystackBackground(child);
return appendedBg; return bg;
} }
if (isPaystackCheckoutFrame(child) || ((child.id || '').indexOf('inline-checkout') === 0)) { if (isCheckoutFrame(child) || isPaystackCheckoutFrame(child)
|| (child.id || '').indexOf('inline-checkout') === 0
|| (child.id || '').indexOf('embed-checkout') === 0) {
var mount = getPaystackMount(); var mount = getPaystackMount();
if (mount) { if (mount) {
var appended = origAppend.call(mount, child); styleMountAsContainer(mount);
styleContainedCheckout(child); // FIRST insert goes into mount — avoids later reparent reload.
return appended; var mounted = nativeAppendChild.call(mount, child);
styleCheckoutInMount(child);
return mounted;
} }
} }
} }
} catch (e) {} } catch (e) {}
return origAppend.call(this, child); var result = nativeAppendChild.call(this, child);
try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
containPaystackIframe(child);
}
} catch (e2) {}
return result;
}; };
var origInsert = Node.prototype.insertBefore;
Node.prototype.insertBefore = function (child, ref) { Node.prototype.insertBefore = function (child, ref) {
try { try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') { if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
if (isPaystackBackgroundFrame(child)) { if (isPaystackBackgroundFrame(child)) {
var insertedBg = origInsert.call(this, child, ref); var bg = nativeInsertBefore.call(this, child, ref);
hidePaystackBackground(child); hidePaystackBackground(child);
return insertedBg; return bg;
} }
if (isPaystackCheckoutFrame(child)) { if (isCheckoutFrame(child) || isPaystackCheckoutFrame(child)) {
var mount = getPaystackMount(); var mount = getPaystackMount();
if (mount) { if (mount) {
var inserted = origAppend.call(mount, child); styleMountAsContainer(mount);
styleContainedCheckout(child); var mounted = nativeAppendChild.call(mount, child);
return inserted; styleCheckoutInMount(child);
return mounted;
} }
} }
} }
} catch (e) {} } catch (e) {}
return origInsert.call(this, child, ref); var result = nativeInsertBefore.call(this, child, ref);
try {
if (window.__ladillContainPaystack && child && child.tagName === 'IFRAME') {
containPaystackIframe(child);
}
} catch (e2) {}
return result;
}; };
} catch (e) {} } catch (e) {}
} }
@@ -556,75 +845,90 @@
this._activeCode = accessCode; this._activeCode = accessCode;
this.launching = true; this.launching = true;
this.error = ''; this.error = '';
// Arm containment before Paystack creates iframes so the first // Wait for the bottomsheet/modal body to lay out, then open Paystack
// body.appendChild is redirected into our sheet mount. // and pin its body iframe over that loading area (no reparent).
setPaystackContainActive(true); var start = function () {
watchPaystackIframes();
loadInlineJs().then(function (PaystackPop) {
if (token !== store._launchToken) return; if (token !== store._launchToken) return;
if (!PaystackPop) throw new Error('Paystack unavailable'); armPaystackMount();
try {
if (activePopup && typeof activePopup.cancelTransaction === 'function') {
activePopup.cancelTransaction();
}
} catch (e) {}
activePopup = null;
setPaystackContainActive(true); setPaystackContainActive(true);
var popup = new PaystackPop(); watchPaystackIframes();
activePopup = popup; waitForPinTarget(1200).then(function () {
popup.resumeTransaction(accessCode, { if (token !== store._launchToken) return;
onLoad: function () { return loadInlineJs();
if (token !== store._launchToken) return; }).then(function (PaystackPop) {
store.launching = false; if (token !== store._launchToken) return;
ensurePaystackIframePermissions(document); if (!PaystackPop) throw new Error('Paystack unavailable');
ensurePaystackContained(document); try {
// After Alpine hides the loading overlay, re-measure mount and re-pin iframe. if (activePopup && typeof activePopup.cancelTransaction === 'function') {
setTimeout(function () { activePopup.cancelTransaction();
ensurePaystackContained(document);
syncPaystackMountBox();
}, 0);
setTimeout(syncPaystackMountBox, 120);
setTimeout(syncPaystackMountBox, 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(); } catch (e) {}
}, activePopup = null;
onCancel: function () { armPaystackMount();
if (token !== store._launchToken) return; setPaystackContainActive(true);
store.launching = false; syncPaystackMountBox();
store._activeCode = ''; // PaystackPop constructor appends iframes to document.body.
activePopup = null; // We leave them there and pin checkout over our sheet body.
setPaystackContainActive(false); var popup = new PaystackPop();
window.dispatchEvent(new CustomEvent('ladill-pay-cancelled')); activePopup = popup;
}, ensurePaystackContained(document);
onError: function (err) { popup.resumeTransaction(accessCode, {
if (token !== store._launchToken) return; onLoad: function () {
store.launching = false; if (token !== store._launchToken) return;
store._activeCode = ''; store.launching = false;
setPaystackContainActive(false); ensurePaystackIframePermissions(document);
store.error = (err && err.message) ? String(err.message) : 'Could not open secure payment.'; 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.';
}); });
// Paystack often inserts frames a tick after resumeTransaction. };
setTimeout(function () { ensurePaystackContained(document); }, 0); if (typeof requestAnimationFrame === 'function') {
setTimeout(function () { ensurePaystackContained(document); }, 50); requestAnimationFrame(function () { requestAnimationFrame(start); });
setTimeout(function () { ensurePaystackContained(document); }, 200); } else {
}).catch(function (err) { setTimeout(start, 32);
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.';
});
}, },
}; };
} }
@@ -667,33 +971,52 @@
})(); })();
</script> </script>
@endonce @endonce
{{-- Override Paystack's full-viewport popup styles so checkout sits in our sheet mount. --}} {{-- Keep Paystack checkout inside the Ladill sheet loading area. --}}
@once('ladill-paystack-contain-css') @once('ladill-paystack-contain-css')
<style> <style>
html.ladill-pay-inline-active iframe[id^="inline-background"] { /* 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; display: none !important;
visibility: hidden !important; visibility: hidden !important;
pointer-events: none !important; pointer-events: none !important;
opacity: 0 !important; opacity: 0 !important;
width: 0 !important;
height: 0 !important;
z-index: -1 !important;
left: -9999px !important;
top: -9999px !important;
position: fixed !important;
} }
[data-ladill-pay-mount] { [data-ladill-pay-mount] {
position: absolute; position: absolute;
inset: 0; inset: 0;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
min-height: 12rem;
/* Containing block so fixed children cannot escape the sheet body. */
transform: translateZ(0);
isolation: isolate;
} }
/* When Paystack is live the shell is transparent so only Paystack's own card shows. */ html.ladill-pay-inline-active [data-ladill-pay-mount] {
html.ladill-pay-inline-active [data-ladill-pay-panel][data-paystack-live="1"] { display: block !important;
background: transparent !important; visibility: visible !important;
box-shadow: none !important; pointer-events: auto !important;
} }
html.ladill-pay-inline-active [data-ladill-pay-panel][data-paystack-live="1"] [data-ladill-pay-mount] { html.ladill-pay-inline-active [data-ladill-pay-panel] {
background: transparent !important; /* Ensure panel is a stacking context above page content. */
z-index: 1;
} }
/* Paystack draws its own modal chrome inside the iframe — fill the mount. */ html.ladill-pay-inline-active [data-ladill-pay-sheet] {
[data-ladill-pay-mount] iframe[data-ladill-pay-contained], z-index: 9999 !important;
[data-ladill-pay-mount] iframe[id^="inline-checkout"], }
[data-ladill-pay-mount] iframe[id^="embed-checkout"] {
/* Preferred: iframe lives inside the mount and fills it. */
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; position: absolute !important;
left: 0 !important; left: 0 !important;
top: 0 !important; top: 0 !important;
@@ -710,26 +1033,43 @@
z-index: 2 !important; z-index: 2 !important;
visibility: visible !important; visibility: visible !important;
display: block !important; display: block !important;
background: transparent !important; opacity: 1 !important;
background: #fff !important;
background-color: #fff !important;
color-scheme: light !important; color-scheme: light !important;
pointer-events: auto !important;
transform: none !important;
} }
/* If a Paystack frame escapes the mount, pin it over the mount box. */
/*
Fallback when the frame stays on body: pin with measured pixels.
Default 0×0 until JS measures the sheet body (never flash full viewport).
*/
html.ladill-pay-inline-active body > iframe[id^="inline-checkout"], 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 body > iframe[id^="embed-checkout"],
html.ladill-pay-inline-active iframe[data-ladill-pay-mode="pin"] {
position: fixed !important; position: fixed !important;
left: var(--ladill-pay-left, 0) !important; left: var(--ladill-pay-left, 0px) !important;
top: var(--ladill-pay-top, 0) !important; top: var(--ladill-pay-top, 0px) !important;
width: var(--ladill-pay-width, 100%) !important; right: auto !important;
height: var(--ladill-pay-height, 100%) !important; bottom: auto !important;
width: var(--ladill-pay-width, 0px) !important;
height: var(--ladill-pay-height, 0px) !important;
max-width: none !important; max-width: none !important;
max-height: none !important; max-height: none !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important;
border: 0 !important; border: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
z-index: 10000 !important; z-index: 10050 !important;
visibility: visible !important; visibility: visible !important;
display: block !important; display: block !important;
background: transparent !important; opacity: 1 !important;
background: #fff !important;
background-color: #fff !important;
color-scheme: light !important;
pointer-events: auto !important;
transform: none !important;
} }
</style> </style>
@endonce @endonce
@@ -779,10 +1119,10 @@
</div> </div>
{{-- {{--
ONE design everywhere (POS / Merchant / Mini / Give / Invoice / Events): ONE surface everywhere (POS / Merchant / Mini / Give / Invoice / Events):
- Loading / error / same-origin: white bottomsheet (mobile) or modal (desktop) - Mobile: bottom sheet
- Paystack Inline ready: transparent shell so Paystack's own card is the only surface - Desktop: centered modal
on the dimmed backdrop (no second white frame / double modal) - Paystack Inline fills the sheet body loading area (not a second full-screen modal)
--}} --}}
<div data-ladill-pay-panel <div data-ladill-pay-panel
x-show="showSheet" x-show="showSheet"
@@ -792,30 +1132,15 @@
x-transition:leave="transition duration-200 ease-in md:duration-150" 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-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" 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" class="relative flex w-full flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl md:rounded-2xl"
:data-paystack-live="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable) ? '1' : '0'" :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) :class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable)
? 'max-w-none rounded-none bg-transparent shadow-none md:w-[min(100vw,720px)]' ? 'md:max-w-3xl'
: 'rounded-t-2xl bg-white shadow-2xl md:max-w-lg md:rounded-2xl'" : 'md:max-w-lg'"
:style="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable) style="height: min(90dvh, 720px); max-height: 90dvh; padding-bottom: env(safe-area-inset-bottom, 0px)"
? 'height: min(90dvh, 640px); max-height: 90dvh; width: min(100vw, 720px); padding-bottom: 0;'
: 'height: min(90dvh, 720px); max-height: 90dvh; padding-bottom: env(safe-area-inset-bottom, 0px);'"
@click.stop> @click.stop>
{{-- Floating close when Paystack owns the surface (Paystack has no host close for us) --}} {{-- Full header while loading / error / same-origin; compact bar once Paystack fills the body --}}
<button type="button"
x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable"
@click="showSheet = false; window.LadillPayCheckout?.cancel?.()"
class="absolute right-3 top-3 z-30 flex h-9 w-9 items-center justify-center rounded-full bg-white/95 text-slate-600 shadow-md ring-1 ring-slate-200/80 backdrop-blur hover:bg-white hover:text-slate-900 md:right-0 md:-mr-12 md:top-0 md:bg-white"
style="top: max(0.75rem, env(safe-area-inset-top, 0px))"
data-ladill-pay-close-live
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>
{{-- Standard POS header: loading, error, or same-origin waiting page --}}
<div x-show="!$store.ladillPayCheckout <div x-show="!$store.ladillPayCheckout
|| $store.ladillPayCheckout.frameable || $store.ladillPayCheckout.frameable
|| $store.ladillPayCheckout.launching || $store.ladillPayCheckout.launching
@@ -845,26 +1170,42 @@
</div> </div>
</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 border-b border-slate-100 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 when ready --}} {{-- Body: spinner while starting; Paystack fills this when ready --}}
<div class="relative min-h-0 flex-1 overflow-hidden overscroll-contain" <div class="relative min-h-0 flex-1 overflow-hidden overscroll-contain bg-white"
data-ladill-pay-body data-ladill-pay-body
:class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable) style="min-height: 22rem">
? 'bg-transparent'
: 'bg-white'"
style="min-height: 20rem">
<iframe x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.frameable" <iframe x-show="$store.ladillPayCheckout && $store.ladillPayCheckout.frameable"
:src="showSheet && $store.ladillPayCheckout && $store.ladillPayCheckout.frameable ? checkoutUrl : ''" :src="showSheet && $store.ladillPayCheckout && $store.ladillPayCheckout.frameable ? checkoutUrl : ''"
class="absolute inset-0 h-full w-full border-0" class="absolute inset-0 h-full w-full border-0"
allow="payment *; fullscreen *; clipboard-read *; clipboard-write *" allow="payment *; fullscreen *; clipboard-read *; clipboard-write *"
title="{{ $iframeTitle }}"></iframe> title="{{ $iframeTitle }}"></iframe>
{{-- Pin target for Paystack: keep in layout while inline checkout is active. --}}
<div x-show="!$store.ladillPayCheckout || !$store.ladillPayCheckout.frameable" <div x-show="!$store.ladillPayCheckout || !$store.ladillPayCheckout.frameable"
data-ladill-pay-mount data-ladill-pay-mount
id="ladill-paystack-mount" class="absolute inset-0 bg-white"
class="absolute inset-0" style="min-height: 12rem"
:class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error)
? 'bg-transparent'
: 'bg-white'"
aria-hidden="true"></div> aria-hidden="true"></div>
<div x-show="(!$store.ladillPayCheckout || !$store.ladillPayCheckout.frameable) <div x-show="(!$store.ladillPayCheckout || !$store.ladillPayCheckout.frameable)