Free members refer from day one: share codes with late chain binding
Every account gets a share code at signup; /join/<code> attributes first-touch site-side and resolves to the referrer's CURRENT on-chain id at the referral's buy time, so activating any time before your people buy locks the line to you. Joining through a code emails the referrer an activate-payouts nudge. Buy flow re-resolves the sponsor at click time. Copy updated across home and members; assets bumped to v=20260904g. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,32 @@ function isAdmin(req) {
|
||||
const h = req.headers.authorization || '';
|
||||
return h === 'Bearer ' + ADMIN_PASSWORD;
|
||||
}
|
||||
// A sponsor token is a numeric chain id or a site share code. Codes resolve
|
||||
// to the referrer's CURRENT chain id, so activation any time before the
|
||||
// referral's first purchase still locks the line to them.
|
||||
async function resolveSponsorToken(tok) {
|
||||
const t = String(tok || '').trim().toLowerCase();
|
||||
if (!t) return 0;
|
||||
if (/^\d+$/.test(t)) return Number(t);
|
||||
const acct = accounts.byCode(t);
|
||||
if (!acct || !acct.address) return 0;
|
||||
try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; }
|
||||
}
|
||||
// The moment someone joins through a code, nudge its owner to activate.
|
||||
function nudgeReferrer(ref) {
|
||||
try {
|
||||
const t = String(ref || '').trim().toLowerCase();
|
||||
if (!t || /^\d+$/.test(t) || !mailer.hasKey()) return;
|
||||
const owner = accounts.byCode(t);
|
||||
if (!owner || !owner.email || owner.address) return; // already activated-ready
|
||||
mailer.send(owner.email, 'Someone just joined through your InstantAdPay link',
|
||||
'Good news: a new member just signed up through your share link.\n\n'
|
||||
+ 'One thing to do so you never miss a payment: sign in and switch on payouts '
|
||||
+ '(one free wallet step). The contract locks each buyer to their sponsor at their '
|
||||
+ 'first purchase, so have payouts on before your people start buying.\n\n'
|
||||
+ 'https://instantadpay.com/my\n\nInstantAdPay').catch(e => console.error('nudge failed', e.message));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ---- live feed (SSE) ----
|
||||
const feedClients = new Set();
|
||||
@@ -99,14 +125,16 @@ const server = http.createServer(async (req, res) => {
|
||||
const u = new URL(req.url, 'http://x');
|
||||
const p = u.pathname;
|
||||
|
||||
// -- join links: /join/<sponsorId> — first-touch attribution cookie
|
||||
let m = /^\/join\/(\d{1,9})$/.exec(p);
|
||||
// -- join links: /join/<memberId or share code> — first-touch cookie.
|
||||
// Codes resolve LATE (at buy time) to whatever chain id the referrer
|
||||
// has by then, so free members refer from day one.
|
||||
let m = /^\/join\/([A-Za-z0-9]{1,16})$/.exec(p);
|
||||
if (m && req.method === 'GET') {
|
||||
const sid = Number(m[1]);
|
||||
const tok = m[1].toLowerCase();
|
||||
const cookies = parseCookies(req);
|
||||
const headers = { Location: '/' };
|
||||
if (!cookies['iap.sponsor']) {
|
||||
headers['Set-Cookie'] = `iap.sponsor=${sid}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
|
||||
headers['Set-Cookie'] = `iap.sponsor=${tok}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
|
||||
}
|
||||
res.writeHead(302, baseHeaders(headers));
|
||||
return res.end();
|
||||
@@ -133,10 +161,9 @@ const server = http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
if (p === '/api/sponsor' && req.method === 'GET') {
|
||||
const sid = Number(parseCookies(req)['iap.sponsor']) || 0;
|
||||
let sponsor = null;
|
||||
if (sid) { try { const mm = await chain.member(sid); if (mm.account !== '0x' + '0'.repeat(40)) sponsor = { id: sid }; } catch (e) {} }
|
||||
return json(res, 200, { sponsorId: sponsor ? sid : 0 });
|
||||
const tok = parseCookies(req)['iap.sponsor'] || '';
|
||||
const sponsorId = await resolveSponsorToken(tok);
|
||||
return json(res, 200, { ref: tok, sponsorId, invited: !!tok });
|
||||
}
|
||||
if (p === '/api/stats' && req.method === 'GET') {
|
||||
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
|
||||
@@ -147,9 +174,10 @@ const server = http.createServer(async (req, res) => {
|
||||
// out only at purchase / payout-activation time and gets linked then)
|
||||
if (p === '/api/signup' && req.method === 'POST') {
|
||||
const b = await readBody(req);
|
||||
const sid = Number(parseCookies(req)['iap.sponsor']) || 0; // first-touch attribution
|
||||
const r = accounts.signup(b.email, b.password, sid);
|
||||
const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
|
||||
const r = accounts.signup(b.email, b.password, ref);
|
||||
if (r.error) return json(res, 400, r);
|
||||
nudgeReferrer(ref);
|
||||
const token = auth.mintSession({ email: r.account.email });
|
||||
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
|
||||
}
|
||||
@@ -191,9 +219,10 @@ const server = http.createServer(async (req, res) => {
|
||||
if (rec.tries > 6) { emailCodes.delete(e); return json(res, 400, { error: 'Too many tries. Request a fresh code.' }); }
|
||||
if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
|
||||
emailCodes.delete(e);
|
||||
const sid = Number(parseCookies(req)['iap.sponsor']) || 0;
|
||||
const r = accounts.ensure(e, sid); // first touch wins; existing accounts unchanged
|
||||
const ref = parseCookies(req)['iap.sponsor'] || '';
|
||||
const r = accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
|
||||
if (r.error) return json(res, 400, r);
|
||||
if (r.created) nudgeReferrer(ref);
|
||||
let memberId = 0;
|
||||
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }
|
||||
const token = auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
|
||||
@@ -234,9 +263,10 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!s) return json(res, 200, { signedIn: false });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const acct = (s.email && accounts.byEmail(s.email)) || (s.address && accounts.byAddress(s.address)) || null;
|
||||
const sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']);
|
||||
const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
|
||||
address: s.address || (acct && acct.address) || null, memberId,
|
||||
sponsorId: (acct && acct.sponsorId) || Number(parseCookies(req)['iap.sponsor']) || 0 };
|
||||
refCode: (acct && acct.code) || null, sponsorId };
|
||||
if (memberId) {
|
||||
try {
|
||||
const mm = await chain.member(memberId);
|
||||
|
||||
Reference in New Issue
Block a user