GUI: fix first-run onboarding when no account exists

The dashboard only ever showed a password field, and start_bridge errored
with "No config found" when nothing was configured yet, so a brand-new user
could never get past the start screen. Now the dashboard shows a Tuta email
field too when no account is set up, and start_bridge bootstraps the config
from that email (with defaults) instead of failing. Once an account exists
the email field disappears and only the password is asked (until a keyring
session is saved).
This commit is contained in:
Anthony
2026-06-12 20:17:29 +02:00
parent e3132bb9f5
commit 389a46d91c
4 changed files with 56 additions and 14 deletions
+15 -1
View File
@@ -42,12 +42,26 @@ pub async fn has_saved_session() -> Result<bool, String> {
#[tauri::command] #[tauri::command]
pub async fn start_bridge( pub async fn start_bridge(
email: Option<String>,
password: Option<String>, password: Option<String>,
state: State<'_, BridgeState>, state: State<'_, BridgeState>,
) -> Result<(), String> { ) -> Result<(), String> {
let mut cfg = match config::load_config() { let mut cfg = match config::load_config() {
Ok(Some(cfg)) if !cfg.email.is_empty() => cfg, Ok(Some(cfg)) if !cfg.email.is_empty() => cfg,
_ => return Err("No config found — save config first".into()), // First run: no account yet. Bootstrap a default config from the email
// the user just entered on the dashboard, instead of erroring out.
_ => {
let email = email.unwrap_or_default().trim().to_string();
if email.is_empty() {
return Err("Enter your Tuta email to get started".into());
}
let cfg = Config {
email,
..Default::default()
};
config::save_config(&cfg).map_err(|e| format!("Failed to save config: {e}"))?;
cfg
}
}; };
config::ensure_bridge_password(&mut cfg) config::ensure_bridge_password(&mut cfg)
+1
View File
@@ -70,6 +70,7 @@ function App() {
<Dashboard <Dashboard
status={bridge.status} status={bridge.status}
stats={bridge.stats} stats={bridge.stats}
config={bridge.config}
hasSavedSession={bridge.hasSavedSession} hasSavedSession={bridge.hasSavedSession}
loading={bridge.loading} loading={bridge.loading}
logs={bridge.logs} logs={bridge.logs}
+38 -11
View File
@@ -1,14 +1,15 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import type { BridgeStatus, BridgeStats } from "../types"; import type { Config, BridgeStatus, BridgeStats } from "../types";
import { isError } from "../types"; import { isError } from "../types";
interface Props { interface Props {
status: BridgeStatus | null; status: BridgeStatus | null;
stats: BridgeStats; stats: BridgeStats;
config: Config | null;
hasSavedSession: boolean; hasSavedSession: boolean;
loading: boolean; loading: boolean;
logs: string[]; logs: string[];
onStart: (password?: string) => Promise<void>; onStart: (password?: string, email?: string) => Promise<void>;
onStop: () => Promise<void>; onStop: () => Promise<void>;
onClearLogs: () => void; onClearLogs: () => void;
} }
@@ -24,6 +25,7 @@ function formatUptime(secs: number): string {
export function Dashboard({ export function Dashboard({
status, status,
stats, stats,
config,
hasSavedSession, hasSavedSession,
loading, loading,
logs, logs,
@@ -32,6 +34,7 @@ export function Dashboard({
onClearLogs, onClearLogs,
}: Props) { }: Props) {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const logEndRef = useRef<HTMLDivElement>(null); const logEndRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@@ -50,6 +53,8 @@ export function Dashboard({
const isStarting = status === "Starting"; const isStarting = status === "Starting";
const errored = isError(status); const errored = isError(status);
const isStopped = status === "Stopped" || errored; const isStopped = status === "Stopped" || errored;
const hasAccount = !!config?.email;
const needsEmail = isStopped && !hasAccount;
const needsPassword = isStopped && !hasSavedSession; const needsPassword = isStopped && !hasSavedSession;
const wsConnected = stats.ws_status === "Connected"; const wsConnected = stats.ws_status === "Connected";
@@ -76,15 +81,16 @@ export function Dashboard({
? "Signing in and syncing your mailbox" ? "Signing in and syncing your mailbox"
: errored : errored
? "See the activity log below" ? "See the activity log below"
: "Start the bridge to connect your mail client"; : needsEmail
? "Sign in with your Tuta account to get started"
: "Start the bridge to connect your mail client";
const handleStart = async () => { const handleStart = async () => {
if (needsPassword) { await onStart(
await onStart(password); needsPassword ? password : undefined,
setPassword(""); needsEmail ? email.trim() : undefined,
} else { );
await onStart(); setPassword("");
}
}; };
return ( return (
@@ -106,6 +112,18 @@ export function Dashboard({
{isStopped && ( {isStopped && (
<div className="start-section"> <div className="start-section">
{needsEmail && (
<div className="form-group">
<label>Tuta Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@tuta.com"
autoFocus
/>
</div>
)}
{needsPassword && ( {needsPassword && (
<div className="form-group"> <div className="form-group">
<label>Tuta Password</label> <label>Tuta Password</label>
@@ -114,14 +132,23 @@ export function Dashboard({
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your Tuta password" placeholder="Enter your Tuta password"
onKeyDown={(e) => e.key === "Enter" && password && handleStart()} onKeyDown={(e) =>
e.key === "Enter" &&
password &&
(!needsEmail || email.trim()) &&
handleStart()
}
/> />
</div> </div>
)} )}
<button <button
className="primary start-btn" className="primary start-btn"
onClick={handleStart} onClick={handleStart}
disabled={loading || (needsPassword && !password)} disabled={
loading ||
(needsPassword && !password) ||
(needsEmail && !email.trim())
}
> >
{loading ? "Connecting…" : "Start Bridge"} {loading ? "Connecting…" : "Start Bridge"}
</button> </button>
+2 -2
View File
@@ -104,10 +104,10 @@ export function useBridge() {
setConfig(cfg); setConfig(cfg);
}, []); }, []);
const startBridge = useCallback(async (password?: string) => { const startBridge = useCallback(async (password?: string, email?: string) => {
setLoading(true); setLoading(true);
try { try {
await invoke("start_bridge", { password: password || null }); await invoke("start_bridge", { password: password || null, email: email || null });
refresh(); refresh();
invoke<string | null>("get_bridge_password").then(setBridgePassword); invoke<string | null>("get_bridge_password").then(setBridgePassword);
} catch (e) { } catch (e) {