Files
instantadpay/public/assets/my.js
T

566 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = '<b>Testnet rehearsal</b> · ' + c.chainName;
}
} catch (e) {}
async function api(path, body) {
const r = await (await fetch(path, { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })).json();
if (r.error) throw new Error(r.error);
return r;
}
function nextMove(d) {
if (!d.address) return 'Grab your link below and start sharing today. Then link your wallet (one free signature) so every payment can lock to you before your people start buying.';
if (!d.memberId) return 'Your wallet is linked. Switch on payouts above (one free transaction) and every purchase in your line pays you the moment it happens.';
if (d.buyerCount === 0) return 'Payouts are on and your link is live. Your next milestone: your first buyer of $20 or more. Every direct pays you 50 percent from their very first package.';
if (d.buyerCount === 1) return 'One qualifying buyer down, one to go. Your next $20+ buyer unlocks level 2: 20 percent of everything your people’s people buy.';
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
}
// ── overview v3 charts: hand-rolled SVG/CSS, real data only ──
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 => '<div class="bar' + (d.alt ? ' alt' : '') + (d.v ? '' : ' empty') + '" style="height:'
+ Math.max(4, Math.round(d.v / max * 100)) + '%" data-v="' + esc(d.tip) + '"></div>').join('')
: '<div class="bar empty" style="height:4%"></div>'.repeat(8);
if (xel) xel.innerHTML = data.map(d => '<span>' + esc(d.label) + '</span>').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 = '<circle cx="21" cy="21" r="15.9155" fill="none" stroke="' + CH.track + '" stroke-width="5"></circle>';
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 += '<circle cx="21" cy="21" r="15.9155" fill="none" stroke="' + s.color + '" stroke-width="5" '
+ 'stroke-dasharray="' + len + ' ' + (100 - len) + '" stroke-dashoffset="' + off + '"></circle>';
off -= len;
}
out += '<text x="21" y="20.5" text-anchor="middle" fill="currentColor" font-size="6.5" font-weight="800" class="donut-center">' + esc(center.big) + '</text>'
+ '<text x="21" y="26.5" text-anchor="middle" fill="#8ba69c" font-size="3.1">' + esc(center.small) + '</text>';
el.innerHTML = out;
}
function legend(el, rows) {
if (el) el.innerHTML = rows.map(r => '<div><i style="background:' + r.color + '"></i>'
+ esc(r.label) + ' <b>' + esc(r.v) + '</b></div>').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) {}
}
async function loadDashboard() {
try {
const d = await (await fetch('/api/my/dashboard')).json();
if (d.error) return;
const wc = d.welcomeCredits || 0;
$('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString();
$('dbCreditsSub').textContent = wc ? (d.credits || 0).toLocaleString() + ' purchased · ' + wc + ' welcome' : '';
$('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0');
$('dbBuyers').textContent = d.buyerCount || 0;
$('dbTeam').textContent = (d.referrals || []).length;
// 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'; }
loadCharts(d);
$('nextMove').textContent = nextMove(d);
$('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%';
const wrap = $('rosterWrap');
if ((d.referrals || []).length) {
$('rosterEmpty').hidden = true;
let t = wrap.querySelector('table');
if (t) t.remove();
t = document.createElement('table');
t.className = 'roster';
t.innerHTML = d.referrals.map(r => '<tr><td>' + String(r.name || r.email || '').replace(/[&<>]/g, '') + '</td>'
+ '<td>' + new Date(r.joined).toLocaleDateString() + '</td>'
+ '<td><span class="badge' + (r.status === 'joined free' ? ' amber' : '') + '">' + r.status + '</span></td></tr>').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 = '<span>🤝 ' + r.email + ' joined your line</span><span class="tx muted small">'
+ new Date(r.joined).toLocaleDateString() + '</span>';
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.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', 'wallet', 'profile'];
const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns',
earn: 'Earn credits', earnings: 'Earnings', promo: 'Promo tools', 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];
document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer
if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name);
}
document.querySelectorAll('.bo-menu [data-pane]').forEach(b =>
b.addEventListener('click', () => setPane(b.dataset.pane)));
document.querySelectorAll('.qa [data-goto]').forEach(b =>
b.addEventListener('click', () => setPane(b.dataset.goto)));
const qaCopy = document.getElementById('qaCopyInvite');
if (qaCopy) qaCopy.addEventListener('click', async () => {
const link = document.getElementById('inviteLine').textContent;
if (!link || !link.startsWith('http')) { setPane('line'); return; }
try { await navigator.clipboard.writeText(link); IAP.status('Invite link copied.', 'ok'); }
catch (e) { setPane('line'); }
});
window.addEventListener('hashchange', () => setPane(location.hash.slice(1)));
if ($('boBurger')) $('boBurger').addEventListener('click', () =>
document.getElementById('memberArea').classList.toggle('side-open'));
async function render() {
const me = await IAP.refreshNavWallet();
const signedIn = me && me.signedIn;
$('authArea').hidden = !!signedIn;
$('memberArea').hidden = !signedIn;
if (!signedIn) return;
setPane(location.hash.slice(1) || 'overview');
$('campGate').hidden = !!me.memberId;
$('earnGate').hidden = !!me.memberId;
loadDashboard();
const who = [];
if (me.email) who.push(me.email);
if (me.address) who.push('wallet <span class="mono">' + me.address.slice(0, 8) + '…' + me.address.slice(-6) + '</span>');
else who.push('no wallet linked yet');
if (me.memberId) who.push('on-chain <b>member #' + me.memberId + '</b>'
+ (me.onchainSponsorId ? ', sponsored by #' + me.onchainSponsorId : ''));
else if (me.sponsorId) who.push('invited by member #' + me.sponsorId);
$('posLine').innerHTML = who.join('<br>');
$('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') + '<br>Wallet: '
+ (me.address ? '<span class="mono">' + me.address.slice(0, 10) + '…' + me.address.slice(-6) + '</span>' : 'not linked yet')
+ '<br>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 = '<b>' + bc + '</b> qualifying buyer(s) referred<br>'
+ (bc >= 5 ? '<span class="badge">Level 3 unlocked: full three-level earnings</span>'
: bc >= 2 ? '<span class="badge">Level 2 unlocked</span> · ' + (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) '
+ '<b>before your people start buying</b>: 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 = '<div class="row muted">' + empty + '</div>'; return; }
for (const ev of evs) el.appendChild(IAP.feedRow(ev, c));
};
fill('earnFeed', a.earnings, 'No payouts yet. They appear here the moment one lands.');
fill('refFeed', a.referrals, 'No referral activity yet. Share your invite link.');
fill('buyFeed', a.purchases, 'No purchases from your wallet yet.');
} catch (e) {}
}
async function loadCampaigns() {
try {
const r = await (await fetch('/api/my/campaigns')).json();
if (r.error) return;
$('rateLine').textContent = 'Available to spend: ' + r.availableCredits.toLocaleString()
+ (r.earnedCredits ? ' (' + r.purchasedCredits.toLocaleString() + ' purchased + ' + r.earnedCredits + ' earned)' : '')
+ ' credits · rates: banner ' + r.rates.bannerCreditsPerBatch + 'cr/' + r.rates.bannerBatch
+ ' views, text ' + r.rates.textCreditsPerBatch + 'cr/' + r.rates.textBatch
+ ' views, login ' + r.rates.loginCreditsPerDay + 'cr/day';
const el = $('campList');
el.innerHTML = '';
if (!r.campaigns.length) { el.innerHTML = '<p class="muted small">No campaigns yet. Launch your first below.</p>'; return; }
const tbl = document.createElement('div');
tbl.className = 'tablewrap';
tbl.innerHTML = '<table><thead><tr><th>Name</th><th>Type</th><th class="num">Views</th><th class="num">Clicks</th>'
+ '<th class="num">Spent</th><th class="num">Budget</th><th>Status</th><th></th></tr></thead><tbody>'
+ r.campaigns.map(c => '<tr><td><b>' + c.name + '</b></td><td>' + c.type + '</td>'
+ '<td class="num">' + c.imps.toLocaleString() + '</td><td class="num">' + c.clicks + '</td>'
+ '<td class="num">' + c.spent + '</td><td class="num">' + c.budget + '</td>'
+ '<td>' + (c.status === 'out' ? '<span class="badge amber">budget spent</span>' : c.status) + '</td>'
+ '<td>' + (c.status === 'active' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="pause">Pause</button>'
: c.status === 'paused' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="resume">Resume</button>' : '')
+ '</td></tr>').join('') + '</tbody></table>';
el.appendChild(tbl);
el.querySelectorAll('button[data-camp]').forEach(b => b.addEventListener('click', async () => {
try { await api('/api/my/campaigns/' + b.dataset.camp + '/' + b.dataset.act); await loadCampaigns(); }
catch (e) { IAP.status(e.message, 'bad'); }
}));
} catch (e) {}
}
$('cType').addEventListener('change', () => {
const t = $('cType').value;
$('cImageRow').hidden = t === 'text';
$('cTitleRow').hidden = t !== 'text';
$('cBodyRow').hidden = t !== 'text';
});
$('createCampBtn').addEventListener('click', busy2($('createCampBtn'), async () => {
await api('/api/my/campaigns', { type: $('cType').value, name: $('cName').value,
targetUrl: $('cTarget').value, imageUrl: $('cImage').value,
title: $('cTitle').value, body: $('cBody').value, budget: Number($('cBudget').value) });
IAP.status('Campaign is live. It starts serving right away.', 'ok');
$('cName').value = ''; $('cBudget').value = '';
await loadCampaigns();
}));
// defers the busy() lookup to click time (busy is declared below)
function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); }
// ── 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)));
}
// ── 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' };
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 = '<div class="name">' + (PKG[p.id] || 'Package ' + p.id) + '</div>'
+ '<div class="price">$' + Math.round(p.priceCents / 100) + '</div>'
+ '<div class="cr">' + p.creditAmount.toLocaleString() + ' credits</div>'
+ '<div class="bonus">' + (bonus > 0 ? '+' + bonus.toLocaleString() + ' bonus credits' : '&nbsp;') + '</div>'
+ '<div class="pol">' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL right now' : 'paused') + '</div>'
+ '<button class="btn small" data-id="' + p.id + '" data-cost="' + (p.costWei || '') + '"'
+ (p.costWei ? '' : ' disabled') + '>Buy</button>';
wrap.appendChild(div);
}
wrap.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', async () => {
try {
b.disabled = true;
const spNow = await (await fetch('/api/sponsor')).json();
IAP.status('Confirm the purchase in your wallet…');
const r = await IAPWallet.buy(Number(b.dataset.id), spNow.sponsorId || 0, b.dataset.cost);
if (r.status !== '0x1' && r.receipt && r.receipt.status !== '0x1') throw new Error('Transaction reverted.');
IAP.status('Purchase settled on-chain. Credits are in your account.', 'ok');
await render();
loadBuyTiles();
} catch (e) { IAP.status('Purchase failed: ' + ((e && e.message) || e), 'bad'); }
finally { b.disabled = false; }
}));
} catch (e) {}
}
loadBuyTiles();
// ── earn-by-viewing: daily ad set with dwell, then claim ──
const earnState = { types: ['banner', 'text'], i: 0, timer: null };
async function earnRefresh() {
try {
const st = await (await fetch('/api/my/earn')).json();
if (st.error) return null;
$('earnProgress').textContent = 'today: ' + st.views + ' / ' + st.target + ' ads viewed';
$('earnBalance').textContent = st.earned + ' earned credits';
const done = st.views >= st.target;
$('earnClaimBtn').hidden = !(done && !st.claimed);
if (st.claimed) $('earnHint').textContent = 'Claimed for today. Come back tomorrow, or put those credits to work in Campaigns.';
else if (done) $('earnHint').textContent = 'Set complete. Claim your ' + st.claimCredits + ' credits.';
return st;
} catch (e) { return null; }
}
async function earnShowAd() {
const type = earnState.types[earnState.i++ % earnState.types.length];
let r = null;
try { r = await (await fetch('/api/my/earnview?type=' + type)).json(); } catch (e) {}
const box = $('earnAdBox');
if (!r || !r.ad) {
await earnRefresh();
box.innerHTML = '<span class="muted small">' + (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.') + '</span>';
return;
}
const ad = r.ad;
box.innerHTML = ad.imageUrl
? '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><img src="' + ad.imageUrl + '" alt="member ad" style="max-height:200px"></a>'
: '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow" style="font-size:17px"><b>' + ad.title + '</b>' + (ad.body ? '<br><span class="muted">' + ad.body + '</span>' : '') + '</a>';
const btn = $('earnNextBtn');
btn.hidden = false;
btn.disabled = true;
// the countdown only runs while this tab is visible AND focused; the
// server separately enforces the full dwell on its own clock
let left = r.status.dwell;
btn.textContent = 'Next ad (' + left + 's)';
clearInterval(earnState.timer);
earnState.timer = setInterval(async () => {
if (document.visibilityState !== 'visible' || !document.hasFocus()) {
btn.textContent = 'Paused: stay on this tab (' + left + 's)';
return;
}
left -= 1;
if (left > 0) { btn.textContent = 'Next ad (' + left + 's)'; return; }
clearInterval(earnState.timer);
btn.textContent = 'Next ad';
const v = await (await fetch('/api/my/adview', { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: r.token }) })).json();
if (v.error) { IAP.status(v.error, 'bad'); btn.disabled = false; return; }
btn.disabled = false;
await earnRefresh();
}, 1000);
}
$('earnStartBtn').addEventListener('click', () => { $('earnStartBtn').hidden = true; earnShowAd(); });
$('earnNextBtn').addEventListener('click', earnShowAd);
$('earnClaimBtn').addEventListener('click', async () => {
try {
const r = await api('/api/my/claim');
IAP.status('+' + r.credited + ' credits earned. Spend them in Campaigns.', 'ok');
await earnRefresh();
loadDashboard();
} catch (e) { IAP.status(e.message, 'bad'); }
});
const busy = (btn, fn) => async () => {
try { btn.disabled = true; await fn(); }
catch (e) { IAP.status((e && e.message) || String(e), 'bad'); }
finally { btn.disabled = false; }
};
// passwordless (feature-flagged on config.emailAuth): code replaces passwords
(async () => {
const cfg = await IAP.getConfig();
if (!cfg.emailAuth) return;
$('passCards').hidden = true;
$('magicCard').hidden = false;
const start = busy($('mcSendBtn'), async () => {
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
$('mcCodeRow').hidden = false;
$('mcVerifyBtn').hidden = false;
$('mcSendBtn').hidden = true;
$('mcResend').hidden = false;
if (r.devCode) { $('mcCode').value = r.devCode; IAP.status('Dev mode: code filled in for you.', 'ok'); }
else IAP.status('Code sent. Check your inbox (and spam, the first time).', 'ok');
$('mcCode').focus();
});
$('mcSendBtn').addEventListener('click', start);
$('mcResend').addEventListener('click', busy($('mcResend'), async () => {
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
if (r.devCode) $('mcCode').value = r.devCode;
IAP.status('Fresh code sent.', 'ok');
}));
$('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => {
await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value });
IAP.status('You are in.', 'ok');
await render();
}));
})();
$('signupBtn').addEventListener('click', busy($('signupBtn'), async () => {
await api('/api/signup', { email: $('suEmail').value, password: $('suPass').value });
IAP.status('Welcome aboard. You are in.', 'ok');
await render();
}));
$('loginBtn').addEventListener('click', busy($('loginBtn'), async () => {
await api('/api/login', { email: $('liEmail').value, password: $('liPass').value });
IAP.status('Logged in.', 'ok');
await render();
}));
$('walletSigninLink').addEventListener('click', async e => {
e.preventDefault();
try {
IAP.status('Check your wallet for the free sign-in signature…');
await IAPWallet.signIn();
IAP.status('Signed in with your wallet.', 'ok');
await render();
} catch (err) { IAP.status((err && err.message) || String(err), 'bad'); }
});
$('linkBtn').addEventListener('click', busy($('linkBtn'), async () => {
IAP.status('Check your wallet for the free link signature…');
await IAPWallet.signIn(); // server binds the wallet to the signed-in email account
IAP.status('Wallet linked. Earnings pay there from now on.', 'ok');
await render();
}));
$('activateBtn').addEventListener('click', busy($('activateBtn'), async () => {
const me = await (await fetch('/api/me')).json();
IAP.status('Confirm the free activation in your wallet…');
const r = await IAPWallet.activate(me.sponsorId || 0);
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
IAP.status('Payouts are on. Your invite link is live.', 'ok');
await render();
}));
$('copyInvite').addEventListener('click', async () => {
try { await navigator.clipboard.writeText($('inviteLine').textContent); IAP.status('Link copied.', 'ok'); }
catch (e) { IAP.status('Copy failed. Select and copy the link text.', 'bad'); }
});
$('logoutLink').addEventListener('click', async e => {
e.preventDefault();
await fetch('/api/auth/logout', { method: 'POST' });
IAP.status('Logged out.', 'ok');
await render();
});
// ad surfaces: login ads greet the sign-in screen; members see live
// banner + text placements inside the back office (they ARE the audience)
IAP.adSlot('login', 'adSlotLogin');
IAP.adSlot('banner', 'adSlotOverview');
IAP.adSlot('text', 'adSlotSide');
render();
})();