aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 00:24:25 -0700
committerAdam Soltys <[email protected]>2026-10-09 00:24:25 -0700
commitb6819d91422adc10ea71ea5174c8e3e2badd1340 (patch)
tree1667f5f3a36a2e661f9dbbc9178f716a0c234080
parent02a6468807c6f4f34f56b2bafb091c1b6ff25f0e (diff)
downloadcoinosv3-b6819d91422adc10ea71ea5174c8e3e2badd1340.tar.gz
coinosv3-b6819d91422adc10ea71ea5174c8e3e2badd1340.zip
Footer: "Show your support" beside How it works opens the hat shop from any page (shown once a wallet is open, since the shop pays from it)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/app.js5
-rw-r--r--src/features/hats.js2
-rw-r--r--src/i18n.js1
-rw-r--r--tools/footer-support-test.js49
4 files changed, 57 insertions, 0 deletions
diff --git a/src/app.js b/src/app.js
index 6945c10..1bb837e 100644
--- a/src/app.js
+++ b/src/app.js
@@ -381,6 +381,11 @@ function footer() {
'div',
{ style: 'margin-top:4px' },
h('button', { class: 'linklike', style: 'font-weight:400', onClick: openHowItWorks }, t('howItWorks')),
+ // the hat shop: needs a wallet to pay from, so only once one is open
+ activeAccount() && FEATURES.some((f) => f.openHatShop)
+ ? h('span', {}, h('span', { class: 'faint' }, ' · '),
+ h('button', { class: 'linklike', style: 'font-weight:400', onClick: () => { clearFeatureNav(); ui.screen = 'wallet'; featureHook('openHatShop'); } }, t('footerSupport')))
+ : null,
// Chrome no longer prompts to install on its own — surface our own link
// once it reports the app is installable. We render from a persisted flag
// (not the live event) so the link is present on the first paint after a
diff --git a/src/features/hats.js b/src/features/hats.js
index 16849f2..a14fc2b 100644
--- a/src/features/hats.js
+++ b/src/features/hats.js
@@ -438,6 +438,8 @@ export function hatsFeature(ctx) {
node,
h('span', { class: 'ava-hat', style: posStyle(hat), html: hat.art }));
},
+ // The footer's "Show your support" link: straight into the shop.
+ openHatShop() { openShop(); try { window.scrollTo(0, 0); } catch {} return true; },
// Own-profile editor entry: the door to the shop.
hatShopEntry() {
return h('button', {
diff --git a/src/i18n.js b/src/i18n.js
index 0ec6366..e6452e1 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -658,6 +658,7 @@ const STR = {
profSaved: 'Profile published',
hats: 'Hats',
hatShopSupport: 'Support Coinos, Buy a Hat!',
+ footerSupport: 'Show your support',
hatShopTitle: 'Hat shop',
hatShopBlurb: 'Support Coinos and wear the proof on your avatar.',
hatShopOffline: 'The hat stand is unreachable right now.',
diff --git a/tools/footer-support-test.js b/tools/footer-support-test.js
new file mode 100644
index 0000000..a37c070
--- /dev/null
+++ b/tools/footer-support-test.js
@@ -0,0 +1,49 @@
+// The footer's "Show your support" link opens the hat shop, from any page,
+// and only shows once a wallet is open (the shop pays from it).
+// Run: bun tools/footer-support-test.js
+import puppeteer from './puppeteer.js';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+import { buildHtml } from '../build.js';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+const html = await buildHtml({ minify: true, pwa: false });
+const server = Bun.serve({ port: 0, fetch: (req) => {
+ const p = new URL(req.url).pathname;
+ if (p.startsWith('/punks') || p === '/verify-worker.js' || p.startsWith('/locales') || /\.js$/.test(p)) { const f = Bun.file('dist' + p); return f.size ? new Response(f) : new Response('', { status: 404 }); }
+ return new Response(html, { headers: { 'content-type': 'text/html' } });
+} });
+const browser = await puppeteer.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const errs = []; page.on('pageerror', (e) => errs.push(String(e)));
+const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t);
+const link = () => page.evaluate(() => [...document.querySelectorAll('.footer button')].some((b) => b.textContent.trim() === 'Show your support'));
+try {
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto(server.url.origin, { waitUntil: 'domcontentloaded' });
+ await sleep(600);
+ check('no link before there is a wallet', !(await link()));
+ await click('create a new wallet'); await sleep(500); await click('to import'); await sleep(400);
+ await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('open wallet');
+ for (let i = 0; i < 80 && !(await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.textContent.trim() === 'Send'))); i++) {
+ await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /^(not now|skip|continue|later)$/i.test(b.textContent.trim()))?.click());
+ await sleep(400);
+ }
+ await sleep(1500);
+ check('the footer has "Show your support"', await link());
+ // from another page: the feed
+ await page.evaluate(() => [...document.querySelectorAll('nav button, .bottom-nav button')].find((b) => /feed/i.test(b.textContent))?.click()); await sleep(800);
+ await page.evaluate(() => [...document.querySelectorAll('.footer button')].find((b) => b.textContent.trim() === 'Show your support').click());
+ await sleep(1200);
+ const txt = await page.evaluate(() => document.body.innerText);
+ check('it opens the hat shop', /hat/i.test(txt) && !!(await page.evaluate(() => document.querySelector('.hat-btn') || /Crown|Top hat|hats?/i.test(document.querySelector('.card')?.innerText || ''))), txt.slice(0, 160).replace(/\n/g, ' | '));
+ await page.evaluate(() => { document.querySelector('.bottom-nav, nav')?.remove(); window.scrollTo(0, 1e6); });
+ await sleep(300);
+ if (process.env.SHOT) await (await page.$('.footer')).screenshot({ path: process.env.SHOT });
+ else await page.screenshot({ path: '/tmp/footer-support.png', fullPage: true });
+ // || '/tmp/footer-support.png', fullPage: true });
+ check('no page errors', errs.length === 0, errs.join('; ').slice(0, 300));
+} catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(); }
+console.log(ok ? 'PASS' : 'FAIL');
+process.exit(ok ? 0 : 1);