Achievement badges + milestone credit bonuses
- Idempotent one-time credit bonus per milestone (payouts/firstBuyer/level2/level3): 10/25/50/100 cr - Overview badges strip (unlockable medals) with canvas share-to-image (username overlaid) - Newly-unlocked milestones toast the bonus on next dashboard load Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,8 @@ async function bootstrap() {
|
|||||||
granted_welcome TINYINT NOT NULL DEFAULT 0,
|
granted_welcome TINYINT NOT NULL DEFAULT 0,
|
||||||
updated BIGINT NOT NULL
|
updated BIGINT NOT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||||
|
const alterSafe0 = async sql => { try { await q(sql); } catch (e) { if (!['ER_DUP_FIELDNAME', 'ER_DUP_KEYNAME'].includes(e.code)) throw e; } };
|
||||||
|
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN milestones VARCHAR(255) NULL'); // comma-joined granted milestone keys
|
||||||
// additive columns (MySQL 8 has no IF NOT EXISTS for columns)
|
// additive columns (MySQL 8 has no IF NOT EXISTS for columns)
|
||||||
const alterSafe = async sql => {
|
const alterSafe = async sql => {
|
||||||
try { await q(sql); }
|
try { await q(sql); }
|
||||||
|
|||||||
@@ -26,6 +26,70 @@
|
|||||||
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
|
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
|
||||||
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
|
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
|
||||||
}
|
}
|
||||||
|
// achievement badges: the milestone ladder as unlockable medals + share-to-image
|
||||||
|
const BADGES = [
|
||||||
|
{ key: 'payouts', label: 'Payouts On', sub: 'wallet linked', icon: '<circle cx="12" cy="12" r="8"/><path d="M9 12l2 2 4-4"/>' },
|
||||||
|
{ key: 'firstBuyer', label: 'First Buyer', sub: 'first qualified referral', icon: '<path d="M6 7h12l-1 13H7z"/><path d="M9 7a3 3 0 0 1 6 0"/>' },
|
||||||
|
{ key: 'level2', label: 'Level 2', sub: '2 qualifying buyers', icon: '<path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5L12 21l-4.9 2.6.9-5.5-4-3.9 5.5-.8z"/>' },
|
||||||
|
{ key: 'level3', label: 'Level 3', sub: 'fully qualified', icon: '<path d="M5 4h14v5a7 7 0 0 1-14 0z"/><path d="M9 20h6M12 16v4"/><path d="M5 6H3v2a3 3 0 0 0 3 3M19 6h2v2a3 3 0 0 1-3 3"/>' }
|
||||||
|
];
|
||||||
|
let lastMilestones = [];
|
||||||
|
function renderBadges(d) {
|
||||||
|
const reached = d.milestonesReached || [];
|
||||||
|
lastMilestones = reached;
|
||||||
|
const strip = $('badgeStrip');
|
||||||
|
if (!strip) return;
|
||||||
|
$('badgeCard').hidden = false;
|
||||||
|
strip.innerHTML = BADGES.map(b => {
|
||||||
|
const got = reached.includes(b.key);
|
||||||
|
return '<div class="badge-a' + (got ? '' : ' locked') + '">'
|
||||||
|
+ '<div class="medal"><svg viewBox="0 0 24 24">' + b.icon + '</svg></div>'
|
||||||
|
+ '<div class="bl">' + b.label + '</div><div class="bs">' + b.sub + '</div>'
|
||||||
|
+ (got ? '<button class="share" data-badge="' + b.key + '">Share</button>' : '') + '</div>';
|
||||||
|
}).join('');
|
||||||
|
strip.querySelectorAll('[data-badge]').forEach(btn =>
|
||||||
|
btn.addEventListener('click', () => shareBadge(btn.dataset.badge, d)));
|
||||||
|
// celebrate anything newly granted this load
|
||||||
|
if (d.milestonesGranted && d.milestonesGranted.length) {
|
||||||
|
const total = d.milestonesGranted.reduce((s, g) => s + g.credited, 0);
|
||||||
|
const names = d.milestonesGranted.map(g => (BADGES.find(b => b.key === g.key) || {}).label).filter(Boolean).join(', ');
|
||||||
|
IAP.status('🏆 Achievement unlocked: ' + names + ' — +' + total + ' bonus credits!', 'ok');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// compose a shareable badge image on a canvas (zero-dep) and download it
|
||||||
|
function shareBadge(key, d) {
|
||||||
|
const b = BADGES.find(x => x.key === key); if (!b) return;
|
||||||
|
const c = document.createElement('canvas'); c.width = 1200; c.height = 630;
|
||||||
|
const x = c.getContext('2d');
|
||||||
|
const g = x.createLinearGradient(0, 0, 1200, 630);
|
||||||
|
g.addColorStop(0, '#041109'); g.addColorStop(1, '#0b2018');
|
||||||
|
x.fillStyle = g; x.fillRect(0, 0, 1200, 630);
|
||||||
|
x.strokeStyle = 'rgba(67,232,195,.5)'; x.lineWidth = 6; x.strokeRect(24, 24, 1152, 582);
|
||||||
|
x.fillStyle = '#43e8c3'; x.font = 'bold 34px Sora, sans-serif'; x.fillText('InstantAdPay', 70, 100);
|
||||||
|
x.fillStyle = '#8ba69c'; x.font = '22px Sora, sans-serif'; x.fillText('ACHIEVEMENT UNLOCKED', 70, 150);
|
||||||
|
// medal circle
|
||||||
|
x.beginPath(); x.arc(600, 320, 110, 0, Math.PI * 2);
|
||||||
|
const mg = x.createRadialGradient(600, 290, 20, 600, 320, 110);
|
||||||
|
mg.addColorStop(0, 'rgba(67,232,195,.45)'); mg.addColorStop(1, 'rgba(9,26,19,.9)');
|
||||||
|
x.fillStyle = mg; x.fill(); x.strokeStyle = '#43e8c3'; x.lineWidth = 5; x.stroke();
|
||||||
|
x.fillStyle = '#eef7f3'; x.textAlign = 'center';
|
||||||
|
x.font = 'bold 64px Sora, sans-serif'; x.fillText(b.label, 600, 500);
|
||||||
|
x.font = '26px Sora, sans-serif'; x.fillStyle = '#8ba69c'; x.fillText(b.sub, 600, 545);
|
||||||
|
const who = d.username ? '@' + d.username : d.memberId ? 'member #' + d.memberId : '';
|
||||||
|
if (who) { x.fillStyle = '#43e8c3'; x.font = 'bold 30px Sora, sans-serif'; x.fillText(who, 600, 590); }
|
||||||
|
x.textAlign = 'left';
|
||||||
|
try {
|
||||||
|
c.toBlob(bl => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(bl);
|
||||||
|
a.download = 'instantadpay-' + key + '-badge.png';
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||||
|
IAP.status('Badge image saved — share it anywhere.', 'ok');
|
||||||
|
}, 'image/png');
|
||||||
|
} catch (e) { IAP.status('Could not generate the image.', 'bad'); }
|
||||||
|
}
|
||||||
|
|
||||||
// milestone stepper under "Your next move": lit nodes for what's done,
|
// milestone stepper under "Your next move": lit nodes for what's done,
|
||||||
// amber glow on the current target — the same ladder the contract pays
|
// amber glow on the current target — the same ladder the contract pays
|
||||||
function renderSteps(d) {
|
function renderSteps(d) {
|
||||||
@@ -147,6 +211,7 @@
|
|||||||
if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; }
|
if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; }
|
||||||
setInboxBadge(d.inboxUnread || 0);
|
setInboxBadge(d.inboxUnread || 0);
|
||||||
if (d.sponsorMsg) showSponsorModal(d.sponsorMsg);
|
if (d.sponsorMsg) showSponsorModal(d.sponsorMsg);
|
||||||
|
renderBadges(d);
|
||||||
loadCharts(d);
|
loadCharts(d);
|
||||||
$('nextMove').textContent = nextMove(d);
|
$('nextMove').textContent = nextMove(d);
|
||||||
renderSteps(d);
|
renderSteps(d);
|
||||||
|
|||||||
@@ -259,6 +259,18 @@ textarea{resize:vertical;font:inherit}
|
|||||||
#boBurger{display:block}
|
#boBurger{display:block}
|
||||||
.bo-content{padding:18px}
|
.bo-content{padding:18px}
|
||||||
}
|
}
|
||||||
|
/* ── achievement badges ── */
|
||||||
|
.badges{display:flex;gap:16px;flex-wrap:wrap;margin-top:12px}
|
||||||
|
.badge-a{display:flex;flex-direction:column;align-items:center;gap:8px;width:120px;text-align:center}
|
||||||
|
.badge-a .medal{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;position:relative;
|
||||||
|
background:radial-gradient(circle at 50% 35%,rgba(67,232,195,.28),rgba(9,26,19,.9));
|
||||||
|
border:2px solid var(--mint);box-shadow:0 0 18px rgba(67,232,195,.3)}
|
||||||
|
.badge-a.locked .medal{background:rgba(139,166,156,.08);border-color:var(--line-strong);box-shadow:none;filter:grayscale(1);opacity:.5}
|
||||||
|
.badge-a .medal svg{width:38px;height:38px;stroke:var(--mint);fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.badge-a.locked .medal svg{stroke:var(--muted)}
|
||||||
|
.badge-a .bl{font-size:12px;font-weight:700;line-height:1.2}
|
||||||
|
.badge-a .bs{font-size:10.5px;color:var(--muted)}
|
||||||
|
.badge-a .share{font-size:11px;color:var(--mint);cursor:pointer;background:none;border:0;padding:0;text-decoration:underline}
|
||||||
/* ── "Your next move": milestone stepper card ── */
|
/* ── "Your next move": milestone stepper card ── */
|
||||||
.next-card{position:relative;overflow:hidden;border-color:rgba(67,232,195,.28)}
|
.next-card{position:relative;overflow:hidden;border-color:rgba(67,232,195,.28)}
|
||||||
.next-card::before{content:"";position:absolute;inset:0;pointer-events:none;background:
|
.next-card::before{content:"";position:absolute;inset:0;pointer-events:none;background:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<title>The contract | InstantAdPay</title>
|
<title>The contract | InstantAdPay</title>
|
||||||
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
|
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -129,8 +129,8 @@
|
|||||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260905z"></script>
|
<script src="/assets/common.js?v=20260906a"></script>
|
||||||
<script src="/assets/contract.js?v=20260905z"></script>
|
<script src="/assets/contract.js?v=20260906a"></script>
|
||||||
<script src="/assets/chat.js?v=20260905z"></script>
|
<script src="/assets/chat.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+5
-5
@@ -5,7 +5,7 @@
|
|||||||
<title>InstantAdPay: advertise and earn, locked in code</title>
|
<title>InstantAdPay: advertise and earn, locked in code</title>
|
||||||
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
|
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -410,9 +410,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260905z"></script>
|
<script src="/assets/common.js?v=20260906a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260905z"></script>
|
<script src="/assets/wallet.js?v=20260906a"></script>
|
||||||
<script src="/assets/home.js?v=20260905z"></script>
|
<script src="/assets/home.js?v=20260906a"></script>
|
||||||
<script src="/assets/chat.js?v=20260905z"></script>
|
<script src="/assets/chat.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+4
-4
@@ -5,7 +5,7 @@
|
|||||||
<title>Live ledger | InstantAdPay</title>
|
<title>Live ledger | InstantAdPay</title>
|
||||||
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
|
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260905z"></script>
|
<script src="/assets/common.js?v=20260906a"></script>
|
||||||
<script src="/assets/ledger.js?v=20260905z"></script>
|
<script src="/assets/ledger.js?v=20260906a"></script>
|
||||||
<script src="/assets/chat.js?v=20260905z"></script>
|
<script src="/assets/chat.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+9
-5
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
@@ -186,6 +186,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="nc-steps" id="ncSteps"></div>
|
<div class="nc-steps" id="ncSteps"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card" id="badgeCard" hidden>
|
||||||
|
<div class="card-head"><h3>Your achievements</h3><span class="sub">unlock as you build</span></div>
|
||||||
|
<div class="badges" id="badgeStrip"></div>
|
||||||
|
</div>
|
||||||
<div class="grid c2">
|
<div class="grid c2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Recent activity</h3>
|
<h3>Recent activity</h3>
|
||||||
@@ -529,9 +533,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260905z"></script>
|
<script src="/assets/common.js?v=20260906a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260905z"></script>
|
<script src="/assets/wallet.js?v=20260906a"></script>
|
||||||
<script src="/assets/my.js?v=20260905z"></script>
|
<script src="/assets/my.js?v=20260906a"></script>
|
||||||
<script src="/assets/chat.js?v=20260905z"></script>
|
<script src="/assets/chat.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Transaction | InstantAdPay</title>
|
<title>Transaction | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="nav"></div>
|
<div id="nav"></div>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
|
<p><a href="/ledger">← Back to the live ledger</a> · <a href="/contract">Read the contract review</a></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="/assets/common.js?v=20260905z"></script>
|
<script src="/assets/common.js?v=20260906a"></script>
|
||||||
<script src="/assets/tx.js?v=20260905z"></script>
|
<script src="/assets/tx.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Viewing ad — InstantAdPay</title>
|
<title>Viewing ad — InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
<style>
|
<style>
|
||||||
html,body{height:100%;margin:0;overflow:hidden}
|
html,body{height:100%;margin:0;overflow:hidden}
|
||||||
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
|
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
|
||||||
@@ -43,6 +43,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
|
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/view.js?v=20260905z"></script>
|
<script src="/assets/view.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Banner wall | InstantAdPay</title>
|
<title>Banner wall | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260905z">
|
<link rel="stylesheet" href="/assets/site.css?v=20260906a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="nav"></div>
|
<div id="nav"></div>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="/assets/common.js?v=20260905z"></script>
|
<script src="/assets/common.js?v=20260906a"></script>
|
||||||
<script src="/assets/wall.js?v=20260905z"></script>
|
<script src="/assets/wall.js?v=20260906a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -461,6 +461,17 @@ const server = http.createServer(async (req, res) => {
|
|||||||
else { out.welcomeCredits = 0; out.gauntletPending = true; }
|
else { out.welcomeCredits = 0; out.gauntletPending = true; }
|
||||||
}
|
}
|
||||||
if (out.email) out.inboxUnread = await ads.unreadCount(out.email); // delivers pending solos too
|
if (out.email) out.inboxUnread = await ads.unreadCount(out.email); // delivers pending solos too
|
||||||
|
// achievement milestones (same ladder as the Overview stepper) + one-time credit bonuses
|
||||||
|
{
|
||||||
|
const bc = out.buyerCount || 0;
|
||||||
|
const reached = [];
|
||||||
|
if (out.memberId) reached.push('payouts');
|
||||||
|
if (bc >= 1) reached.push('firstBuyer');
|
||||||
|
if (bc >= 2) reached.push('level2');
|
||||||
|
if (bc >= 5) reached.push('level3');
|
||||||
|
out.milestonesReached = reached;
|
||||||
|
if (out.email && reached.length) out.milestonesGranted = await ads.grantMilestones(out.email, reached);
|
||||||
|
}
|
||||||
if (out.email) { // unmissable login modal when the upline sent a message
|
if (out.email) { // unmissable login modal when the upline sent a message
|
||||||
const un = await messages.newestUnread(out.email);
|
const un = await messages.newestUnread(out.email);
|
||||||
if (un) {
|
if (un) {
|
||||||
|
|||||||
Reference in New Issue
Block a user