pingql/apps/web/src/views/checkout.ejs

270 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%~ include('./partials/head', { title: 'Upgrade' }) %>
<%~ include('./partials/nav', { nav: 'settings' }) %>
<%
const plan = it.account.plan;
const coins = it.coins || [];
const invoice = it.invoice;
const payApi = it.payApi || '';
const totalSpent = it.totalSpent || 0;
const lifetimeBase = 140;
const lifetimeDiscount = Math.min(totalSpent, lifetimeBase * 0.5);
const lifetimePrice = lifetimeBase - lifetimeDiscount;
%>
<style>
/* Show pro options when pro is selected */
#plan-pro:checked ~ #pro-options { display: block; }
/* Highlight selected plan type */
#plan-pro:checked ~ .plan-labels label[for="plan-pro"] { border-color: rgb(59 130 246 / 0.5); }
#plan-lifetime:checked ~ .plan-labels label[for="plan-lifetime"] { border-color: rgb(234 179 8 / 0.5); }
/* Highlight selected coin */
.coin-radio:checked + label { border-color: rgb(59 130 246); }
</style>
<main class="max-w-2xl mx-auto px-6 py-8">
<div class="mb-6">
<a href="/dashboard/settings" class="text-sm text-gray-500 hover:text-gray-300 transition-colors">&larr; Back to settings</a>
<h2 class="text-lg font-semibold text-gray-200 mt-2">Upgrade Plan</h2>
</div>
<% if (!invoice) { %>
<!-- ─── Step 1: Plan & coin selection (form, works without JS) ─── -->
<form action="/dashboard/checkout" method="POST" class="space-y-6">
<!-- Single hidden field submitted to server -->
<input type="hidden" name="plan" id="plan-value" value="pro">
<!-- Plan type selector (not submitted, drives UI) -->
<input type="radio" name="_planType" value="pro" id="plan-pro" class="hidden" checked>
<input type="radio" name="_planType" value="lifetime" id="plan-lifetime" class="hidden">
<div class="plan-labels grid grid-cols-2 gap-4">
<label for="plan-pro"
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-xl p-5 transition-colors">
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-1">Pro</div>
<div class="text-2xl font-bold text-gray-100">From $12<span class="text-sm font-normal text-gray-500">/mo</span></div>
<div class="text-xs text-gray-500 mt-2">200800 monitors, 5s intervals</div>
</label>
<label for="plan-lifetime"
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-yellow-500/40 rounded-xl p-5 transition-colors relative">
<span class="absolute top-3 right-3 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-500 border border-yellow-500/20">Launch Deal</span>
<div class="text-xs text-yellow-500/70 uppercase tracking-wider font-mono mb-1">Lifetime</div>
<% if (lifetimeDiscount > 0) { %>
<div class="text-2xl font-bold text-gray-100">$<%= lifetimePrice.toFixed(0) %> <span class="text-sm font-normal text-gray-600 line-through">$<%= lifetimeBase %></span></div>
<div class="text-xs text-green-400/70 mt-1">You've paid us $<%= totalSpent.toFixed(0) %>, so we've credited $<%= lifetimeDiscount.toFixed(0) %> toward lifetime</div>
<% } else { %>
<div class="text-2xl font-bold text-gray-100">$<%= lifetimeBase %></div>
<% } %>
<div class="text-xs text-gray-500 mt-2">One-time, 200 monitors forever</div>
</label>
</div>
<!-- Pro tier + months (visible only when pro selected, via CSS) -->
<div id="pro-options" class="hidden space-y-4">
<div>
<label class="block text-sm text-gray-400 mb-2">Tier</label>
<div id="tier-selector" class="grid grid-cols-3 gap-2">
<button type="button" class="tier-btn text-center bg-surface border-2 border-blue-500/50 rounded-lg py-2.5 transition-colors" data-plan="pro">
<div class="text-sm font-semibold text-gray-200">1x</div>
<div class="text-xs text-gray-500">200 monitors · $12/mo</div>
</button>
<button type="button" class="tier-btn text-center bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-lg py-2.5 transition-colors" data-plan="pro2x">
<div class="text-sm font-semibold text-gray-200">2x</div>
<div class="text-xs text-gray-500">400 monitors · $24/mo</div>
</button>
<button type="button" class="tier-btn text-center bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-lg py-2.5 transition-colors" data-plan="pro4x">
<div class="text-sm font-semibold text-gray-200">4x</div>
<div class="text-xs text-gray-500">800 monitors · $48/mo</div>
</button>
</div>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">How many months?</label>
<select id="months-select" name="months" class="input-base px-4 py-2.5 text-gray-100">
<% for (let i = 1; i <= 12; i++) { %>
<option value="<%= i %>" data-base="<%= i * 12 %>"><%= i %> month<%= i > 1 ? 's' : '' %> — $<%= i * 12 %></option>
<% } %>
</select>
</div>
</div>
<!-- Coin selection -->
<div>
<label class="block text-sm text-gray-400 mb-2">Pay with</label>
<div class="grid grid-cols-3 gap-2">
<% coins.forEach(function(c, i) { %>
<input type="radio" name="coin" value="<%= c.id %>" id="coin-<%= c.id %>" class="hidden coin-radio" <%= i === 0 ? 'checked' : '' %>>
<label for="coin-<%= c.id %>"
class="cursor-pointer flex items-center gap-2 bg-surface border-2 border-border-subtle hover:border-border-strong rounded-lg px-3 py-2.5 transition-colors text-left">
<span class="text-sm font-medium text-gray-300"><%= c.ticker %></span>
<span class="text-xs text-gray-600"><%= c.label %></span>
</label>
<% }) %>
</div>
</div>
<button type="submit"
class="w-full btn-primary py-3">
Continue to Payment
</button>
</form>
<% } else { %>
<!-- ─── Step 2: Payment display (SSR, auto-refreshes without JS) ─── -->
<%
const inv = invoice;
const isPending = inv.status === 'pending' || inv.status === 'underpaid' || inv.status === 'confirming';
const received = parseFloat(inv.amount_received || '0');
const total = parseFloat(inv.amount_crypto);
const remaining = Math.max(0, total - received);
const expiresAt = new Date(inv.expires_at);
const now = new Date();
const remainingSec = Math.max(0, Math.floor((expiresAt.getTime() - now.getTime()) / 1000));
const mins = Math.floor(remainingSec / 60);
const secs = remainingSec % 60;
%>
<% if (isPending) { %>
<!-- Auto-refresh every 5s for status updates (no-JS polling) -->
<meta http-equiv="refresh" content="5">
<% } %>
<div class="card-static px-6 py-10 text-center space-y-5">
<% if (inv.status === 'confirming') { %>
<!-- intentionally empty — confirming state is handled below -->
<% } else if (inv.status !== 'paid') { %>
<!-- QR -->
<div>
<a href="<%= inv.pay_uri %>"><img src="<%= inv.qr_url %>" alt="QR Code" class="w-48 h-48 mx-auto rounded-lg bg-white p-2" id="pay-qr"></a>
</div>
<% } %>
<% if (inv.status === 'pending' || inv.status === 'underpaid') { %>
<!-- Amount -->
<div>
<div class="text-2xl font-bold font-mono text-gray-100"><%= received > 0 && remaining > 0 ? remaining.toFixed(8) : inv.amount_crypto %></div>
<div class="text-sm text-gray-500"><%= inv.coin_label %> (<%= inv.coin_ticker %>)</div>
<div class="text-xs text-gray-600 mt-1">$<%= Number(inv.amount_usd).toFixed(2) %> USD</div>
</div>
<% if (received > 0 && remaining > 0) { %>
<!-- Received / Remaining -->
<div class="flex justify-center gap-6 text-sm">
<div>
<span class="text-gray-500">Received:</span>
<span class="text-green-400 font-mono"><%= received.toFixed(8) %></span>
</div>
<div>
<span class="text-gray-500">Remaining:</span>
<span class="text-yellow-400 font-mono"><%= remaining.toFixed(8) %></span>
</div>
</div>
<% } %>
<!-- Address -->
<div>
<label class="block text-xs text-gray-500 mb-1">Send to</label>
<code class="block bg-surface-solid border border-border-subtle rounded-lg px-3 py-2.5 text-blue-400 text-xs font-mono select-all break-all" id="pay-address"><%= inv.address %></code>
</div>
<% } %>
<!-- Status -->
<% if (inv.status === 'pending') { %>
<div class="flex items-center justify-center gap-2 text-sm">
<span class="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></span>
<span class="text-gray-400">Waiting for payment...</span>
</div>
<div class="text-xs text-gray-600 mt-2">
Expires in <span class="font-mono"><%= mins %>:<%= String(secs).padStart(2, '0') %></span>
</div>
<% } else if (inv.status === 'underpaid') { %>
<div class="flex items-center justify-center gap-2 text-sm">
<span class="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></span>
<span class="text-yellow-400">Underpaid - please send the remaining amount</span>
</div>
<div class="text-xs text-gray-600 mt-2">
Expires in <span class="font-mono"><%= mins %>:<%= String(secs).padStart(2, '0') %></span>
</div>
<% } else if (inv.status === 'confirming') { %>
<div class="w-16 h-16 mx-auto rounded-full bg-blue-500/10 border border-blue-500/20 flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
</div>
<p class="text-lg font-semibold text-white">Payment received</p>
<p class="text-sm text-gray-400">Waiting for 1 network confirmation...</p>
<% if (inv.address) { %>
<a href="https://transaction.st/address/<%= inv.address %>" target="_blank" rel="noopener" class="block text-xs text-gray-500 hover:text-gray-400 transition-colors mt-1">View on block explorer &rarr;</a>
<% } %>
<% } else if (inv.status === 'paid') { %>
<div class="w-16 h-16 mx-auto rounded-full bg-green-500/10 border border-green-500/20 flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-green-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</div>
<p class="text-lg font-semibold text-white">Payment confirmed</p>
<p class="text-sm text-gray-400">Your plan has been activated.</p>
<div class="flex items-center justify-center gap-3 mt-2">
<a href="/dashboard/settings" class="btn-primary inline-block px-5 py-2 text-sm">Back to settings</a>
<a href="/dashboard/checkout/<%= inv.id %>/receipt" target="_blank" class="btn-secondary inline-block px-5 py-2 text-sm">View Receipt</a>
</div>
<% if (inv.address) { %>
<a href="https://transaction.st/address/<%= inv.address %>" target="_blank" rel="noopener" class="block text-xs text-gray-500 hover:text-gray-400 transition-colors mt-3">View on block explorer &rarr;</a>
<% } %>
<% } else if (inv.status === 'expired') { %>
<div class="flex items-center justify-center gap-2 text-red-400">
<span class="font-medium">Payment expired</span>
</div>
<a href="/dashboard/checkout" class="mt-3 text-sm text-blue-400 hover:text-blue-300">Try again</a>
<% } %>
</div>
<p class="text-xs text-gray-600 text-center mt-4">You can leave this page. Payments are detected and confirmed automatically.</p>
<% } %>
</main>
<script>
const multipliers = { pro: 1, pro2x: 2, pro4x: 4 };
const planValue = document.getElementById('plan-value');
let selectedTier = 'pro';
// Plan type toggle (pro vs lifetime)
document.querySelectorAll('input[name="_planType"]').forEach(radio => {
radio.addEventListener('change', () => {
planValue.value = radio.value === 'lifetime' ? 'lifetime' : selectedTier;
});
});
// Tier buttons
document.querySelectorAll('.tier-btn').forEach(btn => {
btn.addEventListener('click', () => {
selectedTier = btn.dataset.plan;
planValue.value = selectedTier;
// Update active style
document.querySelectorAll('.tier-btn').forEach(b => {
b.classList.remove('border-blue-500/50');
b.classList.add('border-border-subtle');
});
btn.classList.remove('border-border-subtle');
btn.classList.add('border-blue-500/50');
updateMonthPricing(selectedTier);
});
});
function updateMonthPricing(plan) {
const sel = document.getElementById('months-select');
if (!sel) return;
const mult = multipliers[plan] || 1;
for (const opt of sel.options) {
const base = parseInt(opt.dataset.base);
opt.textContent = opt.value + ' month' + (opt.value > 1 ? 's' : '') + ' — $' + (base * mult);
}
}
</script>
<%~ include('./partials/foot') %>