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()
|
const getTime = useTimeSinceFirstRender()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bets && contractsById) {
|
if (bets && contractsById && signedInUser) {
|
||||||
trackLatency('portfolio', getTime())
|
trackLatency(signedInUser.id, 'portfolio', getTime())
|
||||||
}
|
}
|
||||||
}, [bets, contractsById, getTime])
|
}, [signedInUser, bets, contractsById, getTime])
|
||||||
|
|
||||||
if (!bets || !contractsById) {
|
if (!bets || !contractsById) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
|
|
|
@ -23,6 +23,7 @@ import BetRow from '../bet-row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { ActivityItem } from './activity-items'
|
import { ActivityItem } from './activity-items'
|
||||||
import { useSaveSeenContract } from 'web/hooks/use-seen-contracts'
|
import { useSaveSeenContract } from 'web/hooks/use-seen-contracts'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { trackClick } from 'web/lib/firebase/tracking'
|
import { trackClick } from 'web/lib/firebase/tracking'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import NewContractBadge from '../new-contract-badge'
|
import NewContractBadge from '../new-contract-badge'
|
||||||
|
@ -118,6 +119,7 @@ export function FeedQuestion(props: {
|
||||||
const { volumeLabel } = contractMetrics(contract)
|
const { volumeLabel } = contractMetrics(contract)
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
const isNew = createdTime > Date.now() - DAY_MS && !isResolved
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex gap-2'}>
|
<div className={'flex gap-2'}>
|
||||||
|
@ -149,7 +151,7 @@ export function FeedQuestion(props: {
|
||||||
href={
|
href={
|
||||||
props.contractPath ? props.contractPath : contractPath(contract)
|
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"
|
className="text-lg text-indigo-700 sm:text-xl"
|
||||||
>
|
>
|
||||||
{question}
|
{question}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const useAlgoFeed = (
|
||||||
getDefaultFeed().then((feed) => setAllFeed(feed))
|
getDefaultFeed().then((feed) => setAllFeed(feed))
|
||||||
} else setAllFeed(feed)
|
} else setAllFeed(feed)
|
||||||
|
|
||||||
trackLatency('feed', getTime())
|
trackLatency(user.id, 'feed', getTime())
|
||||||
console.log('"all" feed load time', getTime())
|
console.log('"all" feed load time', getTime())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { trackView } from 'web/lib/firebase/tracking'
|
import { trackView } from 'web/lib/firebase/tracking'
|
||||||
import { useIsVisible } from './use-is-visible'
|
import { useIsVisible } from './use-is-visible'
|
||||||
|
import { useUser } from './use-user'
|
||||||
|
|
||||||
export const useSeenContracts = () => {
|
export const useSeenContracts = () => {
|
||||||
const [seenContracts, setSeenContracts] = useState<{
|
const [seenContracts, setSeenContracts] = useState<{
|
||||||
|
@ -21,18 +22,19 @@ export const useSaveSeenContract = (
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) => {
|
) => {
|
||||||
const isVisible = useIsVisible(elem)
|
const isVisible = useIsVisible(elem)
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible && user) {
|
||||||
const newSeenContracts = {
|
const newSeenContracts = {
|
||||||
...getSeenContracts(),
|
...getSeenContracts(),
|
||||||
[contract.id]: Date.now(),
|
[contract.id]: Date.now(),
|
||||||
}
|
}
|
||||||
localStorage.setItem(key, JSON.stringify(newSeenContracts))
|
localStorage.setItem(key, JSON.stringify(newSeenContracts))
|
||||||
|
|
||||||
trackView(contract.id)
|
trackView(user.id, contract.id)
|
||||||
}
|
}
|
||||||
}, [isVisible, contract])
|
}, [isVisible, user, contract])
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = 'feed-seen-contracts'
|
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 { useFirestoreDocumentData } from '@react-query-firebase/firestore'
|
||||||
import { QueryClient } from 'react-query'
|
import { QueryClient } from 'react-query'
|
||||||
|
|
||||||
|
@ -6,32 +6,14 @@ import { doc, DocumentData } from 'firebase/firestore'
|
||||||
import { PrivateUser } from 'common/user'
|
import { PrivateUser } from 'common/user'
|
||||||
import {
|
import {
|
||||||
getUser,
|
getUser,
|
||||||
listenForLogin,
|
|
||||||
listenForPrivateUser,
|
listenForPrivateUser,
|
||||||
listenForUser,
|
|
||||||
User,
|
User,
|
||||||
users,
|
users,
|
||||||
} from 'web/lib/firebase/users'
|
} from 'web/lib/firebase/users'
|
||||||
import { useStateCheckEquality } from './use-state-check-equality'
|
import { AuthContext } from 'web/components/auth-context'
|
||||||
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
|
|
||||||
|
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
const [user, setUser] = useStateCheckEquality<User | null | undefined>(
|
return useContext(AuthContext)
|
||||||
undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => listenForLogin(setUser), [setUser])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
identifyUser(user.id)
|
|
||||||
setUserProperty('username', user.username)
|
|
||||||
|
|
||||||
return listenForUser(user.id, setUser)
|
|
||||||
}
|
|
||||||
}, [user, setUser])
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePrivateUser = (userId?: string) => {
|
export const usePrivateUser = (userId?: string) => {
|
||||||
|
|
|
@ -2,16 +2,9 @@ import { doc, collection, setDoc } from 'firebase/firestore'
|
||||||
|
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { ClickEvent, LatencyEvent, View } from 'common/tracking'
|
import { ClickEvent, LatencyEvent, View } from 'common/tracking'
|
||||||
import { listenForLogin, User } from './users'
|
|
||||||
|
|
||||||
let user: User | null = null
|
export async function trackView(userId: string, contractId: string) {
|
||||||
if (typeof window !== 'undefined') {
|
const ref = doc(collection(db, 'private-users', userId, 'views'))
|
||||||
listenForLogin((u) => (user = u))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function trackView(contractId: string) {
|
|
||||||
if (!user) return
|
|
||||||
const ref = doc(collection(db, 'private-users', user.id, 'views'))
|
|
||||||
|
|
||||||
const view: View = {
|
const view: View = {
|
||||||
contractId,
|
contractId,
|
||||||
|
@ -21,9 +14,8 @@ export async function trackView(contractId: string) {
|
||||||
return await setDoc(ref, view)
|
return await setDoc(ref, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackClick(contractId: string) {
|
export async function trackClick(userId: string, contractId: string) {
|
||||||
if (!user) return
|
const ref = doc(collection(db, 'private-users', userId, 'events'))
|
||||||
const ref = doc(collection(db, 'private-users', user.id, 'events'))
|
|
||||||
|
|
||||||
const clickEvent: ClickEvent = {
|
const clickEvent: ClickEvent = {
|
||||||
type: 'click',
|
type: 'click',
|
||||||
|
@ -35,11 +27,11 @@ export async function trackClick(contractId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackLatency(
|
export async function trackLatency(
|
||||||
|
userId: string,
|
||||||
type: 'feed' | 'portfolio',
|
type: 'feed' | 'portfolio',
|
||||||
latency: number
|
latency: number
|
||||||
) {
|
) {
|
||||||
if (!user) return
|
const ref = doc(collection(db, 'private-users', userId, 'latency'))
|
||||||
const ref = doc(collection(db, 'private-users', user.id, 'latency'))
|
|
||||||
|
|
||||||
const latencyEvent: LatencyEvent = {
|
const latencyEvent: LatencyEvent = {
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -15,15 +15,10 @@ import {
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { getAuth } from 'firebase/auth'
|
import { getAuth } from 'firebase/auth'
|
||||||
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
|
||||||
import {
|
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'
|
||||||
onIdTokenChanged,
|
|
||||||
GoogleAuthProvider,
|
|
||||||
signInWithPopup,
|
|
||||||
} from 'firebase/auth'
|
|
||||||
import { zip } from 'lodash'
|
import { zip } from 'lodash'
|
||||||
import { app, db } from './init'
|
import { app, db } from './init'
|
||||||
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
|
||||||
import { createUser } from './api'
|
|
||||||
import {
|
import {
|
||||||
coll,
|
coll,
|
||||||
getValue,
|
getValue,
|
||||||
|
@ -37,13 +32,11 @@ import { safeLocalStorage } from '../util/local'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
import { addUserToGroupViaId } from 'web/lib/firebase/groups'
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { randomString } from 'common/util/random'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { deleteAuthCookies, setAuthCookies } from './auth'
|
|
||||||
|
|
||||||
export const users = coll<User>('users')
|
export const users = coll<User>('users')
|
||||||
export const privateUsers = coll<PrivateUser>('private-users')
|
export const privateUsers = coll<PrivateUser>('private-users')
|
||||||
|
@ -97,7 +90,6 @@ export function listenForPrivateUser(
|
||||||
return listenForValue<PrivateUser>(userRef, setPrivateUser)
|
return listenForValue<PrivateUser>(userRef, setPrivateUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
|
||||||
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
||||||
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
||||||
const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_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)
|
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 (!user || user.referredByUserId) return
|
||||||
// if the user wasn't created in the last minute, don't bother
|
// if the user wasn't created in the last minute, don't bother
|
||||||
const now = dayjs().utc()
|
const now = dayjs().utc()
|
||||||
|
@ -181,46 +173,6 @@ async function setCachedReferralInfoForUser(user: User | null) {
|
||||||
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
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() {
|
export async function firebaseLogin() {
|
||||||
const provider = new GoogleAuthProvider()
|
const provider = new GoogleAuthProvider()
|
||||||
return signInWithPopup(auth, provider)
|
return signInWithPopup(auth, provider)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Head from 'next/head'
|
||||||
import Script from 'next/script'
|
import Script from 'next/script'
|
||||||
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
|
import { AuthProvider } from 'web/components/auth-context'
|
||||||
|
|
||||||
function firstLine(msg: string) {
|
function firstLine(msg: string) {
|
||||||
return msg.replace(/\r?\n.*/s, '')
|
return msg.replace(/\r?\n.*/s, '')
|
||||||
|
@ -78,9 +79,11 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
<AuthProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</AuthProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user