Rework onboarding flow, add recovery

This commit is contained in:
Jon Staab 2026-01-16 10:06:25 -08:00
parent f3647e9bc1
commit 6aa297c1a4
11 changed files with 92 additions and 93 deletions

View file

@ -7,13 +7,13 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileEject from "@app/components/ProfileEject.svelte" import KeyRecoveryRequest from "@app/components/KeyRecoveryRequest.svelte"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
const back = () => history.back() const back = () => history.back()
const startEject = () => pushModal(ProfileEject) const startRecoveryRequest = () => pushModal(KeyRecoveryRequest)
</script> </script>
<div class="column gap-4"> <div class="column gap-4">
@ -44,7 +44,7 @@
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button class="btn btn-primary" onclick={startEject}> <Button class="btn btn-primary" onclick={startRecoveryRequest}>
<Icon icon={CheckCircle} /> <Icon icon={CheckCircle} />
I want to hold my own keys I want to hold my own keys
</Button> </Button>

View file

@ -16,8 +16,9 @@
const loadSessions = async () => { const loadSessions = async () => {
if (!isPomadeSession($session)) return if (!isPomadeSession($session)) return
const client = new Client($session.clientOptions)
try { try {
const client = new Client($session.clientOptions)
const result = await client.listSessions() const result = await client.listSessions()
const pubkey = await client.getPubkey() const pubkey = await client.getPubkey()
@ -42,20 +43,9 @@
} }
sessions = Array.from(sessionMap.values()) sessions = Array.from(sessionMap.values())
} else {
pushToast({
theme: "error",
message: "Failed to load sessions",
})
} }
} finally {
client.stop() client.stop()
} catch (e) {
console.error(e)
pushToast({
theme: "error",
message: "Failed to load sessions",
})
} }
} }

View file

@ -7,7 +7,7 @@
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state" import {PROTECTED} from "@app/core/state"
import {updateProfile} from "../core/commands" import {updateProfile} from "@app/core/commands"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile() const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []) const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])

View file

@ -1,21 +1,65 @@
<script lang="ts"> <script lang="ts">
import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/util"
import {makeProfile, makeSecret, getPubkey} from "@welshman/util"
import {loginWithNip01, loginWithPomade} from "@welshman/app"
import Key from "@assets/icons/key-minimalistic.svg?dataurl" import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import LogIn from "@app/components/LogIn.svelte" import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte" import InfoNostr from "@app/components/InfoNostr.svelte"
import SignUpKey from "@app/components/SignUpKey.svelte"
import SignUpEmail from "@app/components/SignUpEmail.svelte"
import SignUpProfile from "@app/components/SignUpProfile.svelte" import SignUpProfile from "@app/components/SignUpProfile.svelte"
import {pushModal} from "@app/util/modal" import SignUpComplete from "@app/components/SignUpComplete.svelte"
import {setChecked} from "@app/util/notifications"
import {pushModal, clearModals} from "@app/util/modal"
import {initProfile} from "@app/core/commands"
import {POMADE_SIGNERS, PLATFORM_NAME} from "@app/core/state" import {POMADE_SIGNERS, PLATFORM_NAME} from "@app/core/state"
setKey("signup.email", "")
setKey("signup.secret", makeSecret())
setKey("signup.profile", makeProfile())
setKey("signup.clientOptions", undefined)
const hasPomade = POMADE_SIGNERS.length >= 3 const hasPomade = POMADE_SIGNERS.length >= 3
const login = () => pushModal(LogIn) const login = () => pushModal(LogIn)
const useEmail = () => pushModal(SignUpProfile, {flow: "email"}) const flows = {
email: {
start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
finalize: () => {
const email = getKey<string>("signup.email")!
const secret = getKey<string>("signup.secret")!
const profile = getKey<Profile>("signup.profile")!
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
const useNostr = () => pushModal(SignUpProfile, {flow: "nostr"}) loginWithPomade(getPubkey(secret), email, clientOptions!)
initProfile(profile)
setChecked("*")
clearModals()
},
},
nostr: {
start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
finalize: () => {
const secret = getKey<string>("signup.secret")!
const profile = getKey<Profile>("signup.profile")!
loginWithNip01(secret)
initProfile(profile)
setChecked("*")
clearModals()
},
},
}
</script> </script>
<div class="column gap-4"> <div class="column gap-4">
@ -26,12 +70,12 @@
users control over their digital identity using <strong>cryptographic key pairs</strong>. users control over their digital identity using <strong>cryptographic key pairs</strong>.
</p> </p>
{#if hasPomade} {#if hasPomade}
<Button onclick={useEmail} class="btn btn-primary"> <Button onclick={flows.email.start} class="btn btn-primary">
<Icon icon={Letter} /> <Icon icon={Letter} />
Sign up with email Sign up with email
</Button> </Button>
{/if} {/if}
<Button onclick={useNostr} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}"> <Button onclick={flows.nostr.start} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}">
<Icon icon={Key} /> <Icon icon={Key} />
Generate a key Generate a key
</Button> </Button>

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import type {Profile} from "@welshman/util"
import {loginWithNip01} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl" import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
@ -8,23 +6,14 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {clearModals} from "@app/util/modal"
import {initProfile} from "@app/core/commands"
type Props = { type Props = {
secret: string next: () => void
profile: Profile
} }
const {secret, profile}: Props = $props() const {next}: Props = $props()
const back = () => history.back() const back = () => history.back()
const next = () => {
loginWithNip01(secret)
initProfile(profile)
clearModals()
}
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(next)}> <form class="column gap-4" onsubmit={preventDefault(next)}>

View file

@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {choice} from "@welshman/lib" import {choice} from "@welshman/lib"
import {makeSecret} from "@welshman/util"
import type {Profile} from "@welshman/util"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Key from "@assets/icons/key.svg?dataurl" import Key from "@assets/icons/key.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import {getKey, setKey} from "@lib/implicit"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@ -19,10 +18,10 @@
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
type Props = { type Props = {
profile: Profile next: () => void
} }
const {profile}: Props = $props() const {next}: Props = $props()
const back = () => history.back() const back = () => history.back()
@ -39,7 +38,8 @@
let client: Client | undefined = undefined let client: Client | undefined = undefined
try { try {
const {clientOptions, ...registerRes} = await Client.register(2, 3, makeSecret()) const secret = getKey<string>("signup.secret")!
const {clientOptions, ...registerRes} = await Client.register(2, 3, secret)
if (!registerRes.ok) { if (!registerRes.ok) {
return pushToast({ return pushToast({
@ -70,7 +70,10 @@
}) })
} }
pushModal(SignUpEmailConfirm, {email, profile, clientOptions}) setKey("signup.email", email)
setKey("signup.clientOptions", clientOptions)
pushModal(SignUpEmailConfirm, {next})
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View file

@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/util"
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {loginWithPomade} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {getKey} from "@lib/implicit"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
@ -13,18 +11,14 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {initProfile} from "@app/core/commands"
import {clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast"
type Props = { type Props = {
email: string next: () => void
profile: Profile
clientOptions: ClientOptions
} }
const {email, profile, clientOptions}: Props = $props() const {next}: Props = $props()
const email = getKey<string>("signup.email")
const back = () => history.back() const back = () => history.back()
@ -34,15 +28,7 @@
// Just pretend we're validating, they clearly got a code from somewhere // Just pretend we're validating, they clearly got a code from somewhere
await sleep(800) await sleep(800)
try { next()
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
pushToast({message: "Successfully logged in!"})
initProfile(profile)
setChecked("*")
clearModals()
} finally {
loading = false
}
} }
let challenge = $state("") let challenge = $state("")

View file

@ -1,17 +1,14 @@
<script lang="ts"> <script lang="ts">
import type {Profile} from "@welshman/util" import {getKey} from "@lib/implicit"
import KeyDownload from "@app/components/KeyDownload.svelte" import KeyDownload from "@app/components/KeyDownload.svelte"
import SignUpComplete from "@app/components/SignUpComplete.svelte"
import {pushModal} from "@app/util/modal"
type Props = { type Props = {
secret: string next: () => void
profile: Profile
} }
const {secret, profile}: Props = $props() const {next}: Props = $props()
const next = () => pushModal(SignUpComplete, {secret, profile}) const secret = getKey<string>("signup.secret")!
</script> </script>
<KeyDownload {secret} {next} /> <KeyDownload {secret} {next} />

View file

@ -1,32 +1,29 @@
<script lang="ts"> <script lang="ts">
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import {makeProfile, makeSecret} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import {getKey, setKey} from "@lib/implicit"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import SignUpKey from "@app/components/SignUpKey.svelte"
import SignUpEmail from "@app/components/SignUpEmail.svelte"
import {pushModal} from "@app/util/modal"
type Props = { type Props = {
flow: "nostr" | "email" next: () => void
} }
const {flow}: Props = $props() const {next}: Props = $props()
const initialValues = { const profile = getKey<Profile>("signup.profile")!
secret: makeSecret(),
profile: makeProfile(), const initialValues = {profile, shouldBroadcast: false}
shouldBroadcast: false,
}
const back = () => history.back() const back = () => history.back()
const onsubmit = (values: {profile: Profile}) => const onsubmit = ({profile}: {profile: Profile}) => {
pushModal(flow === "nostr" ? SignUpKey : SignUpEmail, values) setKey("signup.profile", profile)
next()
}
</script> </script>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">

View file

@ -20,6 +20,7 @@
import * as net from "@welshman/net" import * as net from "@welshman/net"
import * as app from "@welshman/app" import * as app from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import * as implicit from "@lib/implicit"
import AppContainer from "@app/components/AppContainer.svelte" import AppContainer from "@app/components/AppContainer.svelte"
import ModalContainer from "@app/components/ModalContainer.svelte" import ModalContainer from "@app/components/ModalContainer.svelte"
import {setupHistory} from "@app/util/history" import {setupHistory} from "@app/util/history"
@ -50,6 +51,7 @@
nip19, nip19,
theme, theme,
...lib, ...lib,
...implicit,
...welshmanSigner, ...welshmanSigner,
...router, ...router,
...store, ...store,

View file

@ -2,7 +2,7 @@
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {hexToBytes} from "@welshman/lib" import {hexToBytes} from "@welshman/lib"
import {displayPubkey, displayProfile} from "@welshman/util" import {displayPubkey, displayProfile} from "@welshman/util"
import {pubkey, session, displayNip05, deriveProfile, SessionMethod} from "@welshman/app" import {pubkey, session, displayNip05, deriveProfile} from "@welshman/app"
import {slideAndFade} from "@lib/transition" import {slideAndFade} from "@lib/transition"
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl" import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
@ -20,7 +20,6 @@
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
import ProfileEdit from "@app/components/ProfileEdit.svelte" import ProfileEdit from "@app/components/ProfileEdit.svelte"
import ProfileDelete from "@app/components/ProfileDelete.svelte" import ProfileDelete from "@app/components/ProfileDelete.svelte"
import KeyRecoveryRequest from "@app/components/KeyRecoveryRequest.svelte"
import SignerStatus from "@app/components/SignerStatus.svelte" import SignerStatus from "@app/components/SignerStatus.svelte"
import InfoKeys from "@app/components/InfoKeys.svelte" import InfoKeys from "@app/components/InfoKeys.svelte"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
@ -37,11 +36,9 @@
const startEdit = () => pushModal(ProfileEdit) const startEdit = () => pushModal(ProfileEdit)
const startEject = () => pushModal(InfoKeys)
const startDelete = () => pushModal(ProfileDelete) const startDelete = () => pushModal(ProfileDelete)
const startRecovery = () => pushModal(KeyRecoveryRequest) const startRecovery = () => pushModal(InfoKeys)
let showAdvanced = false let showAdvanced = false
</script> </script>
@ -87,7 +84,7 @@
{#snippet info()} {#snippet info()}
<p> <p>
Your email and password can only be used to log into {PLATFORM_NAME}. Your email and password can only be used to log into {PLATFORM_NAME}.
<Button class="link" onclick={startEject}>Start holding your own keys</Button> <Button class="link" onclick={startRecovery}>Start holding your own keys</Button>
</p> </p>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
@ -159,12 +156,6 @@
</div> </div>
{#if showAdvanced} {#if showAdvanced}
<div transition:slideAndFade class="flex flex-col gap-2 pt-4"> <div transition:slideAndFade class="flex flex-col gap-2 pt-4">
{#if $session?.method === SessionMethod.Pomade}
<Button class="btn btn-neutral" onclick={startRecovery}>
<Icon icon={Key} />
Recover your key
</Button>
{/if}
<Button class="btn btn-error" onclick={startDelete}> <Button class="btn btn-error" onclick={startDelete}>
<Icon icon={TrashBin2} /> <Icon icon={TrashBin2} />
Delete your profile Delete your profile