Make a React context to be the source of truth for authenticated user (#675)
* Make a React context to manage the logged in user events * Remove unnecessary new user creation promise machinery * Slight refactoring to auth context code * Improvements in response to James feedback
This commit is contained in:
parent
8aa360c853
commit
03858e4a8c
77
web/components/auth-context.tsx
Normal file
77
web/components/auth-context.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { createContext, useEffect } from 'react'
|
||||
import { User } from 'common/user'
|
||||
import { onIdTokenChanged } from 'firebase/auth'
|
||||
import {
|
||||
auth,
|
||||
listenForUser,
|
||||
getUser,
|
||||
setCachedReferralInfoForUser,
|
||||
} from 'web/lib/firebase/users'
|
||||
import { deleteAuthCookies, setAuthCookies } 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 (User).
|
||||
type AuthUser = undefined | null | User
|
||||
|
||||
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
||||
|
||||
const ensureDeviceToken = () => {
|
||||
let deviceToken = localStorage.getItem('device-token')
|
||||
if (!deviceToken) {
|
||||
deviceToken = randomString()
|
||||
localStorage.setItem('device-token', deviceToken)
|
||||
}
|
||||
return deviceToken
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthUser>(null)
|
||||
|
||||
export function AuthProvider({ children }: any) {
|
||||
const [authUser, setAuthUser] = useStateCheckEquality<AuthUser>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const cachedUser = localStorage.getItem(CACHED_USER_KEY)
|
||||
setAuthUser(cachedUser && JSON.parse(cachedUser))
|
||||
}, [setAuthUser])
|
||||
|
||||
useEffect(() => {
|
||||
return onIdTokenChanged(auth, async (fbUser) => {
|
||||
if (fbUser) {
|
||||
let user = await getUser(fbUser.uid)
|
||||
if (!user) {
|
||||
const deviceToken = ensureDeviceToken()
|
||||
user = (await createUser({ deviceToken })) as User
|
||||
}
|
||||
setAuthUser(user)
|
||||
// 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)
|
||||
setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken)
|
||||
} else {
|
||||
// User logged out; reset to null
|
||||
setAuthUser(null)
|
||||
localStorage.removeItem(CACHED_USER_KEY)
|
||||
deleteAuthCookies()
|
||||
}
|
||||
})
|
||||
}, [setAuthUser])
|
||||
|
||||
const authUserId = authUser?.id
|
||||
const authUsername = authUser?.username
|
||||
useEffect(() => {
|
||||
if (authUserId && authUsername) {
|
||||
identifyUser(authUserId)
|
||||
setUserProperty('username', authUsername)
|
||||
return listenForUser(authUserId, setAuthUser)
|
||||
}
|
||||
}, [authUserId, authUsername, setAuthUser])
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={authUser}>{children}</AuthContext.Provider>
|
||||
)
|
||||
}
|
|
@ -78,10 +78,10 @@ export function BetsList(props: {
|
|||
|
||||
const getTime = useTimeSinceFirstRender()
|
||||
useEffect(() => {
|
||||
if (bets && contractsById) {
|
||||
trackLatency('portfolio', getTime())
|
||||
if (bets && contractsById && signedInUser) {
|
||||
trackLatency(signedInUser.id, 'portfolio', getTime())
|
||||
}
|
||||
}, [bets, contractsById, getTime])
|
||||
}, [signedInUser, bets, contractsById, getTime])
|
||||
|
||||
if (!bets || !contractsById) {
|
||||
return <LoadingIndicator />
|
||||
|
|
|
@ -23,6 +23,7 @@ import BetRow from '../bet-row'
|
|||
import { Avatar } from '../avatar'
|
||||
import { ActivityItem } from './activity-items'
|
||||
import { useSaveSeenContract } from 'web/hooks/use-seen-contracts'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { trackClick } from 'web/lib/firebase/tracking'
|
||||
import { DAY_MS } from 'common/util/time'
|
||||
import NewContractBadge from '../new-contract-badge'
|
||||
|
@ -118,6 +119,7 @@ export function FeedQuestion(props: {
|
|||
const { volumeLabel } = contractMetrics(contract)
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<div className={'flex gap-2'}>
|
||||
|
@ -149,7 +151,7 @@ export function FeedQuestion(props: {
|
|||
href={
|
||||
props.contractPath ? props.contractPath : contractPath(contract)
|
||||
}
|
||||
onClick={() => trackClick(contract.id)}
|
||||
onClick={() => user && trackClick(user.id, contract.id)}
|
||||
className="text-lg text-indigo-700 sm:text-xl"
|
||||
>
|
||||
{question}
|
||||
|
|
|
@ -25,7 +25,7 @@ export const useAlgoFeed = (
|
|||
getDefaultFeed().then((feed) => setAllFeed(feed))
|
||||
} else setAllFeed(feed)
|
||||
|
||||
trackLatency('feed', getTime())
|
||||
trackLatency(user.id, 'feed', getTime())
|
||||
console.log('"all" feed load time', getTime())
|
||||
})
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
|||
import { Contract } from 'common/contract'
|
||||
import { trackView } from 'web/lib/firebase/tracking'
|
||||
import { useIsVisible } from './use-is-visible'
|
||||
import { useUser } from './use-user'
|
||||
|
||||
export const useSeenContracts = () => {
|
||||
const [seenContracts, setSeenContracts] = useState<{
|
||||
|
@ -21,18 +22,19 @@ export const useSaveSeenContract = (
|
|||
contract: Contract
|
||||
) => {
|
||||
const isVisible = useIsVisible(elem)
|
||||
const user = useUser()
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
if (isVisible && user) {
|
||||
const newSeenContracts = {
|
||||
...getSeenContracts(),
|
||||
[contract.id]: Date.now(),
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(newSeenContracts))
|
||||
|
||||
trackView(contract.id)
|
||||
trackView(user.id, contract.id)
|
||||
}
|
||||
}, [isVisible, contract])
|
||||
}, [isVisible, user, contract])
|
||||
}
|
||||
|
||||
const key = 'feed-seen-contracts'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
|
||||
import { QueryClient } from 'react-query'
|
||||
|
||||
|
@ -6,32 +6,14 @@ import { doc, DocumentData } from 'firebase/firestore'
|
|||
import { PrivateUser } from 'common/user'
|
||||
import {
|
||||
getUser,
|
||||
listenForLogin,
|
||||
listenForPrivateUser,
|
||||
listenForUser,
|
||||
User,
|
||||
users,
|
||||
} from 'web/lib/firebase/users'
|
||||
import { useStateCheckEquality } from './use-state-check-equality'
|
||||
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
|
||||
import { AuthContext } from 'web/components/auth-context'
|
||||
|
||||
export const useUser = () => {
|
||||
const [user, setUser] = useStateCheckEquality<User | null | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
useEffect(() => listenForLogin(setUser), [setUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
identifyUser(user.id)
|
||||
setUserProperty('username', user.username)
|
||||
|
||||
return listenForUser(user.id, setUser)
|
||||
}
|
||||
}, [user, setUser])
|
||||
|
||||
return user
|
||||
return useContext(AuthContext)
|
||||
}
|
||||
|
||||
export const usePrivateUser = (userId?: string) => {
|
||||
|
|
|
@ -2,16 +2,9 @@ import { doc, collection, setDoc } from 'firebase/firestore'
|
|||
|
||||
import { db } from './init'
|
||||
import { ClickEvent, LatencyEvent, View } from 'common/tracking'
|
||||
import { listenForLogin, User } from './users'
|
||||
|
||||
let user: User | null = null
|
||||
if (typeof window !== 'undefined') {
|
||||
listenForLogin((u) => (user = u))
|
||||
}
|
||||
|
||||
export async function trackView(contractId: string) {
|
||||
if (!user) return
|
||||
const ref = doc(collection(db, 'private-users', user.id, 'views'))
|
||||
export async function trackView(userId: string, contractId: string) {
|
||||
const ref = doc(collection(db, 'private-users', userId, 'views'))
|
||||
|
||||
const view: View = {
|
||||
contractId,
|
||||
|
@ -21,9 +14,8 @@ export async function trackView(contractId: string) {
|
|||
return await setDoc(ref, view)
|
||||
}
|
||||
|
||||
export async function trackClick(contractId: string) {
|
||||
if (!user) return
|
||||
const ref = doc(collection(db, 'private-users', user.id, 'events'))
|
||||
export async function trackClick(userId: string, contractId: string) {
|
||||
const ref = doc(collection(db, 'private-users', userId, 'events'))
|
||||
|
||||
const clickEvent: ClickEvent = {
|
||||
type: 'click',
|
||||
|
@ -35,11 +27,11 @@ export async function trackClick(contractId: string) {
|
|||
}
|
||||
|
||||
export async function trackLatency(
|
||||
userId: string,
|
||||
type: 'feed' | 'portfolio',
|
||||
latency: number
|
||||
) {
|
||||
if (!user) return
|
||||
const ref = doc(collection(db, 'private-users', user.id, 'latency'))
|
||||
const ref = doc(collection(db, 'private-users', userId, 'latency'))
|
||||
|
||||
const latencyEvent: LatencyEvent = {
|
||||
type,
|
||||
|
|
|
@ -15,15 +15,10 @@ import {
|
|||
} from 'firebase/firestore'
|
||||
import { getAuth } from 'firebase/auth'
|
||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
||||
import {
|
||||
onIdTokenChanged,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup,
|
||||
} from 'firebase/auth'
|
||||
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
||||
import { zip } from 'lodash'
|
||||
import { app, db } from './init'
|
||||
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
||||
import { createUser } from './api'
|
||||
import {
|
||||
coll,
|
||||
getValue,
|
||||
|
@ -37,13 +32,11 @@ import { safeLocalStorage } from '../util/local'
|
|||
import { filterDefined } from 'common/util/array'
|
||||
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
import { randomString } from 'common/util/random'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
dayjs.extend(utc)
|
||||
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { deleteAuthCookies, setAuthCookies } from './auth'
|
||||
|
||||
export const users = coll<User>('users')
|
||||
export const privateUsers = coll<PrivateUser>('private-users')
|
||||
|
@ -97,7 +90,6 @@ export function listenForPrivateUser(
|
|||
return listenForValue<PrivateUser>(userRef, setPrivateUser)
|
||||
}
|
||||
|
||||
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
||||
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
||||
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
||||
const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY'
|
||||
|
@ -130,7 +122,7 @@ export function writeReferralInfo(
|
|||
local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)
|
||||
}
|
||||
|
||||
async function setCachedReferralInfoForUser(user: User | null) {
|
||||
export async function setCachedReferralInfoForUser(user: User | null) {
|
||||
if (!user || user.referredByUserId) return
|
||||
// if the user wasn't created in the last minute, don't bother
|
||||
const now = dayjs().utc()
|
||||
|
@ -181,46 +173,6 @@ async function setCachedReferralInfoForUser(user: User | null) {
|
|||
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
||||
}
|
||||
|
||||
// used to avoid weird race condition
|
||||
let createUserPromise: Promise<User> | undefined = undefined
|
||||
|
||||
export function listenForLogin(onUser: (user: User | null) => void) {
|
||||
const local = safeLocalStorage()
|
||||
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
||||
onUser(cachedUser && JSON.parse(cachedUser))
|
||||
|
||||
return onIdTokenChanged(auth, async (fbUser) => {
|
||||
if (fbUser) {
|
||||
let user: User | null = await getUser(fbUser.uid)
|
||||
if (!user) {
|
||||
if (createUserPromise == null) {
|
||||
const local = safeLocalStorage()
|
||||
let deviceToken = local?.getItem('device-token')
|
||||
if (!deviceToken) {
|
||||
deviceToken = randomString()
|
||||
local?.setItem('device-token', deviceToken)
|
||||
}
|
||||
createUserPromise = createUser({ deviceToken }).then((r) => r as User)
|
||||
}
|
||||
user = await createUserPromise
|
||||
}
|
||||
onUser(user)
|
||||
|
||||
// Persist to local storage, to reduce login blink next time.
|
||||
// Note: Cap on localStorage size is ~5mb
|
||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||
setCachedReferralInfoForUser(user)
|
||||
setAuthCookies(await fbUser.getIdToken(), fbUser.refreshToken)
|
||||
} else {
|
||||
// User logged out; reset to null
|
||||
onUser(null)
|
||||
createUserPromise = undefined
|
||||
local?.removeItem(CACHED_USER_KEY)
|
||||
deleteAuthCookies()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function firebaseLogin() {
|
||||
const provider = new GoogleAuthProvider()
|
||||
return signInWithPopup(auth, provider)
|
||||
|
|
|
@ -5,6 +5,7 @@ import Head from 'next/head'
|
|||
import Script from 'next/script'
|
||||
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { AuthProvider } from 'web/components/auth-context'
|
||||
|
||||
function firstLine(msg: string) {
|
||||
return msg.replace(/\r?\n.*/s, '')
|
||||
|
@ -78,9 +79,11 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
/>
|
||||
</Head>
|
||||
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Component {...pageProps} />
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user