diff --git a/app/Http/Controllers/Public/PaymentController.php b/app/Http/Controllers/Public/PaymentController.php index d17f22f..20af88e 100644 --- a/app/Http/Controllers/Public/PaymentController.php +++ b/app/Http/Controllers/Public/PaymentController.php @@ -41,7 +41,7 @@ class PaymentController extends Controller return back()->withInput()->with('error', $e->getMessage()); } - if ($request->expectsJson()) { + if ($request->expectsJson() || $request->ajax()) { return response()->json([ 'checkout_url' => $result['checkout_url'], 'access_code' => $result['access_code'] ?? null, @@ -52,7 +52,24 @@ class PaymentController extends Controller ]); } - return redirect()->away($result['checkout_url']); + // Never send the browser to checkout.paystack.com — reopen the landing + // with checkout payload so Paystack Inline can run in-page. + $provider = (string) ($result['provider'] ?? ''); + if ($provider === 'mtn_momo') { + $waiting = (string) ($result['checkout_url'] ?? ''); + if ($waiting !== '' && str_starts_with($waiting, url('/'))) { + return redirect()->to($waiting); + } + } + + return redirect() + ->to(url('/q/'.$shortCode)) + ->with('checkout_url', $result['checkout_url'] ?? null) + ->with('access_code', $result['access_code'] ?? null) + ->with('public_key', $result['public_key'] ?? null) + ->with('callback_url', $result['callback_url'] ?? null) + ->with('provider', $provider !== '' ? $provider : 'paystack') + ->with('amount', $validated['amount']); } public function callback(Request $request, string $shortCode): View diff --git a/app/Http/Middleware/RedirectLegacyQrToLadillLink.php b/app/Http/Middleware/RedirectLegacyQrToLadillLink.php index 292a3de..ddb85d7 100644 --- a/app/Http/Middleware/RedirectLegacyQrToLadillLink.php +++ b/app/Http/Middleware/RedirectLegacyQrToLadillLink.php @@ -19,13 +19,20 @@ 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. + // Same-origin pay from Mini-hosted pages (e.g. mini.ladill.com) must hit + // local /q/{code}/pay — redirecting to ladl.link breaks CORS / form POSTs + // and used to dump the browser onto checkout.paystack.com. if ($request->ajax() || $request->expectsJson() || $request->wantsJson()) { return $next($request); } + // Form POST (and any method) to the Mini pay endpoint stays on Mini so + // PaymentController can return JSON or flash payload for in-page Inline. + $path = ltrim($request->path(), '/'); + if (preg_match('#^q/[^/]+/pay(?:/callback)?$#i', $path) === 1) { + return $next($request); + } + return LadillLink::legacyRedirect($request); } } diff --git a/resources/js/app.js b/resources/js/app.js index e3e8576..208402a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -201,6 +201,17 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ } this._syncSheet(); + + // Non-JS form POST fallback flashes checkout payload — open Inline in-page. + if (config.bootstrapCheckoutUrl || config.bootstrapAccessCode) { + this.openInlineCheckout({ + checkout_url: config.bootstrapCheckoutUrl || '', + access_code: config.bootstrapAccessCode || '', + public_key: config.bootstrapPublicKey || '', + callback_url: config.bootstrapCallbackUrl || '', + provider: config.bootstrapProvider || 'paystack', + }); + } }, destroy() { @@ -211,6 +222,30 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ document.documentElement.classList.remove('mini-payment-page'); }, + isSameOrigin(url) { + if (!url) return false; + try { + return new URL(url, window.location.href).origin === window.location.origin; + } catch (e) { + return false; + } + }, + + openInlineCheckout(data = {}) { + // Never navigate to checkout.paystack.com. Same-origin MoMo waiting can navigate. + if (data.provider === 'mtn_momo' && this.isSameOrigin(data.checkout_url)) { + window.location.assign(data.checkout_url); + return false; + } + + this.checkoutUrl = data.checkout_url || ''; + this.accessCode = data.access_code || ''; + this.publicKey = data.public_key || ''; + this.returnUrl = data.callback_url || ''; + this.showSheet = true; + return true; + }, + async submitPay() { const value = parseFloat(this.amount); if (!value || value <= 0) { @@ -224,8 +259,8 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ this.accessCode = ''; this.publicKey = ''; this.returnUrl = ''; - // Show sheet/modal immediately; Paystack Inline opens in-page from the sheet. - this.showSheet = true; + // Keep sheet closed until we have a checkout payload so Inline syncs once with access_code. + this.showSheet = false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 45000); @@ -257,17 +292,7 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ return; } - // Legacy MoMo waiting page must be full-page — never trap it in the Paystack iframe - // (cross-origin iframe is blank / looks like "Pay did nothing"). - const useSheet = data.provider !== 'mtn_momo'; - if (useSheet) { - this.checkoutUrl = data.checkout_url || ''; - this.accessCode = data.access_code || ''; - this.publicKey = data.public_key || ''; - this.returnUrl = data.callback_url || ''; - } else { - this.showSheet = false; - window.location.assign(data.checkout_url); + if (!this.openInlineCheckout(data)) { return; } } catch (e) { diff --git a/resources/views/partials/paystack-sheet.blade.php b/resources/views/partials/paystack-sheet.blade.php index 4aee647..e5dfd9a 100644 --- a/resources/views/partials/paystack-sheet.blade.php +++ b/resources/views/partials/paystack-sheet.blade.php @@ -187,12 +187,15 @@ this.launchInline(code, returnUrl); return; } - // Non-Paystack external URL without access_code — cannot embed. + // Never navigate to checkout.paystack.com (external tab/window). + // Without access_code we cannot open Inline in-page. this.frameable = false; this.inline = false; this.launching = false; this._activeCode = ''; - this.error = 'Secure checkout could not be opened in-page. Please try again.'; + this.error = 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; @@ -293,118 +296,124 @@ ); } " - @keydown.escape.window="if (showSheet) showSheet = false" + @keydown.escape.window="if (showSheet && $store.ladillPayCheckout && ($store.ladillPayCheckout.frameable || $store.ladillPayCheckout.error || $store.ladillPayCheckout.launching)) showSheet = false" @ladill-pay-cancelled.window="showSheet = false" - class="fixed inset-0 z-[9999] flex items-end justify-center md:items-center md:p-6" + class="fixed inset-0 z-[9999]" + :class="($store.ladillPayCheckout && $store.ladillPayCheckout.inline && !$store.ladillPayCheckout.launching && !$store.ladillPayCheckout.error && !$store.ladillPayCheckout.frameable) ? 'pointer-events-none' : ''" role="dialog" aria-modal="true" :aria-hidden="(!showSheet).toString()" aria-label="{{ $sheetAria }}"> -
Opening payment…
+{{ $sheetTitle }}
- @if (! empty($sheetSubtitle)) -{{ $sheetSubtitle }}
- @endif -