add NIP-07 browser extension signer support for wasm

This commit is contained in:
2026-10-24 20:29:09 +07:00
parent f829a2774d
commit f9b40d9d49
10 changed files with 251 additions and 51 deletions
Generated
+27 -11
View File
@@ -1223,6 +1223,7 @@ dependencies = [
name = "common" name = "common"
version = "2.0.0-alpha2" version = "2.0.0-alpha2"
dependencies = [ dependencies = [
"anyhow",
"chrono", "chrono",
"dirs 5.0.1", "dirs 5.0.1",
"futures", "futures",
@@ -1232,6 +1233,7 @@ dependencies = [
"nostr", "nostr",
"nostr-sdk", "nostr-sdk",
"regex", "regex",
"web-sys",
] ]
[[package]] [[package]]
@@ -1824,7 +1826,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.3", "redox_users 0.5.3",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -2052,7 +2054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4900,6 +4902,19 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "nostr-browser-signer"
version = "0.45.1"
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
dependencies = [
"js-sys",
"nostr",
"opaquerr 0.2.0",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.45.2" version = "0.45.2"
@@ -5047,7 +5062,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6134,7 +6149,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6707,7 +6722,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6775,7 +6790,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -7381,7 +7396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -7434,6 +7449,7 @@ dependencies = [
"mime_guess", "mime_guess",
"nostr", "nostr",
"nostr-blossom", "nostr-blossom",
"nostr-browser-signer",
"nostr-connect", "nostr-connect",
"nostr-gossip-memory", "nostr-gossip-memory",
"nostr-lmdb", "nostr-lmdb",
@@ -7702,7 +7718,7 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -8233,7 +8249,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -9031,7 +9047,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@@ -9656,7 +9672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
+1
View File
@@ -32,6 +32,7 @@ nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" } nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-browser-signer = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44", "nip98" ] } nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44", "nip98" ] }
+4
View File
@@ -16,3 +16,7 @@ futures.workspace = true
dirs = "5.0" dirs = "5.0"
regex = "1.10" regex = "1.10"
anyhow.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["Window", "Storage"] }
+10 -7
View File
@@ -1,13 +1,16 @@
pub use debounced_delay::*;
pub use display::*;
pub use event::*;
pub use media_extractor::*;
pub use parser::*;
pub use paths::*;
mod debounced_delay; mod debounced_delay;
mod display; mod display;
mod event; mod event;
mod media_extractor; mod media_extractor;
mod parser; mod parser;
mod paths; mod paths;
#[cfg(target_arch = "wasm32")]
pub mod web_store;
pub use debounced_delay::*;
pub use display::*;
pub use event::*;
pub use media_extractor::*;
pub use parser::*;
pub use paths::*;
+32
View File
@@ -0,0 +1,32 @@
use anyhow::{Error, anyhow};
const LOCAL_STORAGE: &str = "localStorage";
fn storage() -> Result<web_sys::Storage, Error> {
web_sys::window()
.ok_or_else(|| anyhow!("no browser window"))?
.local_storage()
.map_err(|e| anyhow!("{LOCAL_STORAGE} unavailable: {e:?}"))?
.ok_or_else(|| anyhow!("{LOCAL_STORAGE} unavailable"))
}
/// Read a value previously written with [`set`]
pub fn get(key: &str) -> Result<Option<String>, Error> {
storage()?
.get_item(key)
.map_err(|e| anyhow!("{LOCAL_STORAGE} read failed: {e:?}"))
}
/// Write a value, replacing any previous one
pub fn set(key: &str, value: &str) -> Result<(), Error> {
storage()?
.set_item(key, value)
.map_err(|e| anyhow!("{LOCAL_STORAGE} write failed: {e:?}"))
}
/// Remove a value if it exists
pub fn remove(key: &str) -> Result<(), Error> {
storage()?
.remove_item(key)
.map_err(|e| anyhow!("{LOCAL_STORAGE} delete failed: {e:?}"))
}
+32 -18
View File
@@ -1,12 +1,17 @@
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{Error, anyhow}; use anyhow::{Error, anyhow};
#[cfg(not(target_arch = "wasm32"))]
use common::config_dir; use common::config_dir;
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task, px}; use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task, px};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
/// Browser storage key for the settings JSON
#[cfg(target_arch = "wasm32")]
const SETTINGS_KEY: &str = "coop.settings";
/// The user's appearance preference, persisted with the rest of the settings. /// The user's appearance preference, persisted with the rest of the settings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AppearanceMode { pub enum AppearanceMode {
@@ -63,21 +68,21 @@ pub fn init(cx: &mut App) {
} }
/// Read the persisted appearance mode synchronously. /// Read the persisted appearance mode synchronously.
///
/// The async load fills the global settings later; the native theme must be
/// applied before the first window opens, so boot reads the mode straight
/// from the settings file instead.
fn persisted_settings() -> Option<Settings> { fn persisted_settings() -> Option<Settings> {
#[cfg(not(target_arch = "wasm32"))] let content = persisted_content();
{ content.and_then(|content| serde_json::from_str(&content).ok())
let path = config_dir().join(".settings"); }
if let Ok(content) = std::fs::read_to_string(path)
&& let Ok(settings) = serde_json::from_str::<Settings>(&content) /// The persisted settings JSON, from the config file or browser storage
{ #[cfg(not(target_arch = "wasm32"))]
return Some(settings); fn persisted_content() -> Option<String> {
} let path = config_dir().join(".settings");
} std::fs::read_to_string(path).ok()
None }
#[cfg(target_arch = "wasm32")]
fn persisted_content() -> Option<String> {
common::web_store::get(SETTINGS_KEY).ok().flatten()
} }
pub fn persisted_appearance_mode() -> AppearanceMode { pub fn persisted_appearance_mode() -> AppearanceMode {
@@ -87,10 +92,6 @@ pub fn persisted_appearance_mode() -> AppearanceMode {
} }
/// Read the persisted theme tuning synchronously. /// Read the persisted theme tuning synchronously.
///
/// Like [`persisted_appearance_mode`], the native theme is applied before the
/// async settings load completes, so boot reads the tuning straight from the
/// settings file instead.
pub fn persisted_theme() -> ThemeSettings { pub fn persisted_theme() -> ThemeSettings {
persisted_settings() persisted_settings()
.map(|settings| settings.theme) .map(|settings| settings.theme)
@@ -357,6 +358,14 @@ impl AppSettings {
return Ok(serde_json::from_str(&content)?); return Ok(serde_json::from_str(&content)?);
} }
} }
#[cfg(target_arch = "wasm32")]
{
if let Some(content) = common::web_store::get(SETTINGS_KEY)? {
return Ok(serde_json::from_str(&content)?);
}
}
Err(anyhow!("Not found")) Err(anyhow!("Not found"))
}); });
@@ -389,6 +398,11 @@ impl AppSettings {
smol::fs::write(&path, content).await.ok(); smol::fs::write(&path, content).await.ok();
}) })
.detach(); .detach();
#[cfg(target_arch = "wasm32")]
if let Err(e) = common::web_store::set(SETTINGS_KEY, &content) {
log::warn!("failed to persist settings: {e}");
}
} }
} }
+1
View File
@@ -28,6 +28,7 @@ sha2.workspace = true
data-encoding.workspace = true data-encoding.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
nostr-browser-signer.workspace = true
nostr-memory.workspace = true nostr-memory.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+51
View File
@@ -0,0 +1,51 @@
use anyhow::Result;
#[cfg(target_arch = "wasm32")]
use common::web_store;
use gpui::{App, Task};
use crate::USER_KEYRING;
/// Read the stored credential as `(username, secret)`
#[allow(clippy::type_complexity)]
pub(crate) fn read(cx: &App) -> Task<Result<Option<(String, Vec<u8>)>>> {
#[cfg(not(target_arch = "wasm32"))]
return cx.read_credentials(USER_KEYRING);
#[cfg(target_arch = "wasm32")]
{
let _ = cx;
Task::ready(
web_store::get(USER_KEYRING)
.map(|stored| stored.map(|content| (String::new(), content.into_bytes()))),
)
}
}
/// Write the stored credential
pub(crate) fn write(cx: &App, username: &str, secret: &[u8]) -> Task<Result<()>> {
#[cfg(not(target_arch = "wasm32"))]
return cx.write_credentials(USER_KEYRING, username, secret);
#[cfg(target_arch = "wasm32")]
{
let _ = (cx, username);
let content = match std::str::from_utf8(secret) {
Ok(content) => content,
Err(e) => return Task::ready(Err(anyhow::anyhow!("credential is not UTF-8: {e}"))),
};
Task::ready(web_store::set(USER_KEYRING, content))
}
}
/// Delete the stored credential
pub(crate) fn delete(cx: &App) -> Task<Result<()>> {
#[cfg(not(target_arch = "wasm32"))]
return cx.delete_credentials(USER_KEYRING);
#[cfg(target_arch = "wasm32")]
{
let _ = cx;
Task::ready(web_store::remove(USER_KEYRING))
}
}
+61 -13
View File
@@ -9,6 +9,8 @@ use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, TaskExt
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use instant::Duration; use instant::Duration;
#[cfg(target_arch = "wasm32")]
use nostr_browser_signer::BrowserSigner;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_gossip_memory::prelude::*; use nostr_gossip_memory::prelude::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -19,6 +21,7 @@ use nostr_sdk::prelude::*;
mod blossom; mod blossom;
mod constants; mod constants;
mod credentials;
mod file; mod file;
mod nip05; mod nip05;
mod nip4e; mod nip4e;
@@ -138,9 +141,7 @@ impl NostrRegistry {
let _ = entity.update(cx, |this, cx| { let _ = entity.update(cx, |this, cx| {
this.connect_bootstrap_relays(cx); this.connect_bootstrap_relays(cx);
if cfg!(target_arch = "wasm32") { if let Some(secret) = cli_key {
cx.emit(StateEvent::NoSigner);
} else if let Some(secret) = cli_key {
// Use CLI-provided key -- same path as restore_session // Use CLI-provided key -- same path as restore_session
let keys = Keys::new(secret); let keys = Keys::new(secret);
this.set_signer(keys, cx); this.set_signer(keys, cx);
@@ -242,7 +243,7 @@ impl NostrRegistry {
/// Restore the signer from the stored credential /// Restore the signer from the stored credential
fn restore_session(&mut self, cx: &mut Context<Self>) { fn restore_session(&mut self, cx: &mut Context<Self>) {
let user_keyring = cx.read_credentials(USER_KEYRING); let user_keyring = credentials::read(cx);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let result = async { let result = async {
@@ -279,6 +280,11 @@ impl NostrRegistry {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.connect_proxy(cx); this.connect_proxy(cx);
})?; })?;
} else if content == "extension" {
#[cfg(target_arch = "wasm32")]
this.update(cx, |this, cx| {
this.connect_extension(cx);
})?;
} else { } else {
this.update(cx, |_, cx| { this.update(cx, |_, cx| {
cx.emit(StateEvent::NoSigner); cx.emit(StateEvent::NoSigner);
@@ -314,7 +320,7 @@ impl NostrRegistry {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<PublicKey, Error>> { ) -> Task<Result<PublicKey, Error>> {
let password = password.to_owned(); let password = password.to_owned();
let user_keyring = cx.read_credentials(USER_KEYRING); let user_keyring = credentials::read(cx);
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let content = user_keyring let content = user_keyring
@@ -373,9 +379,8 @@ impl NostrRegistry {
let (keys, ncryptsec) = job.await?; let (keys, ncryptsec) = job.await?;
let public_key = keys.public_key(); let public_key = keys.public_key();
let write = cx.update(|cx| { let write =
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), ncryptsec.as_bytes()) cx.update(|cx| credentials::write(cx, &public_key.to_hex(), ncryptsec.as_bytes()));
});
write.await?; write.await?;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@@ -412,9 +417,8 @@ impl NostrRegistry {
let keys = decrypt_task.await?; let keys = decrypt_task.await?;
let public_key = keys.public_key(); let public_key = keys.public_key();
let write = cx.update(|cx| { let write =
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), stored.as_bytes()) cx.update(|cx| credentials::write(cx, &public_key.to_hex(), stored.as_bytes()));
});
write.await?; write.await?;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@@ -440,7 +444,7 @@ impl NostrRegistry {
let keys = Keys::generate(); let keys = Keys::generate();
let credential = with_master_key(&uri_string, &keys); let credential = with_master_key(&uri_string, &keys);
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes()); let write = credentials::write(cx, "bunker", credential.as_bytes());
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let signer = NostrConnect::builder(connect_uri, keys) let signer = NostrConnect::builder(connect_uri, keys)
@@ -464,12 +468,56 @@ impl NostrRegistry {
self.current_user = None; self.current_user = None;
self.passphrase_required = false; self.passphrase_required = false;
cx.delete_credentials(USER_KEYRING).detach_and_log_err(cx); credentials::delete(cx).detach_and_log_err(cx);
cx.emit(StateEvent::SignerChanged); cx.emit(StateEvent::SignerChanged);
cx.emit(StateEvent::NoSigner); cx.emit(StateEvent::NoSigner);
cx.notify(); cx.notify();
} }
/// Connect the browser extension signer (NIP-07), remembering the choice so
/// reloads reconnect automatically
#[cfg(target_arch = "wasm32")]
pub fn connect_extension(&mut self, cx: &mut Context<Self>) {
let signer = match BrowserSigner::new() {
Ok(signer) => signer,
Err(e) => {
cx.emit(StateEvent::error(format!(
"No Nostr extension found: {e}. Install a NIP-07 extension like Alby or nos2x."
)));
cx.emit(StateEvent::NoSigner);
return;
}
};
self.tasks.push(cx.spawn(async move |this, cx| {
match signer.get_public_key_async().await {
Ok(public_key) => {
// Remember the choice so reloads reconnect automatically
this.update(cx, |this, cx| {
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
this.signer.swap_inner(signer);
this.current_user = Some(public_key);
this.passphrase_required = false;
cx.emit(StateEvent::SignerChanged);
cx.notify();
})?;
}
Err(e) => {
// Drop the remembered choice so the next visit does not
// retry a signer that is no longer answering
this.update(cx, |_, cx| {
credentials::delete(cx).detach_and_log_err(cx);
cx.emit(StateEvent::error(e.to_string()));
cx.emit(StateEvent::NoSigner);
})?;
}
};
Ok(())
}));
}
/// Start the browser proxy /// Start the browser proxy
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn connect_proxy(&mut self, cx: &mut Context<Self>) { pub fn connect_proxy(&mut self, cx: &mut Context<Self>) {
+32 -2
View File
@@ -9,7 +9,7 @@ use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex}; use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex};
use instant::Duration; use instant::Duration;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use state::NostrRegistry; use state::{NostrRegistry, StateEvent};
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(window: &mut Window, cx: &mut App) {
let import = cx.new(|cx| ImportIdentity::new(window, cx)); let import = cx.new(|cx| ImportIdentity::new(window, cx));
@@ -73,12 +73,13 @@ impl ImportIdentity {
} }
fn build(mode: Mode, window: &mut Window, cx: &mut Context<Self>) -> Self { fn build(mode: Mode, window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("secret or bunker://")); let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("secret or bunker://"));
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let repass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let repass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let error = cx.new(|_| None); let error = cx.new(|_| None);
let subscriptions = vec![ let mut subscriptions = vec![
cx.subscribe_in(&key_input, window, |this, _input, event, _window, cx| { cx.subscribe_in(&key_input, window, |this, _input, event, _window, cx| {
if let InputEvent::PressEnter { .. } = event { if let InputEvent::PressEnter { .. } = event {
this.login(cx); this.login(cx);
@@ -96,6 +97,14 @@ impl ImportIdentity {
}), }),
]; ];
subscriptions.push(
cx.subscribe_in(&nostr, window, |this, _nostr, event, _window, cx| {
if let StateEvent::Error(error) = event {
this.set_error(error.clone(), cx);
}
}),
);
Self { Self {
mode, mode,
key_input, key_input,
@@ -188,6 +197,14 @@ impl ImportIdentity {
}); });
} }
#[cfg(target_arch = "wasm32")]
fn extension(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.connect_extension(cx);
});
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) { fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
self.loading = status; self.loading = status;
cx.notify(); cx.notify();
@@ -329,6 +346,19 @@ impl Render for ImportIdentity {
) )
}); });
#[cfg(target_arch = "wasm32")]
let content = content.when(!is_unlock, |this| {
this.child(
Button::new("extension")
.label("Continue with Browser Extension (NIP-07)")
.ghost()
.disabled(self.loading)
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.extension(cx);
})),
)
});
content content
}) })
.when_some(self.error.read(cx).as_ref(), |this, error| { .when_some(self.error.read(cx).as_ref(), |this, error| {