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
+3 -3
View File
@@ -7,13 +7,13 @@
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.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 {pushModal} from "@app/util/modal"
const back = () => history.back()
const startEject = () => pushModal(ProfileEject)
const startRecoveryRequest = () => pushModal(KeyRecoveryRequest)
</script>
<div class="column gap-4">
@@ -44,7 +44,7 @@
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="btn btn-primary" onclick={startEject}>
<Button class="btn btn-primary" onclick={startRecoveryRequest}>
<Icon icon={CheckCircle} />
I want to hold my own keys
</Button>
+3 -13
View File
@@ -16,8 +16,9 @@
const loadSessions = async () => {
if (!isPomadeSession($session)) return
const client = new Client($session.clientOptions)
try {
const client = new Client($session.clientOptions)
const result = await client.listSessions()
const pubkey = await client.getPubkey()
@@ -42,20 +43,9 @@
}
sessions = Array.from(sessionMap.values())
} else {
pushToast({
theme: "error",
message: "Failed to load sessions",
})
}
} finally {
client.stop()
} catch (e) {
console.error(e)
pushToast({
theme: "error",
message: "Failed to load sessions",
})
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {updateProfile} from "../core/commands"
import {updateProfile} from "@app/core/commands"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
+49 -5
View File
@@ -1,21 +1,65 @@
<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 Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import LogIn from "@app/components/LogIn.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 {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"
setKey("signup.email", "")
setKey("signup.secret", makeSecret())
setKey("signup.profile", makeProfile())
setKey("signup.clientOptions", undefined)
const hasPomade = POMADE_SIGNERS.length >= 3
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>
<div class="column gap-4">
@@ -26,12 +70,12 @@
users control over their digital identity using <strong>cryptographic key pairs</strong>.
</p>
{#if hasPomade}
<Button onclick={useEmail} class="btn btn-primary">
<Button onclick={flows.email.start} class="btn btn-primary">
<Icon icon={Letter} />
Sign up with email
</Button>
{/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} />
Generate a key
</Button>
+2 -13
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import type {Profile} from "@welshman/util"
import {loginWithNip01} from "@welshman/app"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
@@ -8,23 +6,14 @@
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {clearModals} from "@app/util/modal"
import {initProfile} from "@app/core/commands"
type Props = {
secret: string
profile: Profile
next: () => void
}
const {secret, profile}: Props = $props()
const {next}: Props = $props()
const back = () => history.back()
const next = () => {
loginWithNip01(secret)
initProfile(profile)
clearModals()
}
</script>
<form class="column gap-4" onsubmit={preventDefault(next)}>
+9 -6
View File
@@ -1,13 +1,12 @@
<script lang="ts">
import {Client} from "@pomade/core"
import {choice} from "@welshman/lib"
import {makeSecret} from "@welshman/util"
import type {Profile} from "@welshman/util"
import {preventDefault} from "@lib/html"
import Letter from "@assets/icons/letter.svg?dataurl"
import Key from "@assets/icons/key.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.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 FieldInline from "@lib/components/FieldInline.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@@ -19,10 +18,10 @@
import {pushModal} from "@app/util/modal"
type Props = {
profile: Profile
next: () => void
}
const {profile}: Props = $props()
const {next}: Props = $props()
const back = () => history.back()
@@ -39,7 +38,8 @@
let client: Client | undefined = undefined
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) {
return pushToast({
@@ -70,7 +70,10 @@
})
}
pushModal(SignUpEmailConfirm, {email, profile, clientOptions})
setKey("signup.email", email)
setKey("signup.clientOptions", clientOptions)
pushModal(SignUpEmailConfirm, {next})
} catch (e) {
console.error(e)
+6 -20
View File
@@ -1,9 +1,7 @@
<script lang="ts">
import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/util"
import {sleep} from "@welshman/lib"
import {loginWithPomade} from "@welshman/app"
import {preventDefault} from "@lib/html"
import {getKey} from "@lib/implicit"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
@@ -13,18 +11,14 @@
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.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 = {
email: string
profile: Profile
clientOptions: ClientOptions
next: () => void
}
const {email, profile, clientOptions}: Props = $props()
const {next}: Props = $props()
const email = getKey<string>("signup.email")
const back = () => history.back()
@@ -34,15 +28,7 @@
// Just pretend we're validating, they clearly got a code from somewhere
await sleep(800)
try {
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
pushToast({message: "Successfully logged in!"})
initProfile(profile)
setChecked("*")
clearModals()
} finally {
loading = false
}
next()
}
let challenge = $state("")
+4 -7
View File
@@ -1,17 +1,14 @@
<script lang="ts">
import type {Profile} from "@welshman/util"
import {getKey} from "@lib/implicit"
import KeyDownload from "@app/components/KeyDownload.svelte"
import SignUpComplete from "@app/components/SignUpComplete.svelte"
import {pushModal} from "@app/util/modal"
type Props = {
secret: string
profile: Profile
next: () => void
}
const {secret, profile}: Props = $props()
const {next}: Props = $props()
const next = () => pushModal(SignUpComplete, {secret, profile})
const secret = getKey<string>("signup.secret")!
</script>
<KeyDownload {secret} {next} />
+10 -13
View File
@@ -1,32 +1,29 @@
<script lang="ts">
import type {Profile} from "@welshman/util"
import {makeProfile, makeSecret} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.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 Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.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 = {
flow: "nostr" | "email"
next: () => void
}
const {flow}: Props = $props()
const {next}: Props = $props()
const initialValues = {
secret: makeSecret(),
profile: makeProfile(),
shouldBroadcast: false,
}
const profile = getKey<Profile>("signup.profile")!
const initialValues = {profile, shouldBroadcast: false}
const back = () => history.back()
const onsubmit = (values: {profile: Profile}) =>
pushModal(flow === "nostr" ? SignUpKey : SignUpEmail, values)
const onsubmit = ({profile}: {profile: Profile}) => {
setKey("signup.profile", profile)
next()
}
</script>
<div class="flex flex-col gap-4">
+2
View File
@@ -20,6 +20,7 @@
import * as net from "@welshman/net"
import * as app from "@welshman/app"
import {isMobile} from "@lib/html"
import * as implicit from "@lib/implicit"
import AppContainer from "@app/components/AppContainer.svelte"
import ModalContainer from "@app/components/ModalContainer.svelte"
import {setupHistory} from "@app/util/history"
@@ -50,6 +51,7 @@
nip19,
theme,
...lib,
...implicit,
...welshmanSigner,
...router,
...store,
+3 -12
View File
@@ -2,7 +2,7 @@
import * as nip19 from "nostr-tools/nip19"
import {hexToBytes} from "@welshman/lib"
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 PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
@@ -20,7 +20,6 @@
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import ProfileEdit from "@app/components/ProfileEdit.svelte"
import ProfileDelete from "@app/components/ProfileDelete.svelte"
import KeyRecoveryRequest from "@app/components/KeyRecoveryRequest.svelte"
import SignerStatus from "@app/components/SignerStatus.svelte"
import InfoKeys from "@app/components/InfoKeys.svelte"
import {PLATFORM_NAME} from "@app/core/state"
@@ -37,11 +36,9 @@
const startEdit = () => pushModal(ProfileEdit)
const startEject = () => pushModal(InfoKeys)
const startDelete = () => pushModal(ProfileDelete)
const startRecovery = () => pushModal(KeyRecoveryRequest)
const startRecovery = () => pushModal(InfoKeys)
let showAdvanced = false
</script>
@@ -87,7 +84,7 @@
{#snippet info()}
<p>
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>
{/snippet}
</FieldInline>
@@ -159,12 +156,6 @@
</div>
{#if showAdvanced}
<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}>
<Icon icon={TrashBin2} />
Delete your profile