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

315 lines
16 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 hasLifetime = it.hasLifetime || false;
const lifetimeBase = 140;
const lifetimeDiscount = hasLifetime ? 0 : Math.min(totalSpent, lifetimeBase * 0.75);
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) { %>
<% if (plan !== 'free') { %>
<div class="mb-6 rounded-lg border border-border-subtle bg-surface px-4 py-3 text-xs text-gray-400">
Buying a higher tier? Your current plan pauses and picks back up once the new one expires. If you'd rather combine them, reach out to support.
</div>
<% } %>
<!-- ─── 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>
<% if (hasLifetime) { %>
<div class="text-left bg-surface border-2 border-border-subtle rounded-xl p-5 opacity-50 cursor-not-allowed relative">
<span class="absolute top-3 right-3 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-gray-500/15 text-gray-500 border border-gray-500/20">Owned</span>
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-1">Lifetime</div>
<div class="text-2xl font-bold text-gray-400">$<%= lifetimeBase %></div>
<div class="text-xs text-gray-600 mt-2">You already have Lifetime</div>
</div>
<% } else { %>
<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 ─── -->
<%
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;
const pct = Math.min(100, Math.round((received / total) * 100));
const invPlanNames = { pro: 'Pro', pro2x: 'Pro 2x', pro4x: 'Pro 4x', lifetime: 'Lifetime' };
const invPlanLabel = invPlanNames[inv.plan] || inv.plan;
%>
<% if (isPending) { %>
<meta http-equiv="refresh" content="5">
<% } %>
<div class="card-static overflow-hidden max-w-md mx-auto">
<!-- Header bar -->
<div class="px-5 py-3 border-b divider flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-200"><%= invPlanLabel %></span>
<% if (inv.months) { %><span class="text-xs text-gray-500"><%= inv.months %>mo</span><% } %>
</div>
<div class="text-sm font-medium text-gray-300">$<%= Number(inv.amount_usd).toFixed(2) %></div>
</div>
<% if (inv.status === 'pending' || inv.status === 'underpaid') { %>
<div class="px-5 pt-6 pb-5">
<!-- QR + amount side by side on wider, stacked on narrow -->
<div class="flex flex-col items-center gap-5">
<!-- QR -->
<a href="<%= inv.pay_uri %>" class="inline-block">
<img src="<%= inv.qr_url %>" alt="QR Code" class="w-44 h-44 rounded-xl bg-white p-2" id="pay-qr">
</a>
<!-- Open in wallet -->
<a href="<%= inv.pay_uri %>" class="inline-flex items-center gap-2 text-xs text-gray-400 hover:text-gray-200 border border-border-subtle hover:border-border-strong rounded-lg px-4 py-2 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
Open in wallet
</a>
<!-- Amount -->
<div class="text-center">
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">Send exactly</div>
<div class="flex items-center justify-center gap-2">
<span class="text-xl font-bold font-mono text-white"><%= received > 0 && remaining > 0 ? remaining.toFixed(8) : inv.amount_crypto %></span>
<span class="text-sm font-medium text-gray-400"><%= inv.coin_ticker %></span>
</div>
</div>
<% if (received > 0 && remaining > 0) { %>
<!-- Progress bar -->
<div class="w-full">
<div class="w-full h-1.5 rounded-full bg-gray-800 overflow-hidden">
<div class="h-full rounded-full bg-green-500 transition-all" style="width: <%= pct %>%"></div>
</div>
<div class="flex justify-between mt-1.5 text-xs">
<span class="text-green-400 font-mono"><%= received.toFixed(8) %> received</span>
<span class="text-gray-500 font-mono"><%= remaining.toFixed(8) %> left</span>
</div>
</div>
<% } %>
<!-- Address -->
<div class="w-full">
<div class="flex items-center justify-between mb-1.5">
<span class="text-xs text-gray-500">Address</span>
<button onclick="copyAddr()" id="copy-btn" class="text-xs text-gray-500 hover:text-gray-300 transition-colors">Copy</button>
</div>
<code class="block bg-surface-solid border border-border-subtle rounded-lg px-3 py-2.5 text-blue-400 text-[11px] font-mono select-all break-all leading-relaxed" id="pay-address"><%= inv.address %></code>
</div>
</div>
</div>
<!-- Footer: status + timer -->
<div class="px-5 py-3 border-t divider flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full <%= inv.status === 'underpaid' ? 'bg-yellow-500' : 'bg-blue-500' %> animate-pulse"></span>
<span class="text-xs <%= inv.status === 'underpaid' ? 'text-yellow-400' : 'text-gray-400' %>"><%= inv.status === 'underpaid' ? 'Underpaid, send the rest' : 'Waiting for payment' %></span>
</div>
<span class="text-xs text-gray-500 font-mono"><%= mins %>:<%= String(secs).padStart(2, '0') %></span>
</div>
<% } else if (inv.status === 'confirming') { %>
<div class="px-5 py-12 text-center">
<div class="w-14 h-14 mx-auto rounded-full bg-blue-500/10 border border-blue-500/20 flex items-center justify-center mb-5">
<svg class="w-7 h-7 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-base font-semibold text-white mb-1">Payment received</p>
<p class="text-sm text-gray-400">Waiting for network confirmation</p>
<% if (inv.address) { %>
<a href="https://transaction.st/address/<%= inv.address %>" target="_blank" rel="noopener" class="inline-block text-xs text-gray-500 hover:text-gray-300 transition-colors mt-4">View on explorer &rarr;</a>
<% } %>
</div>
<% } else if (inv.status === 'paid') { %>
<div class="px-5 py-12 text-center">
<div class="w-14 h-14 mx-auto rounded-full bg-green-500/10 border border-green-500/20 flex items-center justify-center mb-5">
<svg class="w-7 h-7 text-green-400" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</div>
<p class="text-base font-semibold text-white mb-1">Payment confirmed</p>
<p class="text-sm text-gray-400 mb-5">Your <%= invPlanLabel %> plan is now active.</p>
<div class="flex items-center justify-center gap-3">
<a href="/dashboard/settings" class="btn-primary px-5 py-2 text-sm">settings</a>
<a href="/dashboard/checkout/<%= inv.id %>/receipt" target="_blank" class="btn-secondary px-5 py-2 text-sm">Receipt</a>
</div>
<% if (inv.address) { %>
<a href="https://transaction.st/address/<%= inv.address %>" target="_blank" rel="noopener" class="inline-block text-xs text-gray-500 hover:text-gray-300 transition-colors mt-4">View on explorer &rarr;</a>
<% } %>
</div>
<% } else if (inv.status === 'expired') { %>
<div class="px-5 py-12 text-center">
<div class="w-14 h-14 mx-auto rounded-full bg-red-500/10 border border-red-500/20 flex items-center justify-center mb-5">
<svg class="w-7 h-7 text-red-400" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</div>
<p class="text-base font-semibold text-white mb-1">Invoice expired</p>
<p class="text-sm text-gray-400 mb-5">This payment window has closed.</p>
<a href="/dashboard/checkout" class="btn-primary inline-block px-5 py-2 text-sm">Try again</a>
</div>
<% } %>
</div>
<p class="text-xs text-gray-600 text-center mt-4">You can close this page. Payments are detected automatically.</p>
<% } %>
</main>
<script>
// Copy address
function copyAddr() {
const addr = document.getElementById('pay-address');
const btn = document.getElementById('copy-btn');
if (addr) navigator.clipboard.writeText(addr.textContent);
if (btn) { btn.textContent = 'Copied'; setTimeout(() => btn.textContent = 'Copy', 1500); }
}
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') %>