Email-first membership: signup/login, wallet linked at purchase time

Normal people join with email + password (sponsor attribution via cookie at
signup); the wallet only appears when buying or activating payouts, and gets
linked to the account then. Wallet-only sign-in remains for crypto-native
users. Sessions carry {email, address, memberId}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 13:00:56 -05:00
parent a87a63d75d
commit a90d5eee78
6 changed files with 271 additions and 114 deletions
+6
View File
@@ -31,6 +31,12 @@
async function buyPack(btn) {
try {
btn.disabled = true;
// email members get their wallet linked to the account at buy time
const me = await (await fetch('/api/me')).json();
if (me.signedIn && me.email && !me.address) {
IAP.status('First, a free signature links your wallet to your account…');
await IAPWallet.signIn();
}
IAP.status('Confirm the purchase in your wallet…');
const r = await IAPWallet.buy(Number(btn.dataset.id), sp.sponsorId || 0, btn.dataset.cost);
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
+74 -38
View File
@@ -1,36 +1,51 @@
// My account: SIWE sign-in, member state, free activation, invite link.
// 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();
if (!me || !me.signedIn) { $('signinCard').hidden = false; $('memberArea').hidden = true; return; }
$('signinCard').hidden = true;
$('memberArea').hidden = false;
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;
if (me.memberId) {
$('posLine').innerHTML = 'On-chain <b>member #' + me.memberId + '</b><br>wallet <span class="mono">'
+ me.address.slice(0, 8) + '…' + me.address.slice(-6) + '</span>'
+ (me.onchainSponsorId ? '<br>sponsored by member #' + me.onchainSponsorId : '<br>no sponsor (house line)');
$('creditLine').textContent = (me.credits || 0).toLocaleString();
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 ≥$20 buyer(s) unlock level 2');
$('activateCard').hidden = true;
: (2 - bc) + ' more buyer(s) of $20+ unlock level 2');
$('inviteLine').textContent = location.origin + '/join/' + me.memberId;
$('copyInvite').hidden = false;
loadActivity();
} else {
$('posLine').innerHTML = 'Signed in as <span class="mono">' + me.address.slice(0, 8) + '…' + me.address.slice(-6)
+ '</span><br>free member, not on-chain yet'
+ (me.sponsorId ? '<br>invited by member #' + me.sponsorId : '');
$('creditLine').textContent = '0';
$('qualLine').textContent = 'Activate your payout wallet (or buy any package) to start; referrals who buy ≥$20 packages qualify you.';
$('activateCard').hidden = false;
$('inviteLine').textContent = 'Your link appears after your free on-chain activation.';
$('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;
}
}
@@ -42,43 +57,64 @@
const fill = (id, evs, empty) => {
const el = $(id);
el.innerHTML = '';
if (!evs.length) { el.innerHTML = '<div class="row muted">' + empty + '</div>'; return; }
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 this wallet yet.');
fill('buyFeed', a.purchases, 'No purchases from your wallet yet.');
} catch (e) {}
}
$('signinBtn').addEventListener('click', async () => {
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; }
};
$('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 {
$('signinBtn').disabled = true;
IAP.status('Check your wallet for the free sign-in signature…');
await IAPWallet.signIn();
IAP.status('Signed in.', 'ok');
IAP.status('Signed in with your wallet.', 'ok');
await render();
} catch (e) { IAP.status((e && e.message) || String(e), 'bad'); }
finally { $('signinBtn').disabled = false; }
} catch (err) { IAP.status((err && err.message) || String(err), 'bad'); }
});
$('activateBtn').addEventListener('click', async () => {
try {
$('activateBtn').disabled = true;
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('Payout wallet activated. Your invite link is live.', 'ok');
await render();
} catch (e) { IAP.status('Activation failed: ' + ((e && e.message) || e), 'bad'); }
finally { $('activateBtn').disabled = false; }
});
$('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();
})();
+55 -30
View File
@@ -9,66 +9,91 @@
<div class="wrap">
<section class="hero" style="padding-bottom:16px">
<h1>My <em>account</em></h1>
<p class="lead" id="introLead">Sign in with one free wallet signature. No email. No password.
No account to create. The signature is free and cannot move funds.</p>
<p class="lead" id="introLead">Join free with your email. Your wallet only comes out when you buy
a package or switch on payouts, and it stays yours the whole time.</p>
</section>
<div class="card" id="signinCard">
<h3>Sign in with your wallet</h3>
<p class="muted small">New here? The same button creates your free membership. Your earnings always go
straight to this wallet. We never hold them.</p>
<button class="btn" id="signinBtn">Connect &amp; sign in</button>
<div id="authArea">
<div class="grid c2">
<div class="card">
<h3>Create your free account</h3>
<p class="muted small">Takes ten seconds. No wallet needed to join.</p>
<p><input id="suEmail" type="email" placeholder="Email" autocomplete="email" style="width:100%"></p>
<p><input id="suPass" type="password" placeholder="Password (8+ characters)" autocomplete="new-password" style="width:100%"></p>
<button class="btn" id="signupBtn">Join free</button>
</div>
<div class="card">
<h3>Log in</h3>
<p class="muted small">Welcome back.</p>
<p><input id="liEmail" type="email" placeholder="Email" autocomplete="email" style="width:100%"></p>
<p><input id="liPass" type="password" placeholder="Password" autocomplete="current-password" style="width:100%"></p>
<button class="btn sec" id="loginBtn">Log in</button>
</div>
</div>
<p class="small muted">Crypto-native? You can also <a href="#" id="walletSigninLink">sign in with just your wallet</a>.
One free signature, no email needed.</p>
</div>
<div id="memberArea" hidden>
<div class="grid c3">
<div class="card"><h3>Your position</h3>
<div class="card"><h3>Your account</h3>
<p id="posLine" class="muted small">…</p></div>
<div class="card"><h3>Ad credits</h3>
<p class="mono" style="font-size:26px;margin:0" id="creditLine">—</p>
<p class="muted small">1 credit = 1¢ of delivery across the network. Recorded on-chain; only your campaigns can spend them.</p></div>
<div class="card"><h3>Qualification</h3>
<p class="mono" style="font-size:26px;margin:0" id="creditLine">0</p>
<p class="muted small">1 credit = 1 cent of ad delivery across the network. Recorded on-chain. Only your campaigns can spend them.</p></div>
<div class="card"><h3>Earning levels</h3>
<p id="qualLine" class="muted small">…</p></div>
</div>
<div class="card" id="linkCard" hidden>
<h3>Link your wallet</h3>
<p class="muted small">Your earnings pay straight to your own wallet, so we need to know which one is yours.
One free signature links it. It cannot move funds or approve anything. Buying any package links it automatically too.</p>
<button class="btn" id="linkBtn">Connect &amp; link wallet</button>
</div>
<div class="card" id="activateCard" hidden>
<h3>Activate your payout wallet, free</h3>
<p class="muted small">One free transaction registers this wallet on-chain so commissions can reach it.
Buying any package does this automatically, so you can also just start with a package below.</p>
<button class="btn sec" id="activateBtn">Activate payout wallet</button>
<h3>Switch on payouts, free</h3>
<p class="muted small">One free transaction registers your linked wallet on-chain so commissions can reach it,
and it unlocks your invite link. Buying any package does this automatically.</p>
<button class="btn sec" id="activateBtn">Activate payouts</button>
</div>
<div class="card">
<h3>Your invite link</h3>
<p class="muted small">Share it anywhere. Everyone who joins through it becomes part of your line.
You earn 50 percent of every ad package they ever buy, plus levels 2 and 3 of their teams as you qualify.</p>
<p class="mono" id="inviteLine">Sign in to get your link.</p>
<p class="mono" id="inviteLine">…</p>
<button class="btn small sec" id="copyInvite" hidden>Copy link</button>
</div>
<div class="card">
<h3>Buy ad packages</h3>
<p class="muted small">The full ladder with live pricing is on the <a href="/">home page</a>.
Purchases from this wallet automatically credit this account.</p>
Purchases from your linked wallet automatically credit this account.</p>
</div>
<h2>Your activity, straight from the chain</h2>
<div class="grid c2">
<div class="card">
<h3>Earnings</h3>
<p class="muted small">Every payout the contract has sent to your wallet. Each line links to the transaction.</p>
<div class="feed" id="earnFeed"><div class="row muted">Nothing yet.</div></div>
<div id="activityArea" hidden>
<h2>Your activity, straight from the chain</h2>
<div class="grid c2">
<div class="card">
<h3>Earnings</h3>
<p class="muted small">Every payout the contract has sent to your wallet. Each line links to the transaction.</p>
<div class="feed" id="earnFeed"><div class="row muted">Nothing yet.</div></div>
</div>
<div class="card">
<h3>Your referrals</h3>
<p class="muted small">Activations and qualifying buys in your line.</p>
<div class="feed" id="refFeed"><div class="row muted">Nothing yet.</div></div>
</div>
</div>
<div class="card">
<h3>Your referrals</h3>
<p class="muted small">Activations and qualifying buys in your line.</p>
<div class="feed" id="refFeed"><div class="row muted">Nothing yet.</div></div>
<h3>Your purchases</h3>
<div class="feed" id="buyFeed"><div class="row muted">Nothing yet.</div></div>
</div>
</div>
<div class="card">
<h3>Your purchases</h3>
<div class="feed" id="buyFeed"><div class="row muted">Nothing yet.</div></div>
</div>
<p class="small"><a href="#" id="logoutLink">Log out</a></p>
</div>
<footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer>