Member identity, promo tools, back-office color, seeded inventory
Real people, not numbers: usernames (unique, profile pane to set them), shown across the ledger, activity, rosters, and the sidebar chip; vanity invite links (/join/<username>) with late chain binding intact. Promo tools pane ships Branded Voice share posts and an email swipe personalized with each member's link. The earn viewer excludes a member's own campaigns, so nobody earns from their own spend. Back office gains the cyan/violet/amber accent family over the green base. Three house campaigns seeded so every surface shows live inventory. Assets v=20260905i. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ const emailCodes = new Map();
|
||||
const earnTokens = new Map();
|
||||
async function boot() {
|
||||
await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode)
|
||||
chain.init({ onEvent: ev => pushFeed(ev) });
|
||||
chain.init({ onEvent: ev => attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev)) });
|
||||
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
|
||||
accounts.init({ dataDir: DATA_DIR });
|
||||
ads.init({ dataDir: DATA_DIR, chain });
|
||||
@@ -101,6 +101,18 @@ function isAdmin(req) {
|
||||
const h = req.headers.authorization || '';
|
||||
return h === 'Bearer ' + ADMIN_PASSWORD;
|
||||
}
|
||||
// attach a memberId->username map to events so activity shows real people
|
||||
async function attachNames(evts) {
|
||||
try {
|
||||
const ids = [];
|
||||
for (const ev of evts)
|
||||
for (const k of ['id', 'buyerId', 'recipientId', 'skippedId', 'sponsorId', 'newBuyerId', 'toId', 'memberId'])
|
||||
if (ev[k]) ids.push(ev[k]);
|
||||
const names = await accounts.namesForMembers(ids);
|
||||
if (!Object.keys(names).length) return evts;
|
||||
return evts.map(ev => Object.assign({}, ev, { names }));
|
||||
} catch (e) { return evts; }
|
||||
}
|
||||
// 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.
|
||||
@@ -108,7 +120,8 @@ async function resolveSponsorToken(tok) {
|
||||
const t = String(tok || '').trim().toLowerCase();
|
||||
if (!t) return 0;
|
||||
if (/^\d+$/.test(t)) return Number(t);
|
||||
const acct = await accounts.byCode(t);
|
||||
let acct = await accounts.byCode(t);
|
||||
if (!acct) acct = await accounts.byUsername(t); // vanity links: /join/<username>
|
||||
if (!acct || !acct.address) return 0;
|
||||
try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; }
|
||||
}
|
||||
@@ -144,7 +157,7 @@ const server = http.createServer(async (req, res) => {
|
||||
// -- 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);
|
||||
let m = /^\/join\/([A-Za-z0-9_]{1,20})$/.exec(p);
|
||||
if (m && req.method === 'GET') {
|
||||
const tok = m[1].toLowerCase();
|
||||
const cookies = parseCookies(req);
|
||||
@@ -167,7 +180,7 @@ const server = http.createServer(async (req, res) => {
|
||||
return json(res, 200, { products: await chain.catalog() });
|
||||
}
|
||||
if (p === '/api/feed' && req.method === 'GET') {
|
||||
return json(res, 200, { events: chain.recentEvents(Number(u.searchParams.get('n')) || 100) });
|
||||
return json(res, 200, { events: await attachNames(chain.recentEvents(Number(u.searchParams.get('n')) || 100)) });
|
||||
}
|
||||
if (p === '/api/feed/live' && req.method === 'GET') {
|
||||
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', Connection: 'keep-alive' }));
|
||||
@@ -297,8 +310,10 @@ const server = http.createServer(async (req, res) => {
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
|
||||
const sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']);
|
||||
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
||||
const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
|
||||
address: s.address || (acct && acct.address) || null, memberId,
|
||||
username: (acct && acct.username) || null,
|
||||
refCode: (acct && acct.code) || null, sponsorId };
|
||||
if (memberId) {
|
||||
try {
|
||||
@@ -316,8 +331,10 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!s) return json(res, 401, { error: 'Sign in first.' });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
|
||||
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
||||
const out = { memberId, email: s.email || (acct && acct.email) || null,
|
||||
address: s.address || (acct && acct.address) || null,
|
||||
username: (acct && acct.username) || null,
|
||||
refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0,
|
||||
earnedWei: '0', earnCount: 0, referrals: [], welcomeCredits: 0 };
|
||||
if (out.email) out.welcomeCredits = await ads.grantWelcome(out.email); // idempotent lazy grant
|
||||
@@ -342,12 +359,19 @@ const server = http.createServer(async (req, res) => {
|
||||
if (memberId) refs.push(String(memberId));
|
||||
const joined = await accounts.listByReferrer(refs);
|
||||
out.referrals = joined.map(r => ({
|
||||
email: r.email.replace(/^(.).*(@.*)$/, '$1***$2'), // privacy mask
|
||||
name: r.username || r.email.replace(/^(.).*(@.*)$/, '$1***$2'), // username, else privacy mask
|
||||
joined: r.created,
|
||||
status: r.address ? 'wallet linked' : 'joined free'
|
||||
}));
|
||||
return json(res, 200, out);
|
||||
}
|
||||
if (p === '/api/my/profile' && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const b = await readBody(req);
|
||||
const r = await accounts.setUsername(s.email, b.username);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
// -- earn credits by viewing ads (attention-gated daily claim)
|
||||
if (p === '/api/my/earn' && req.method === 'GET') {
|
||||
const s = await auth.fromRequest(req);
|
||||
@@ -364,7 +388,8 @@ const server = http.createServer(async (req, res) => {
|
||||
const status = await ads.viewStatus(s.email);
|
||||
if (status.views >= status.target || status.claimed) return json(res, 200, { ad: null, status });
|
||||
const type = String(u.searchParams.get('type') || 'banner');
|
||||
const ad = await ads.serve(type === 'text' ? 'text' : 'banner');
|
||||
// members never see (or earn from) their own campaigns in the viewer
|
||||
const ad = await ads.serve(type === 'text' ? 'text' : 'banner', { excludeEmail: s.email });
|
||||
if (!ad) return json(res, 200, { ad: null, status });
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
earnTokens.set(s.email, { token, ts: Date.now() });
|
||||
@@ -397,9 +422,9 @@ const server = http.createServer(async (req, res) => {
|
||||
const evs = chain.recentEvents(600);
|
||||
return json(res, 200, {
|
||||
memberId: id,
|
||||
earnings: evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id)),
|
||||
purchases: evs.filter(e => e.type === 'Purchase' && e.buyerId === id),
|
||||
referrals: evs.filter(e => (e.type === 'MemberActivated' && e.sponsorId === id) || (e.type === 'BuyerCounted' && e.sponsorId === id))
|
||||
earnings: await attachNames(evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id))),
|
||||
purchases: await attachNames(evs.filter(e => e.type === 'Purchase' && e.buyerId === id)),
|
||||
referrals: await attachNames(evs.filter(e => (e.type === 'MemberActivated' && e.sponsorId === id) || (e.type === 'BuyerCounted' && e.sponsorId === id)))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user