// 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 + '
' + 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) =>
'
' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL right now' : 'paused') + '
'
+ '';
wrap.appendChild(div);
}
wrap.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', async () => {
try {
b.disabled = true;
// the buyer's credits are read by the member id of their LINKED wallet.
// If they only connected a wallet (e.g. for the faucet) but never linked
// it, link it first (one signature) so the purchase's credits show up.
// retry these through transient failures — mobile drops in-flight
// fetches when the page returns from the wallet app-switch
const jretry = async url => { let err; for (let i = 0; i < 4; i++) { try { return await (await fetch(url)).json(); } catch (e) { err = e; await new Promise(s => setTimeout(s, 500 * (i + 1))); } } throw err; };
const meNow = await jretry('/api/me');
if (!meNow.address) {
IAP.status('Link your wallet first — one quick signature…');
await IAPWallet.signIn();
}
const spNow = await jretry('/api/sponsor');
// pre-flight: stop early if the POL is not there. Trust Wallet also hard-blocks any
// transaction that spends most of the balance ("drain your wallet"), so Trust users
// get a heads-up first; other wallets go straight to the confirmation.
try {
const need = BigInt(b.dataset.cost) + BigInt(b.dataset.cost) / 50n; // same 2% pad as buy()
const bal = await IAPWallet.balance(IAPWallet.address() || meNow.address);
if (bal < need) {
IAP.status('That wallet holds ' + IAP.fmtPol(bal.toString()) + ' POL, but this package needs about ' + IAP.fmtPol(need.toString()) + ' POL plus a little for gas. Top it up and try again.', 'bad');
return;
}
const pct = Number(need * 100n / bal);
const isTrust = /trust/i.test(IAPWallet.walletName() || '');
if (isTrust && pct > 55 && !confirm('Heads up for Trust Wallet users: this purchase uses about ' + pct + '% of the POL in your wallet, and Trust Wallet refuses transactions that spend most of the balance (it shows a "drain your wallet" warning with only Stop and go back).' + '\n\n' + 'Options: pick a smaller package first, add some POL, or connect a different wallet (MetaMask, Phantom, SafePal). Extra POL always stays yours.' + '\n\n' + 'Try it anyway?')) {
IAP.status('Purchase paused. Pick a smaller package, add POL, or connect another wallet, then try again.', 'ok');
return;
}
} catch (e) { /* balance read failed: let the wallet decide */ }
IAP.status('Confirm the purchase in your wallet…');
const r = await IAPWallet.buy(Number(b.dataset.id), spNow.sponsorId || 0, b.dataset.cost);
if (r.status !== '0x1' && r.receipt && r.receipt.status !== '0x1') throw new Error('Transaction reverted.');
IAP.status('Purchase settled on-chain. Credits are in your account.', 'ok');
await render();
loadBuyTiles();
} catch (e) { IAP.status('Purchase failed: ' + ((e && e.message) || e), 'bad'); }
finally { b.disabled = false; }
}));
// brand new to crypto? buy POL with a card, sent straight to the wallet
const builder = products.find(p => p.priceCents === 5000) || products[products.length - 1];
const needPol = (builder && builder.costWei) ? Math.max(30, Math.ceil(Number(builder.costWei) / 1e18) + 3) : 30;
const cta = document.createElement('div');
cta.style.cssText = 'grid-column:1/-1;margin-top:10px;text-align:center';
cta.innerHTML = '
New to crypto? Buy POL with a debit or credit card, Apple Pay, or Google Pay. It lands straight in your own wallet, and this site never touches your money.
'
+ '';
wrap.appendChild(cta);
const mb = $('moonpayBtn'); if (mb) mb.addEventListener('click', () => openMoonpay(needPol));
} catch (e) {}
}
loadBuyTiles();
// ── earn-by-viewing: daily ad set with dwell, then claim ──
const earnState = { types: ['banner', 'text'], i: 0, timer: null };
async function earnRefresh() {
try {
const st = await (await fetch('/api/my/earn')).json();
if (st.error) return null;
$('earnProgress').textContent = 'today: ' + st.views + ' / ' + st.target + ' ads viewed';
$('earnBalance').textContent = st.earned + ' earned credits';
const done = st.views >= st.target;
$('earnClaimBtn').hidden = !(done && !st.claimed);
if (st.claimed) $('earnHint').textContent = 'Claimed for today. Come back tomorrow, or put those credits to work in Campaigns.';
else if (done) $('earnHint').textContent = 'Set complete. Claim your ' + st.claimCredits + ' credits.';
return st;
} catch (e) { return null; }
}
async function earnShowAd() {
// each view happens full screen in its own tab: /view/ frames the
// advertiser's site, runs the countdown, then a human check credits it
const type = earnState.types[earnState.i++ % earnState.types.length];
let r = null;
try { r = await (await fetch('/api/my/earnview?type=' + type)).json(); } catch (e) {}
const box = $('earnAdBox');
if (!r || !r.ad || !r.viewUrl) {
await earnRefresh();
box.innerHTML = '' + (r && r.status && r.status.views >= r.status.target
? 'Set complete for today.'
: 'No member ads are live in rotation right now. Views resume the moment a campaign is running.') + '';
return;
}
openAdOverlay(r.viewUrl);
box.innerHTML = 'Watch the countdown and pass the quick check. Your view credits itself and this page updates right away.';
$('earnStartBtn').textContent = 'View next ad';
}
// In-page ad overlay: no new tab (mobile and in-app wallet browsers handle tabs
// badly), no window.close needed. The /view page runs the dwell + check inside,
// messages back when it credits or when the user is done.
function openAdOverlay(url) {
closeAdOverlay();
const back = document.createElement('div');
back.id = 'adOverlay';
back.style.cssText = 'position:fixed;inset:0;z-index:100000;background:#050b09;display:flex;flex-direction:column';
const x = document.createElement('button');
x.type = 'button'; x.setAttribute('aria-label', 'Close ad');
x.textContent = '✕';
x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:40px;height:40px;border-radius:50%;border:1px solid rgba(255,255,255,.25);background:rgba(6,10,16,.85);color:#fff;font-size:20px;font-weight:700;cursor:pointer';
x.addEventListener('click', closeAdOverlay);
const ifr = document.createElement('iframe');
ifr.src = url;
ifr.style.cssText = 'flex:1;width:100%;border:0;background:#050b09';
back.appendChild(ifr); back.appendChild(x);
document.body.appendChild(back);
document.body.style.overflow = 'hidden';
}
function closeAdOverlay() {
const m = $('adOverlay'); if (m) m.remove();
document.body.style.overflow = '';
earnRefresh(); loadDashboard();
}
window.addEventListener('message', e => {
if (e.origin !== location.origin || !e.data) return;
if (e.data.t === 'iap-view-close') closeAdOverlay();
else if (e.data.t === 'iap-view-done') { earnRefresh(); loadDashboard(); }
});
$('earnStartBtn').addEventListener('click', () => earnShowAd());
// the viewer tab pings localStorage when a view credits; refresh instantly
window.addEventListener('storage', e => {
if (e.key === 'iap-view-done') { earnRefresh(); loadDashboard(); }
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && $('pane-earn') && !$('pane-earn').hidden) earnRefresh();
});
$('earnClaimBtn').addEventListener('click', async () => {
try {
const r = await api('/api/my/claim');
IAP.status('+' + r.credited + ' credits earned. Spend them in Campaigns.', 'ok');
await earnRefresh();
loadDashboard();
} catch (e) { IAP.status(e.message, 'bad'); }
});
const busy = (btn, fn) => async () => {
try { btn.disabled = true; await fn(); }
catch (e) { IAP.status((e && e.message) || String(e), 'bad'); }
finally { btn.disabled = false; }
};
// passwordless (feature-flagged on config.emailAuth): code replaces passwords
(async () => {
const cfg = await IAP.getConfig();
if (!cfg.emailAuth) return;
$('passCards').hidden = true;
$('magicCard').hidden = false;
const start = busy($('mcSendBtn'), async () => {
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
$('mcCodeRow').hidden = false;
$('mcVerifyBtn').hidden = false;
$('mcSendBtn').hidden = true;
$('mcResend').hidden = false;
if (r.devCode) { $('mcCode').value = r.devCode; IAP.status('Dev mode: code filled in for you.', 'ok'); }
else IAP.status('Code sent. Check your inbox (and spam, the first time).', 'ok');
$('mcCode').focus();
});
$('mcSendBtn').addEventListener('click', start);
$('mcResend').addEventListener('click', busy($('mcResend'), async () => {
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
if (r.devCode) $('mcCode').value = r.devCode;
IAP.status('Fresh code sent.', 'ok');
}));
$('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => {
const r = await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value, newsletter: !!($('nlOptin') && $('nlOptin').checked) });
IAP.status('You are in.', 'ok');
if (r.created && !(r.account && r.account.username)) await showOnboard(); // pick a username first
if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad
await render();
}));
})();
// new-member onboarding: choose a username, optionally a bio (both skippable)
function showOnboard() {
return new Promise(resolve => {
const m = $('onboardModal'); if (!m) return resolve();
m.hidden = false; $('obErr').hidden = true;
const done = () => { m.hidden = true; resolve(); };
$('obSkip').onclick = done;
$('obSave').onclick = async () => {
const u = $('obUsername').value.trim();
try {
if (u) await api('/api/my/profile', { username: u });
const bio = $('obBio').value.trim();
if (bio) await api('/api/my/profile-details', { bio });
done();
} catch (e) { $('obErr').hidden = false; $('obErr').textContent = e.message || 'Could not save that. Try a different username.'; }
};
});
}
$('signupBtn').addEventListener('click', busy($('signupBtn'), async () => {
const r = await api('/api/signup', { email: $('suEmail').value, password: $('suPass').value, newsletter: !!($('nlOptin') && $('nlOptin').checked) });
IAP.status('Welcome aboard. You are in.', 'ok');
if (!(r.account && r.account.username)) await showOnboard();
await render();
}));
$('loginBtn').addEventListener('click', busy($('loginBtn'), async () => {
await api('/api/login', { email: $('liEmail').value, password: $('liPass').value });
IAP.status('Logged in.', 'ok');
if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad
await render();
}));
$('linkBtn').addEventListener('click', busy($('linkBtn'), async () => {
IAP.status('Check your wallet for the free link signature…');
await IAPWallet.signIn(); // server binds the wallet to the signed-in email account
IAP.status('Wallet linked. Earnings pay there from now on.', 'ok');
await render();
}));
if ($('faucetBtn')) $('faucetBtn').addEventListener('click', busy($('faucetBtn'), async () => {
IAP.status('Connect your wallet first…');
const addr = await IAPWallet.connect();
let copied = false;
try { await navigator.clipboard.writeText(addr); copied = true; } catch (e) {}
window.open('https://faucet.polygon.technology/', '_blank', 'noopener');
$('faucetInfo').innerHTML = 'Your address ' + addr.slice(0, 8) + '…' + addr.slice(-6) + ''
+ (copied ? ' is copied' : '') + '. On the faucet, choose Polygon Amoy, paste your address, and request POL. Then come back and buy.';
IAP.status('Faucet opened in a new tab. Choose Polygon Amoy, paste your address, and request test POL.', 'ok');
}));
if ($('wcDisconnect')) $('wcDisconnect').addEventListener('click', busy($('wcDisconnect'), async () => {
// fire-and-forget: don't gate the reload on disconnect resolving (it can
// stall). The reload drops all in-memory wallet state and the picker returns.
IAPWallet.disconnect().catch(() => {});
$('wcDisconnectInfo').textContent = 'Disconnecting — reloading so you can pick a different wallet…';
IAP.status('Disconnecting — reloading…', 'ok');
setTimeout(() => location.reload(), 900);
}));
$('activateBtn').addEventListener('click', busy($('activateBtn'), async () => {
const me = await (await fetch('/api/me')).json();
IAP.status('Confirm the free activation in your wallet…');
const r = await IAPWallet.activate(me.sponsorId || 0);
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
IAP.status('Payouts are on. Your invite link is live.', 'ok');
await render();
}));
$('copyInvite').addEventListener('click', async () => {
try { await navigator.clipboard.writeText($('inviteLine').textContent); IAP.status('Link copied.', 'ok'); }
catch (e) { IAP.status('Copy failed. Select and copy the link text.', 'bad'); }
});
$('logoutLink').addEventListener('click', async e => {
e.preventDefault();
await fetch('/api/auth/logout', { method: 'POST' });
IAP.status('Logged out.', 'ok');
await render();
});
// ── welcome tour (viral banner gauntlet): a new member meets their 3-level
// upline's sites, 10 focus-paused seconds each, then unlocks welcome credits.
// Same three levels the contract pays — the tour IS the org chart.
async function showGauntlet() {
try {
const g = await (await fetch('/api/my/gauntlet')).json();
if (!g.pending || !g.slides || !g.slides.length) return false;
const gate = $('gauntGate');
gate.hidden = false;
for (let i = 0; i < g.slides.length; i++) {
const s = g.slides[i];
$('ggWho').textContent = 'Position ' + (i + 1) + ': ' + s.name + (i === 0 ? ' — the person who invited you' : '');
$('ggProgress').textContent = 'Meeting your line: ' + (i + 1) + ' of ' + g.slides.length;
$('ggFrame').hidden = false;
$('ggFrame').src = s.targetUrl;
let left = g.dwell || 10;
$('ggTimer').textContent = left + 's';
await new Promise(done => {
const t = setInterval(() => {
if (document.visibilityState !== 'visible' || !document.hasFocus()) return;
left -= 1;
$('ggTimer').textContent = Math.max(0, left) + 's';
if (left <= 0) { clearInterval(t); done(); }
}, 1000);
});
}
$('ggFrame').src = 'about:blank';
$('ggFrame').hidden = true; // avoid a white about:blank panel once the tour is done
$('ggTimer').textContent = '✓';
$('ggWho').textContent = 'That is your line. When you grow, they earn — and yours starts the day you share.';
$('ggClaim').hidden = false;
await new Promise(done => {
$('ggClaim').onclick = async () => {
try {
const r = await api('/api/my/gauntlet/complete', { token: g.token });
IAP.status('+' + r.credited + ' welcome credits unlocked. They spend on real campaigns.', 'ok');
} catch (e) { IAP.status(e.message, 'bad'); }
done();
};
});
gate.hidden = true;
$('ggClaim').hidden = true;
return true;
} catch (e) { return false; }
}
// ── line banner (profile): the member's slot on welcome tours + their wall ──
function fillLineBanner(a) {
if (!a) return;
if (a.lineTargetUrl) $('lbTarget').value = a.lineTargetUrl;
if (a.lineBannerUrl) {
$('lbBanner').value = a.lineBannerUrl;
$('lbPreview').hidden = false;
$('lbPreview').innerHTML = '';
}
$('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 = '';
} 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
? ''
: '' + (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)
? '