Implement catalog CRUD overhaul, snapshot fallback activation, and billing/UX hardening

This commit is contained in:
Dwindi Ramadhana
2026-02-17 00:03:35 +07:00
parent e6aef31dd1
commit 2726b6c312
37 changed files with 2936 additions and 204 deletions

View File

@@ -135,6 +135,9 @@
$canQris = $pakasirEnabled ?? false;
$paypalEnabled = $paypalEnabled ?? false;
$paypalPlans = $paypalPlans ?? ['personal_monthly' => false, 'personal_annual' => false];
$hasActiveLifetime = (bool) ($hasActiveLifetime ?? false);
$hasPendingPayment = (bool) ($hasPendingPayment ?? false);
$pendingCooldownRemaining = max(0, (int) ($pendingCooldownRemaining ?? 0));
@endphp
<div class="mb-6 flex flex-wrap items-center justify-center gap-3 text-sm text-gray-400">
@@ -185,15 +188,18 @@
data-paypal-enabled="{{ $paypalEnabled && $paypalPlans['personal_monthly'] ? 'true' : 'false' }}"
data-paypal-annual-enabled="{{ $paypalEnabled && $paypalPlans['personal_annual'] ? 'true' : 'false' }}"
data-qris-enabled="{{ $canQris ? 'true' : 'false' }}"
data-has-lifetime="{{ $hasActiveLifetime ? 'true' : 'false' }}"
data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"
data-pending-cooldown="{{ $pendingCooldownRemaining }}"
class="!text-white w-full py-2.5 rounded-xl bg-brand-ocean hover:bg-brand-oceanSoft text-white font-semibold text-center block">
Pay now
</button>
</div>
<div class="hidden">
<button type="button" data-paypal-plan="personal_monthly" data-original="Start Personal"></button>
<button type="button" data-paypal-plan="personal_annual" data-original="Start Personal"></button>
<button type="button" data-qris-plan="personal_monthly" data-original="Start Personal"></button>
<button type="button" data-qris-plan="personal_annual" data-original="Start Personal"></button>
<button type="button" data-paypal-plan="personal_monthly" data-original="Start Personal" data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"></button>
<button type="button" data-paypal-plan="personal_annual" data-original="Start Personal" data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"></button>
<button type="button" data-qris-plan="personal_monthly" data-original="Start Personal" data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"></button>
<button type="button" data-qris-plan="personal_annual" data-original="Start Personal" data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"></button>
</div>
</section>
@@ -223,14 +229,17 @@
id="lifetime-pay-btn"
data-paypal-enabled="{{ $paypalEnabled ? 'true' : 'false' }}"
data-qris-enabled="{{ $canQris ? 'true' : 'false' }}"
data-has-lifetime="{{ $hasActiveLifetime ? 'true' : 'false' }}"
data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"
data-pending-cooldown="{{ $pendingCooldownRemaining }}"
class="force-white w-full py-2.5 rounded-xl border border-brand-ocean/60 text-brand-ocean font-semibold text-center block hover:bg-brand-ocean/10">
Pay now
</button>
</div>
<div class="hidden">
<button type="button" data-paypal-plan="personal_lifetime" data-original="Get Lifetime Access"></button>
<button type="button" data-paypal-plan="personal_lifetime" data-original="Get Lifetime Access" data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}"></button>
<button type="button"
data-qris-plan="personal_lifetime" data-original="Get Lifetime Access">
data-qris-plan="personal_lifetime" data-original="Get Lifetime Access" data-has-pending="{{ $hasPendingPayment ? 'true' : 'false' }}">
QRIS Lifetime
</button>
</div>
@@ -341,6 +350,16 @@
if (!buttons.length) return;
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const isAuthed = @json(auth()->check());
const confirmReplacePending = async (btn) => {
if ((btn.dataset.hasPending || 'false') !== 'true') return true;
return window.dewemojiConfirm(
'You have a pending payment. Starting a new checkout will cancel the previous pending payment. Continue?',
{
title: 'Replace pending payment',
okText: 'Continue checkout',
}
);
};
buttons.forEach((btn) => {
btn.addEventListener('click', async () => {
@@ -350,6 +369,8 @@
window.location.href = "{{ route('login') }}";
return;
}
const proceed = await confirmReplacePending(btn);
if (!proceed) return;
const original = btn.dataset.original || btn.textContent;
btn.disabled = true;
btn.textContent = 'Redirecting...';
@@ -365,8 +386,17 @@
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.approve_url) {
const reason = data?.error ? ` (${data.error})` : '';
alert('Could not start PayPal checkout. Please try again.' + reason);
if (data?.error === 'lifetime_active') {
alert('Lifetime plan is already active. Monthly/annual checkout is disabled.');
} else if (data?.error === 'pending_cooldown') {
if (window.dewemojiStartCheckoutCooldown) {
window.dewemojiStartCheckoutCooldown(Number(data.retry_after || 120));
}
alert(`Payment confirmation is in progress. Please wait ${Number(data.retry_after || 120)}s or continue pending from Billing.`);
} else {
const reason = data?.error ? ` (${data.error})` : '';
alert('Could not start PayPal checkout. Please try again.' + reason);
}
btn.disabled = false;
btn.textContent = original;
return;
@@ -396,8 +426,17 @@
const lifetimeNote = document.getElementById('lifetime-pay-note');
if (!priceWrap || !secondary || !payBtn || !lifetimePrice || !lifetimeSecondary || !lifetimePay) return;
let period = 'monthly';
let currency = document.querySelector('[data-default-currency]')?.dataset.defaultCurrency || 'USD';
const params = new URLSearchParams(window.location.search);
const requestedPeriod = params.get('period');
const requestedCurrency = params.get('currency');
let period = requestedPeriod === 'annual' ? 'annual' : 'monthly';
let currency = requestedCurrency === 'IDR'
? 'IDR'
: (requestedCurrency === 'USD'
? 'USD'
: (document.querySelector('[data-default-currency]')?.dataset.defaultCurrency || 'USD'));
let checkoutCooldownRemaining = Math.max(0, Number(payBtn.dataset.pendingCooldown || 0));
let cooldownTimer = null;
const setActive = (nodes, value) => {
nodes.forEach((btn) => {
@@ -414,6 +453,23 @@
btn.classList.add('inline-flex', 'items-center', 'justify-center', 'gap-2');
btn.innerHTML = `${icon}<span>${label}</span>`;
};
const getCooldownRemaining = () => Math.max(0, Math.floor(checkoutCooldownRemaining));
const startCooldown = (seconds) => {
checkoutCooldownRemaining = Math.max(getCooldownRemaining(), Math.max(0, Math.floor(Number(seconds) || 0)));
if (getCooldownRemaining() <= 0) return;
if (!cooldownTimer) {
cooldownTimer = setInterval(() => {
checkoutCooldownRemaining = Math.max(0, getCooldownRemaining() - 1);
updatePrice();
if (getCooldownRemaining() <= 0 && cooldownTimer) {
clearInterval(cooldownTimer);
cooldownTimer = null;
}
}, 1000);
}
updatePrice();
};
window.dewemojiStartCheckoutCooldown = startCooldown;
const updatePrice = () => {
const amount = currency === 'USD'
@@ -431,12 +487,26 @@
const canPaypal = (period === 'monthly' ? payBtn.dataset.paypalEnabled === 'true' : payBtn.dataset.paypalAnnualEnabled === 'true');
const canQris = payBtn.dataset.qrisEnabled === 'true';
const hasLifetime = payBtn.dataset.hasLifetime === 'true';
const cooldownRemaining = getCooldownRemaining();
let disabled = false;
let label = 'Start Personal';
let note = '';
if (currency === 'USD') {
if (hasLifetime) {
disabled = true;
label = 'Lifetime active';
note = 'You already own Lifetime. Monthly/Annual checkout is disabled.';
payBtn.classList.remove('bg-brand-sun', 'hover:bg-brand-sunSoft', 'text-black');
payBtn.classList.add('bg-brand-ocean', 'hover:bg-brand-oceanSoft', 'text-white');
} else if (cooldownRemaining > 0) {
disabled = true;
label = 'Processing...';
note = `Payment confirmation in progress. Try again in ${cooldownRemaining}s or continue pending from Billing.`;
payBtn.classList.remove('bg-brand-sun', 'hover:bg-brand-sunSoft', 'text-black');
payBtn.classList.add('bg-brand-ocean', 'hover:bg-brand-oceanSoft', 'text-white');
} else if (currency === 'USD') {
disabled = !canPaypal;
label = 'Start Personal';
note = canPaypal ? '' : 'PayPal is not configured for this plan.';
@@ -473,10 +543,23 @@
const canLifetimePaypal = lifetimePay.dataset.paypalEnabled === 'true';
const canLifetimeQris = lifetimePay.dataset.qrisEnabled === 'true';
const hasLifetimeOnAccount = lifetimePay.dataset.hasLifetime === 'true';
let lifetimeDisabled = false;
let lifetimeLabel = 'Get Lifetime Access';
let lifetimeHint = '';
if (currency === 'USD') {
if (hasLifetimeOnAccount) {
lifetimeDisabled = true;
lifetimeLabel = 'Lifetime active';
lifetimeHint = 'Your lifetime plan is already active.';
lifetimePay.classList.remove('border-brand-sun/60', 'text-brand-sun', 'hover:bg-brand-sun/10');
lifetimePay.classList.add('border-brand-ocean/60', 'text-brand-ocean', 'hover:bg-brand-ocean/10');
} else if (cooldownRemaining > 0) {
lifetimeDisabled = true;
lifetimeLabel = 'Processing...';
lifetimeHint = `Payment confirmation in progress. Try again in ${cooldownRemaining}s or continue pending from Billing.`;
lifetimePay.classList.remove('border-brand-sun/60', 'text-brand-sun', 'hover:bg-brand-sun/10');
lifetimePay.classList.add('border-brand-ocean/60', 'text-brand-ocean', 'hover:bg-brand-ocean/10');
} else if (currency === 'USD') {
lifetimeDisabled = !canLifetimePaypal;
lifetimeLabel = 'Get Lifetime Access';
lifetimeHint = canLifetimePaypal ? '' : 'PayPal is not configured.';
@@ -540,6 +623,18 @@
setActive(periodButtons, period);
setActive(currencyButtons, currency);
updatePrice();
if (getCooldownRemaining() > 0) {
startCooldown(getCooldownRemaining());
}
if (params.get('target') === 'lifetime') {
const lifetimeCard = lifetimePay.closest('section');
if (lifetimeCard) {
lifetimeCard.classList.add('ring-2', 'ring-brand-sun/60');
lifetimeCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => lifetimeCard.classList.remove('ring-2', 'ring-brand-sun/60'), 2200);
}
}
})();
</script>
@@ -562,6 +657,16 @@
let currentOrderId = null;
let modalOpen = false;
let pollTimer = null;
const confirmReplacePending = async (btn) => {
if ((btn.dataset.hasPending || 'false') !== 'true') return true;
return window.dewemojiConfirm(
'You have a pending payment. Starting a new checkout will cancel the previous pending payment. Continue?',
{
title: 'Replace pending payment',
okText: 'Continue checkout',
}
);
};
const openModal = () => {
if (!modal) return;
@@ -657,6 +762,8 @@
window.location.href = "{{ route('login') }}";
return;
}
const proceed = await confirmReplacePending(btn);
if (!proceed) return;
const original = btn.dataset.original || btn.textContent;
btn.disabled = true;
btn.textContent = 'Generating QR...';
@@ -672,7 +779,16 @@
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.payment_number) {
alert('Could not generate QRIS. Please try again.');
if (data?.error === 'lifetime_active') {
alert('Lifetime plan is already active. Monthly/annual checkout is disabled.');
} else if (data?.error === 'pending_cooldown') {
if (window.dewemojiStartCheckoutCooldown) {
window.dewemojiStartCheckoutCooldown(Number(data.retry_after || 120));
}
alert(`Payment confirmation is in progress. Please wait ${Number(data.retry_after || 120)}s or continue pending from Billing.`);
} else {
alert('Could not generate QRIS. Please try again.');
}
btn.disabled = false;
btn.textContent = original;
return;