diff --git a/CHANGELOG.md b/CHANGELOG.md index 7753f2037..7730307c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -342,3 +342,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). currently knows about becomes briefly unreachable at once — a temporary, self-recovering condition. The app now recognizes it and says "All Dash network servers are temporarily unreachable. Please wait a minute and retry." instead. +- The onboarding Welcome screen on first launch no longer shows a red + "Disconnected — check your internet connection" banner before you have done + anything. On a fresh start there is no wallet yet and no sync has been + attempted, so that message was misleading; it now stays hidden until you + finish onboarding, and real connection problems are still reported afterwards. diff --git a/src/app.rs b/src/app.rs index 21f874d64..d2e735ccd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2489,10 +2489,12 @@ impl App for AppState { // runs before the connection banner, which suppresses its redundant // Connecting/Syncing text while the overlay is up. let spv_overlaying = self.spv_block.is_overlaying(); - if let Some(task) = self - .connection_banner - .update(ctx, &active_context, spv_overlaying) - { + if let Some(task) = self.connection_banner.update( + ctx, + &active_context, + spv_overlaying, + self.show_welcome_screen, + ) { self.handle_backend_task(task); } if !self.network_selection_required diff --git a/src/app/reconcilers.rs b/src/app/reconcilers.rs index f042bf0c9..7ac9cf39c 100644 --- a/src/app/reconcilers.rs +++ b/src/app/reconcilers.rs @@ -252,12 +252,15 @@ impl ConnectionBanner { /// Update the banner for the current connection state. `spv_overlaying` /// suppresses the redundant Connecting/Syncing copy while the SPV block is - /// up. Returns a [`BackendTask`] to dispatch on the first `Synced`. + /// up; `onboarding_active` suppresses the initial `Disconnected` banner while + /// the Welcome screen is showing (pre-sync, not a real failure). Returns a + /// [`BackendTask`] to dispatch on the first `Synced`. pub(super) fn update( &mut self, ctx: &egui::Context, app_context: &Arc, spv_overlaying: bool, + onboarding_active: bool, ) -> Option { let connection_status = app_context.connection_status(); let current_state = connection_status.overall_state(); @@ -284,6 +287,17 @@ impl ConnectionBanner { return None; } + // The Welcome screen initially reads Disconnected before sync starts. + if onboarding_active && current_state == OverallConnectionState::Disconnected { + if let Some(handle) = self.handle.take() { + handle.clear(); + } + // Invalidate rather than cache either state so suppression exit and + // recurring pre-suppression states both force reconciliation. + self.previous_state = None; + return None; + } + // Clear old banner on state transitions. if state_changed && let Some(handle) = self.handle.take() { handle.clear(); @@ -879,4 +893,75 @@ mod tests { )), ); } + + /// Disconnected stays hidden throughout onboarding and appears on the first + /// frame after onboarding ends, even when the connection state is unchanged. + #[test] + fn connection_banner_suppresses_disconnected_until_onboarding_ends() { + let tmp = tempfile::tempdir().expect("tempdir"); + let app_context = test_app_context(tmp.path()); + // ConnectionStatus defaults to Disconnected — no sync has been asked for. + assert_eq!( + app_context.connection_status().overall_state(), + OverallConnectionState::Disconnected + ); + let ctx = egui::Context::default(); + let mut banner = ConnectionBanner::new(); + + // Frame 1: onboarding active, Disconnected — suppressed. + assert!(banner.update(&ctx, &app_context, false, true).is_none()); + assert!( + banner.handle.is_none(), + "the Disconnected banner must stay hidden while onboarding is active" + ); + + // Frame 2: onboarding still active, state unchanged — still suppressed. + assert!(banner.update(&ctx, &app_context, false, true).is_none()); + assert!( + banner.handle.is_none(), + "Disconnected must stay hidden for every frame while onboarding is active" + ); + + // Frame 3: onboarding ends, connection is still Disconnected — the real + // banner must now appear even though `current_state` never changed. + assert!(banner.update(&ctx, &app_context, false, false).is_none()); + assert!( + banner.handle.is_some(), + "a genuine Disconnected state must be reported once onboarding ends, \ + even if the connection state itself never changed" + ); + } + + #[test] + fn connection_banner_restores_recurring_error_after_onboarding_suppression() { + let tmp = tempfile::tempdir().expect("tempdir"); + let app_context = test_app_context(tmp.path()); + let connection_status = app_context.connection_status(); + let ctx = egui::Context::default(); + let mut banner = ConnectionBanner::new(); + + connection_status.set_spv_status(crate::model::spv_status::SpvStatus::Error); + connection_status.refresh_state(); + assert!(banner.update(&ctx, &app_context, false, true).is_none()); + assert!( + banner.handle.is_some(), + "the first SPV error must be reported" + ); + + connection_status.set_spv_status(crate::model::spv_status::SpvStatus::Idle); + connection_status.refresh_state(); + assert!(banner.update(&ctx, &app_context, false, true).is_none()); + assert!( + banner.handle.is_none(), + "Disconnected must be suppressed while onboarding is active" + ); + + connection_status.set_spv_status(crate::model::spv_status::SpvStatus::Error); + connection_status.refresh_state(); + assert!(banner.update(&ctx, &app_context, false, true).is_none()); + assert!( + banner.handle.is_some(), + "a recurring SPV error must be restored after Disconnected suppression" + ); + } } diff --git a/tests/kittest/welcome_screen.rs b/tests/kittest/welcome_screen.rs index 8289f4b8e..c64f341ce 100644 --- a/tests/kittest/welcome_screen.rs +++ b/tests/kittest/welcome_screen.rs @@ -222,3 +222,39 @@ fn just_explore_lands_on_identities_hub() { ); }); } + +/// A cold start opens on the Welcome screen while the connection state is still +/// its default `Disconnected` — no wallet exists and nothing has asked SPV to +/// sync, so this is "hasn't started yet", not a real failure. The alarming red +/// "Disconnected — check your internet connection" banner must be suppressed +/// until onboarding completes; otherwise it reads as a connectivity problem the +/// user cannot act on. +#[test] +fn welcome_screen_suppresses_disconnected_banner() { + with_isolated_data_dir(|| { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + let _guard = rt.enter(); + + // A fresh data dir leaves onboarding incomplete, so the welcome screen + // renders on the first frame while the connection state is still the + // default `Disconnected` (no sync has been attempted). + let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { + dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()) + .expect("Failed to create AppState") + .with_animations(false) + }); + harness.set_size(egui::vec2(1024.0, 768.0)); + harness.run_steps(10); + + assert!( + harness.state().show_welcome_screen, + "a fresh data dir must open on the onboarding welcome screen" + ); + assert!( + harness + .query_by_label_contains("check your internet connection") + .is_none(), + "the onboarding screen must not show the red 'Disconnected' connection banner" + ); + }); +}