mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: drop a signed-out account's calendar feeds, and clear recent searches when an eviction removes the last account
This commit is contained in:
@@ -261,7 +261,7 @@ describe('what a sign-out leaves behind', () => {
|
||||
feeds: useCalendarSubscriptionsStore.getState().subscriptions.map((s) => s.id),
|
||||
});
|
||||
|
||||
it('keeps the recent searches and calendar feeds while an account is still signed in', async () => {
|
||||
it("keeps the recent searches and the other accounts' 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)] });
|
||||
@@ -269,7 +269,69 @@ describe('what a sign-out leaves behind', () => {
|
||||
await useAuthStore.getState().logout();
|
||||
await settle();
|
||||
|
||||
expect(leftovers()).toEqual({ searches: ['invoice'], feeds: [`sub-${A}`, `sub-${B}`] });
|
||||
expect(leftovers()).toEqual({ searches: ['invoice'], feeds: [`sub-${B}`] });
|
||||
});
|
||||
|
||||
it("drops a removed account's calendar feeds right away", async () => {
|
||||
await signedIn(A, B, C);
|
||||
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A), feed(B), feed(C)] });
|
||||
|
||||
await useAuthStore.getState().removeAccount(C);
|
||||
|
||||
expect(leftovers().feeds).toEqual([`sub-${A}`, `sub-${B}`]);
|
||||
});
|
||||
|
||||
it('drops the feeds of an account whose sign-in is refused on the switch to it', async () => {
|
||||
await signedIn(A, B);
|
||||
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A), feed(B)] });
|
||||
client.loadErrors = { [B]: new AuthenticationError('Invalid credentials') };
|
||||
|
||||
await useAuthStore.getState().switchAccount(B);
|
||||
|
||||
expect(useAccountStore.getState().accounts.map((a) => a.id)).toEqual([A]);
|
||||
expect(leftovers().feeds).toEqual([`sub-${A}`]);
|
||||
});
|
||||
|
||||
it('clears them when a switch evicts the last account', async () => {
|
||||
// Signed out of the active account while the last one's server was
|
||||
// away; it is refused when the user picks it.
|
||||
await signedIn(A);
|
||||
useAuthStore.setState({ isAuthenticated: false, session: null, activeAccountId: null, accountId: null });
|
||||
client.served = null;
|
||||
useSearchHistoryStore.setState({ recentSearches: ['invoice'] });
|
||||
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A)] });
|
||||
client.loadErrors = { [A]: new AuthenticationError('Invalid credentials') };
|
||||
|
||||
await useAuthStore.getState().switchAccount(A);
|
||||
|
||||
expect(useAccountStore.getState().accounts).toEqual([]);
|
||||
expect(leftovers()).toEqual({ searches: [], feeds: [] });
|
||||
});
|
||||
|
||||
it('clears them when the restore on launch evicts the last account', async () => {
|
||||
await signedIn(A);
|
||||
useAuthStore.setState({ isAuthenticated: false, session: null, activeAccountId: null, accountId: null });
|
||||
useSearchHistoryStore.setState({ recentSearches: ['invoice'] });
|
||||
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A)] });
|
||||
client.loadErrors = { [A]: new AuthenticationError('Invalid credentials') };
|
||||
|
||||
expect(await useAuthStore.getState().restoreSession()).toBe(false);
|
||||
|
||||
expect(useAccountStore.getState().accounts).toEqual([]);
|
||||
expect(leftovers()).toEqual({ searches: [], feeds: [] });
|
||||
});
|
||||
|
||||
it('clears them when a session retry evicts the last account', async () => {
|
||||
await signedIn(A);
|
||||
useAuthStore.setState({ session: null });
|
||||
useSearchHistoryStore.setState({ recentSearches: ['invoice'] });
|
||||
useCalendarSubscriptionsStore.setState({ subscriptions: [feed(A)] });
|
||||
client.loadErrors = { [A]: new AuthenticationError('Invalid credentials') };
|
||||
|
||||
expect(await useAuthStore.getState().retrySession()).toBe(false);
|
||||
|
||||
expect(useAccountStore.getState().accounts).toEqual([]);
|
||||
expect(leftovers()).toEqual({ searches: [], feeds: [] });
|
||||
});
|
||||
|
||||
it('clears them when the last account signs out', async () => {
|
||||
|
||||
@@ -147,6 +147,27 @@ function clearDeviceWideAccountData(): void {
|
||||
useCalendarSubscriptionsStore.getState().clearAll();
|
||||
}
|
||||
|
||||
// An account left the registry: signed out, or evicted because its
|
||||
// credentials are gone or refused. Its own calendar feeds go now, since feed
|
||||
// URLs are often secret; feeds made before they recorded their account stay
|
||||
// until nobody is signed in. When it was the last account, the device-wide
|
||||
// data goes too.
|
||||
async function forgetRemovedAccount(accountId: string): Promise<void> {
|
||||
try {
|
||||
const feeds = useCalendarSubscriptionsStore;
|
||||
// On a cold start the stored list may not have been read yet; written
|
||||
// before that, the filtered empty default would replace it.
|
||||
if (!feeds.persist.hasHydrated()) await feeds.persist.rehydrate();
|
||||
const { subscriptions } = feeds.getState();
|
||||
if (subscriptions.some((s) => s.localAccountId === accountId)) {
|
||||
feeds.setState({ subscriptions: subscriptions.filter((s) => s.localAccountId !== accountId) });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[auth-store] could not drop the calendar feeds of a removed account:', err);
|
||||
}
|
||||
if (useAccountStore.getState().accounts.length === 0) clearDeviceWideAccountData();
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -719,6 +740,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
await revokeStoredRefreshToken(currentId);
|
||||
await jmapClient.clearAccountCredentials(currentId);
|
||||
accountStore.removeAccount(currentId);
|
||||
await forgetRemovedAccount(currentId);
|
||||
} else {
|
||||
await jmapClient.logout();
|
||||
}
|
||||
@@ -877,6 +899,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
useEmailStore.getState().removeAccount(accountId);
|
||||
restorePrevious();
|
||||
set({ isLoading: false, error: 'Session expired for this account' });
|
||||
await forgetRemovedAccount(accountId);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -888,6 +911,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
useEmailStore.getState().removeAccount(accountId);
|
||||
restorePrevious();
|
||||
set({ isLoading: false, error: 'Session expired for this account' });
|
||||
await forgetRemovedAccount(accountId);
|
||||
return;
|
||||
}
|
||||
// NetworkError or anything else - keep the previous active account
|
||||
@@ -943,6 +967,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
useEmailStore.getState().removeAccount(accountId);
|
||||
clearViewerCaches();
|
||||
accountStore.removeAccount(accountId);
|
||||
await forgetRemovedAccount(accountId);
|
||||
},
|
||||
|
||||
restoreSession: async () => {
|
||||
@@ -1004,6 +1029,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
// No stored credentials (or corrupt) — genuine logout.
|
||||
accountStore.removeAccount(target.id);
|
||||
useEmailStore.getState().removeAccount(target.id);
|
||||
await forgetRemovedAccount(target.id);
|
||||
set({ isLoading: false, hasRestoredSession: true });
|
||||
return false;
|
||||
}
|
||||
@@ -1038,6 +1064,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
await jmapClient.clearAccountCredentials(target.id).catch(() => undefined);
|
||||
accountStore.removeAccount(target.id);
|
||||
useEmailStore.getState().removeAccount(target.id);
|
||||
await forgetRemovedAccount(target.id);
|
||||
set({ isLoading: false, hasRestoredSession: true, error: 'Session expired' });
|
||||
return false;
|
||||
}
|
||||
@@ -1107,6 +1134,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
// Now we know the credentials are bad — fall back to logout flow.
|
||||
await jmapClient.clearAccountCredentials(activeAccountId).catch(() => undefined);
|
||||
accountStore.removeAccount(activeAccountId);
|
||||
await forgetRemovedAccount(activeAccountId);
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
|
||||
Reference in New Issue
Block a user