Rewrite localstorage context (#749)
* Rewrite the local storage context into a redux store for user data * Separate out the mega methods * Fix infinite loop * Missing dependency * Missing dependency, rename version field
This commit is contained in:
parent
b28ad2865d
commit
19a53cd999
@ -48,6 +48,7 @@
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"swr": "0.1.18",
|
||||
"use-media": "^1.4.0"
|
||||
|
@ -16,7 +16,7 @@ import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { useTokenAllowance } from '../../data/Allowances'
|
||||
import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
|
||||
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
|
||||
import { useAddUserToken, useFetchTokenByAddress } from '../../state/user/hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useAllTokens, useToken } from '../../contexts/Tokens'
|
||||
import { usePendingApproval, useTransactionAdder } from '../../contexts/Transactions'
|
||||
@ -113,7 +113,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
|
||||
}
|
||||
|
||||
// ensure input + output tokens are added to localstorage
|
||||
const [, { fetchTokenByAddress, addToken }] = useLocalStorageTokens()
|
||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
||||
const addToken = useAddUserToken()
|
||||
const allTokens = useAllTokens()
|
||||
const inputTokenAddress = fieldData[Field.INPUT].address
|
||||
useEffect(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Send, Sun, Moon } from 'react-feather'
|
||||
import { useDarkModeManager } from '../../contexts/LocalStorage'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
|
@ -14,7 +14,7 @@ import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
import { useDarkModeManager } from '../../contexts/LocalStorage'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
|
@ -16,7 +16,7 @@ import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
|
||||
import { useToken } from '../../contexts/Tokens'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
import { useLocalStoragePairAdder } from '../../contexts/LocalStorage'
|
||||
import { usePairAdder } from '../../state/user/hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
|
||||
const Fields = {
|
||||
@ -35,7 +35,7 @@ function PoolFinder({ history }: RouteComponentProps) {
|
||||
const token1: Token = useToken(token1Address)
|
||||
|
||||
const pair: Pair = usePair(token0, token1)
|
||||
const addPair = useLocalStoragePairAdder()
|
||||
const addPair = usePairAdder()
|
||||
|
||||
useEffect(() => {
|
||||
if (pair) {
|
||||
|
@ -24,7 +24,12 @@ import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { useLocalStorageTokens, useAllDummyPairs } from '../../contexts/LocalStorage'
|
||||
import {
|
||||
useAllDummyPairs,
|
||||
useFetchTokenByAddress,
|
||||
useAddUserToken,
|
||||
useRemoveUserAddedToken
|
||||
} from '../../state/user/hooks'
|
||||
import { useAllBalances } from '../../contexts/Balances'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToken, useAllTokens, ALL_TOKENS } from '../../contexts/Tokens'
|
||||
@ -179,7 +184,9 @@ function SearchModal({
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [sortDirection, setSortDirection] = useState(true)
|
||||
|
||||
const [, { fetchTokenByAddress, addToken, removeTokenByAddress }] = useLocalStorageTokens()
|
||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
||||
const addToken = useAddUserToken()
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it
|
||||
const token = useToken(searchQuery)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
import { useDarkModeManager } from '../../contexts/LocalStorage'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
|
@ -6,7 +6,7 @@ import { useAllTokens } from './Tokens'
|
||||
import { useWeb3React, useDebounce } from '../hooks'
|
||||
|
||||
import { getEtherBalance, getTokenBalance, isAddress } from '../utils'
|
||||
import { useAllDummyPairs } from './LocalStorage'
|
||||
import { useAllDummyPairs } from '../state/user/hooks'
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'BALANCES'
|
||||
const SHORT_BLOCK_TIMEOUT = (60 * 2) / 15 // in seconds, represented as a block number delta
|
||||
|
@ -1,325 +0,0 @@
|
||||
import React, { createContext, useContext, useMemo, useCallback, useEffect, useState } from 'react'
|
||||
import { Token, Pair, TokenAmount, JSBI, WETH, ChainId } from '@uniswap/sdk'
|
||||
import { getTokenDecimals, getTokenSymbol, getTokenName, isAddress } from '../utils'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAllTokens } from './Tokens'
|
||||
|
||||
enum LocalStorageKeys {
|
||||
VERSION = 'version',
|
||||
LAST_SAVED = 'lastSaved',
|
||||
BETA_MESSAGE_DISMISSED = 'betaMessageDismissed',
|
||||
MIGRATION_MESSAGE_DISMISSED = 'migrationMessageDismissed',
|
||||
DARK_MODE = 'darkMode',
|
||||
TOKENS = 'tokens',
|
||||
PAIRS = 'pairs'
|
||||
}
|
||||
|
||||
function useLocalStorage<T, S = T>(
|
||||
key: LocalStorageKeys,
|
||||
defaultValue: T,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ serialize, deserialize }: { serialize: (toSerialize: T) => S; deserialize: (toDeserialize: S) => T } = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serialize: (toSerialize): S => (toSerialize as unknown) as S,
|
||||
deserialize: (toDeserialize): T => (toDeserialize as unknown) as T
|
||||
}
|
||||
): [T, (value: T) => void] {
|
||||
const [value, setValue] = useState(() => {
|
||||
try {
|
||||
return deserialize(JSON.parse(window.localStorage.getItem(key))) ?? defaultValue
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(serialize(value)))
|
||||
} catch {}
|
||||
}, [key, serialize, value])
|
||||
|
||||
return [value, setValue]
|
||||
}
|
||||
|
||||
interface SerializedToken {
|
||||
chainId: number
|
||||
address: string
|
||||
decimals: number
|
||||
symbol: string
|
||||
name: string
|
||||
}
|
||||
|
||||
function serializeToken(token: Token): SerializedToken {
|
||||
return {
|
||||
chainId: token.chainId,
|
||||
address: token.address,
|
||||
decimals: token.decimals,
|
||||
symbol: token.symbol,
|
||||
name: token.name
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeToken(serializedToken: SerializedToken): Token {
|
||||
return new Token(
|
||||
serializedToken.chainId,
|
||||
serializedToken.address,
|
||||
serializedToken.decimals,
|
||||
serializedToken.symbol,
|
||||
serializedToken.name
|
||||
)
|
||||
}
|
||||
|
||||
const LocalStorageContext = createContext<[any, any]>([{}, {}])
|
||||
|
||||
function useLocalStorageContext() {
|
||||
return useContext(LocalStorageContext)
|
||||
}
|
||||
|
||||
export default function Provider({ children }: { children: React.ReactNode }) {
|
||||
// global localstorage state
|
||||
const [version, setVersion] = useLocalStorage<number>(LocalStorageKeys.VERSION, 0)
|
||||
const [lastSaved, setLastSaved] = useLocalStorage<number>(LocalStorageKeys.LAST_SAVED, Math.floor(Date.now() / 1000))
|
||||
const [betaMessageDismissed, setBetaMessageDismissed] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.BETA_MESSAGE_DISMISSED,
|
||||
false
|
||||
)
|
||||
const [migrationMessageDismissed, setMigrationMessageDismissed] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.MIGRATION_MESSAGE_DISMISSED,
|
||||
false
|
||||
)
|
||||
const [darkMode, setDarkMode] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.DARK_MODE,
|
||||
window?.matchMedia('(prefers-color-scheme: dark)')?.matches ? true : false
|
||||
)
|
||||
const [tokens, setTokens] = useLocalStorage<Token[], SerializedToken[]>(LocalStorageKeys.TOKENS, [], {
|
||||
serialize: (tokens: Token[]) => tokens.map(serializeToken),
|
||||
deserialize: (serializedTokens: SerializedToken[]) => serializedTokens.map(deserializeToken)
|
||||
})
|
||||
const [pairs, setPairs] = useLocalStorage<Token[][], SerializedToken[][]>(LocalStorageKeys.PAIRS, [], {
|
||||
serialize: (nestedTokens: Token[][]) => nestedTokens.map(tokens => tokens.map(serializeToken)),
|
||||
deserialize: (serializedNestedTokens: SerializedToken[][]) =>
|
||||
serializedNestedTokens.map(serializedTokens => serializedTokens.map(deserializeToken))
|
||||
})
|
||||
|
||||
return (
|
||||
<LocalStorageContext.Provider
|
||||
value={useMemo(
|
||||
() => [
|
||||
{ version, lastSaved, betaMessageDismissed, migrationMessageDismissed, darkMode, tokens, pairs },
|
||||
{
|
||||
setVersion,
|
||||
setLastSaved,
|
||||
setBetaMessageDismissed,
|
||||
setMigrationMessageDismissed,
|
||||
setDarkMode,
|
||||
setTokens,
|
||||
setPairs
|
||||
}
|
||||
],
|
||||
[
|
||||
version,
|
||||
lastSaved,
|
||||
betaMessageDismissed,
|
||||
migrationMessageDismissed,
|
||||
darkMode,
|
||||
|
||||
tokens,
|
||||
pairs,
|
||||
setVersion,
|
||||
setLastSaved,
|
||||
setBetaMessageDismissed,
|
||||
setMigrationMessageDismissed,
|
||||
setDarkMode,
|
||||
setTokens,
|
||||
setPairs
|
||||
]
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</LocalStorageContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function Updater() {
|
||||
const [, { setDarkMode }] = useLocalStorageContext()
|
||||
|
||||
useEffect(() => {
|
||||
const darkHandler = (match: MediaQueryListEvent) => {
|
||||
if (match.matches) {
|
||||
setDarkMode(true)
|
||||
}
|
||||
}
|
||||
const lightHandler = (match: MediaQueryListEvent) => {
|
||||
if (match.matches) {
|
||||
setDarkMode(false)
|
||||
}
|
||||
}
|
||||
|
||||
window?.matchMedia('(prefers-color-scheme: dark)')?.addListener(darkHandler)
|
||||
window?.matchMedia('(prefers-color-scheme: light)')?.addListener(lightHandler)
|
||||
|
||||
return () => {
|
||||
window?.matchMedia('(prefers-color-scheme: dark)')?.removeListener(darkHandler)
|
||||
window?.matchMedia('(prefers-color-scheme: light)')?.removeListener(lightHandler)
|
||||
}
|
||||
}, [setDarkMode])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function useBetaMessageManager() {
|
||||
const [{ betaMessageDismissed }, { setBetaMessageDismissed }] = useLocalStorageContext()
|
||||
|
||||
const dismissBetaMessage = useCallback(() => {
|
||||
setBetaMessageDismissed(true)
|
||||
}, [setBetaMessageDismissed])
|
||||
|
||||
return [!betaMessageDismissed, dismissBetaMessage]
|
||||
}
|
||||
|
||||
export function useMigrationMessageManager() {
|
||||
const [{ migrationMessageDismissed }, { setMigrationMessageDismissed }] = useLocalStorageContext()
|
||||
|
||||
const dismissMigrationMessage = useCallback(() => {
|
||||
setMigrationMessageDismissed(true)
|
||||
}, [setMigrationMessageDismissed])
|
||||
|
||||
return [!migrationMessageDismissed, dismissMigrationMessage]
|
||||
}
|
||||
|
||||
export function useDarkModeManager() {
|
||||
const [{ darkMode }, { setDarkMode }] = useLocalStorageContext()
|
||||
|
||||
const toggleSetDarkMode = useCallback(
|
||||
value => {
|
||||
setDarkMode(typeof value === 'boolean' ? value : !darkMode)
|
||||
},
|
||||
[darkMode, setDarkMode]
|
||||
)
|
||||
|
||||
return [darkMode, toggleSetDarkMode]
|
||||
}
|
||||
|
||||
export function useLocalStorageTokens(): [
|
||||
Token[],
|
||||
{
|
||||
fetchTokenByAddress: (address: string) => Promise<Token | null>
|
||||
addToken: (token: Token) => void
|
||||
removeTokenByAddress: (chainId: number, address: string) => void
|
||||
}
|
||||
] {
|
||||
const { library, chainId } = useWeb3React()
|
||||
const [{ tokens }, { setTokens }] = useLocalStorageContext()
|
||||
|
||||
const fetchTokenByAddress = useCallback(
|
||||
async (address: string) => {
|
||||
const [decimals, symbol, name] = await Promise.all([
|
||||
getTokenDecimals(address, library).catch(() => null),
|
||||
getTokenSymbol(address, library).catch(() => 'UNKNOWN'),
|
||||
getTokenName(address, library).catch(() => 'Unknown')
|
||||
])
|
||||
|
||||
if (decimals === null) {
|
||||
return null
|
||||
} else {
|
||||
return new Token(chainId, address, decimals, symbol, name)
|
||||
}
|
||||
},
|
||||
[library, chainId]
|
||||
)
|
||||
|
||||
const addToken = useCallback(
|
||||
(token: Token) => {
|
||||
setTokens(tokens => tokens.filter(currentToken => !currentToken.equals(token)).concat([token]))
|
||||
},
|
||||
[setTokens]
|
||||
)
|
||||
|
||||
const removeTokenByAddress = useCallback(
|
||||
(chainId: number, address: string) => {
|
||||
setTokens(tokens =>
|
||||
tokens.filter(
|
||||
currentToken => !(currentToken.chainId === chainId && currentToken.address === isAddress(address))
|
||||
)
|
||||
)
|
||||
},
|
||||
[setTokens]
|
||||
)
|
||||
|
||||
return [tokens, { fetchTokenByAddress, addToken, removeTokenByAddress }]
|
||||
}
|
||||
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
export function useLocalStoragePairAdder(): (pair: Pair) => void {
|
||||
const [, { setPairs }] = useLocalStorageContext()
|
||||
|
||||
return useCallback(
|
||||
(pair: Pair) => {
|
||||
setPairs(pairs =>
|
||||
pairs
|
||||
.filter(tokens => !(tokens[0].equals(pair.token0) && tokens[1].equals(pair.token1)))
|
||||
.concat([[pair.token0, pair.token1]])
|
||||
)
|
||||
},
|
||||
[setPairs]
|
||||
)
|
||||
}
|
||||
|
||||
const bases = [
|
||||
...Object.values(WETH),
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
]
|
||||
|
||||
export function useAllDummyPairs(): Pair[] {
|
||||
const { chainId } = useWeb3React()
|
||||
const tokens = useAllTokens()
|
||||
const generatedPairs: Pair[] = useMemo(
|
||||
() =>
|
||||
Object.values(tokens)
|
||||
// select only tokens on the current chain
|
||||
.filter(token => token.chainId === chainId)
|
||||
.flatMap(token => {
|
||||
// for each token on the current chain,
|
||||
return (
|
||||
bases
|
||||
// loop through all the bases valid for the current chain,
|
||||
.filter(base => base.chainId === chainId)
|
||||
// to construct pairs of the given token with each base
|
||||
.map(base => {
|
||||
if (base.equals(token)) {
|
||||
return null
|
||||
} else {
|
||||
return new Pair(new TokenAmount(base, ZERO), new TokenAmount(token, ZERO))
|
||||
}
|
||||
})
|
||||
.filter(pair => !!pair)
|
||||
)
|
||||
}),
|
||||
[tokens, chainId]
|
||||
)
|
||||
|
||||
const [{ pairs }] = useLocalStorageContext()
|
||||
const userPairs = useMemo(
|
||||
() =>
|
||||
pairs
|
||||
.filter(tokens => tokens[0].chainId === chainId)
|
||||
.map(tokens => new Pair(new TokenAmount(tokens[0], ZERO), new TokenAmount(tokens[1], ZERO))),
|
||||
[pairs, chainId]
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
generatedPairs
|
||||
.concat(userPairs)
|
||||
// filter out duplicate pairs
|
||||
.filter((pair, i, concatenatedPairs) => {
|
||||
const firstAppearance = concatenatedPairs.findIndex(
|
||||
concatenatedPair =>
|
||||
concatenatedPair.token0.equals(pair.token0) && concatenatedPair.token1.equals(pair.token1)
|
||||
)
|
||||
return i === firstAppearance
|
||||
})
|
||||
)
|
||||
}, [generatedPairs, userPairs])
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { ChainId, WETH, Token } from '@uniswap/sdk'
|
||||
import { useWeb3React } from '../hooks'
|
||||
import { useLocalStorageTokens } from './LocalStorage'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
|
||||
export const ALL_TOKENS = [
|
||||
// WETH on all chains
|
||||
@ -47,28 +47,22 @@ export const ALL_TOKENS = [
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const { chainId } = useWeb3React()
|
||||
const [localStorageTokens] = useLocalStorageTokens()
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
localStorageTokens
|
||||
// filter to the current chain
|
||||
.filter(token => token.chainId === chainId)
|
||||
userAddedTokens
|
||||
// reduce into all ALL_TOKENS filtered by the current chain
|
||||
.reduce((tokenMap, token) => {
|
||||
return {
|
||||
...tokenMap,
|
||||
[token.address]: token
|
||||
}
|
||||
.reduce<{ [address: string]: Token }>((tokenMap, token) => {
|
||||
tokenMap[token.address] = token
|
||||
return tokenMap
|
||||
}, ALL_TOKENS?.[chainId] ?? {})
|
||||
)
|
||||
}, [localStorageTokens, chainId])
|
||||
}, [userAddedTokens, chainId])
|
||||
}
|
||||
|
||||
export function useToken(tokenAddress: string): Token {
|
||||
const tokens = useAllTokens()
|
||||
|
||||
const token = tokens?.[tokenAddress]
|
||||
|
||||
return token
|
||||
return tokens?.[tokenAddress]
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { Provider } from 'react-redux'
|
||||
|
||||
import { NetworkContextName } from './constants'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import LocalStorageContextProvider, { Updater as LocalStorageContextUpdater } from './contexts/LocalStorage'
|
||||
import { Updater as LocalStorageContextUpdater } from './state/user/hooks'
|
||||
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
|
||||
import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances'
|
||||
import App from './pages/App'
|
||||
@ -37,11 +37,9 @@ ReactGA.pageview(window.location.pathname + window.location.search)
|
||||
|
||||
function ContextProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LocalStorageContextProvider>
|
||||
<TransactionContextProvider>
|
||||
<BalancesContextProvider>{children}</BalancesContextProvider>
|
||||
</TransactionContextProvider>
|
||||
</LocalStorageContextProvider>
|
||||
<TransactionContextProvider>
|
||||
<BalancesContextProvider>{children}</BalancesContextProvider>
|
||||
</TransactionContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { getRouterContract, calculateGasMargin, calculateSlippageAmount } from '../../utils'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
|
||||
import { useAddUserToken, useFetchTokenByAddress } from '../../state/user/hooks'
|
||||
import { useAllTokens } from '../../contexts/Tokens'
|
||||
|
||||
// denominated in bips
|
||||
@ -187,7 +187,9 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
|
||||
}
|
||||
|
||||
// ensure input + output tokens are added to localstorage
|
||||
const [, { fetchTokenByAddress, addToken }] = useLocalStorageTokens()
|
||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const inputTokenAddress = fieldData[Field.INPUT].address
|
||||
useEffect(() => {
|
||||
|
@ -16,7 +16,7 @@ import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAllBalances, useAccountLPBalances } from '../../contexts/Balances'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useAllDummyPairs } from '../../contexts/LocalStorage'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
|
||||
const Positions = styled.div`
|
||||
position: relative;
|
||||
|
@ -1,10 +1,17 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
|
||||
import application from './application/reducer'
|
||||
import user from './user/reducer'
|
||||
import { save, load } from 'redux-localstorage-simple'
|
||||
|
||||
const PERSISTED_KEYS: string[] = ['user']
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
application
|
||||
}
|
||||
application,
|
||||
user
|
||||
},
|
||||
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
|
||||
preloadedState: load({ states: PERSISTED_KEYS })
|
||||
})
|
||||
|
||||
export default store
|
||||
|
25
src/state/user/actions.ts
Normal file
25
src/state/user/actions.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface SerializedToken {
|
||||
chainId: number
|
||||
address: string
|
||||
decimals: number
|
||||
symbol: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface SerializedPair {
|
||||
token0: SerializedToken
|
||||
token1: SerializedToken
|
||||
}
|
||||
|
||||
export const updateVersion = createAction<void>('updateVersion')
|
||||
export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('updateMatchesDarkMode')
|
||||
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('updateUserDarkMode')
|
||||
export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('addSerializedToken')
|
||||
export const removeSerializedToken = createAction<{ chainId: number; address: string }>('removeSerializedToken')
|
||||
export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('addSerializedPair')
|
||||
export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>(
|
||||
'removeSerializedPair'
|
||||
)
|
||||
export const dismissBetaMessage = createAction<void>('dismissBetaMessage')
|
235
src/state/user/hooks.tsx
Normal file
235
src/state/user/hooks.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
import { ChainId, JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
|
||||
import { useAllTokens } from '../../contexts/Tokens'
|
||||
import { getTokenDecimals, getTokenName, getTokenSymbol } from '../../utils'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
dismissBetaMessage,
|
||||
removeSerializedToken,
|
||||
SerializedPair,
|
||||
SerializedToken,
|
||||
updateMatchesDarkMode,
|
||||
updateUserDarkMode,
|
||||
updateVersion
|
||||
} from './actions'
|
||||
|
||||
function serializeToken(token: Token): SerializedToken {
|
||||
return {
|
||||
chainId: token.chainId,
|
||||
address: token.address,
|
||||
decimals: token.decimals,
|
||||
symbol: token.symbol,
|
||||
name: token.name
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeToken(serializedToken: SerializedToken): Token {
|
||||
return new Token(
|
||||
serializedToken.chainId,
|
||||
serializedToken.address,
|
||||
serializedToken.decimals,
|
||||
serializedToken.symbol,
|
||||
serializedToken.name
|
||||
)
|
||||
}
|
||||
|
||||
export function Updater() {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateVersion())
|
||||
}, [dispatch])
|
||||
|
||||
// keep dark mode in sync with the system
|
||||
useEffect(() => {
|
||||
const darkHandler = (match: MediaQueryListEvent) => {
|
||||
dispatch(updateMatchesDarkMode({ matchesDarkMode: match.matches }))
|
||||
}
|
||||
|
||||
const match = window?.matchMedia('(prefers-color-scheme: dark)')
|
||||
dispatch(updateMatchesDarkMode({ matchesDarkMode: match.matches }))
|
||||
|
||||
match?.addEventListener('change', darkHandler)
|
||||
|
||||
return () => {
|
||||
match?.removeEventListener('change', darkHandler)
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// this currently isn't used anywhere, but is kept as an example of how to store/update a simple boolean
|
||||
export function useBetaMessageManager() {
|
||||
const betaMessageDismissed = useSelector<AppState, boolean>(state => state.user.betaMessageDismissed)
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const wrappedDismissBetaMessage = useCallback(() => {
|
||||
dispatch(dismissBetaMessage())
|
||||
}, [dispatch])
|
||||
|
||||
return [!betaMessageDismissed, wrappedDismissBetaMessage]
|
||||
}
|
||||
|
||||
export function useIsDarkMode(): boolean {
|
||||
const { userDarkMode, matchesDarkMode } = useSelector<AppState, { userDarkMode: boolean; matchesDarkMode: boolean }>(
|
||||
({ user: { matchesDarkMode, userDarkMode } }) => ({
|
||||
userDarkMode,
|
||||
matchesDarkMode
|
||||
}),
|
||||
shallowEqual
|
||||
)
|
||||
|
||||
return userDarkMode === null ? matchesDarkMode : userDarkMode
|
||||
}
|
||||
|
||||
export function useDarkModeManager(): [boolean, () => void] {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const darkMode = useIsDarkMode()
|
||||
|
||||
const toggleSetDarkMode = useCallback(() => {
|
||||
dispatch(updateUserDarkMode({ userDarkMode: !darkMode }))
|
||||
}, [darkMode, dispatch])
|
||||
|
||||
return [darkMode, toggleSetDarkMode]
|
||||
}
|
||||
|
||||
export function useFetchTokenByAddress(): (address: string) => Promise<Token | null> {
|
||||
const { library, chainId } = useWeb3React()
|
||||
|
||||
return useCallback(
|
||||
async (address: string) => {
|
||||
const [decimals, symbol, name] = await Promise.all([
|
||||
getTokenDecimals(address, library).catch(() => null),
|
||||
getTokenSymbol(address, library).catch(() => 'UNKNOWN'),
|
||||
getTokenName(address, library).catch(() => 'Unknown')
|
||||
])
|
||||
|
||||
if (decimals === null) {
|
||||
return null
|
||||
} else {
|
||||
return new Token(chainId, address, decimals, symbol, name)
|
||||
}
|
||||
},
|
||||
[library, chainId]
|
||||
)
|
||||
}
|
||||
|
||||
export function useAddUserToken(): (token: Token) => void {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
return useCallback(
|
||||
(token: Token) => {
|
||||
dispatch(addSerializedToken({ serializedToken: serializeToken(token) }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
export function useRemoveUserAddedToken(): (chainId: number, address: string) => void {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
return useCallback(
|
||||
(chainId: number, address: string) => {
|
||||
dispatch(removeSerializedToken({ chainId, address }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
export function useUserAddedTokens(): Token[] {
|
||||
const { chainId } = useWeb3React()
|
||||
const serializedTokensMap = useSelector<AppState, AppState['user']['tokens']>(({ user: { tokens } }) => tokens)
|
||||
|
||||
return useMemo(() => {
|
||||
return Object.values(serializedTokensMap[chainId] ?? {}).map(deserializeToken)
|
||||
}, [serializedTokensMap, chainId])
|
||||
}
|
||||
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
|
||||
function serializePair(pair: Pair): SerializedPair {
|
||||
return {
|
||||
token0: serializeToken(pair.token0),
|
||||
token1: serializeToken(pair.token1)
|
||||
}
|
||||
}
|
||||
|
||||
export function usePairAdder(): (pair: Pair) => void {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
return useCallback(
|
||||
(pair: Pair) => {
|
||||
dispatch(addSerializedPair({ serializedPair: serializePair(pair) }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
const bases = [
|
||||
...Object.values(WETH),
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
]
|
||||
|
||||
export function useAllDummyPairs(): Pair[] {
|
||||
const { chainId } = useWeb3React()
|
||||
const tokens = useAllTokens()
|
||||
|
||||
const generatedPairs: Pair[] = useMemo(
|
||||
() =>
|
||||
Object.values(tokens)
|
||||
// select only tokens on the current chain
|
||||
.filter(token => token.chainId === chainId)
|
||||
.flatMap(token => {
|
||||
// for each token on the current chain,
|
||||
return (
|
||||
bases
|
||||
// loop through all the bases valid for the current chain,
|
||||
.filter(base => base.chainId === chainId)
|
||||
// to construct pairs of the given token with each base
|
||||
.map(base => {
|
||||
if (base.equals(token)) {
|
||||
return null
|
||||
} else {
|
||||
return new Pair(new TokenAmount(base, ZERO), new TokenAmount(token, ZERO))
|
||||
}
|
||||
})
|
||||
.filter(pair => !!pair)
|
||||
)
|
||||
}),
|
||||
[tokens, chainId]
|
||||
)
|
||||
|
||||
const savedSerializedPairs = useSelector<AppState>(({ user: { pairs } }) => pairs)
|
||||
|
||||
const userPairs = useMemo(
|
||||
() =>
|
||||
Object.values<SerializedPair>(savedSerializedPairs[chainId] ?? {}).map(
|
||||
pair =>
|
||||
new Pair(
|
||||
new TokenAmount(deserializeToken(pair.token0), ZERO),
|
||||
new TokenAmount(deserializeToken(pair.token1), ZERO)
|
||||
)
|
||||
),
|
||||
[savedSerializedPairs, chainId]
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
const cache: { [pairKey: string]: boolean } = {}
|
||||
return (
|
||||
generatedPairs
|
||||
.concat(userPairs)
|
||||
// filter out duplicate pairs
|
||||
.filter(pair => {
|
||||
const pairKey = `${pair.token0.address}:${pair.token1.address}`
|
||||
if (cache[pairKey]) {
|
||||
return false
|
||||
}
|
||||
return (cache[pairKey] = true)
|
||||
})
|
||||
)
|
||||
}, [generatedPairs, userPairs])
|
||||
}
|
104
src/state/user/reducer.ts
Normal file
104
src/state/user/reducer.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
removeSerializedPair,
|
||||
removeSerializedToken,
|
||||
SerializedPair,
|
||||
SerializedToken,
|
||||
updateMatchesDarkMode,
|
||||
updateUserDarkMode,
|
||||
updateVersion
|
||||
} from './actions'
|
||||
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
interface UserState {
|
||||
lastVersion: string
|
||||
|
||||
userDarkMode: boolean | null // the user's choice for dark mode or light mode
|
||||
matchesDarkMode: boolean // whether the dark mode media query matches
|
||||
|
||||
betaMessageDismissed: boolean
|
||||
|
||||
tokens: {
|
||||
[chainId: number]: {
|
||||
[address: string]: SerializedToken
|
||||
}
|
||||
}
|
||||
|
||||
pairs: {
|
||||
[chainId: number]: {
|
||||
// keyed by token0Address:token1Address
|
||||
[key: string]: SerializedPair
|
||||
}
|
||||
}
|
||||
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
function pairKey(token0Address: string, token1Address: string) {
|
||||
return `${token0Address};${token1Address}`
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
lastVersion: '',
|
||||
|
||||
userDarkMode: null,
|
||||
matchesDarkMode: false,
|
||||
|
||||
betaMessageDismissed: false,
|
||||
|
||||
tokens: {},
|
||||
pairs: {},
|
||||
|
||||
timestamp: currentTimestamp()
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(updateVersion, state => {
|
||||
if (state.lastVersion !== process.env.REACT_APP_GIT_COMMIT_HASH) {
|
||||
state.lastVersion = process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
// other stuff
|
||||
}
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateUserDarkMode, (state, action) => {
|
||||
state.userDarkMode = action.payload.userDarkMode
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateMatchesDarkMode, (state, action) => {
|
||||
state.matchesDarkMode = action.payload.matchesDarkMode
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(addSerializedToken, (state, { payload: { serializedToken } }) => {
|
||||
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
|
||||
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(removeSerializedToken, (state, { payload: { address, chainId } }) => {
|
||||
state.tokens[chainId] = state.tokens[chainId] || {}
|
||||
delete state.tokens[chainId][address]
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(addSerializedPair, (state, { payload: { serializedPair } }) => {
|
||||
if (
|
||||
serializedPair.token0.chainId === serializedPair.token1.chainId &&
|
||||
serializedPair.token0.address !== serializedPair.token1.address
|
||||
) {
|
||||
const chainId = serializedPair.token0.chainId
|
||||
state.pairs[chainId] = state.pairs[chainId] || {}
|
||||
state.pairs[chainId][pairKey(serializedPair.token0.address, serializedPair.token1.address)] = serializedPair
|
||||
}
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(removeSerializedPair, (state, { payload: { chainId, tokenAAddress, tokenBAddress } }) => {
|
||||
if (state.pairs[chainId]) {
|
||||
// just delete both keys if either exists
|
||||
delete state.pairs[chainId][pairKey(tokenAAddress, tokenBAddress)]
|
||||
delete state.pairs[chainId][pairKey(tokenBAddress, tokenAAddress)]
|
||||
}
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
)
|
@ -1,13 +1,16 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled, {
|
||||
ThemeProvider as StyledComponentsThemeProvider,
|
||||
createGlobalStyle,
|
||||
css,
|
||||
DefaultTheme
|
||||
} from 'styled-components'
|
||||
import { AppDispatch, AppState } from '../state'
|
||||
import { updateUserDarkMode } from '../state/user/actions'
|
||||
import { getQueryParam, checkSupportedTheme } from '../utils'
|
||||
import { SUPPORTED_THEMES } from '../constants'
|
||||
import { useDarkModeManager } from '../contexts/LocalStorage'
|
||||
import { useIsDarkMode } from '../state/user/hooks'
|
||||
import { Text, TextProps } from 'rebass'
|
||||
import { Colors } from './styled'
|
||||
|
||||
@ -115,21 +118,28 @@ export function theme(darkMode: boolean): DefaultTheme {
|
||||
}
|
||||
|
||||
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const userDarkMode = useSelector<AppState, boolean | null>(state => state.user.userDarkMode)
|
||||
const darkMode = useIsDarkMode()
|
||||
|
||||
const themeURL = checkSupportedTheme(getQueryParam(window.location, 'theme'))
|
||||
const themeToRender = themeURL
|
||||
const urlContainsDarkMode: boolean | null = themeURL
|
||||
? themeURL.toUpperCase() === SUPPORTED_THEMES.DARK
|
||||
? true
|
||||
: themeURL.toUpperCase() === SUPPORTED_THEMES.LIGHT
|
||||
? false
|
||||
: darkMode
|
||||
: darkMode
|
||||
: null
|
||||
: null
|
||||
|
||||
useEffect(() => {
|
||||
themeURL && toggleDarkMode(themeToRender)
|
||||
}, [toggleDarkMode, themeToRender, themeURL])
|
||||
if (urlContainsDarkMode !== null && userDarkMode === null) {
|
||||
dispatch(updateUserDarkMode({ userDarkMode: urlContainsDarkMode }))
|
||||
}
|
||||
}, [dispatch, userDarkMode, urlContainsDarkMode])
|
||||
|
||||
return <StyledComponentsThemeProvider theme={theme(themeToRender)}>{children}</StyledComponentsThemeProvider>
|
||||
const themeObject = useMemo(() => theme(darkMode), [darkMode])
|
||||
|
||||
return <StyledComponentsThemeProvider theme={themeObject}>{children}</StyledComponentsThemeProvider>
|
||||
}
|
||||
|
||||
const TextWrapper = styled(Text)<{ color: keyof Colors }>`
|
||||
|
25
yarn.lock
25
yarn.lock
@ -5267,6 +5267,11 @@ clone-deep@^4.0.1:
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clone-function@>=1.0.1:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/clone-function/-/clone-function-1.0.6.tgz#428471937750bca9c48ecbfbc16f6e232f74a03d"
|
||||
integrity sha1-QoRxk3dQvKnEjsv7wW9uIy90oD0=
|
||||
|
||||
clone-response@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
|
||||
@ -12069,6 +12074,11 @@ object-copy@^0.1.0:
|
||||
define-property "^0.2.5"
|
||||
kind-of "^3.0.3"
|
||||
|
||||
object-foreach@>=0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/object-foreach/-/object-foreach-0.1.2.tgz#d7421c5b40e3b6a3ef57ac624368d21d8f8d2dec"
|
||||
integrity sha1-10IcW0DjtqPvV6xiQ2jSHY+NLew=
|
||||
|
||||
object-hash@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
|
||||
@ -12097,6 +12107,14 @@ object-keys@~0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
|
||||
integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
|
||||
|
||||
object-merge@2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/object-merge/-/object-merge-2.5.1.tgz#077e8915ce38ea7294788448c5dd339e34df4227"
|
||||
integrity sha1-B36JFc446nKUeIRIxd0znjTfQic=
|
||||
dependencies:
|
||||
clone-function ">=1.0.1"
|
||||
object-foreach ">=0.1.2"
|
||||
|
||||
object-path@0.11.4:
|
||||
version "0.11.4"
|
||||
resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949"
|
||||
@ -14316,6 +14334,13 @@ recursive-readdir@2.2.2:
|
||||
dependencies:
|
||||
minimatch "3.0.4"
|
||||
|
||||
redux-localstorage-simple@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-localstorage-simple/-/redux-localstorage-simple-2.2.0.tgz#f60a70b0d858626d5db861b3db353ff848427f01"
|
||||
integrity sha512-BmgnJ3NkxTDvNsnHAZrRVDgODafg2Vtb17q2F2LEhuJ+EderZBJA6aqRsyqZC32BJWpu8PPtferv4Io9dpUf3w==
|
||||
dependencies:
|
||||
object-merge "2.5.1"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
|
Loading…
Reference in New Issue
Block a user