From 74426e13c8470d9436d1ff8322d103e6977887b1 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 25 Jan 2024 15:25:40 +0700 Subject: [PATCH] wip: improve onboarding --- apps/desktop/src/router.tsx | 24 +- .../src/routes/auth/create-address.tsx | 261 ++++++++++++ apps/desktop/src/routes/auth/create-keys.tsx | 91 +++++ apps/desktop/src/routes/auth/create.tsx | 376 +++--------------- apps/desktop/src/routes/auth/login-key.tsx | 2 +- .../src/routes/auth/login-nsecbunker.tsx | 2 +- apps/desktop/src/routes/auth/login-oauth.tsx | 4 +- apps/desktop/src/routes/auth/login.tsx | 18 +- apps/desktop/src/routes/auth/welcome.tsx | 27 +- 9 files changed, 446 insertions(+), 359 deletions(-) create mode 100644 apps/desktop/src/routes/auth/create-address.tsx create mode 100644 apps/desktop/src/routes/auth/create-keys.tsx diff --git a/apps/desktop/src/router.tsx b/apps/desktop/src/router.tsx index 3fd3b071..a840b26f 100644 --- a/apps/desktop/src/router.tsx +++ b/apps/desktop/src/router.tsx @@ -207,9 +207,6 @@ export default function Router() { }, { path: "create", - loader: async () => { - return await ark.getOAuthServices(); - }, async lazy() { const { CreateAccountScreen } = await import( "./routes/auth/create" @@ -217,6 +214,27 @@ export default function Router() { return { Component: CreateAccountScreen }; }, }, + { + path: "create-keys", + async lazy() { + const { CreateAccountKeys } = await import( + "./routes/auth/create-keys" + ); + return { Component: CreateAccountKeys }; + }, + }, + { + path: "create-address", + loader: async () => { + return await ark.getOAuthServices(); + }, + async lazy() { + const { CreateAccountAddress } = await import( + "./routes/auth/create-address" + ); + return { Component: CreateAccountAddress }; + }, + }, { path: "login", async lazy() { diff --git a/apps/desktop/src/routes/auth/create-address.tsx b/apps/desktop/src/routes/auth/create-address.tsx new file mode 100644 index 00000000..8a525bdc --- /dev/null +++ b/apps/desktop/src/routes/auth/create-address.tsx @@ -0,0 +1,261 @@ +import { useArk } from "@lume/ark"; +import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons"; +import { useStorage } from "@lume/storage"; +import { onboardingAtom } from "@lume/utils"; +import NDK, { + NDKEvent, + NDKKind, + NDKNip46Signer, + NDKPrivateKeySigner, +} from "@nostr-dev-kit/ndk"; +import * as Select from "@radix-ui/react-select"; +import { UnlistenFn } from "@tauri-apps/api/event"; +import { Window } from "@tauri-apps/api/window"; +import { useSetAtom } from "jotai"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +const Item = ({ event }: { event: NDKEvent }) => { + const domain = JSON.parse(event.content).nip05.replace("_@", ""); + + return ( + + @{domain} + + + + + ); +}; + +export function CreateAccountAddress() { + const ark = useArk(); + const storage = useStorage(); + const services = useLoaderData() as NDKEvent[]; + const setOnboarding = useSetAtom(onboardingAtom); + const navigate = useNavigate(); + + const [serviceId, setServiceId] = useState(services?.[0]?.id); + const [loading, setIsLoading] = useState(false); + + const { + register, + handleSubmit, + formState: { isValid }, + } = useForm(); + + const getDomainName = (id: string) => { + const event = services.find((ev) => ev.id === id); + return JSON.parse(event.content).nip05.replace("_@", "") as string; + }; + + const onSubmit = async (data: { username: string; email: string }) => { + try { + setIsLoading(true); + + const domain = getDomainName(serviceId); + const service = services.find((ev) => ev.id === serviceId); + + // generate ndk for nsecbunker + const localSigner = NDKPrivateKeySigner.generate(); + const bunker = new NDK({ + explicitRelayUrls: [ + "wss://relay.nsecbunker.com/", + "wss://nostr.vulpem.com/", + ], + }); + await bunker.connect(2000); + + // generate tmp remote singer for create account + const remoteSigner = new NDKNip46Signer( + bunker, + service.pubkey, + localSigner, + ); + + // handle auth url request + let unlisten: UnlistenFn; + let authWindow: Window; + let account: string = undefined; + + remoteSigner.addListener("authUrl", async (authUrl: string) => { + authWindow = new Window(`auth-${serviceId}`, { + url: authUrl, + title: domain, + titleBarStyle: "overlay", + width: 600, + height: 650, + center: true, + closable: false, + }); + unlisten = await authWindow.onCloseRequested(() => { + if (!account) { + setIsLoading(false); + unlisten(); + + return authWindow.close(); + } + }); + }); + + // create new account + account = await remoteSigner.createAccount( + data.username, + domain, + data.email, + ); + + if (!account) { + unlisten(); + setIsLoading(false); + + authWindow.close(); + + return toast.error("Failed to create new account, try again later"); + } + + unlisten(); + authWindow.close(); + + // add account to storage + await storage.createSetting("nsecbunker", "1"); + const dbAccount = await storage.createAccount({ + pubkey: account, + privkey: localSigner.privateKey, + }); + ark.account = dbAccount; + + // get final signer with newly created account + const finalSigner = new NDKNip46Signer(bunker, account, localSigner); + await finalSigner.blockUntilReady(); + + // update main ndk instance signer + ark.updateNostrSigner({ signer: finalSigner }); + + // remove default nsecbunker profile and contact list + // await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] }); + await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] }); + + setIsLoading(false); + setOnboarding({ open: true, newUser: true }); + + return navigate("/auth/onboarding", { replace: true }); + } catch (e) { + setIsLoading(false); + toast.error(String(e)); + } + }; + + return ( +
+
+
+

Create Account

+
+ {!services ? ( +
+ +
+ ) : ( +
+
+
+ +
+
+ + + + @{getDomainName(serviceId)} + + + + + + + + + + Choose a Provider + + {services.map((service) => ( + + ))} + + + + + +
+ + Use to login to Lume and other Nostr apps. You can choose + provider you trust to manage your account + +
+
+
+
+ + +
+ + Use for recover your account if you lose your password + +
+
+
+ +
+
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/routes/auth/create-keys.tsx b/apps/desktop/src/routes/auth/create-keys.tsx new file mode 100644 index 00000000..4090075c --- /dev/null +++ b/apps/desktop/src/routes/auth/create-keys.tsx @@ -0,0 +1,91 @@ +import { useArk } from "@lume/ark"; +import { useStorage } from "@lume/storage"; +import { onboardingAtom } from "@lume/utils"; +import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; +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 { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +export function CreateAccountKeys() { + const ark = useArk(); + const storage = useStorage(); + const setOnboarding = useSetAtom(onboardingAtom); + const navigate = useNavigate(); + + const generateNostrKeys = async () => { + const signer = NDKPrivateKeySigner.generate(); + const pubkey = getPublicKey(signer.privateKey); + + const npub = nip19.npubEncode(pubkey); + const nsec = nip19.nsecEncode(signer.privateKey); + + ark.updateNostrSigner({ signer }); + + 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---\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 }); + }; + + return ( +
+
+
+

Create Account

+
+
+
+ + +
+
+ + +
+
+
+
+ ); +} diff --git a/apps/desktop/src/routes/auth/create.tsx b/apps/desktop/src/routes/auth/create.tsx index 83ed6973..9a1a7c3f 100644 --- a/apps/desktop/src/routes/auth/create.tsx +++ b/apps/desktop/src/routes/auth/create.tsx @@ -1,342 +1,74 @@ -import { useArk } from "@lume/ark"; -import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { onboardingAtom } from "@lume/utils"; -import NDK, { - NDKEvent, - NDKKind, - NDKNip46Signer, - NDKPrivateKeySigner, -} from "@nostr-dev-kit/ndk"; -import * as Select from "@radix-ui/react-select"; -import { UnlistenFn } from "@tauri-apps/api/event"; -import { desktopDir } from "@tauri-apps/api/path"; -import { Window } from "@tauri-apps/api/window"; -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 { LoaderIcon } from "@lume/icons"; +import { cn } from "@lume/utils"; import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { useLoaderData, useNavigate } from "react-router-dom"; -import { toast } from "sonner"; - -const Item = ({ event }: { event: NDKEvent }) => { - const domain = JSON.parse(event.content).nip05.replace("_@", ""); - - return ( - - @{domain} - - - - - ); -}; +import { Link, useNavigate } from "react-router-dom"; export function CreateAccountScreen() { - const ark = useArk(); - const storage = useStorage(); - const services = useLoaderData() as NDKEvent[]; - const setOnboarding = useSetAtom(onboardingAtom); const navigate = useNavigate(); - const [serviceId, setServiceId] = useState(services?.[0]?.id); - const [loading, setIsLoading] = useState(false); + const [method, setMethod] = useState<"self" | "managed">("self"); + const [loading, setLoading] = useState(false); - const { - register, - handleSubmit, - formState: { isValid }, - } = useForm(); + const next = () => { + setLoading(true); - const getDomainName = (id: string) => { - const event = services.find((ev) => ev.id === id); - return JSON.parse(event.content).nip05.replace("_@", "") as string; - }; - - const generateNostrKeys = async () => { - const signer = NDKPrivateKeySigner.generate(); - const pubkey = getPublicKey(signer.privateKey); - - const npub = nip19.npubEncode(pubkey); - const nsec = nip19.nsecEncode(signer.privateKey); - - ark.updateNostrSigner({ signer }); - - 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---\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 }); - }; - - const onSubmit = async (data: { username: string; email: string }) => { - try { - setIsLoading(true); - - const domain = getDomainName(serviceId); - const service = services.find((ev) => ev.id === serviceId); - - // generate ndk for nsecbunker - const localSigner = NDKPrivateKeySigner.generate(); - const bunker = new NDK({ - explicitRelayUrls: [ - "wss://relay.nsecbunker.com/", - "wss://nostr.vulpem.com/", - ], - }); - await bunker.connect(2000); - - // generate tmp remote singer for create account - const remoteSigner = new NDKNip46Signer( - bunker, - service.pubkey, - localSigner, - ); - - // handle auth url request - let unlisten: UnlistenFn; - let authWindow: Window; - let account: string = undefined; - - remoteSigner.addListener("authUrl", async (authUrl: string) => { - authWindow = new Window(`auth-${serviceId}`, { - url: authUrl, - title: domain, - titleBarStyle: "overlay", - width: 600, - height: 650, - center: true, - closable: false, - }); - unlisten = await authWindow.onCloseRequested(() => { - if (!account) { - setIsLoading(false); - unlisten(); - - return authWindow.close(); - } - }); - }); - - // create new account - account = await remoteSigner.createAccount( - data.username, - domain, - data.email, - ); - - if (!account) { - unlisten(); - setIsLoading(false); - - authWindow.close(); - - return toast.error("Failed to create new account, try again later"); - } - - unlisten(); - authWindow.close(); - - // add account to storage - await storage.createSetting("nsecbunker", "1"); - const dbAccount = await storage.createAccount({ - pubkey: account, - privkey: localSigner.privateKey, - }); - ark.account = dbAccount; - - // get final signer with newly created account - const finalSigner = new NDKNip46Signer(bunker, account, localSigner); - await finalSigner.blockUntilReady(); - - // update main ndk instance signer - ark.updateNostrSigner({ signer: finalSigner }); - - // remove default nsecbunker profile and contact list - // await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] }); - await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] }); - - setIsLoading(false); - setOnboarding({ open: true, newUser: true }); - - return navigate("/auth/onboarding", { replace: true }); - } catch (e) { - setIsLoading(false); - toast.error(String(e)); + if (method === "self") { + navigate("/auth/create-keys"); + } else { + navigate("/auth/create-address"); } }; return (
-
+

Let's get you set up on Nostr.

+

+ Choose one of methods below to create your account +

+
+
+ + +
- {!services ? ( -
- -
- ) : ( -
-
-
-
- -
-
- - - - - @{getDomainName(serviceId)} - - - - - - - - - - - Choose a Provider - - {services.map((service) => ( - - ))} - - - - - -
- - Use to login to Lume and other Nostr apps. You can choose - provider you trust to manage your account - -
-
-
-
- - -
- - Use for recover your account if you lose your password - -
-
-
- -
-
-
-
-
-
-
-
-
- - Or manage your own keys - -
-
- - Mostly compatible with other Nostr clients - -
-
- -

- If you are using this option, please make sure to store your - keys safely. You{" "} - cannot recover them if - they're lost, and will be{" "} - unable to access your - account. -

-
-
-
- )}
); diff --git a/apps/desktop/src/routes/auth/login-key.tsx b/apps/desktop/src/routes/auth/login-key.tsx index 871d26de..3a65e074 100644 --- a/apps/desktop/src/routes/auth/login-key.tsx +++ b/apps/desktop/src/routes/auth/login-key.tsx @@ -50,7 +50,7 @@ export function LoginWithKey() { return (
-
+

Enter your Private Key

diff --git a/apps/desktop/src/routes/auth/login-nsecbunker.tsx b/apps/desktop/src/routes/auth/login-nsecbunker.tsx index 4679ab76..20480e3e 100644 --- a/apps/desktop/src/routes/auth/login-nsecbunker.tsx +++ b/apps/desktop/src/routes/auth/login-nsecbunker.tsx @@ -66,7 +66,7 @@ export function LoginWithNsecbunker() { return (

-
+

Enter your nsecbunker token diff --git a/apps/desktop/src/routes/auth/login-oauth.tsx b/apps/desktop/src/routes/auth/login-oauth.tsx index 87d88496..228932fa 100644 --- a/apps/desktop/src/routes/auth/login-oauth.tsx +++ b/apps/desktop/src/routes/auth/login-oauth.tsx @@ -128,9 +128,9 @@ export function LoginWithOAuth() { return (
-
+
-

Enter your NIP-05 address

+

Enter your Nostr Address

-
+
-

- Continue your experience on Nostr -

+

Welcome back, anon!

@@ -15,13 +13,13 @@ export function LoginScreen() { to="/auth/login-oauth" 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" > - Login with Address + Login with Nostr Address - Login with nsecbunker + Login with nsecBunker
@@ -31,7 +29,7 @@ export function LoginScreen() {
- Or (Not recommended) + Or continue with
@@ -43,8 +41,10 @@ export function LoginScreen() { Login with Private Key

- Lume will store your Private Key in{" "} - OS Secure Storage + Lume will put your Private Key in{" "} + Secure Storage depended + on your OS Platform. It will be secured by Password or Biometric + ID

diff --git a/apps/desktop/src/routes/auth/welcome.tsx b/apps/desktop/src/routes/auth/welcome.tsx index a9986055..83789082 100644 --- a/apps/desktop/src/routes/auth/welcome.tsx +++ b/apps/desktop/src/routes/auth/welcome.tsx @@ -1,16 +1,6 @@ -import { LoaderIcon } from "@lume/icons"; -import { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; export function WelcomeScreen() { - const navigate = useNavigate(); - const [loading, setLoading] = useState(false); - - const gotoCreateAccount = () => { - setLoading(true); - navigate("/auth/create"); - }; - return (
@@ -29,17 +19,12 @@ export function WelcomeScreen() {

- + Join Nostr +
-

+

Before joining Nostr, you can take time to learn more about Nostr{" "}