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]
pub async fn start_bridge(
email: Option<String>,
password: Option<String>,
state: State<'_, BridgeState>,
) -> Result<(), String> {
let mut cfg = match config::load_config() {
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)
+1
View File
@@ -70,6 +70,7 @@ function App() {
<Dashboard
status={bridge.status}
stats={bridge.stats}
config={bridge.config}
hasSavedSession={bridge.hasSavedSession}
loading={bridge.loading}
logs={bridge.logs}
+38 -11
View File
@@ -1,14 +1,15 @@
import { useState, useEffect, useRef } from "react";
import type { BridgeStatus, BridgeStats } from "../types";
import type { Config, BridgeStatus, BridgeStats } from "../types";
import { isError } from "../types";
interface Props {
status: BridgeStatus | null;
stats: BridgeStats;
config: Config | null;
hasSavedSession: boolean;
loading: boolean;
logs: string[];
onStart: (password?: string) => Promise<void>;
onStart: (password?: string, email?: string) => Promise<void>;
onStop: () => Promise<void>;
onClearLogs: () => void;
}
@@ -24,6 +25,7 @@ function formatUptime(secs: number): string {
export function Dashboard({
status,
stats,
config,
hasSavedSession,
loading,
logs,
@@ -32,6 +34,7 @@ export function Dashboard({
onClearLogs,
}: Props) {
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const logEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -50,6 +53,8 @@ export function Dashboard({
const isStarting = status === "Starting";
const errored = isError(status);
const isStopped = status === "Stopped" || errored;
const hasAccount = !!config?.email;
const needsEmail = isStopped && !hasAccount;
const needsPassword = isStopped && !hasSavedSession;
const wsConnected = stats.ws_status === "Connected";
@@ -76,15 +81,16 @@ export function Dashboard({
? "Signing in and syncing your mailbox"
: errored
? "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 () => {
if (needsPassword) {
await onStart(password);
setPassword("");
} else {
await onStart();
}
await onStart(
needsPassword ? password : undefined,
needsEmail ? email.trim() : undefined,
);
setPassword("");
};
return (
@@ -106,6 +112,18 @@ export function Dashboard({
{isStopped && (
<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 && (
<div className="form-group">
<label>Tuta Password</label>
@@ -114,14 +132,23 @@ export function Dashboard({
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your Tuta password"
onKeyDown={(e) => e.key === "Enter" && password && handleStart()}
onKeyDown={(e) =>
e.key === "Enter" &&
password &&
(!needsEmail || email.trim()) &&
handleStart()
}
/>
</div>
)}
<button
className="primary start-btn"
onClick={handleStart}
disabled={loading || (needsPassword && !password)}
disabled={
loading ||
(needsPassword && !password) ||
(needsEmail && !email.trim())
}
>
{loading ? "Connecting…" : "Start Bridge"}
</button>
+2 -2
View File
@@ -104,10 +104,10 @@ export function useBridge() {
setConfig(cfg);
}, []);
const startBridge = useCallback(async (password?: string) => {
const startBridge = useCallback(async (password?: string, email?: string) => {
setLoading(true);
try {
await invoke("start_bridge", { password: password || null });
await invoke("start_bridge", { password: password || null, email: email || null });
refresh();
invoke<string | null>("get_bridge_password").then(setBridgePassword);
} catch (e) {