// 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; } if (c.rehearsal && $('faucetCard')) $('faucetCard').hidden = false; } catch (e) {} // if they arrived through a sponsor's link, show who they're joining under (async () => { try { const sp = await (await fetch('/api/sponsor')).json(); if (sp && sp.invited && sp.name && $('sponsorNote')) { $('sponsorNoteName').textContent = sp.name; $('sponsorNote').hidden = false; } } 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.'; } // featured rotation strip on the overview let featItems = [], featIdx = 0, featTimer = null; function renderOneFeatured() { if (!featItems.length) return; const i = featItems[featIdx % featItems.length]; $('featStrip').innerHTML = '' + esc(i.title) + '' + (i.by ? '' + esc(i.by) + '' : '') + ''; } async function loadFeatured() { try { const r = await (await fetch('/api/featured')).json(); const card = $('featuredCard'); if (!card) return; if (!r.items || !r.items.length) { card.hidden = true; return; } card.hidden = false; featItems = r.items; featIdx = Math.floor(Math.random() * featItems.length); $('featSub').textContent = r.items.length + ' link' + (r.items.length === 1 ? '' : 's') + ' in rotation'; renderOneFeatured(); // show ONE at a time (true rotation), cycle if more than one clearInterval(featTimer); if (featItems.length > 1) featTimer = setInterval(() => { featIdx++; renderOneFeatured(); }, 6000); // self-sell: today's open slots + a CTA to feature your own link try { const st = await (await fetch('/api/featured/stats')).json(); const today = (st.occupancy || []).find(d => d.offset === 0); const cta = $('featCta'); if (cta) { cta.innerHTML = (today ? '' + today.open + ' of ' + today.cap + ' featured slots open today. ' : '') + 'Feature your link →'; const fb = $('featBuy'); if (fb) fb.addEventListener('click', e => { e.preventDefault(); setPane('campaigns'); const ct = $('cType'); if (ct) { ct.value = 'featured'; ct.dispatchEvent(new Event('change')); } }); } } catch (e) {} } catch (e) {} } // achievement badges: the milestone ladder as unlockable medals + share-to-image const BADGES = [ // ribbonY = vertical center of each badge's name ribbon (art differs per tier) { key: 'payouts', label: 'Spark', sub: 'payouts on', img: '/badges/badge-spark.jpg', ribbonY: 0.728 }, { key: 'firstBuyer', label: 'Surge', sub: 'first qualifying buyer', img: '/badges/badge-surge.jpg?v=2', ribbonY: 0.76 }, { key: 'level2', label: 'Circuit', sub: '2 qualifying buyers', img: '/badges/badge-circuit.jpg?v=2', ribbonY: 0.72 }, { key: 'level3', label: 'Nexus', sub: 'fully qualified', img: '/badges/badge-nexus.jpg?v=2', ribbonY: 0.73 } ]; let lastMilestones = []; function renderBadges(d) { const reached = d.milestonesReached || []; lastMilestones = reached; const strip = $('badgeStrip'); if (!strip) return; $('badgeCard').hidden = false; strip.innerHTML = BADGES.map(b => { const got = reached.includes(b.key); return '
' + '' + b.label + ' badge' + '
' + b.label + '
' + b.sub + '
' + (got ? '' : '
🔒 locked
') + '
'; }).join(''); strip.querySelectorAll('[data-badge]').forEach(btn => btn.addEventListener('click', () => shareBadge(btn.dataset.badge, d))); // celebrate anything newly granted this load if (d.milestonesGranted && d.milestonesGranted.length) { const total = d.milestonesGranted.reduce((s, g) => s + g.credited, 0); const names = d.milestonesGranted.map(g => (BADGES.find(b => b.key === g.key) || {}).label).filter(Boolean).join(', '); IAP.status('🏆 Achievement unlocked: ' + names + ' — +' + total + ' bonus credits!', 'ok'); } } // compose a shareable badge image on a canvas (zero-dep) and download it // composite the ornate badge template with the member's @username on the ribbon function shareBadge(key, d) { const b = BADGES.find(x => x.key === key); if (!b) return; const who = d.username ? d.username : d.memberId ? 'member #' + d.memberId : ''; const img = new Image(); img.onload = () => { const c = document.createElement('canvas'); c.width = img.width; c.height = img.height; const x = c.getContext('2d'); x.drawImage(img, 0, 0); if (who) { // name it on the blank ribbon: gold with a dark outline so it reads on any badge color x.textAlign = 'center'; x.textBaseline = 'middle'; x.font = 'bold ' + Math.round(c.width * 0.055) + 'px Sora, "Segoe UI", sans-serif'; const y = c.height * (b.ribbonY || 0.75); x.lineJoin = 'round'; x.lineWidth = Math.max(3, Math.round(c.width * 0.008)); x.strokeStyle = 'rgba(4,20,15,.9)'; x.strokeText(who, c.width / 2, y); x.fillStyle = '#ffd15c'; x.fillText(who, c.width / 2, y); x.textAlign = 'left'; x.textBaseline = 'alphabetic'; } try { c.toBlob(bl => { const a = document.createElement('a'); a.href = URL.createObjectURL(bl); a.download = 'instantadpay-' + b.label.toLowerCase() + '-badge.jpg'; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000); IAP.status('Your ' + b.label + ' badge is saved — share it anywhere.', 'ok'); }, 'image/jpeg', 0.9); } catch (e) { IAP.status('Could not generate the image.', 'bad'); } }; img.onerror = () => IAP.status('Could not load the badge art.', 'bad'); img.src = b.img; // same-origin, canvas stays untainted } // milestone stepper under "Your next move": lit nodes for what's done, // amber glow on the current target — the same ladder the contract pays function renderSteps(d) { const el = $('ncSteps'); if (!el) return; const b = d.buyerCount || 0; const steps = [ { label: 'Joined', sub: 'free account', hit: true }, { label: 'Spark', sub: 'payouts on', hit: !!d.memberId }, { label: 'Surge', sub: 'first buyer · 50%', hit: b >= 1 }, { label: 'Circuit', sub: '2 buyers · +20%', hit: b >= 2 }, { label: 'Nexus', sub: '5 buyers · +10%', hit: b >= 5 } ]; const cur = steps.findIndex(s => !s.hit); el.innerHTML = steps.map((s, i) => '
' + '' + (s.hit ? '✓' : i + 1) + '' + '' + s.label + '' + s.sub + '
').join(''); } // ── overview v3 charts: hand-rolled SVG/CSS, real data only ── const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' }; function bars(el, xel, data) { // data: [{v,label,tip,alt}] if (!el) return; const max = Math.max(1, ...data.map(d => d.v)); el.innerHTML = data.length ? data.map(d => '
').join('') : '
'.repeat(8); if (xel) xel.innerHTML = data.map(d => '' + esc(d.label) + '').join(''); } function donut(el, segs, center) { // segs sum to the ring; r=15.9155 → circumference 100 if (!el) return; const total = segs.reduce((s, x) => s + x.v, 0); let off = 25, out = ''; if (total > 0) for (const s of segs) { const len = s.v / total * 100; if (len <= 0 || s.color === 'none') { off -= Math.max(0, len); continue; } out += ''; off -= len; } out += '' + esc(center.big) + '' + '' + esc(center.small) + ''; el.innerHTML = out; } function legend(el, rows) { if (el) el.innerHTML = rows.map(r => '
' + esc(r.label) + ' ' + esc(r.v) + '
').join(''); } async function loadCharts(d) { // credit composition donut + today's viewing ring (live balances) try { const st = await (await fetch('/api/my/earn')).json(); if (!st.error) { const purchased = d.credits || 0, earned = st.earned || 0, wc = Math.min(d.welcomeCredits || 0, earned); $('dbCredits').textContent = (purchased + earned).toLocaleString(); $('dbCreditsSub').textContent = purchased.toLocaleString() + ' purchased · ' + earned.toLocaleString() + ' earned'; donut($('chDonut'), [{ v: purchased, color: CH.mint }, { v: wc, color: CH.amber }, { v: earned - wc, color: CH.cyan }], { big: (purchased + earned).toLocaleString(), small: 'spendable' }); legend($('chDonutLegend'), [ { color: CH.mint, label: 'Purchased', v: purchased.toLocaleString() }, { color: CH.amber, label: 'Welcome', v: wc.toLocaleString() }, { color: CH.cyan, label: 'Earned by viewing', v: Math.max(0, earned - wc).toLocaleString() }]); const done = Math.min(st.views || 0, st.target || 5), left = Math.max(0, (st.target || 5) - done); donut($('chRing'), [{ v: done, color: st.claimed ? CH.mint : CH.cyan }, { v: left, color: 'none' }], { big: done + '/' + (st.target || 5), small: st.claimed ? 'claimed' : 'ads viewed' }); legend($('chRingLegend'), [ { color: st.claimed ? CH.mint : CH.cyan, label: 'Viewed today', v: done }, { color: CH.track, label: 'To go', v: left }, { color: CH.amber, label: 'Claim pays', v: '+' + (st.claimCredits || 0) + ' credits' }]); } } catch (e) {} // recent on-chain payouts, sized to scale, oldest→newest try { let evs = []; if (d.memberId) { const a = await (await fetch('/api/my/activity')).json(); evs = (a.earnings || []).filter(e => e.amountWei).sort((x, y) => x.block - y.block).slice(-12); } bars($('chEarnBars'), $('chEarnX'), evs.map(e => ({ v: Number(BigInt(e.amountWei) / 1000000000000n) / 1e6, label: e.type === 'AwardPaid' ? 'award' : 'L' + (e.tier || 1), tip: IAP.fmtPol(e.amountWei) + ' POL', alt: e.type === 'AwardPaid' }))); if (!evs.length) $('chEarnSub').textContent = d.memberId ? 'no payouts yet — share your link' : 'activate a package to start earning'; } catch (e) {} // campaign delivery: impressions per campaign try { const r = await (await fetch('/api/my/campaigns')).json(); const cs = (r.campaigns || []).slice(0, 8); bars($('chCampBars'), $('chCampX'), cs.map(c => ({ v: c.imps || 0, label: String(c.name || c.type).slice(0, 9), tip: (c.name || c.type) + ': ' + (c.imps || 0) + ' imps · ' + (c.clicks || 0) + ' clicks', alt: c.type === 'text' }))); if (!cs.length) $('chCampSub').textContent = 'no campaigns yet — place your first ad'; } catch (e) {} } // ── live updates: subscribe to the chain event stream so the members area // reacts to on-chain changes (payouts, qualifications) without a refresh ── let MYID = 0; // synthesized sounds (no asset files; CSP-safe). cha-ching on a payment, pop on a message. function playSound(kind) { try { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; const ctx = window.__iapAC || (window.__iapAC = new AC()); if (ctx.state === 'suspended') ctx.resume(); const now = ctx.currentTime; const tone = (freq, at, dur, type, peak) => { const o = ctx.createOscillator(), g = ctx.createGain(); o.type = type || 'sine'; o.frequency.value = freq; g.gain.setValueAtTime(0.0001, now + at); g.gain.exponentialRampToValueAtTime(peak || 0.22, now + at + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, now + at + dur); o.connect(g).connect(ctx.destination); o.start(now + at); o.stop(now + at + dur + 0.02); }; if (kind === 'chaching') { tone(1318, 0, 0.34, 'sine', 0.25); tone(1760, 0.09, 0.4, 'sine', 0.25); } else { tone(680, 0, 0.16, 'triangle', 0.18); tone(1020, 0.05, 0.16, 'triangle', 0.16); } } catch (e) {} } function startLiveFeed() { if (window.__iapFeed || !window.EventSource) return; try { const es = new EventSource('/api/feed/live'); window.__iapFeed = es; es.onmessage = m => { let ev; try { ev = JSON.parse(m.data); } catch (e) { return; } handleLiveEvent(ev); }; // browser auto-reconnects on error; nothing to do } catch (e) {} } let liveRefreshT = null; function liveRefresh() { // debounce a burst of events into one refresh clearTimeout(liveRefreshT); liveRefreshT = setTimeout(() => { loadDashboard(); try { loadLineage(); } catch (e) {} }, 600); } function handleLiveEvent(ev) { if (!MYID || !ev || !ev.type) return; let toast = null, kind = 'ok'; let sound = null; if (ev.type === 'TierPaid' && ev.recipientId === MYID) { toast = '💸 You earned a level-' + ev.tier + ' payout of ' + IAP.fmtPol(ev.amountWei) + ' POL!'; sound = 'chaching'; } else if (ev.type === 'AwardPaid' && ev.toId === MYID) { toast = '💸 You received ' + IAP.fmtPol(ev.amountWei) + ' POL!'; sound = 'chaching'; } else if (ev.type === 'BuyerCounted' && ev.sponsorId === MYID) toast = '🎯 A referral just qualified — you now have ' + ev.newCount + ' qualifying buyer' + (ev.newCount === 1 ? '' : 's') + '!'; else if (ev.type === 'PassedUp' && ev.skippedId === MYID) { toast = '⚠️ A level-' + ev.tier + ' payout passed you by. Get qualified to catch these.'; kind = 'bad'; } else if (ev.type === 'Purchase' && ev.buyerId === MYID) toast = '✅ Purchase settled on-chain — your credits are updated.'; else if (ev.type === 'MemberActivated' && ev.sponsorId === MYID) toast = '🤝 A new member just activated in your line!'; if (toast) { IAP.status(toast, kind); if (sound) playSound(sound); liveRefresh(); } } // training center: videos + materials (admin-curated via data/training.json) async function loadTraining() { try { const r = await (await fetch('/api/training')).json(); const el = $('trainingList'); if (!el) return; const items = r.items || []; if (!items.length) { el.innerHTML = '

Training materials are being added. Check back soon.

'; return; } el.innerHTML = items.map(it => { const isVid = it.videoUrl && /\.(mp4|webm)(\?|$)/i.test(it.videoUrl); const media = isVid ? '' : ''; const links = []; if (it.videoUrl && !isVid) links.push('Watch'); if (it.docUrl) links.push('Open material'); return '

' + esc(it.title || 'Lesson') + '

' + (it.desc ? '

' + esc(it.desc) + '

' : '') + (media ? '

' + media + '

' : '') + (links.length ? '

' + links.join(' ') + '

' : '') + '
'; }).join(''); } catch (e) {} } // visual line tree on the Overview: YOU + three levels, qualified directs in gold async function loadLineTree(buyerCount) { try { const r = await (await fetch('/api/my/line')).json(); const el = $('lineTree'); if (!el) return; const levels = r.levels || []; const total = levels.reduce((n, L) => n + L.members.length, 0); if ($('treeSub')) $('treeSub').textContent = total ? total + ' across 3 levels' : 'share your link to grow'; const chip = (m, q) => '' + esc(String(m.name || 'M').replace(/^@/, '').slice(0, 12)) + ''; const rowFor = lvl => { const L = levels.find(x => x.level === lvl); const members = L ? L.members : []; const qn = lvl === 1 ? (buyerCount || 0) : 0; // highlight your qualified directs let html = members.map((m, i) => chip(m, i < qn)).join(''); if (lvl <= 2 && members.length < (lvl === 1 ? 2 : 4)) html += '+ open'; if (!html) html = '+ open'; return '
L' + lvl + '
' + html + '
'; }; el.innerHTML = '
YOU
' + rowFor(1) + rowFor(2) + rowFor(3); } catch (e) {} } async function loadDashboard() { try { const d = await (await fetch('/api/my/dashboard')).json(); if (d.error) return; chatSync(d); MYID = d.memberId || MYID; startLiveFeed(); if (!window.__lbChecked) { // daily login bonus, once per session (server guards once/day) window.__lbChecked = true; api('/api/my/login-bonus').then(r => { if (r && r.granted > 0) { IAP.status('🎁 Daily login bonus: +' + r.granted + ' credits' + (r.streak > 1 ? ' · ' + r.streak + '-day streak' : '') + '!', 'ok'); playSound('chaching'); setTimeout(loadDashboard, 900); } }).catch(() => {}); } 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; // trend chips: only real, computable facts const ec = $('dbEarnedChip'); if (ec && d.earnCount) { ec.hidden = false; ec.textContent = d.earnCount + ' instant payout' + (d.earnCount > 1 ? 's' : ''); } const bc = $('dbBuyersChip'); if (bc && d.memberId) { bc.hidden = false; bc.textContent = (d.buyerCount || 0) >= 5 ? 'level 3 open' : (d.buyerCount || 0) >= 2 ? 'level 2 open' : 'level 2 at 2 buyers'; } const tc = $('dbTeamChip'), wk = (d.referrals || []).filter(r => Date.now() - new Date(r.joined) < 6048e5).length; if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; } setInboxBadge(d.inboxUnread || 0); if (d.sponsorMsg) showSponsorModal(d.sponsorMsg); renderBadges(d); loadFeatured(); loadLineTree(d.buyerCount || 0); loadCharts(d); $('nextMove').textContent = nextMove(d); renderSteps(d); 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 => '' + String(r.name || r.email || '').replace(/[&<>]/g, '') + '' + '' + 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 = '🤝 ' + esc(r.name || r.email || 'A new member') + ' 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 + promo tools, personalized const link = location.origin + '/join/' + (d.username || d.refCode || d.memberId || ''); fillPromo(link, d); if (d.username) { // wall link rides the username const wl = location.origin + '/wall/' + d.username; $('wallLine').textContent = wl; $('wallCopy').hidden = false; $('wallOpen').hidden = false; $('wallOpen').href = '/wall/' + d.username; $('wallCopy').onclick = async () => { try { await navigator.clipboard.writeText(wl); IAP.status('Wall link copied.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the link text instead.', 'bad'); } }; } 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', 'promo', 'training', 'wallet', 'profile']; const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns', earn: 'Earn credits', earnings: 'Earnings', promo: 'Promo tools', training: 'Training', wallet: 'Wallet & account', profile: 'Profile' }; 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]; if (name === 'earn') setEarnSub(earnSub); // refresh whichever sub-tab is active if (name === 'profile') loadLineBanner(); if (name === 'line') { loadLineage(); loadUplineMessages(); } if (name === 'campaigns') ['cTarget', 'cImage', 'cVideoUrl'].forEach(id => { if ($(id)) $(id).value = ''; }); // no residual URL between visits if (name === 'training') loadTraining(); 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(); // one way in: email. A wallet-only session (no account) is sent back to the // email card with a finish-setup note; verifying the code links that wallet. const walletOnly = !!(me && me.signedIn && !me.email); const signedIn = me && me.signedIn && !walletOnly; $('authArea').hidden = !!signedIn; $('memberArea').hidden = !signedIn; if ($('mcFinish')) $('mcFinish').hidden = !walletOnly; if (!signedIn) return; if ($('adminLink')) $('adminLink').hidden = !me.isAdmin; // admin portal link, only for ADMIN_EMAIL 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(); // profile pane state $('pfCurrent').textContent = me.username ? 'Current username: @' + me.username : 'No username yet. Members see you as a number until you pick one.'; if (!$('pfUsername').value) $('pfUsername').value = me.username || ''; $('pfDetails').innerHTML = 'Email: ' + (me.email || 'none') + '
Wallet: ' + (me.address ? '' + me.address.slice(0, 10) + '…' + me.address.slice(-6) + '' : 'not linked yet') + '
On-chain member: ' + (me.memberId ? '#' + me.memberId : 'not yet'); // the share link works from day one; usernames make it a vanity link if (me.username || me.refCode || me.memberId) { $('inviteLine').textContent = location.origin + '/join/' + (me.username || 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; lastRates = r.rates; // populate the banner-size dropdown once (ids map to NAS width/height) if (r.bannerSizes && $('cSize') && !$('cSize').options.length) $('cSize').innerHTML = r.bannerSizes.map(s => '').join(''); soloHint(); $('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, solo ' + (r.rates.soloCostPerRecipient || 5) + 'cr/delivery'; 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.type === 'banner' && c.width ? ' ' + c.width + '×' + c.height + '' : '') + '' + c.imps.toLocaleString() + (c.impsNas ? ' +' + c.impsNas.toLocaleString() + ' network' : '') + '' + 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'); } })); el.querySelectorAll('button[data-topup]').forEach(b => b.addEventListener('click', async () => { const n = prompt('How many credits to add to this campaign? (buys more views)'); if (!n) return; try { const r = await api('/api/my/campaigns/' + b.dataset.topup + '/topup', { credits: Number(n) }); IAP.status('Added ' + r.added + ' credits' + (r.reactivated ? ' — campaign is live again.' : '.'), 'ok'); await loadCampaigns(); loadDashboard(); } catch (e) { IAP.status(e.message, 'bad'); } })); } catch (e) {} } let lastRates = null; function soloHint() { if (!lastRates || $('cType').value !== 'solo') return; const cost = lastRates.soloCostPerRecipient || 5; const n = Math.floor((Number($('cBudget').value) || 0) / cost); $('cSoloHint').textContent = cost + ' credits per guaranteed inbox delivery' + (n ? ' — this budget reaches ' + n + ' members' : '') + '. Readers earn ' + (lastRates.soloReadCredits || 2) + ' credits for a real read, so your message gets opened.'; } $('cBudget').addEventListener('input', soloHint); // rich solo editor: small toolbar over contenteditable (CSP allows no external editor); // the server whitelist-sanitizes whatever HTML arrives, this is just authoring comfort document.querySelectorAll('.ed-bar [data-cmd]').forEach(btn => btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand(btn.dataset.cmd, false, null); })); document.querySelectorAll('.ed-bar [data-block]').forEach(btn => btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand('formatBlock', false, btn.dataset.block); })); $('edLinkBtn').addEventListener('click', () => { const url = prompt('Link URL (https://…)'); if (!url) return; $('cSoloEd').focus(); document.execCommand('createLink', false, url); }); // inline media: upload, then drop the element at the cursor (BV-style) let mediaMode = 'image'; function insertHtmlAtCursor(html) { const ed = $('cSoloEd'); ed.focus(); if (!document.execCommand('insertHTML', false, html)) ed.insertAdjacentHTML('beforeend', html); } $('edImgBtn').addEventListener('click', () => { mediaMode = 'image'; $('cSoloFile').accept = 'image/png,image/jpeg,image/webp,image/gif'; $('cSoloFile').click(); }); $('edVidBtn').addEventListener('click', () => { mediaMode = 'video'; $('cSoloFile').accept = 'video/mp4,video/webm'; $('cSoloFile').click(); }); $('cSoloFile').addEventListener('change', async () => { const f = $('cSoloFile').files[0]; if (!f) return; $('edMediaInfo').textContent = 'Uploading ' + f.name + '…'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('edMediaInfo').textContent = r.error; $('cSoloFile').value = ''; return; } insertHtmlAtCursor(r.type === 'video' ? '


' : '


'); $('edMediaInfo').textContent = f.name + ' inserted'; } catch (e) { $('edMediaInfo').textContent = 'Upload failed. Try again.'; } $('cSoloFile').value = ''; }); // raw-text toggle: swap the WYSIWYG surface for the underlying HTML and back $('edRawBtn').addEventListener('click', () => { const ed = $('cSoloEd'), raw = $('cSoloRaw'); if (raw.hidden) { raw.value = ed.innerHTML; raw.hidden = false; ed.hidden = true; $('edRawBtn').textContent = 'Visual'; } else { ed.innerHTML = raw.value; ed.hidden = false; raw.hidden = true; $('edRawBtn').textContent = 'Raw text'; } }); $('cType').addEventListener('change', () => { const t = $('cType').value; $('cImageRow').hidden = t !== 'banner'; // only banners carry a creative; login frames its URL $('cSizeRow').hidden = t !== 'banner'; $('cTitleRow').hidden = t !== 'text' && t !== 'solo'; $('cBodyRow').hidden = t !== 'text'; $('cSoloRow').hidden = t !== 'solo'; $('cSoloHint').hidden = t !== 'solo'; $('cVideoRow').hidden = t !== 'video'; $('cFeaturedRow').hidden = t !== 'featured'; $('cVisitsRow').hidden = t !== 'visits'; // fixed-cost types derive their spend (featured = day slots, visits = flat pack), // so hide the free-form Budget field for them to avoid confusion $('cBudgetRow').hidden = (t === 'featured' || t === 'visits'); // solo ads have a real floor (5cr x 10 deliveries): default to 50 so 10 isn't rejected if (t === 'solo' && (!$('cBudget').value || Number($('cBudget').value) < 50)) $('cBudget').value = (lastRates && lastRates.soloCostPerRecipient ? lastRates.soloCostPerRecipient : 5) * 10; if (t === 'visits') visitHint(); $('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)'; soloHint(); if (t === 'video') videoHint(); if (t === 'featured') featHint(); }); let featStartDay = 0; // selected start-day offset (0 = today) async function featHint() { if (!lastRates || !lastRates.featuredDurations) return; if ($('cFeatDays') && !$('cFeatDays').options.length) $('cFeatDays').innerHTML = lastRates.featuredDurations.map(dys => '').join(''); let s = null; try { s = await (await fetch('/api/featured/stats')).json(); } catch (e) {} if (s && s.occupancy) { const grid = $('cFeatDaysGrid'); grid.innerHTML = s.occupancy.map(d => { const label = d.offset === 0 ? 'Today' : d.offset === 1 ? 'Tomorrow' : new Date(d.day + 'T00:00:00Z').toLocaleDateString(undefined, { weekday: 'short', month: 'numeric', day: 'numeric' }); const full = d.open <= 0; return '
' + '
' + label + '
' + d.count + '/' + d.cap + (full ? ' full' : ' left ' + d.open) + '
'; }).join(''); grid.querySelectorAll('.feat-day:not(.full)').forEach(el => el.addEventListener('click', () => { featStartDay = Number(el.dataset.off); featHint(); })); } const dys = Number($('cFeatDays').value) || (lastRates.featuredDurations[0]); const dayLabel = featStartDay === 0 ? 'today' : featStartDay === 1 ? 'tomorrow' : 'in ' + featStartDay + ' days'; $('cFeatHint').textContent = 'Runs ' + dys + ' day' + (dys > 1 ? 's' : '') + ' starting ' + dayLabel + ' for ' + (dys * lastRates.featuredPerDay) + ' credits. Max ' + (s ? s.slotsPerDay : 10) + ' links share any day.'; } document.addEventListener('change', e => { if (e.target && e.target.id === 'cFeatDays') featHint(); }); function visitHint() { if (!lastRates) return; const n = Number($('cVisitCount').value) || 0; const cost = n * (lastRates.visitCostPerVisit || 3); $('cVisitHint').textContent = (lastRates.visitCostPerVisit || 3) + ' credits per verified visit' + (n >= (lastRates.visitMinPack || 20) ? ' — ' + n + ' visits = ' + cost + ' credits' : ' (min ' + (lastRates.visitMinPack || 20) + ')') + '. Each is a unique member, dwell + human-check verified.'; } $('cVisitCount').addEventListener('input', visitHint); // video composer: tier dropdown + upload + price hint function videoHint() { if (!lastRates || !lastRates.videoTiers) return; if ($('cWatchSecs') && !$('cWatchSecs').options.length) $('cWatchSecs').innerHTML = lastRates.videoTiers.map(t => '').join(''); const tier = lastRates.videoTiers.find(t => t.secs === Number($('cWatchSecs').value)) || lastRates.videoTiers[0]; const n = tier ? Math.floor((Number($('cBudget').value) || 0) / tier.cost) : 0; $('cVideoHint').textContent = tier ? (tier.cost + ' credits per completed ' + tier.secs + 's view' + (n ? ' — this budget buys ' + n + ' views' : '') + '. Viewers earn ' + tier.reward + ' credits each, so they watch.') : ''; } document.addEventListener('change', e => { if (e.target && e.target.id === 'cWatchSecs') videoHint(); }); $('cBudget').addEventListener('input', () => { if ($('cType').value === 'video') videoHint(); }); $('cVideoUploadBtn').addEventListener('click', () => $('cVideoFile').click()); $('cVideoUrl').addEventListener('change', async () => { const url = $('cVideoUrl').value.trim(); $('cVideoInfo').textContent = url ? 'checking video…' : ''; cVidDims = url ? await probeVideoDims(url) : null; if (url && !cVidDims) $('cVideoInfo').textContent = 'could not read that video'; else { $('cVideoInfo').textContent = ''; showVidOrient(); } }); $('cVideoFile').addEventListener('change', async () => { const f = $('cVideoFile').files[0]; if (!f) return; $('cVideoInfo').textContent = 'Uploading ' + f.name + '… (large files take a moment)'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('cVideoInfo').textContent = r.error; $('cVideoFile').value = ''; return; } $('cVideoUrl').value = r.url; $('cVideoInfo').textContent = f.name + ' uploaded'; $('cVideoPrev').hidden = false; $('cVideoPrev').innerHTML = ''; cVidDims = await probeVideoDims(r.url); showVidOrient(); } catch (e) { $('cVideoInfo').textContent = 'Upload failed. Try again.'; } $('cVideoFile').value = ''; }); $('createCampBtn').addEventListener('click', busy2($('createCampBtn'), async () => { // in raw mode the source of truth is the textarea; sync it back first let soloBody = $('cSoloEd').innerHTML; if ($('cSoloRaw') && !$('cSoloRaw').hidden) soloBody = $('cSoloRaw').value; const t = $('cType').value; const isVideo = t === 'video', isFeat = t === 'featured'; if (isVideo && !cVidDims && $('cVideoUrl').value) cVidDims = await probeVideoDims($('cVideoUrl').value); await api('/api/my/campaigns', { type: t, name: $('cName').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, size: $('cSize').value, title: isVideo ? $('cVideoTitle').value : isFeat ? $('cFeatTitle').value : t === 'visits' ? $('cVisitTitle').value : $('cTitle').value, body: t === 'solo' ? soloBody : $('cBody').value, videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value), videoW: cVidDims ? cVidDims.w : null, videoH: cVidDims ? cVidDims.h : null, days: Number($('cFeatDays').value), startDay: featStartDay, count: Number($('cVisitCount').value), ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value, budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) }); IAP.status('Campaign is live. It starts serving right away.', 'ok'); // clear EVERY field so no target/creative carries into the next campaign ['cName', 'cBudget', 'cTarget', 'cImage', 'cTitle', 'cBody', 'cCtaLabel', 'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle'] .forEach(id => { if ($(id)) $(id).value = ''; }); $('cSoloEd').innerHTML = ''; if ($('cSoloRaw')) $('cSoloRaw').value = ''; $('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = ''; if ($('edMediaInfo')) $('edMediaInfo').textContent = ''; cVidDims = null; await loadCampaigns(); })); // defers the busy() lookup to click time (busy is declared below) function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); } // ── verified visits: open a member's site (new tab), dwell, human-check, earn ── const visState = { token: null, id: null, dwell: 8 }; async function loadVisitStatus() { try { const st = await (await fetch('/api/my/visits')).json(); if (st.error) return; $('vsProgress').textContent = 'today: ' + (st.status.count || 0) + ' / ' + st.status.cap + ' verified visits'; $('vsStartBtn').hidden = st.status.count >= st.status.cap; if (st.status.count >= st.status.cap) $('vsBox').innerHTML = 'That\'s today\'s visits. Come back tomorrow.'; } catch (e) {} } async function loadVisit() { $('vsCheck').hidden = true; $('vsVisit').hidden = true; $('vsHint').textContent = ''; let r = null; try { r = await (await fetch('/api/my/visits')).json(); } catch (e) {} if (!r || !r.ad) { $('vsBox').innerHTML = '' + (r && r.status && r.status.count >= r.status.cap ? 'That\'s today\'s visits. Come back tomorrow.' : 'No verified-visit packs are running right now. Check back soon.') + ''; return; } visState.token = r.token; visState.id = r.ad.id; visState.dwell = r.status.dwell || 8; $('vsBox').innerHTML = '' + esc(r.ad.title || 'Member site') + '
Open the site and stay ' + visState.dwell + 's.'; const visit = $('vsVisit'); visit.href = r.ad.url; visit.hidden = false; $('vsStartBtn').textContent = 'Loading…'; $('vsStartBtn').disabled = true; // opening the site starts the dwell; then we ask the human-check visit.onclick = () => { let left = visState.dwell; $('vsHint').textContent = 'Counting your visit: ' + left + 's'; const t = setInterval(async () => { left--; if (left > 0) { $('vsHint').textContent = 'Counting your visit: ' + left + 's'; return; } clearInterval(t); $('vsHint').textContent = 'One quick check to count the visit:'; let c = await (await fetch('/api/my/visitchallenge?token=' + visState.token)).json(); if (c.early) { await new Promise(r2 => setTimeout(r2, (c.wait || 1) * 1000 + 300)); c = await (await fetch('/api/my/visitchallenge?token=' + visState.token)).json(); } if (c.error) { $('vsHint').textContent = c.error; return; } renderVisitCheck(c); }, 1000); }; $('vsStartBtn').textContent = 'Next visit'; $('vsStartBtn').disabled = false; } function renderVisitCheck(c) { $('vsPrompt').textContent = 'Click the ' + c.prompt + ':'; const w = $('vsOpts'); w.innerHTML = ''; c.options.forEach((em, i) => { const b = document.createElement('button'); b.className = 'btn small sec'; b.style.marginRight = '6px'; b.textContent = em; b.addEventListener('click', () => answerVisit(i)); w.appendChild(b); }); $('vsCheck').hidden = false; } async function answerVisit(i) { const r = await (await fetch('/api/my/visitdone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: visState.token, answer: i }) })).json(); if (r.error) { if (r.retry) { const c = await (await fetch('/api/my/visitchallenge?token=' + visState.token)).json(); if (!c.error) return renderVisitCheck(c); } $('vsHint').textContent = r.error; $('vsCheck').hidden = true; return; } $('vsCheck').hidden = true; $('vsHint').textContent = '+' + r.credited + ' credits — visit counted. Load the next one.'; $('vsProgress').textContent = 'today: ' + (r.status.count || 0) + ' / ' + (r.status.cap || 0) + ' verified visits'; IAP.status('+' + r.credited + ' credits for a verified visit.', 'ok'); loadDashboard(); } $('vsStartBtn').addEventListener('click', () => loadVisit()); // ── watch-to-earn videos: escape-proof player, server-clock reward ── const vidState = { token: null, secs: 0, maxSeen: 0, done: false, credited: false }; // detected dimensions of the video being created — portrait goes to Shorts, landscape to Watch videos let cVidDims = null; function probeVideoDims(url) { return new Promise(resolve => { if (!url) return resolve(null); const v = document.createElement('video'); v.preload = 'metadata'; v.muted = true; let done = false; const fin = r => { if (!done) { done = true; resolve(r); } }; v.onloadedmetadata = () => fin(v.videoWidth && v.videoHeight ? { w: v.videoWidth, h: v.videoHeight } : null); v.onerror = () => fin(null); setTimeout(() => fin(null), 8000); v.src = url; }); } function showVidOrient() { const el = $('cVideoInfo'); if (!el) return; if (!cVidDims) return; const portrait = cVidDims.h > cVidDims.w; el.textContent = (el.textContent ? el.textContent + ' · ' : '') + (portrait ? 'portrait — shows in the Shorts reel' : 'landscape — shows in the Watch videos tab'); } async function loadVideoStatus() { try { const st = await (await fetch('/api/my/videos?orientation=landscape')).json(); if (st.error) return; $('vidProgress').textContent = 'today: ' + (st.status.count || 0) + ' / ' + st.status.cap + ' videos watched'; if (st.status.left <= 0) { $('vidBox').innerHTML = 'That is today\'s video set. Come back tomorrow.'; $('vidStartBtn').hidden = true; return; } $('vidStartBtn').hidden = false; } catch (e) {} } async function loadVideoAd() { let r = null; try { r = await (await fetch('/api/my/videos?orientation=landscape')).json(); } catch (e) {} if (!r || !r.ad) { $('vidBox').innerHTML = '' + (r && r.status && r.status.left <= 0 ? 'That is today\'s video set. Come back tomorrow.' : 'No member videos are live right now. Check back when a campaign is running.') + ''; $('vidWrap').hidden = true; return; } const ad = r.ad; vidState.token = r.token; vidState.secs = ad.watchSecs; vidState.maxSeen = 0; vidState.done = false; vidState.credited = false; $('vidBox').hidden = true; $('vidWrap').hidden = false; $('vidCta').hidden = true; $('vidHint').textContent = ad.title ? 'Now playing: ' + ad.title : ''; const p = $('vidPlayer'); p.src = ad.videoUrl; p.currentTime = 0; // escape-proof: no seeking past what's been watched; track max reached p.onseeking = () => { if (p.currentTime > vidState.maxSeen + 0.5) p.currentTime = vidState.maxSeen; }; p.ontimeupdate = () => { if (p.currentTime > vidState.maxSeen) vidState.maxSeen = p.currentTime; const left = Math.max(0, Math.ceil(vidState.secs - vidState.maxSeen)); $('vidTimer').textContent = left > 0 ? 'Watch ' + left + 's more to earn' : 'Watch time met — finishing…'; if (!vidState.done && vidState.maxSeen >= vidState.secs) { vidState.done = true; completeVideo(ad); } }; $('vidStartBtn').textContent = 'Playing…'; $('vidStartBtn').disabled = true; try { await p.play(); } catch (e) { $('vidStartBtn').disabled = false; $('vidStartBtn').textContent = 'Tap to play'; } $('vidCta').href = ad.ctaUrl; $('vidCta').textContent = ad.ctaLabel || 'Learn more'; $('vidCta').hidden = false; } async function completeVideo(ad) { if (vidState.credited) return; vidState.credited = true; try { const r = await (await fetch('/api/my/videowatch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: vidState.token }) })).json(); if (r.error) { IAP.status(r.error, 'bad'); $('vidHint').textContent = r.error; } else if (r.credited) { IAP.status('+' + r.credited + ' credits earned for watching.', 'ok'); $('vidHint').textContent = '+' + r.credited + ' credits earned. Load the next one.'; loadDashboard(); } else $('vidHint').textContent = 'That video just ran out of budget — load another.'; $('vidProgress').textContent = 'today: ' + ((r.status && r.status.count) || 0) + ' / ' + ((r.status && r.status.cap) || 0) + ' videos watched'; } catch (e) { IAP.status('Could not confirm that watch. Try the next one.', 'bad'); } $('vidStartBtn').disabled = false; $('vidStartBtn').textContent = 'Next video'; } $('vidStartBtn').addEventListener('click', () => loadVideoAd()); // presence enforcement: pause the watch-to-earn video when the tab/window loses focus, // resume when it comes back — the viewer must stay on the page for the watch to complete document.addEventListener('visibilitychange', () => { const p = $('vidPlayer'); if (!p || !p.src) return; if (document.hidden) p.pause(); else if (!vidState.done) p.play().catch(() => {}); }); window.addEventListener('blur', () => { const p = $('vidPlayer'); if (p && p.src) p.pause(); }); window.addEventListener('focus', () => { const p = $('vidPlayer'); if (p && p.src && !vidState.done) p.play().catch(() => {}); }); // ── unmissable sponsor-message modal on sign-in ── function showSponsorModal(msg) { if (!$('msgModal')) return; $('mmFrom').textContent = 'A message from ' + (msg.fromName || 'your sponsor'); $('mmSubject').textContent = msg.subject || ''; $('mmBody').innerHTML = msg.body || ''; // server-sanitized $('msgModal').hidden = false; $('mmAck').onclick = async () => { $('msgModal').hidden = true; try { await fetch('/api/my/messages/' + msg.id + '/read', { method: 'POST' }); } catch (e) {} }; } // ── downline lineage + sponsor broadcast + upline messages ── async function loadLineage() { try { const r = await (await fetch('/api/my/line')).json(); const el = $('lineageWrap'); if (r.error || !r.levels || !r.levels.every) return; if (!r.levels.length || !r.levels.some(L => L.members.length)) { el.innerHTML = '

No one in your downline yet. Share your link and it fills in here.

'; return; } el.innerHTML = r.levels.map(L => !L.members.length ? '' : '
Level ' + L.level + ' · ' + L.members.length + (L.level === 1 ? ' direct' : '') + '
' + L.members.map(m => '
' + esc(m.name) + '' + (m.email ? '' + esc(m.email) + '' : '#' + m.memberId + '') + '' + new Date(m.joined).toLocaleDateString() + '' + (m.email ? '' : '') + '
').join('') + '
').join(''); el.querySelectorAll('[data-cemail]').forEach(b => b.addEventListener('click', () => openConvo(b.dataset.cemail, b.dataset.cname))); } catch (e) {} } async function loadUplineMessages() { try { const r = await (await fetch('/api/my/messages')).json(); if (r.error) return; const card = $('upMsgCard'), list = $('upList'); if (!r.items || !r.items.length) { card.hidden = true; return; } card.hidden = false; list.innerHTML = r.items.map(i => '
' + '
' + esc(i.subject) + '' + '' + esc(i.fromName) + ' · ' + new Date(i.sent).toLocaleDateString() + '
' + '
' + (i.body || '') + '
').join(''); // opening the pane marks them read for (const i of r.items) if (!i.read) fetch('/api/my/messages/' + i.id + '/read', { method: 'POST' }).catch(() => {}); } catch (e) {} } // broadcast composer editor (its own small rich editor, server sanitizes) document.querySelectorAll('[data-bc]').forEach(b => b.addEventListener('click', () => { $('bcEd').focus(); document.execCommand(b.dataset.bc, false, null); })); document.querySelectorAll('[data-bcblock]').forEach(b => b.addEventListener('click', () => { $('bcEd').focus(); document.execCommand('formatBlock', false, b.dataset.bcblock); })); if ($('bcLinkBtn')) $('bcLinkBtn').addEventListener('click', () => { const u = prompt('Link URL (https://…)'); if (u) { $('bcEd').focus(); document.execCommand('createLink', false, u); } }); if ($('bcSendBtn')) $('bcSendBtn').addEventListener('click', busy2($('bcSendBtn'), async () => { const r = await api('/api/my/broadcast', { scope: $('bcScope').value, subject: $('bcSubject').value, body: $('bcEd').innerHTML }); IAP.status('Broadcast sent to ' + r.sent + ' member' + (r.sent === 1 ? '' : 's') + '.', 'ok'); $('bcSubject').value = ''; $('bcEd').innerHTML = ''; $('bcHint').textContent = 'Sent. You can send your next broadcast in 24 hours.'; })); // ── solo-ads inbox: list, read view, dwell-gated read reward ── let ibTimer = null; function setInboxBadge(n) { for (const id of ['inboxBadge', 'inboxBadge2']) { const b = $(id); if (b) { b.hidden = !n; b.textContent = n; } } } // Earn credits sub-tabs: Watch ads | Inbox let earnSub = 'watch'; function setEarnSub(which) { earnSub = ['inbox', 'videos', 'visits'].includes(which) ? which : 'watch'; const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'), vs = $('earn-visits'); if (w) w.hidden = earnSub !== 'watch'; if (i) i.hidden = earnSub !== 'inbox'; if (v) v.hidden = earnSub !== 'videos'; if (vs) vs.hidden = earnSub !== 'visits'; document.querySelectorAll('.subtabs [data-earn]').forEach(b => b.classList.toggle('on', b.dataset.earn === earnSub)); if (earnSub === 'inbox') loadInbox(); else if (earnSub === 'videos') loadVideoStatus(); else if (earnSub === 'visits') loadVisitStatus(); else earnRefresh(); } document.querySelectorAll('.subtabs [data-earn]').forEach(b => b.addEventListener('click', () => setEarnSub(b.dataset.earn))); // promo tools: pill menu switches between posts / swipes / banners / wall / videos function setPromoSub(name) { const ids = ['posts', 'text', 'swipe', 'banners', 'wall', 'objections', 'videos']; if (!ids.includes(name)) name = 'posts'; ids.forEach(id => { const el = $('promo-' + id); if (el) el.hidden = id !== name; }); document.querySelectorAll('.promo-pills [data-promo]').forEach(b => { b.classList.toggle('on', b.dataset.promo === name); b.setAttribute('aria-selected', b.dataset.promo === name ? 'true' : 'false'); }); try { localStorage.setItem('iap.promoSub', name); } catch (e) {} } document.querySelectorAll('.promo-pills [data-promo]').forEach(b => b.addEventListener('click', () => setPromoSub(b.dataset.promo))); try { setPromoSub(localStorage.getItem('iap.promoSub') || 'posts'); } catch (e) { setPromoSub('posts'); } async function loadInbox() { try { const r = await (await fetch('/api/my/inbox')).json(); if (r.error) return; $('ibRewardNote').textContent = '+' + r.readCredits + ' credits per real read (up to ' + r.readCap + ' rewarded reads a day)'; const el = $('ibList'); $('inboxReadCard').hidden = true; $('inboxListCard').hidden = false; setInboxBadge(r.unread); if (!r.items.length) { el.innerHTML = '

No solo ads yet. When a member sends one, it lands here — and reading it pays.

'; return; } el.innerHTML = ''; for (const i of r.items) { const d = document.createElement('div'); d.className = 'ib-row' + (i.read ? '' : ' unread'); d.innerHTML = '' + (i.rewarded ? 'claimed' : i.read ? '' : 'new') + '' + new Date(i.delivered).toLocaleDateString() + ''; d.querySelector('.sub').textContent = i.subject || '(no subject)'; d.querySelector('.from').textContent = 'from ' + (i.fromName || 'a member'); d.addEventListener('click', () => openInboxItem(i.id)); el.appendChild(d); } } catch (e) {} } async function openInboxItem(id) { try { const r = await (await fetch('/api/my/inbox/' + id)).json(); if (r.error) { IAP.status(r.error, 'bad'); return; } $('inboxListCard').hidden = true; $('inboxReadCard').hidden = false; $('ibSubject').textContent = r.subject || '(no subject)'; $('ibMeta').textContent = 'from ' + (r.fromName || 'a member') + ' · ' + new Date(r.delivered).toLocaleString(); $('ibBody').innerHTML = r.body || ''; // whitelist-sanitized on the server at submit const mv = $('ibMedia'); mv.hidden = !r.mediaUrl; mv.innerHTML = !r.mediaUrl ? '' : r.mediaType === 'video' ? '' : 'attachment'; // the read reward needs BOTH the dwell AND an actual click-through to the // advertiser — the visit is what makes the ad worth the sender's credits const visit = $('ibVisit'); visit.href = r.url; visit.textContent = r.ctaLabel || 'Learn more'; visit.target = '_blank'; const btn = $('ibClaimBtn'); clearInterval(ibTimer); if (r.rewarded) { btn.hidden = true; visit.classList.remove('cta-need'); $('ibHint').textContent = 'Read reward already claimed for this one.'; return; } let dwellDone = false; let visited = !!r.visited; btn.hidden = false; btn.disabled = true; visit.classList.toggle('cta-need', !visited); let left = r.dwell; const refresh = () => { if (!dwellDone) { btn.textContent = 'Read it — claim in ' + left + 's'; return; } if (!visited) { btn.textContent = 'Claim +' + r.reward + ' — visit the ad first'; btn.disabled = true; return; } btn.textContent = 'Claim +' + r.reward + ' credits'; btn.disabled = false; }; $('ibHint').textContent = 'Read the message, click through to the advertiser, then claim your credits.'; refresh(); // countdown pauses off-tab; the server separately enforces the dwell on its own clock ibTimer = setInterval(() => { if (document.visibilityState !== 'visible' || !document.hasFocus()) return; left -= 1; if (left > 0) { refresh(); return; } clearInterval(ibTimer); dwellDone = true; refresh(); }, 1000); // clicking the CTA records the visit (and counts the advertiser's click) visit.onclick = async () => { visited = true; visit.classList.remove('cta-need'); try { await fetch('/api/my/inbox/' + id + '/visit', { method: 'POST' }); } catch (e2) {} refresh(); }; btn.onclick = async () => { try { const c = await api('/api/my/inbox/' + id + '/claim'); IAP.status('+' + c.credited + ' credits for reading. They spend like any earned credits.', 'ok'); btn.hidden = true; $('ibHint').textContent = 'Claimed. Head back for the next one.'; loadDashboard(); } catch (e2) { IAP.status(e2.message, 'bad'); } }; } catch (e) {} } $('ibBack').addEventListener('click', ev => { ev.preventDefault(); clearInterval(ibTimer); loadInbox(); }); // ── promo tools: content + rendering live in promo.js (IAPPromo.fill) ── function fillPromo(link, me) { if (window.IAPPromo) IAPPromo.fill(link, me || {}); // banner kit const bwrap = $('promoBanners'); if (bwrap && !bwrap.dataset.filled) { bwrap.dataset.filled = '1'; const BANNERS = [ { file: 'iap-hero-1200x630.png', size: '1200×630 (social / hero)' }, { file: 'iap-1080x1080.png', size: '1080×1080 (Instagram / Facebook square)' }, { file: 'iap-1080x1920.png', size: '1080×1920 (story / reel)' }, { file: 'iap-1280x720.png', size: '1280×720 (Telegram group post)' }, { file: 'iap-728x90.png', size: '728×90 (leaderboard)' }, { file: 'iap-336x280.png', size: '336×280 (large rectangle)' }, { file: 'iap-300x250.png', size: '300×250 (rectangle)' }, { file: 'iap-125x125.png', size: '125×125 (square button)' }, { file: 'iap-468x60.png', size: '468×60 (banner)' }, { file: 'iap-160x600.png', size: '160×600 (wide skyscraper)' }, { file: 'iap-120x600.png', size: '120×600 (skyscraper)' }, { file: 'iap-320x50.svg', size: '320×50 (mobile leaderboard)' } ]; bwrap.innerHTML = ''; for (const b of BANNERS) { const url = location.origin + '/banners/' + b.file; const d = document.createElement('div'); d.className = 'pb-item'; d.innerHTML = 'InstantAdPay banner ' + b.size + '' + '
' + b.size + '
'; const dl = document.createElement('a'); dl.className = 'btn small'; dl.textContent = 'Download'; dl.href = '/banners/' + b.file; dl.setAttribute('download', b.file); d.querySelector('.pb-row').appendChild(dl); const btn = document.createElement('button'); btn.className = 'btn small sec'; btn.textContent = 'Copy image URL'; btn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(url); IAP.status('Banner URL copied.', 'ok'); } catch (e) { IAP.status('Copy failed.', 'bad'); } }); d.querySelector('.pb-row').appendChild(btn); bwrap.appendChild(d); } } } // ── profile ── (busy2 defers the busy lookup past its TDZ) $('pfSaveBtn').addEventListener('click', busy2($('pfSaveBtn'), async () => { const r = await api('/api/my/profile', { username: $('pfUsername').value }); IAP.status('You are @' + r.account.username + ' now.', 'ok'); await render(); })); // ── in-dashboard package buying ── const PKG = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' }; // Card on-ramp: buy POL with a card via MoonPay, delivered to the buyer's own // wallet. Signed + wallet-prefilled once MoonPay keys are set; generic page // otherwise. The site never touches funds — MoonPay is merchant of record. async function openMoonpay(pol) { try { let addr = ''; try { const me = await (await fetch('/api/me')).json(); addr = me.address || ''; } catch (e) {} const q = '/api/moonpay-url?pol=' + encodeURIComponent(pol || '') + (addr ? '&address=' + encodeURIComponent(addr) : ''); const r = await (await fetch(q)).json(); if (r && r.url) { window.open(r.url, '_blank', 'noopener'); IAP.status(r.signed ? 'MoonPay opened in a new tab with your wallet address pre-filled. Choose POL on Polygon, finish the purchase, then come back and buy your package.' : 'MoonPay opened in a new tab. Choose POL on the Polygon network and paste your own wallet address as the destination, then come back.', 'ok'); } } catch (e) { IAP.status('Could not open MoonPay: ' + ((e && e.message) || e), 'bad'); } } 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; // the buyer's credits are read by the member id of their LINKED wallet. // If they only connected a wallet (e.g. for the faucet) but never linked // it, link it first (one signature) so the purchase's credits show up. // retry these through transient failures — mobile drops in-flight // fetches when the page returns from the wallet app-switch const jretry = async url => { let err; for (let i = 0; i < 4; i++) { try { return await (await fetch(url)).json(); } catch (e) { err = e; await new Promise(s => setTimeout(s, 500 * (i + 1))); } } throw err; }; const meNow = await jretry('/api/me'); if (!meNow.address) { IAP.status('Link your wallet first — one quick signature…'); await IAPWallet.signIn(); } const spNow = await jretry('/api/sponsor'); // pre-flight: stop early if the POL is not there. Trust Wallet also hard-blocks any // transaction that spends most of the balance ("drain your wallet"), so Trust users // get a heads-up first; other wallets go straight to the confirmation. try { const need = BigInt(b.dataset.cost) + BigInt(b.dataset.cost) / 50n; // same 2% pad as buy() const bal = await IAPWallet.balance(IAPWallet.address() || meNow.address); if (bal < need) { IAP.status('That wallet holds ' + IAP.fmtPol(bal.toString()) + ' POL, but this package needs about ' + IAP.fmtPol(need.toString()) + ' POL plus a little for gas. Top it up and try again.', 'bad'); return; } const pct = Number(need * 100n / bal); const isTrust = /trust/i.test(IAPWallet.walletName() || ''); if (isTrust && pct > 55 && !confirm('Heads up for Trust Wallet users: this purchase uses about ' + pct + '% of the POL in your wallet, and Trust Wallet refuses transactions that spend most of the balance (it shows a "drain your wallet" warning with only Stop and go back).' + '\n\n' + 'Options: pick a smaller package first, add some POL, or connect a different wallet (MetaMask, Phantom, SafePal). Extra POL always stays yours.' + '\n\n' + 'Try it anyway?')) { IAP.status('Purchase paused. Pick a smaller package, add POL, or connect another wallet, then try again.', 'ok'); return; } } catch (e) { /* balance read failed: let the wallet decide */ } 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; } })); // brand new to crypto? buy POL with a card, sent straight to the wallet const builder = products.find(p => p.priceCents === 5000) || products[products.length - 1]; const needPol = (builder && builder.costWei) ? Math.max(30, Math.ceil(Number(builder.costWei) / 1e18) + 3) : 30; const cta = document.createElement('div'); cta.style.cssText = 'grid-column:1/-1;margin-top:10px;text-align:center'; cta.innerHTML = '

New to crypto? Buy POL with a debit or credit card, Apple Pay, or Google Pay. It lands straight in your own wallet, and this site never touches your money.

' + ''; wrap.appendChild(cta); const mb = $('moonpayBtn'); if (mb) mb.addEventListener('click', () => openMoonpay(needPol)); } 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() { // each view happens full screen in its own tab: /view/ frames the // advertiser's site, runs the countdown, then a human check credits it 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 || !r.viewUrl) { 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; } openAdOverlay(r.viewUrl); box.innerHTML = 'Watch the countdown and pass the quick check. Your view credits itself and this page updates right away.'; $('earnStartBtn').textContent = 'View next ad'; } // In-page ad overlay: no new tab (mobile and in-app wallet browsers handle tabs // badly), no window.close needed. The /view page runs the dwell + check inside, // messages back when it credits or when the user is done. function openAdOverlay(url) { closeAdOverlay(); const back = document.createElement('div'); back.id = 'adOverlay'; back.style.cssText = 'position:fixed;inset:0;z-index:100000;background:#050b09;display:flex;flex-direction:column'; const x = document.createElement('button'); x.type = 'button'; x.setAttribute('aria-label', 'Close ad'); x.textContent = '✕'; x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:40px;height:40px;border-radius:50%;border:1px solid rgba(255,255,255,.25);background:rgba(6,10,16,.85);color:#fff;font-size:20px;font-weight:700;cursor:pointer'; x.addEventListener('click', closeAdOverlay); const ifr = document.createElement('iframe'); ifr.src = url; ifr.style.cssText = 'flex:1;width:100%;border:0;background:#050b09'; back.appendChild(ifr); back.appendChild(x); document.body.appendChild(back); document.body.style.overflow = 'hidden'; } function closeAdOverlay() { const m = $('adOverlay'); if (m) m.remove(); document.body.style.overflow = ''; earnRefresh(); loadDashboard(); } window.addEventListener('message', e => { if (e.origin !== location.origin || !e.data) return; if (e.data.t === 'iap-view-close') closeAdOverlay(); else if (e.data.t === 'iap-view-done') { earnRefresh(); loadDashboard(); } }); $('earnStartBtn').addEventListener('click', () => earnShowAd()); // the viewer tab pings localStorage when a view credits; refresh instantly window.addEventListener('storage', e => { if (e.key === 'iap-view-done') { earnRefresh(); loadDashboard(); } }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && $('pane-earn') && !$('pane-earn').hidden) earnRefresh(); }); $('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 () => { const r = await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value, newsletter: !!($('nlOptin') && $('nlOptin').checked) }); IAP.status('You are in.', 'ok'); if (r.created && !(r.account && r.account.username)) await showOnboard(); // pick a username first if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); })); })(); // new-member onboarding: choose a username, optionally a bio (both skippable) function showOnboard() { return new Promise(resolve => { const m = $('onboardModal'); if (!m) return resolve(); m.hidden = false; $('obErr').hidden = true; const done = () => { m.hidden = true; resolve(); }; $('obSkip').onclick = done; $('obSave').onclick = async () => { const u = $('obUsername').value.trim(); try { if (u) await api('/api/my/profile', { username: u }); const bio = $('obBio').value.trim(); if (bio) await api('/api/my/profile-details', { bio }); done(); } catch (e) { $('obErr').hidden = false; $('obErr').textContent = e.message || 'Could not save that. Try a different username.'; } }; }); } $('signupBtn').addEventListener('click', busy($('signupBtn'), async () => { const r = await api('/api/signup', { email: $('suEmail').value, password: $('suPass').value, newsletter: !!($('nlOptin') && $('nlOptin').checked) }); IAP.status('Welcome aboard. You are in.', 'ok'); if (!(r.account && r.account.username)) await showOnboard(); await render(); })); $('loginBtn').addEventListener('click', busy($('loginBtn'), async () => { await api('/api/login', { email: $('liEmail').value, password: $('liPass').value }); IAP.status('Logged in.', 'ok'); if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); })); $('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(); })); if ($('faucetBtn')) $('faucetBtn').addEventListener('click', busy($('faucetBtn'), async () => { IAP.status('Connect your wallet first…'); const addr = await IAPWallet.connect(); let copied = false; try { await navigator.clipboard.writeText(addr); copied = true; } catch (e) {} window.open('https://faucet.polygon.technology/', '_blank', 'noopener'); $('faucetInfo').innerHTML = 'Your address ' + addr.slice(0, 8) + '…' + addr.slice(-6) + '' + (copied ? ' is copied' : '') + '. On the faucet, choose Polygon Amoy, paste your address, and request POL. Then come back and buy.'; IAP.status('Faucet opened in a new tab. Choose Polygon Amoy, paste your address, and request test POL.', 'ok'); })); if ($('wcDisconnect')) $('wcDisconnect').addEventListener('click', busy($('wcDisconnect'), async () => { // fire-and-forget: don't gate the reload on disconnect resolving (it can // stall). The reload drops all in-memory wallet state and the picker returns. IAPWallet.disconnect().catch(() => {}); $('wcDisconnectInfo').textContent = 'Disconnecting — reloading so you can pick a different wallet…'; IAP.status('Disconnecting — reloading…', 'ok'); setTimeout(() => location.reload(), 900); })); $('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(); }); // ── welcome tour (viral banner gauntlet): a new member meets their 3-level // upline's sites, 10 focus-paused seconds each, then unlocks welcome credits. // Same three levels the contract pays — the tour IS the org chart. async function showGauntlet() { try { const g = await (await fetch('/api/my/gauntlet')).json(); if (!g.pending || !g.slides || !g.slides.length) return false; const gate = $('gauntGate'); gate.hidden = false; for (let i = 0; i < g.slides.length; i++) { const s = g.slides[i]; $('ggWho').textContent = 'Position ' + (i + 1) + ': ' + s.name + (i === 0 ? ' — the person who invited you' : ''); $('ggProgress').textContent = 'Meeting your line: ' + (i + 1) + ' of ' + g.slides.length; $('ggFrame').hidden = false; $('ggFrame').src = s.targetUrl; let left = g.dwell || 10; $('ggTimer').textContent = left + 's'; await new Promise(done => { const t = setInterval(() => { if (document.visibilityState !== 'visible' || !document.hasFocus()) return; left -= 1; $('ggTimer').textContent = Math.max(0, left) + 's'; if (left <= 0) { clearInterval(t); done(); } }, 1000); }); } $('ggFrame').src = 'about:blank'; $('ggFrame').hidden = true; // avoid a white about:blank panel once the tour is done $('ggTimer').textContent = '✓'; $('ggWho').textContent = 'That is your line. When you grow, they earn — and yours starts the day you share.'; $('ggClaim').hidden = false; await new Promise(done => { $('ggClaim').onclick = async () => { try { const r = await api('/api/my/gauntlet/complete', { token: g.token }); IAP.status('+' + r.credited + ' welcome credits unlocked. They spend on real campaigns.', 'ok'); } catch (e) { IAP.status(e.message, 'bad'); } done(); }; }); gate.hidden = true; $('ggClaim').hidden = true; return true; } catch (e) { return false; } } // ── line banner (profile): the member's slot on welcome tours + their wall ── function fillLineBanner(a) { if (!a) return; if (a.lineTargetUrl) $('lbTarget').value = a.lineTargetUrl; if (a.lineBannerUrl) { $('lbBanner').value = a.lineBannerUrl; $('lbPreview').hidden = false; $('lbPreview').innerHTML = 'line banner'; } $('lbCurrent').textContent = a.lineTargetUrl ? 'Live: your next three levels meet ' + a.lineTargetUrl + ' on their welcome tour.' : 'Not set yet. Until you set one, your tour slot is skipped.'; } async function loadLineBanner() { try { const a = await (await fetch('/api/me')).json(); fillLineBanner(a); fillProfileDetails(a); } catch (e) {} } const SOCIALS = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website']; function fillProfileDetails(a) { if (!a) return; if (a.bio) $('pfBio').value = a.bio; if (a.avatarUrl) { const p = $('pfAvatarPrev'); p.src = a.avatarUrl; p.hidden = false; } let soc = {}; try { soc = a.socials ? JSON.parse(a.socials) : {}; } catch (e) {} for (const p of SOCIALS) if ($('soc-' + p)) $('soc-' + p).value = soc[p] || ''; if (a.username) { const link = location.origin + '/wall/' + a.username; $('pfBioLink').textContent = link; $('pfViewBio').hidden = false; $('pfViewBio').href = '/wall/' + a.username; } } let pfAvatar; // pending avatar url $('pfAvatarBtn').addEventListener('click', () => $('pfAvatarFile').click()); $('pfAvatarFile').addEventListener('change', async () => { const f = $('pfAvatarFile').files[0]; if (!f) return; $('pfAvatarInfo').textContent = 'Uploading…'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('pfAvatarInfo').textContent = r.error; $('pfAvatarFile').value = ''; return; } pfAvatar = r.url; $('pfAvatarInfo').textContent = 'Uploaded — save to apply.'; const p = $('pfAvatarPrev'); p.src = r.url; p.hidden = false; } catch (e) { $('pfAvatarInfo').textContent = 'Upload failed.'; } $('pfAvatarFile').value = ''; }); $('pfDetailsSave').addEventListener('click', busy2($('pfDetailsSave'), async () => { const body = { bio: $('pfBio').value, socials: {} }; for (const p of SOCIALS) body.socials[p] = ($('soc-' + p) && $('soc-' + p).value.trim()) || ''; if (pfAvatar) body.avatarUrl = pfAvatar; const r = await api('/api/my/profile-details', body); IAP.status('Profile saved.', 'ok'); if (r.account) fillProfileDetails(r.account); })); $('lbUploadBtn').addEventListener('click', () => $('lbFile').click()); $('lbFile').addEventListener('change', async () => { const f = $('lbFile').files[0]; if (!f) return; $('lbUpInfo').textContent = 'Uploading…'; try { const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('lbUpInfo').textContent = r.error; $('lbFile').value = ''; return; } $('lbBanner').value = r.url; $('lbUpInfo').textContent = 'Uploaded.'; $('lbPreview').hidden = false; $('lbPreview').innerHTML = 'line banner'; } catch (e) { $('lbUpInfo').textContent = 'Upload failed. Try again.'; } $('lbFile').value = ''; }); $('lbSaveBtn').addEventListener('click', busy2($('lbSaveBtn'), async () => { const r = await api('/api/my/linebanner', { bannerUrl: $('lbBanner').value, targetUrl: $('lbTarget').value }); IAP.status('Line banner saved. Your next three levels will meet it.', 'ok'); if (r.account) fillLineBanner(r.account); })); // ── login ad interstitial (ClickBaitPays pattern): after a successful // sign-in the sponsor card appears; "Open Ad" opens the CTA link in a NEW // tab (a real, counted click) while the timer counts down on THIS page — // deliberately not paused, the member is expected to be in the ad tab. // At zero, "Go to dashboard" appears. async function showLoginAd() { try { const { ad } = await (await fetch('/api/ads/slot?type=login')).json(); if (!ad || !ad.targetUrl) return; const gate = $('loginGate'); const openBtn = $('lgOpen'); const goBtn = $('lgContinue'); const timer = $('lgTimer'); $('lgCreative').innerHTML = ad.imageUrl ? 'sponsor ad' : '' + (ad.title ? String(ad.title).replace(/[&<>]/g, '') : 'Visit today\'s sponsor') + ''; $('lgStatus').textContent = 'Login ad sponsor — click "Open Ad" to begin.'; timer.hidden = true; goBtn.hidden = true; openBtn.disabled = false; gate.hidden = false; await new Promise(done => { openBtn.onclick = () => { window.open(ad.targetUrl, '_blank'); // the click relay counts it openBtn.disabled = true; $('lgStatus').textContent = 'Ad open in a new tab. View it and come back — the timer runs here.'; let left = ad.dwell || 10; timer.hidden = false; timer.textContent = 'Time remaining: ' + left + 's'; const t = setInterval(() => { left -= 1; if (left > 0) { timer.textContent = 'Time remaining: ' + left + 's'; return; } clearInterval(t); timer.textContent = 'Time is up'; timer.classList.add('done'); $('lgStatus').textContent = 'Thanks for the look. Your dashboard is ready.'; goBtn.hidden = false; }, 1000); goBtn.onclick = () => done(); }; }); gate.hidden = true; $('lgTimer').classList.remove('done'); } catch (e) {} } // ── SPONSOR CHAT: two-way threads, presence-aware, with a slide-in drawer ── let CHAT_ME = null, CHAT_OTHER = null, CHAT_LASTID = 0, CHAT_POLL = null, CHAT_CANMUTE = false, CHAT_IMUTE = false, CHAT_SPONSOR = null, CHAT_LAST_UNREAD = 0; function chatSync(d) { CHAT_ME = d.email || CHAT_ME; CHAT_SPONSOR = (d.sponsor && d.sponsor.email) ? d.sponsor : null; const link = $('chatMenuBtn'); if (link && link.closest('.bo-links')) link.closest('.bo-links').hidden = !d.email; setChatBadge(d.chatUnread || 0); CHAT_LAST_UNREAD = d.chatUnread || 0; const tog = $('chatAvailToggle'); if (tog) tog.checked = d.chatAvailable !== false; // Overview quick action + My line card: message-your-sponsor entry points const qs = $('qaMsgSponsor'); if (qs) { if (CHAT_SPONSOR) { qs.hidden = false; qs.dataset.email = CHAT_SPONSOR.email; qs.dataset.name = CHAT_SPONSOR.name || 'your sponsor'; } else qs.hidden = true; } const card = $('sponsorMsgCard'); if (card) { card.hidden = !CHAT_SPONSOR; if (CHAT_SPONSOR && $('sponsorMsgName')) $('sponsorMsgName').textContent = CHAT_SPONSOR.name || 'your sponsor'; } } function setChatBadge(n) { const b = $('chatNavBadge'); if (b) { b.hidden = !n; b.textContent = n > 9 ? '9+' : n; } } function stopPoll() { if (CHAT_POLL) { clearInterval(CHAT_POLL); CHAT_POLL = null; } } function startPoll() { stopPoll(); CHAT_POLL = setInterval(() => pullThread(false), 4000); } function autoGrow(el) { el.style.height = 'auto'; el.style.height = Math.min(120, el.scrollHeight) + 'px'; } function closeDrawer() { stopPoll(); if ($('chatDrawer')) $('chatDrawer').hidden = true; CHAT_OTHER = null; loadDashboard(); } async function openThreads() { const dr = $('chatDrawer'); if (!dr) return; dr.hidden = false; $('chatConvo').hidden = true; $('chatThreads').hidden = false; $('chatBack').hidden = true; $('chatMute').hidden = true; $('chatDot').hidden = true; $('chatTitle').textContent = 'Messages'; $('chatStatus').textContent = ''; stopPoll(); CHAT_OTHER = null; $('chatThreads').innerHTML = '

Loading…

'; try { const r = await (await fetch('/api/my/chat/threads')).json(); const list = r.threads || []; // always offer "message your sponsor" up top when they have one and no thread yet const hasSponsorThread = CHAT_SPONSOR && list.some(t => t.email === CHAT_SPONSOR.email); const sponsorRow = (CHAT_SPONSOR && !hasSponsorThread) ? '
' + '' + '
' + esc(CHAT_SPONSOR.name) + '
' + '
Your sponsor · tap to message
' : ''; if (!list.length && !sponsorRow) { $('chatThreads').innerHTML = '
No conversations yet. You can message anyone in your line from “My line”.
'; return; } $('chatThreads').innerHTML = sponsorRow + list.map(t => '
' + '' + '
' + esc(t.name) + '
' + '
' + (t.last.fromMe ? 'You: ' : '') + esc((t.last.body || '').slice(0, 64)) + '
' + (t.unread ? '' + t.unread + '' : '') + '
').join(''); $('chatThreads').querySelectorAll('.chat-thread').forEach(el => el.addEventListener('click', () => openConvo(el.dataset.email, el.dataset.name))); } catch (e) { $('chatThreads').innerHTML = '
Could not load messages.
'; } } async function openConvo(email, name) { if (!email) return; CHAT_OTHER = email; CHAT_LASTID = 0; const dr = $('chatDrawer'); if (!dr) return; dr.hidden = false; $('chatThreads').hidden = true; $('chatConvo').hidden = false; $('chatBack').hidden = false; $('chatDot').hidden = false; $('chatTitle').textContent = name || email; $('chatStatus').textContent = '…'; $('chatMsgs').innerHTML = ''; $('chatBanner').hidden = true; $('chatInput').disabled = false; $('chatSend').disabled = false; await pullThread(true); startPoll(); setTimeout(() => { const i = $('chatInput'); if (i) i.focus(); }, 60); } function renderMsgs(msgs) { const box = $('chatMsgs'); if (!box) return; const atBottom = box.scrollTop + box.clientHeight >= box.scrollHeight - 60; for (const m of msgs) { if (m.id <= CHAT_LASTID) continue; CHAT_LASTID = Math.max(CHAT_LASTID, m.id); const d = document.createElement('div'); d.className = 'cbub ' + (m.fromMe ? 'me' : 'them'); d.textContent = m.body; const t = document.createElement('span'); t.className = 'ct-time'; t.textContent = new Date(m.sent).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); d.appendChild(t); box.appendChild(d); } if (atBottom) box.scrollTop = box.scrollHeight; } async function pullThread(reset) { if (!CHAT_OTHER) return; try { const r = await (await fetch('/api/my/chat/thread?with=' + encodeURIComponent(CHAT_OTHER) + '&after=' + (reset ? 0 : CHAT_LASTID))).json(); if (r.error) { $('chatBanner').hidden = false; $('chatBanner').textContent = r.error; return; } if (reset) { $('chatMsgs').innerHTML = ''; CHAT_LASTID = 0; } renderMsgs(r.messages || []); $('chatDot').className = 'pres-dot' + (r.online ? ' on' : ''); $('chatStatus').textContent = r.online ? 'active now' : (r.available ? 'away · will get your note' : 'not taking live chats · leave a note'); CHAT_CANMUTE = !!r.canMute; CHAT_IMUTE = !!r.iMute; const mb = $('chatMute'); mb.hidden = !CHAT_CANMUTE; mb.textContent = CHAT_IMUTE ? 'Unmute' : 'Mute'; const blocked = !!r.blocked; $('chatInput').disabled = blocked; $('chatSend').disabled = blocked; if (blocked) { $('chatBanner').hidden = false; $('chatBanner').textContent = 'They are not accepting messages from you right now.'; } else if (CHAT_IMUTE) { $('chatBanner').hidden = false; $('chatBanner').textContent = 'You muted this member. Unmute to let them message you again.'; } else $('chatBanner').hidden = true; } catch (e) {} } async function chatSend() { const inp = $('chatInput'); if (!inp) return; const text = inp.value.trim(); if (!text || !CHAT_OTHER) return; $('chatSend').disabled = true; try { const r = await api('/api/my/chat/send', { to: CHAT_OTHER, body: text }); inp.value = ''; autoGrow(inp); renderMsgs([r.message]); const box = $('chatMsgs'); box.scrollTop = box.scrollHeight; } catch (e) { IAP.status(e.message || 'Could not send.', 'bad'); } finally { $('chatSend').disabled = false; inp.focus(); } } async function toggleMute() { if (!CHAT_OTHER) return; try { const r = await api('/api/my/chat/mute', { email: CHAT_OTHER, muted: !CHAT_IMUTE }); CHAT_IMUTE = r.muted; $('chatMute').textContent = CHAT_IMUTE ? 'Unmute' : 'Mute'; pullThread(false); } catch (e) { IAP.status(e.message, 'bad'); } } (function chatInit() { if (!$('chatDrawer')) return; const on = (id, ev, fn) => { const el = $(id); if (el) el.addEventListener(ev, fn); }; on('chatMenuBtn', 'click', e => { if (e && e.preventDefault) e.preventDefault(); openThreads(); }); on('chatClose', 'click', closeDrawer); on('chatBack', 'click', openThreads); on('chatMute', 'click', toggleMute); on('chatSend', 'click', chatSend); const inp = $('chatInput'); if (inp) { inp.addEventListener('input', () => autoGrow(inp)); inp.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chatSend(); } }); } on('qaMsgSponsor', 'click', () => { const q = $('qaMsgSponsor'); if (q.dataset.email) openConvo(q.dataset.email, q.dataset.name || 'your sponsor'); }); on('lineMsgSponsor', 'click', () => { if (CHAT_SPONSOR) openConvo(CHAT_SPONSOR.email, CHAT_SPONSOR.name || 'your sponsor'); }); on('chatAvailToggle', 'change', async () => { const t = $('chatAvailToggle'); try { await api('/api/my/chat/available', { available: t.checked }); IAP.status(t.checked ? 'You are available to chat with your line.' : 'Live chat off. People can still leave you a note.', 'ok'); } catch (e) { IAP.status(e.message, 'bad'); t.checked = !t.checked; } }); // presence heartbeat + new-message notifier: pop + toast when unread rises setInterval(async () => { try { const r = await (await fetch('/api/my/ping')).json(); const n = r.chatUnread || 0; if (n > CHAT_LAST_UNREAD) { playSound('pop'); IAP.status('💬 New message from your team.', 'ok'); } CHAT_LAST_UNREAD = n; setChatBadge(n); } catch (e) {} }, 20000); })(); // ad surfaces: a banner greets the sign-in screen; members see live // banner + text placements inside the back office (they ARE the audience) IAP.adSlot('banner', 'adSlotLogin'); IAP.adSlot('banner', 'adSlotOverview'); IAP.adSlot('banner', 'adSlotSide', { width: 125, height: 125 }); // square button ad in the sidebar render(); })();