aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-13 09:48:54 -0700
committerAdam Soltys <[email protected]>2026-09-13 09:48:54 -0700
commit38b3d091e8e1bece958c0d7e80a5ee78434de933 (patch)
treea6ef243527c8f8c9499d90ffb9e2c43d9d03a8b2
parent810d3d8bfd523a8a2a4a51cd2e426d72af049d13 (diff)
downloadcoinosv3-38b3d091e8e1bece958c0d7e80a5ee78434de933.tar.gz
coinosv3-38b3d091e8e1bece958c0d7e80a5ee78434de933.zip
The front door points at your own keys.
Four buttons of equal weight, and only the sign-in ones said what they did. "Get started" said nothing, so the concrete offer on that screen was Google — which is a convenience wrapped around a seed phrase, not an alternative to one. Now the page says what the wallet is before it offers a button, the way in reads "Create a new wallet" with the cost under it (a minute, twelve words), and the three sign-ins sit below a line labelled "or, if you already have an account". And the second half of the same problem: a wallet that arrives by sign-in has a recovery phrase its owner has never seen and nobody can reissue. So the wizard shows it once, right after the first login, while it still reads as setting up rather than as a warning out of nowhere. "I've written it down" is the way on; "Later" is a real answer, and the wallet then keeps one quiet line — not a modal, not dismissible, gone the moment the phrase is actually seen, here or in settings. seedSeen records that per account: creating shows the words, importing means they typed them, revealing in settings counts. Only a derived seed starts false, which is exactly the case worth asking about. tools/onboard-backup-test.js covers both halves, including that "Later" leaves a reminder that survives a reload and leads somewhere. The sixteen tests that clicked the old button by its text are updated, and it has an id now so its copy can change without breaking them again. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
-rw-r--r--src/app.js119
-rw-r--r--src/i18n.js11
-rw-r--r--src/style.css9
-rw-r--r--tools/avatar-thumb-test.js2
-rw-r--r--tools/boot-balance-test.js2
-rw-r--r--tools/boot-signin-flash-test.js6
-rw-r--r--tools/chat-layout-test.js2
-rw-r--r--tools/feed-follow-test.js2
-rw-r--r--tools/lock-wallet-test.js8
-rw-r--r--tools/minimal-smoke.js2
-rw-r--r--tools/onboard-backup-test.js121
-rw-r--r--tools/padlock-ask-test.js2
-rw-r--r--tools/padlock-flow-test.js4
-rw-r--r--tools/post-actions-test.js2
-rw-r--r--tools/profile-batch-test.js2
-rw-r--r--tools/profile-draft-test.js2
-rw-r--r--tools/profile-feed-test.js2
-rw-r--r--tools/profile-open-test.js2
-rw-r--r--tools/push-unavailable-test.js2
-rw-r--r--tools/quote-card-test.js2
-rw-r--r--tools/ui-morph-test.js4
-rw-r--r--tools/unifiedpush-test.js2
-rw-r--r--tools/youtube-embed-test.js2
-rw-r--r--tools/zap-tally-cache-test.js2
24 files changed, 269 insertions, 45 deletions
diff --git a/src/app.js b/src/app.js
index 4914528..69fcf1a 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1280,6 +1280,12 @@ async function enterWallet(mnemonic, passphrase, opts = {}) {
mnemonic: (mnemonic || '').trim().replace(/\s+/g, ' '),
passphrase: passphrase || '',
});
+ // Has this person ever had the recovery phrase in front of them? Creating
+ // a wallet shows it; importing one means they typed it. A sign-in DERIVES
+ // it — Google, a passkey and a nostr key all end up at a seed the user has
+ // never seen, and which nobody can reissue for them. That is the one case
+ // worth asking about, so it's recorded once and never guessed at again.
+ if (acc.seedSeen === undefined) acc.seedSeen = !opts.nostrPubkey;
await activateAccount(acc, { ...opts, fresh: true });
}
@@ -2888,7 +2894,12 @@ function recoveryCard(a) {
a.passphrase ? copyBtn(a.passphrase, t('copyPassphrase')) : null,
h('button', { class: 'btn-sm grow', onClick: () => { ui.revealShown = false; render(); } }, t('hide')))
: h('div', { class: 'row gap6' },
- h('button', { class: 'btn-primary grow', onClick: () => { ui.revealShown = 'words'; render(); } }, t('revealWords')),
+ h('button', { class: 'btn-primary grow', onClick: () => {
+ ui.revealShown = 'words';
+ // seeing it here counts: no need to be asked again elsewhere
+ if (!a.seedSeen) { a.seedSeen = true; persistAccounts(); if (a.persisted) writeVault(); }
+ render();
+ } }, t('revealWords')),
h('button', { class: 'btn-sm', onClick: () => { ui.revealShown = false; render(); } }, t('hide')))
);
}
@@ -3080,14 +3091,23 @@ function onboardScreen() {
// the login finishes.
if ((o.step === 'welcome' || o.step === 'seed' || o.step === 'signin')
&& ui.screen === 'wallet' && activeAccount() && !ui.nostrLoginBusy) {
- ui.onb = null;
- try { localStorage.removeItem(ONB_STEP_KEY); } catch {}
- // On-chain first: the wizard's promise is "your keys, your coins", and
- // Spending stays out of sight until the user sets it up.
- ui.account = 'savings';
- try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {}
- ui.tab = 'history';
- return null; // the caller falls through to the wallet itself
+ // A wallet that arrived by SIGN-IN has a recovery phrase its owner has
+ // never seen. Show it once, here, while it still reads as part of
+ // setting up — rather than leaving it buried in settings until the day
+ // they lose the Google account and find out it was never a backup.
+ // Handled further down, where page()/title() exist.
+ const arrived = activeAccount();
+ if (arrived && arrived.mnemonic && !arrived.seedSeen) o.step = 'backup';
+ else {
+ ui.onb = null;
+ try { localStorage.removeItem(ONB_STEP_KEY); } catch {}
+ // On-chain first: the wizard's promise is "your keys, your coins", and
+ // Spending stays out of sight until the user sets it up.
+ ui.account = 'savings';
+ try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {}
+ ui.tab = 'history';
+ return null; // the caller falls through to the wallet itself
+ }
}
if (o.step === 'spend') {
const addr = featureHook('namesAddress');
@@ -3164,14 +3184,24 @@ function onboardScreen() {
return page([
h('div', { class: 'onb-hero' }, brandHeader(false)),
title(t('onbWelcomeTitle')),
- // Starting is the primary path; bringing a nostr account is the
- // alternative, offered here rather than as a question of its own.
- h('button', { class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: () => {
- ui.unlockTab = 'create';
- ui.unlockError = '';
- o.step = 'seed';
- render();
- } }, t('onbStart')),
+ // What this wallet IS, before the buttons. Four equally weighted
+ // buttons with only the sign-in ones saying what they do sent people
+ // straight past the seed phrase — which is the wallet — into a login
+ // that is only a convenience wrapped around it.
+ h('p', { class: 'muted', style: 'margin:0' }, t('onbWelcomeBody')),
+ // Making your own keys is the path; a sign-in is the alternative,
+ // below the line.
+ h('button', {
+ // a stable hook for tests, so this button survives its own copy
+ id: 'onb-create',
+ class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: () => {
+ ui.unlockTab = 'create';
+ ui.unlockError = '';
+ o.step = 'seed';
+ render();
+ } }, t('onbStart')),
+ h('p', { class: 'small muted', style: 'margin:-6px 0 0;text-align:center' }, t('onbStartHint')),
+ h('div', { class: 'onb-or' }, h('span', {}, t('onbOrSignIn'))),
// All the sign-in doors right on the welcome screen (Google, passkey,
// Nostr) — the feature supplies them; the single-button fallback keeps
// builds without it working.
@@ -3186,6 +3216,42 @@ function onboardScreen() {
ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
]);
}
+ if (o.step === 'backup') {
+ const acc = activeAccount();
+ // the wallet went away (switched, logged out) — nothing to back up
+ if (!acc || !acc.mnemonic) { ui.onb = null; try { localStorage.removeItem(ONB_STEP_KEY); } catch {} return null; }
+ const words = acc.mnemonic.split(' ');
+ const done = () => {
+ acc.seedSeen = true;
+ persistAccounts();
+ if (acc.persisted) writeVault();
+ ui.onb = null;
+ try { localStorage.removeItem(ONB_STEP_KEY); } catch {}
+ ui.account = 'savings';
+ try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {}
+ ui.tab = 'history';
+ render();
+ };
+ return page([
+ title(t('onbBackupTitle')),
+ h('p', { class: 'muted', style: 'margin:0' }, t('onbBackupBody')),
+ h('div', { class: 'warn-box' }, t('writeDownWarn')),
+ h('div', { class: 'words' }, words.map((w, i) =>
+ h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w)))),
+ h('div', { class: 'row gap6' }, copyBtn(acc.mnemonic, t('copyPhrase'))),
+ h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: done }, t('onbBackupDone')),
+ // Later means later, not never: seedSeen stays false, so the wallet
+ // keeps a quiet reminder until the phrase has actually been seen.
+ h('button', { class: 'btn-ghost btn-block', onClick: () => {
+ ui.onb = null;
+ try { localStorage.removeItem(ONB_STEP_KEY); } catch {}
+ ui.account = 'savings';
+ try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {}
+ ui.tab = 'history';
+ render();
+ } }, t('onbBackupLater')),
+ ]);
+ }
if (o.step === 'seed') {
// The classic create/import card, inside the wizard: getting started
// means seeing (or bringing) your seed phrase, not a wallet minted
@@ -3439,12 +3505,31 @@ function walletScreen() {
brandHeader(true),
h('div', { class: 'mt16' }, balanceCard()),
ui.offlineFallback && wallet.offline ? offlineBanner() : null,
+ backupNotice(),
...featureAll('walletNotices'),
tabsBar(),
pane
);
}
+// A wallet whose recovery phrase has never been on screen keeps one quiet
+// line about it. "Later" is a real answer, so this is a row and not a modal —
+// but it doesn't dismiss, because the risk doesn't go away on its own. It
+// disappears the moment the phrase is actually seen (here or in settings).
+function backupNotice() {
+ const a = activeAccount();
+ if (!a || !a.mnemonic || a.seedSeen || wallet.watchOnly) return null;
+ return h('div', { class: 'small muted', style: 'margin:10px 0 0;text-align:center' },
+ t('backupReminder'),
+ ' ',
+ h('button', { class: 'linklike small', onClick: () => {
+ ui.tab = 'settings';
+ ui.settingsPage = 'wallet';
+ ui.revealShown = 'masked';
+ render();
+ } }, t('backupReminderGo')));
+}
+
function offlineBanner() {
return h(
'div',
diff --git a/src/i18n.js b/src/i18n.js
index 3faa843..6a46823 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -716,7 +716,16 @@ const STR = {
or: 'or',
nlSignIn: 'Sign in',
onbWelcomeTitle: 'Welcome to coinos',
- onbStart: 'Get started',
+ onbWelcomeBody: 'Bitcoin you actually hold. Your wallet is twelve words — nobody else ever has them, and they work in any wallet, anywhere.',
+ onbStart: 'Create a new wallet',
+ onbStartHint: "Takes a minute: write down twelve words and it's yours",
+ onbOrSignIn: 'or, if you already have an account',
+ onbBackupTitle: 'Write down your recovery phrase',
+ onbBackupBody: "Signing in was the easy part — but the sign-in isn't your wallet. These twelve words are. They're the only way back in if you lose access to that account, and nobody can reissue them for you.",
+ onbBackupDone: "I've written it down",
+ onbBackupLater: 'Later',
+ backupReminder: 'Your recovery phrase is still unwritten.',
+ backupReminderGo: 'Write it down',
onbSeedTitle: 'Set up your wallet',
onbSigninTitle: 'Sign in',
onbLegacyTitle: 'Move your coinos.io account',
diff --git a/src/style.css b/src/style.css
index a16101c..c9b4368 100644
--- a/src/style.css
+++ b/src/style.css
@@ -865,6 +865,15 @@ body::after {
column to the 420px cap even on narrower phones, overflowing the page. */
.onb { width: 100%; max-width: 420px; margin: 8vh auto 0; padding: 0 8px; }
.onb-title { margin: 0; font-size: 26px; letter-spacing: -0.02em; }
+/* The line between making your own keys and signing in with someone else's
+ account. Without it the four buttons read as four equal choices. */
+.onb-or {
+ display: flex; align-items: center; gap: 12px; margin: 4px 0 -4px;
+ color: var(--faint); font-size: 13px;
+}
+.onb-or::before, .onb-or::after {
+ content: ''; flex: 1; height: 1px; background: var(--line);
+}
.onb-hero { margin-bottom: 8px; }
.onb-punks { display: flex; gap: 10px; overflow-x: auto; padding: 6px 2px; }
.onb-punk { width: 56px; height: 56px; border-radius: 50%; flex-shrink: 0; cursor: pointer; border: 3px solid transparent; transition: transform .15s, border-color .15s; }
diff --git a/tools/avatar-thumb-test.js b/tools/avatar-thumb-test.js
index 727c49a..c776179 100644
--- a/tools/avatar-thumb-test.js
+++ b/tools/avatar-thumb-test.js
@@ -73,7 +73,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mnemonic = generateMnemonic(wordlist);
diff --git a/tools/boot-balance-test.js b/tools/boot-balance-test.js
index 96f47a9..c34712b 100644
--- a/tools/boot-balance-test.js
+++ b/tools/boot-balance-test.js
@@ -28,7 +28,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
await page.type('textarea', generateMnemonic(wordlist));
diff --git a/tools/boot-signin-flash-test.js b/tools/boot-signin-flash-test.js
index e46ee72..b29bc63 100644
--- a/tools/boot-signin-flash-test.js
+++ b/tools/boot-signin-flash-test.js
@@ -1,7 +1,7 @@
// A signed-in wallet must never flash the signed-out welcome screen. Anything
// that renders before boot has restored the session (a deferred feature chunk
// landing, an emitter tick) is rendering without knowing whether anyone is
-// signed in, and "Get started" over a wallet that's about to open is the one
+// signed in, and the start page over a wallet that's about to open is the one
// wrong guess that's unmistakable.
//
// The race is widened here (the locale fetch is delayed, so the deferred
@@ -27,7 +27,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
await page.type('textarea', generateMnemonic(wordlist));
@@ -48,7 +48,7 @@ try {
window.__sawSignin = null;
const tick = () => {
const txt = (document.body && document.body.innerText) || '';
- if (!window.__sawSignin && /get started|sign in with/i.test(txt)) window.__sawSignin = Math.round(performance.now());
+ if (!window.__sawSignin && /create a new wallet|sign in with/i.test(txt)) window.__sawSignin = Math.round(performance.now());
if (!window.__walletAt && document.querySelector('.balance')) window.__walletAt = Math.round(performance.now());
if (!window.__walletAt) requestAnimationFrame(tick);
};
diff --git a/tools/chat-layout-test.js b/tools/chat-layout-test.js
index 853aafa..772e38f 100644
--- a/tools/chat-layout-test.js
+++ b/tools/chat-layout-test.js
@@ -43,7 +43,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('button', 'Get started');
+ await click('button', 'Create a new wallet');
await sleep(300);
await click('button', 'Import existing');
await sleep(300);
diff --git a/tools/feed-follow-test.js b/tools/feed-follow-test.js
index 4c1fed3..ae486a6 100644
--- a/tools/feed-follow-test.js
+++ b/tools/feed-follow-test.js
@@ -31,7 +31,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mnemonic = generateMnemonic(wordlist);
diff --git a/tools/lock-wallet-test.js b/tools/lock-wallet-test.js
index 8db392a..494195e 100644
--- a/tools/lock-wallet-test.js
+++ b/tools/lock-wallet-test.js
@@ -44,7 +44,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('button', 'Get started');
+ await click('button', 'Create a new wallet');
await sleep(500);
await click('button', 'Import existing');
await sleep(400);
@@ -76,7 +76,7 @@ try {
await logout();
txt = await body();
check('it does not ask a second time', !/protect this device/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
- check('logout means the front door', /create new|get started|sign in/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
+ check('logout means the front door', /create a new wallet|sign in/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
await page.reload({ waitUntil: 'domcontentloaded' });
check('and a saved passwordless wallet is one reload away', await waitText('receive'));
await sleep(1000);
@@ -109,7 +109,7 @@ try {
check('back in the wallet', await waitText('receive'));
await logout();
txt = await body();
- check('logout is still the front door', /create new|get started|sign in/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
+ check('logout is still the front door', /create a new wallet|sign in/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
check('the wallet is still saved', (await vaultSize()) > 0);
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(800);
@@ -148,7 +148,7 @@ try {
await sleep(1200);
txt = await body();
check('everything is gone', (await vaultSize()) === 0);
- check('back at the start page', /get started/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
+ check('back at the start page', /create a new wallet/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
} catch (e) {
check('run completed', false, e.message);
} finally {
diff --git a/tools/minimal-smoke.js b/tools/minimal-smoke.js
index f3f896c..2bdc4a6 100644
--- a/tools/minimal-smoke.js
+++ b/tools/minimal-smoke.js
@@ -65,7 +65,7 @@ try {
await sleep(400);
console.log('\n[1] import + core wallet');
- await clickText('button', 'Get started'); // the front door gained a step
+ await clickText('button', 'Create a new wallet'); // the front door gained a step
await sleep(300);
await clickText('button', 'Import existing');
await sleep(200);
diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js
new file mode 100644
index 0000000..3b242dd
--- /dev/null
+++ b/tools/onboard-backup-test.js
@@ -0,0 +1,121 @@
+// Two things about the front door.
+//
+// One: making your own keys has to look like the way in. Four equally
+// weighted buttons, where only the sign-in ones said what they did, sent
+// people straight past the seed phrase — which IS the wallet — into a login
+// that is only a convenience wrapped around one.
+//
+// Two: a wallet that arrived by SIGN-IN has a recovery phrase its owner has
+// never seen, and nobody can reissue it. So it gets shown once, during
+// setup, and if they put it off the wallet keeps one quiet line about it.
+//
+// Run: bun tools/onboard-backup-test.js
+import puppeteer from 'puppeteer-core';
+import { bech32 } from '@scure/base';
+import { generateSecretKey } from 'nostr-tools/pure';
+import { seedFromNostrKey } from '../src/nostr-login.js';
+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: 5291, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+
+const click = (page, t, sel = 'button') => page.evaluate((s, x) => {
+ const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x));
+ if (e) { e.click(); return true; } return false;
+}, sel, t.toLowerCase());
+const text = (page) => page.evaluate(() => document.body.innerText);
+const waitText = async (page, x, ms = 30000) => {
+ for (let i = 0; i < ms / 250; i++) { if ((await text(page)).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); }
+ return false;
+};
+
+// Sign in with a pasted nsec and stop at whatever the wizard shows next.
+// Its own browser context, so each run meets a genuinely fresh front door
+// rather than the wallet the previous one left in localStorage.
+async function signIn() {
+ const ctx = await browser.createBrowserContext();
+ const page = await ctx.newPage();
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ // wait for the front door to actually be painted before poking it
+ await waitText(page, 'sign in with nostr');
+ const sk = generateSecretKey();
+ const nsec = bech32.encode('nsec', bech32.toWords(sk), 1000);
+ await click(page, 'sign in with nostr');
+ await waitText(page, 'log in with nostr');
+ await page.waitForSelector('input[placeholder*="nsec" i], textarea[placeholder*="nsec" i]', { timeout: 15000 });
+ await page.type('input[placeholder*="nsec" i], textarea[placeholder*="nsec" i]', nsec);
+ await click(page, 'log in');
+ return { page, ctx, seed: seedFromNostrKey(sk) };
+}
+
+try {
+ console.log('[the start page]');
+ {
+ const page = await browser.newPage();
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' });
+ await sleep(1500);
+ const front = await page.evaluate(() => {
+ const btns = [...document.querySelectorAll('button')].filter((b) => b.offsetParent);
+ const primary = document.getElementById('onb-create');
+ const idx = btns.indexOf(primary);
+ const signins = btns.filter((b) => /sign in with/i.test(b.textContent));
+ return {
+ label: primary ? primary.textContent.trim() : null,
+ first: idx === 0 || (idx >= 0 && idx < btns.findIndex((b) => /sign in with/i.test(b.textContent))),
+ primaryTop: primary ? primary.getBoundingClientRect().top : -1,
+ signinTop: signins.length ? signins[0].getBoundingClientRect().top : -1,
+ divider: !!document.querySelector('.onb-or'),
+ body: (document.querySelector('.onb p') || {}).textContent || '',
+ };
+ });
+ check('the way in says what it makes', /wallet/i.test(front.label || ''), front.label || 'missing');
+ check('...and comes before the sign-in buttons', front.first && front.primaryTop < front.signinTop,
+ `create at ${Math.round(front.primaryTop)}, first sign-in at ${Math.round(front.signinTop)}`);
+ check('...with a line between them, so they read as the alternative', front.divider);
+ check('the page says what the wallet actually is', /twelve words|seed|phrase/i.test(front.body),
+ front.body.slice(0, 60) + '…');
+ await page.close();
+ }
+
+ console.log('\n[signing in, then writing it down]');
+ {
+ const { page, ctx, seed } = await signIn();
+ check('a sign-in is asked to write the phrase down', await waitText(page, 'write down your recovery phrase'));
+ const shown = await page.evaluate(() => [...document.querySelectorAll('.words .w .t')].map((e) => e.textContent.trim()));
+ check('...and the real twelve words are on screen', shown.length === 12 && shown.join(' ') === seed,
+ shown.length + ' words' + (shown.join(' ') === seed ? ', matching the derived seed' : ', WRONG'));
+ await click(page, "i've written it down");
+ check('confirming lands in the wallet', await waitText(page, 'receive'));
+ const nag = await text(page);
+ check('...and there is no reminder afterwards', !/still unwritten/i.test(nag));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ check('...not after a reload either', await waitText(page, 'receive') && !/still unwritten/i.test(await text(page)));
+ await ctx.close();
+ }
+
+ console.log('\n[signing in, then putting it off]');
+ {
+ const { page, ctx } = await signIn();
+ check('the ask appears', await waitText(page, 'write down your recovery phrase'));
+ await click(page, 'later');
+ check('later lands in the wallet too', await waitText(page, 'receive'));
+ check('...but the wallet keeps a quiet reminder', /still unwritten/i.test(await text(page)));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitText(page, 'receive');
+ check('...which survives a reload', /still unwritten/i.test(await text(page)));
+ // and it is a way IN, not just a scold
+ await click(page, 'write it down');
+ check('tapping it opens the recovery phrase', await waitText(page, 'recovery phrase'));
+ await ctx.close();
+ }
+} finally { await browser.close(); server.stop(true); }
+console.log(ok ? '\n✅ the front door points at your own keys' : '\n❌ failed');
+process.exit(ok ? 0 : 1);
diff --git a/tools/padlock-ask-test.js b/tools/padlock-ask-test.js
index 8007aad..eb35047 100644
--- a/tools/padlock-ask-test.js
+++ b/tools/padlock-ask-test.js
@@ -26,7 +26,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('button', 'get started');
+ await click('button', 'create a new wallet');
await sleep(500);
await click('button', 'Import existing');
await sleep(400);
diff --git a/tools/padlock-flow-test.js b/tools/padlock-flow-test.js
index 4b659aa..be4f5ec 100644
--- a/tools/padlock-flow-test.js
+++ b/tools/padlock-flow-test.js
@@ -29,7 +29,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('button', 'Get started');
+ await click('button', 'Create a new wallet');
await sleep(500);
await click('button', 'Import existing');
await sleep(400);
@@ -117,7 +117,7 @@ try {
await p2.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await p2.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click2('Get started');
+ await click2('Create a new wallet');
await sleep(500);
check('wallet A open', await importWallet());
await sleep(2000);
diff --git a/tools/post-actions-test.js b/tools/post-actions-test.js
index 8cc525c..06d4739 100644
--- a/tools/post-actions-test.js
+++ b/tools/post-actions-test.js
@@ -38,7 +38,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/profile-batch-test.js b/tools/profile-batch-test.js
index 335bfbc..bf8dcf4 100644
--- a/tools/profile-batch-test.js
+++ b/tools/profile-batch-test.js
@@ -68,7 +68,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/profile-draft-test.js b/tools/profile-draft-test.js
index 243f8ec..cb016da 100644
--- a/tools/profile-draft-test.js
+++ b/tools/profile-draft-test.js
@@ -41,7 +41,7 @@ try {
await page.goto(origin);
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
- await click('Get started');
+ await click('Create a new wallet');
await click('Import existing');
await page.waitForSelector('textarea');
const mnemonic = generateMnemonic(wordlist);
diff --git a/tools/profile-feed-test.js b/tools/profile-feed-test.js
index 1e7283b..d285c44 100644
--- a/tools/profile-feed-test.js
+++ b/tools/profile-feed-test.js
@@ -25,7 +25,7 @@ const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) {
try {
await page.goto('http://localhost:5234/', { waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('button', 'Get started');
+ await click('button', 'Create a new wallet');
await sleep(300);
await click('button', 'Import existing');
await sleep(300);
diff --git a/tools/profile-open-test.js b/tools/profile-open-test.js
index 7c88dee..da90af5 100644
--- a/tools/profile-open-test.js
+++ b/tools/profile-open-test.js
@@ -62,7 +62,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/push-unavailable-test.js b/tools/push-unavailable-test.js
index b285aee..4808501 100644
--- a/tools/push-unavailable-test.js
+++ b/tools/push-unavailable-test.js
@@ -38,7 +38,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/quote-card-test.js b/tools/quote-card-test.js
index 5225f4a..695ee93 100644
--- a/tools/quote-card-test.js
+++ b/tools/quote-card-test.js
@@ -38,7 +38,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/ui-morph-test.js b/tools/ui-morph-test.js
index 858a5a7..343ccd2 100644
--- a/tools/ui-morph-test.js
+++ b/tools/ui-morph-test.js
@@ -29,12 +29,12 @@ check('app booted to a screen with buttons', hasButton);
// walk into a wallet if the entry button exists
const clicked = await page.evaluate(() => {
- const btn = [...document.querySelectorAll('button')].find((b) => /get started/i.test(b.textContent));
+ const btn = [...document.querySelectorAll('button')].find((b) => /create a new wallet/i.test(b.textContent));
if (btn) { btn.click(); return true; }
return false;
});
await sleep(4000);
-check('entered next screen', await page.evaluate(() => document.body.textContent.length > 100), clicked ? '' : '(no Get started button — already in wallet)');
+check('entered next screen', await page.evaluate(() => document.body.textContent.length > 100), clicked ? '' : '(no start button — already in wallet)');
// node identity across background renders: tag a button, force renders, compare
const identity = await page.evaluate(async () => {
diff --git a/tools/unifiedpush-test.js b/tools/unifiedpush-test.js
index 022ead6..6a3819c 100644
--- a/tools/unifiedpush-test.js
+++ b/tools/unifiedpush-test.js
@@ -55,7 +55,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/youtube-embed-test.js b/tools/youtube-embed-test.js
index f9bbf0d..a70272c 100644
--- a/tools/youtube-embed-test.js
+++ b/tools/youtube-embed-test.js
@@ -22,7 +22,7 @@ await page.goto('http://localhost:5267/', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => { localStorage.setItem('btc-wallet-network', 'regtest'); localStorage.setItem('btc-wallet-theme', 'dark'); });
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
-await click('get started'); await sleep(500);
+await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mn = generateMnemonic(wordlist);
diff --git a/tools/zap-tally-cache-test.js b/tools/zap-tally-cache-test.js
index 98cfe7e..4e91260 100644
--- a/tools/zap-tally-cache-test.js
+++ b/tools/zap-tally-cache-test.js
@@ -24,7 +24,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
- await click('get started'); await sleep(500);
+ await click('create a new wallet'); await sleep(500);
await click('import existing'); await sleep(400);
await page.waitForSelector('textarea');
const mnemonic = generateMnemonic(wordlist);