// 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 = '
' + '' + '' + 'How it works' + 'Ad packages' + 'Live ledger' + 'The contract' + 'Members' + 'โ€ฆ
'; 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
' + '
' + 'How it worksLive ledgerThe contract' + 'TermsPrivacyDisclaimer
'; 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 = '' + 'advertisement' + '
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 => '').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, hp_field_x9: (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])); // โ”€โ”€ founding-week checklist, read from the live account. Shared by /launch and // the dashboard mark. Two items are the member's own call and persist locally. const LAUNCH_KEY = 'iap.launch.manual'; const manualSet = () => { try { return new Set(JSON.parse(localStorage.getItem(LAUNCH_KEY) || '[]')); } catch (e) { return new Set(); } }; function launchToggle(key) { const s = manualSet(); if (s.has(key)) s.delete(key); else s.add(key); try { localStorage.setItem(LAUNCH_KEY, JSON.stringify([...s])); } catch (e) {} } function launchChecks(me) { const m = me || {}, man = manualSet(); const bc = Number(m.buyerCount || 0), refs = (m.referrals || []).length; return [ { key: 'username', title: 'Pick your username', done: !!m.username, href: '/my#profile', cta: 'Profile', how: 'Profile tab. It becomes your invite link and your public page, and it is permanent.', why: 'Every link, banner and video you hand out this week carries it. Change it later and the links you already sent die.' }, { key: 'wallet', title: 'Link your wallet', done: !!m.address, href: '/my#wallet', cta: 'Wallet', how: 'Wallet tab, Connect, sign the free message. MetaMask recommended. Never held crypto? The wallet guide in Training walks through buying POL with a card.', why: 'Payouts go to this address. No wallet, nowhere to pay you.' }, { key: 'payouts', title: 'Switch on payouts', done: !!m.memberId, href: '/my#wallet', cta: 'Wallet', how: 'Wallet tab, one small transaction. It registers your address with the contract.', why: 'The contract binds each buyer to their sponsor at their first purchase. If payouts are off when your first person buys, that commission is not yours.' }, { key: 'level2', title: 'Qualify: open level 2', done: bc >= 2, href: '/my#buy', cta: 'Buy packages', how: 'Two of your people buy a $20 or more package. Or use Qualified Start: add two positions from extra wallets in your own MetaMask and buy a $20 package from each (about $23 of POL in each wallet).', why: 'Until you have two qualifying buyers, every level 2 payment from your team climbs past you.', note: 'Qualifying buyers so far: ' + bc + ' of 2.' }, { key: 'level3', title: 'The leader play: open all three levels', done: bc >= 5, href: '/my#buy', cta: 'Qualified Start', how: 'Five qualifying buyers, real or Qualified Start, up to five linked positions. Once qualified, buy from your main wallet so your sponsor is paid in full.', why: 'A leader whose team goes three deep this week collects level 3 from day one instead of watching those 10% payments pass upward. Optional for members, the play for leaders.', note: 'Qualifying buyers so far: ' + bc + ' of 5.' }, { key: 'banner', title: 'Upload your line banner', done: !!m.lineBannerUrl, href: '/my#profile', cta: 'Profile', how: 'Profile tab, line banner. It shows on the welcome tour to everyone in your next three levels.', why: 'Your first advertising to your own team, free, and it is live the moment they join.' }, { key: 'links', title: 'Copy your links and pick a play', done: man.has('links'), manual: true, how: 'Promo tools, Your links: the invite link and the five angle links, plus the matching hook videos. Then read the plays page and choose one.', why: 'On launch day you send links, not explanations. Having them ready is the whole difference between a launch and a scramble.' }, { key: 'two', title: 'Place your first two', done: refs >= 2, href: '/my#line', cta: 'My line', how: 'Two people you have actually talked to, joined through your link, walked through items 1 to 3 on their own accounts.', why: 'Your first two are the shape of your whole line. Choose them, do not wait for them.', note: 'Joined through you so far: ' + refs + '.' } ]; } // In-page dialogs instead of window.prompt / confirm. Mobile Safari shows a red "Suppress dialogs" // option on the second native pop-up in a row and, once tapped, swallows every later prompt on the // site until reload. ask() resolves the typed value (null on cancel); confirmBox() resolves true/false. function dialog(o) { return new Promise(resolve => { const esc = t => String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const back = document.createElement('div'); back.className = 'modal-back'; back.style.zIndex = '200'; const field = o.type === 'none' ? '' : o.type === 'textarea' ? '' : ''; back.innerHTML = ''; document.body.appendChild(back); const inp = back.querySelector('#dlgInput'); const done = v => { document.removeEventListener('keydown', onKey); back.remove(); resolve(v); }; const okv = () => done(o.type === 'none' ? true : (inp ? inp.value : '')); const onKey = e => { if (e.key === 'Escape') { e.preventDefault(); done(o.type === 'none' ? false : null); } else if (e.key === 'Enter' && o.type !== 'textarea') { e.preventDefault(); okv(); } }; document.addEventListener('keydown', onKey); back.querySelector('#dlgOk').addEventListener('click', okv); back.querySelector('#dlgCancel').addEventListener('click', () => done(o.type === 'none' ? false : null)); back.addEventListener('click', e => { if (e.target === back) done(o.type === 'none' ? false : null); }); setTimeout(() => { if (inp) { inp.focus(); if (inp.select && o.type !== 'textarea') inp.select(); } else back.querySelector('#dlgOk').focus(); }, 30); }); } function ask(o) { return dialog(Object.assign({ type: 'text', value: '', placeholder: '' }, o || {})); } function confirmBox(text, o) { return dialog(Object.assign({ type: 'none', text, ok: 'Yes', cancel: 'No' }, o || {})); } return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, requestCode, launchChecks, launchToggle, ask, confirmBox, $ }; })();