Wall ownership ladder: positions 2 and 3 become the member's own at 2 / 5 qualifying buyers
- accounts.wall_offers (JSON, up to 2 offers: label, https link, banner) set from Profile > Your wall; upload supported; locks show the buyer threshold. - /api/wall assembles: position 1 = own line banner; positions 2-3 = own offer when unlocked and set, else upline banners (only uplines with a live banner), else house ads. Response carries unlocked + buyerCount; wall labels show "this wall" / "their line" / "InstantAdPay". - Chatbot canned answer + facts, follow-up email 7 mention the ladder. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,14 @@ async function uplineSlides(email, depth = 3) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Wall ownership ladder: position 1 is always the member's own line banner;
|
||||
// positions 2 and 3 become theirs at 2 and 5 qualifying buyers (the same
|
||||
// thresholds that open payout levels 2 and 3). Until then, or while an unlocked
|
||||
// slot is empty, the slot shows an upline's banner, then a house ad.
|
||||
const wallUnlockedFor = bc => (bc >= 5 ? 3 : bc >= 2 ? 2 : 1);
|
||||
function parseWallOffers(a) {
|
||||
try { const v = JSON.parse((a && a.wallOffers) || '[]'); return Array.isArray(v) ? v.slice(0, 2) : []; } catch (e) { return []; }
|
||||
}
|
||||
// admin fallback ads for wall positions 2 & 3 when a member has no upline.
|
||||
// Configurable by dropping data/admin-wall-ads.json ([{name,targetUrl,bannerUrl}]).
|
||||
function getAdminWallAds() {
|
||||
@@ -681,6 +689,7 @@ const server = http.createServer(async (req, res) => {
|
||||
// profile + line-banner fields so the Profile pane repopulates on reload (were being saved but not returned)
|
||||
avatarUrl: (acct && acct.avatarUrl) || null, bio: (acct && acct.bio) || null,
|
||||
socials: (acct && acct.socials) || null,
|
||||
wallOffers: parseWallOffers(acct),
|
||||
lineBannerUrl: (acct && acct.lineBannerUrl) || null, lineTargetUrl: (acct && acct.lineTargetUrl) || null };
|
||||
if (memberId) {
|
||||
try {
|
||||
@@ -772,6 +781,7 @@ const server = http.createServer(async (req, res) => {
|
||||
status: r.address ? 'wallet linked' : 'joined free'
|
||||
}));
|
||||
out.isAdmin = !!(ADMIN_EMAIL && out.email && String(out.email).toLowerCase() === ADMIN_EMAIL); // shows the Admin link
|
||||
out.wallUnlocked = wallUnlockedFor(out.buyerCount || 0); // how many wall positions are the member's own
|
||||
return json(res, 200, out);
|
||||
}
|
||||
if (p === '/api/my/profile' && req.method === 'POST') {
|
||||
@@ -1129,6 +1139,25 @@ const server = http.createServer(async (req, res) => {
|
||||
const r = await accounts.setProfile(s.email, avatar, bio, socials);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
// -- wall positions 2 & 3: the member's own offers, unlocked at 2 / 5 qualifying buyers
|
||||
if (p === '/api/my/wall-offers' && 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 src = Array.isArray(b.offers) ? b.offers.slice(0, 2) : [];
|
||||
const out = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const o = src[i] || {};
|
||||
const targetUrl = String(o.targetUrl || '').trim();
|
||||
const bannerUrl = String(o.bannerUrl || '').trim();
|
||||
const title = String(o.title || '').trim().slice(0, 60);
|
||||
if (targetUrl && !/^https:\/\/[^\s]+$/i.test(targetUrl)) return json(res, 400, { error: 'Position ' + (i + 2) + ': the link must start with https://' });
|
||||
if (bannerUrl && !/^(\/uploads\/[a-z0-9]{24}\.(png|jpg|webp|gif)|https:\/\/[^\s]+)$/i.test(bannerUrl)) return json(res, 400, { error: 'Position ' + (i + 2) + ': banner must be an uploaded image or an https image URL.' });
|
||||
out.push(targetUrl ? { title: title || null, bannerUrl: bannerUrl || null, targetUrl } : null);
|
||||
}
|
||||
const r = await accounts.setWallOffers(s.email, out.some(Boolean) ? JSON.stringify(out) : null);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
// -- line banner: the member's viral slot on welcome tours + their wall
|
||||
if (p === '/api/my/linebanner' && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
@@ -1175,31 +1204,36 @@ const server = http.createServer(async (req, res) => {
|
||||
let a = await accounts.byUsername(tok);
|
||||
if (!a) a = await accounts.byCode(tok);
|
||||
if (!a) return json(res, 404, { error: 'No wall under that name.' });
|
||||
const ladder = [a, ...await uplineSlides(a.email, 2)]
|
||||
const ownName = a.username ? '@' + a.username : a.memberId ? 'member #' + a.memberId : 'a member';
|
||||
let bc = 0;
|
||||
if (a.memberId) { try { bc = (await chain.member(a.memberId)).buyerCount || 0; } catch (e) {} }
|
||||
const unlocked = wallUnlockedFor(bc);
|
||||
const offers = parseWallOffers(a);
|
||||
// uplines with a live banner, in order (an upline with nothing set is skipped, not shown empty)
|
||||
const ups = (await uplineSlides(a.email, 2)).filter(x => x.lineTargetUrl)
|
||||
.map(x => ({ name: x.username ? '@' + x.username : x.memberId ? 'member #' + x.memberId : 'a member',
|
||||
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl || null }));
|
||||
// fill positions 2 & 3 with admin ads when there isn't enough upline, so the wall is never sparse
|
||||
const adAds = getAdminWallAds();
|
||||
let ai = 0;
|
||||
while (ladder.length < 3 && adAds.length) {
|
||||
const ad = adAds[ai++ % adAds.length];
|
||||
ladder.push({ name: ad.name || 'InstantAdPay', bannerUrl: ad.bannerUrl || null, targetUrl: ad.targetUrl || 'https://instantadpay.com/', admin: true });
|
||||
bannerUrl: x.lineBannerUrl || null, targetUrl: x.lineTargetUrl, upline: true }));
|
||||
const adAds = getAdminWallAds(); let ai = 0;
|
||||
const houseAd = () => { if (!adAds.length) return null; const ad = adAds[ai++ % adAds.length]; return { name: ad.name || 'InstantAdPay', bannerUrl: ad.bannerUrl || null, targetUrl: ad.targetUrl || 'https://instantadpay.com/', admin: true }; };
|
||||
const ladder = [{ name: ownName, bannerUrl: a.lineBannerUrl || null, targetUrl: a.lineTargetUrl || null, own: true }];
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const o = offers[i - 1];
|
||||
if (i < unlocked && o && o.targetUrl) { ladder.push({ name: (o.title || ownName), bannerUrl: o.bannerUrl || null, targetUrl: o.targetUrl, own: true }); continue; }
|
||||
const u = ups.shift(); if (u) { ladder.push(u); continue; }
|
||||
const h = houseAd(); if (h) ladder.push(h);
|
||||
}
|
||||
const finalLadder = ladder.slice(0, 3);
|
||||
// the wall owner's achievement badge (their highest reached tier)
|
||||
let badge = null;
|
||||
if (a.memberId) {
|
||||
try {
|
||||
const bc = (await chain.member(a.memberId)).buyerCount || 0;
|
||||
const t = bc >= 5 ? ['nexus', 'Nexus'] : bc >= 2 ? ['circuit', 'Circuit'] : bc >= 1 ? ['surge', 'Surge'] : ['spark', 'Spark'];
|
||||
badge = { img: '/badges/badge-' + t[0] + '.jpg?v=2', label: t[1] };
|
||||
} catch (e) {}
|
||||
const t = bc >= 5 ? ['nexus', 'Nexus'] : bc >= 2 ? ['circuit', 'Circuit'] : bc >= 1 ? ['surge', 'Surge'] : ['spark', 'Spark'];
|
||||
badge = { img: '/badges/badge-' + t[0] + '.jpg?v=2', label: t[1] };
|
||||
}
|
||||
const joinPath = '/join/' + (a.username || a.code);
|
||||
let socials = null; try { socials = a.socials ? JSON.parse(a.socials) : null; } catch (e) {}
|
||||
return json(res, 200, { name: a.username ? '@' + a.username : 'member #' + (a.memberId || 0),
|
||||
avatarUrl: a.avatarUrl || null, bio: a.bio || null, socials, badge,
|
||||
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder: finalLadder });
|
||||
joinUrl: joinPath, qrUrl: '/api/qr?d=' + encodeURIComponent('https://instantadpay.com' + joinPath), ladder: finalLadder, unlocked, buyerCount: bc });
|
||||
}
|
||||
// -- watch-to-earn video ads: serve one, then reward a server-clock-verified watch
|
||||
if (p === '/api/my/videos' && req.method === 'GET') {
|
||||
|
||||
Reference in New Issue
Block a user