diff options
| author | Adam Soltys <[email protected]> | 2026-09-11 08:33:43 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-11 08:33:43 -0700 |
| commit | fb33e7de7de8c787ed8d906026f0e0e4270af33e (patch) | |
| tree | 2f4798a4d61c37ef1ff59d63cd045ae965365eda /src | |
| parent | 2a7b18005bb573e1aa54f50b1f87918cc1dcd92d (diff) | |
| download | coinosv3-fb33e7de7de8c787ed8d906026f0e0e4270af33e.tar.gz coinosv3-fb33e7de7de8c787ed8d906026f0e0e4270af33e.zip | |
Offer migration only for submitted unmigrated legacy names
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 3 | ||||
| -rw-r--r-- | src/features/messages.js | 14 | ||||
| -rw-r--r-- | src/features/names.js | 71 |
3 files changed, 62 insertions, 26 deletions
@@ -3087,9 +3087,6 @@ function onboardScreen() { try { localStorage.removeItem(ONB_STEP_KEY); } catch {} render(); } }, t('onbNotNow')), - // Most people are new. The ones who aren't know it, and can say so here - // rather than everyone being asked first. - h('button', { class: 'linklike small', onClick: () => { ui.onbError = ''; o.step = 'legacy'; render(); } }, t('onbHaveCoinos')), ]); } if (o.step === 'avatar') { diff --git a/src/features/messages.js b/src/features/messages.js index a9da0c3..a01c063 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2609,6 +2609,7 @@ export function messagesFeature(ctx) { const id = await identity(); if (!id) { noIdToast(); return; } const e = ui.profEdit; + e.migrationName = null; ui.profSaving = true; render(); try { @@ -2649,6 +2650,7 @@ export function messagesFeature(ctx) { ui.profEdit = null; ui.profEditFilled = false; toast(t('profSaved')); } catch (err) { + if (ui.profEdit === e && e.uname === err.migrationName) e.migrationName = err.migrationName; toast(err.message || String(err)); } finally { ui.profSaving = false; @@ -2839,12 +2841,16 @@ export function messagesFeature(ctx) { type: 'text', style: 'flex:1;min-width:0', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ui.profEdit.uname, - onInput: (ev) => { ui.profEdit.uname = ev.target.value.toLowerCase().replace(/[^a-z0-9._-]/g, ''); ev.target.value = ui.profEdit.uname; }, + onInput: (ev) => { + ui.profEdit.uname = ev.target.value.toLowerCase().replace(/[^a-z0-9._-]/g, ''); + ev.target.value = ui.profEdit.uname; + if (ui.profEdit.migrationName) { ui.profEdit.migrationName = null; render(); } + }, }), h('span', { class: 'muted', style: 'white-space:nowrap;padding:0 8px' }, '@' + myAddr.split('@')[1]))) : null, - // the migrate door, right where a coinos.io veteran is looking - // at the name they want to bring over (mainnet names only) - myAddr && myAddr.split('@')[1] === 'coinos.io' ? h('button', { + // Offered only for a submitted name confirmed as legacy-only. + ui.profEdit.migrationName && ui.profEdit.migrationName === ui.profEdit.uname + && myAddr && myAddr.split('@')[1] === 'coinos.io' ? h('button', { type: 'button', class: 'linklike small', style: 'align-self:flex-start', onClick: () => { location.href = `https://coinos.io/migrate?to=${encodeURIComponent(myAddr)}&back=${encodeURIComponent(location.origin + '/')}`; diff --git a/src/features/names.js b/src/features/names.js index 7a0f0de..f05ac09 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -138,6 +138,30 @@ export function namesFeature(ctx) { save({}); } + // Only an explicit, rejected name submission can offer migration. A legacy + // account still exists after moving to v3, and its `migrated` flag predates + // v3, so the registrar record is what tells us the name has already moved. + async function claimRequestedName(name, opts = {}) { + const domain = DOMAIN(); + try { return await claim(name, opts); } + catch (err) { + if (domain === 'coinos.io' && ['name is taken', 'name is reserved'].includes(err.message)) { + try { + const r = await fetch(`${REGISTRAR}/name/${encodeURIComponent(name)}?domain=${encodeURIComponent(domain)}`, + { signal: AbortSignal.timeout(8000), cache: 'no-store' }); + const record = r.ok ? await r.json() : null; + if (record?.name === name && record.domain === domain && record.reserved === true && !record.pubkey) { + const legacy = await fetch(`https://coinos.io/api/users/${encodeURIComponent(name)}`, + { signal: AbortSignal.timeout(8000), cache: 'no-store' }); + const user = legacy.ok ? await legacy.json() : null; + if (user?.username?.toLowerCase() === name) err.migrationName = name; + } + } catch {} // An unavailable lookup leaves the original claim error. + } + throw err; + } + } + // An imported seed (or a new browser) doesn't know its username — the // registrar does. Ask by the wallet's nostr pubkey and adopt the answer // before deciding whether anything needs claiming. @@ -398,25 +422,45 @@ export function namesFeature(ctx) { type: 'text', placeholder: t('namesPlaceholder'), style: 'flex:1' + (big ? ';font-size:18px' : ''), autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ui.nameClaim || '', - onInput: (e) => { ui.nameClaim = e.target.value.toLowerCase().trim(); }, + onInput: (e) => { + ui.nameClaim = e.target.value.toLowerCase().trim(); + if (ui.nameClaimMigration) { ui.nameClaimMigration = null; render(); } + }, }), h('span', { class: 'muted', style: 'align-self:center' }, '@' + DOMAIN())), h('button', { class: 'btn-primary btn-block', disabled: ui.busy, onClick: async () => { const name = (ui.nameClaim || '').toLowerCase().trim(); if (!name) return; - ui.busy = true; ui.nameClaimError = null; render(); + ui.busy = true; ui.nameClaimError = null; ui.nameClaimMigration = null; render(); try { // Sign with the login identity when one can be (re)attached: the // registrar lets a migrated coinos user's own identity take their // name back, which the wallet key alone could never prove. const signer = await Promise.resolve(hook('nostrLoginResume')).catch(() => null); - await claim(name, signer ? { signer, manager: wallet.nostrPubkey() } : {}); + await claimRequestedName(name, signer ? { signer, manager: wallet.nostrPubkey() } : {}); ui.nameClaim = ''; ui.nameEditOpen = false; toast(t('namesClaimed', { name: `${name}@${DOMAIN()}` })); - } catch (e) { ui.nameClaimError = e.message; } + } catch (e) { + ui.nameClaimError = e.message; + if (ui.nameClaim === name) ui.nameClaimMigration = e.migrationName || null; + } ui.busy = false; render(); - } }, ui.busy ? h('span', { class: 'spinner' }) : t('namesClaim'))); + } }, ui.busy ? h('span', { class: 'spinner' }) : t('namesClaim')), + ui.nameClaimMigration && ui.nameClaimMigration === ui.nameClaim && DOMAIN() === 'coinos.io' + ? h('button', { + type: 'button', class: 'linklike small', + onClick: () => { + if (ui.onb?.step === 'spend') { + ui.onbError = ''; ui.onb.step = 'legacy'; render(); + } else { + const st = load(); + if (!st.name) return; + const addr = `${st.name}@${st.domain || DOMAIN()}`; + location.href = `https://coinos.io/migrate?to=${encodeURIComponent(addr)}&back=${encodeURIComponent(location.origin + '/')}`; + } + }, + }, t('onbHaveCoinos')) : null); } // Settings: one card per concern, all laid out (no collapsed sections — @@ -796,7 +840,7 @@ export function namesFeature(ctx) { checked = false; lastError = null; // The claim field is a draft for THIS account — a name typed before a // logout used to greet the next account that opened on this device. - ui.nameClaim = null; ui.nameClaimError = null; suggestedFor = null; + ui.nameClaim = null; ui.nameClaimError = null; ui.nameClaimMigration = null; suggestedFor = null; refresh(); }, // The pencil by the address opens this: choosing a name gets a page of @@ -811,18 +855,7 @@ export function namesFeature(ctx) { h('h3', {}, t('namesCustom')), addr ? h('div', { class: 'addr-box break', style: 'font-size:14px' }, addr) : null, h('p', { class: 'small muted', style: 'margin:0' }, t('namesCustomHow')), - claimForm(true), - // The same door the onboarding wizard offers — a coinos.io veteran - // who skipped it there shouldn't have to make a new account to find - // it again. Round trip: coinos.io sweeps + releases, the ?migrated= - // return claims the name into this wallet. Mainnet only — staging - // names have no legacy counterpart. - addr && DOMAIN() === 'coinos.io' ? h('button', { - class: 'linklike small', style: 'align-self:center;margin-top:6px', - onClick: () => { - location.href = `https://coinos.io/migrate?to=${encodeURIComponent(addr)}&back=${encodeURIComponent(location.origin + '/')}`; - }, - }, t('onbHaveCoinos')) : null), + claimForm(true)), h('button', { class: 'btn-ghost btn-block', onClick: () => goBack(() => { ui.nameEditOpen = null; ui.nameClaimError = null; }) }, t('back'))); }, namesAdoptIdentity(signer, npub) { return adoptIdentity(signer, npub); }, @@ -869,7 +902,7 @@ export function namesFeature(ctx) { // the onboarding wizard renders the same claim form on its username step namesClaimForm() { return claimForm(true); }, // claim a specific name (the migration flow, after coinos.io released it) - namesClaimName(name, opts) { return claim(String(name).toLowerCase(), opts || {}); }, + namesClaimName(name, opts) { return claimRequestedName(String(name).toLowerCase(), opts || {}); }, stop() { clearTimeout(retryTimer); clearTimeout(deadline); deadline = null; }, receiveModes() { if (!available()) return []; |
