// My account: email-first join/login, wallet link at purchase time,
// free payout activation, invite link, on-chain activity.
(async function () {
// back-office shell: no marketing nav here; rehearsal notice lives in the top bar
const $ = IAP.$;
try {
const c = await IAP.getConfig();
if (c.rehearsal && $('boRehearsal')) {
$('boRehearsal').hidden = false;
$('boRehearsal').innerHTML = 'Testnet rehearsal · ' + c.chainName;
}
} catch (e) {}
async function api(path, body) {
const r = await (await fetch(path, { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })).json();
if (r.error) throw new Error(r.error);
return r;
}
function nextMove(d) {
if (!d.address) return 'Grab your link below and start sharing today. Then link your wallet (one free signature) so every payment can lock to you before your people start buying.';
if (!d.memberId) return 'Your wallet is linked. Switch on payouts above (one free transaction) and every purchase in your line pays you the moment it happens.';
if (d.buyerCount === 0) return 'Payouts are on and your link is live. Your next milestone: your first buyer of $20 or more. Every direct pays you 50 percent from their very first package.';
if (d.buyerCount === 1) return 'One qualifying buyer down, one to go. Your next $20+ buyer unlocks level 2: 20 percent of everything your people’s people buy.';
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
}
// ── 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 => '
';
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) {}
}
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
let soloMedia = null; // { url, type } from /api/my/upload
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);
});
$('edAttachBtn').addEventListener('click', () => $('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; }
soloMedia = r;
$('edMediaInfo').textContent = f.name + ' attached';
$('edMediaRemove').hidden = false;
const pv = $('edMediaPrev');
pv.hidden = false;
pv.innerHTML = r.type === 'video'
? ''
: '';
} catch (e) { $('edMediaInfo').textContent = 'Upload failed. Try again.'; }
$('cSoloFile').value = '';
});
$('edMediaRemove').addEventListener('click', () => {
soloMedia = null;
$('edMediaInfo').textContent = '';
$('edMediaRemove').hidden = true;
$('edMediaPrev').hidden = true;
$('edMediaPrev').innerHTML = '';
});
$('cType').addEventListener('change', () => {
const t = $('cType').value;
$('cImageRow').hidden = t === 'text' || t === 'solo';
$('cTitleRow').hidden = t !== 'text' && t !== 'solo';
$('cBodyRow').hidden = t !== 'text';
$('cSoloRow').hidden = t !== 'solo';
$('cSoloHint').hidden = t !== 'solo';
$('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)';
soloHint();
});
$('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: $('cType').value === 'solo' ? $('cSoloEd').innerHTML : $('cBody').value,
mediaUrl: $('cType').value === 'solo' && soloMedia ? soloMedia.url : '',
ctaLabel: $('cCtaLabel').value,
budget: Number($('cBudget').value) });
IAP.status('Campaign is live. It starts serving right away.', 'ok');
$('cName').value = ''; $('cBudget').value = '';
$('cSoloEd').innerHTML = ''; $('cCtaLabel').value = '';
if ($('edMediaRemove') && !$('edMediaRemove').hidden) $('edMediaRemove').click();
await loadCampaigns();
}));
// defers the busy() lookup to click time (busy is declared below)
function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); }
// ── solo-ads inbox: list, read view, dwell-gated read reward ──
let ibTimer = null;
function setInboxBadge(n) {
const b = $('inboxBadge');
if (b) { b.hidden = !n; b.textContent = n; }
}
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'
? ''
: '';
$('ibVisit').href = r.url;
$('ibVisit').textContent = r.ctaLabel || 'Learn more';
const btn = $('ibClaimBtn');
clearInterval(ibTimer);
if (r.rewarded) {
btn.hidden = true;
$('ibHint').textContent = 'Read reward already claimed for this one.';
return;
}
btn.hidden = false;
btn.disabled = true;
let left = r.dwell;
btn.textContent = 'Read it — claim in ' + left + 's';
$('ibHint').textContent = 'Stay on this tab while you read; the claim unlocks when the timer is done.';
// 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) { btn.textContent = 'Read it — claim in ' + left + 's'; return; }
clearInterval(ibTimer);
btn.disabled = false;
btn.textContent = 'Claim +' + r.reward + ' credits';
}, 1000);
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)));
}
// ── 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 = '