315 lines
16 KiB
Plaintext
315 lines
16 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) { %>
|
||
<% 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">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>
|
||
|
||
</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 →</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 →</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') %>
|