diff --git a/src/stores/__tests__/auth-store-logout.test.ts b/src/stores/__tests__/auth-store-logout.test.ts index 0b15451f..4c126c70 100644 --- a/src/stores/__tests__/auth-store-logout.test.ts +++ b/src/stores/__tests__/auth-store-logout.test.ts @@ -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: [] }); + }); }); diff --git a/src/stores/auth-store.ts b/src/stores/auth-store.ts index a905c02c..7312e9d0 100644 --- a/src/stores/auth-store.ts +++ b/src/stores/auth-store.ts @@ -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((set, get) => ({ return; } + // That was the last account. + if (useAccountStore.getState().accounts.length === 0) clearDeviceWideAccountData(); set({ isAuthenticated: false, isLoading: false, diff --git a/src/stores/calendar-subscriptions-store.ts b/src/stores/calendar-subscriptions-store.ts index 113dffb9..2de807ca 100644 --- a/src/stores/calendar-subscriptions-store.ts +++ b/src/stores/calendar-subscriptions-store.ts @@ -80,6 +80,11 @@ interface SubscriptionsState { * account has that id. */ unlinkAccount: (localAccountId: string, jmapAccountIds: { previous: string | null; next: string | null }) => Promise; + /** + * 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()( : s)), }); }, + + clearAll: () => set({ subscriptions: [], syncing: {} }), }), { name: 'calendar-subscriptions',