diff --git a/app/Http/Middleware/RedirectLegacyQrToLadillLink.php b/app/Http/Middleware/RedirectLegacyQrToLadillLink.php index 3e3d799..292a3de 100644 --- a/app/Http/Middleware/RedirectLegacyQrToLadillLink.php +++ b/app/Http/Middleware/RedirectLegacyQrToLadillLink.php @@ -19,6 +19,13 @@ class RedirectLegacyQrToLadillLink return $next($request); } + // Same-origin AJAX from Mini-hosted payment pages (e.g. mini.ladill.com) + // must hit local /q/{code}/pay — redirecting to ladl.link breaks CORS and + // the checkout sheet never opens. + if ($request->ajax() || $request->expectsJson() || $request->wantsJson()) { + return $next($request); + } + return LadillLink::legacyRedirect($request); } } diff --git a/resources/js/app.js b/resources/js/app.js index c39d9f2..b23f0f6 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -217,9 +217,9 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ this.errorMsg = ''; this.loading = true; - - // Open a blank checkout window synchronously so Paystack is not blocked after await. - window.LadillPayCheckout?.prepare?.(); + this.checkoutUrl = ''; + // Show sheet/modal immediately; Paystack opens from the in-sheet Continue CTA. + this.showSheet = true; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 45000); @@ -241,12 +241,12 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ ? data.error : (typeof data.message === 'string' ? data.message : ''); if (!res.ok || apiError) { - window.LadillPayCheckout?.cancel?.(); + this.showSheet = false; this.errorMsg = apiError || (`Could not start payment (${res.status}). Please try again.`); return; } if (!data.checkout_url) { - window.LadillPayCheckout?.cancel?.(); + this.showSheet = false; this.errorMsg = 'Could not start payment. Please try again.'; return; } @@ -256,14 +256,13 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ const useSheet = data.provider !== 'mtn_momo'; if (useSheet) { this.checkoutUrl = data.checkout_url; - this.showSheet = true; } else { - window.LadillPayCheckout?.cancel?.(); + this.showSheet = false; window.location.assign(data.checkout_url); return; } } catch (e) { - window.LadillPayCheckout?.cancel?.(); + this.showSheet = false; this.errorMsg = e?.name === 'AbortError' ? 'Payment is taking too long. Please try again.' : 'Network error. Please try again.'; diff --git a/resources/views/partials/paystack-sheet.blade.php b/resources/views/partials/paystack-sheet.blade.php index 17afe3c..9817a4e 100644 --- a/resources/views/partials/paystack-sheet.blade.php +++ b/resources/views/partials/paystack-sheet.blade.php @@ -10,9 +10,9 @@ 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. + External checkouts stay in this chrome with a Continue CTA that opens Paystack + in a named window (user gesture — not blocked). Auto-popups are intentionally + avoided so the sheet/modal always remains visibly on screen. --}} @php $audience = $audience ?? 'buyer'; @@ -64,17 +64,9 @@ } catch (e) {} } + // Kept for callers; no longer opens a window on Pay click (that stole focus + // from the sheet/modal). Prefer opening from the in-sheet Continue CTA. 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; } @@ -111,31 +103,35 @@ window.Alpine.store('ladillPayCheckout', { frameable: false, popupBlocked: false, - launchKey: '', + ready: false, reset() { this.frameable = false; this.popupBlocked = false; - this.launchKey = ''; + this.ready = false; }, sync(show, url) { if (!show) { this.reset(); return; } - if (!url) return; - this.frameable = isFrameable(url); - if (this.frameable) { - cancel(); + this.ready = !!url; + if (!url) { + this.frameable = false; this.popupBlocked = false; return; } - if (this.launchKey === url) return; - this.launchKey = url; - var popup = openCheckout(url); - this.popupBlocked = !(popup && !popup.closed); + this.frameable = isFrameable(url); + // Never auto-open an external popup here — the sheet/modal must stay + // visible. External checkouts use the Continue CTA (user gesture). + this.popupBlocked = !this.frameable; }, continueTo(url) { if (!url) return; + if (isFrameable(url)) { + this.frameable = true; + this.popupBlocked = false; + return; + } var popup = openCheckout(url); this.popupBlocked = !(popup && !popup.closed); if (this.popupBlocked) { @@ -168,13 +164,15 @@