Sign in with Google, Apple…: remember prompt, item row, new-login picker
ci / build (ubuntu-22.04) (push) Canceled after 0s
ci / build (windows-2025) (push) Canceled after 0s
ci / deny (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-24 15:29:50 +07:00
co-authored by Claude Opus 5.5
parent e2a3c47825
commit a8f56ee00f
5 changed files with 54 additions and 3 deletions
+21 -3
View File
@@ -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<string>("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\.)?/, "")),