diff --git a/resources/views/partials/paystack-sheet.blade.php b/resources/views/partials/paystack-sheet.blade.php index 17afe3c..4aee647 100644 --- a/resources/views/partials/paystack-sheet.blade.php +++ b/resources/views/partials/paystack-sheet.blade.php @@ -6,13 +6,16 @@ - 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). + 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=) - Paystack (checkout.paystack.com) sends X-Frame-Options: SAMEORIGIN and cannot be - embedded. Same-origin URLs (e.g. MoMo waiting pages) still load in an iframe. - External checkouts open in a named window. Call LadillPayCheckout.prepare() - synchronously in the Pay click handler (before await fetch) so the popup is not - blocked after the async response. An in-sheet Continue CTA remains as fallback. + Paystack 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'; @@ -43,8 +46,9 @@ (function () { if (window.LadillPayCheckout) return; - var PENDING_NAME = 'ladill_pay_checkout'; - var pendingWindow = null; + var INLINE_SRC = 'https://js.paystack.co/v2/inline.js'; + var inlineLoading = null; + var activePopup = null; function isFrameable(url) { if (!url) return false; @@ -55,54 +59,80 @@ } } - function writePendingPlaceholder(win) { - if (!win) return; + function isPaystackCheckoutUrl(url) { + if (!url) return false; try { - win.document.open(); - win.document.write('
Opening secure checkout…
'); - win.document.close(); - } catch (e) {} - } - - function prepare() { - cancel(); - try { - var win = window.open('about:blank', PENDING_NAME, 'width=480,height=720'); - if (win && !win.closed) { - pendingWindow = win; - writePendingPlaceholder(win); - return win; - } - } catch (e) {} - pendingWindow = null; - return null; - } - - function cancel() { - if (pendingWindow && !pendingWindow.closed) { - try { pendingWindow.close(); } catch (e) {} - } - pendingWindow = null; - } - - function openCheckout(url) { - if (!url) return null; - try { - if (pendingWindow && !pendingWindow.closed) { - pendingWindow.location = url; - var win = pendingWindow; - pendingWindow = null; - return win; - } - pendingWindow = null; - // Named window (no noopener) so payment-return can redirect window.opener. - return window.open(url, PENDING_NAME, 'width=480,height=720'); + var host = new URL(url, window.location.href).hostname.toLowerCase(); + return host === 'checkout.paystack.com' || host.endsWith('.paystack.com'); } catch (e) { - pendingWindow = null; - return null; + 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) { + 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; + } + + 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; + } + function ensureStore() { if (!window.Alpine || typeof window.Alpine.store !== 'function') return null; if (window.Alpine.store('ladillPayCheckout')) { @@ -110,37 +140,109 @@ } window.Alpine.store('ladillPayCheckout', { frameable: false, - popupBlocked: false, - launchKey: '', + inline: false, + ready: false, + launching: false, + error: '', + _launchToken: 0, + _activeCode: '', reset() { this.frameable = false; - this.popupBlocked = false; - this.launchKey = ''; + this.inline = false; + this.ready = false; + this.launching = false; + this.error = ''; + this._activeCode = ''; + cancelInline(); }, - sync(show, url) { + sync(show, url, accessCode, returnUrl) { if (!show) { + this._launchToken += 1; this.reset(); return; } - if (!url) return; - this.frameable = isFrameable(url); - if (this.frameable) { - cancel(); - this.popupBlocked = false; + this.ready = !!(url || accessCode); + if (!url && !accessCode) { + this.frameable = false; + this.inline = false; + this.launching = false; + this._activeCode = ''; return; } - if (this.launchKey === url) return; - this.launchKey = url; - var popup = openCheckout(url); - this.popupBlocked = !(popup && !popup.closed); - }, - continueTo(url) { - if (!url) return; - var popup = openCheckout(url); - this.popupBlocked = !(popup && !popup.closed); - if (this.popupBlocked) { - window.location.assign(url); + if (isFrameable(url)) { + this.frameable = true; + this.inline = false; + this.launching = false; + 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; + } + // Non-Paystack external URL without access_code — cannot embed. + this.frameable = false; + this.inline = false; + this.launching = false; + this._activeCode = ''; + this.error = '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 = ''; + loadInlineJs().then(function (PaystackPop) { + if (token !== store._launchToken) return; + if (!PaystackPop) throw new Error('Paystack unavailable'); + cancelInline(); + var popup = new PaystackPop(); + activePopup = popup; + popup.resumeTransaction(accessCode, { + onLoad: function () { + if (token !== store._launchToken) return; + store.launching = false; + }, + onSuccess: function (transaction) { + if (token !== store._launchToken) return; + store.launching = false; + var reference = (transaction && (transaction.reference || transaction.trxref)) || ''; + var dest = buildReturnUrl(returnUrl, reference); + if (dest) { + window.location.assign(dest); + return; + } + // Fallback: reload so server can reconcile via webhook. + window.location.reload(); + }, + onCancel: function () { + if (token !== store._launchToken) return; + store.launching = false; + store._activeCode = ''; + activePopup = null; + window.dispatchEvent(new CustomEvent('ladill-pay-cancelled')); + }, + onError: function (err) { + if (token !== store._launchToken) return; + store.launching = false; + store._activeCode = ''; + store.error = (err && err.message) ? String(err.message) : 'Could not open secure payment.'; + }, + }); + }).catch(function (err) { + if (token !== store._launchToken) return; + store.launching = false; + store._activeCode = ''; + store.error = (err && err.message) ? String(err.message) : 'Could not load payment script.'; + }); }, }); return window.Alpine.store('ladillPayCheckout'); @@ -157,9 +259,16 @@ window.LadillPayCheckout = { isFrameable: isFrameable, - prepare: prepare, - cancel: cancel, - open: openCheckout, + isPaystackCheckoutUrl: isPaystackCheckoutUrl, + accessCodeFromUrl: accessCodeFromUrl, + resolveAccessCode: resolveAccessCode, + prepare: function () { return null; }, + cancel: function () { + cancelInline(); + var store = ensureStore(); + if (store) store.reset(); + }, + open: function () { return null; }, ensureStore: ensureStore, }; })(); @@ -168,16 +277,24 @@{{ $sheetTitle }}
@@ -217,8 +336,9 @@ @endif- A Paystack window should be open. If you do not see it, tap continue below. - Your browser blocked the popup. Tap continue to open Paystack. -
-- {{ $sheetFooter }} -
- @endif -