From a8f56ee00fe76f64906236644421a2e333e8dae5 Mon Sep 17 00:00:00 2001 From: samialabri Date: Sat, 24 Oct 2026 15:29:50 +0700 Subject: [PATCH] =?UTF-8?q?Sign=20in=20with=20Google,=20Apple=E2=80=A6:=20?= =?UTF-8?q?remember=20prompt,=20item=20row,=20new-login=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src-tauri/examples/test-host.rs | 1 + src-tauri/src/main.rs | 30 ++++++++++++++++++++++++++++++ src-tauri/tests/live_sync.rs | 1 + src-tauri/tests/nmh_stdio.rs | 1 + src/main.ts | 24 +++++++++++++++++++++--- 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src-tauri/examples/test-host.rs b/src-tauri/examples/test-host.rs index 4dd0cc8..da527f8 100644 --- a/src-tauri/examples/test-host.rs +++ b/src-tauri/examples/test-host.rs @@ -41,6 +41,7 @@ fn main() -> Result<(), Box> { notes: None, favorite: false, tags: vec![], + sign_in_with: None, modified_at: 0, })?; let broker = Broker::new(core, Arc::new(Quiet), PairingStore::in_memory()); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bb57a1c..ddc7a1a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -81,6 +81,22 @@ struct Detail { totp: Option, notes: Option, tags: Vec, + /// "Google · sami@example.com" when the account signs in with a provider. + sign_in_with: Option, +} + +#[derive(Serialize, Deserialize)] +struct SignInView { + provider: String, + #[serde(default)] + provider_name: String, + account: Option, +} + +#[derive(Serialize)] +struct ProviderView { + id: String, + name: String, } #[derive(Deserialize)] @@ -92,6 +108,8 @@ struct NewLogin { totp: Option, #[serde(default)] tags: Vec, + #[serde(default)] + sign_in_with: Option, } #[derive(Serialize)] @@ -249,9 +267,19 @@ fn get_item(app: State<'_, App>, id: String) -> CmdResult { totp: d.totp, notes: d.notes, tags: d.tags, + sign_in_with: d.sign_in_with.map(|w| SignInView { + provider_name: kv_ffi::sign_in_provider_name(w.provider.clone()), + provider: w.provider, + account: w.account, + }), }) } +#[tauri::command] +fn sign_in_providers() -> Vec { + kv_ffi::sign_in_providers().into_iter().map(|p| ProviderView { id: p.id, name: p.name }).collect() +} + /// Replaces an item's tags (the core normalises them); everything else about the item stays. #[tauri::command] fn set_tags(app: State<'_, App>, id: String, tags: Vec) -> CmdResult<()> { @@ -276,6 +304,7 @@ fn save_item(app: State<'_, App>, item: NewLogin) -> CmdResult { notes: None, favorite: false, tags: item.tags, + sign_in_with: item.sign_in_with.map(|w| kv_ffi::SignInWithDto { provider: w.provider, account: w.account }), modified_at: 0, }) .map_err(err) @@ -701,6 +730,7 @@ fn main() { pending_saves, pending_passkeys, set_tags, + sign_in_providers, resolve_passkey, resolve_save, sync_status, diff --git a/src-tauri/tests/live_sync.rs b/src-tauri/tests/live_sync.rs index 957741d..f36d4c8 100644 --- a/src-tauri/tests/live_sync.rs +++ b/src-tauri/tests/live_sync.rs @@ -38,6 +38,7 @@ fn login(title: &str) -> ItemDto { notes: None, favorite: false, tags: vec![], + sign_in_with: None, modified_at: 0, } } diff --git a/src-tauri/tests/nmh_stdio.rs b/src-tauri/tests/nmh_stdio.rs index 3e360e8..abacc73 100644 --- a/src-tauri/tests/nmh_stdio.rs +++ b/src-tauri/tests/nmh_stdio.rs @@ -90,6 +90,7 @@ fn pair_and_fill_through_the_native_host() { notes: None, favorite: false, tags: vec![], + sign_in_with: None, modified_at: 0, }) .unwrap(); diff --git a/src/main.ts b/src/main.ts index 3ab3d9d..2e0764b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,10 +10,11 @@ import { interface AppState { has_account: boolean; locked: boolean; remembers_secret_key: boolean; ipc_error: string | null; idle_minutes: number } interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean; has_totp: boolean; tags: string[] } -interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null; tags: string[] } +interface SignInView { provider: string; provider_name: string; account: string | null } +interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null; tags: string[]; sign_in_with: SignInView | null } interface PairingView { id: string; name: string; caller: string; words: string[] } interface Pairing { id: string; name: string; caller: string; words: string[]; created_at: number } -interface SaveView { id: string; origin: string; username: string; update: boolean } +interface SaveView { id: string; origin: string; username: string; update: boolean; provider: string | null } interface PasskeyChoice { credential_id: string; user_name: string; title: string } interface PasskeyView { id: string; kind: "get" | "create"; origin: string; rp_id: string; rp_name: string; user_name: string; choices: PasskeyChoice[]; locked: boolean } interface SyncStatus { @@ -630,6 +631,10 @@ async function renderDetail(target: HTMLElement, id: string) { if (item.username) { rows.push(fieldRow("Username", h("span", "", item.username), copyButton("Copy username", () => invoke("copy_secret", { id, field: "username" })))); } + if (item.sign_in_with) { + const w = item.sign_in_with; + rows.push(fieldRow("Sign in with", h("span", "", w.account ? `${w.provider_name} · ${w.account}` : w.provider_name))); + } if (item.password) { const value = h("span", "", dots("Password")); let shown = false; @@ -713,13 +718,17 @@ function renderEditor(target: HTMLElement) { const url = input({ type: "url", placeholder: "https://example.com", spellcheck: false }); const totp = input({ placeholder: "otpauth://totp/… or base32 secret" }, true); const tags = input({ placeholder: "work, finance", spellcheck: false }); + const provider = h("select", "kv-field", Object.assign(h("option", "", "Password only"), { value: "" })); + void invoke<{ id: string; name: string }[]>("sign_in_providers").then((ps) => provider.append(...ps.map((x) => Object.assign(h("option", "", x.name), { value: x.id })))); + const account = input({ placeholder: "Account used with the provider (optional)", spellcheck: false }); const gen = button("Generate", "quiet", async () => { pass.value = await invoke("generate_password", { length: 24 }); }); const err = errBox(); const save = button("Save", "primary"); save.onclick = async () => { try { await invoke("save_item", { - item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [], totp: totp.value || null, tags: parseTags(tags.value) }, + item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [], totp: totp.value || null, tags: parseTags(tags.value), + sign_in_with: provider.value ? { provider: provider.value, account: account.value.trim() || null } : null }, }); await render(); } catch (e) { @@ -731,6 +740,7 @@ function renderEditor(target: HTMLElement) { field("Title", title), field("Username", user), h("label", "kv-fieldset", h("span", "kv-label", "Password"), h("div", "row", pass, gen)), field("Website", url), field("One-time code", totp, "Optional."), field("Tags", tags, "Optional. Separate tags with commas."), + field("Sign in with", provider, "For sites where you use Google, Apple… instead of a password."), field("Provider account", account), err, h("div", "actions", save, button("Cancel", "secondary", () => target.replaceChildren(emptyDetail()))))); title.focus(); } @@ -1115,6 +1125,14 @@ function saveCard(s: SaveView): HTMLElement { }; yes.onclick = () => resolve(true); no.onclick = () => resolve(false); + if (s.provider) { + yes.textContent = "Remember"; + return h("div", "kv-sheet prompt", + h("h2", "kv-title-2", `Signed in with ${s.provider}?`), + p("", `Remember that you sign in to ${s.origin.replace(/^https?:\/\/(www\.)?/, "")} with ${s.provider}${s.username ? ` (${s.username})` : ""}. Kestrove will offer it next time.`), + err, + h("div", "actions end", no, yes)); + } return h("div", "kv-sheet prompt", h("h2", "kv-title-2", s.update ? "Update saved password?" : "Save this login?"), h("div", "kv-card rows", h("div", "kv-frow", tile(s.origin.replace(/^https?:\/\/(www\.)?/, "")),