diff --git a/app/Http/Controllers/Public/BookingController.php b/app/Http/Controllers/Public/BookingController.php index 0cba266..9668052 100644 --- a/app/Http/Controllers/Public/BookingController.php +++ b/app/Http/Controllers/Public/BookingController.php @@ -68,20 +68,26 @@ class BookingController extends Controller ]); } - public function callback(Request $request, string $shortCode): RedirectResponse + public function callback(Request $request, string $shortCode): View { $reference = trim((string) $request->query('reference', '')); if ($reference === '') { - return redirect()->away(LadillLink::url($shortCode))->with('error', 'Missing payment reference.'); + return view('public.payment-return', [ + 'redirect' => LadillLink::url($shortCode), + ]); } try { $booking = $this->sales->completeBooking($reference); } catch (\Throwable) { - return redirect()->away(LadillLink::url($shortCode))->with('order_error', 'Payment could not be verified. Reference: '.$reference); + return view('public.payment-return', [ + 'redirect' => LadillLink::url($shortCode), + ]); } - return redirect()->away(LadillLink::path($shortCode, 'booking/confirmed/'.$booking->id)); + return view('public.payment-return', [ + 'redirect' => LadillLink::path($shortCode, 'booking/confirmed/'.$booking->id), + ]); } public function confirmed(string $shortCode, int $booking): View diff --git a/app/Http/Controllers/Public/SaleOrderController.php b/app/Http/Controllers/Public/SaleOrderController.php index 67c812c..bf99117 100644 --- a/app/Http/Controllers/Public/SaleOrderController.php +++ b/app/Http/Controllers/Public/SaleOrderController.php @@ -4,11 +4,11 @@ namespace App\Http\Controllers\Public; use App\Http\Controllers\Controller; use App\Models\QrCode; +use App\Models\QrSaleOrder; use App\Services\Merchant\MerchantSaleService; use App\Support\LadillLink; use App\Support\Qr\QrTypeCatalog; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use RuntimeException; @@ -56,19 +56,40 @@ class SaleOrderController extends Controller ]); } - public function callback(Request $request, string $shortCode): RedirectResponse|View + public function callback(Request $request, string $shortCode): View { $reference = trim((string) $request->query('reference', '')); if ($reference === '') { - return redirect()->away(LadillLink::url($shortCode))->with('error', 'Missing payment reference.'); + return view('public.payment-return', [ + 'redirect' => LadillLink::url($shortCode), + ]); } try { $order = $this->sales->completeOrder($reference); } catch (\Throwable) { - return redirect()->away(LadillLink::url($shortCode))->with('order_error', 'Payment could not be verified. Contact the merchant with reference: '.$reference); + return view('public.payment-return', [ + 'redirect' => LadillLink::url($shortCode), + ]); } + return view('public.payment-return', [ + 'redirect' => route('qr.public.order.confirmed', [ + 'shortCode' => $shortCode, + 'reference' => $order->payment_reference, + ], absolute: true), + ]); + } + + public function confirmed(string $shortCode, string $reference): View + { + $order = QrSaleOrder::query() + ->with('qrCode') + ->where('payment_reference', $reference) + ->where('status', QrSaleOrder::STATUS_PAID) + ->whereHas('qrCode', fn ($q) => $q->where('short_code', $shortCode)) + ->firstOrFail(); + return view('public.qr.order-success', [ 'order' => $order, 'qrCode' => $order->qrCode, diff --git a/resources/js/app.js b/resources/js/app.js index 60b7507..bb6c591 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -239,13 +239,8 @@ Alpine.data('miniPaymentLanding', (config = {}) => ({ this.loading = false; return; } - if (window.innerWidth < 768) { - this.checkoutUrl = data.checkout_url; + this.checkoutUrl = data.checkout_url; this.showSheet = true; - this.loading = false; - } else { - window.location.href = data.checkout_url; - } } catch (e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; diff --git a/resources/views/components/user/service-topup-modal.blade.php b/resources/views/components/user/service-topup-modal.blade.php index f9a5675..acdbd94 100644 --- a/resources/views/components/user/service-topup-modal.blade.php +++ b/resources/views/components/user/service-topup-modal.blade.php @@ -36,7 +36,7 @@ loading: false, errorMsg: '', async submitTopup(event) { - if (this.method !== 'paystack' || window.innerWidth >= 768) return; + if (this.method !== 'paystack') return; event.preventDefault(); this.loading = true; diff --git a/resources/views/partials/paystack-sheet.blade.php b/resources/views/partials/paystack-sheet.blade.php index ce2f127..9d0c4a4 100644 --- a/resources/views/partials/paystack-sheet.blade.php +++ b/resources/views/partials/paystack-sheet.blade.php @@ -1,44 +1,147 @@ {{-- - Paystack mobile bottom-sheet iframe. - Requires Alpine.js ancestor with: showSheet (bool), checkoutUrl (string). - On mobile (< md) the sheet slides up; on desktop nothing renders. + Responsive payment checkout shell: bottom sheet on mobile, centered modal on desktop. + + Audiences: + - buyer (default): public ticket buyers, donors, shoppers — self-serve checkout + - 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). + + Loads the provider authorization URL in-frame (verified via production Events/POS). + MoMo and other non-frameable providers should redirect instead of opening this sheet. --}} +@php + $audience = $audience ?? 'buyer'; + $defaults = match ($audience) { + 'operator' => [ + 'title' => 'Complete payment', + 'subtitle' => null, + 'aria' => 'Complete payment', + 'iframe' => 'Payment checkout', + 'footer' => null, + ], + default => [ + 'title' => 'Complete your payment', + 'subtitle' => 'Pay securely. You\'ll get a confirmation when it succeeds.', + 'aria' => 'Complete your payment', + 'iframe' => 'Secure payment checkout', + 'footer' => 'Your payment is processed securely. Do not close this window until finished.', + ], + }; + $sheetTitle = $sheetTitle ?? $defaults['title']; + $sheetSubtitle = $sheetSubtitle ?? $defaults['subtitle']; + $sheetAria = $sheetAria ?? $defaults['aria']; + $iframeTitle = $iframeTitle ?? $defaults['iframe']; + $sheetFooter = $sheetFooter ?? $defaults['footer']; +@endphp diff --git a/resources/views/public/payment-return.blade.php b/resources/views/public/payment-return.blade.php new file mode 100644 index 0000000..82ed789 --- /dev/null +++ b/resources/views/public/payment-return.blade.php @@ -0,0 +1,27 @@ + + + + + + Payment + + + +

Confirming your payment…

+ + + diff --git a/resources/views/public/qr/book-landing.blade.php b/resources/views/public/qr/book-landing.blade.php index fe773d9..ba9bcb0 100644 --- a/resources/views/public/qr/book-landing.blade.php +++ b/resources/views/public/qr/book-landing.blade.php @@ -45,14 +45,10 @@ }); const data = await res.json(); if (data.error) { this.errorMsg = data.error; this.loading = false; return; } - if (window.innerWidth < 768) { - this.showModal = false; - this.checkoutUrl = data.checkout_url; - this.showSheet = true; - this.loading = false; - } else { - window.location.href = data.checkout_url; - } + this.showModal = false; + this.checkoutUrl = data.checkout_url; + this.showSheet = true; + this.loading = false; } catch(e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; diff --git a/resources/views/public/qr/booking-landing.blade.php b/resources/views/public/qr/booking-landing.blade.php index da3eb65..b5a9aef 100644 --- a/resources/views/public/qr/booking-landing.blade.php +++ b/resources/views/public/qr/booking-landing.blade.php @@ -75,13 +75,9 @@ window.location.href = data.redirect_url; return; } - if (window.innerWidth < 768) { - this.checkoutUrl = data.checkout_url; - this.showSheet = true; - this.loading = false; - } else { - window.location.href = data.checkout_url; - } + this.checkoutUrl = data.checkout_url; + this.showSheet = true; + this.loading = false; } catch (e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; diff --git a/resources/views/public/qr/landing.blade.php b/resources/views/public/qr/landing.blade.php index 493ae11..bb7322c 100644 --- a/resources/views/public/qr/landing.blade.php +++ b/resources/views/public/qr/landing.blade.php @@ -357,13 +357,8 @@ }); const data = await res.json(); if (data.error) { this.errorMsg = data.error; this.loading = false; return; } - if (window.innerWidth < 768) { - this.checkoutUrl = data.checkout_url; - this.showSheet = true; - this.loading = false; - } else { - window.location.href = data.checkout_url; - } + this.checkoutUrl = data.checkout_url; + this.showSheet = true; } catch(e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; @@ -667,8 +662,8 @@ const data = await res.json(); if (data.error) { this.errorMsg = data.error; this.loading = false; return; } if (!data.paid) { window.location.href = data.success_url; return; } - if (window.innerWidth < 768) { this.checkoutUrl = data.checkout_url; this.showSheet = true; this.loading = false; } - else { window.location.href = data.checkout_url; } + this.checkoutUrl = data.checkout_url; + this.showSheet = true; } catch(e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; } } }" class="min-h-screen bg-slate-50 pb-12"> @@ -1198,13 +1193,8 @@ }); const data = await res.json(); if (data.error) { this.errorMsg = data.error; this.loading = false; return; } - if (window.innerWidth < 768) { - this.checkoutUrl = data.checkout_url; - this.showSheet = true; - this.loading = false; - } else { - window.location.href = data.checkout_url; - } + this.checkoutUrl = data.checkout_url; + this.showSheet = true; } catch(e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; diff --git a/resources/views/public/qr/storefront.blade.php b/resources/views/public/qr/storefront.blade.php index 9cab98f..2d043bd 100644 --- a/resources/views/public/qr/storefront.blade.php +++ b/resources/views/public/qr/storefront.blade.php @@ -26,7 +26,7 @@ - +
+ @include('partials.paystack-sheet') {{-- Cover + header --}}
@@ -161,6 +162,8 @@ function storefront(cfg) { open: false, loading: false, errorMsg: '', + showSheet: false, + checkoutUrl: '', name: '', email: '', phone: '', cart: {}, add(name, price, key) { @@ -191,7 +194,11 @@ function storefront(cfg) { }); const data = await res.json(); if (!res.ok) { this.errorMsg = data.error || 'Could not start checkout.'; this.loading = false; return; } - window.location = data.checkout_url; + if (!data.checkout_url) { this.errorMsg = 'Could not start checkout.'; this.loading = false; return; } + this.open = false; + this.checkoutUrl = data.checkout_url; + this.showSheet = true; + this.loading = false; } catch (e) { this.errorMsg = 'Network error. Please try again.'; this.loading = false; diff --git a/routes/web.php b/routes/web.php index 8c8c298..637d050 100644 --- a/routes/web.php +++ b/routes/web.php @@ -53,6 +53,7 @@ Route::get('/q/{shortCode}/booking/callback', [BookingController::class, 'callba Route::get('/q/{shortCode}/booking/confirmed/{booking}', [BookingController::class, 'confirmed'])->name('qr.public.booking.confirmed'); Route::post('/q/{shortCode}/order', [SaleOrderController::class, 'store'])->name('qr.public.order'); Route::get('/q/{shortCode}/order/callback', [SaleOrderController::class, 'callback'])->name('qr.public.order.callback'); +Route::get('/q/{shortCode}/order/confirmed/{reference}', [SaleOrderController::class, 'confirmed'])->name('qr.public.order.confirmed'); }); Route::middleware(['auth', 'platform.session'])->group(function () {