summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-02-17 15:33:10 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-05 23:32:07 +0100
commitee2858eb52d8e3fb28f8e994c3c4fb9d6f8ea2ae (patch)
treecb0801590cc1f348be77986d9f51e71d0c9fda89
parent7afe2c6ce0bf6cbb3c0e6c8526a1b694fed8acd0 (diff)
downloadmullvadvpn-ee2858eb52d8e3fb28f8e994c3c4fb9d6f8ea2ae.tar.xz
mullvadvpn-ee2858eb52d8e3fb28f8e994c3c4fb9d6f8ea2ae.zip
Add improved error messages
Co-authored-by: Joakim Hulthe <joakim.hulthe@mullvad.net>
-rw-r--r--installer-downloader/Cargo.toml1
-rw-r--r--installer-downloader/assets/alert-circle.pngbin0 -> 1115 bytes
-rw-r--r--installer-downloader/assets/alert-circle.svg8
-rw-r--r--installer-downloader/convert-assets.py1
-rw-r--r--installer-downloader/src/controller.rs117
-rw-r--r--installer-downloader/src/delegate.rs23
-rw-r--r--installer-downloader/src/resource.rs33
-rw-r--r--installer-downloader/src/ui_downloader.rs38
-rw-r--r--installer-downloader/src/winapi_impl/delegate.rs46
-rw-r--r--installer-downloader/src/winapi_impl/ui.rs69
-rw-r--r--installer-downloader/tests/controller.rs35
-rw-r--r--installer-downloader/tests/snapshots/controller__download-2.snap13
-rw-r--r--installer-downloader/tests/snapshots/controller__download-3.snap13
-rw-r--r--installer-downloader/tests/snapshots/controller__download.snap5
-rw-r--r--installer-downloader/tests/snapshots/controller__failed_directory_creation.snap18
-rw-r--r--installer-downloader/tests/snapshots/controller__failed_verification.snap23
-rw-r--r--installer-downloader/tests/snapshots/controller__fetch_version-2.snap9
-rw-r--r--installer-downloader/tests/snapshots/controller__fetch_version.snap5
18 files changed, 418 insertions, 39 deletions
diff --git a/installer-downloader/Cargo.toml b/installer-downloader/Cargo.toml
index be2c830c2d..79c7066074 100644
--- a/installer-downloader/Cargo.toml
+++ b/installer-downloader/Cargo.toml
@@ -19,6 +19,7 @@ windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_Li
anyhow = "1.0"
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
+serde = { workspace = true, features = ["derive"] }
mullvad-update = { path = "../mullvad-update" }
diff --git a/installer-downloader/assets/alert-circle.png b/installer-downloader/assets/alert-circle.png
new file mode 100644
index 0000000000..d53d283e33
--- /dev/null
+++ b/installer-downloader/assets/alert-circle.png
Binary files differ
diff --git a/installer-downloader/assets/alert-circle.svg b/installer-downloader/assets/alert-circle.svg
new file mode 100644
index 0000000000..abb561611f
--- /dev/null
+++ b/installer-downloader/assets/alert-circle.svg
@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<mask id="mask0_4714_1057" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32">
+<path d="M32 0H0V32H32V0Z" fill="#D9D9D9"/>
+</mask>
+<g mask="url(#mask0_4714_1057)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M30 16C30 23.732 23.732 30 16 30C8.26801 30 2 23.732 2 16C2 8.26801 8.26801 2 16 2C23.732 2 30 8.26801 30 16ZM17.4 23C17.4 23.7732 16.7732 24.4 16 24.4C15.2268 24.4 14.6 23.7732 14.6 23C14.6 22.2268 15.2268 21.6 16 21.6C16.7732 21.6 17.4 22.2268 17.4 23ZM17.4 17.4V9C17.4 8.2268 16.7732 7.6 16 7.6C15.2268 7.6 14.6 8.2268 14.6 9V17.4C14.6 18.1732 15.2268 18.8 16 18.8C16.7732 18.8 17.4 18.1732 17.4 17.4Z" fill="#E34039"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/installer-downloader/convert-assets.py b/installer-downloader/convert-assets.py
index ede39abe21..1dcdfa97a2 100644
--- a/installer-downloader/convert-assets.py
+++ b/installer-downloader/convert-assets.py
@@ -6,3 +6,4 @@ from cairosvg import svg2png
svg2png(url="assets/logo-icon.svg", write_to="assets/logo-icon.png", output_width=32)
svg2png(url="assets/logo-text.svg", write_to="assets/logo-text.png", output_width=122)
+svg2png(url="assets/alert-circle.svg", write_to="assets/alert-circle.png", output_width=32)
diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs
index e241daf4d8..09860ec56c 100644
--- a/installer-downloader/src/controller.rs
+++ b/installer-downloader/src/controller.rs
@@ -85,11 +85,12 @@ impl AppController {
let (task_tx, task_rx) = mpsc::channel(1);
tokio::spawn(handle_action_messages::<D, A, DirProvider>(
delegate.queue(),
+ task_tx.clone(),
task_rx,
));
delegate.set_status_text(resource::FETCH_VERSION_DESC);
tokio::spawn(fetch_app_version_info::<D, V>(
- delegate,
+ delegate.queue(),
task_tx.clone(),
version_provider,
));
@@ -121,32 +122,77 @@ impl AppController {
/// Background task that fetches app version data.
fn fetch_app_version_info<Delegate, VersionProvider>(
- delegate: &mut Delegate,
+ queue: Delegate::Queue,
download_tx: mpsc::Sender<TaskMessage>,
version_provider: VersionProvider,
) -> impl Future<Output = ()>
where
- Delegate: AppDelegate,
+ Delegate: AppDelegate + 'static,
VersionProvider: VersionInfoProvider + Send,
{
- let queue = delegate.queue();
-
async move {
- let version_params = VersionParameters {
- // TODO: detect current architecture
- architecture: VersionArchitecture::X86,
- // For the downloader, the rollout version is always preferred
- rollout: 1.,
- };
+ loop {
+ let version_params = VersionParameters {
+ // TODO: detect current architecture
+ architecture: VersionArchitecture::X86,
+ // For the downloader, the rollout version is always preferred
+ rollout: 1.,
+ };
+
+ let err = match version_provider.get_version_info(version_params).await {
+ Ok(version_info) => {
+ let _ = download_tx.try_send(TaskMessage::SetVersionInfo(version_info));
+ return;
+ }
+ Err(err) => err,
+ };
+
+ eprintln!("Failed to get version info: {err}");
+
+ enum Action {
+ Retry,
+ Cancel,
+ }
- // TODO: handle errors, retry
- let Ok(version_info) = version_provider.get_version_info(version_params).await else {
+ let (action_tx, mut action_rx) = mpsc::channel(1);
+
+ // show error message (needs to happen on the UI thread)
+ // send Action when user presses a button to contin
queue.queue_main(move |self_| {
- self_.set_status_text("Failed to fetch version info");
+ self_.hide_download_button();
+
+ let (retry_tx, cancel_tx) = (action_tx.clone(), action_tx);
+
+ self_.set_status_text("");
+ self_.on_error_message_retry(move || {
+ let _ = retry_tx.try_send(Action::Retry);
+ });
+ self_.on_error_message_cancel(move || {
+ let _ = cancel_tx.try_send(Action::Cancel);
+ });
+ self_.show_error_message(crate::delegate::ErrorMessage {
+ status_text: resource::FETCH_VERSION_ERROR_DESC.to_owned(),
+ cancel_button_text: resource::FETCH_VERSION_ERROR_CANCEL_BUTTON_TEXT.to_owned(),
+ retry_button_text: resource::FETCH_VERSION_ERROR_RETRY_BUTTON_TEXT.to_owned(),
+ });
});
- return;
- };
- let _ = download_tx.try_send(TaskMessage::SetVersionInfo(version_info));
+
+ // wait for user to press either button
+ let Some(action) = action_rx.recv().await else {
+ panic!("channel was dropped? argh")
+ };
+
+ match action {
+ Action::Retry => {
+ continue;
+ }
+ Action::Cancel => {
+ queue.queue_main(|self_| {
+ self_.quit();
+ });
+ }
+ }
+ }
}
}
@@ -160,6 +206,7 @@ enum TargetVersion {
/// labels.
async fn handle_action_messages<D, A, DirProvider>(
queue: D::Queue,
+ tx: mpsc::Sender<TaskMessage>,
mut rx: mpsc::Receiver<TaskMessage>,
) where
D: AppDelegate + 'static,
@@ -218,19 +265,39 @@ async fn handle_action_messages<D, A, DirProvider>(
});
}
TaskMessage::BeginDownload => {
- if active_download.is_some() {
- continue;
+ if let Some(_) = active_download.take() {
+ println!("Interrupting ongoing download");
}
let Some(version_info) = version_info.clone() else {
continue;
};
+ let (retry_tx, cancel_tx) = (tx.clone(), tx.clone());
+ queue.queue_main(move |self_| {
+ self_.hide_error_message();
+ self_.on_error_message_retry(move || {
+ let _ = retry_tx.try_send(TaskMessage::BeginDownload);
+ });
+ self_.on_error_message_cancel(move || {
+ let _ = cancel_tx.try_send(TaskMessage::Cancel);
+ });
+ });
+
// Create temporary dir
let download_dir = match DirProvider::create_download_dir().await {
Ok(dir) => dir,
Err(_err) => {
queue.queue_main(move |self_| {
- self_.set_status_text("Failed to create download directory");
+ self_.set_status_text("");
+ self_.hide_download_button();
+ self_.hide_beta_text();
+ self_.hide_stable_text();
+
+ self_.show_error_message(crate::delegate::ErrorMessage {
+ status_text: "Failed to create download directory".to_owned(),
+ cancel_button_text: "Cancel".to_owned(),
+ retry_button_text: "Try again".to_owned(),
+ });
});
continue;
}
@@ -277,11 +344,10 @@ async fn handle_action_messages<D, A, DirProvider>(
active_download = rx.await.ok();
}
TaskMessage::Cancel => {
- let Some(active_download) = active_download.take() else {
- continue;
- };
- active_download.abort();
- let _ = active_download.await;
+ if let Some(active_download) = active_download.take() {
+ active_download.abort();
+ let _ = active_download.await;
+ }
let Some(version_info) = version_info.as_ref() else {
continue;
@@ -301,6 +367,7 @@ async fn handle_action_messages<D, A, DirProvider>(
self_.set_status_text(&version_label);
self_.set_download_text("");
self_.show_download_button();
+ self_.hide_error_message();
if target_version == TargetVersion::Stable {
if has_beta {
diff --git a/installer-downloader/src/delegate.rs b/installer-downloader/src/delegate.rs
index de4313aeb2..40d54efc3f 100644
--- a/installer-downloader/src/delegate.rs
+++ b/installer-downloader/src/delegate.rs
@@ -78,6 +78,22 @@ pub trait AppDelegate {
/// Hide stable text
fn hide_stable_text(&mut self);
+ /// Show error message
+ fn show_error_message(&mut self, message: ErrorMessage);
+
+ /// Hide error message
+ fn hide_error_message(&mut self);
+
+ /// Set error cancel callback
+ fn on_error_message_retry<F>(&mut self, callback: F)
+ where
+ F: Fn() + Send + 'static;
+
+ /// Set error cancel callback
+ fn on_error_message_cancel<F>(&mut self, callback: F)
+ where
+ F: Fn() + Send + 'static;
+
/// Exit the application
fn quit(&mut self);
@@ -85,6 +101,13 @@ pub trait AppDelegate {
fn queue(&self) -> Self::Queue;
}
+#[derive(Default, serde::Serialize)]
+pub struct ErrorMessage {
+ pub status_text: String,
+ pub cancel_button_text: String,
+ pub retry_button_text: String,
+}
+
/// Schedules actions on the UI thread from other threads
pub trait AppDelegateQueue<T: ?Sized>: Send {
fn queue_main<F: FnOnce(&mut T) + 'static + Send>(&self, callback: F);
diff --git a/installer-downloader/src/resource.rs b/installer-downloader/src/resource.rs
index af54455872..a8aa106c82 100644
--- a/installer-downloader/src/resource.rs
+++ b/installer-downloader/src/resource.rs
@@ -40,11 +40,44 @@ pub const LATEST_VERSION_PREFIX: &str = "Version";
/// Displayed while fetching version info from the API failed
pub const FETCH_VERSION_ERROR_DESC: &str = "Couldn't load version details, please try again or make sure you have the latest installer downloader.";
+/// Displayed while fetching version info from the API failed (retry button)
+pub const FETCH_VERSION_ERROR_RETRY_BUTTON_TEXT: &str = "Try again";
+
+/// Displayed while fetching version info from the API failed (cancel button)
+pub const FETCH_VERSION_ERROR_CANCEL_BUTTON_TEXT: &str = "Cancel";
+
/// The first part of "Downloading from <some url>... (x%)", displayed during download
pub const DOWNLOADING_DESC_PREFIX: &str = "Downloading from";
/// Displayed after completed download
pub const DOWNLOAD_COMPLETE_DESC: &str = "Download complete. Verifying...";
+/// Displayed when download fails
+pub const DOWNLOAD_FAILED_DESC: &str = "Download failed";
+
+/// Displayed when download fails (retry button)
+pub const DOWNLOAD_FAILED_RETRY_BUTTON_TEXT: &str = "Redownload";
+
+/// Displayed when download fails (cancel button)
+pub const DOWNLOAD_FAILED_CANCEL_BUTTON_TEXT: &str = "Cancel";
+
+/// Displayed when download fails
+pub const VERIFICATION_FAILED_DESC: &str = "Couldn’t verify download, please try downloading again or contact our support by sending an email at support@mullvadvpn.net";
+
+/// Displayed when download fails (retry button)
+pub const VERIFICATION_FAILED_RETRY_BUTTON_TEXT: &str = "Redownload";
+
+/// Displayed when download fails (cancel button)
+pub const VERIFICATION_FAILED_CANCEL_BUTTON_TEXT: &str = "Cancel";
+
/// Displayed after verification
pub const VERIFICATION_SUCCEEDED_DESC: &str = "Verification successful. Starting install...";
+
+/// Displayed when launch fails
+pub const LAUNCH_FAILED_DESC: &str = "Couldn’t launch installer, please try again or contact our support by sending an email at support@mullvadvpn.net";
+
+/// Displayed when launch fails (retry button)
+pub const LAUNCH_FAILED_RETRY_BUTTON_TEXT: &str = "Try again";
+
+/// Displayed when launch fails (cancel button)
+pub const LAUNCH_FAILED_CANCEL_BUTTON_TEXT: &str = "Cancel";
diff --git a/installer-downloader/src/ui_downloader.rs b/installer-downloader/src/ui_downloader.rs
index 437320a537..69e405797b 100644
--- a/installer-downloader/src/ui_downloader.rs
+++ b/installer-downloader/src/ui_downloader.rs
@@ -49,7 +49,17 @@ impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> AppDownl
}
Err(err) => {
self.queue.queue_main(move |self_| {
- self_.set_download_text("ERROR: Download failed. Please try again.");
+ self_.set_status_text("");
+ self_.set_download_text("");
+ self_.hide_download_progress();
+ self_.hide_download_button();
+ self_.hide_cancel_button();
+
+ self_.show_error_message(crate::delegate::ErrorMessage {
+ status_text: resource::DOWNLOAD_FAILED_DESC.to_owned(),
+ cancel_button_text: resource::DOWNLOAD_FAILED_CANCEL_BUTTON_TEXT.to_owned(),
+ retry_button_text: resource::DOWNLOAD_FAILED_RETRY_BUTTON_TEXT.to_owned(),
+ });
});
Err(err)
@@ -68,7 +78,19 @@ impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> AppDownl
}
Err(error) => {
self.queue.queue_main(move |self_| {
- self_.set_download_text("ERROR: Verification failed!");
+ self_.set_status_text("");
+ self_.set_download_text("");
+ self_.hide_download_progress();
+ self_.hide_download_button();
+ self_.hide_cancel_button();
+
+ self_.show_error_message(crate::delegate::ErrorMessage {
+ status_text: resource::VERIFICATION_FAILED_DESC.to_owned(),
+ cancel_button_text: resource::VERIFICATION_FAILED_CANCEL_BUTTON_TEXT
+ .to_owned(),
+ retry_button_text: resource::VERIFICATION_FAILED_RETRY_BUTTON_TEXT
+ .to_owned(),
+ });
});
Err(error)
@@ -87,7 +109,17 @@ impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> AppDownl
}
Err(error) => {
self.queue.queue_main(move |self_| {
- self_.set_download_text("ERROR: Failed to launch installer!");
+ self_.set_status_text("");
+ self_.set_download_text("");
+ self_.hide_download_progress();
+ self_.hide_download_button();
+ self_.hide_cancel_button();
+
+ self_.show_error_message(crate::delegate::ErrorMessage {
+ status_text: resource::LAUNCH_FAILED_DESC.to_owned(),
+ cancel_button_text: resource::LAUNCH_FAILED_CANCEL_BUTTON_TEXT.to_owned(),
+ retry_button_text: resource::LAUNCH_FAILED_RETRY_BUTTON_TEXT.to_owned(),
+ });
});
Err(error)
diff --git a/installer-downloader/src/winapi_impl/delegate.rs b/installer-downloader/src/winapi_impl/delegate.rs
index da2e5ca317..b4b9793bdd 100644
--- a/installer-downloader/src/winapi_impl/delegate.rs
+++ b/installer-downloader/src/winapi_impl/delegate.rs
@@ -1,6 +1,7 @@
//! This module implements [AppDelegate] and [Queue], which allows the NWG UI to be hooked up to our
//! generic controller.
+use installer_downloader::delegate::ErrorMessage;
use native_windows_gui::{self as nwg, Event};
use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW;
@@ -39,7 +40,12 @@ impl AppDelegate for AppWindow {
}
fn set_status_text(&mut self, text: &str) {
- self.status_text.set_text(text);
+ if !text.is_empty() {
+ self.status_text.set_visible(true);
+ self.status_text.set_text(text);
+ } else {
+ self.status_text.set_visible(false);
+ }
}
fn set_download_text(&mut self, text: &str) {
@@ -113,6 +119,44 @@ impl AppDelegate for AppWindow {
self.stable_message_frame.set_visible(false);
}
+ fn show_error_message(&mut self, error: ErrorMessage) {
+ self.error_view.error_text.set_text(&error.status_text);
+ self.error_view
+ .error_retry_button
+ .set_text(&error.retry_button_text);
+ self.error_view
+ .error_cancel_button
+ .set_text(&error.cancel_button_text);
+
+ self.error_view.error_frame.set_visible(true);
+ }
+
+ fn hide_error_message(&mut self) {
+ self.error_view.error_frame.set_visible(false);
+ }
+
+ fn on_error_message_retry<F>(&mut self, callback: F)
+ where
+ F: Fn() + Send + 'static,
+ {
+ register_click_handler(
+ self.error_view.error_frame.handle,
+ self.error_view.error_retry_button.handle,
+ callback,
+ );
+ }
+
+ fn on_error_message_cancel<F>(&mut self, callback: F)
+ where
+ F: Fn() + Send + 'static,
+ {
+ register_click_handler(
+ self.error_view.error_frame.handle,
+ self.error_view.error_cancel_button.handle,
+ callback,
+ );
+ }
+
fn quit(&mut self) {
nwg::stop_thread_dispatch();
}
diff --git a/installer-downloader/src/winapi_impl/ui.rs b/installer-downloader/src/winapi_impl/ui.rs
index bd5589beca..820972a914 100644
--- a/installer-downloader/src/winapi_impl/ui.rs
+++ b/installer-downloader/src/winapi_impl/ui.rs
@@ -1,6 +1,5 @@
//! This module handles setting up and rendering changes to the UI
-//use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::Rc;
@@ -22,6 +21,7 @@ use super::delegate::QueueContext;
static BANNER_IMAGE_DATA: &[u8] = include_bytes!("../../assets/logo-icon.png");
static BANNER_TEXT_IMAGE_DATA: &[u8] = include_bytes!("../../assets/logo-text.png");
+static ERROR_IMAGE_DATA: &[u8] = include_bytes!("../../assets/alert-circle.png");
const BACKGROUND_COLOR: [u8; 3] = [0x19, 0x2e, 0x45];
/// Beta link color: #003E92
@@ -68,6 +68,71 @@ pub struct AppWindow {
pub stable_message_frame: nwg::ImageFrame,
pub stable_prefix: nwg::Label,
pub stable_link: nwg::Label,
+
+ pub error_view: ErrorView,
+}
+
+#[derive(Default)]
+pub struct ErrorView {
+ pub error_frame: nwg::Frame,
+ pub error_text: nwg::Label,
+ pub error_icon: nwg::ImageFrame,
+ pub error_icon_bmp: nwg::Bitmap,
+ pub error_cancel_button: nwg::Button,
+ pub error_retry_button: nwg::Button,
+}
+
+impl ErrorView {
+ pub fn layout(&mut self, parent: &nwg::ControlHandle) -> Result<(), nwg::NwgError> {
+ nwg::Frame::builder()
+ .parent(parent)
+ .position((0, 102))
+ .size((WINDOW_WIDTH as i32, 204))
+ .flags(nwg::FrameFlags::empty())
+ .build(&mut self.error_frame)?;
+
+ nwg::Label::builder()
+ .parent(&self.error_frame)
+ .v_align(nwg::VTextAlign::Center)
+ .position((80, 45))
+ .size((488, 64))
+ .build(&mut self.error_text)?;
+
+ nwg::ImageFrame::builder()
+ .parent(&self.error_frame)
+ .size((32, 32))
+ .position((34, 49))
+ .build(&mut self.error_icon)?;
+
+ // TODO: put buttons 24px below bottom edge of text label
+ let text_bottom_y = 96; // TODO
+ let button_top_y = text_bottom_y + 24;
+
+ nwg::Button::builder()
+ .parent(&self.error_frame)
+ .position((304, button_top_y))
+ .size((232, 32))
+ .build(&mut self.error_cancel_button)?;
+
+ nwg::Button::builder()
+ .parent(&self.error_frame)
+ .position((64, button_top_y))
+ .size((232, 32))
+ .build(&mut self.error_retry_button)?;
+
+ self.load_error_icon()?;
+
+ Ok(())
+ }
+
+ /// Load the error icon and display it in `error_icon`
+ fn load_error_icon(&mut self) -> Result<(), nwg::NwgError> {
+ let src = ImageDecoder::new()?.from_stream(ERROR_IMAGE_DATA)?;
+ let frame = src.frame(0)?;
+ self.error_icon_bmp = frame.as_bitmap().unwrap();
+ self.error_icon.set_bitmap(Some(&self.error_icon_bmp));
+ Ok(())
+ }
}
impl AppWindow {
@@ -235,6 +300,8 @@ impl AppWindow {
self.window.set_visible(true);
+ self.error_view.layout(&self.window.handle)?;
+
let event_handle = self.window.handle;
let app = Rc::new(RefCell::new(self));
diff --git a/installer-downloader/tests/controller.rs b/installer-downloader/tests/controller.rs
index 8ac544b98a..4c8f94813b 100644
--- a/installer-downloader/tests/controller.rs
+++ b/installer-downloader/tests/controller.rs
@@ -6,7 +6,7 @@
use insta::assert_yaml_snapshot;
use installer_downloader::controller::{AppController, DirectoryProvider};
-use installer_downloader::delegate::{AppDelegate, AppDelegateQueue};
+use installer_downloader::delegate::{AppDelegate, AppDelegateQueue, ErrorMessage};
use installer_downloader::ui_downloader::UiAppDownloaderParameters;
use mullvad_update::api::VersionInfoProvider;
use mullvad_update::app::{AppDownloader, DownloadError};
@@ -151,6 +151,10 @@ pub struct FakeAppDelegate {
pub beta_callback: Option<Box<dyn Fn() + Send>>,
/// Callback registered by `on_stable_link`
pub stable_callback: Option<Box<dyn Fn() + Send>>,
+ /// Callback registered by `on_error_cancel`
+ pub error_cancel_callback: Option<Box<dyn Fn() + Send>>,
+ /// Callback registered by `on_error_retry`
+ pub error_retry_callback: Option<Box<dyn Fn() + Send>>,
/// State of delegate
pub state: DelegateState,
/// Queue used to simulate the main thread
@@ -170,6 +174,8 @@ pub struct DelegateState {
pub download_progress_visible: bool,
pub beta_text_visible: bool,
pub stable_text_visible: bool,
+ pub error_message_visible: bool,
+ pub error_message: ErrorMessage,
pub quit: bool,
/// Record of method calls.
pub call_log: Vec<String>,
@@ -301,6 +307,33 @@ impl AppDelegate for FakeAppDelegate {
self.state.stable_text_visible = false;
}
+ fn show_error_message(&mut self, message: ErrorMessage) {
+ self.state.call_log.push("show_error_message".into());
+ self.state.error_message = message;
+ self.state.error_message_visible = true;
+ }
+
+ fn hide_error_message(&mut self) {
+ self.state.call_log.push("hide_error_message".into());
+ self.state.error_message_visible = false;
+ }
+
+ fn on_error_message_cancel<F>(&mut self, callback: F)
+ where
+ F: Fn() + Send + 'static,
+ {
+ self.state.call_log.push("on_error_message_cancel".into());
+ self.error_cancel_callback = Some(Box::new(callback));
+ }
+
+ fn on_error_message_retry<F>(&mut self, callback: F)
+ where
+ F: Fn() + Send + 'static,
+ {
+ self.state.call_log.push("on_error_message_retry".into());
+ self.error_retry_callback = Some(Box::new(callback));
+ }
+
fn quit(&mut self) {
self.state.call_log.push("quit".into());
self.state.quit = true;
diff --git a/installer-downloader/tests/snapshots/controller__download-2.snap b/installer-downloader/tests/snapshots/controller__download-2.snap
index 390e172778..ee35e8f8ee 100644
--- a/installer-downloader/tests/snapshots/controller__download-2.snap
+++ b/installer-downloader/tests/snapshots/controller__download-2.snap
@@ -12,6 +12,12 @@ download_button_enabled: true
download_progress: 0
download_progress_visible: true
beta_text_visible: false
+stable_text_visible: false
+error_message_visible: false
+error_message:
+ status_text: ""
+ cancel_button_text: ""
+ retry_button_text: ""
quit: false
call_log:
- hide_download_progress
@@ -19,14 +25,21 @@ call_log:
- disable_download_button
- hide_cancel_button
- hide_beta_text
+ - hide_stable_text
- "set_status_text: Loading version details..."
- on_download
- on_cancel
+ - on_beta_link
+ - on_stable_link
- "set_status_text: Version: 2025.1"
- enable_download_button
+ - hide_error_message
+ - on_error_message_retry
+ - on_error_message_cancel
- "set_download_text: "
- hide_download_button
- hide_beta_text
+ - hide_stable_text
- show_cancel_button
- enable_cancel_button
- show_download_progress
diff --git a/installer-downloader/tests/snapshots/controller__download-3.snap b/installer-downloader/tests/snapshots/controller__download-3.snap
index 6d918cf341..5b40b79b9d 100644
--- a/installer-downloader/tests/snapshots/controller__download-3.snap
+++ b/installer-downloader/tests/snapshots/controller__download-3.snap
@@ -12,6 +12,12 @@ download_button_enabled: true
download_progress: 100
download_progress_visible: true
beta_text_visible: false
+stable_text_visible: false
+error_message_visible: false
+error_message:
+ status_text: ""
+ cancel_button_text: ""
+ retry_button_text: ""
quit: true
call_log:
- hide_download_progress
@@ -19,14 +25,21 @@ call_log:
- disable_download_button
- hide_cancel_button
- hide_beta_text
+ - hide_stable_text
- "set_status_text: Loading version details..."
- on_download
- on_cancel
+ - on_beta_link
+ - on_stable_link
- "set_status_text: Version: 2025.1"
- enable_download_button
+ - hide_error_message
+ - on_error_message_retry
+ - on_error_message_cancel
- "set_download_text: "
- hide_download_button
- hide_beta_text
+ - hide_stable_text
- show_cancel_button
- enable_cancel_button
- show_download_progress
diff --git a/installer-downloader/tests/snapshots/controller__download.snap b/installer-downloader/tests/snapshots/controller__download.snap
index ded40dd4a4..12a2423d91 100644
--- a/installer-downloader/tests/snapshots/controller__download.snap
+++ b/installer-downloader/tests/snapshots/controller__download.snap
@@ -13,6 +13,11 @@ download_progress: 0
download_progress_visible: false
beta_text_visible: false
stable_text_visible: false
+error_message_visible: false
+error_message:
+ status_text: ""
+ cancel_button_text: ""
+ retry_button_text: ""
quit: false
call_log:
- hide_download_progress
diff --git a/installer-downloader/tests/snapshots/controller__failed_directory_creation.snap b/installer-downloader/tests/snapshots/controller__failed_directory_creation.snap
index 2abc026735..b3f9705149 100644
--- a/installer-downloader/tests/snapshots/controller__failed_directory_creation.snap
+++ b/installer-downloader/tests/snapshots/controller__failed_directory_creation.snap
@@ -3,9 +3,9 @@ source: installer-downloader/tests/controller.rs
expression: delegate.state
snapshot_kind: text
---
-status_text: Failed to create download directory
+status_text: ""
download_text: ""
-download_button_visible: true
+download_button_visible: false
cancel_button_visible: false
cancel_button_enabled: false
download_button_enabled: true
@@ -13,6 +13,11 @@ download_progress: 0
download_progress_visible: false
beta_text_visible: false
stable_text_visible: false
+error_message_visible: true
+error_message:
+ status_text: Failed to create download directory
+ cancel_button_text: Cancel
+ retry_button_text: Try again
quit: false
call_log:
- hide_download_progress
@@ -28,4 +33,11 @@ call_log:
- on_stable_link
- "set_status_text: Version: 2025.1"
- enable_download_button
- - "set_status_text: Failed to create download directory"
+ - hide_error_message
+ - on_error_message_retry
+ - on_error_message_cancel
+ - "set_status_text: "
+ - hide_download_button
+ - hide_beta_text
+ - hide_stable_text
+ - show_error_message
diff --git a/installer-downloader/tests/snapshots/controller__failed_verification.snap b/installer-downloader/tests/snapshots/controller__failed_verification.snap
index acf41ff257..3bb0a7e130 100644
--- a/installer-downloader/tests/snapshots/controller__failed_verification.snap
+++ b/installer-downloader/tests/snapshots/controller__failed_verification.snap
@@ -3,16 +3,21 @@ source: installer-downloader/tests/controller.rs
expression: delegate.state
snapshot_kind: text
---
-status_text: "Version: 2025.1"
-download_text: "ERROR: Verification failed!"
+status_text: ""
+download_text: ""
download_button_visible: false
-cancel_button_visible: true
+cancel_button_visible: false
cancel_button_enabled: false
download_button_enabled: true
download_progress: 100
-download_progress_visible: true
+download_progress_visible: false
beta_text_visible: false
stable_text_visible: false
+error_message_visible: true
+error_message:
+ status_text: "Couldn’t verify download, please try downloading again or contact our support by sending an email at support@mullvadvpn.net"
+ cancel_button_text: Cancel
+ retry_button_text: Redownload
quit: false
call_log:
- hide_download_progress
@@ -28,6 +33,9 @@ call_log:
- on_stable_link
- "set_status_text: Version: 2025.1"
- enable_download_button
+ - hide_error_message
+ - on_error_message_retry
+ - on_error_message_cancel
- "set_download_text: "
- hide_download_button
- hide_beta_text
@@ -41,4 +49,9 @@ call_log:
- "set_download_text: Downloading from mullvad.net... (100%)"
- "set_download_text: Download complete. Verifying..."
- disable_cancel_button
- - "set_download_text: ERROR: Verification failed!"
+ - "set_status_text: "
+ - "set_download_text: "
+ - hide_download_progress
+ - hide_download_button
+ - hide_cancel_button
+ - show_error_message
diff --git a/installer-downloader/tests/snapshots/controller__fetch_version-2.snap b/installer-downloader/tests/snapshots/controller__fetch_version-2.snap
index 3d79a1922e..12a2423d91 100644
--- a/installer-downloader/tests/snapshots/controller__fetch_version-2.snap
+++ b/installer-downloader/tests/snapshots/controller__fetch_version-2.snap
@@ -12,6 +12,12 @@ download_button_enabled: true
download_progress: 0
download_progress_visible: false
beta_text_visible: false
+stable_text_visible: false
+error_message_visible: false
+error_message:
+ status_text: ""
+ cancel_button_text: ""
+ retry_button_text: ""
quit: false
call_log:
- hide_download_progress
@@ -19,8 +25,11 @@ call_log:
- disable_download_button
- hide_cancel_button
- hide_beta_text
+ - hide_stable_text
- "set_status_text: Loading version details..."
- on_download
- on_cancel
+ - on_beta_link
+ - on_stable_link
- "set_status_text: Version: 2025.1"
- enable_download_button
diff --git a/installer-downloader/tests/snapshots/controller__fetch_version.snap b/installer-downloader/tests/snapshots/controller__fetch_version.snap
index 17f1b954cf..eb10659291 100644
--- a/installer-downloader/tests/snapshots/controller__fetch_version.snap
+++ b/installer-downloader/tests/snapshots/controller__fetch_version.snap
@@ -13,6 +13,11 @@ download_progress: 0
download_progress_visible: false
beta_text_visible: false
stable_text_visible: false
+error_message_visible: false
+error_message:
+ status_text: ""
+ cancel_button_text: ""
+ retry_button_text: ""
quit: false
call_log:
- hide_download_progress