From 005cbeab728b4e0d1de29abb4993877312967645 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:59:36 +0700 Subject: [PATCH] feat: add auth screens --- src-tauri/src/commands/account.rs | 162 ++++++++++++++---- src-tauri/src/main.rs | 13 +- src/commands.ts | 32 +++- src/components/user/provider.tsx | 2 +- src/routes.gen.ts | 88 ++++++++-- ...ts.$id.tsx => $account.chats.$id.lazy.tsx} | 4 +- ...ount.chats.tsx => $account.chats.lazy.tsx} | 4 +- src/routes/create-account.lazy.tsx | 94 ++++++++++ src/routes/import-key.lazy.tsx | 102 +++++++++++ src/routes/index.tsx | 4 +- src/routes/new.lazy.tsx | 42 ++++- src/routes/nostr-connect.lazy.tsx | 93 ++++++++++ 12 files changed, 571 insertions(+), 69 deletions(-) rename src/routes/{$account.chats.$id.tsx => $account.chats.$id.lazy.tsx} (98%) rename src/routes/{$account.chats.tsx => $account.chats.lazy.tsx} (97%) create mode 100644 src/routes/create-account.lazy.tsx create mode 100644 src/routes/import-key.lazy.tsx create mode 100644 src/routes/nostr-connect.lazy.tsx diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 33fa51e..2463390 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,9 +1,8 @@ -use itertools::Itertools; use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; use serde::Serialize; -use std::collections::HashSet; +use std::{collections::HashSet, time::Duration}; use tauri::{Emitter, Manager, State}; use crate::Nostr; @@ -28,7 +27,7 @@ pub fn get_accounts() -> Vec { #[tauri::command] #[specta::specta] -pub async fn get_profile(id: String, state: State<'_, Nostr>) -> Result { +pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result { let client = &state.client; let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; let filter = Filter::new().author(public_key).kind(Kind::Metadata).limit(1); @@ -45,10 +44,108 @@ pub async fn get_profile(id: String, state: State<'_, Nostr>) -> Result, +) -> Result<(), String> { + let client = &state.client; + let keys = Keys::generate(); + let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?; + let nsec = keys.secret_key().unwrap().to_bech32().map_err(|e| e.to_string())?; + + // Save account + let keyring = Entry::new(&npub, "nostr_secret").unwrap(); + let _ = keyring.set_password(&nsec); + + let signer = NostrSigner::Keys(keys); + + // Update signer + client.set_signer(Some(signer)).await; + + // Update metadata + let url = Url::parse(&picture).map_err(|e| e.to_string())?; + let metadata = Metadata::new().display_name(name).picture(url); + + match client.set_metadata(&metadata).await { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn import_key( + nsec: &str, + password: &str, + state: State<'_, Nostr>, +) -> Result { + let secret_key = if nsec.starts_with("ncryptsec") { + let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap(); + encrypted_key.to_secret_key(password).map_err(|err| err.to_string()) + } else { + SecretKey::from_bech32(nsec).map_err(|err| err.to_string()) + }; + + match secret_key { + Ok(val) => { + let nostr_keys = Keys::new(val); + let npub = nostr_keys.public_key().to_bech32().unwrap(); + let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap(); + + let keyring = Entry::new(&npub, "nostr_secret").unwrap(); + let _ = keyring.set_password(&nsec); + + let signer = NostrSigner::Keys(nostr_keys); + let client = &state.client; + + // Update client's signer + client.set_signer(Some(signer)).await; + + Ok(npub) + } + Err(msg) => Err(msg), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result { + let client = &state.client; + + match NostrConnectURI::parse(uri) { + Ok(bunker_uri) => { + let app_keys = Keys::generate(); + let app_secret = app_keys.secret_key().unwrap().to_string(); + + // Get remote user + let remote_user = bunker_uri.signer_public_key().unwrap(); + let remote_npub = remote_user.to_bech32().unwrap(); + + match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await { + Ok(signer) => { + let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap(); + let _ = keyring.set_password(&app_secret); + + // Update signer + let _ = client.set_signer(Some(signer.into())).await; + + Ok(remote_npub) + } + Err(err) => Err(err.to_string()), + } + } + Err(err) => Err(err.to_string()), + } +} + #[tauri::command] #[specta::specta] pub async fn login( id: String, + bunker: Option, state: State<'_, Nostr>, handle: tauri::AppHandle, ) -> Result { @@ -61,12 +158,33 @@ pub async fn login( Err(_) => return Err("Cancelled".into()), }; - let keys = Keys::parse(password).expect("Secret Key is modified, please check again."); - let signer = NostrSigner::Keys(keys); + match bunker { + Some(uri) => { + let app_keys = + Keys::parse(password).expect("Secret Key is modified, please check again."); - // Set signer - client.set_signer(Some(signer)).await; + match NostrConnectURI::parse(uri) { + Ok(bunker_uri) => { + match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None) + .await + { + Ok(signer) => client.set_signer(Some(signer.into())).await, + Err(err) => return Err(err.to_string()), + } + } + Err(err) => return Err(err.to_string()), + } + } + None => { + let keys = Keys::parse(password).expect("Secret Key is modified, please check again."); + let signer = NostrSigner::Keys(keys); + // Update signer + client.set_signer(Some(signer)).await; + } + } + + let hex = public_key.to_hex(); let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1); if let Ok(events) = client.get_events_of(vec![inbox], None).await { @@ -92,33 +210,11 @@ pub async fn login( let state = window.state::(); let client = &state.client; - let old = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).until(Timestamp::now()); + let old = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let new = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0); - if let Ok(report) = client.reconcile(old, NegentropyOptions::default()).await { - let receives = report.received.clone(); - let ids = receives.into_iter().collect::>(); - - if let Ok(events) = - client.database().query(vec![Filter::new().ids(ids)], Order::Desc).await - { - let pubkeys = events - .into_iter() - .unique_by(|ev| ev.pubkey) - .map(|ev| ev.pubkey) - .collect::>(); - - if client - .reconcile( - Filter::new().kind(Kind::GiftWrap).pubkeys(pubkeys), - NegentropyOptions::default(), - ) - .await - .is_ok() - { - println!("Sync done.") - } - } + if client.reconcile(old, NegentropyOptions::default()).await.is_ok() { + println!("Sync done.") }; if client.subscribe(vec![new], None).await.is_ok() { @@ -169,7 +265,5 @@ pub async fn login( .await }); - let hex = public_key.to_hex(); - Ok(hex) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0c7b139..fc8e8af 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,13 +8,7 @@ use std::{fs, sync::Mutex, time::Duration}; use tauri::Manager; use tauri_plugin_decorum::WebviewWindowExt; -use commands::{ - account::{get_accounts, get_profile, login}, - chat::{ - drop_inbox, get_chat_messages, get_chats, get_inboxes, send_message, subscribe_to, - unsubscribe, - }, -}; +use commands::{account::*, chat::*}; mod commands; mod common; @@ -30,8 +24,11 @@ fn main() { let invoke_handler = { let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![ login, + create_account, + import_key, + connect_account, get_accounts, - get_profile, + get_metadata, get_inboxes, get_chats, get_chat_messages, diff --git a/src/commands.ts b/src/commands.ts index 0987ce7..2bf36e1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -4,9 +4,33 @@ /** user-defined commands **/ export const commands = { -async login(id: string) : Promise> { +async login(id: string, bunker: string | null) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("login", { id }) }; + return { status: "ok", data: await TAURI_INVOKE("login", { id, bunker }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async createAccount(name: string, picture: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("create_account", { name, picture }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async importKey(nsec: string, password: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("import_key", { nsec, password }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async connectAccount(uri: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("connect_account", { uri }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -15,9 +39,9 @@ try { async getAccounts() : Promise { return await TAURI_INVOKE("get_accounts"); }, -async getProfile(id: string) : Promise> { +async getMetadata(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) }; + return { status: "ok", data: await TAURI_INVOKE("get_metadata", { id }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; diff --git a/src/components/user/provider.tsx b/src/components/user/provider.tsx index 995c2c6..06c391c 100644 --- a/src/components/user/provider.tsx +++ b/src/components/user/provider.tsx @@ -42,7 +42,7 @@ export function UserProvider({ .replace("nostr:", "") .replace(/[^\w\s]/gi, ""); - const query: string = await invoke("get_profile", { + const query: string = await invoke("get_metadata", { id: normalizePubkey, }); diff --git a/src/routes.gen.ts b/src/routes.gen.ts index 726ce09..2d95daf 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -14,34 +14,58 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './routes/__root' import { Route as IndexImport } from './routes/index' -import { Route as AccountChatsImport } from './routes/$account.chats' -import { Route as AccountChatsIdImport } from './routes/$account.chats.$id' // Create Virtual Routes +const NostrConnectLazyImport = createFileRoute('/nostr-connect')() const NewLazyImport = createFileRoute('/new')() +const ImportKeyLazyImport = createFileRoute('/import-key')() +const CreateAccountLazyImport = createFileRoute('/create-account')() +const AccountChatsLazyImport = createFileRoute('/$account/chats')() +const AccountChatsIdLazyImport = createFileRoute('/$account/chats/$id')() // Create/Update Routes +const NostrConnectLazyRoute = NostrConnectLazyImport.update({ + path: '/nostr-connect', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/nostr-connect.lazy').then((d) => d.Route)) + const NewLazyRoute = NewLazyImport.update({ path: '/new', getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) +const ImportKeyLazyRoute = ImportKeyLazyImport.update({ + path: '/import-key', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/import-key.lazy').then((d) => d.Route)) + +const CreateAccountLazyRoute = CreateAccountLazyImport.update({ + path: '/create-account', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/create-account.lazy').then((d) => d.Route), +) + const IndexRoute = IndexImport.update({ path: '/', getParentRoute: () => rootRoute, } as any) -const AccountChatsRoute = AccountChatsImport.update({ +const AccountChatsLazyRoute = AccountChatsLazyImport.update({ path: '/$account/chats', getParentRoute: () => rootRoute, -} as any) +} as any).lazy(() => + import('./routes/$account.chats.lazy').then((d) => d.Route), +) -const AccountChatsIdRoute = AccountChatsIdImport.update({ +const AccountChatsIdLazyRoute = AccountChatsIdLazyImport.update({ path: '/$id', - getParentRoute: () => AccountChatsRoute, -} as any) + getParentRoute: () => AccountChatsLazyRoute, +} as any).lazy(() => + import('./routes/$account.chats.$id.lazy').then((d) => d.Route), +) // Populate the FileRoutesByPath interface @@ -54,6 +78,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/create-account': { + id: '/create-account' + path: '/create-account' + fullPath: '/create-account' + preLoaderRoute: typeof CreateAccountLazyImport + parentRoute: typeof rootRoute + } + '/import-key': { + id: '/import-key' + path: '/import-key' + fullPath: '/import-key' + preLoaderRoute: typeof ImportKeyLazyImport + parentRoute: typeof rootRoute + } '/new': { id: '/new' path: '/new' @@ -61,19 +99,26 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof NewLazyImport parentRoute: typeof rootRoute } + '/nostr-connect': { + id: '/nostr-connect' + path: '/nostr-connect' + fullPath: '/nostr-connect' + preLoaderRoute: typeof NostrConnectLazyImport + parentRoute: typeof rootRoute + } '/$account/chats': { id: '/$account/chats' path: '/$account/chats' fullPath: '/$account/chats' - preLoaderRoute: typeof AccountChatsImport + preLoaderRoute: typeof AccountChatsLazyImport parentRoute: typeof rootRoute } '/$account/chats/$id': { id: '/$account/chats/$id' path: '/$id' fullPath: '/$account/chats/$id' - preLoaderRoute: typeof AccountChatsIdImport - parentRoute: typeof AccountChatsImport + preLoaderRoute: typeof AccountChatsIdLazyImport + parentRoute: typeof AccountChatsLazyImport } } } @@ -82,8 +127,13 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren({ IndexRoute, + CreateAccountLazyRoute, + ImportKeyLazyRoute, NewLazyRoute, - AccountChatsRoute: AccountChatsRoute.addChildren({ AccountChatsIdRoute }), + NostrConnectLazyRoute, + AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({ + AccountChatsIdLazyRoute, + }), }) /* prettier-ignore-end */ @@ -95,24 +145,36 @@ export const routeTree = rootRoute.addChildren({ "filePath": "__root.tsx", "children": [ "/", + "/create-account", + "/import-key", "/new", + "/nostr-connect", "/$account/chats" ] }, "/": { "filePath": "index.tsx" }, + "/create-account": { + "filePath": "create-account.lazy.tsx" + }, + "/import-key": { + "filePath": "import-key.lazy.tsx" + }, "/new": { "filePath": "new.lazy.tsx" }, + "/nostr-connect": { + "filePath": "nostr-connect.lazy.tsx" + }, "/$account/chats": { - "filePath": "$account.chats.tsx", + "filePath": "$account.chats.lazy.tsx", "children": [ "/$account/chats/$id" ] }, "/$account/chats/$id": { - "filePath": "$account.chats.$id.tsx", + "filePath": "$account.chats.$id.lazy.tsx", "parent": "/$account/chats" } } diff --git a/src/routes/$account.chats.$id.tsx b/src/routes/$account.chats.$id.lazy.tsx similarity index 98% rename from src/routes/$account.chats.$id.tsx rename to src/routes/$account.chats.$id.lazy.tsx index c2942b4..25cfe67 100644 --- a/src/routes/$account.chats.$id.tsx +++ b/src/routes/$account.chats.$id.lazy.tsx @@ -4,7 +4,7 @@ import { Spinner } from "@/components/spinner"; import { ArrowUp, CloudArrowUp, Paperclip } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { createFileRoute } from "@tanstack/react-router"; +import { createLazyFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { message } from "@tauri-apps/plugin-dialog"; import type { NostrEvent } from "nostr-tools"; @@ -17,7 +17,7 @@ type Payload = { sender: string; }; -export const Route = createFileRoute("/$account/chats/$id")({ +export const Route = createLazyFileRoute("/$account/chats/$id")({ component: Screen, }); diff --git a/src/routes/$account.chats.tsx b/src/routes/$account.chats.lazy.tsx similarity index 97% rename from src/routes/$account.chats.tsx rename to src/routes/$account.chats.lazy.tsx index 1b5810e..1f158bb 100644 --- a/src/routes/$account.chats.tsx +++ b/src/routes/$account.chats.lazy.tsx @@ -4,7 +4,7 @@ import { User } from "@/components/user"; import { Plus, UsersThree } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Link, Outlet, createFileRoute } from "@tanstack/react-router"; +import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import type { NostrEvent } from "nostr-tools"; import { useEffect } from "react"; @@ -14,7 +14,7 @@ type Payload = { sender: string; }; -export const Route = createFileRoute("/$account/chats")({ +export const Route = createLazyFileRoute("/$account/chats")({ component: Screen, }); diff --git a/src/routes/create-account.lazy.tsx b/src/routes/create-account.lazy.tsx new file mode 100644 index 0000000..5b045b0 --- /dev/null +++ b/src/routes/create-account.lazy.tsx @@ -0,0 +1,94 @@ +import { commands } from "@/commands"; +import { Frame } from "@/components/frame"; +import { Spinner } from "@/components/spinner"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { message } from "@tauri-apps/plugin-dialog"; +import { useState, useTransition } from "react"; + +export const Route = createLazyFileRoute("/create-account")({ + component: Screen, +}); + +function Screen() { + const navigate = Route.useNavigate(); + + const [picture, setPicture] = useState(""); + const [name, setName] = useState(""); + const [isPending, startTransition] = useTransition(); + + const submit = async () => { + startTransition(async () => { + const res = await commands.createAccount(name, picture); + + if (res.status === "ok") { + navigate({ to: "/", replace: true }); + } else { + await message(res.error, { + title: "New Identity", + kind: "error", + }); + return; + } + }); + }; + + return ( +
+
+
+

+ Import Private Key +

+
+
+ +
+ + setPicture(e.target.value)} + className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none" + /> +
+
+ + setName(e.target.value)} + className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none" + /> +
+ +
+ +
+
+
+
+ ); +} diff --git a/src/routes/import-key.lazy.tsx b/src/routes/import-key.lazy.tsx new file mode 100644 index 0000000..ae1bf12 --- /dev/null +++ b/src/routes/import-key.lazy.tsx @@ -0,0 +1,102 @@ +import { commands } from "@/commands"; +import { Frame } from "@/components/frame"; +import { Spinner } from "@/components/spinner"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { message } from "@tauri-apps/plugin-dialog"; +import { useState, useTransition } from "react"; + +export const Route = createLazyFileRoute("/import-key")({ + component: Screen, +}); + +function Screen() { + const navigate = Route.useNavigate(); + + const [key, setKey] = useState(""); + const [password, setPassword] = useState(""); + const [isPending, startTransition] = useTransition(); + + const submit = async () => { + startTransition(async () => { + if (!key.startsWith("nsec1")) { + await message( + "You need to enter a valid private key starts with nsec or ncryptsec", + { title: "Import Key", kind: "info" }, + ); + return; + } + + const res = await commands.importKey(key, password); + + if (res.status === "ok") { + navigate({ to: "/", replace: true }); + } else { + await message(res.error, { + title: "Import Private Ket", + kind: "error", + }); + return; + } + }); + }; + + return ( +
+
+
+

+ Import Private Key +

+
+
+ +
+ + setKey(e.target.value)} + className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none" + /> +
+
+ + setPassword(e.target.value)} + className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none" + /> +
+ +
+ +
+
+
+
+ ); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index b0d4830..b32ddbe 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -43,7 +43,9 @@ function Screen() { const loginWith = async (npub: string) => { setValue(npub); startTransition(async () => { - const res = await commands.login(npub); + const bunker: string = localStorage.getItem(`${npub}_bunker`); + const verifyBunker = bunker?.length && bunker?.startsWith("bunker://"); + const res = await commands.login(npub, verifyBunker ? bunker : null); if (res.status === "ok") { navigate({ diff --git a/src/routes/new.lazy.tsx b/src/routes/new.lazy.tsx index 4344d2f..fac252b 100644 --- a/src/routes/new.lazy.tsx +++ b/src/routes/new.lazy.tsx @@ -1,5 +1,39 @@ -import { createLazyFileRoute } from '@tanstack/react-router' +import { createLazyFileRoute, Link } from "@tanstack/react-router"; -export const Route = createLazyFileRoute('/new')({ - component: () =>
Hello /new!
-}) \ No newline at end of file +export const Route = createLazyFileRoute("/new")({ + component: Screen, +}); + +function Screen() { + return ( +
+
+
+

+ Direct Message client for Nostr. +

+
+
+ + Create a new identity + + + Login with Nostr Connect + + + Login with Private Key (not recommended) + +
+
+
+ ); +} diff --git a/src/routes/nostr-connect.lazy.tsx b/src/routes/nostr-connect.lazy.tsx new file mode 100644 index 0000000..878516f --- /dev/null +++ b/src/routes/nostr-connect.lazy.tsx @@ -0,0 +1,93 @@ +import { commands } from "@/commands"; +import { Frame } from "@/components/frame"; +import { Spinner } from "@/components/spinner"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { message } from "@tauri-apps/plugin-dialog"; +import { useState, useTransition } from "react"; + +export const Route = createLazyFileRoute("/nostr-connect")({ + component: Screen, +}); + +function Screen() { + const navigate = Route.useNavigate(); + + const [uri, setUri] = useState(""); + const [isPending, startTransition] = useTransition(); + + const submit = async () => { + startTransition(async () => { + if (!uri.startsWith("bunker://")) { + await message( + "You need to enter a valid Connect URI starts with bunker://", + { title: "Nostr Connect", kind: "info" }, + ); + return; + } + + const res = await commands.connectAccount(uri); + + if (res.status === "ok") { + const npub = res.data; + const parsed = new URL(uri); + parsed.searchParams.delete("secret"); + + // save connection string + localStorage.setItem(`${npub}_bunker`, parsed.toString()); + + navigate({ to: "/", replace: true }); + } else { + await message(res.error, { title: "Nostr Connect", kind: "error" }); + return; + } + }); + }; + + return ( +
+
+
+

+ Nostr Connect. +

+
+
+ + + setUri(e.target.value)} + className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none" + /> + +
+ + {isPending ? ( +

+ Waiting confirmation... +

+ ) : null} +
+
+
+
+ ); +}