import * as React from 'react' import { type AuthClientConfig, ClientSessionError, fetchData, now, parseUrl, useOnline, type SessionContextValue, type SessionProviderProps, type GetSessionParams, type UseSessionOptions, type LiteralUnion, type SignInOptions, type SignInAuthorizationParams, type SignInResponse, type ClientSafeProvider, type SignOutParams, type SignOutResponse, WindowProps, AuthState, } from './client' import type { LoggerInstance, Session } from '@auth/core/types' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import type { BuiltInProviderType, RedirectableProviderType } from '@auth/core/providers' const logger: LoggerInstance = { debug: console.debug, error: console.error, warn: console.warn, } class AuthConfigManager { private static instance: AuthConfigManager | null = null private config: AuthClientConfig private constructor() { this.config = this.createDefaultConfig() } private createDefaultConfig(): AuthClientConfig { return { baseUrl: typeof window !== 'undefined' ? parseUrl(window.location.origin).origin : '', basePath: typeof window !== 'undefined' ? parseUrl(window.location.origin).path : '/api/auth', credentials: 'same-origin', lastSync: 0, session: null, fetchSession: async () => void 0, } } static getInstance(): AuthConfigManager { if (!AuthConfigManager.instance) { AuthConfigManager.instance = new AuthConfigManager() } return AuthConfigManager.instance } setConfig(userConfig: Partial): void { this.config = { ...this.config, ...userConfig } } getConfig(): AuthClientConfig { return this.config } initializeConfig(hasInitialSession: boolean): void { this.config.lastSync = hasInitialSession ? now() : 0 } } export const authConfigManager = AuthConfigManager.getInstance() export const SessionContext = React.createContext(undefined) function useInitializeSession(hasInitialSession: boolean, initialSession: Session | null) { const authConfig = authConfigManager.getConfig() const [session, setSession] = React.useState(initialSession) const [loading, setLoading] = React.useState(!hasInitialSession) useEffect(() => { authConfig.fetchSession = async ({ event } = {}) => { try { const isStorageEvent = event === 'storage' if (isStorageEvent || !authConfig.session) { authConfig.lastSync = now() authConfig.session = await getSession() setSession(authConfig.session) return } if (!event || !authConfig.session || now() < authConfig.lastSync) { return } authConfig.lastSync = now() authConfig.session = await getSession() setSession(authConfig.session) } catch (error) { logger.error(new ClientSessionError((error as Error).message, error as any)) } finally { setLoading(false) } } authConfig.fetchSession() return () => { authConfig.lastSync = 0 authConfig.session = null authConfig.fetchSession = async () => void 0 } }, []) return { session, setSession, loading, setLoading } } function useVisibilityChangeEventListener( authConfig: AuthClientConfig, refetchOnWindowFocus: boolean ) { useEffect(() => { const abortController = new AbortController() const handleVisibilityChange = () => { if (refetchOnWindowFocus && document.visibilityState === 'visible') { authConfig.fetchSession({ event: 'visibilitychange' }) } } document.addEventListener('visibilitychange', handleVisibilityChange, { signal: abortController.signal, }) return () => abortController.abort() }, [refetchOnWindowFocus]) } function useRefetchInterval( authConfig: AuthClientConfig, refetchInterval?: number, shouldRefetch?: boolean ) { useEffect(() => { if (refetchInterval && shouldRefetch) { const intervalId = setInterval(() => { if (authConfig.session) { authConfig.fetchSession({ event: 'poll' }) } }, refetchInterval * 1000) return () => clearInterval(intervalId) } }, [refetchInterval, shouldRefetch]) } export async function getSession(params?: GetSessionParams) { const { baseUrl, basePath, credentials } = authConfigManager.getConfig() const session = await fetchData( 'session', { baseUrl, basePath, credentials, }, logger, params ) return session } export async function getCsrfToken() { const { baseUrl, basePath, credentials } = authConfigManager.getConfig() const response = await fetchData<{ csrfToken: string }>( 'csrf', { baseUrl, basePath, credentials, }, logger ) return response?.csrfToken ?? '' } export function SessionProvider(props: SessionProviderProps) { if (!SessionContext) { throw new Error('React Context is unavailable in Server Components') } const { children, refetchInterval, refetchWhenOffline = true } = props const authConfig = authConfigManager.getConfig() const hasInitialSession = !!props.session authConfigManager.initializeConfig(hasInitialSession) const { session, setSession, loading, setLoading } = useInitializeSession( hasInitialSession, props.session ?? null ) useVisibilityChangeEventListener(authConfig, props.refetchOnWindowFocus ?? true) const isOnline = useOnline() const shouldRefetch = refetchWhenOffline || isOnline useRefetchInterval(authConfig, refetchInterval, shouldRefetch) const contextValue: SessionContextValue = useMemo( () => ({ data: session, status: loading ? 'loading' : session ? 'authenticated' : 'unauthenticated', update: async (data) => { if (loading || !session) { return } setLoading(true) const updatedSession = await fetchData( 'session', authConfig, logger, data ? { body: { csrfToken: await getCsrfToken(), data } } : undefined ) setLoading(false) if (updatedSession) { setSession(updatedSession) } return updatedSession }, } as SessionContextValue), [session, loading, setSession] ) return {children} } export function useSession( options?: UseSessionOptions ): SessionContextValue { if (!SessionContext) { throw new Error('React Context is unavailable in Server Components') } const config = authConfigManager.getConfig() const session = useContext(SessionContext) const { required, onUnauthenticated } = options ?? {} const requiredAndNotLoading = required && session?.status === 'unauthenticated' useEffect(() => { if (requiredAndNotLoading) { const url = `${config.baseUrl}${config.basePath}/signin?${new URLSearchParams({ error: 'SessionRequired', callbackUrl: window.location.href, })}` if (onUnauthenticated) { onUnauthenticated() } else { window.location.href = url } } }, [requiredAndNotLoading, onUnauthenticated]) if (requiredAndNotLoading) { return { data: session?.data, update: session?.update, status: 'loading', } } return session as SessionContextValue } type ProvidersType = Record, ClientSafeProvider> export async function getProviders() { return fetchData('providers', authConfigManager.getConfig(), logger) } export async function signIn

( provider?: LiteralUnion< P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType >, options: SignInOptions = {}, authorizationParams: SignInAuthorizationParams = {} ): Promise

{ const { callbackUrl = window.location.href, redirect = true, ...opts } = options const config = authConfigManager.getConfig() const href = `${config.baseUrl}${config.basePath}` const providers = await getProviders() if (!providers) { window.location.href = `${href}/error` return } if (!provider || !(provider in providers)) { window.location.href = `${href}/signin?${new URLSearchParams({ callbackUrl })}` return } const isCredentials = providers[provider].type === 'credentials' const isEmail = providers[provider].type === 'email' const signInUrl = `${href}/${isCredentials ? 'callback' : 'signin'}/${provider}` const csrfToken = await getCsrfToken() const res = await fetch(`${signInUrl}?${new URLSearchParams(authorizationParams)}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Auth-Return-Redirect': '1', }, body: new URLSearchParams({ ...opts, csrfToken, callbackUrl }), credentials: config.credentials, }) const data = (await res.json()) as { url: string } if (redirect) { const url = data.url ?? callbackUrl window.location.href = url if (url.includes('#')) { window.location.reload() } return } const error = new URL(data.url).searchParams.get('error') if (res.ok) { await config.fetchSession?.({ event: 'storage' }) } return { error, status: res.status, ok: res.ok, url: error ? null : data.url, } as P extends RedirectableProviderType ? SignInResponse : undefined } export async function signOut( options?: SignOutParams ): Promise { const { callbackUrl = window.location.href, redirect = true } = options ?? {} const config = authConfigManager.getConfig() const csrfToken = await getCsrfToken() const res = await fetch(`${config.baseUrl}${config.basePath}/signout`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Auth-Return-Redirect': '1', }, body: new URLSearchParams({ csrfToken, callbackUrl }), credentials: config.credentials, }) const data = (await res.json()) as { url: string } if (redirect) { const url = data.url ?? callbackUrl window.location.href = url if (url.includes('#')) { window.location.reload() } return undefined as R extends true ? undefined : SignOutResponse } await config.fetchSession?.({ event: 'storage' }) return data as R extends true ? undefined : SignOutResponse } const createPopup = ({ url, title, height, width }: WindowProps) => { const left = window.screenX + (window.outerWidth - width) / 2 const top = window.screenY + (window.outerHeight - height) / 2.5 const externalPopup = window.open( url, title, `width=${width},height=${height},left=${left},top=${top}` ) return externalPopup } interface PopupLoginOptions extends Partial> { onSuccess?: () => void callbackUrl?: string } export const useOauthPopupLogin = ( provider: Parameters[0], options: PopupLoginOptions = {} ) => { const { width = 500, height = 500, title = 'Signin', onSuccess, callbackUrl = '/' } = options const [externalWindow, setExternalWindow] = useState() const [state, setState] = useState({ status: 'loading' }) const popUpSignin = useCallback(async () => { const res = await signIn(provider, { redirect: false, callbackUrl, }) if (res?.error) { setState({ status: 'errored', error: res.error }) return } setExternalWindow( createPopup({ url: res?.url as string, title, width, height, }) ) }, []) useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.origin !== window.location.origin) return if (event.data.status) { setState(event.data) if (event.data.status === 'success') { onSuccess?.() } externalWindow?.close() } } window.addEventListener('message', handleMessage) return () => { window.removeEventListener('message', handleMessage) externalWindow?.close() } }, [externalWindow]) return { popUpSignin, ...state } }