Add nip 01 login flow to mobile
This commit is contained in:
parent
27d9d4fff1
commit
69d0e11ba4
7 changed files with 305 additions and 64 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
createEvent,
|
createEvent,
|
||||||
makeProfile,
|
makeProfile,
|
||||||
|
|
@ -8,76 +9,31 @@
|
||||||
isPublishedProfile,
|
isPublishedProfile,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
|
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
import InfoHandle from "@app/components/InfoHandle.svelte"
|
import {clearModals} from "@app/modal"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
const values = $state({...($profilesByPubkey.get($pubkey!) || makeProfile())})
|
const initialValues = {...($profilesByPubkey.get($pubkey!) || makeProfile())}
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const saveEdit = () => {
|
const onsubmit = (profile: Profile) => {
|
||||||
const relays = ctx.app.router.FromUser().getUrls()
|
const relays = ctx.app.router.FromUser().getUrls()
|
||||||
const template = isPublishedProfile(values)
|
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||||
? editProfile($state.snapshot(values))
|
|
||||||
: createProfile($state.snapshot(values))
|
|
||||||
const event = createEvent(template.kind, template)
|
const event = createEvent(template.kind, template)
|
||||||
|
|
||||||
publishThunk({event, relays})
|
publishThunk({event, relays})
|
||||||
pushToast({message: "Your profile has been updated!"})
|
pushToast({message: "Your profile has been updated!"})
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
let file: File | undefined = $state()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="col-4" onsubmit={preventDefault(saveEdit)}>
|
<ProfileEditForm {initialValues} {onsubmit}>
|
||||||
<div class="flex justify-center py-2">
|
{#snippet footer()}
|
||||||
<InputProfilePicture bind:file bind:url={values.picture} />
|
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||||
</div>
|
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
|
||||||
<Field>
|
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
||||||
{#snippet label()}
|
</div>
|
||||||
<p>Username</p>
|
{/snippet}
|
||||||
{/snippet}
|
</ProfileEditForm>
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="user-circle" />
|
|
||||||
<input bind:value={values.name} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>About You</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={values.about}>
|
|
||||||
</textarea>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Nostr Address</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="map-point" />
|
|
||||||
<input bind:value={values.nip05} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<p>
|
|
||||||
<Button class="link" onclick={() => pushModal(InfoHandle)}>What is a nostr address?</Button>
|
|
||||||
</p>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
|
||||||
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
|
||||||
79
src/app/components/ProfileEditForm.svelte
Normal file
79
src/app/components/ProfileEditForm.svelte
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
|
import {makeProfile} from "@welshman/util"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
||||||
|
import InfoHandle from "@app/components/InfoHandle.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues?: Profile
|
||||||
|
onsubmit: (profile: Profile) => void
|
||||||
|
hideAddress?: boolean
|
||||||
|
footer: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {initialValues = makeProfile(), hideAddress, onsubmit, footer}: Props = $props()
|
||||||
|
|
||||||
|
const values = $state(initialValues)
|
||||||
|
|
||||||
|
const submit = () => onsubmit($state.snapshot(values))
|
||||||
|
|
||||||
|
let file: File | undefined = $state()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="col-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<div class="flex justify-center py-2">
|
||||||
|
<InputProfilePicture bind:file bind:url={values.picture} />
|
||||||
|
</div>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Username</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="user-circle" />
|
||||||
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
What would you like people to call you?
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>About You</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={values.about}
|
||||||
|
></textarea>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Give a brief introduction to why you're here.
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{#if !hideAddress}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Nostr Address</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="map-point" />
|
||||||
|
<input bind:value={values.nip05} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
<Button class="link" onclick={() => pushModal(InfoHandle)}
|
||||||
|
>What is a nostr address?</Button>
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
|
{@render footer()}
|
||||||
|
</form>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {postJson} from "@welshman/lib"
|
import {postJson} from "@welshman/lib"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.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 SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
|
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
|
||||||
|
|
@ -37,18 +39,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signup = () => {
|
const usePassword = () => {
|
||||||
if (BURROW_URL) {
|
if (BURROW_URL) {
|
||||||
signupPassword()
|
signupPassword()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useKey = () => pushModal(SignUpKey)
|
||||||
|
|
||||||
let email = $state("")
|
let email = $state("")
|
||||||
let password = $state("")
|
let password = $state("")
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(signup)}>
|
<form class="column gap-4" onsubmit={preventDefault(usePassword)}>
|
||||||
<h1 class="heading">Sign up with Nostr</h1>
|
<h1 class="heading">Sign up with Nostr</h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
{PLATFORM_NAME} is built using the
|
{PLATFORM_NAME} is built using the
|
||||||
|
|
@ -89,10 +93,21 @@
|
||||||
</p>
|
</p>
|
||||||
<Divider>Or</Divider>
|
<Divider>Or</Divider>
|
||||||
{/if}
|
{/if}
|
||||||
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
{#if Capacitor.isNativePlatform()}
|
||||||
<Icon icon="square-share-line" />
|
<Button onclick={useKey} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
Get going on nstart
|
<Icon icon="key" />
|
||||||
</a>
|
Generate a key
|
||||||
|
</Button>
|
||||||
|
<a href={nstart} class="btn">
|
||||||
|
<Icon icon="square-share-line" />
|
||||||
|
Create an account on Nstart
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
|
<Icon icon="square-share-line" />
|
||||||
|
Create an account on Nstart
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<Button class="link" onclick={login}>Log in instead</Button>
|
<Button class="link" onclick={login}>Log in instead</Button>
|
||||||
|
|
|
||||||
84
src/app/components/SignUpKey.svelte
Normal file
84
src/app/components/SignUpKey.svelte
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {encrypt} from "nostr-tools/nip49"
|
||||||
|
import {hexToBytes} from "@noble/hashes/utils"
|
||||||
|
import {makeSecret, getPubkey} from "@welshman/signer"
|
||||||
|
import {preventDefault, downloadText} from "@lib/html"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SignUpKeyConfirm from "@app/components/SignUpKeyConfirm.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const secret = makeSecret()
|
||||||
|
|
||||||
|
const pubkey = getPubkey(secret)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (password.length < 12) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Passwords must be at least 12 characters long.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ncryptsec = encrypt(hexToBytes(secret), password)
|
||||||
|
|
||||||
|
downloadText("Nostr Secret Key.txt", ncryptsec)
|
||||||
|
|
||||||
|
pushModal(SignUpKeyConfirm, {secret, pubkey, ncryptsec})
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Welcome to Nostr!</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
|
||||||
|
talk to each other. Users own their social identity instead of renting it from a tech company, and
|
||||||
|
can take it with them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This means that instead of using a password to log in, you generate a <strong
|
||||||
|
>secret key</strong>
|
||||||
|
which gives you full control over your account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Keeping this key safe is very important, so we encourage you to download an encrypted copy. To
|
||||||
|
do this, go ahead and fill in the password you'd like to use to secure your key below.
|
||||||
|
</p>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Password*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input bind:value={password} class="grow" type="password" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Passwords should be at least 12 characters long. Write this down!</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit">
|
||||||
|
Download my key
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
66
src/app/components/SignUpKeyConfirm.svelte
Normal file
66
src/app/components/SignUpKeyConfirm.svelte
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {preventDefault, copyToClipboard} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
pubkey: string
|
||||||
|
ncryptsec: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, pubkey, ncryptsec}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
copyToClipboard(ncryptsec)
|
||||||
|
pushToast({message: "Your secret key has been copied to your clipboard!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
pushModal(SignUpProfile, {secret, pubkey})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Download your key</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
Great! We've encrypted your secret key and saved it to your device. If that didn't work, or if
|
||||||
|
you'd rather save your key somewhere else, you can find the encrypted version below:
|
||||||
|
</p>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Encrypted Secret Key
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input value={ncryptsec} class="ellipsize grow" />
|
||||||
|
<Button onclick={copy} class="flex items-center">
|
||||||
|
<Icon icon="copy" />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit">
|
||||||
|
Fill out your profile
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
28
src/app/components/SignUpProfile.svelte
Normal file
28
src/app/components/SignUpProfile.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
|
import {PROFILE, createProfile, createEvent} from "@welshman/util"
|
||||||
|
import {addSession, publishThunk} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
|
import {INDEXER_RELAYS} from "@app/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, pubkey}: Props = $props()
|
||||||
|
|
||||||
|
const onsubmit = (profile: Profile) => {
|
||||||
|
const event = createEvent(PROFILE, createProfile(profile))
|
||||||
|
|
||||||
|
addSession({method: "nip01", secret, pubkey})
|
||||||
|
publishThunk({event, relays: INDEXER_RELAYS})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProfileEditForm hideAddress {onsubmit}>
|
||||||
|
{#snippet footer()}
|
||||||
|
<Button type="submit" class="btn btn-primary">Create Account</Button>
|
||||||
|
{/snippet}
|
||||||
|
</ProfileEditForm>
|
||||||
|
|
@ -76,3 +76,16 @@ export const createScroller = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMobile = "ontouchstart" in document.documentElement
|
export const isMobile = "ontouchstart" in document.documentElement
|
||||||
|
|
||||||
|
export const downloadText = (filename: string, text: string) => {
|
||||||
|
const blob = new Blob([text], {type: "text/plain"})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue