aboutsummaryrefslogtreecommitdiffstats
path: root/src
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
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')
-rw-r--r--src/app.js91
-rw-r--r--src/features/names.js8
-rw-r--r--src/i18n.js3
3 files changed, 77 insertions, 25 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 {}
diff --git a/src/features/names.js b/src/features/names.js
index ea94d2a..a29ee7d 100644
--- a/src/features/names.js
+++ b/src/features/names.js
@@ -457,6 +457,9 @@ export function namesFeature(ctx) {
// name back, which the wallet key alone could never prove.
const signer = await Promise.resolve(hook('nostrLoginResume')).catch(() => null);
await claimRequestedName(name, signer ? { signer, manager: wallet.nostrPubkey() } : {});
+ // the wizard's Spending step tells a name claimed here from one
+ // the registrar handed back (an account set up before)
+ if (ui.onb && ui.onb.step === 'spend') ui.onb.claimedHere = name;
ui.nameClaim = '';
ui.nameEditOpen = false;
toast(t('namesClaimed', { name: `${name}@${DOMAIN()}` }));
@@ -925,7 +928,10 @@ export function namesFeature(ctx) {
// for wallets with a history. The background claim still runs.
identityGenerated() { checked = true; },
// the onboarding wizard renders the same claim form on its username step
- namesClaimForm() { return claimForm(true); },
+ // Nothing to type into until the registrar has answered who this wallet
+ // already is (the wizard shows "Getting things ready…" meanwhile): a
+ // restored account's name arrives in that window and ends the step.
+ namesClaimForm() { return checked ? claimForm(true) : null; },
// claim a specific name (the migration flow, after coinos.io released it)
namesClaimName(name, opts) { return claimRequestedName(String(name).toLowerCase(), opts || {}); },
stop() { clearTimeout(retryTimer); clearTimeout(deadline); deadline = null; },
diff --git a/src/i18n.js b/src/i18n.js
index e6452e1..6081c2d 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1132,6 +1132,9 @@ const STR = {
onbLegacyNoAddr: "Your payment address isn't ready yet. Try again in a moment.",
onbLegacyDone: "I'm done, continue",
onbHaveCoinos: 'Migrate an existing coinos account',
+ onbMigrateDoor: 'Migrate a coinos account',
+ onbMigrateIntro: "Moving from the original coinos.io? Your balance and username come with you. First, set up the wallet they'll move into and write down its twelve words: they're yours alone, and the only way back in. Then we'll take you to coinos.io to finish the move.",
+ onbMigrateCreate: 'Create my new wallet',
migrateClaimed: 'Welcome back. {name} is yours here now.',
migrateClaimFailed: "Your balance moved, but {name} isn't free yet. Try claiming it in a minute.",
migrateWorking: 'Claiming {name}…',