diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index 5610a9c3..2d6fff67 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -1,10 +1,11 @@ import { ReactNode, createContext, useEffect } from 'react' -import { User } from 'common/user' +import { User, PrivateUser } from 'common/user' import { onIdTokenChanged } from 'firebase/auth' import { auth, listenForUser, - getUser, + listenForPrivateUser, + getUserAndPrivateUser, setCachedReferralInfoForUser, } from 'web/lib/firebase/users' import { deleteTokenCookies, setTokenCookies } from 'web/lib/firebase/auth' @@ -13,11 +14,13 @@ 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 (User). -type AuthUser = undefined | null | User +type UserAndPrivateUser = { user: User; privateUser: PrivateUser } -const CACHED_USER_KEY = 'CACHED_USER_KEY' +// 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') @@ -36,6 +39,7 @@ export function AuthProvider(props: { }) { const { children, serverUser } = props const [authUser, setAuthUser] = useStateCheckEquality(serverUser) + useEffect(() => { if (serverUser === undefined) { const cachedUser = localStorage.getItem(CACHED_USER_KEY) @@ -50,16 +54,16 @@ export function AuthProvider(props: { id: await fbUser.getIdToken(), refresh: fbUser.refreshToken, }) - let user = await getUser(fbUser.uid) - if (!user) { + let current = await getUserAndPrivateUser(fbUser.uid) + if (!current) { const deviceToken = ensureDeviceToken() - user = (await createUser({ deviceToken })).user as User + current = (await createUser({ deviceToken })) as UserAndPrivateUser } - setAuthUser(user) + 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(user)) - setCachedReferralInfoForUser(user) + localStorage.setItem(CACHED_USER_KEY, JSON.stringify(current)) + setCachedReferralInfoForUser(current.user) } else { // User logged out; reset to null deleteTokenCookies() @@ -69,15 +73,30 @@ export function AuthProvider(props: { }) }, [setAuthUser]) - const authUserId = authUser?.id - const authUsername = authUser?.username + const uid = authUser?.user.id + const username = authUser?.user.username useEffect(() => { - if (authUserId && authUsername) { - identifyUser(authUserId) - setUserProperty('username', authUsername) - return listenForUser(authUserId, setAuthUser) + 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() + } } - }, [authUserId, authUsername, setAuthUser]) + }, [uid, username, setAuthUser]) return ( {children} diff --git a/web/hooks/use-user.ts b/web/hooks/use-user.ts index d84c7d03..85b23e30 100644 --- a/web/hooks/use-user.ts +++ b/web/hooks/use-user.ts @@ -13,7 +13,8 @@ import { import { AuthContext } from 'web/components/auth-context' export const useUser = () => { - return useContext(AuthContext) + const authUser = useContext(AuthContext) + return authUser ? authUser.user : authUser } export const usePrivateUser = (userId?: string) => { diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index cf2415ab..302bac10 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -57,6 +57,16 @@ export async function getPrivateUser(userId: string) { return (await getDoc(doc(privateUsers, userId))).data()! } +export async function getUserAndPrivateUser(userId: string) { + const [user, privateUser] = ( + await Promise.all([ + getDoc(doc(users, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + getDoc(doc(privateUsers, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + ]) + ).map((d) => d.data()) as [User, PrivateUser] + return { user, privateUser } +} + export async function getUserByUsername(username: string) { // Find a user whose username matches the given username, or null if no such user exists. const q = query(users, where('username', '==', username), limit(1))