Buy packages pane inside the member back office

Members buy directly from the dashboard: live-priced tiles in a dedicated
menu section, wallet flow with buy-time sponsor resolution, dashboard
refresh on settle. No more round-trip to the homepage. Assets v=20260905e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-05 06:07:33 -05:00
parent cdeb59ed7f
commit a4c7028c0e
5 changed files with 67 additions and 20 deletions
+39 -2
View File
@@ -68,8 +68,8 @@
}
// ── back-office menu: hash-routed panes ───────────────
const PANES = ['overview', 'line', 'campaigns', 'earn', 'earnings', 'wallet'];
const TITLES = { overview: 'Overview', line: 'My line', campaigns: 'Campaigns',
const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'wallet'];
const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns',
earn: 'Earn credits', earnings: 'Earnings', wallet: 'Wallet & account' };
function setPane(name) {
if (!PANES.includes(name)) name = 'overview';
@@ -202,6 +202,43 @@
// defers the busy() lookup to click time (busy is declared below)
function busy2(btn, fn) { return (...a) => busy(btn, fn)(...a); }
// ── in-dashboard package buying ──
const PKG = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' };
async function loadBuyTiles() {
try {
const { products } = await (await fetch('/api/catalog')).json();
const wrap = $('boTiles');
wrap.innerHTML = '';
for (const p of products) {
const bonus = p.creditAmount - p.priceCents;
const div = document.createElement('div');
div.className = 'tile' + (p.priceCents === 5000 ? ' hot' : '');
div.innerHTML = '<div class="name">' + (PKG[p.id] || 'Package ' + p.id) + '</div>'
+ '<div class="price">$' + Math.round(p.priceCents / 100) + '</div>'
+ '<div class="cr">' + p.creditAmount.toLocaleString() + ' credits</div>'
+ '<div class="bonus">' + (bonus > 0 ? '+' + bonus.toLocaleString() + ' bonus credits' : '&nbsp;') + '</div>'
+ '<div class="pol">' + (p.costWei ? IAP.fmtPol(p.costWei) + ' POL right now' : 'paused') + '</div>'
+ '<button class="btn small" data-id="' + p.id + '" data-cost="' + (p.costWei || '') + '"'
+ (p.costWei ? '' : ' disabled') + '>Buy</button>';
wrap.appendChild(div);
}
wrap.querySelectorAll('button[data-id]').forEach(b => b.addEventListener('click', async () => {
try {
b.disabled = true;
const spNow = await (await fetch('/api/sponsor')).json();
IAP.status('Confirm the purchase in your wallet…');
const r = await IAPWallet.buy(Number(b.dataset.id), spNow.sponsorId || 0, b.dataset.cost);
if (r.status !== '0x1' && r.receipt && r.receipt.status !== '0x1') throw new Error('Transaction reverted.');
IAP.status('Purchase settled on-chain. Credits are in your account.', 'ok');
await render();
loadBuyTiles();
} catch (e) { IAP.status('Purchase failed: ' + ((e && e.message) || e), 'bad'); }
finally { b.disabled = false; }
}));
} catch (e) {}
}
loadBuyTiles();
// ── earn-by-viewing: daily ad set with dwell, then claim ──
const earnState = { types: ['banner', 'text'], i: 0, timer: null };
async function earnRefresh() {