aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 01:41:24 -0700
committerAdam Soltys <[email protected]>2026-10-09 01:41:24 -0700
commitde8cb7a6f2272fa0b601a155dd244fe3a3a3f331 (patch)
treef8f66645e01e3e87e0d793a16771b272ed8bc9d1 /src/app.js
parent7031e547714be960d70f18359523d762f27292e1 (diff)
downloadcoinosv3-de8cb7a6f2272fa0b601a155dd244fe3a3a3f331.tar.gz
coinosv3-de8cb7a6f2272fa0b601a155dd244fe3a3a3f331.zip
Front door: a "Migrate a coinos account" button under the sign-in doors, opening a page that says a wallet and its twelve words come first (Create, or the sign-in doors), and /migrate on a device without a wallet opens on that page instead of an unexplained welcome; while a move is queued the recovery-phrase step offers no Later. Spending step: an account whose custom name the registrar handed back (a wallet a sign-in restored) leaves the wizard instead of being shown a claim form it could only retype its own name into — claiming it again "succeeded" onto the same page; the claim form also waits behind "Getting things ready…" until the registrar lookups settle
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js91
1 files changed, 67 insertions, 24 deletions
diff --git a/src/app.js b/src/app.js
index 52adce4..6a80f8c 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3674,13 +3674,15 @@ function onboardScreen() {
let saved = null;
try { saved = localStorage.getItem(ONB_STEP_KEY); } catch {}
// a saved step past wallet creation only makes sense if the wallet is here
- if (saved && !['welcome', 'signin', 'seed'].includes(saved) && !activeAccount()) saved = 'welcome';
+ if (saved && !['welcome', 'signin', 'seed', 'migrate'].includes(saved) && !activeAccount()) saved = 'welcome';
// 'legacy-went' survives the coinos.io round trip: the user left for the
// migrate page, so coming back must offer "I'm done, continue" — the
// in-memory flag died with the navigation and left them looping.
ui.onb = saved === 'legacy-went'
? { step: 'legacy', wentToLegacy: true, fromLink: migrateLinkPending() }
- : { step: saved || 'welcome', fromLink: saved === 'legacy' && migrateLinkPending() };
+ // the /migrate link on a device with no wallet opens on the move's own
+ // page, which says a wallet comes first — not an unexplained welcome
+ : { step: saved || (!activeAccount() && migrateLinkPending() ? 'migrate' : 'welcome'), fromLink: saved === 'legacy' && migrateLinkPending() };
if (saved === 'signin') ui.nostrLoginOpen = true; // reopen the login card
}
const o = ui.onb;
@@ -3719,9 +3721,17 @@ function onboardScreen() {
if (o.step === 'spend') {
const addr = featureHook('namesAddress');
if (o.enterAddr === undefined) o.enterAddr = addr || null;
+ const custom = !!addr && !/^npub1/.test(addr);
+ // A custom name this page didn't claim is an account that set itself up
+ // before: a wallet a sign-in restored, whose name the registrar handed
+ // back (owned by the login identity or the wallet key). There is nothing
+ // to claim — the form stood here anyway, suggesting a profile name, and
+ // retyping the name one already had "claimed" it again and painted the
+ // same page. Out of the wizard, into the wallet as it was.
+ if (custom && !o.claimedHere) { leaveWizard(); return null; }
// Only a real (custom) name advances by itself — the background
// auto-claim of the npub-shaped default doesn't count as setting up.
- if (addr && addr !== o.enterAddr && !/^npub1/.test(addr)) {
+ if (custom && addr !== o.enterAddr) {
// The avatar ask is for brand-new profiles. An identity that already
// wears a face (a nostr login with a published picture) skips straight
// past the punk picker — restyling a real profile helps nobody.
@@ -3792,6 +3802,26 @@ function onboardScreen() {
ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
]);
}
+ // Making your own keys: the words are the wallet, so show them at once,
+ // no Generate to tap.
+ const startCreate = () => {
+ ui.unlockTab = 'create';
+ ui.unlockError = '';
+ ui.createStep = 'gen';
+ if (!ui.draftMnemonic) ui.draftRandom = ui.draftMnemonic = newMnemonic();
+ o.step = 'seed';
+ render();
+ };
+ // All the sign-in doors (Google, passkey, Nostr) — the feature supplies
+ // them; the single-button fallback keeps builds without it working.
+ const signinDoors = () => featureHook('frontDoorSignin')
+ || h('button', { class: 'btn-block', style: 'padding:14px', onClick: () => {
+ o.step = 'signin';
+ ui.nostrLoginOpen = true;
+ render();
+ } }, h('span', { style: 'display:flex;align-items:center;justify-content:center;gap:8px' },
+ h('span', { style: 'display:flex;flex-shrink:0', html: NOSTR_MARK }),
+ t('nlSignIn')));
if (o.step === 'welcome') {
return page([
h('div', { class: 'onb-hero' }, brandHeader(false)),
@@ -3801,27 +3831,38 @@ function onboardScreen() {
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 = '';
- ui.createStep = 'gen';
- // the words are the wallet: show them at once, no Generate to tap
- if (!ui.draftMnemonic) ui.draftRandom = ui.draftMnemonic = newMnemonic();
- o.step = 'seed';
- render();
- } }, t('onbStart')),
+ class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: startCreate,
+ }, t('onbStart')),
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.
- featureHook('frontDoorSignin')
- || h('button', { class: 'btn-block', style: 'padding:14px', onClick: () => {
- o.step = 'signin';
- ui.nostrLoginOpen = true;
- render();
- } }, h('span', { style: 'display:flex;align-items:center;justify-content:center;gap:8px' },
- h('span', { style: 'display:flex;flex-shrink:0', html: NOSTR_MARK }),
- t('nlSignIn'))),
+ signinDoors(),
+ // 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.
+ 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')),
+ ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
+ ]);
+ }
+ if (o.step === 'migrate') {
+ // The intent is kept across whichever way in they pick: once a wallet
+ // exists, the Spending offer gives way to the coinos.io move (above).
+ return page([
+ h('div', { class: 'onb-hero' }, brandHeader(false)),
+ title(t('onbLegacyTitle')),
+ h('p', { class: 'muted', style: 'margin:0' }, t('onbMigrateIntro')),
+ h('button', {
+ id: 'onb-migrate-create',
+ class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: startCreate,
+ }, 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')),
ui.onbError ? h('div', { class: 'notice err' }, ui.onbError) : null,
]);
}
@@ -3850,7 +3891,9 @@ function onboardScreen() {
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: () => {
+ // Except on the way to a coinos.io move: the balance about to arrive
+ // must have its twelve words written down first, so Later isn't offered.
+ migrateLinkPending() ? null : h('button', { class: 'btn-ghost btn-block', onClick: () => {
if (canOfferSpending()) { o.step = 'spend'; o.enterAddr = undefined; render(); return; }
ui.onb = null;
try { localStorage.removeItem(ONB_STEP_KEY); } catch {}