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
```

<img width="664" alt="image"
src="https://github.com/firezone/firezone/assets/13400041/d5def59c-b075-4135-91e5-85f9f9212fa5">

---------

Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
Reactor Scram
2024-03-19 09:55:03 -05:00
committed by GitHub
parent 09dbd70dc5
commit adea43e63b
9 changed files with 123 additions and 20 deletions

View File

@@ -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 <https://github.com/firezone/firezone/issues/2697> and <https://github.com/firezone/firezone/issues/2711>,
@@ -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<String> {
pub fn get() -> Result<DeviceId> {
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<String> {
.ok()
.and_then(|s| serde_json::from_str::<DeviceIdJson>(&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<String> {
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)]

View File

@@ -16,6 +16,7 @@ mod resolvers;
mod settings;
mod updates;
mod uptime;
mod welcome;
#[cfg(target_os = "windows")]
mod wintun_install;

View File

@@ -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<AdvancedSettings>),
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,

View File

@@ -82,12 +82,15 @@ pub(crate) async fn get_advanced_settings(
managed: tauri::State<'_, Managed>,
) -> Result<AdvancedSettings, String> {
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())
}

View File

@@ -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(())
}

View File

@@ -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
}
]
}

View File

@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="output.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to Firezone</title>
<script src="./flowbite.min.js" defer></script>
<script type="module" src="welcome.js" defer></script>
</head>
<body class="bg-neutral-100 text-neutral-900">
<div class="container mx-auto">
<div class="flex justify-center mt-8">
<h1 class="text-3xl font-bold">Welcome to Firezone.</h1>
</div>
<p class="mt-8 flex justify-center">Sign in below to get started.</p>
<img
src="logo.png"
alt="Firezone Logo"
class="mt-8 rounded-full w-48 h-48 mx-auto bg-white shadow border-2 border-black"
/>
<div class="flex justify-center mt-8">
<button
class="text-white bg-accent-450 hover:bg-accent-700 font-medium rounded text-lg w-full sm:w-auto px-5 py-2.5 text-center"
id="sign-in"
>
Sign in
</button>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
import "./tauri_stub.js";
const invoke = window.__TAURI__.tauri.invoke;
const signInBtn = <HTMLButtonElement>(
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());

View File

@@ -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();