cdeb59ed7f
Members earn credits by attention (spec 8b): daily 5-ad set with dwell timing and a too-fast guard, then a claimable daily batch. Earned credits now FUND campaigns: banner/text budgets draw earned-first (free members can advertise on welcome credits alone), purchased credits and the on-chain burn queue only cover the remainder; earned-only campaigns pause when the pool runs dry. New Earn credits section in the member menu with the viewer. Explorer links degrade gracefully for the private chain (contract page points at the audited Amoy verification). Site flipped to the anvil rehearsal chain at rpc.instantadpay.com: unlimited test POL, no more faucets. E2E: welcome->views->claim->earned-funded campaign->charged serving, all green. Assets v=20260905d. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
5.2 KiB
JavaScript
112 lines
5.2 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>'
|
||
+ (c.explorer
|
||
? '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>'
|
||
: '<span class="tx mono">' + ev.tx.slice(0, 12) + '…</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, $ };
|
||
})();
|