fix: sign out of one account without dropping the others #1201

Logout switched to the first account in the list. After a restore that
is often an account still waiting for sign-in, which has no client, so
logout took the full sign-out path and dropped every account. It now
switches to the first account that is connected.

Boot also counted waiting accounts when deciding whether any account
could be restored, so a reload with only waiting accounts left showed a
connection error instead of the login page.
This commit is contained in:
Linus Rath
2026-10-10 19:23:59 +02:00
parent d4b3b86329
commit 5f749fea63
2 changed files with 38 additions and 3 deletions
@@ -95,3 +95,27 @@ it('stops waiting once the account is signed in', () => {
expect(alice).toMatchObject({ label: 'Alice', avatarColor: '#16a34a', isConnected: true });
expect(alice?.awaitingSignIn).toBeUndefined();
});
it('signs out to an account that is signed in, not to one awaiting sign-in', async () => {
restoreAccounts(archived);
useAccountStore.setState((s) => ({
accounts: [...s.accounts, { ...signedIn, id: 'carol@x.test@mail.x.test', username: 'carol@x.test', email: 'carol@x.test', label: 'Carol', cookieSlot: 2, isDefault: false }] as never,
}));
await useAuthStore.getState().checkAuth();
// Wait for the background restore of the other remembered account.
await vi.waitFor(() => expect(useAccountStore.getState().getAccountById('carol@x.test@mail.x.test')?.isConnected).toBe(true));
await useAuthStore.getState().logout();
expect(useAuthStore.getState().activeAccountId).toBe('carol@x.test@mail.x.test');
expect(useAuthStore.getState().isAuthenticated).toBe(true);
expect(useAccountStore.getState().accounts.map(a => a.id)).toEqual(['alice@x.test@mail.x.test', 'carol@x.test@mail.x.test']);
});
it('treats a browser left with only accounts awaiting sign-in as signed out, not as a connection failure', async () => {
useAccountStore.setState({ accounts: [{ ...signedIn, rememberMe: false }] as never });
restoreAccounts(archived.slice(1));
await useAuthStore.getState().checkAuth();
expect(useAuthStore.getState().isAuthenticated).toBe(false);
expect(useAuthStore.getState().error).not.toBe('connection_failed');
expect(useAccountStore.getState().accounts.map(a => a.id)).toEqual(['alice@x.test@mail.x.test']);
});
+14 -3
View File
@@ -2112,7 +2112,13 @@ export const useAuthStore = create<AuthState>()(
// Check if there are remaining accounts to switch to. Read the store
// afresh: `accountStore` is the state from before removeAccount().
const nextAccount = wasDemoMode ? undefined : useAccountStore.getState().accounts[0];
// Prefer an account that is signed in here: one restored from the
// account list backup (or otherwise not connected) has no client, and
// picking it signs out every account left.
const remainingAccounts = useAccountStore.getState().accounts;
const nextAccount = wasDemoMode
? undefined
: remainingAccounts.find((a) => clients.has(a.id)) ?? remainingAccounts[0];
const droppedAccounts: AccountEntry[] = [];
if (nextAccount) {
@@ -2762,8 +2768,13 @@ export const useAuthStore = create<AuthState>()(
}
}
// No accounts could be restored
if (accounts.some((account) => accountStore.getAccountById(account.id))) {
// No accounts could be restored. Accounts restored from the account
// list backup were never signed in here, so they say nothing about
// the connection.
if (accounts.some((account) => {
const entry = accountStore.getAccountById(account.id);
return entry && !entry.awaitingSignIn;
})) {
set({
isAuthenticated: false,
isLoading: false,