diff --git a/app/Services/Pay/PayClient.php b/app/Services/Pay/PayClient.php index 06bd055..5f9591b 100644 --- a/app/Services/Pay/PayClient.php +++ b/app/Services/Pay/PayClient.php @@ -26,7 +26,7 @@ class PayClient /** @param array $payload */ public function createCheckout(array $payload): array { - $res = Http::withToken($this->token())->acceptJson()->timeout(15)->post($this->base().'/checkouts', $payload); + $res = Http::withToken($this->token())->acceptJson()->timeout(45)->post($this->base().'/checkouts', $payload); return $this->jsonOrFail($res, 'Could not start checkout. Please try again.'); } diff --git a/resources/js/app.js b/resources/js/app.js index ad12e36..db90b0c 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -15,6 +15,7 @@ window.QRCodeStyling = QRCodeStyling; import qrcode from 'qrcode-generator'; window.qrcode = qrcode; +window.Alpine = Alpine; Alpine.plugin(collapse); registerLadillClipboard(Alpine); @@ -224,6 +225,9 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ this.errorMsg = ''; this.loading = true; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 45000); + try { const res = await fetch(config.payUrl, { method: 'POST', @@ -234,33 +238,37 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify({ amount: value, customer_phone: phone }), + signal: controller.signal, }); const data = await res.json().catch(() => ({})); const apiError = typeof data.error === 'string' ? data.error : (typeof data.message === 'string' ? data.message : ''); if (!res.ok || apiError) { - this.errorMsg = apiError || 'Could not start payment. Please try again.'; - this.loading = false; + this.errorMsg = apiError || (`Could not start payment (${res.status}). Please try again.`); return; } if (!data.checkout_url) { this.errorMsg = 'Could not start payment. Please try again.'; - this.loading = false; return; } - // MoMo waiting page must be full-page (prompt + status poll), not the Paystack iframe sheet. + // 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' && window.innerWidth < 768; if (useSheet) { this.checkoutUrl = data.checkout_url; this.showSheet = true; - this.loading = false; } else { - window.location.href = data.checkout_url; + window.location.assign(data.checkout_url); + return; } } catch (e) { - this.errorMsg = 'Network error. Please try again.'; + this.errorMsg = e?.name === 'AbortError' + ? 'Payment is taking too long. Please try again.' + : 'Network error. Please try again.'; + } finally { + clearTimeout(timer); this.loading = false; } }, diff --git a/resources/views/public/qr/payment-landing.blade.php b/resources/views/public/qr/payment-landing.blade.php index 4cd640e..f4b7af7 100644 --- a/resources/views/public/qr/payment-landing.blade.php +++ b/resources/views/public/qr/payment-landing.blade.php @@ -14,6 +14,7 @@ Pay {{ $businessName }} + @vite(['resources/css/app.css', 'resources/js/app.js'])
-
+ {{-- Always-visible (no x-cloak) server/client error so failures are never silent --}} +
{{ session('error') }}
- -
- {{ $currency }} - -
+
+ @csrf + +
+ {{ $currency }} + +
- - + + - + +

Pay with MTN MoMo. Powered by Ladill Pay

@@ -91,40 +97,141 @@ @endif -
+
{{ session('error') }}
-
+
+ @csrf
{{ $currency }} - + placeholder="0.00" value="{{ old('amount') }}">
- + placeholder="e.g. 024XXXXXXX" value="{{ old('customer_phone') }}"> - -
+

Pay with MTN MoMo. Powered by Ladill Pay

@include('partials.paystack-sheet') + + {{-- + Safety net: if Alpine/Vite fails to boot, native form POST still works. + If Alpine boots but miniPaymentLanding was missing from an old bundle, redefine it. + --}} +