// 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 = '
';
document.body.prepend(nav);
if (c.rehearsal) {
const b = document.createElement('div');
b.className = 'rehearsal';
b.innerHTML = 'Testnet rehearsal: 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) {
// identity order: username, then email, then wallet
const who = me.username
? '@' + String(me.username).replace(/[&<>]/g, '') + ''
: (me.email ? String(me.email).replace(/[&<>]/g, '')
: (me.address ? '' + me.address.slice(0, 6) + 'โฆ' + me.address.slice(-4) + '' : 'signed in'));
el.innerHTML = (me.memberId ? 'member #' + me.memberId + ' ' : '') + who;
} else {
el.innerHTML = 'Sign in';
}
return me;
} catch (e) { return null; }
}
function describeEvent(ev, c) {
const pol = w => fmtPol(w) + ' POL';
// real people, not numbers: use usernames when the site knows them
const nm = id => (ev.names && ev.names[id])
? String(ev.names[id]).replace(/[&<>]/g, '')
: 'member #' + id;
switch (ev.type) {
case 'Purchase': return '๐งพ ' + nm(ev.buyerId) + ' bought package #' + ev.productId
+ ' (' + fmtUsd(ev.priceCents) + ') for ' + pol(ev.paidWei) + ' โ +' + ev.creditAmount.toLocaleString() + ' credits';
case 'TierPaid': return '๐ธ level ' + ev.tier + ' payout โ ' + nm(ev.recipientId) + ': ' + pol(ev.amountWei)
+ (ev.hops ? ' (passed up ' + ev.hops + ')' : '');
case 'PassedUp': return 'โท level ' + ev.tier + ' passed over ' + nm(ev.skippedId) + ' (' + ev.reason + ')';
case 'AdminPaid': return '๐ platform fee settled: ' + pol(ev.amountWei);
case 'BuyerCounted': return 'โญ ' + nm(ev.sponsorId) + ' now has ' + ev.newCount + ' qualifying buyer(s)';
case 'MemberActivated': return '๐ค ' + nm(ev.id) + ' activated a payout wallet';
case 'AwardPaid': return '๐ award: ' + pol(ev.amountWei) + ' โ ' + nm(ev.toId);
case 'CreditsConsumed': return '๐ฃ ' + nm(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 = '' + describeEvent(ev, c) + ''
+ (c.explorer
? 'verify โ'
: 'verify โ'); // built-in viewer when the chain has no public explorer
return div;
}
// Render one served ad into #. 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 = ''
+ '
'
+ 'member ad
';
} else {
el.innerHTML = '' + ad.title + ''
+ (ad.body ? ' ยท ' + ad.body : '') + ' member ad';
}
el.hidden = false;
} catch (e) {}
}
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, $ };
})();