From adea43e63b9e279c7672159d32e65a452b4c610d Mon Sep 17 00:00:00 2001 From: Reactor Scram Date: Tue, 19 Mar 2024 09:55:03 -0500 Subject: [PATCH] feat(gui-client): Tauri welcome screen (#4013) Closes #3961 No tests yet, might be tricky to test since it's all I/O. I cued it off the device ID being generated, so it will have a minor merge conflict with #3920 ```[tasklist] ### Before merging - [ ] UI polish, or disable the welcome screen temporarily ``` image --------- Co-authored-by: Jamil Bou Kheir --- rust/connlib/shared/src/device_id.rs | 26 +++++++++++---- rust/gui-client/src-tauri/src/client.rs | 1 + rust/gui-client/src-tauri/src/client/gui.rs | 32 +++++++++++++----- .../src-tauri/src/client/settings.rs | 7 ++-- .../src-tauri/src/client/welcome.rs | 12 +++++++ rust/gui-client/src-tauri/tauri.conf.json | 12 ++++++- rust/gui-client/src/welcome.html | 33 +++++++++++++++++++ rust/gui-client/src/welcome.ts | 18 ++++++++++ rust/linux-client/src/main.rs | 2 +- 9 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 rust/gui-client/src-tauri/src/client/welcome.rs create mode 100644 rust/gui-client/src/welcome.html create mode 100644 rust/gui-client/src/welcome.ts diff --git a/rust/connlib/shared/src/device_id.rs b/rust/connlib/shared/src/device_id.rs index 3b7eab95b..9116cf23e 100644 --- a/rust/connlib/shared/src/device_id.rs +++ b/rust/connlib/shared/src/device_id.rs @@ -2,6 +2,12 @@ use anyhow::{Context, Result}; use std::fs; use std::io::Write; +pub struct DeviceId { + /// True iff the device ID was not found on disk and we had to generate it, meaning this is the app's first run since installing. + pub is_first_time: bool, + pub id: String, +} + /// Returns the device ID, generating it and saving it to disk if needed. /// /// Per and , @@ -10,7 +16,7 @@ use std::io::Write; /// Returns: The UUID as a String, suitable for sending verbatim to `connlib_client_shared::Session::connect`. /// /// Errors: If the disk is unwritable when initially generating the ID, or unwritable when re-generating an invalid ID. -pub fn get() -> Result { +pub fn get() -> Result { let dir = imp::path().context("Failed to compute path for firezone-id file")?; let path = dir.join("firezone-id.json"); @@ -19,9 +25,12 @@ pub fn get() -> Result { .ok() .and_then(|s| serde_json::from_str::(&s).ok()) { - let device_id = j.device_id(); - tracing::debug!(?device_id, "Loaded device ID from disk"); - return Ok(device_id); + let id = j.device_id(); + tracing::debug!(?id, "Loaded device ID from disk"); + return Ok(DeviceId { + is_first_time: false, + id, + }); } // Couldn't read, it's missing or invalid, generate a new one and save it. @@ -39,9 +48,12 @@ pub fn get() -> Result { file.write(|f| f.write_all(content.as_bytes())) .context("Failed to write firezone-id file")?; - let device_id = j.device_id(); - tracing::debug!(?device_id, "Saved device ID to disk"); - Ok(j.device_id()) + let id = j.device_id(); + tracing::debug!(?id, "Saved device ID to disk"); + Ok(DeviceId { + is_first_time: true, + id, + }) } #[derive(serde::Deserialize, serde::Serialize)] diff --git a/rust/gui-client/src-tauri/src/client.rs b/rust/gui-client/src-tauri/src/client.rs index b98a3ddc9..404a56e4e 100644 --- a/rust/gui-client/src-tauri/src/client.rs +++ b/rust/gui-client/src-tauri/src/client.rs @@ -16,6 +16,7 @@ mod resolvers; mod settings; mod updates; mod uptime; +mod welcome; #[cfg(target_os = "windows")] mod wintun_install; diff --git a/rust/gui-client/src-tauri/src/client/gui.rs b/rust/gui-client/src-tauri/src/client/gui.rs index fb717eaf9..bf0f2d75f 100644 --- a/rust/gui-client/src-tauri/src/client/gui.rs +++ b/rust/gui-client/src-tauri/src/client/gui.rs @@ -8,7 +8,7 @@ use crate::client::{ settings::{self, AdvancedSettings}, Failure, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{bail, Context, Result}; use arc_swap::ArcSwap; use connlib_client_shared::{file_logger, ResourceDescription}; use connlib_shared::{keypair, messages::ResourceId, LoginUrl, BUNDLE_ID}; @@ -209,6 +209,7 @@ pub(crate) fn run(cli: &client::Cli) -> Result<(), Error> { settings::apply_advanced_settings, settings::reset_advanced_settings, settings::get_advanced_settings, + crate::client::welcome::sign_in, ]) .system_tray(tray) .on_system_tray_event(|app, event| { @@ -430,6 +431,7 @@ pub(crate) enum ControllerRequest { Fail(Failure), GetAdvancedSettings(oneshot::Sender), SchemeRequest(SecretString), + SignIn, SystemTrayMenu(TrayMenuEvent), TunnelReady, UpdateAvailable(client::updates::Release), @@ -618,6 +620,17 @@ impl Controller { .handle_deep_link(&url) .await .context("Couldn't handle deep link")?, + Req::SignIn | Req::SystemTrayMenu(TrayMenuEvent::SignIn) => { + if let Some(req) = self.auth.start_sign_in()? { + let url = req.to_url(&self.advanced_settings.auth_base_url); + self.refresh_system_tray_menu()?; + os::open_url(&self.app, &url)?; + self.app + .get_window("welcome") + .context("Couldn't get handle to Welcome window")? + .hide()?; + } + } Req::SystemTrayMenu(TrayMenuEvent::CancelSignIn) => { if self.session.is_some() { // If the user opened the menu, then sign-in completed, then they @@ -648,13 +661,6 @@ impl Controller { Req::SystemTrayMenu(TrayMenuEvent::Resource { id }) => self .copy_resource(&id) .context("Couldn't copy resource to clipboard")?, - Req::SystemTrayMenu(TrayMenuEvent::SignIn) => { - if let Some(req) = self.auth.start_sign_in()? { - let url = req.to_url(&self.advanced_settings.auth_base_url); - self.refresh_system_tray_menu()?; - os::open_url(&self.app, &url)?; - } - } Req::SystemTrayMenu(TrayMenuEvent::SignOut) => { tracing::info!("User asked to sign out"); self.sign_out()?; @@ -759,7 +765,7 @@ impl Controller { let win = self .app .get_window(id) - .ok_or_else(|| anyhow!("getting handle to `{id}` window"))?; + .context("Couldn't get handle to `{id}` window")?; win.show()?; win.unminimize()?; @@ -779,6 +785,14 @@ async fn run_controller( let device_id = connlib_shared::device_id::get().context("Failed to read / create device ID")?; + if device_id.is_first_time { + let win = app + .get_window("welcome") + .context("Couldn't get handle to Welcome window")?; + win.show()?; + } + let device_id = device_id.id; + let mut controller = Controller { advanced_settings, app, diff --git a/rust/gui-client/src-tauri/src/client/settings.rs b/rust/gui-client/src-tauri/src/client/settings.rs index 6299d321a..99302bd92 100644 --- a/rust/gui-client/src-tauri/src/client/settings.rs +++ b/rust/gui-client/src-tauri/src/client/settings.rs @@ -82,12 +82,15 @@ pub(crate) async fn get_advanced_settings( managed: tauri::State<'_, Managed>, ) -> Result { let (tx, rx) = oneshot::channel(); - if let Err(e) = managed + if let Err(error) = managed .ctlr_tx .send(ControllerRequest::GetAdvancedSettings(tx)) .await { - tracing::error!("couldn't request advanced settings from controller task: {e}"); + tracing::error!( + ?error, + "couldn't request advanced settings from controller task" + ); } Ok(rx.await.unwrap()) } diff --git a/rust/gui-client/src-tauri/src/client/welcome.rs b/rust/gui-client/src-tauri/src/client/welcome.rs new file mode 100644 index 000000000..607e80130 --- /dev/null +++ b/rust/gui-client/src-tauri/src/client/welcome.rs @@ -0,0 +1,12 @@ +//! Everything related to the Welcome window + +use crate::client::gui::{ControllerRequest, Managed}; + +// Tauri requires a `Result` here, maybe in case the managed state can't be retrieved +#[tauri::command] +pub(crate) async fn sign_in(managed: tauri::State<'_, Managed>) -> anyhow::Result<(), String> { + if let Err(error) = managed.ctlr_tx.send(ControllerRequest::SignIn).await { + tracing::error!(?error, "Couldn't request `Controller` to begin signing in"); + } + Ok(()) +} diff --git a/rust/gui-client/src-tauri/tauri.conf.json b/rust/gui-client/src-tauri/tauri.conf.json index 19bef7deb..62f78da1e 100644 --- a/rust/gui-client/src-tauri/tauri.conf.json +++ b/rust/gui-client/src-tauri/tauri.conf.json @@ -65,7 +65,17 @@ "resizable": true, "width": 640, "height": 480, - "visible": true + "visible": false + }, + { + "label": "welcome", + "title": "Welcome", + "url": "welcome.html", + "fullscreen": false, + "resizable": true, + "width": 640, + "height": 480, + "visible": false } ] } diff --git a/rust/gui-client/src/welcome.html b/rust/gui-client/src/welcome.html new file mode 100644 index 000000000..6cf4e813e --- /dev/null +++ b/rust/gui-client/src/welcome.html @@ -0,0 +1,33 @@ + + + + + + + Welcome to Firezone + + + + + +
+
+

Welcome to Firezone.

+
+

Sign in below to get started.

+ Firezone Logo +
+ +
+
+ + diff --git a/rust/gui-client/src/welcome.ts b/rust/gui-client/src/welcome.ts new file mode 100644 index 000000000..300251fc8 --- /dev/null +++ b/rust/gui-client/src/welcome.ts @@ -0,0 +1,18 @@ +import "./tauri_stub.js"; + +const invoke = window.__TAURI__.tauri.invoke; + +const signInBtn = ( + document.getElementById("sign-in") +); + +async function sign_in() { + console.log("Signing in..."); + invoke("sign_in") + .then(() => {}) + .catch((e: Error) => { + console.error(e); + }); +} + +signInBtn.addEventListener("click", (e) => sign_in()); diff --git a/rust/linux-client/src/main.rs b/rust/linux-client/src/main.rs index 09e308e81..5a393dd66 100644 --- a/rust/linux-client/src/main.rs +++ b/rust/linux-client/src/main.rs @@ -28,7 +28,7 @@ async fn main() -> Result<()> { // AKA "Device ID", not the Firezone slug let firezone_id = match cli.firezone_id { Some(id) => id, - None => connlib_shared::device_id::get().context("Could not get `firezone_id` from CLI, could not read it from disk, could not generate it and save it to disk")?, + None => connlib_shared::device_id::get().context("Could not get `firezone_id` from CLI, could not read it from disk, could not generate it and save it to disk")?.id, }; let (private_key, public_key) = keypair();