-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAuthSessionProvider.tsx
More file actions
118 lines (108 loc) · 3.46 KB
/
AuthSessionProvider.tsx
File metadata and controls
118 lines (108 loc) · 3.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
'use client';
import {
createContext,
useContext,
useEffect,
useRef,
useState,
type ReactNode,
type ReactElement,
} from 'react';
import { useDispatch } from 'react-redux';
import { app } from '../../firebase';
import { anonymousLogin } from '../store/profile-reducer';
import { setUserCookieSession } from '../services/session-service';
interface AuthSession {
isAuthReady: boolean;
email: string | null;
isAuthenticated: boolean;
displayName?: string | null;
}
const AuthReadyContext = createContext<AuthSession>({
isAuthReady: false,
email: null,
isAuthenticated: false,
displayName: null,
});
/**
* Returns the current auth session state once Firebase has resolved.
* Use this instead of registering your own `onAuthStateChanged` listener.
*/
export function useAuthSession(): AuthSession {
return useContext(AuthReadyContext);
}
/**
* Global auth session provider. Renders inside the Redux provider tree
* and manages a single `onAuthStateChanged` listener that:
*
* 1. Triggers anonymous sign-in when no user exists.
* 2. Re-establishes the `md_session` cookie on return visits (Firebase
* restores auth from IndexedDB but the 1-hour cookie has expired).
* 3. Schedules the next renewal at exactly `expiresAt - 5 min` using
* a setTimeout derived from the value stored in localStorage.
* 4. Deduplicates POSTs across tabs — localStorage is shared across all
* tabs, so a renewal written by any tab is immediately visible to all
* others via the `isCookieFresh` check in setUserCookieSession.
* 5. Exposes `isAuthReady` via context.
*/
export function AuthSessionProvider({
children,
}: {
children: ReactNode;
}): ReactElement {
const dispatch = useDispatch();
const [session, setSession] = useState<AuthSession>({
isAuthReady: false,
email: null,
isAuthenticated: false,
displayName: null,
});
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
const unsubscribe = app.auth().onIdTokenChanged((user) => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (user != null) {
setSession({
isAuthReady: true,
email: user.email ?? null,
isAuthenticated: !user.isAnonymous,
displayName: user.displayName ?? null,
});
setUserCookieSession().catch(() => {
console.error('Failed to establish session cookie');
});
// Check every 5 minutes; the cookie lasts 60 minutes, so this ensures renewal well before expiry
// If the cookie is not expired, it will return early and skip the POST
// The token will refresh 5 minutes before expiry which is why the 5 minute interval is used here.
intervalRef.current = setInterval(
() => {
setUserCookieSession().catch(() => {
console.error('Failed to establish session cookie');
});
},
5 * 60 * 1000,
); // 5 minutes
} else {
setSession({
isAuthReady: false,
email: null,
isAuthenticated: false,
displayName: null,
});
dispatch(anonymousLogin());
}
});
return () => {
unsubscribe();
if (intervalRef.current != null) clearInterval(intervalRef.current);
};
}, [dispatch]);
return (
<AuthReadyContext.Provider value={session}>
{children}
</AuthReadyContext.Provider>
);
}