// 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;
}
// POL amounts display with two decimals (rounded half-up), e.g. 523.39
function fmtPol(wei) {
const cents = (BigInt(wei) + 5000000000000000n) / 10000000000000000n; // wei -> hundredths of a POL
const s = cents.toString().padStart(3, '0');
return s.slice(0, -2) + '.' + s.slice(-2);
}
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();
renderFooter();
}
function renderFooter() {
if (document.getElementById('iapFooter')) return;
const f = document.createElement('footer'); f.id = 'iapFooter';
f.style.cssText = 'border-top:1px solid var(--line);margin-top:48px;padding:26px 22px;text-align:center;color:var(--muted);font-size:13px';
f.innerHTML = 'ยฉ ' + new Date().getFullYear() + ' InstantAdPay
'
+ '';
document.body.appendChild(f);
}
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;
const when = ev.ts ? new Date(ev.ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
div.innerHTML = (when ? '' + when + ' ' : '') + '' + 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.
// report an ad (auto-approved ads need a member-facing flag โ admin notified)
function reportAd(campaignId) {
if (!campaignId) return;
const reason = (prompt('Report this ad. Reason: broken, inappropriate, spam, scam, or other', 'broken') || '').trim().toLowerCase();
if (!reason) return;
const note = prompt('Anything to add? (optional)') || '';
fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId, reason, note }) })
.then(() => status('Thanks โ this ad was reported to the admin for review.', 'ok'))
.catch(() => status('Could not send the report. Try again.', 'bad'));
}
const reportTag = ad => ' โ report ';
function wireReport(el) {
const rl = el.querySelector('.ad-report');
if (rl) rl.addEventListener('click', e => { e.preventDefault(); reportAd(Number(rl.dataset.cid)); });
}
async function adSlot(type, elId, opts) {
try {
const q = '/api/ads/slot?type=' + type + (opts && opts.width ? '&w=' + opts.width + '&h=' + opts.height : '');
const { ad } = await (await fetch(q)).json();
const el = $(elId);
if (!ad || !el) return;
el.hidden = false;
if (ad.imageUrl) {
el.style.textAlign = 'center';
el.innerHTML = ''
+ ' '
+ 'member ad' + reportTag(ad) + '
';
} else {
el.innerHTML = '' + ad.title + ' '
+ (ad.body ? ' ยท ' + ad.body : '') + ' member ad' + reportTag(ad) + ' ';
}
wireReport(el);
el.hidden = false;
} catch (e) {}
}
// โโ sign-up code request with the invisible guard fields (form age + honeypot)
// and the icon check the server asks for only after an IP trips a limit โโ
const FORM_TS = Date.now();
function iconCheck(host, ch, note) {
return new Promise(resolve => {
host.hidden = false;
host.innerHTML = '' + (note ? esc(note) + ' ' : '') + 'Tap the ' + esc(ch.prompt) + ' .
'
+ '' + ch.options.map(o => '' + esc(o) + ' ').join('') + '
';
host.querySelectorAll('.ic-btn').forEach(b => b.addEventListener('click', () => { host.innerHTML = ''; host.hidden = true; resolve(b.textContent); }, { once: true }));
});
}
async function requestCode(email, opts) {
const o = opts || {};
let pick = null;
for (let i = 0; i < 4; i++) {
const r = await (await fetch('/api/auth/email/start', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, fts: FORM_TS, website: (o.honeypot && o.honeypot.value) || '', pick }) })).json();
if (r.challenge && o.host) { pick = await iconCheck(o.host, r.challenge, r.error); continue; }
if (r.error) throw new Error(r.error);
return r;
}
throw new Error('Could not verify. Refresh the page and try again.');
}
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, requestCode, $ };
})();