manifold/web/components/auth-context.tsx
Marshall Polaris 3cb28cdecb
Teach AuthContext to manage the private user doc (#738)
* Return both user and privateUser from `createuser`

* Make `useStateCheckEquality` more flexible

* Make `AuthContext` track the private user doc

* Change `usePrivateUser` hook to use the auth context data

* Pass both user and private user through SSR to auth context

* Fix bug in create user flow
2022-08-12 13:41:00 -07:00

103 lines
3.3 KiB
TypeScript

import { ReactNode, createContext, useEffect } from 'react'
import { onIdTokenChanged } from 'firebase/auth'
import {
UserAndPrivateUser,
auth,
listenForUser,
listenForPrivateUser,
getUserAndPrivateUser,
setCachedReferralInfoForUser,
} from 'web/lib/firebase/users'
import { deleteTokenCookies, setTokenCookies } from 'web/lib/firebase/auth'
import { createUser } from 'web/lib/firebase/api'
import { randomString } from 'common/util/random'
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
import { useStateCheckEquality } from 'web/hooks/use-state-check-equality'
// Either we haven't looked up the logged in user yet (undefined), or we know
// the user is not logged in (null), or we know the user is logged in.
type AuthUser = undefined | null | UserAndPrivateUser
const CACHED_USER_KEY = 'CACHED_USER_KEY_V2'
const ensureDeviceToken = () => {
let deviceToken = localStorage.getItem('device-token')
if (!deviceToken) {
deviceToken = randomString()
localStorage.setItem('device-token', deviceToken)
}
return deviceToken
}
export const AuthContext = createContext<AuthUser>(undefined)
export function AuthProvider(props: {
children: ReactNode
serverUser?: AuthUser
}) {
const { children, serverUser } = props
const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(serverUser)
useEffect(() => {
if (serverUser === undefined) {
const cachedUser = localStorage.getItem(CACHED_USER_KEY)
setAuthUser(cachedUser && JSON.parse(cachedUser))
}
}, [setAuthUser, serverUser])
useEffect(() => {
return onIdTokenChanged(auth, async (fbUser) => {
if (fbUser) {
setTokenCookies({
id: await fbUser.getIdToken(),
refresh: fbUser.refreshToken,
})
let current = await getUserAndPrivateUser(fbUser.uid)
if (!current.user || !current.privateUser) {
const deviceToken = ensureDeviceToken()
current = (await createUser({ deviceToken })) as UserAndPrivateUser
}
setAuthUser(current)
// Persist to local storage, to reduce login blink next time.
// Note: Cap on localStorage size is ~5mb
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(current))
setCachedReferralInfoForUser(current.user)
} else {
// User logged out; reset to null
deleteTokenCookies()
setAuthUser(null)
localStorage.removeItem(CACHED_USER_KEY)
}
})
}, [setAuthUser])
const uid = authUser?.user.id
const username = authUser?.user.username
useEffect(() => {
if (uid && username) {
identifyUser(uid)
setUserProperty('username', username)
const userListener = listenForUser(uid, (user) =>
setAuthUser((authUser) => {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
return { ...authUser!, user: user! }
})
)
const privateUserListener = listenForPrivateUser(uid, (privateUser) => {
setAuthUser((authUser) => {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
return { ...authUser!, privateUser: privateUser! }
})
})
return () => {
userListener()
privateUserListener()
}
}
}, [uid, username, setAuthUser])
return (
<AuthContext.Provider value={authUser}>{children}</AuthContext.Provider>
)
}