upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/lib/login
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-27 13:49:28 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-27 14:25:34 +0000
commitbccac9db18b93a08e5087578231ca9ac0df1903c (patch)
treea5c3acb53939885a554095876950676276b09c1c /src/lib/login
parent7b393f2e249e3bda141994d0c67aa9812d87302a (diff)
fix: connect to relay before user acknowledges nostrconnect QR/URL
Previously NostrConnect was only instantiated after the user selected 'waiting for signer app to connect...', so any connection made while the prompt was visible was missed. Now listening starts immediately after the QR/URL is displayed, with a spinner that auto-resolves on connection rather than requiring a manual prompt dismissal.
Diffstat (limited to 'src/lib/login')
-rw-r--r--src/lib/login/fresh.rs154
1 files changed, 117 insertions, 37 deletions
diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs
index 8d3a806..057b41a 100644
--- a/src/lib/login/fresh.rs
+++ b/src/lib/login/fresh.rs
@@ -3,6 +3,7 @@ use std::{str::FromStr, sync::Arc, time::Duration};
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use console::Style; 4use console::Style;
5use dialoguer::theme::{ColorfulTheme, Theme}; 5use dialoguer::theme::{ColorfulTheme, Theme};
6use indicatif::{ProgressBar, ProgressStyle};
6use nostr::nips::nip46::NostrConnectURI; 7use nostr::nips::nip46::NostrConnectURI;
7use nostr_connect::client::NostrConnect; 8use nostr_connect::client::NostrConnect;
8use nostr_sdk::{EventBuilder, Keys, Metadata, NostrSigner, PublicKey, RelayUrl, ToBech32}; 9use nostr_sdk::{EventBuilder, Keys, Metadata, NostrSigner, PublicKey, RelayUrl, ToBech32};
@@ -337,7 +338,72 @@ pub async fn get_fresh_nip46_signer(
337 // Display QR or URL with the current relay list. 338 // Display QR or URL with the current relay list.
338 display_nostr_connect(signer_choice, &current_url)?; 339 display_nostr_connect(signer_choice, &current_url)?;
339 340
340 // Offer the option to change relays or proceed. 341 // Start listening for the signer immediately after displaying
342 // the QR/URL — don't wait for the user to press anything.
343 let nostr_connect = Arc::new(NostrConnect::new(
344 current_url.clone(),
345 app_key.clone(),
346 Duration::from_secs(10 * 60),
347 None,
348 )?);
349 let signer_arc: Arc<dyn NostrSigner> = nostr_connect.clone();
350 let pubkey_handle = tokio::spawn(async move { signer_arc.get_public_key().await });
351
352 // Show a spinner while waiting; Ctrl+C lets the user change
353 // relays or go back instead of waiting indefinitely.
354 eprintln!();
355 // TODO: remove the dim delay note once the rust-nostr
356 // NostrConnect handshake delay bug is fixed.
357 let spinner_msg = format!(
358 "{} {}",
359 console::style("waiting for signer app to connect...").bold(),
360 console::style("(may take 10s+ to connect once added)").color256(247),
361 );
362 let spinner = ProgressBar::new_spinner()
363 .with_style(
364 ProgressStyle::with_template("{spinner} {msg}")
365 .unwrap()
366 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
367 )
368 .with_message(spinner_msg);
369 spinner.enable_steady_tick(Duration::from_millis(100));
370
371 let res = tokio::select! {
372 result = pubkey_handle => {
373 spinner.finish_and_clear();
374 match result {
375 Ok(Ok(pk)) => Some(pk),
376 _ => None,
377 }
378 },
379 _ = signal::ctrl_c() => {
380 spinner.finish_and_clear();
381 None
382 }
383 };
384
385 if let Some(public_key) = res {
386 // Connection succeeded — retrieve the canonical bunker URI
387 // and return.
388 let bunker_url = nostr_connect
389 .bunker_uri()
390 .await
391 .context("failed to get bunker URI from NostrConnect client")?;
392 let signer_info = SignerInfo::Bunker {
393 bunker_uri: bunker_url.to_string(),
394 bunker_app_key: app_key.secret_key().to_secret_hex(),
395 npub: Some(public_key.to_bech32()?),
396 };
397 return Ok(Some((
398 nostr_connect as Arc<dyn NostrSigner>,
399 public_key,
400 signer_info,
401 SignerInfoSource::GitGlobal,
402 )));
403 }
404
405 // Ctrl+C was pressed — offer the user a chance to change
406 // relays or go back to the top-level login menu.
341 let action = Interactor::default().choice( 407 let action = Interactor::default().choice(
342 PromptChoiceParms::default() 408 PromptChoiceParms::default()
343 .with_prompt(format!( 409 .with_prompt(format!(
@@ -351,22 +417,29 @@ pub async fn get_fresh_nip46_signer(
351 )) 417 ))
352 .with_default(0) 418 .with_default(0)
353 .with_choices(vec![ 419 .with_choices(vec![
354 "waiting for signer app to connect...".to_string(), 420 "try again".to_string(),
355 "change signer relays".to_string(), 421 "change signer relays".to_string(),
422 "back".to_string(),
356 ]) 423 ])
357 .dont_report(), 424 .dont_report(),
358 )?; 425 )?;
359 426
360 if action == 0 { 427 match action {
361 break current_url; 428 0 => {
362 } 429 // Redisplay QR/URL and start listening again.
363 430 continue;
364 // User wants to change relays — run the multiselect and rebuild URL. 431 }
365 let selected = select_signer_relays(&current_url)?; 432 1 => {
366 if !selected.is_empty() { 433 // Change relays and rebuild URL.
367 let new_relays: Vec<RelayUrl> = 434 let selected = select_signer_relays(&current_url)?;
368 selected.iter().flat_map(|s| RelayUrl::parse(s)).collect(); 435 if !selected.is_empty() {
369 current_url = NostrConnectURI::client(app_key.public_key(), new_relays, "ngit"); 436 let new_relays: Vec<RelayUrl> =
437 selected.iter().flat_map(|s| RelayUrl::parse(s)).collect();
438 current_url =
439 NostrConnectURI::client(app_key.public_key(), new_relays, "ngit");
440 }
441 }
442 _ => return Ok(None),
370 } 443 }
371 } 444 }
372 } 445 }
@@ -412,28 +485,9 @@ pub async fn get_fresh_nip46_signer(
412 { 485 {
413 let printer_clone = Arc::clone(&printer); 486 let printer_clone = Arc::clone(&printer);
414 let mut printer_locked = printer_clone.lock().await; 487 let mut printer_locked = printer_clone.lock().await;
415 // For choices 0 and 1 the content was already printed by display_nostr_connect 488 printer_locked.println(
416 // inside the relay-selection loop above; only the "waiting" hint is added here. 489 "add / approve in your signer or use ctrl + c to go back to login menu...".to_string(),
417 match signer_choice { 490 );
418 0 => {
419 printer_locked.println(
420 "scan QR code in signer app or use ctrl + c to go back to login menu..."
421 .to_string(),
422 );
423 }
424 1 => {
425 printer_locked.println(
426 "paste this url into signer app or use ctrl + c to go back to login menu..."
427 .to_string(),
428 );
429 }
430 _ => {
431 printer_locked.println(
432 "add / approve in your signer or use ctrl + c to go back to login menu..."
433 .to_string(),
434 );
435 }
436 }
437 } 491 }
438 492
439 let (signer, user_public_key, bunker_url) = 493 let (signer, user_public_key, bunker_url) =
@@ -487,17 +541,43 @@ pub fn generate_nostr_connect_app(
487/// `choice` must be 0 (QR) or 1 (URL). Output goes directly to stderr so it 541/// `choice` must be 0 (QR) or 1 (URL). Output goes directly to stderr so it
488/// is visible before the relay-selection choice prompt that follows. 542/// is visible before the relay-selection choice prompt that follows.
489fn display_nostr_connect(choice: usize, url: &NostrConnectURI) -> Result<()> { 543fn display_nostr_connect(choice: usize, url: &NostrConnectURI) -> Result<()> {
490 eprintln!("login to nostr with remote signer via nostr connect"); 544 let dim = Style::new().for_stderr().color256(247);
545 let hint = dim.apply_to("(ctrl+c to change)");
546 eprintln!(
547 "{}",
548 Style::new().for_stderr().bold().apply_to("nostr connect")
549 );
491 if choice == 0 { 550 if choice == 0 {
492 eprintln!("scan QR code in signer app (eg Amber):"); 551 eprintln!(
552 "{}",
553 dim.apply_to("scan QR code in signer app (eg. Amber):")
554 );
493 for line in generate_qr(&url.to_string())? { 555 for line in generate_qr(&url.to_string())? {
494 eprintln!("{line}"); 556 eprintln!("{line}");
495 } 557 }
496 } else { 558 } else {
497 eprintln!(); 559 eprintln!();
498 eprintln!("{}", Style::new().bold().apply_to(url.to_string())); 560 eprintln!(
561 "{}",
562 Style::new()
563 .for_stderr()
564 .bold()
565 .cyan()
566 .apply_to(url.to_string())
567 );
499 eprintln!(); 568 eprintln!();
500 } 569 }
570 let relays = url
571 .relays()
572 .iter()
573 .map(std::string::ToString::to_string)
574 .collect::<Vec<_>>()
575 .join(", ");
576 eprintln!(
577 "{} {}",
578 dim.apply_to(format!("signer relays: {relays}")),
579 hint
580 );
501 Ok(()) 581 Ok(())
502} 582}
503 583