Files
instantadpay/public/assets/my.js
T
martbost 08f7a408e3 Passwordless sign-in: 6-digit email codes, feature-flagged on SendGrid key
/api/auth/email/start issues a 15-min code (60s resend guard, 6 tries);
verify creates the account passwordless (sponsor cookie first-touch) and
mints the session. UI swaps the password cards for the code flow when
config.emailAuth is on; dev mode returns the code inline. Password flow
remains until the key lands in the volume (data/sendgrid.key) or
SENDGRID_KEY env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 13:38:11 -05:00

198 lines
9.4 KiB
JavaScript

// My account: email-first join/login, wallet link at purchase time,
// free payout activation, invite link, on-chain activity.
(async function () {
await IAP.renderNav('my');
const $ = IAP.$;
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;
}
async function render() {
const me = await IAP.refreshNavWallet();
const signedIn = me && me.signedIn;
$('authArea').hidden = !!signedIn;
$('memberArea').hidden = !signedIn;
if (!signedIn) return;
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;
$('campaignCard').hidden = !me.memberId;
if (me.memberId) loadCampaigns();
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');
$('inviteLine').textContent = location.origin + '/join/' + me.memberId;
$('copyInvite').hidden = false;
loadActivity();
} else {
$('qualLine').textContent = 'Level 1 pays the moment you are on-chain. Referrals who buy packages of $20 or more unlock levels 2 and 3.';
$('inviteLine').textContent = me.address
? 'Activate payouts above and your invite link appears here.'
: 'Link a wallet and switch on payouts to get your invite link.';
$('copyInvite').hidden = true;
}
}
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()
+ ' 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); }
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();
});
render();
})();