From c7e498fd9a7169fcbff49daf66d30fab02c35b74 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Wed, 7 Oct 2026 10:05:36 +0100 Subject: [PATCH 1/3] Lay out the Account page like the rest of the app Rebuild the Account page from the Home page's status band and grouped rows: a band with the signed-in email (or Sign in / Check your email while signing in) and Sign out; rows for Settings sync and What syncs; and Delete account in its own card with a danger-styled inline confirmation. Rows whose choice is two buttons put them under the text. Behaviour, focus handling and announcements are unchanged. Refs #997 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/App.test.tsx | 2 +- src/settings/AccountSection.test.tsx | 16 ++- src/settings/AccountSection.tsx | 169 +++++++++++++++++---------- src/ui/app.css | 12 ++ 4 files changed, 131 insertions(+), 68 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 32587cea..f16d5827 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -621,7 +621,7 @@ describe("Switchify PC shell", () => { const entry = await (await sidebar()).findByRole("button", { name: /^Account, signed in/ }); fireEvent.click(entry); expect(await screen.findByRole("heading", { name: "Account", level: 1 })).toBeInTheDocument(); - expect(await screen.findByText(/Signed in as/)).toBeInTheDocument(); + expect(await screen.findByRole("heading", { name: "me@example.com" })).toBeInTheDocument(); expect(entry).toHaveAttribute("aria-current", "page"); expect(screen.queryByRole("tablist", { name: "Settings sections" })).not.toBeInTheDocument(); }); diff --git a/src/settings/AccountSection.test.tsx b/src/settings/AccountSection.test.tsx index 3326417a..f106b40a 100644 --- a/src/settings/AccountSection.test.tsx +++ b/src/settings/AccountSection.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { api } from "../api"; import type { AccountView, SyncView } from "../types"; @@ -71,17 +71,20 @@ describe("AccountSection", () => { expect(await screen.findByText("Could not reach Switchify to sync settings.")).toBeInTheDocument(); }); - it("is hidden while confirming account deletion", async () => { + it("stays in its own row while deletion is confirmed in another", async () => { start(signedIn); await screen.findByRole("button", { name: "Sync now" }); fireEvent.click(screen.getByRole("button", { name: "Delete account…" })); - expect(screen.queryByRole("button", { name: "Sync now" })).not.toBeInTheDocument(); + const confirm = screen.getByRole("group", { name: "Confirm account deletion" }); + expect(within(confirm).getByRole("button", { name: "Delete permanently" })).toBeInTheDocument(); + expect(within(confirm).queryByRole("button", { name: "Sync now" })).not.toBeInTheDocument(); + expect(within(screen.getByRole("region", { name: "Settings sync" })).getByRole("button", { name: "Sync now" })).toBeInTheDocument(); }); }); it("says when accounts are unavailable in this build", async () => { start({ ...signedOut, available: false }); - expect(await screen.findByText("Accounts are unavailable in this build.")).toBeInTheDocument(); + expect(await screen.findByRole("heading", { name: "Accounts are unavailable in this build" })).toBeInTheDocument(); expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); }); @@ -103,7 +106,8 @@ describe("AccountSection", () => { fireEvent.click(screen.getByRole("button", { name: "Sign in" })); expect(verify).toHaveBeenCalledWith("123456"); await waitFor(() => expect(screen.getByRole("button", { name: "Sign out" })).toHaveFocus()); - expect(screen.getByText(/Signed in as/)).toHaveTextContent("Signed in as me@example.com."); + expect(screen.getByRole("heading", { name: "me@example.com" })).toBeInTheDocument(); + expect(screen.getByText(/^Signed in./)).toHaveTextContent("Signed in. It's the same account as Switchify on Android."); }); it("shows a wrong-code error and keeps the code form", async () => { @@ -136,7 +140,7 @@ describe("AccountSection", () => { const remove = vi.spyOn(api, "deleteAccount").mockResolvedValue(signedOut); fireEvent.click(await screen.findByRole("button", { name: "Delete account…" })); expect(remove).not.toHaveBeenCalled(); - expect(screen.getByText(/removes it and its saved settings from every device/)).toBeInTheDocument(); + expect(screen.getByText(/removes your account and its saved settings from every device/)).toBeInTheDocument(); await waitFor(() => expect(screen.getByRole("button", { name: "Keep account" })).toHaveFocus()); fireEvent.click(screen.getByRole("button", { name: "Keep account" })); await waitFor(() => expect(screen.getByRole("button", { name: "Delete account…" })).toHaveFocus()); diff --git a/src/settings/AccountSection.tsx b/src/settings/AccountSection.tsx index 648e058b..edfbfb80 100644 --- a/src/settings/AccountSection.tsx +++ b/src/settings/AccountSection.tsx @@ -1,10 +1,11 @@ -import { useEffect, useRef, useState, type FormEvent } from "react"; +import { useEffect, useRef, useState, type FormEvent, type ReactNode } from "react"; +import { CircleUserRound, ListChecks, LockKeyhole, MailCheck, RefreshCw, Trash2 } from "lucide-react"; import { api } from "../api"; import { Button, Input } from "../ui/controls"; import type { AccountView, SyncView } from "../types"; -import { SettingGroup } from "./controls"; -const description = "The same account as Switchify on Android. We email you a code to sign in; there is no password."; +const sameAccount = "It's the same account as Switchify on Android."; +const whatSyncs = "Pointer, repeat, dwell and cursor settings; your switches and their keys; scanning and keyboard layout; switch profiles; and remote switches. Pairings, diagnostics and starting with your computer stay on each computer."; type FocusTarget = "email" | "code" | "signOut" | "deleteAccount" | "keepAccount"; type Screen = "loading" | "unavailable" | "keychain" | "signedIn" | "pending" | "signedOut"; @@ -17,6 +18,26 @@ function screenOf(account: AccountView | null): Screen { return account.pendingEmail ? "pending" : "signedOut"; } +// The status band and grouped rows used on Home, so the page reads like the +// rest of the app: a colour-and-shape cue (tick or mark) on the icon. +function StatusIcon({ ok, children }: { ok?: boolean; children: ReactNode }) { + return ; +} + +function Band({ tone, ok, icon, title, children, action }: { tone: "ready" | "attention" | "neutral"; ok?: boolean; icon: ReactNode; title: ReactNode; children: ReactNode; action?: ReactNode }) { + return
+ {icon} +

{title}

{children}
+ {action} +
; +} + +function WhatSyncs() { + return
+

What syncs

{whatSyncs}

+
; +} + export function AccountSection() { const [account, setAccount] = useState(null); const [email, setEmail] = useState(""); @@ -108,69 +129,89 @@ export function AccountSection() { void run(() => api.verifySignInCode(code), () => "signOut").then((ok) => { if (ok) setCode(""); }); }; - if (!account) return

Loading…

; + if (!account) return

Loading…

; // Keyed by occurrence so the same message is announced again. const errorText = error && {error.text}; const errorProps = { "aria-invalid": Boolean(error), "aria-describedby": error ? "account-error" : undefined }; if (!account.available) { - return

Accounts are unavailable in this build.

; + return } title="Accounts are unavailable in this build">

This build was made without account support.

; } if (account.keychainUnavailable) { - return

Your saved sign-in can't be read because this computer's keychain is locked or unavailable. Unlock it, then open this page again.

; + return } title="Your keychain is locked"> +

Your saved sign-in can't be read because this computer's keychain is locked or unavailable. Unlock it, then open this page again.

+
; } if (account.signedIn) { - return -

Signed in as {account.email}.

- {!confirmDelete && } - {confirmDelete - ?
-

Deleting your account removes it and its saved settings from every device, including Switchify on Android. This cannot be undone.

-
- - -
-
- :
- - -
} + return
+ } title={{account.email}} + action={}> +

Signed in. {sameAccount}

+
+
+ +
+ {confirmDelete + ?
+ +

Delete your account?

This removes your account and its saved settings from every device, including Switchify on Android. It cannot be undone.

+
+ + +
+
+ :
+ +

Delete account

Remove your account and its saved settings from every device.

+ +
} +
{errorText} - ; +
; } if (account.pendingEmail) { - return -
-

We sent a code to {account.pendingEmail}. It can take a minute to arrive.

-