diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-27 09:31:45 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-27 14:24:52 +0000 |
| commit | 436b707b2bdecb995bbdb374a029714c9f4c5159 (patch) | |
| tree | f45ec5c6f166070e078ca7415f9756e33ee91ebf | |
| parent | 76a5e7b46dbe90ebf5e31904cb510e6cab242cf4 (diff) | |
feat: show and allow changing signer relays in nostrconnect flow
display signer relays below QR code and nostrconnect URL with an option
to change them via the existing multiselect UI before connecting
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/lib/login/fresh.rs | 109 |
2 files changed, 96 insertions, 14 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f88b1d..930fe39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
| @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 13 | - `ngit sync` now publishes the current state event to grasp server relays that are missing it or have a stale version before attempting git pushes, preventing rejections; per-relay state visibility is captured during the nostr fetch and surfaced via `FetchReport::state_per_relay` | 13 | - `ngit sync` now publishes the current state event to grasp server relays that are missing it or have a stale version before attempting git pushes, preventing rejections; per-relay state visibility is captured during the nostr fetch and surfaced via `FetchReport::state_per_relay` |
| 14 | - Fetch filters now request kind-5 deletion events for cached state and repo announcement events by `#e` tag (NIP-09), in addition to the existing `#a`-tagged filter; ensures deletions of these events are received even from clients that do not embed a repo coordinate in their deletion event | 14 | - Fetch filters now request kind-5 deletion events for cached state and repo announcement events by `#e` tag (NIP-09), in addition to the existing `#a`-tagged filter; ensures deletions of these events are received even from clients that do not embed a repo coordinate in their deletion event |
| 15 | - `FetchReport` now tracks and displays a count of kind-5 deletion events received (e.g. `"1 deletion"` in the fetch summary) | 15 | - `FetchReport` now tracks and displays a count of kind-5 deletion events received (e.g. `"1 deletion"` in the fetch summary) |
| 16 | - `ngit account login` nostrconnect flow now shows current signer relays and allows changing them | ||
| 16 | 17 | ||
| 17 | ### Fixed | 18 | ### Fixed |
| 18 | 19 | ||
diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs index e81b74a..0b5922b 100644 --- a/src/lib/login/fresh.rs +++ b/src/lib/login/fresh.rs | |||
| @@ -23,7 +23,8 @@ use crate::client::MockConnect; | |||
| 23 | use crate::{ | 23 | use crate::{ |
| 24 | cli_interactor::{ | 24 | cli_interactor::{ |
| 25 | Interactor, InteractorPrompt, Printer, PromptChoiceParms, PromptConfirmParms, | 25 | Interactor, InteractorPrompt, Printer, PromptChoiceParms, PromptConfirmParms, |
| 26 | PromptInputParms, PromptPasswordParms, | 26 | PromptInputParms, PromptPasswordParms, multi_select_with_custom_value, |
| 27 | show_multi_input_prompt_success, | ||
| 27 | }, | 28 | }, |
| 28 | client::{Connect, nip05_query, save_event_in_global_cache, send_events}, | 29 | client::{Connect, nip05_query, save_event_in_global_cache, send_events}, |
| 29 | git::{Repo, RepoActions, remove_git_config_item, save_git_config_item}, | 30 | git::{Repo, RepoActions, remove_git_config_item, save_git_config_item}, |
| @@ -273,7 +274,46 @@ pub async fn get_fresh_nip46_signer( | |||
| 273 | .dont_report(), | 274 | .dont_report(), |
| 274 | )?; | 275 | )?; |
| 275 | let url = match signer_choice { | 276 | let url = match signer_choice { |
| 276 | 0 | 1 => nostr_connect_url, | 277 | 0 | 1 => { |
| 278 | // Loop so the user can change relays and see a refreshed QR/URL. | ||
| 279 | let mut current_url = nostr_connect_url; | ||
| 280 | loop { | ||
| 281 | // Display QR or URL with the current relay list. | ||
| 282 | display_nostr_connect(signer_choice, ¤t_url)?; | ||
| 283 | |||
| 284 | // Offer the option to change relays or proceed. | ||
| 285 | let action = Interactor::default().choice( | ||
| 286 | PromptChoiceParms::default() | ||
| 287 | .with_prompt(format!( | ||
| 288 | "signer relays: {}", | ||
| 289 | current_url | ||
| 290 | .relays() | ||
| 291 | .iter() | ||
| 292 | .map(std::string::ToString::to_string) | ||
| 293 | .collect::<Vec<_>>() | ||
| 294 | .join(", ") | ||
| 295 | )) | ||
| 296 | .with_default(0) | ||
| 297 | .with_choices(vec![ | ||
| 298 | "waiting for signer app to connect...".to_string(), | ||
| 299 | "change signer relays".to_string(), | ||
| 300 | ]) | ||
| 301 | .dont_report(), | ||
| 302 | )?; | ||
| 303 | |||
| 304 | if action == 0 { | ||
| 305 | break current_url; | ||
| 306 | } | ||
| 307 | |||
| 308 | // User wants to change relays — run the multiselect and rebuild URL. | ||
| 309 | let selected = select_signer_relays(¤t_url)?; | ||
| 310 | if !selected.is_empty() { | ||
| 311 | let new_relays: Vec<RelayUrl> = | ||
| 312 | selected.iter().flat_map(|s| RelayUrl::parse(s)).collect(); | ||
| 313 | current_url = NostrConnectURI::client(app_key.public_key(), new_relays, "ngit"); | ||
| 314 | } | ||
| 315 | } | ||
| 316 | } | ||
| 277 | 2 => { | 317 | 2 => { |
| 278 | let mut error = None; | 318 | let mut error = None; |
| 279 | loop { | 319 | loop { |
| @@ -316,26 +356,16 @@ pub async fn get_fresh_nip46_signer( | |||
| 316 | { | 356 | { |
| 317 | let printer_clone = Arc::clone(&printer); | 357 | let printer_clone = Arc::clone(&printer); |
| 318 | let mut printer_locked = printer_clone.lock().await; | 358 | let mut printer_locked = printer_clone.lock().await; |
| 359 | // For choices 0 and 1 the content was already printed by display_nostr_connect | ||
| 360 | // inside the relay-selection loop above; only the "waiting" hint is added here. | ||
| 319 | match signer_choice { | 361 | match signer_choice { |
| 320 | 0 => { | 362 | 0 => { |
| 321 | printer_locked | ||
| 322 | .println("login to nostr with remote signer via nostr connect".to_string()); | ||
| 323 | printer_locked.println("scan QR code in signer app (eg Amber):".to_string()); | ||
| 324 | printer_locked.printlns(generate_qr(&url.to_string())?); | ||
| 325 | printer_locked.println( | 363 | printer_locked.println( |
| 326 | "scan QR code in signer app or use ctrl + c to go back to login menu..." | 364 | "scan QR code in signer app or use ctrl + c to go back to login menu..." |
| 327 | .to_string(), | 365 | .to_string(), |
| 328 | ); | 366 | ); |
| 329 | } | 367 | } |
| 330 | 1 => { | 368 | 1 => { |
| 331 | printer_locked | ||
| 332 | .println("login to nostr with remote signer via nostr connect".to_string()); | ||
| 333 | printer_locked.println("".to_string()); | ||
| 334 | printer_locked.println_with_custom_formatting( | ||
| 335 | format!("{}", Style::new().bold().apply_to(url.to_string()),), | ||
| 336 | url.to_string(), | ||
| 337 | ); | ||
| 338 | printer_locked.println("".to_string()); | ||
| 339 | printer_locked.println( | 369 | printer_locked.println( |
| 340 | "paste this url into signer app or use ctrl + c to go back to login menu..." | 370 | "paste this url into signer app or use ctrl + c to go back to login menu..." |
| 341 | .to_string(), | 371 | .to_string(), |
| @@ -396,6 +426,57 @@ pub fn generate_nostr_connect_app( | |||
| 396 | Ok((app_key, nostr_connect_url)) | 426 | Ok((app_key, nostr_connect_url)) |
| 397 | } | 427 | } |
| 398 | 428 | ||
| 429 | /// Print the QR code or nostrconnect URL to stderr. | ||
| 430 | /// | ||
| 431 | /// `choice` must be 0 (QR) or 1 (URL). Output goes directly to stderr so it | ||
| 432 | /// is visible before the relay-selection choice prompt that follows. | ||
| 433 | fn display_nostr_connect(choice: usize, url: &NostrConnectURI) -> Result<()> { | ||
| 434 | eprintln!("login to nostr with remote signer via nostr connect"); | ||
| 435 | if choice == 0 { | ||
| 436 | eprintln!("scan QR code in signer app (eg Amber):"); | ||
| 437 | for line in generate_qr(&url.to_string())? { | ||
| 438 | eprintln!("{line}"); | ||
| 439 | } | ||
| 440 | } else { | ||
| 441 | eprintln!(); | ||
| 442 | eprintln!("{}", Style::new().bold().apply_to(url.to_string())); | ||
| 443 | eprintln!(); | ||
| 444 | } | ||
| 445 | Ok(()) | ||
| 446 | } | ||
| 447 | |||
| 448 | /// Present the multiselect UI for choosing signer relays. | ||
| 449 | /// | ||
| 450 | /// Returns the selected relay list as strings. An empty return means the user | ||
| 451 | /// submitted without selecting anything (caller should keep the existing URL). | ||
| 452 | fn select_signer_relays(nostr_connect_url: &NostrConnectURI) -> Result<Vec<String>> { | ||
| 453 | let current_relays: Vec<String> = nostr_connect_url | ||
| 454 | .relays() | ||
| 455 | .iter() | ||
| 456 | .map(std::string::ToString::to_string) | ||
| 457 | .collect(); | ||
| 458 | |||
| 459 | let defaults = vec![true; current_relays.len()]; | ||
| 460 | let selected = multi_select_with_custom_value( | ||
| 461 | "signer relays", | ||
| 462 | "signer relay", | ||
| 463 | current_relays, | ||
| 464 | defaults, | ||
| 465 | |s| { | ||
| 466 | let url = if s.starts_with("ws://") || s.starts_with("wss://") { | ||
| 467 | s.to_string() | ||
| 468 | } else { | ||
| 469 | format!("wss://{s}") | ||
| 470 | }; | ||
| 471 | RelayUrl::parse(&url) | ||
| 472 | .map(|r| r.to_string()) | ||
| 473 | .context(format!("invalid relay URL: {s}")) | ||
| 474 | }, | ||
| 475 | )?; | ||
| 476 | show_multi_input_prompt_success("signer relays", &selected); | ||
| 477 | Ok(selected) | ||
| 478 | } | ||
| 479 | |||
| 399 | pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> { | 480 | pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> { |
| 400 | let term = console::Term::stderr(); | 481 | let term = console::Term::stderr(); |
| 401 | term.write_line("contacting login service provider...")?; | 482 | term.write_line("contacting login service provider...")?; |