;
+}
+
export function AccountSection() {
const [account, setAccount] = useState(null);
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
- const [error, setError] = useState<{ text: string; id: number } | null>(null);
+ // Where an error is shown: beside the control that failed, so it is seen
+ // by whoever pressed it, not off screen at the bottom of the page.
+ type ErrorPlace = "form" | "signOut" | "delete";
+ const [error, setError] = useState<{ text: string; id: number; place: ErrorPlace } | null>(null);
const [busy, setBusy] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
// Set only by the user's own actions, so opening the page never moves focus.
@@ -79,7 +99,7 @@ export function AccountSection() {
// Controls stay enabled while busy so focus is never dropped to the page;
// repeated presses are ignored instead.
- const run = async (action: () => Promise, next: (view: AccountView) => FocusTarget | null) => {
+ const run = async (action: () => Promise, next: (view: AccountView) => FocusTarget | null, place: ErrorPlace = "form") => {
if (busy) return false;
setBusy(true);
setError(null);
@@ -89,7 +109,7 @@ export function AccountSection() {
setFocusNext(next(view));
return true;
} catch (failure) {
- setError({ text: failure instanceof Error ? failure.message : String(failure), id: Date.now() });
+ setError({ text: failure instanceof Error ? failure.message : String(failure), id: Date.now(), place });
return false;
} finally {
setBusy(false);
@@ -108,69 +128,92 @@ 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};
+ // Off the signed-in screen (e.g. a session rejected mid-delete) the
+ // failed control is gone, so the error joins the form that replaced it.
+ const errorAt = (place: ErrorPlace) => error && (account.signedIn ? error.place : "form") === place && {error.text};
+ const errorText = errorAt("form");
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="Can't read your saved sign-in">
+
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}
+ {errorAt("signOut")}
+
+
+
+
+ {confirmDelete
+ ?
+
+
Delete your account?
This removes your account and its saved settings from every device, including Switchify on Android. It cannot be undone.
{errorAt("delete")}
+
+
+
+
+
+ :
+
+
Delete account
Remove your account and its saved settings from every device.
+
+ }
+
+
;
+ }
+
+ if (account.pendingEmail) {
+ return
+ } title="Check your email">
+
We sent a code to {account.pendingEmail}. It can take a minute to arrive.