diff --git a/config/ladill_launcher.php b/config/ladill_launcher.php
index 3183cd4..2a0d785 100644
--- a/config/ladill_launcher.php
+++ b/config/ladill_launcher.php
@@ -32,7 +32,6 @@ return [
['name' => 'Link', 'url' => 'https://link.'.$root.'/sso/connect?redirect='.urlencode('https://link.'.$root.'/dashboard'), 'icon' => 'link.svg'],
['name' => 'Frontdesk', 'url' => 'https://frontdesk.'.$root.'/sso/connect?redirect='.urlencode('https://frontdesk.'.$root.'/dashboard'), 'icon' => 'frontdesk.svg'],
['name' => 'Care', 'url' => 'https://care.'.$root.'/sso/connect?redirect='.urlencode('https://care.'.$root.'/dashboard'), 'icon' => 'care.svg'],
- ['name' => 'One', 'url' => 'https://one.'.$root.'/sso/connect?redirect='.urlencode('https://one.'.$root.'/dashboard'), 'icon' => 'one.svg'],
['name' => 'Queue', 'url' => 'https://queue.'.$root.'/sso/connect?redirect='.urlencode('https://queue.'.$root.'/dashboard'), 'icon' => 'queue.svg'],
['name' => 'SMS', 'url' => 'https://sms.'.$root, 'icon' => 'sms.svg'],
['name' => 'Bird', 'url' => 'https://bird.'.$root, 'icon' => 'bird.svg'],
diff --git a/resources/views/components/user/service-topup-modal.blade.php b/resources/views/components/user/service-topup-modal.blade.php
index d1aa9fe..36a867a 100644
--- a/resources/views/components/user/service-topup-modal.blade.php
+++ b/resources/views/components/user/service-topup-modal.blade.php
@@ -108,8 +108,7 @@
}
this.checkoutUrl = data.checkout_url;
- this.showSheet = true;
- this.loading = false;
+ this.showSheet = true; // loading cleared when payment UI opens (ladill-pay-opened)
} catch (error) {
window.LadillPayCheckout?.cancel?.(); this.errorMsg = 'Network error. Please try again.';
this.loading = false;
diff --git a/resources/views/partials/paystack-sheet.blade.php b/resources/views/partials/paystack-sheet.blade.php
index 558f236..51f3e62 100644
--- a/resources/views/partials/paystack-sheet.blade.php
+++ b/resources/views/partials/paystack-sheet.blade.php
@@ -1,5 +1,5 @@
{{--
- Responsive payment checkout shell: bottom sheet on mobile, centered modal on desktop.
+ Payment checkout shell.
Audiences:
- buyer (default): public ticket buyers, donors, shoppers — self-serve checkout
@@ -10,12 +10,15 @@
- 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
+ - publicKey (string, optional)
- 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.
+
+ Paystack Inline owns its secure payment popup. We do not place a Ladill
+ loading sheet in front of it: that created a needless first screen before
+ the checkout appeared. The Ladill bottomsheet/modal is reserved for
+ same-origin waiting pages (such as MoMo) and recoverable errors.
--}}
@php
$audience = $audience ?? 'buyer';
@@ -117,15 +120,6 @@
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 {
@@ -145,377 +139,31 @@
}
} 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;
}
+ // Fullscreen permission for Paystack's own iframes (do not reparent them).
+ // Only write attributes when they actually change — writing always re-fires
+ // MutationObservers and freezes the page (wallet include path).
function applyIframePermissions(iframe) {
if (!iframe || !iframe.setAttribute) return;
try {
- var next = mergeIframeAllow((iframe.getAttribute('allow') || '').trim());
- if ((iframe.getAttribute('allow') || '') !== next) {
+ var cur = (iframe.getAttribute('allow') || '').trim();
+ var need = ['payment *', 'fullscreen *', 'clipboard-read *', 'clipboard-write *', 'publickey-credentials-get *'];
+ var parts = cur ? cur.split(';').map(function (p) { return p.trim(); }).filter(Boolean) : [];
+ var lower = parts.map(function (p) { return p.toLowerCase(); });
+ need.forEach(function (perm) {
+ var base = perm.split(' ')[0].toLowerCase();
+ if (!lower.some(function (p) { return p === base || p.indexOf(base + ' ') === 0; })) {
+ parts.push(perm);
+ }
+ });
+ var next = parts.join('; ');
+ if (cur !== 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) {}
}
@@ -525,116 +173,16 @@
var nodes = scope.querySelectorAll ? scope.querySelectorAll('iframe') : [];
for (var i = 0; i < nodes.length; i++) {
var iframe = nodes[i];
- if (isPaystackIframe(iframe) || isCheckoutFrame(iframe)) {
+ var id = (iframe.id || '').toLowerCase();
+ var src = '';
+ try { src = (iframe.getAttribute('src') || '').toLowerCase(); } catch (e) {}
+ if (id.indexOf('inline-') === 0 || id.indexOf('embed-') === 0 || src.indexOf('paystack') !== -1) {
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;
@@ -652,134 +200,48 @@
} 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) {}
- }
-
+ /**
+ * Arm iframe permission helpers only while a payment is starting.
+ * Do not install on every wallet/page load — observing style/class + always
+ * rewriting allow caused an infinite MutationObserver loop (page unresponsive).
+ */
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;
- }
+ // childList only — never re-enter on attribute writes.
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') {
+ if (node.tagName === '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 emitPayEvent(name, detail) {
+ try {
+ window.dispatchEvent(new CustomEvent(name, { detail: detail || {} }));
+ } catch (e) {}
+ }
function createStoreDefinition() {
return {
@@ -790,6 +252,10 @@
error: '',
_launchToken: 0,
_activeCode: '',
+ _openedEmitted: false,
+ shellVisible() {
+ return !!(this.frameable || this.error);
+ },
reset() {
this.frameable = false;
this.inline = false;
@@ -797,8 +263,22 @@
this.launching = false;
this.error = '';
this._activeCode = '';
+ this._openedEmitted = false;
cancelInline();
},
+ markOpened() {
+ if (this._openedEmitted) return;
+ this._openedEmitted = true;
+ this.launching = false;
+ emitPayEvent('ladill-pay-opened');
+ },
+ markFailed(message) {
+ this.launching = false;
+ this._activeCode = '';
+ this._openedEmitted = false;
+ this.error = message || 'Could not open secure payment.';
+ emitPayEvent('ladill-pay-error', { message: this.error });
+ },
sync(show, url, accessCode, returnUrl) {
url = (url || '').toString();
accessCode = (accessCode || '').toString();
@@ -809,25 +289,35 @@
this.reset();
return;
}
+
this.ready = !!(url || accessCode);
+ // Keep a previous recoverable error only until a new attempt supplies payload.
+ if (url || accessCode) {
+ this.error = '';
+ }
+
if (!url && !accessCode) {
+ // Waiting for parent to set access_code / checkout_url after fetch.
this.frameable = false;
this.inline = false;
- this.launching = true; // waiting for checkout payload
- this.error = '';
+ this.launching = true;
this._activeCode = '';
- setPaystackContainActive(false);
+ this._openedEmitted = false;
return;
}
+
if (isFrameable(url)) {
this.frameable = true;
this.inline = false;
this.launching = false;
- this.error = '';
this._activeCode = '';
+ this._openedEmitted = false;
cancelInline();
+ // Same-origin waiting UI is our shell — it is "open" for busy-state release.
+ emitPayEvent('ladill-pay-opened');
return;
}
+
var code = resolveAccessCode(accessCode, url);
if (code) {
this.frameable = false;
@@ -838,15 +328,14 @@
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.';
+ this.markFailed(
+ 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;
@@ -854,90 +343,84 @@
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 () {
+ this.inline = true;
+ this.frameable = false;
+ this._openedEmitted = false;
+
+ watchPaystackIframes();
+
+ // Ensure Paystack script is fully ready before resumeTransaction so we
+ // do not paint an empty popup shell for a beat (the pre-modal flash).
+ loadInlineJs().then(function (PaystackPop) {
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();
+ if (!PaystackPop) throw new Error('Paystack unavailable');
+
+ cancelInline();
+
+ var popup = new PaystackPop();
+ activePopup = popup;
+ ensurePaystackIframePermissions(document);
+
+ popup.resumeTransaction(accessCode, {
+ onLoad: function () {
+ if (token !== store._launchToken) return;
+ store.markOpened();
+ ensurePaystackIframePermissions(document);
+ },
+ onSuccess: function (transaction) {
+ if (token !== store._launchToken) return;
+ store.launching = false;
+ activePopup = null;
+ var reference = (transaction && (transaction.reference || transaction.trxref)) || '';
+ var dest = buildReturnUrl(returnUrl, reference);
+ if (dest) {
+ window.location.assign(dest);
+ return;
}
- } 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.';
+ window.location.reload();
+ },
+ onCancel: function () {
+ if (token !== store._launchToken) return;
+ store.launching = false;
+ store._activeCode = '';
+ store._openedEmitted = false;
+ activePopup = null;
+ emitPayEvent('ladill-pay-cancelled');
+ },
+ onError: function (err) {
+ if (token !== store._launchToken) return;
+ activePopup = null;
+ store.markFailed((err && err.message) ? String(err.message) : 'Could not open secure payment.');
+ },
});
- };
- if (typeof requestAnimationFrame === 'function') {
- requestAnimationFrame(function () { requestAnimationFrame(start); });
- } else {
- setTimeout(start, 32);
- }
+
+ // Safety: if onLoad is slow, treat a live checkout iframe as open.
+ var polls = 0;
+ var pollId = setInterval(function () {
+ if (token !== store._launchToken) {
+ clearInterval(pollId);
+ return;
+ }
+ ensurePaystackIframePermissions(document);
+ var live = document.querySelector(
+ 'iframe[id^="inline-checkout"], iframe[id^="embed-checkout"]'
+ );
+ if (live) {
+ store.markOpened();
+ clearInterval(pollId);
+ }
+ polls += 1;
+ if (polls >= 50) {
+ clearInterval(pollId);
+ if (store.launching && token === store._launchToken) {
+ store.markFailed('Payment UI did not open. Please try again.');
+ }
+ }
+ }, 100);
+ }).catch(function (err) {
+ if (token !== store._launchToken) return;
+ store.markFailed((err && err.message) ? String(err.message) : 'Could not load payment script.');
+ });
},
};
}
@@ -955,20 +438,24 @@
document.addEventListener('alpine:init', function () {
ensureStore();
});
-
- // If Alpine already started (late include), register immediately.
if (window.Alpine && window.Alpine.version) {
ensureStore();
}
- watchPaystackIframes();
+ // Do not watch iframes or preload Paystack on every page that includes this
+ // partial (e.g. wallet). That froze the tab. prepare()/launchInline arm it.
window.LadillPayCheckout = {
isFrameable: isFrameable,
isPaystackCheckoutUrl: isPaystackCheckoutUrl,
accessCodeFromUrl: accessCodeFromUrl,
resolveAccessCode: resolveAccessCode,
- prepare: function () { loadInlineJs().catch(function () {}); return null; },
+ prepare: function () {
+ // Warm script + hooks only when user is about to pay.
+ installIframeAllowHook();
+ loadInlineJs().catch(function () {});
+ return null;
+ },
cancel: function () {
cancelInline();
var store = ensureStore();
@@ -980,131 +467,25 @@
})();
@endonce
-{{-- Keep Paystack checkout flush inside the Ladill sheet loading area (no double card). --}}
-@once('ladill-paystack-contain-css')
-
-@endonce
-{{-- No local x-data: inherit showSheet/checkoutUrl/accessCode from the including scope. --}}
+{{-- Inherit showSheet / checkoutUrl / accessCode / returnUrl from parent Alpine scope. --}}
-
-
-
{{--
- 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)
+ Ladill bottomsheet (mobile) / modal (desktop) — ONLY for:
+ - same-origin waiting pages (MoMo etc.)
+ - recoverable errors
+ Paystack Inline opens its own secure payment popup; this shell stays
+ hidden during launch so nothing flashes in front of it.
--}}
-
+
+
+
- {{-- Full header while loading / error / same-origin; compact bar once Paystack fills the body --}}
-
-
-
-
-
{{ $sheetTitle }}
- @if (! empty($sheetSubtitle))
-
{{ $sheetSubtitle }}
- @endif
+
+
+
+
+
+
+
{{ $sheetTitle }}
+ @if (! empty($sheetSubtitle))
+
{{ $sheetSubtitle }}
+ @endif
+
+
-
-
+ {{-- Markers kept for tests / callers that query live close chrome. --}}
+
+
-
-
-
{{ $sheetTitle }}
-
-
+
+
- {{-- Body: spinner while starting; Paystack fills this flush when ready (no second card stage) --}}
-
-
-
- {{-- Loading-area mount: Paystack fills this edge-to-edge when live. --}}
-
-
-