Files
ladill-mini/resources/views/public/qr/payment-landing.blade.php
T
isaaccladandCursor 5cdc0a046d
Deploy Ladill Mini / deploy (push) Successful in 1m23s
Use Paystack/Ladill Pay on Mini payment landing.
Drop the required MTN MoMo number field and MoMo copy so public payment QRs open Paystack checkout via Ladill Pay like the other restored storefronts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 14:55:14 +00:00

218 lines
12 KiB
PHP

@php
$content = $qrCode->content();
$businessName = $content['business_name'] ?? $qrCode->label;
$currency = $content['currency'] ?? 'GHS';
$payUrl = $qrCode->publicPath('pay');
$csrf = csrf_token();
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="csrf-token" content="{{ $csrf }}">
<title>Pay {{ $businessName }}</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
<style>[x-cloak]{display:none!important}</style>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen bg-slate-50 font-sans text-slate-900 antialiased"
x-data="miniPaymentLanding({
payUrl: @js($payUrl),
csrf: @js($csrf),
amount: @js(old('amount')),
errorMsg: @js(session('error')),
})">
{{-- Mobile: Ladill Mini branding + fixed payment sheet --}}
<div class="md:hidden">
<div class="fixed inset-0 overflow-hidden bg-slate-50">
<div class="flex h-full flex-col items-center justify-center px-6 pb-72 text-center pt-[max(2.5rem,env(safe-area-inset-top))]">
<img src="{{ asset('images/launcher-icons/mini.svg') }}?v={{ @filemtime(public_path('images/launcher-icons/mini.svg')) ?: '1' }}"
alt="Ladill Mini" class="h-14 w-14 object-contain">
<h1 class="mt-4 text-xl font-bold text-slate-900">{{ $businessName }}</h1>
@if(!empty($content['branch_label']))
<p class="mt-1 text-sm text-slate-500">{{ $content['branch_label'] }}</p>
@endif
</div>
</div>
<div class="mini-payment-bottom-bleed md:hidden"
:style="sheetBleedStyle"
aria-hidden="true"></div>
<div class="mini-payment-sheet rounded-t-3xl border-t border-slate-200/80 bg-white px-5 pt-3 shadow-[0_-12px_40px_rgba(15,23,42,0.12)] md:hidden"
:style="paymentSheetStyle">
<div class="mx-auto mb-4 h-1 w-10 rounded-full bg-slate-200"></div>
{{-- Always-visible (no x-cloak) server/client error so failures are never silent --}}
<div class="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700"
x-show="errorMsg"
x-text="errorMsg"
@if(session('error')) style="display:block" @else style="display:none" @endif>{{ session('error') }}</div>
<form method="POST" action="{{ $payUrl }}" @submit.prevent="submitPay()" class="space-y-0">
@csrf
<label for="amount-mobile" class="sr-only">Amount ({{ $currency }})</label>
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4 text-lg font-semibold text-slate-400">{{ $currency }}</span>
<input type="number" id="amount-mobile" name="amount" x-model="amount" step="0.01" min="0.01" required autofocus
inputmode="decimal"
class="w-full rounded-2xl border-slate-200 py-4 pl-16 pr-4 text-3xl font-bold tracking-tight text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
placeholder="0.00" value="{{ old('amount') }}">
</div>
<button type="submit" :disabled="loading"
class="mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-indigo-600 py-4 text-base font-semibold text-white hover:bg-indigo-700 disabled:opacity-60">
<svg x-show="loading" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24" style="display:none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span x-text="loading ? 'Opening payment…' : 'Pay'">Pay</span>
</button>
</form>
<p class="mt-3 text-center text-[11px] text-slate-400">Secured by Paystack. Powered by Ladill Pay</p>
</div>
</div>
{{-- Desktop: centered card --}}
<main class="mx-auto hidden min-h-screen max-w-sm flex-col justify-center px-4 py-8 md:flex">
<div class="rounded-3xl border border-slate-200/80 bg-white p-6 shadow-sm">
<div class="text-center">
@if(!empty($content['logo_path']))
<img src="{{ $qrCode->publicPath('payment-logo') }}" alt="" class="mx-auto h-14 w-14 rounded-2xl object-cover">
@endif
<h1 class="mt-3 text-lg font-bold text-slate-900">{{ $businessName }}</h1>
@if(!empty($content['branch_label']))
<p class="mt-0.5 text-sm text-slate-500">{{ $content['branch_label'] }}</p>
@endif
</div>
<div class="mt-4 rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700"
x-show="errorMsg"
x-text="errorMsg"
@if(session('error')) style="display:block" @else style="display:none" @endif>{{ session('error') }}</div>
<form method="POST" action="{{ $payUrl }}" @submit.prevent="submitPay()" class="mt-6">
@csrf
<label for="amount-desktop" class="sr-only">Amount ({{ $currency }})</label>
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4 text-lg font-semibold text-slate-400">{{ $currency }}</span>
<input type="number" id="amount-desktop" name="amount" x-model="amount" step="0.01" min="0.01" required
inputmode="decimal"
class="w-full rounded-2xl border-slate-200 py-4 pl-16 pr-4 text-3xl font-bold tracking-tight text-slate-900 focus:border-indigo-500 focus:ring-indigo-500"
placeholder="0.00" value="{{ old('amount') }}">
</div>
<button type="submit" :disabled="loading"
class="mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-indigo-600 py-4 text-base font-semibold text-white hover:bg-indigo-700 disabled:opacity-60">
<svg x-show="loading" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24" style="display:none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span x-text="loading ? 'Opening payment…' : 'Pay'">Pay</span>
</button>
</form>
<p class="mt-4 text-center text-[11px] text-slate-400">Secured by Paystack. Powered by Ladill Pay</p>
</div>
</main>
@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.
--}}
<script>
document.addEventListener('alpine:init', () => {
const Alpine = window.Alpine;
if (!Alpine || typeof Alpine.data !== 'function') return;
// Re-register so Pay works even when Blade deploys ahead of an old Vite bundle.
Alpine.data('miniPaymentLanding', (config = {}) => ({
amount: config.amount ?? '',
loading: false,
errorMsg: config.errorMsg ?? '',
showSheet: false,
checkoutUrl: '',
paymentSheetStyle: '',
sheetBleedStyle: '',
init() {
this._syncSheet = () => {
if (window.innerWidth >= 768) {
this.paymentSheetStyle = '';
this.sheetBleedStyle = '';
return;
}
const vp = window.visualViewport;
const offset = vp ? Math.max(0, Math.round(window.innerHeight - vp.height - vp.offsetTop)) : 0;
const safePad = offset > 0 ? '1.25rem' : 'max(1.25rem, env(safe-area-inset-bottom))';
this.paymentSheetStyle = `bottom: ${offset}px; padding-bottom: ${safePad};`;
this.sheetBleedStyle = `bottom: ${offset}px;`;
};
this._onViewportChange = () => this._syncSheet();
window.visualViewport?.addEventListener('resize', this._onViewportChange);
window.visualViewport?.addEventListener('scroll', this._onViewportChange);
if (window.innerWidth < 768) document.documentElement.classList.add('mini-payment-page');
this._syncSheet();
},
async submitPay() {
const value = parseFloat(this.amount);
if (!value || value <= 0) {
this.errorMsg = 'Enter an amount greater than zero.';
return;
}
this.errorMsg = '';
this.loading = true;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 45000);
try {
const res = await fetch(config.payUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': config.csrf,
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ amount: value }),
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 (' + res.status + '). Please try again.');
return;
}
if (!data.checkout_url) {
this.errorMsg = 'Could not start payment. Please try again.';
return;
}
// Legacy MoMo waiting page must be full-page — never trap it in the Paystack iframe.
const useSheet = data.provider !== 'mtn_momo' && window.innerWidth < 768;
if (useSheet) {
this.checkoutUrl = data.checkout_url;
this.showSheet = true;
} else {
window.location.assign(data.checkout_url);
return;
}
} catch (e) {
this.errorMsg = e?.name === 'AbortError'
? 'Payment is taking too long. Please try again.'
: 'Network error. Please try again.';
} finally {
clearTimeout(timer);
this.loading = false;
}
},
}));
});
</script>
</body>
</html>