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:
@@ -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
@@ -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
@@ -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 & 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 & 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>
|
||||
|
||||
Reference in New Issue
Block a user