feat: improve onboarding

This commit is contained in:
reya 2024-01-26 10:17:23 +07:00
parent 74426e13c8
commit 9ba584bf14
14 changed files with 222 additions and 125 deletions

View File

@ -25,6 +25,7 @@
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",

View File

@ -124,11 +124,11 @@ export function CreateAccountAddress() {
// add account to storage
await storage.createSetting("nsecbunker", "1");
const dbAccount = await storage.createAccount({
const newAccount = await storage.createAccount({
pubkey: account,
privkey: localSigner.privateKey,
});
ark.account = dbAccount;
ark.account = newAccount;
// get final signer with newly created account
const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
@ -153,9 +153,11 @@ export function CreateAccountAddress() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Create Account</h1>
<h1 className="text-2xl font-semibold">
Let's set up your account on Nostr
</h1>
</div>
{!services ? (
<div className="flex items-center justify-center w-full">

View File

@ -1,13 +1,16 @@
import { useArk } from "@lume/ark";
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import * as Checkbox from "@radix-ui/react-checkbox";
import { desktopDir } from "@tauri-apps/api/path";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai";
import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -17,73 +20,165 @@ export function CreateAccountKeys() {
const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate();
const generateNostrKeys = async () => {
const signer = NDKPrivateKeySigner.generate();
const pubkey = getPublicKey(signer.privateKey);
const [key, setKey] = useState("");
const [loading, setLoading] = useState(false);
const [showKey, setShowKey] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(signer.privateKey);
const submit = async () => {
try {
setLoading(true);
ark.updateNostrSigner({ signer });
const privkey = nip19.decode(key).data as string;
const signer = new NDKPrivateKeySigner(privkey);
const pubkey = getPublicKey(privkey);
const downloadPath = await desktopDir();
const fileName = `nostr_keys_${nanoid(4)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
ark.updateNostrSigner({ signer });
if (!filePath) {
return toast.info("You need to save account keys before continue.");
const downloadPath = await desktopDir();
const fileName = `nostr_keys_${nanoid(4)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
if (!filePath) {
return toast.info("You need to save account keys before continue.");
}
await writeTextFile(
filePath,
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPrivate key: ${key}`,
);
const newAccount = await storage.createAccount({
pubkey: pubkey,
privkey: privkey,
});
ark.account = newAccount;
setLoading(false);
setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
toast.error(String(e));
}
await writeTextFile(
filePath,
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPublic key: ${npub}\nPrivate key: ${nsec}`,
);
await storage.createAccount({
pubkey: pubkey,
privkey: signer.privateKey,
});
setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true });
};
useEffect(() => {
const privkey = NDKPrivateKeySigner.generate().privateKey;
setKey(nip19.nsecEncode(privkey));
}, []);
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Create Account</h1>
<h1 className="text-2xl font-semibold">
This is your new Account Key
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Keep your key in safe place. If you lose this key, you will lose
access to your account.
</p>
</div>
<div className="flex flex-col gap-3 mb-0">
<div className="flex flex-col gap-2">
<label
htmlFor="npub"
className="text-sm font-semibold uppercase text-neutral-600"
>
Public Key
</label>
<input
readOnly
type="text"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="nsec"
className="text-sm font-semibold uppercase text-neutral-600"
>
Private Key
</label>
<input
readOnly
type="text"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
<div className="flex flex-col gap-6 mb-0">
<div className="flex flex-col gap-6">
<div className="relative">
<input
readOnly
value={key}
type={showKey ? "text" : "password"}
className="pl-3 pr-14 w-full resize-none text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
<button
type="button"
onClick={() => setShowKey((state) => !state)}
className="absolute right-2 top-2 size-10 inline-flex items-center justify-center rounded-lg text-white bg-neutral-800 hover:bg-neutral-700"
>
{showKey ? (
<EyeOnIcon className="size-5" />
) : (
<EyeOffIcon className="size-5" />
)}
</button>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c1}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm1"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm1"
>
I understand the risk of lost private key.
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c2}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm2"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm2"
>
I will make sure keep it safe and not sharing with anyone.
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm3"
>
I understand I cannot recover private key.
</label>
</div>
</div>
</div>
<button
type="button"
onClick={submit}
disabled={!confirm.c1 || !confirm.c2 || !confirm.c3}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Save key & Continue"
)}
</button>
</div>
</div>
</div>

View File

@ -21,29 +21,14 @@ export function CreateAccountScreen() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Let's get you set up on Nostr.
</h1>
<h1 className="text-2xl font-semibold">Let's Get Started</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Choose one of methods below to create your account
</p>
</div>
<div className="flex flex-col gap-4">
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "managed" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Managed by Provider</p>
<p className="text-sm font-medium text-neutral-500">
A 3rd party provider will handle your sign in keys for you.
</p>
</button>
<button
type="button"
onClick={() => setMethod("self")}
@ -57,6 +42,19 @@ export function CreateAccountScreen() {
You create your keys and keep them safe.
</p>
</button>
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "managed" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Managed by Provider</p>
<p className="text-sm font-medium text-neutral-500">
A 3rd party provider will handle your sign in keys for you.
</p>
</button>
<button
type="button"
onClick={next}

View File

@ -50,7 +50,7 @@ export function LoginWithKey() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Private Key</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">

View File

@ -66,7 +66,7 @@ export function LoginWithNsecbunker() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Enter your nsecbunker token

View File

@ -128,7 +128,7 @@ export function LoginWithOAuth() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Nostr Address</h1>
</div>

View File

@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
export function LoginScreen() {
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Welcome back, anon!</h1>
</div>

View File

@ -2,10 +2,7 @@ import { useArk } from "@lume/ark";
import { InfoIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { TranslateRegisterModal } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import * as Switch from "@radix-ui/react-switch";
import { useQueryClient } from "@tanstack/react-query";
import {
isPermissionGranted,
requestPermission,
@ -17,7 +14,6 @@ import { toast } from "sonner";
export function OnboardingScreen() {
const ark = useArk();
const storage = useStorage();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
@ -61,32 +57,7 @@ export function OnboardingScreen() {
// get account contacts
await ark.getUserContacts();
// refetch newsfeed
await queryClient.prefetchInfiniteQuery({
queryKey: ["timeline-9999"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
});
navigate("/");
navigate("/", { replace: true });
};
useEffect(() => {
@ -117,7 +88,7 @@ export function OnboardingScreen() {
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
You&apos;re almost ready to use Lume.

View File

@ -145,7 +145,7 @@ export class Ark {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!profile) throw new Error("user not found");
if (!profile) return null;
return profile;
} catch {
throw new Error("user not found");

View File

@ -57,8 +57,8 @@ export function OnboardingInterestScreen() {
<div className="w-full flex-1 min-h-0 flex flex-col justify-between">
<div className="flex-1 min-h-0 overflow-y-auto px-8 py-8">
<div className="flex flex-col gap-8">
{TOPICS.map((topic, index) => (
<div key={topic.title + index} className="flex flex-col gap-4">
{TOPICS.map((topic) => (
<div key={topic.title} className="flex flex-col gap-4">
<div className="w-full flex items-center justify-between">
<div className="inline-flex items-center gap-2.5">
<img
@ -79,6 +79,7 @@ export function OnboardingInterestScreen() {
<div className="flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => (
<button
key={hashtag}
type="button"
onClick={() => toggleHashtag(hashtag)}
className={cn(

View File

@ -35,19 +35,17 @@ export function OnboardingProfileScreen() {
navigate("/interests");
}
const oldProfile = await ark.getUserProfile();
const profile: NDKUserProfile = {
const prevProfile = await ark.getUserProfile();
const newProfile: NDKUserProfile = {
...data,
lud16: "", // temporary remove lud16
nip05: oldProfile?.nip05 || "",
nip05: prevProfile?.nip05 || "",
bio: data.about,
image: picture,
picture: picture,
};
const publish = await ark.createEvent({
content: JSON.stringify(profile),
content: JSON.stringify(newProfile),
kind: NDKKind.Metadata,
tags: [],
});
@ -57,7 +55,7 @@ export function OnboardingProfileScreen() {
await storage.clearProfileCache(ark.account.pubkey);
await queryClient.setQueryData(
["user", ark.account.pubkey],
() => profile,
() => newProfile,
);
setLoading(false);

View File

@ -114,11 +114,11 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
</div>
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-44 w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" />
</div>
) : isError ? (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-44 w-full items-center justify-center">
Error. Cannot get trending users
</div>
) : (
@ -171,9 +171,9 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
type="button"
onClick={submit}
disabled={loading}
className="inline-flex items-center justify-center gap-2 px-6 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
>
Save
Save & Go Back
</button>
</div>
</div>

View File

@ -117,6 +117,9 @@ importers:
'@radix-ui/react-avatar':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
@ -2138,6 +2141,34 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.8
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies: