From 2bd994babceb4ffd394c31acf0b60daa1e1dd335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A9l=20Solano?= Date: Mon, 9 Mar 2026 05:50:31 +0100 Subject: [PATCH] fix: preserve custom session fields on focus refresh (#8354) --- .../src/client/session-refresh.test.ts | 103 ++++++++++++++++++ .../better-auth/src/client/session-refresh.ts | 33 +++--- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/packages/better-auth/src/client/session-refresh.test.ts b/packages/better-auth/src/client/session-refresh.test.ts index 8a4a44f56e..8c78d0d880 100644 --- a/packages/better-auth/src/client/session-refresh.test.ts +++ b/packages/better-auth/src/client/session-refresh.test.ts @@ -622,6 +622,109 @@ describe("session-refresh", () => { vi.useRealTimers(); }); + /** + * https://github.com/better-auth/better-auth/issues/8325 + */ + it("should preserve custom session fields on poll refresh", async () => { + vi.useFakeTimers(); + + const sessionAtom: SessionAtom = atom({ + data: { + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + }, + error: null, + isPending: false, + }); + const sessionSignal = atom(false); + + const customSessionData = { + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + activationInfo: { isActivated: true, activatedAt: "2024-01-01" }, + role: "admin", + }; + + const mockFetch = vi.fn(async () => ({ + data: customSessionData, + error: null, + })); + + const manager = createSessionRefreshManager({ + sessionAtom, + sessionSignal, + $fetch: mockFetch as any, + options: { + sessionOptions: { + refetchInterval: 5, + }, + }, + }); + + manager.init(); + + await vi.advanceTimersByTimeAsync(5000); + + const updatedSession = sessionAtom.get(); + expect(updatedSession.data).toEqual(customSessionData); + expect((updatedSession.data as any)?.activationInfo).toEqual({ + isActivated: true, + activatedAt: "2024-01-01", + }); + expect((updatedSession.data as any)?.role).toBe("admin"); + + manager.cleanup(); + vi.useRealTimers(); + }); + + it("should preserve custom session fields on visibilitychange refresh", async () => { + vi.useFakeTimers(); + + const sessionAtom: SessionAtom = atom({ + data: { + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + }, + error: null, + isPending: false, + }); + const sessionSignal = atom(false); + + const customSessionData = { + user: { id: "1", email: "test@test.com" }, + session: { id: "session-1" }, + customField: "custom value", + }; + + const mockFetch = vi.fn(async () => ({ + data: customSessionData, + error: null, + })); + + const manager = createSessionRefreshManager({ + sessionAtom, + sessionSignal, + $fetch: mockFetch as any, + options: { + sessionOptions: { + refetchOnWindowFocus: true, + }, + }, + }); + + manager.init(); + + manager.triggerRefetch({ event: "visibilitychange" }); + await vi.runAllTimersAsync(); + + const updatedSession = sessionAtom.get(); + expect(updatedSession.data).toEqual(customSessionData); + expect((updatedSession.data as any)?.customField).toBe("custom value"); + + manager.cleanup(); + vi.useRealTimers(); + }); + it("should broadcast session update when broadcastSessionUpdate is called with signout", () => { const channel = getGlobalBroadcastChannel(); const postSpy = vi.spyOn(channel, "post"); diff --git a/packages/better-auth/src/client/session-refresh.ts b/packages/better-auth/src/client/session-refresh.ts index 8383d8220e..a4c117c6ff 100644 --- a/packages/better-auth/src/client/session-refresh.ts +++ b/packages/better-auth/src/client/session-refresh.ts @@ -15,10 +15,12 @@ const now = () => Math.floor(Date.now() / 1000); const FOCUS_REFETCH_RATE_LIMIT_SECONDS = 5; export interface SessionRefreshOptions { - sessionAtom: AuthQueryAtom<{ - user: User; - session: Session; - }>; + sessionAtom: AuthQueryAtom< + { + user: User; + session: Session; + } & Record + >; sessionSignal: WritableAtom; $fetch: BetterFetch; options?: BetterAuthClientOptions | undefined; @@ -34,11 +36,19 @@ interface SessionRefreshState { unsubscribeOnline?: (() => void) | undefined; } -interface SessionResponse { - session: Session | null; - user: User | null; - needsRefresh?: boolean; -} +export type SessionResponse = ( + | { + session: null; + user: null; + needsRefresh?: boolean; + } + | { + session: Session; + user: User; + needsRefresh?: boolean; + } +) & + Record; export function createSessionRefreshManager(opts: SessionRefreshOptions) { const { sessionAtom, sessionSignal, $fetch, options = {} } = opts; @@ -93,10 +103,7 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) { } catch {} } - const sessionData = - data?.session && data?.user - ? { session: data.session, user: data.user } - : null; + const sessionData = data?.session && data?.user ? data : null; sessionAtom.set({ ...currentSession,