From 4b191f4eb8ad519e982f6667f2b540cdd77e539a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 2 Aug 2026 23:48:31 -0700 Subject: First-run onboarding: a five-step wizard and a coach-mark tour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fresh devices land in a full-screen flow instead of the unlock screen: welcome → "do you have a nostr account?" (sign in brings wallet, name, and profile along; No mints a wallet on the spot) → claim a username (the background npub-default only surfaces a Keep button; a custom claim — or a restored login's recovered name — advances by itself) → pick a punk or upload a photo (nostr.build), published as the kind-0 name+picture → confetti. "Enter coinos" drops into a three-stop tour: the avatar menu, the Spending/Savings toggle, and the tabs with the swipe tip — each highlighted by a measured gold ring over a dimmed page. One time only (localStorage flag), with an "I already have a wallet" escape to the classic screen. New seams: names.namesClaimForm, messages.publishProfile. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi --- src/features/messages.js | 22 ++++++++++++++++++++++ src/features/names.js | 2 ++ 2 files changed, 24 insertions(+) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 6ff94a4..46f7399 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -1356,6 +1356,28 @@ export function messagesFeature(ctx) { // Anyone (ark's history, other features) can open a profile or render a // small clickable identity chip. showProfile(pk) { openProfile(pk); return true; }, + // Publish (merge) kind-0 fields for the current identity — the onboarding + // wizard sets name + picture through this. + async publishProfile(fields) { + const id = await identity(); + if (!id) throw new Error(t('msgNoIdentity')); + const evs = await queryOn([...new Set([...PROFILE_RELAYS, ...DM_RELAYS])], { kinds: [0], authors: [id.pubkey] }, 3000); + const newest = evs.sort((a, b) => b.created_at - a.created_at)[0]; + let base = {}; + try { base = newest ? JSON.parse(newest.content) : {}; } catch {} + const merged = { ...base }; + for (const [k, v] of Object.entries(fields || {})) { + if (v) merged[k] = v; + } + if (merged.name) merged.display_name = merged.name; + const partial = { kind: 0, content: JSON.stringify(merged), tags: [], created_at: Math.floor(Date.now() / 1000) }; + const evt = id.signer instanceof Uint8Array ? finalizeEvent(partial, id.signer) : await id.signer.signEvent(partial); + const ok = await publishOn([...new Set([...PROFILE_RELAYS, ...DM_RELAYS])], evt); + if (!ok) throw new Error(t('msgSendFailed')); + fullProfiles.set(id.pubkey, merged); + profiles.set(id.pubkey, { name: merged.name || null, picture: merged.picture || null, t: Date.now() }); + return true; + }, profileChip(pk) { return h('span', { class: 'zap-chip', diff --git a/src/features/names.js b/src/features/names.js index 3952214..a30038e 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -310,6 +310,8 @@ export function namesFeature(ctx) { namesAdoptIdentity(signer, npub) { return adoptIdentity(signer, npub); }, // the claimed payment address, for anyone prefilling a lightning address namesAddress() { const st = load(); return st.name ? `${st.name}@${st.domain || DOMAIN}` : null; }, + // the onboarding wizard renders the same claim form on its username step + namesClaimForm() { return claimForm(true); }, stop() { clearTimeout(retryTimer); clearTimeout(deadline); deadline = null; }, receiveModes() { if (!available()) return []; -- cgit v1.2.3