diff --git a/apps/desktop/package.json b/apps/desktop/package.json index af8dba4d..ad2263a2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -28,9 +28,10 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", - "@tanstack/react-query": "^5.17.0", + "@tanstack/react-query": "^5.17.1", "@tauri-apps/api": "2.0.0-alpha.13", "@tauri-apps/plugin-autostart": "2.0.0-alpha.5", "@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.5", @@ -45,7 +46,7 @@ "@tauri-apps/plugin-updater": "2.0.0-alpha.5", "@tauri-apps/plugin-upload": "2.0.0-alpha.5", "@vidstack/react": "^1.9.8", - "framer-motion": "^10.17.0", + "framer-motion": "^10.17.6", "minidenticons": "^4.2.0", "nanoid": "^5.0.4", "nostr-fetch": "^0.14.1", @@ -71,7 +72,7 @@ "autoprefixer": "^10.4.16", "cross-env": "^7.0.3", "encoding": "^0.1.13", - "postcss": "^8.4.32", + "postcss": "^8.4.33", "tailwind-merge": "^1.14.0", "tailwindcss": "^3.4.0", "typescript": "^5.3.3", diff --git a/apps/desktop/src/router.tsx b/apps/desktop/src/router.tsx index 4a0662f8..8a10a20a 100644 --- a/apps/desktop/src/router.tsx +++ b/apps/desktop/src/router.tsx @@ -1,7 +1,7 @@ import { useArk, useStorage } from "@lume/ark"; import { LoaderIcon } from "@lume/icons"; import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui"; -import { NDKKind } from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { fetch } from "@tauri-apps/plugin-http"; import { RouterProvider, @@ -179,6 +179,25 @@ export default function Router() { }, { path: "create", + loader: async () => { + const trusted: NDKEvent[] = []; + + const services = await ark.ndk.fetchEvents({ + kinds: [NDKKind.AppHandler], + "#k": ["24133"], + }); + + for (const service of services) { + const nip05 = JSON.parse(service.content).nip05; + const validate = await ark.validateNIP05({ + pubkey: service.pubkey, + nip05, + }); + if (validate) trusted.push(service); + } + + return trusted; + }, async lazy() { const { CreateAccountScreen } = await import( "./routes/auth/create" @@ -186,6 +205,15 @@ export default function Router() { return { Component: CreateAccountScreen }; }, }, + { + path: "create-profile", + async lazy() { + const { CreateProfileScreen } = await import( + "./routes/auth/create-profile" + ); + return { Component: CreateProfileScreen }; + }, + }, { path: "import", async lazy() { diff --git a/apps/desktop/src/routes/auth/create-profile.tsx b/apps/desktop/src/routes/auth/create-profile.tsx new file mode 100644 index 00000000..33dc23ef --- /dev/null +++ b/apps/desktop/src/routes/auth/create-profile.tsx @@ -0,0 +1,5 @@ +export function CreateProfileScreen() { + return ( +
WIP
+ ); +} diff --git a/apps/desktop/src/routes/auth/create.tsx b/apps/desktop/src/routes/auth/create.tsx index df68ba64..f429785d 100644 --- a/apps/desktop/src/routes/auth/create.tsx +++ b/apps/desktop/src/routes/auth/create.tsx @@ -1,28 +1,40 @@ -import { useArk, useStorage } from "@lume/ark"; -import { ArrowLeftIcon, InfoIcon, LoaderIcon } from "@lume/icons"; -import { User } from "@lume/ui"; -import { NDKKind, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; -import { downloadDir } from "@tauri-apps/api/path"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { save } from "@tauri-apps/plugin-dialog"; -import { writeTextFile } from "@tauri-apps/plugin-fs"; -import { motion } from "framer-motion"; -import { minidenticon } from "minidenticons"; -import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools"; +import { useStorage } from "@lume/ark"; +import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons"; +import NDK, { + NDKEvent, + NDKNip46Signer, + NDKPrivateKeySigner, +} from "@nostr-dev-kit/ndk"; +import * as Select from "@radix-ui/react-select"; +import { Window } from "@tauri-apps/api/window"; import { useState } from "react"; import { useForm } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; +import { useLoaderData, useNavigate } from "react-router-dom"; import { toast } from "sonner"; -import { AvatarUploader } from "./components/avatarUploader"; + +const Item = ({ event }: { event: NDKEvent }) => { + const domain = JSON.parse(event.content).nip05.replace("_@", ""); + + return ( + + @{domain} + + + + + ); +}; export function CreateAccountScreen() { - const [picture, setPicture] = useState(""); - const [downloaded, setDownloaded] = useState(false); - const [loading, setLoading] = useState(false); - const [keys, setKeys] = useState(null); + const storage = useStorage(); + const services = useLoaderData() as NDKEvent[]; + const navigate = useNavigate(); + + const [serviceId, setServiceId] = useState(services[0].id); + const [loading, setIsLoading] = useState(false); const { register, @@ -30,282 +42,177 @@ export function CreateAccountScreen() { formState: { isDirty, isValid }, } = useForm(); - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent( - minidenticon("lume new account", 90, 50), - )}`; - - const onSubmit = async (data: { name: string; about: string }) => { - try { - setLoading(true); - - const profile = { - ...data, - name: data.name, - display_name: data.name, - bio: data.about, - picture: picture, - avatar: picture, - }; - - const userPrivkey = generatePrivateKey(); - const userPubkey = getPublicKey(userPrivkey); - const userNpub = nip19.npubEncode(userPubkey); - const userNsec = nip19.nsecEncode(userPrivkey); - - const signer = new NDKPrivateKeySigner(userPrivkey); - ark.updateNostrSigner({ signer }); - - const publish = await ark.createEvent({ - content: JSON.stringify(profile), - kind: NDKKind.Metadata, - tags: [], - }); - - if (publish) { - await storage.createAccount({ - id: userNpub, - pubkey: userPubkey, - privkey: userPrivkey, - }); - - setKeys({ npub: userNpub, nsec: userNsec }); - setLoading(false); - } else { - toast.error("Cannot publish user profile, please try again later."); - setLoading(false); - } - } catch (e) { - return toast.error(e); - } + const getDomainName = (id: string) => { + const event = services.find((ev) => ev.id === id); + return JSON.parse(event.content).nip05.replace("_@", "") as string; }; - const copyNsec = async () => { - await writeText(keys.nsec); - }; + const onSubmit = async (data: { username: string; email: string }) => { + setIsLoading(true); - const download = async () => { - try { - const downloadPath = await downloadDir(); - const fileName = `nostr_keys_${new Date().toISOString()}.txt`; - const filePath = await save({ - defaultPath: `${downloadPath}/${fileName}`, + const domain = getDomainName(serviceId); + const service = services.find((ev) => ev.id === serviceId); + + const localSigner = NDKPrivateKeySigner.generate(); + const localUser = await localSigner.user(); + + const bunker = new NDK({ + explicitRelayUrls: [ + "wss://relay.nsecbunker.com/", + "wss://nostr.vulpem.com/", + ], + }); + + await bunker.connect(2000); + + const remoteSigner = new NDKNip46Signer( + bunker, + service.pubkey, + localSigner, + ); + + remoteSigner.addListener("authUrl", (authUrl: string) => { + const authWindow = new Window("auth", { + url: authUrl, + title: domain, + titleBarStyle: "overlay", + width: 415, + height: 600, + center: true }); + authWindow.listen( + "tauri://close-requested", + async () => await authWindow.close(), + ); + }); - if (filePath) { - await writeTextFile( - filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`, - ); + const account = await remoteSigner.createAccount( + data.username, + domain, + data.email, + ); - setDownloaded(true); - } // else { user cancel action } - } catch (e) { - return toast.error(e); + if (!account) { + setIsLoading(false); + return toast.error("Failed to create new account, try again later"); } + + await storage.createAccount({ + id: localUser.npub, + pubkey: localUser.pubkey, + privkey: localSigner.privateKey, + }); + + setIsLoading(false); + return navigate("/auth/create-profile"); }; return ( -
-
- {!keys ? ( - - ) : null} -
-
-

- Let's set up your account. -

+
+
- {!keys ? ( -
-
+

+ Let's get you set up on Nostr. +

+

+ With an account on Nostr, you'll be able to use with any client that + you want. +

+
+
+ +
+
+ +
+ + + + @{getDomainName(serviceId)} + + + + + + + + + + Public services + + {services.map((service) => ( + + ))} + + + + + +
+
+
+ -
-
- Avatar -
- {picture.length > 0 ? ( - user's avatar - ) : ( - user's avatar - )} - -
-
-
- - -
-
- -