fix: clear recent searches and calendar feeds when the last account signs out

This commit is contained in:
Linus Rath
2026-10-29 03:03:58 +01:00
parent 99a4212804
commit 986216f4d6
3 changed files with 69 additions and 0 deletions
@@ -125,6 +125,8 @@ import { useAccountStore } from '../account-store';
import { useAuthStore } from '../auth-store';
import { useEmailStore } from '../email-store';
import { useOutboxStore } from '../outbox-store';
import { useSearchHistoryStore } from '../search-history-store';
import { useCalendarSubscriptionsStore, type CalendarSubscription } from '../calendar-subscriptions-store';
const { A, B, C } = client;
const settle = async () => {
@@ -244,4 +246,49 @@ describe('what a sign-out leaves behind', () => {
await useAuthStore.getState().logoutAll();
expect(vi.mocked(forgetRefreshRotations).mock.calls.at(-1)).toEqual([`rt-${B}`]);
});
const feed = (localAccountId: string): CalendarSubscription => ({
id: `sub-${localAccountId}`,
name: 'Team',
url: 'https://cal.example.org/feed?token=secret',
calendarId: 'c1',
localAccountId,
lastSyncAt: null,
lastError: null,
});
const leftovers = () => ({
searches: useSearchHistoryStore.getState().recentSearches,
feeds: useCalendarSubscriptionsStore.getState().subscriptions.map((s) => s.id),
});
it('keeps the recent searches and calendar feeds while an account is still signed in', async () => {
await signedIn(A, B);
useSearchHistoryStore.setState({ recentSearches: ['invoice'] });
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A), feed(B)] });
await useAuthStore.getState().logout();
await settle();
expect(leftovers()).toEqual({ searches: ['invoice'], feeds: [`sub-${A}`, `sub-${B}`] });
});
it('clears them when the last account signs out', async () => {
await signedIn(A);
useSearchHistoryStore.setState({ recentSearches: ['invoice'] });
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A)] });
await useAuthStore.getState().logout();
expect(leftovers()).toEqual({ searches: [], feeds: [] });
});
it('clears them when everyone signs out at once', async () => {
await signedIn(A, B);
useSearchHistoryStore.setState({ recentSearches: ['invoice'] });
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A), feed(B)] });
await useAuthStore.getState().logoutAll();
expect(leftovers()).toEqual({ searches: [], feeds: [] });
});
});
+15
View File
@@ -16,6 +16,8 @@ import { useCalendarStore } from './calendar-store';
import { useSettingsStore } from './settings-store';
import { useFilterStore } from './filter-store';
import { useOutboxStore } from './outbox-store';
import { useSearchHistoryStore } from './search-history-store';
import { useCalendarSubscriptionsStore } from './calendar-subscriptions-store';
import { flushPersistedWrites } from './persist-storage';
import { clearEmailDetailCache } from '../lib/email-detail-cache';
import { clearBodyDocuments } from '../lib/email-body-document';
@@ -130,10 +132,21 @@ function clearAllFeatureStores(): void {
useContactsStore.getState().reset();
useCalendarStore.getState().reset();
useFilterStore.getState().clearState();
clearDeviceWideAccountData();
// Cache writes are held back briefly; get the signed-out data off disk now.
void flushPersistedWrites();
}
// Kept across accounts, not per account, so they go only once nobody is
// signed in (webmail lib/sign-out-cleanup.ts): the recent searches name what
// the accounts' mail is about, and calendar feed URLs are often secret.
// Templates and tag names stay, as in the webmail: they are the user's own
// work on this device, not a copy of an account's data.
function clearDeviceWideAccountData(): void {
useSearchHistoryStore.getState().clearRecentSearches();
useCalendarSubscriptionsStore.getState().clearAll();
}
// Drop the named account from the email cache, then reset the (per-session,
// not yet per-account) contacts and calendar stores. Used by logout when
// signing one account out while others remain.
@@ -746,6 +759,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
return;
}
// That was the last account.
if (useAccountStore.getState().accounts.length === 0) clearDeviceWideAccountData();
set({
isAuthenticated: false,
isLoading: false,
@@ -80,6 +80,11 @@ interface SubscriptionsState {
* account has that id.
*/
unlinkAccount: (localAccountId: string, jmapAccountIds: { previous: string | null; next: string | null }) => Promise<void>;
/**
* Everyone signed out: forget every subscription on this device (feed URLs
* are often secret). The mirror calendars stay on the server.
*/
clearAll: () => void;
}
/**
@@ -417,6 +422,8 @@ export const useCalendarSubscriptionsStore = create<SubscriptionsState>()(
: s)),
});
},
clearAll: () => set({ subscriptions: [], syncing: {} }),
}),
{
name: 'calendar-subscriptions',