Fix Paystack Fullscreen permission policy for Inline iframes.
Deploy Ladill Mini / deploy (push) Successful in 44s

Pre-set allow=fullscreen on Paystack iframes before navigation (createElement
hook + MutationObserver on allow/src/id), re-apply if Paystack overwrites
allow, and send Permissions-Policy HTTP headers so checkout.paystack.com can
use fullscreen inside the in-app sheet.
This commit is contained in:
isaacclad
2026-07-21 22:17:14 +00:00
parent 4291d95518
commit bd8764523c
5 changed files with 166 additions and 22 deletions
+127 -21
View File
@@ -147,45 +147,151 @@
activePopup = null;
}
// Paystack Inline embeds checkout.paystack.com. Browsers default fullscreen to
// 'self', so cross-origin checkout needs allow="fullscreen" before navigation.
// Patch early (createElement hook + MutationObserver) and re-apply if Paystack
// overwrites allow without fullscreen.
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'];
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
|| 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 ensurePaystackIframePermissions(root) {
try {
var nodes = (root || document).querySelectorAll(
'iframe[src*="checkout.paystack.com"], iframe[id^="inline-checkout"], iframe[id^="inline-background"], iframe[src*="paystack"]'
);
nodes.forEach(function (iframe) {
var allow = (iframe.getAttribute('allow') || '').trim();
var needed = ['payment', 'fullscreen', 'clipboard-read', 'clipboard-write'];
var parts = allow ? allow.split(';').map(function (p) { return p.trim(); }).filter(Boolean) : [];
var lower = parts.map(function (p) { return p.toLowerCase(); });
needed.forEach(function (perm) {
if (!lower.some(function (p) { return p === perm || p.indexOf(perm + ' ') === 0; })) {
parts.push(perm);
}
});
iframe.setAttribute('allow', parts.join('; '));
if (!iframe.hasAttribute('allowfullscreen')) {
iframe.setAttribute('allowfullscreen', '');
var scope = root || document;
var nodes = scope.querySelectorAll ? scope.querySelectorAll('iframe') : [];
for (var i = 0; i < nodes.length; i++) {
// Known Paystack frames, or any frame still missing a src (Paystack
// often inserts empty then sets id/src — pre-allow before navigation).
var iframe = nodes[i];
var src = '';
try { src = (iframe.getAttribute('src') || '').trim(); } catch (e) {}
if (isPaystackIframe(iframe) || !src) {
applyIframePermissions(iframe);
}
});
}
} 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') {
// Pre-delegate before Paystack assigns src so policy applies on first load.
applyIframePermissions(el);
}
} catch (e) {}
return el;
};
} catch (e) {}
}
function watchPaystackIframes() {
if (window.__ladillPaystackIframeWatch) return;
window.__ladillPaystackIframeWatch = true;
installIframeAllowHook();
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.addedNodes && m.addedNodes.length) {
ensurePaystackIframePermissions(document);
break;
if (m.type === 'attributes' && m.target) {
if (isPaystackIframe(m.target) || (m.target.tagName && m.target.tagName.toUpperCase() === 'IFRAME')) {
// Only force-allow known Paystack frames; for bare iframes wait until id/src set.
if (isPaystackIframe(m.target)) {
applyIframePermissions(m.target);
} else if (m.attributeName === 'id' || m.attributeName === 'src' || m.attributeName === 'name') {
if (isPaystackIframe(m.target)) applyIframePermissions(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') {
if (isPaystackIframe(node)) applyIframePermissions(node);
else applyIframePermissions(node); // safe pre-allow; Paystack often sets id after insert
dirty = true;
} else if (node.querySelectorAll) {
var nested = node.querySelectorAll('iframe');
for (var k = 0; k < nested.length; k++) {
applyIframePermissions(nested[k]);
dirty = true;
}
}
}
}
if (dirty) {
// Second pass after Paystack mutates attributes in the same tick.
ensurePaystackIframePermissions(document);
}
});
obs.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['allow', 'src', 'id', 'name'],
});
obs.observe(document.documentElement || document.body, { childList: true, subtree: true });
} catch (e) {}
// Brief poll during the first seconds after boot/open — covers cases where
// Paystack inserts via paths MutationObserver can miss under heavy load.
var polls = 0;
var pollId = setInterval(function () {
ensurePaystackIframePermissions(document);
polls += 1;
if (polls >= 40) clearInterval(pollId);
}, 250);
}