Files
waggle-os/app/src-tauri/src/commands/onboarding.rs
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

121 lines
4.8 KiB
Rust

// CC Sesija A §2.3 Task A10 — first-launch detection via filesystem flag.
//
// Brief: briefs/2026-04-30-cc-sesija-A-waggle-apps-web-integration.md §2.3 Task A10
//
// Persists a flag file at `~/.waggle/first-launch.flag` so the onboarding wizard
// state survives across app reinstalls (browser localStorage doesn't, since
// Tauri builds may use a fresh WebView profile per install). The web `npm run
// dev` path continues to use localStorage via useOnboarding — these commands
// are the durable Tauri-mode addition, not a replacement.
//
// Cross-platform user-home resolution uses std::env (USERPROFILE on Windows,
// HOME on Unix) to avoid pulling in a new dirs/home crate dep.
use std::path::PathBuf;
const FLAG_DIR: &str = ".waggle";
const FLAG_FILE: &str = "first-launch.flag";
fn home_dir() -> Option<PathBuf> {
std::env::var_os("USERPROFILE")
.or_else(|| std::env::var_os("HOME"))
.map(PathBuf::from)
}
/// Resolve the flag location: `WAGGLE_DATA_DIR` (when set and non-empty) else
/// `~/.waggle` — the SAME resolution order the sidecar's resolveDataDir uses
/// (UX-Refactor P4/D11). Without this, a custom-data-dir install reads/writes
/// the flag in `~/.waggle` while the server's completion stamp lives in the
/// data dir — and a stale `~/.waggle` flag from a prior default install would
/// auto-skip onboarding against a brand-new data dir (P4 review, 5 findings).
fn flag_path() -> Result<PathBuf, String> {
if let Some(dir) = std::env::var_os("WAGGLE_DATA_DIR") {
if !dir.is_empty() {
return Ok(PathBuf::from(dir).join(FLAG_FILE));
}
}
let home = home_dir().ok_or_else(|| {
"could not resolve user home directory (USERPROFILE/HOME unset)".to_string()
})?;
Ok(home.join(FLAG_DIR).join(FLAG_FILE))
}
/// Returns true if the user has not yet completed onboarding.
/// Implementation: returns `!flag_file_exists`. On any IO error (e.g. home dir
/// unresolvable in a sandboxed environment) returns `true` so the wizard runs
/// — better to show the wizard once too often than to silently skip it.
#[tauri::command]
pub async fn is_first_launch() -> Result<bool, String> {
let path = match flag_path() {
Ok(p) => p,
Err(_) => return Ok(true),
};
Ok(!path.exists())
}
/// Marks onboarding as complete by creating the flag file. Idempotent.
/// Creates the parent `~/.waggle/` directory if needed.
#[tauri::command]
pub async fn mark_first_launch_complete() -> Result<(), String> {
let path = flag_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create_dir_all {} failed: {}", parent.display(), e))?;
}
std::fs::write(&path, b"completed\n")
.map_err(|e| format!("write {} failed: {}", path.display(), e))?;
Ok(())
}
/// Resets the first-launch flag (deletes the file). For dev / QA flows that
/// need to re-trigger onboarding without a full reinstall.
#[tauri::command]
pub async fn reset_first_launch() -> Result<(), String> {
let path = flag_path()?;
if path.exists() {
std::fs::remove_file(&path)
.map_err(|e| format!("remove {} failed: {}", path.display(), e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_path_resolution_order() {
// ONE sequential test — env vars are process-global and cargo runs
// tests in parallel threads; split tests would race on WAGGLE_DATA_DIR.
let tmp = std::env::temp_dir().join(format!("waggle-test-home-{}", std::process::id()));
std::env::set_var("USERPROFILE", &tmp);
std::env::set_var("HOME", &tmp);
// Default: ~/.waggle.
std::env::remove_var("WAGGLE_DATA_DIR");
let path = flag_path().expect("flag_path resolves with USERPROFILE/HOME set");
assert!(
path.ends_with(".waggle/first-launch.flag")
|| path.ends_with(".waggle\\first-launch.flag")
);
// P4/D11: a custom data dir keeps the flag NEXT TO the server's stamp.
let data_dir =
std::env::temp_dir().join(format!("waggle-test-datadir-{}", std::process::id()));
std::env::set_var("WAGGLE_DATA_DIR", &data_dir);
let custom = flag_path().expect("flag_path resolves with WAGGLE_DATA_DIR set");
assert!(custom.starts_with(&data_dir));
assert!(custom.ends_with("first-launch.flag"));
// Empty env value falls through to the home default (matches resolveDataDir).
std::env::set_var("WAGGLE_DATA_DIR", "");
let fallback = flag_path().expect("flag_path resolves with empty WAGGLE_DATA_DIR");
assert!(
fallback.ends_with(".waggle/first-launch.flag")
|| fallback.ends_with(".waggle\\first-launch.flag")
);
std::env::remove_var("WAGGLE_DATA_DIR");
}
}