diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index fdb0ff9..9babda6 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -30,7 +30,7 @@ import { usePocketBase } from './usePocketBase'; * }; * ``` */ -export function useAuth({ collectionName = 'users', realtime = true }: UseAuthOptions = {}): UseAuthResult { +export function useAuth({ collectionName = 'users', realtime = true, refreshOnMount = false }: UseAuthOptions = {}): UseAuthResult { const pb = usePocketBase(); const [user, setUser] = useState(pb.authStore.record as User); const [isLoading, setIsLoading] = useState(false); @@ -43,6 +43,31 @@ export function useAuth({ collectionName = 'users', rea }); }, [pb]); + useEffect(() => { + if (!refreshOnMount || !pb.authStore.isValid) { + return; + } + + let cancelled = false; + + const refresh = async () => { + try { + const authData = await recordService.authRefresh(); + if (!cancelled) { + setUser(authData.record); + } + } catch { + // silently ignore refresh errors on mount (e.g., network issues, invalid token) + } + }; + + refresh(); + + return () => { + cancelled = true; + }; + }, [refreshOnMount, pb.authStore.isValid, recordService]); + useEffect(() => { if (!user || !realtime) { return; diff --git a/src/types/useAuth.type.ts b/src/types/useAuth.type.ts index d59e3fb..6ff7359 100644 --- a/src/types/useAuth.type.ts +++ b/src/types/useAuth.type.ts @@ -14,6 +14,15 @@ export interface UseAuthOptions { * Enable or disable real-time updates (default: true) */ realtime?: boolean; + + /** + * Refresh user data from the server on mount (default: false) + * + * When true, the hook will call authRefresh() on mount if the user is + * already authenticated. This ensures the user data is up-to-date + * after screen navigation or component remounting. + */ + refreshOnMount?: boolean; } /** diff --git a/tests/hooks/useAuth.test.tsx b/tests/hooks/useAuth.test.tsx index 5188eb5..4d1bd43 100644 --- a/tests/hooks/useAuth.test.tsx +++ b/tests/hooks/useAuth.test.tsx @@ -208,6 +208,105 @@ describe('useAuth', () => { }); }); + describe('refreshOnMount', () => { + it('should refresh user data on mount when refreshOnMount is true', async () => { + const staleUser = { id: '1', email: 'test@example.com', custom_field: 'old_value' }; + const freshUser = { id: '1', email: 'test@example.com', custom_field: 'new_value' }; + + const mockPocketBaseWithUser = createMockAuthPocketBase({ + isValid: true, + record: staleUser, + }); + + const mockAuthRefresh = vi.fn().mockResolvedValue({ record: freshUser, token: 'new-token' }); + mockPocketBaseWithUser.collection = vi.fn().mockReturnValue({ + authRefresh: mockAuthRefresh, + subscribe: vi.fn(() => Promise.resolve(() => {})), + }); + + const wrapper = createWrapper(mockPocketBaseWithUser); + + const { result } = renderHook(() => useAuth({ refreshOnMount: true }), { wrapper }); + + expect(result.current.user).toEqual(staleUser); + + await waitFor(() => { + expect(mockAuthRefresh).toHaveBeenCalled(); + expect(result.current.user).toEqual(freshUser); + }); + }); + + it('should not refresh when refreshOnMount is false (default)', async () => { + const staleUser = { id: '1', email: 'test@example.com', custom_field: 'old_value' }; + + const mockPocketBaseWithUser = createMockAuthPocketBase({ + isValid: true, + record: staleUser, + }); + + const mockAuthRefresh = vi.fn().mockResolvedValue({ record: staleUser, token: 'token' }); + mockPocketBaseWithUser.collection = vi.fn().mockReturnValue({ + authRefresh: mockAuthRefresh, + subscribe: vi.fn(() => Promise.resolve(() => {})), + }); + + const wrapper = createWrapper(mockPocketBaseWithUser); + + renderHook(() => useAuth(), { wrapper }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(mockAuthRefresh).not.toHaveBeenCalled(); + }); + + it('should not refresh when not authenticated', async () => { + const mockPocketBase = createMockAuthPocketBase({ + isValid: false, + record: null, + }); + + const mockAuthRefresh = vi.fn().mockResolvedValue({ record: {}, token: 'token' }); + mockPocketBase.collection = vi.fn().mockReturnValue({ + authRefresh: mockAuthRefresh, + subscribe: vi.fn(() => Promise.resolve(() => {})), + }); + + const wrapper = createWrapper(mockPocketBase); + + renderHook(() => useAuth({ refreshOnMount: true }), { wrapper }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(mockAuthRefresh).not.toHaveBeenCalled(); + }); + + it('should silently ignore refresh errors', async () => { + const staleUser = { id: '1', email: 'test@example.com' }; + + const mockPocketBaseWithUser = createMockAuthPocketBase({ + isValid: true, + record: staleUser, + }); + + const mockAuthRefresh = vi.fn().mockRejectedValue(new Error('Network error')); + mockPocketBaseWithUser.collection = vi.fn().mockReturnValue({ + authRefresh: mockAuthRefresh, + subscribe: vi.fn(() => Promise.resolve(() => {})), + }); + + const wrapper = createWrapper(mockPocketBaseWithUser); + + const { result } = renderHook(() => useAuth({ refreshOnMount: true }), { wrapper }); + + await waitFor(() => { + expect(mockAuthRefresh).toHaveBeenCalled(); + }); + + expect(result.current.user).toEqual(staleUser); + expect(result.current.error).toBe(null); + }); + }); + describe('signIn', () => { it.each` scenario | options | expected