282 lines
14 KiB
Plaintext
282 lines
14 KiB
Plaintext
<%~ 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">← 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">200–800 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>
|
||
|
||
<p class="text-xs text-gray-600 text-center mt-3">Upgrading? Your current plan's remaining time is frozen and resumes after the new plan expires. To merge existing time into a higher plan, contact support.</p>
|
||
</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 →</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 →</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') %>
|