Implement catalog CRUD overhaul, snapshot fallback activation, and billing/UX hardening
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user