diff options
| -rw-r--r-- | src/app.js | 73 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | tools/footer-support-test.js | 38 |
3 files changed, 59 insertions, 54 deletions
@@ -375,49 +375,39 @@ function setupFooter() { return h('div', { class: 'footer small muted center' }, h('div', { class: 'row gap6', style: 'justify-content:center' }, lang)); } +// The footer is the display preferences alone: the language this reads in, +// and the currency amounts are shown in. Everything else that used to sit +// here (credits, How it works, support, install, the theme, the commit) is +// on Settings → About (Adam, 2026-11-15). function footer() { - return h( - 'div', - { class: 'footer small muted center' }, - h( - 'div', - {}, - t('footerMadeBy') + ' ', - h('a', { href: 'https://adamsoltys.com', target: '_blank', rel: 'noopener' }, 'Adam Soltys'), - h('span', { class: 'faint' }, ' · '), - t('footerSourceOn') + ' ', - h('a', { href: 'https://git.coinos.io/coinosv3.git/', target: '_blank', rel: 'noopener' }, 'git.coinos.io') - ), - h( - '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 + return h('div', { class: 'footer small muted center' }, + h('div', { class: 'row gap6', style: 'justify-content:center' }, languagePicker(), currencyPicker())); +} + +function aboutSettingsView() { + const row = (label, onClick, attrs = {}) => h('button', { class: 'btn-block', onClick, ...attrs }, label); + return h('div', { class: 'col', style: 'gap:16px' }, + h('div', { class: 'card col about-card', style: 'gap:10px' }, + h('h3', {}, t('aboutTitle')), + h('p', { class: 'small muted', style: 'margin:0' }, + t('footerMadeBy').replace(/^./, (c) => c.toUpperCase()) + ' ', + h('a', { href: 'https://adamsoltys.com', target: '_blank', rel: 'noopener' }, 'Adam Soltys'), + '. ' + t('footerSourceOn').replace(/^./, (c) => c.toUpperCase()) + ' ', + h('a', { href: 'https://git.coinos.io/coinosv3.git/', target: '_blank', rel: 'noopener' }, 'git.coinos.io'), '.'), + row(t('howItWorks'), openHowItWorks), + // the hat shop pays from the wallet, 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 - // refresh, avoiding a layout shift when beforeinstallprompt fires late. - installable() - ? h('span', {}, h('span', { class: 'faint' }, ' · '), - h('button', { class: 'linklike', style: 'font-weight:400', onClick: triggerInstall }, t('installApp'))) + ? row(t('footerSupport'), () => { clearFeatureNav(); ui.screen = 'wallet'; featureHook('openHatShop'); }, { id: 'about-support' }) : null, - h('span', { class: 'faint' }, ' · '), - h('button', { class: 'linklike', style: 'font-weight:400', onClick: toggleTheme }, resolvedTheme() === 'dark' ? t('lightMode') : t('darkMode')) - ), - // Display preferences live together: the language this reads in, and the - // money amounts are shown in. (The currency sat under Settings → Network - // for exactly one day, where nobody found it.) - h('div', { class: 'row gap6', style: 'margin-top:8px;justify-content:center' }, languagePicker(), currencyPicker()), - // The commit stamp settles "is my PWA fresh?" AND "what code is this?" in - // one glance — its own last line, so it never wraps the credits on mobile. - h('div', { class: 'faint', style: 'margin-top:6px' }, - 'Running git commit ', - h('a', { class: 'faint', href: 'https://git.coinos.io/coinosv3.git/commit/?id={{COMMIT}}', target: '_blank', rel: 'noopener' }, '{{COMMIT}}')) - ); + // Chrome no longer prompts to install by itself: our own button, once + // it has said the app is installable (a persisted flag, see installable) + installable() ? row(t('installApp'), triggerInstall) : null, + row(resolvedTheme() === 'dark' ? t('lightMode') : t('darkMode'), toggleTheme, { id: 'about-theme' }), + // "is my PWA fresh?" and "what code is this?" in one line + h('div', { class: 'small faint', style: 'margin-top:2px' }, + 'Running git commit ', + h('a', { class: 'faint', href: 'https://git.coinos.io/coinosv3.git/commit/?id={{COMMIT}}', target: '_blank', rel: 'noopener' }, '{{COMMIT}}'))), + h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.settingsPage = null; render(); } }, t('back'))); } const THEME_KEY = 'btc-wallet-theme'; @@ -3257,6 +3247,7 @@ function settingsTab() { case 'nostr': return nostrSettingsView(); case 'notifications': return page(featureAll('notifySettingsCards').reverse()); case 'advanced': return advancedSettingsView(); + case 'about': return aboutSettingsView(); } const I = (d) => '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>'; const TILE_ICONS = { @@ -3267,6 +3258,7 @@ function settingsTab() { // a till receipt: paper with a torn foot and two lines of figures pos: I('<path d="M6 3h12v18l-2-1.5-2 1.5-2-1.5-2 1.5-2-1.5L6 21z"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="12" y1="16" x2="15" y2="16"/>'), notifications: I('<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>'), + about: I('<circle cx="12" cy="12" r="9"/><line x1="12" y1="11" x2="12" y2="16.5"/><circle cx="12" cy="7.8" r=".6" fill="currentColor"/>'), advanced: I('<line x1="4" y1="8" x2="20" y2="8"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="9" cy="8" r="2.2" fill="var(--surface,#fff)"/><circle cx="15" cy="16" r="2.2" fill="var(--surface,#fff)"/>'), }; const tile = (id, label, desc) => @@ -3292,6 +3284,7 @@ function settingsTab() { tile('nostr', t('nostrSettings'), t('settingsNostrDesc')), tile('notifications', t('settingsNotifications'), t('settingsNotifDesc')), tile('advanced', t('advancedSettings'), t('settingsAdvancedDesc')), + tile('about', t('aboutTitle'), t('settingsAboutDesc')), ]), h('button', { class: 'btn-ghost btn-block', style: 'margin-top:6px', onClick: () => { ui.tab = wallet.offline ? 'settings' : 'history'; render(); } }, t('back')) ); diff --git a/src/i18n.js b/src/i18n.js index c901576..121c806 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1676,6 +1676,8 @@ const STR = { settingsNostrDesc: 'Identity and connections', settingsNotifDesc: 'What buzzes this device', settingsAdvancedDesc: 'Network, rarities, rescues', + aboutTitle: 'About', + settingsAboutDesc: 'How it works, support, app, theme', settingsPosDesc: 'Till mode, tips and the /pos link', unlockForLabel: 'Stay unlocked', unlockFor5m: '5 minutes', diff --git a/tools/footer-support-test.js b/tools/footer-support-test.js index a37c070..93ae8a1 100644 --- a/tools/footer-support-test.js +++ b/tools/footer-support-test.js @@ -1,5 +1,6 @@ -// 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). +// "Show your support" lives on Settings → About (2026-11-15: the footer is +// just the language and currency now) and opens the hat shop; it shows only +// 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'; @@ -18,31 +19,40 @@ const browser = await puppeteer.launch({ executablePath: process.env.CHROME || ' 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')); +const link = () => page.evaluate(() => !!document.getElementById('about-support')); +const footerOnly = () => page.evaluate(() => { const f = document.querySelector('.footer'); return f ? { selects: f.querySelectorAll('select').length, other: f.querySelectorAll('button, a').length, text: f.innerText.replace(/\s+/g, ' ').trim() } : null; }); +const openAbout = async () => { + await page.evaluate(() => [...document.querySelectorAll('button')].find((e) => e.getAttribute('aria-label') === 'Settings').click()); await sleep(500); + await page.evaluate(() => [...document.querySelectorAll('.settings-tile')].find((e) => /^About/.test(e.textContent.trim())).click()); await sleep(500); +}; 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('#onb-signin'); await page.click('#onb-signin'); await sleep(400); + await page.click('#onb-restore'); 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()); + const f = await footerOnly(); + check('the footer is the language and currency alone', f && f.selects === 2 && f.other === 0, JSON.stringify(f)); + await openAbout(); + const about = await page.evaluate(() => document.querySelector('.about-card')?.innerText || ''); + check('About has the credits, How it works, the theme and the commit', /Adam Soltys/.test(about) && /git\.coinos\.io/.test(about) && /How it works/.test(about) && /(Dark|Light) mode/.test(about) && /Running git commit [0-9a-f]{7}/.test(about), about.replace(/\n/g, ' | ')); + check('...and "Show your support"', await link()); + const th = await page.evaluate(() => document.documentElement.dataset.theme); + await page.click('#about-theme'); await sleep(300); + check('the theme switch works from there', await page.evaluate(() => document.documentElement.dataset.theme) !== th); + await page.click('#about-theme'); await sleep(300); + if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT }); + await page.click('#about-support'); 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('it opens the hat shop', /hat/i.test(txt), txt.slice(0, 160).replace(/\n/g, ' | ')); 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'); |
