aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-04 00:09:47 -0700
committerAdam Soltys <[email protected]>2026-08-04 00:09:47 -0700
commitb75db0949961bb3b8c1b7e3b46e1ad0e4733ae8e (patch)
tree3bce078cdafb521786a866b70b1af2fa4587335b
parentd8964bdf6f047d058731edfae6d07e57106661c8 (diff)
downloadcoinosv3-b75db0949961bb3b8c1b7e3b46e1ad0e4733ae8e.tar.gz
coinosv3-b75db0949961bb3b8c1b7e3b46e1ad0e4733ae8e.zip
One question fewer on the way in, and an 'or' that means it
The sign-in screen stacked 'Connect a signer app' directly above a bunker/nsec field, which reads as two steps rather than two ways in. A divider says which it is. The wizard's second step asked whether you have a nostr account, then sent you to a screen that asks the same thing. Step one now offers both paths directly — Get started, or sign in with Nostr — and the middle step is gone, along with its strings. The welcome blurb goes too: the screen is one decision, and prose above it is something to read before you can make it. The footer sinks to the bottom of the window rather than trailing the content up the page. #app is a full-height column now; the chat card sizes itself against the viewport, so the layout test covers it at three sizes. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
-rw-r--r--src/app.js32
-rw-r--r--src/features/nostrlogin.js3
-rw-r--r--src/i18n.js8
-rw-r--r--src/style.css12
-rw-r--r--tools/chat-layout-test.js3
5 files changed, 30 insertions, 28 deletions
diff --git a/src/app.js b/src/app.js
index 6871ce8..0477441 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2120,7 +2120,7 @@ function onboardScreen() {
ui.onb ||= { step: 'welcome' };
const o = ui.onb;
// steps that were waiting on something advance the moment it exists
- if ((o.step === 'nostr' || o.step === 'signin') && ui.screen === 'wallet' && activeAccount()) {
+ if ((o.step === 'welcome' || o.step === 'signin') && ui.screen === 'wallet' && activeAccount()) {
o.step = 'legacy';
o.enterAddr = featureHook('namesAddress') || null;
}
@@ -2173,28 +2173,20 @@ function onboardScreen() {
return page([
h('div', { class: 'onb-hero' }, brandHeader(false)),
title(t('onbWelcomeTitle')),
- h('p', { class: 'muted', style: 'margin:0;font-size:16px' }, t('onbWelcomeBody')),
- h('button', { class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: () => { o.step = 'nostr'; render(); } }, t('onbStart')),
- h('button', { class: 'linklike small', onClick: () => { ui.onbSkip = true; ui.onb = null; render(); } }, t('onbHaveWallet')),
- ]);
- }
- if (o.step === 'nostr') {
- return page([
- h('div', { class: 'onb-mark', html: NOSTR_MARK }),
- title(t('onbNostrTitle')),
- h('p', { class: 'muted', style: 'margin:0' }, t('onbNostrBody')),
- h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: () => {
- o.step = 'signin';
- ui.nostrLoginOpen = true;
- render();
- } }, t('onbNostrYes')),
- h('button', { class: 'btn-block', style: 'padding:14px', disabled: ui.onbBusy, onClick: async () => {
+ // 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', disabled: ui.onbBusy, onClick: async () => {
ui.onbBusy = true; render();
try { await enterWallet(newMnemonic(), '', { generated: true }); } catch (e) { ui.onbError = e.message; }
ui.onbBusy = false; render();
- } }, ui.onbBusy ? h('span', { class: 'spinner sm' }) : t('onbNostrNo')),
+ } }, ui.onbBusy ? h('span', { class: 'spinner sm' }) : t('onbStart')),
+ h('button', { class: 'btn-block', style: 'padding:14px', onClick: () => {
+ o.step = 'signin';
+ ui.nostrLoginOpen = true;
+ render();
+ } }, t('nlSignIn')),
ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
- h('button', { class: 'linklike small', onClick: () => { o.step = 'welcome'; render(); } }, t('back')),
+ h('button', { class: 'linklike small', onClick: () => { ui.onbSkip = true; ui.onb = null; render(); } }, t('onbHaveWallet')),
]);
}
if (o.step === 'signin') {
@@ -2202,7 +2194,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 = 'nostr'; render(); } }, t('back')),
+ h('button', { class: 'linklike small', onClick: () => { o.step = 'welcome'; render(); } }, t('back')),
]);
}
if (o.step === 'username') {
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index ced5504..97ce8be 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -199,6 +199,9 @@ export function nostrLoginFeature(ctx) {
onClick: () => run(() => extensionSigner()) }, t('nlExtension'))
: null,
nostrConnectSection(run),
+ // These are alternatives, not steps: without the divider the field under
+ // the button reads as something you also have to fill in.
+ h('div', { class: 'or-split' }, h('span', {}, t('or'))),
h('div', { class: 'row gap6' },
h('input', {
type: 'password', placeholder: t('nlKeyOrBunker'), style: 'flex:1',
diff --git a/src/i18n.js b/src/i18n.js
index 99640a3..9b65a77 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -519,15 +519,11 @@ const STR = {
arkLnPayFailed: 'Payment failed',
arkLnRefunded: 'Payment failed — your money is back in Spending.',
arkLnPaidTitle: 'Paid!',
+ or: 'or',
nlSignIn: 'Sign in with Nostr',
onbWelcomeTitle: 'Welcome to coinos',
- onbWelcomeBody: 'A bitcoin wallet that lives in your browser: instant payments, encrypted chat, and your keys never leave the device. Ready to get started?',
- onbStart: "Let's go",
+ onbStart: 'Get started',
onbHaveWallet: 'I already have a wallet',
- onbNostrTitle: 'Do you already have a nostr account?',
- onbNostrBody: 'Sign in with it and your wallet, name, and profile come with you. No idea what nostr is? No problem — we\u2019ll set everything up fresh.',
- onbNostrYes: 'Yes — sign me in',
- onbNostrNo: 'No — create my wallet',
onbSigninTitle: 'Sign in with Nostr',
onbLegacyTitle: 'Do you have an account on coinos.io?',
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/src/style.css b/src/style.css
index ea29891..5064fae 100644
--- a/src/style.css
+++ b/src/style.css
@@ -78,7 +78,13 @@ html, body {
#app {
max-width: 560px;
margin: 0 auto;
- padding: 24px 16px 64px;
+ padding: 24px 16px 24px;
+ /* A column the height of the window, so the footer sits at the bottom of
+ short screens instead of floating under the content. */
+ min-height: 100vh;
+ min-height: 100dvh;
+ display: flex;
+ flex-direction: column;
}
/* ---- typography ---- */
@@ -268,7 +274,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si
background: var(--green-soft); color: var(--green);
display: grid; place-items: center; font-size: 34px; font-weight: 700;
}
-.footer { margin-top: 28px; padding-bottom: 8px; }
+.footer { margin-top: auto; padding-top: 28px; padding-bottom: 8px; }
.footer a { color: var(--accent-ink); font-weight: 600; }
/* Clickable unit labels (BTC/sats) — look like text, toggle the global unit. */
@@ -570,3 +576,5 @@ body::after {
.balance-move .input-group > button { flex: 1 1 150px; }
.chat-signer-off { padding: 6px 8px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface2); }
.chan-pick { width: auto; min-width: 0; padding: 6px 32px 6px 10px; font-size: 13px; font-weight: 600; border-radius: 9px; background-position: right 10px center; }
+.or-split { display: flex; align-items: center; gap: 10px; color: var(--faint); font-size: 13px; }
+.or-split::before, .or-split::after { content: ''; flex: 1; height: 1px; background: var(--line); }
diff --git a/tools/chat-layout-test.js b/tools/chat-layout-test.js
index ef86291..f39b87e 100644
--- a/tools/chat-layout-test.js
+++ b/tools/chat-layout-test.js
@@ -115,6 +115,9 @@ try {
await page.evaluate(() => { const i = document.querySelector('input[placeholder*="channel" i]'); const b = i && i.parentElement.querySelector('button'); if (b) b.click(); });
for (let i = 0; i < 60 && !(await page.$('.chan-pick')); i++) await sleep(500);
+ // the picker appears on the relay echo, which can beat createChannel's own
+ // continuation — give the field a moment to put itself away
+ for (let i = 0; i < 20 && (await page.$('input[placeholder*="channel" i]')); i++) await sleep(500);
const two = await page.evaluate(() => {
const sel = document.querySelector('.chan-pick');
return { picker: !!sel, tag: sel && sel.tagName, opts: sel ? [...sel.options].map((o) => o.textContent) : [], field: !!document.querySelector('input[placeholder*="channel" i]') };