aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-06 23:13:26 -0700
committerAdam Soltys <[email protected]>2026-11-06 23:13:26 -0700
commit2dc574fceac569dea95e57be4134ed4ecd757d40 (patch)
tree53baf962e819d9c19d37ab17e451272693e28f76
parentf698dd9b1c638092c5c313889c1ac94feb91245f (diff)
downloadcoinosv3-2dc574fceac569dea95e57be4134ed4ecd757d40.tar.gz
coinosv3-2dc574fceac569dea95e57be4134ed4ecd757d40.zip
The front door is two choices: "Create a new wallet" or "Sign in to an existing account". "Login page is getting busy" (Adam, 2026-11-06): it had grown a door per method — Google, passkey, Nostr, twelve words, coinos.io — under the way in, and a footer of credits, pickers and the build stamp under that. Now the welcome step holds the two choices and nothing else (no footer on the front-door steps; the language follows the browser's), and the second opens a sign-in step with the ways in: "Sign in with seed phrase" first (was "Restore from twelve words"), then the feature's doors (Google, passkey, Nostr), then "Migrate a coinos account". Back from the seed-phrase import, the Nostr card and the coinos.io move returns to the sign-in step. Tests follow the extra step: onboard-backup (two choices, no footer; the seed phrase first among the ways in), onboard-migrate-door (Back to the sign-in choices), amber-login, account-access (logout lands on the two choices)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
-rw-r--r--src/app.js48
-rw-r--r--src/i18n.js3
-rw-r--r--tools/account-access-test.js2
-rw-r--r--tools/amber-login-test.js1
-rw-r--r--tools/onboard-backup-test.js33
-rw-r--r--tools/onboard-migrate-door-test.js5
6 files changed, 58 insertions, 34 deletions
diff --git a/src/app.js b/src/app.js
index 2d29c4f..a7ab2fb 100644
--- a/src/app.js
+++ b/src/app.js
@@ -891,7 +891,10 @@ function renderInner() {
// not inside it — the page's entrance animation transforms it, and a
// transform makes fixed descendants position within the page.
const floaters = h('div', { class: 'page-floaters', style: 'display:contents' }, ...featureAll('pageFloaters'));
- morphChildren(root, [screen, floaters, footer(), ...(bottomNav ? [bottomNav] : []), ...(side ? [side] : []), ...(ui.lightbox ? [imageViewer()] : []), ...(callOv ? [callOv] : [])]);
+ // The front door is two choices and nothing else: no credits, pickers or
+ // build stamp under them (the language follows the browser's).
+ const frontDoor = !activeAccount() && ui.screen === 'unlock' && ui.onb && ['welcome', 'doors', 'migrate'].includes(ui.onb.step);
+ morphChildren(root, [screen, floaters, ...(frontDoor ? [] : [footer()]), ...(bottomNav ? [bottomNav] : []), ...(side ? [side] : []), ...(ui.lightbox ? [imageViewer()] : []), ...(callOv ? [callOv] : [])]);
promoteLazySrc(root); // sources on the nodes that actually made it into the page
try { document.documentElement.classList.toggle('no-scroll', !!(ui.lightbox || ui.mediaPager)); } catch {}
if (fpath) {
@@ -4184,23 +4187,34 @@ function onboardScreen() {
h('span', { style: 'display:flex;flex-shrink:0', html: NOSTR_MARK }),
t('nlSignIn')));
if (o.step === 'welcome') {
+ // Two ways in, nothing else: a new wallet, or one you already have. The
+ // front page had grown a door per sign-in method (Google, passkey, Nostr,
+ // twelve words, coinos.io) and read as a form; they now wait one step
+ // further in, behind the choice that leads to them.
return page([
h('div', { class: 'onb-hero' }, brandHeader(false)),
title(t('onbWelcomeTitle')),
- // 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: startCreate,
}, t('onbStart')),
- h('div', { class: 'onb-or' }, h('span', {}, t('onbOrSignIn'))),
- signinDoors(),
- // Bringing a wallet from another device: its twelve words, typed in.
- // This used to hide as a line under the freshly minted words behind
+ h('button', {
+ id: 'onb-signin',
+ class: 'btn-block', style: 'font-size:17px;padding:14px',
+ onClick: () => { ui.onbError = ''; o.step = 'doors'; render(); },
+ }, t('onbSignInExisting')),
+ ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
+ ]);
+ }
+ if (o.step === 'doors') {
+ return page([
+ h('div', { class: 'onb-hero' }, brandHeader(false)),
+ title(t('onbSigninTitle')),
+ // The seed phrase first: it IS the wallet, and works anywhere. It
+ // used to hide as a line under the freshly minted words behind
// "Create a new wallet", where a person signing in on their second
- // phone found it only by chance (user feedback, 2026-11-02). Its own
- // door, straight to the import pane.
+ // phone found it only by chance (user feedback, 2026-11-02).
h('button', { id: 'onb-restore', class: 'btn-block', style: 'padding:14px', onClick: () => {
clearSeedDrafts();
ui.unlockTab = 'import';
@@ -4209,17 +4223,17 @@ function onboardScreen() {
o.step = 'seed';
render();
} }, t('onbRestoreDoor')),
- // The move from the original coinos.io used to wait behind the
- // Spending offer, where nobody looking for it found it. Its own door
- // on the front page: it stashes the intent the /migrate link stashes
- // (the move needs a wallet and a payment address first) and explains
- // that before any twelve words appear.
+ signinDoors(),
+ // The move from the original coinos.io: it stashes the intent the
+ // /migrate link stashes (the move needs a wallet and a payment address
+ // first) and explains that before any twelve words appear.
h('button', { id: 'onb-migrate', class: 'btn-block', style: 'padding:14px', onClick: () => {
try { localStorage.setItem(MIGRATE_LINK_KEY, String(Date.now())); } catch {}
ui.onbError = '';
o.step = 'migrate';
render();
} }, t('onbMigrateDoor')),
+ h('button', { class: 'linklike small', onClick: () => { ui.onbError = ''; o.step = 'welcome'; render(); } }, t('back')),
ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
]);
}
@@ -4236,7 +4250,7 @@ function onboardScreen() {
}, t('onbMigrateCreate')),
h('div', { class: 'onb-or' }, h('span', {}, t('onbOrSignIn'))),
signinDoors(),
- h('button', { class: 'linklike small', onClick: () => { endMigrateLink(); ui.onbError = ''; o.step = 'welcome'; render(); } }, t('back')),
+ h('button', { class: 'linklike small', onClick: () => { endMigrateLink(); ui.onbError = ''; o.step = 'doors'; render(); } }, t('back')),
ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
]);
}
@@ -4286,7 +4300,7 @@ function onboardScreen() {
title(t(ui.unlockTab === 'import' ? 'onbRestoreTitle' : 'onbSeedTitle')),
unlockCard(),
// the entropy page brings its own Back — one is enough
- ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { ui.unlockError = ''; o.step = 'welcome'; render(); } }, t('back')),
+ ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { ui.unlockError = ''; o.step = ui.unlockTab === 'import' ? 'doors' : 'welcome'; render(); } }, t('back')),
]);
}
if (o.step === 'signin') {
@@ -4294,7 +4308,7 @@ function onboardScreen() {
h('div', { class: 'onb-mark', html: NOSTR_MARK }),
title(t('onbSigninTitle')),
featureHook('unlockExtra') || h('div', { class: 'notice err' }, 'nostr login unavailable'),
- h('button', { class: 'linklike small', onClick: () => { o.step = 'welcome'; render(); } }, t('back')),
+ h('button', { class: 'linklike small', onClick: () => { o.step = 'doors'; render(); } }, t('back')),
]);
}
if (o.step === 'spend') {
diff --git a/src/i18n.js b/src/i18n.js
index 36beef0..a6aafdb 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1402,7 +1402,8 @@ const STR = {
backupReminderGo: 'Write it down',
onbSeedTitle: 'Set up your wallet',
onbRestoreTitle: 'Restore your wallet',
- onbRestoreDoor: 'Restore from twelve words',
+ onbRestoreDoor: 'Sign in with seed phrase',
+ onbSignInExisting: 'Sign in to an existing account',
onbSigninTitle: 'Sign in',
onbLegacyTitle: 'Move your coinos.io account',
onbLegacyBody: 'The original coinos. We can move your balance over and point your username here. Your old account keeps working, it just stops receiving payments.',
diff --git a/tools/account-access-test.js b/tools/account-access-test.js
index c9af32e..3e481cc 100644
--- a/tools/account-access-test.js
+++ b/tools/account-access-test.js
@@ -48,7 +48,7 @@ try {
await click('Log out');
check('logout asks before ending the session', await page.evaluate(() => document.body.innerText.includes('Your accounts stay saved on this device.')));
await click('Log out');
- check('logout reaches the sign-in screen and retains the saved wallet', await page.evaluate((saved) => !document.querySelector('.header-avatar') && document.body.innerText.includes('Sign in with Nostr') && localStorage.getItem('btc-wallet-watch') === saved, saved));
+ check('logout reaches the sign-in screen and retains the saved wallet', await page.evaluate((saved) => !document.querySelector('.header-avatar') && document.body.innerText.includes('Sign in to an existing account') && localStorage.getItem('btc-wallet-watch') === saved, saved));
// Remember a public login identity on the watch-only account so every
// header destination is available, without supplying a private key.
await page.evaluate(({ key }) => {
diff --git a/tools/amber-login-test.js b/tools/amber-login-test.js
index 51f0991..46a16f6 100644
--- a/tools/amber-login-test.js
+++ b/tools/amber-login-test.js
@@ -39,6 +39,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(500);
console.log('\n[the Amber door]');
+ await click('sign in to an existing account'); await sleep(300);
check('the front door offers Nostr sign-in', await click('sign in with nostr') || await click('nostr'));
await sleep(500);
check('on Android, the login offers Amber', await waitText('sign in with a signer app', 5000));
diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js
index 3cb607c..e7740ae 100644
--- a/tools/onboard-backup-test.js
+++ b/tools/onboard-backup-test.js
@@ -43,7 +43,10 @@ async function signIn() {
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
+ // wait for the front door to actually be painted before poking it; the
+ // sign-in ways are one step in
+ await waitText(page, 'sign in to an existing account');
+ await page.evaluate(() => document.getElementById('onb-signin')?.click());
await waitText(page, 'sign in with nostr');
const sk = generateSecretKey();
const nsec = bech32.encode('nsec', bech32.toWords(sk), 1000);
@@ -62,23 +65,23 @@ try {
await page.setViewport({ width: 390, height: 844 });
await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' });
await sleep(1500);
+ // Two choices and nothing else (2026-11-06): a new wallet, or one you
+ // already have — the sign-in methods wait behind the second.
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));
+ const signin = document.getElementById('onb-signin');
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'),
+ labels: btns.map((b) => b.textContent.trim()),
+ order: !!primary && !!signin && primary.getBoundingClientRect().top < signin.getBoundingClientRect().top,
+ footer: !!document.querySelector('.footer'),
};
});
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('...and the only other choice is signing in to one you have', front.order && front.labels.length === 2,
+ front.labels.join(' | '));
+ check('...with no footer under them', !front.footer);
// The welcome card is just the title and the buttons now (42c8d4e): the
// way in itself says what the wallet is — tapping it puts the twelve
// words on screen at once, with what they are.
@@ -104,14 +107,16 @@ try {
await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitText(page, 'sign in to an existing account');
+ await page.evaluate(() => document.getElementById('onb-signin')?.click());
await waitText(page, 'sign in with nostr');
const door = await page.evaluate(() => {
+ const btns = [...document.querySelectorAll('button')].filter((b) => b.offsetParent && b.textContent.trim());
const b = document.getElementById('onb-restore');
- const c = document.getElementById('onb-create');
- return b && c ? { label: b.textContent.trim(), below: b.getBoundingClientRect().top > c.getBoundingClientRect().top, visible: !!b.offsetParent } : null;
+ return b ? { label: b.textContent.trim(), first: btns[0] === b, visible: !!b.offsetParent } : null;
});
- check('the front page has a door for an existing wallet', !!door && door.visible && /twelve words|seed/i.test(door.label), door ? door.label : 'missing');
- check('...below the way in, with the other alternatives', !!door && door.below);
+ check('signing in offers the seed phrase', !!door && door.visible && /seed phrase/i.test(door.label), door ? door.label : 'missing');
+ check('...first, above the other ways', !!door && door.first);
await page.evaluate(() => document.getElementById('onb-restore')?.click());
await sleep(600);
const pane = await page.evaluate(() => ({
diff --git a/tools/onboard-migrate-door-test.js b/tools/onboard-migrate-door-test.js
index 3b16dd0..6328919 100644
--- a/tools/onboard-migrate-door-test.js
+++ b/tools/onboard-migrate-door-test.js
@@ -29,6 +29,9 @@ try {
await page.setViewport({ width: 390, height: 844 });
await page.goto('http://localhost:5293/', { waitUntil: 'domcontentloaded' });
check('the front door paints', await waitText(page, 'create a new wallet'));
+ // the sign-in ways (migrating included) wait behind "Sign in to an existing account"
+ await page.click('#onb-signin');
+ await waitText(page, 'sign in with seed phrase', 5000);
const btns = await visible(page);
const iMigrate = btns.findIndex((b) => b.id === 'onb-migrate');
const iLastSignin = btns.map((b) => /sign in/i.test(b.text)).lastIndexOf(true);
@@ -44,7 +47,7 @@ try {
doors.map((b) => b.text).join(' | '));
await page.reload({ waitUntil: 'domcontentloaded' });
check('a refresh stays on the move page', await waitText(page, 'move your coinos.io account', 8000));
- check('Back returns to the front door', await clickText(page, 'back') && await waitText(page, 'create a new wallet', 5000));
+ check('Back returns to the sign-in choices', await clickText(page, 'back') && await waitText(page, 'sign in with seed phrase', 5000));
const forgot = await page.evaluate(() => !localStorage.getItem('btc-wallet-migrate-link'));
check('…and forgets the intent', forgot);
await page.click('#onb-migrate');