547bff0d05
refreshNavWallet assumed every signed-in session has a wallet address and threw on null for email-only members; the catch returned null and render() treated the member as signed out even though the session and cookie were valid. Nav now shows the email (or member badge) when no wallet is linked. Verified with a full browser cycle: session landing, logout, fresh code sign-in. Assets v=20260904k. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
110 lines
5.1 KiB
JavaScript
110 lines
5.1 KiB
JavaScript
// Shared page runtime: site config, nav, formatting. Zero dependencies.
|
||
window.IAP = (function () {
|
||
let config = null;
|
||
const $ = id => document.getElementById(id);
|
||
|
||
async function getConfig() {
|
||
if (!config) config = await (await fetch('/api/config')).json();
|
||
return config;
|
||
}
|
||
function fmtPol(wei) {
|
||
const s = BigInt(wei).toString().padStart(19, '0');
|
||
const whole = s.slice(0, -18) || '0';
|
||
const frac = s.slice(-18, -12).replace(/0+$/, '');
|
||
return whole + (frac ? '.' + frac : '');
|
||
}
|
||
const fmtUsd = cents => '$' + (cents / 100).toFixed(2);
|
||
|
||
function status(msg, cls) {
|
||
let el = $('status');
|
||
if (!el) { el = document.createElement('div'); el.id = 'status'; document.body.appendChild(el); }
|
||
el.textContent = msg; el.className = cls || ''; el.hidden = false;
|
||
clearTimeout(status._t);
|
||
if (cls === 'ok') status._t = setTimeout(() => { el.hidden = true; }, 6000);
|
||
}
|
||
|
||
async function renderNav(active) {
|
||
const c = await getConfig();
|
||
const nav = document.createElement('nav');
|
||
nav.innerHTML = '<div class="wrap">'
|
||
+ '<a class="logo" href="/">Instant<b>AdPay</b></a>'
|
||
+ '<span class="links">'
|
||
+ '<a href="/" data-p="home">How it works</a>'
|
||
+ '<a href="/#ladder" data-p="pricing">Ad packages</a>'
|
||
+ '<a href="/ledger" data-p="ledger">Live ledger</a>'
|
||
+ '<a href="/contract" data-p="contract">The contract</a>'
|
||
+ '<a href="/my" data-p="my">Members</a>'
|
||
+ '</span><span id="navWallet" class="muted">…</span></div>';
|
||
document.body.prepend(nav);
|
||
if (c.rehearsal) {
|
||
const b = document.createElement('div');
|
||
b.className = 'rehearsal';
|
||
b.innerHTML = '<b>Testnet rehearsal</b>: running on ' + c.chainName + '. Purchases use valueless test POL while we prove every payout in public.';
|
||
document.body.prepend(b);
|
||
}
|
||
const a = nav.querySelector('[data-p="' + active + '"]');
|
||
if (a) a.className = 'active';
|
||
refreshNavWallet();
|
||
}
|
||
async function refreshNavWallet() {
|
||
try {
|
||
const me = await (await fetch('/api/me')).json();
|
||
const el = $('navWallet');
|
||
if (!el) return;
|
||
if (me.signedIn) {
|
||
// email-only members have no wallet address yet
|
||
const who = me.address
|
||
? '<span class="mono">' + me.address.slice(0, 6) + '…' + me.address.slice(-4) + '</span>'
|
||
: (me.email ? String(me.email).replace(/[&<>]/g, '') : 'signed in');
|
||
el.innerHTML = (me.memberId ? '<span class="badge">member #' + me.memberId + '</span> ' : '') + who;
|
||
} else {
|
||
el.innerHTML = '<a href="/my">Sign in</a>';
|
||
}
|
||
return me;
|
||
} catch (e) { return null; }
|
||
}
|
||
function describeEvent(ev, c) {
|
||
const pol = w => fmtPol(w) + ' POL';
|
||
switch (ev.type) {
|
||
case 'Purchase': return '🧾 member #' + ev.buyerId + ' bought package #' + ev.productId
|
||
+ ' (' + fmtUsd(ev.priceCents) + ') for ' + pol(ev.paidWei) + ' → +' + ev.creditAmount.toLocaleString() + ' credits';
|
||
case 'TierPaid': return '💸 level ' + ev.tier + ' payout → member #' + ev.recipientId + ': ' + pol(ev.amountWei)
|
||
+ (ev.hops ? ' (passed up ' + ev.hops + ')' : '');
|
||
case 'PassedUp': return '↷ level ' + ev.tier + ' passed over #' + ev.skippedId + ' (' + ev.reason + ')';
|
||
case 'AdminPaid': return '🏛 platform fee settled: ' + pol(ev.amountWei);
|
||
case 'BuyerCounted': return '⭐ member #' + ev.sponsorId + ' now has ' + ev.newCount + ' qualifying buyer(s)';
|
||
case 'MemberActivated': return '👤 member #' + ev.id + ' activated a payout wallet';
|
||
case 'AwardPaid': return '🎁 award: ' + pol(ev.amountWei) + ' → member #' + ev.toId;
|
||
case 'CreditsConsumed': return '📣 member #' + ev.memberId + ' ran ads: −' + ev.amount.toLocaleString() + ' credits';
|
||
case 'PriceCached': return '🔮 oracle price refreshed';
|
||
case 'FallbackPriceUsed': return '🔮 cached price bridged an oracle gap';
|
||
default: return '· ' + ev.type;
|
||
}
|
||
}
|
||
function feedRow(ev, c) {
|
||
const div = document.createElement('div');
|
||
div.className = 'row t-' + ev.type;
|
||
div.innerHTML = '<span>' + describeEvent(ev, c) + '</span>'
|
||
+ '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>';
|
||
return div;
|
||
}
|
||
// Render one served ad into #<elId>. Silent if no inventory.
|
||
async function adSlot(type, elId) {
|
||
try {
|
||
const { ad } = await (await fetch('/api/ads/slot?type=' + type)).json();
|
||
const el = $(elId);
|
||
if (!ad || !el) return;
|
||
if (ad.imageUrl) {
|
||
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow">'
|
||
+ '<img src="' + ad.imageUrl + '" alt="advertisement" style="max-width:100%;border-radius:8px"></a>'
|
||
+ '<div class="small muted">member ad</div>';
|
||
} else {
|
||
el.innerHTML = '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><b>' + ad.title + '</b>'
|
||
+ (ad.body ? ' · ' + ad.body : '') + '</a> <span class="small muted">member ad</span>';
|
||
}
|
||
el.hidden = false;
|
||
} catch (e) {}
|
||
}
|
||
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, $ };
|
||
})();
|