diff --git a/Cargo.lock b/Cargo.lock index baaaf2d..9b14fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9217,6 +9217,7 @@ dependencies = [ "indexset", "itertools 0.13.0", "log", + "nostr-connect", "nostr-sdk", "oneshot", "person", diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index beaf610..9b5dfcf 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -282,10 +282,11 @@ impl NostrRegistry { })); } - fn get_master_key(&mut self, cx: &mut Context) -> Task { + /// Get the master key that used for Nostr Connect + pub fn get_master_key(&self, cx: &App) -> Task { let task = cx.read_credentials(MASTER_KEYRING); - cx.spawn(async move |_, cx| { + cx.spawn(async move |cx| { let (keys, new_key) = match task.await { Ok(Some((_user, secret))) => match SecretKey::from_slice(&secret) { Ok(secret_key) => (Keys::new(secret_key), false), diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 8bc47a9..31c01da 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -18,6 +18,8 @@ person = { path = "../person" } gpui.workspace = true nostr-sdk.workspace = true +nostr-connect.workspace = true + anyhow.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/workspace/src/dialogs/import.rs b/crates/workspace/src/dialogs/import.rs index 775a8aa..c0ba7c4 100644 --- a/crates/workspace/src/dialogs/import.rs +++ b/crates/workspace/src/dialogs/import.rs @@ -6,8 +6,8 @@ use gpui::{ AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, }; -use nostr_sdk::prelude::*; -use state::NostrRegistry; +use nostr_connect::prelude::*; +use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{Input, InputEvent, InputState}; @@ -66,6 +66,18 @@ impl ImportIdentity { return; } + if value.starts_with("bunker://") { + match NostrConnectUri::parse(value) { + Ok(uri) => { + self.bunker(uri, window, cx); + } + Err(e) => { + self.set_error(e.to_string(), cx); + } + } + return; + } + if let Ok(secret) = SecretKey::parse(&value) { let keys = Keys::new(secret); let nostr = NostrRegistry::global(cx); @@ -126,6 +138,33 @@ impl ImportIdentity { })); } + fn bunker(&mut self, uri: NostrConnectUri, window: &mut Window, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let master_keys = nostr.read(cx).get_master_key(cx); + let password = uri.to_string(); + let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes()); + + cx.spawn_in(window, async move |_this, cx| { + let keys = master_keys.await; + let timeout = Duration::from_secs(30); + + // Construct the nostr connect signer + let mut signer = NostrConnect::new(uri, keys, timeout, None)?; + + // Handle auth url with the default browser + signer.auth_url_handler(CoopAuthUrlHandler); + + nostr.update(cx, |this, cx| { + this.set_signer(signer, cx); + cx.background_spawn(async move { save.await.ok() }).detach(); + cx.notify(); + }); + + Ok::<(), anyhow::Error>(()) + }) + .detach(); + } + fn set_error(&mut self, message: S, cx: &mut Context) where S: Into, @@ -154,34 +193,40 @@ impl ImportIdentity { impl Render for ImportIdentity { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { - const MSG: &str = "Coop isn't stored your identity secret in local device. Everything will be reset on the next login."; + const MSG: &str = "Coop won't stored your identity key on the local device. Use Nostr Connect for persistent login."; + + let require_password = self.key_input.read(cx).value().starts_with("ncryptsec1"); + let key_warning = self.key_input.read(cx).value().starts_with("nsec1") || require_password; v_flex() .size_full() .gap_2() - .text_sm() .child( v_flex() .gap_1() .text_sm() .text_color(cx.theme().text_muted) - .child("nsec or ncryptsec://") + .child("Continue with existing key or bunker connection") .child(Input::new(&self.key_input)), ) - .when( - self.key_input.read(cx).value().starts_with("ncryptsec1"), - |this| { - this.child( - v_flex() - .gap_1() - .text_sm() - .text_color(cx.theme().text_muted) - .child("Password:") - .child(Input::new(&self.pass_input)), - ) - }, - ) - .child(div().text_xs().text_color(cx.theme().text_muted).child(MSG)) + .when(require_password, |this| { + this.child( + v_flex() + .gap_1() + .text_sm() + .text_color(cx.theme().text_muted) + .child("Decrypt Password:") + .child(Input::new(&self.pass_input)), + ) + }) + .when(key_warning, |this| { + this.child( + div() + .text_xs() + .text_color(cx.theme().warning_active) + .child(MSG), + ) + }) .child( Button::new("login") .label("Continue")