// My account: email-first join/login, wallet link at purchase time, // free payout activation, invite link, on-chain activity. (async function () { // back-office shell: no marketing nav here; rehearsal notice lives in the top bar const $ = IAP.$; try { const c = await IAP.getConfig(); if (c.rehearsal && $('boRehearsal')) { $('boRehearsal').hidden = false; $('boRehearsal').innerHTML = 'Testnet rehearsal · ' + c.chainName; } } catch (e) {} async function api(path, body) { const r = await (await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })).json(); if (r.error) throw new Error(r.error); return r; } function nextMove(d) { if (!d.address) return 'Grab your link below and start sharing today. Then link your wallet (one free signature) so every payment can lock to you before your people start buying.'; if (!d.memberId) return 'Your wallet is linked. Switch on payouts above (one free transaction) and every purchase in your line pays you the moment it happens.'; if (d.buyerCount === 0) return 'Payouts are on and your link is live. Your next milestone: your first buyer of $20 or more. Every direct pays you 50 percent from their very first package.'; if (d.buyerCount === 1) return 'One qualifying buyer down, one to go. Your next $20+ buyer unlocks level 2: 20 percent of everything your people’s people buy.'; if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.'; return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.'; } async function loadDashboard() { try { const d = await (await fetch('/api/my/dashboard')).json(); if (d.error) return; const wc = d.welcomeCredits || 0; $('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString(); $('dbCreditsSub').textContent = wc ? (d.credits || 0).toLocaleString() + ' purchased · ' + wc + ' welcome' : ''; $('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0'); $('dbBuyers').textContent = d.buyerCount || 0; $('dbTeam').textContent = (d.referrals || []).length; $('nextMove').textContent = nextMove(d); $('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%'; const wrap = $('rosterWrap'); if ((d.referrals || []).length) { $('rosterEmpty').hidden = true; let t = wrap.querySelector('table'); if (t) t.remove(); t = document.createElement('table'); t.className = 'roster'; t.innerHTML = d.referrals.map(r => '' + r.email + '' + '' + new Date(r.joined).toLocaleDateString() + '' + '' + r.status + '').join(''); wrap.appendChild(t); } // overview recent-activity widget: chain events + line joins, newest first try { const c = await IAP.getConfig(); const ov = $('ovFeed'); const rows = []; if (d.memberId) { const a = await (await fetch('/api/my/activity')).json(); for (const ev of [...(a.earnings || []), ...(a.purchases || [])] .sort((x, y) => y.block - x.block).slice(0, 5)) rows.push(IAP.feedRow(ev, c)); } for (const r of (d.referrals || []).slice(0, 3)) { const div = document.createElement('div'); div.className = 'row'; div.innerHTML = '🤝 ' + r.email + ' joined your line' + new Date(r.joined).toLocaleDateString() + ''; rows.push(div); } if (rows.length) { ov.innerHTML = ''; rows.slice(0, 6).forEach(r => ov.appendChild(r)); } } catch (e) {} // ready-to-send share message const link = location.origin + '/join/' + (d.refCode || d.memberId || ''); if (d.refCode || d.memberId) { const pitch = 'I found an advertising site that pays referrals instantly to your own wallet. ' + 'No withdrawals, no waiting, and every payment is public on a blockchain ledger you can check yourself. ' + 'Free to join and look around: ' + link; $('copyPitch').hidden = false; $('pitchPreview').hidden = false; $('pitchPreview').textContent = '"' + pitch + '"'; $('copyPitch').onclick = async () => { try { await navigator.clipboard.writeText(pitch); IAP.status('Message copied. Paste it anywhere.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the preview text instead.', 'bad'); } }; } } catch (e) {} } // ── back-office menu: hash-routed panes ─────────────── const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'wallet']; const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns', earn: 'Earn credits', earnings: 'Earnings', wallet: 'Wallet & account' }; function setPane(name) { if (!PANES.includes(name)) name = 'overview'; for (const p of PANES) { const el = $('pane-' + p); if (el) el.hidden = p !== name; } document.querySelectorAll('.bo-menu [data-pane]').forEach(b => b.classList.toggle('on', b.dataset.pane === name)); if ($('boTitle')) $('boTitle').textContent = TITLES[name]; document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name); } document.querySelectorAll('.bo-menu [data-pane]').forEach(b => b.addEventListener('click', () => setPane(b.dataset.pane))); document.querySelectorAll('.qa [data-goto]').forEach(b => b.addEventListener('click', () => setPane(b.dataset.goto))); const qaCopy = document.getElementById('qaCopyInvite'); if (qaCopy) qaCopy.addEventListener('click', async () => { const link = document.getElementById('inviteLine').textContent; if (!link || !link.startsWith('http')) { setPane('line'); return; } try { await navigator.clipboard.writeText(link); IAP.status('Invite link copied.', 'ok'); } catch (e) { setPane('line'); } }); window.addEventListener('hashchange', () => setPane(location.hash.slice(1))); if ($('boBurger')) $('boBurger').addEventListener('click', () => document.getElementById('memberArea').classList.toggle('side-open')); async function render() { const me = await IAP.refreshNavWallet(); const signedIn = me && me.signedIn; $('authArea').hidden = !!signedIn; $('memberArea').hidden = !signedIn; if (!signedIn) return; setPane(location.hash.slice(1) || 'overview'); $('campGate').hidden = !!me.memberId; $('earnGate').hidden = !!me.memberId; loadDashboard(); const who = []; if (me.email) who.push(me.email); if (me.address) who.push('wallet ' + me.address.slice(0, 8) + '…' + me.address.slice(-6) + ''); else who.push('no wallet linked yet'); if (me.memberId) who.push('on-chain member #' + me.memberId + '' + (me.onchainSponsorId ? ', sponsored by #' + me.onchainSponsorId : '')); else if (me.sponsorId) who.push('invited by member #' + me.sponsorId); $('posLine').innerHTML = who.join('
'); $('creditLine').textContent = (me.credits || 0).toLocaleString(); $('linkCard').hidden = !!me.address; $('activateCard').hidden = !(me.address && !me.memberId); $('activityArea').hidden = !me.memberId; $('campGate').hidden = true; // earned credits fund campaigns for everyone $('campaignCard').hidden = false; loadCampaigns(); // the share link works from day one; codes resolve to your chain id later if (me.refCode || me.memberId) { $('inviteLine').textContent = location.origin + '/join/' + (me.refCode || me.memberId); $('copyInvite').hidden = false; } else { $('inviteLine').textContent = 'Sign in with your email to get your link.'; $('copyInvite').hidden = true; } if (me.memberId) { const bc = me.buyerCount || 0; $('qualLine').innerHTML = '' + bc + ' qualifying buyer(s) referred
' + (bc >= 5 ? 'Level 3 unlocked: full three-level earnings' : bc >= 2 ? 'Level 2 unlocked · ' + (5 - bc) + ' more for level 3' : (2 - bc) + ' more buyer(s) of $20+ unlock level 2'); loadActivity(); } else { $('qualLine').innerHTML = 'Share your link now. Then switch on payouts (free, above) ' + 'before your people start buying: the contract locks each buyer to their sponsor at ' + 'their first purchase, and payments only route to wallets that are switched on.'; } } async function loadActivity() { try { const c = await IAP.getConfig(); const a = await (await fetch('/api/my/activity')).json(); const fill = (id, evs, empty) => { const el = $(id); el.innerHTML = ''; if (!evs || !evs.length) { el.innerHTML = '
' + empty + '
'; return; } for (const ev of evs) el.appendChild(IAP.feedRow(ev, c)); }; fill('earnFeed', a.earnings, 'No payouts yet. They appear here the moment one lands.'); fill('refFeed', a.referrals, 'No referral activity yet. Share your invite link.'); fill('buyFeed', a.purchases, 'No purchases from your wallet yet.'); } catch (e) {} } async function loadCampaigns() { try { const r = await (await fetch('/api/my/campaigns')).json(); if (r.error) return; $('rateLine').textContent = 'Available to spend: ' + r.availableCredits.toLocaleString() + (r.earnedCredits ? ' (' + r.purchasedCredits.toLocaleString() + ' purchased + ' + r.earnedCredits + ' earned)' : '') + ' credits · rates: banner ' + r.rates.bannerCreditsPerBatch + 'cr/' + r.rates.bannerBatch + ' views, text ' + r.rates.textCreditsPerBatch + 'cr/' + r.rates.textBatch + ' views, login ' + r.rates.loginCreditsPerDay + 'cr/day'; const el = $('campList'); el.innerHTML = ''; if (!r.campaigns.length) { el.innerHTML = '

No campaigns yet. Launch your first below.

'; return; } const tbl = document.createElement('div'); tbl.className = 'tablewrap'; tbl.innerHTML = '' + '' + r.campaigns.map(c => '' + '' + '' + '' + '').join('') + '
NameTypeViewsClicksSpentBudgetStatus
' + c.name + '' + c.type + '' + c.imps.toLocaleString() + '' + c.clicks + '' + c.spent + '' + c.budget + '' + (c.status === 'out' ? 'budget spent' : c.status) + '' + (c.status === 'active' ? '' : c.status === 'paused' ? '' : '') + '
'; el.appendChild(tbl); el.querySelectorAll('button[data-camp]').forEach(b => b.addEventListener('click', async () => { try { await api('/api/my/campaigns/' + b.dataset.camp + '/' + b.dataset.act); await loadCampaigns(); } catch (e) { IAP.status(e.message, 'bad'); } })); } catch (e) {} } $('cType').addEventListener('change', () => { const t = $('cType').value; $('cImageRow').hidden = t === 'text'; $('cTitleRow').hidden = t !== 'text'; $('cBodyRow').hidden = t !== 'text'; }); $('createCampBtn').addEventListener('click', busy2($('createCampBtn'), async () => { await api('/api/my/campaigns', { type: $('cType').value, name: $('cName').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, title: $('cTitle').value, body: $('cBody').value, budget: Number($('cBudget').value) }); IAP.status('Campaign is live. It starts serving right away.', 'ok'); $('cName').value = ''; $('cBudget').value = ''; await loadCampaigns(); })); // defers the busy() lookup to click time (busy is declared below) function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); } // ── in-dashboard package buying ── const PKG = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' }; async function loadBuyTiles() { try { const { products } = await (await fetch('/api/catalog')).json(); const wrap = $('boTiles'); wrap.innerHTML = ''; for (const p of products) { const bonus = p.creditAmount - p.priceCents; const div = document.createElement('div'); div.className = 'tile' + (p.priceCents === 5000 ? ' hot' : ''); div.innerHTML = '
' + (PKG[p.id] || 'Package ' + p.id) + '
' + '
$' + Math.round(p.priceCents / 100) + '
' + '
' + p.creditAmount.toLocaleString() + ' credits
' + '
' + (bonus > 0 ? '+' + bonus.toLocaleString() + ' bonus credits' : ' ') + '
' + '
' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL right now' : 'paused') + '
' + ''; wrap.appendChild(div); } wrap.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', async () => { try { b.disabled = true; const spNow = await (await fetch('/api/sponsor')).json(); IAP.status('Confirm the purchase in your wallet…'); const r = await IAPWallet.buy(Number(b.dataset.id), spNow.sponsorId || 0, b.dataset.cost); if (r.status !== '0x1' && r.receipt && r.receipt.status !== '0x1') throw new Error('Transaction reverted.'); IAP.status('Purchase settled on-chain. Credits are in your account.', 'ok'); await render(); loadBuyTiles(); } catch (e) { IAP.status('Purchase failed: ' + ((e && e.message) || e), 'bad'); } finally { b.disabled = false; } })); } catch (e) {} } loadBuyTiles(); // ── earn-by-viewing: daily ad set with dwell, then claim ── const earnState = { types: ['banner', 'text'], i: 0, timer: null }; async function earnRefresh() { try { const st = await (await fetch('/api/my/earn')).json(); if (st.error) return null; $('earnProgress').textContent = 'today: ' + st.views + ' / ' + st.target + ' ads viewed'; $('earnBalance').textContent = st.earned + ' earned credits'; const done = st.views >= st.target; $('earnClaimBtn').hidden = !(done && !st.claimed); if (st.claimed) $('earnHint').textContent = 'Claimed for today. Come back tomorrow, or put those credits to work in Campaigns.'; else if (done) $('earnHint').textContent = 'Set complete. Claim your ' + st.claimCredits + ' credits.'; return st; } catch (e) { return null; } } async function earnShowAd() { const type = earnState.types[earnState.i++ % earnState.types.length]; let r = null; try { r = await (await fetch('/api/my/earnview?type=' + type)).json(); } catch (e) {} const box = $('earnAdBox'); if (!r || !r.ad) { await earnRefresh(); box.innerHTML = '' + (r && r.status && r.status.views >= r.status.target ? 'Set complete for today.' : 'No member ads are live in rotation right now. Views resume the moment a campaign is running.') + ''; return; } const ad = r.ad; box.innerHTML = ad.imageUrl ? 'member ad' : '' + ad.title + '' + (ad.body ? '
' + ad.body + '' : '') + '
'; const btn = $('earnNextBtn'); btn.hidden = false; btn.disabled = true; // the countdown only runs while this tab is visible AND focused; the // server separately enforces the full dwell on its own clock let left = r.status.dwell; btn.textContent = 'Next ad (' + left + 's)'; clearInterval(earnState.timer); earnState.timer = setInterval(async () => { if (document.visibilityState !== 'visible' || !document.hasFocus()) { btn.textContent = 'Paused: stay on this tab (' + left + 's)'; return; } left -= 1; if (left > 0) { btn.textContent = 'Next ad (' + left + 's)'; return; } clearInterval(earnState.timer); btn.textContent = 'Next ad'; const v = await (await fetch('/api/my/adview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: r.token }) })).json(); if (v.error) { IAP.status(v.error, 'bad'); btn.disabled = false; return; } btn.disabled = false; await earnRefresh(); }, 1000); } $('earnStartBtn').addEventListener('click', () => { $('earnStartBtn').hidden = true; earnShowAd(); }); $('earnNextBtn').addEventListener('click', earnShowAd); $('earnClaimBtn').addEventListener('click', async () => { try { const r = await api('/api/my/claim'); IAP.status('+' + r.credited + ' credits earned. Spend them in Campaigns.', 'ok'); await earnRefresh(); loadDashboard(); } catch (e) { IAP.status(e.message, 'bad'); } }); const busy = (btn, fn) => async () => { try { btn.disabled = true; await fn(); } catch (e) { IAP.status((e && e.message) || String(e), 'bad'); } finally { btn.disabled = false; } }; // passwordless (feature-flagged on config.emailAuth): code replaces passwords (async () => { const cfg = await IAP.getConfig(); if (!cfg.emailAuth) return; $('passCards').hidden = true; $('magicCard').hidden = false; const start = busy($('mcSendBtn'), async () => { const r = await api('/api/auth/email/start', { email: $('mcEmail').value }); $('mcCodeRow').hidden = false; $('mcVerifyBtn').hidden = false; $('mcSendBtn').hidden = true; $('mcResend').hidden = false; if (r.devCode) { $('mcCode').value = r.devCode; IAP.status('Dev mode: code filled in for you.', 'ok'); } else IAP.status('Code sent. Check your inbox (and spam, the first time).', 'ok'); $('mcCode').focus(); }); $('mcSendBtn').addEventListener('click', start); $('mcResend').addEventListener('click', busy($('mcResend'), async () => { const r = await api('/api/auth/email/start', { email: $('mcEmail').value }); if (r.devCode) $('mcCode').value = r.devCode; IAP.status('Fresh code sent.', 'ok'); })); $('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => { await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value }); IAP.status('You are in.', 'ok'); await render(); })); })(); $('signupBtn').addEventListener('click', busy($('signupBtn'), async () => { await api('/api/signup', { email: $('suEmail').value, password: $('suPass').value }); IAP.status('Welcome aboard. You are in.', 'ok'); await render(); })); $('loginBtn').addEventListener('click', busy($('loginBtn'), async () => { await api('/api/login', { email: $('liEmail').value, password: $('liPass').value }); IAP.status('Logged in.', 'ok'); await render(); })); $('walletSigninLink').addEventListener('click', async e => { e.preventDefault(); try { IAP.status('Check your wallet for the free sign-in signature…'); await IAPWallet.signIn(); IAP.status('Signed in with your wallet.', 'ok'); await render(); } catch (err) { IAP.status((err && err.message) || String(err), 'bad'); } }); $('linkBtn').addEventListener('click', busy($('linkBtn'), async () => { IAP.status('Check your wallet for the free link signature…'); await IAPWallet.signIn(); // server binds the wallet to the signed-in email account IAP.status('Wallet linked. Earnings pay there from now on.', 'ok'); await render(); })); $('activateBtn').addEventListener('click', busy($('activateBtn'), async () => { const me = await (await fetch('/api/me')).json(); IAP.status('Confirm the free activation in your wallet…'); const r = await IAPWallet.activate(me.sponsorId || 0); if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.'); IAP.status('Payouts are on. Your invite link is live.', 'ok'); await render(); })); $('copyInvite').addEventListener('click', async () => { try { await navigator.clipboard.writeText($('inviteLine').textContent); IAP.status('Link copied.', 'ok'); } catch (e) { IAP.status('Copy failed. Select and copy the link text.', 'bad'); } }); $('logoutLink').addEventListener('click', async e => { e.preventDefault(); await fetch('/api/auth/logout', { method: 'POST' }); IAP.status('Logged out.', 'ok'); await render(); }); // ad surfaces: login ads greet the sign-in screen; members see live // banner + text placements inside the back office (they ARE the audience) IAP.adSlot('login', 'adSlotLogin'); IAP.adSlot('banner', 'adSlotOverview'); IAP.adSlot('text', 'adSlotSide'); render(); })();