// 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); 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(); const signedIn = me && me.signedIn; $('authArea').hidden = !!signedIn; $('memberArea').hidden = !signedIn; 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))); 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: Branded Voice copy, personalized with the member link ── const PROMO_POSTS = [ 'A membership site where money is handled by code, not people. Every purchase splits instantly to sponsor wallets on the Polygon blockchain. Nothing to withdraw. The money just lands in your wallet. Plus you earn ad credits for viewing ads while you\'re there. {{LINK}}', 'No withdrawal button. Think about that. A smart contract on Polygon splits every payment the second it hits. 50% to the sponsor. 20% to the next level. 10% to the next. Lands straight in your own wallet. No button to push. No waiting. Just money where it belongs. See how it works: {{LINK}}', 'Every payment is public on the blockchain. You can watch the ledger move in real time. Every split, every wallet, every transaction. Nothing hidden. Nothing you have to take on faith. That\'s the difference between a platform that asks for trust and one where trust isn\'t needed. See for yourself: {{LINK}}' ]; const PROMO_SWIPE = { subject: 'Your Wallet Gets Paid Instantly..', body: 'You know the usual drill. Someone buys on your link, you wait for a payout. Maybe days. Maybe an approval hold. Maybe a "your account is under review."\n\nInstantAdPay doesn\'t work like that.\n\nA smart contract on the Polygon blockchain handles every purchase the second it happens. 50% to the sponsor. 20% to the next level. 10% to the one after that. 20% to the platform. Each split lands directly in your own wallet. No withdrawal button. No "request payout." No approval queue.\n\nThe money just shows up.\n\nYou can watch every transaction on the public ledger. Real time. Anyone can verify it.\n\nFree to join. Packages from $5 to $250. No income promises. It\'s advertising, not investing.\n\n{{LINK}}' }; function promoBlock(text) { const div = document.createElement('div'); div.className = 'promo-block'; div.textContent = text; const btn = document.createElement('button'); btn.className = 'btn small sec'; btn.textContent = 'Copy'; btn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(text); IAP.status('Copied. Paste it anywhere.', 'ok'); } catch (e) { IAP.status('Copy failed. Select the text instead.', 'bad'); } }); div.appendChild(btn); return div; } function fillPromo(link) { const posts = $('promoPosts'); if (!posts || posts.dataset.filled === link) return; posts.dataset.filled = link; posts.innerHTML = ''; for (const p of PROMO_POSTS) posts.appendChild(promoBlock(p.replace('{{LINK}}', link))); const sw = $('promoSwipeWrap'); sw.innerHTML = ''; sw.appendChild(promoBlock('Subject: ' + PROMO_SWIPE.subject + '\n\n' + PROMO_SWIPE.body.replace('{{LINK}}', link))); // 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-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 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'); 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(); })); $('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'); if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad 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(); })); 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(); })();