Commit 199eb8bf authored by Moody Salem's avatar Moody Salem Committed by GitHub

Refactor pages directory (#809)

* Revert "Temporarily disable the token warning for style fixes"

This reverts commit 70722b5e

* Move pages around and refactor to better support rendering content outside the app body

* Automatically add tokens in the add and remove liquidity pages

* Put the warning above the send/swap pages

* Add a margin below the token warning cards

* Save token warning dismissal state in user state

* Linting errors

* style fix
parent f1c6119f
import React, { useState, useEffect } from 'react'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import { Token, WETH } from '@uniswap/sdk'
import Row, { AutoRow } from '../Row'
import TokenLogo from '../TokenLogo'
import SearchModal from '../SearchModal'
import AddLiquidity from '../../pages/Pool/AddLiquidity'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
import { TYPE, Link } from '../../theme'
import { AutoColumn, ColumnCenter } from '../Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
import { useToken } from '../../hooks/Tokens'
import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
const Fields = {
TOKEN0: 0,
TOKEN1: 1
}
const STEP = {
SELECT_TOKENS: 'SELECT_TOKENS', // choose input and output tokens
READY_TO_CREATE: 'READY_TO_CREATE', // enable 'create' button
SHOW_CREATE_PAGE: 'SHOW_CREATE_PAGE' // show create page
}
function CreatePool({ history }: RouteComponentProps) {
const { chainId } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
const [token1Address, setToken1Address] = useState<string>()
const token0: Token = useToken(token0Address)
const token1: Token = useToken(token1Address)
const [step, setStep] = useState<string>(STEP.SELECT_TOKENS)
const pair = usePair(token0, token1)
// if both tokens selected but pair doesnt exist, enable button to create pair
useEffect(() => {
if (token0Address && token1Address && pair === null) {
setStep(STEP.READY_TO_CREATE)
}
}, [pair, token0Address, token1Address])
// if theyve clicked create, show add liquidity page
if (step === STEP.SHOW_CREATE_PAGE) {
return <AddLiquidity token0={token0Address} token1={token1Address} />
}
return (
<AutoColumn gap="20px">
<AutoColumn gap="24px">
{!token0Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Text fontSize={20}>Select first token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Row align="flex-end">
<TokenLogo address={token0Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0?.symbol}{' '}
</Text>
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
{token0?.address === 'ETH' && '(default)'}
</TYPE.darkGray>
</Row>
</ButtonDropwdownLight>
)}
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
{!token1Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
disabled={step !== STEP.SELECT_TOKENS}
>
<Text fontSize={20}>Select second token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
<Row>
<TokenLogo address={token1Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1?.symbol}
</Text>
</Row>
</ButtonDropwdownLight>
)}
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
<AutoRow padding="10px" justify="center">
<TYPE.body textAlign="center">
Pool already exists!
<Link onClick={() => history.push('/add/' + token0Address + '-' + token1Address)}> Join the pool.</Link>
</TYPE.body>
</AutoRow>
) : (
<ButtonPrimary disabled={step !== STEP.READY_TO_CREATE} onClick={() => setStep(STEP.SHOW_CREATE_PAGE)}>
<Text fontWeight={500} fontSize={20}>
Create Pool
</Text>
</ButtonPrimary>
)}
</AutoColumn>
<SearchModal
isOpen={showSearch}
filterType="tokens"
onTokenSelect={address => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
}}
onDismiss={() => {
setShowSearch(false)
}}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
showCommonBases={activeField === Fields.TOKEN0}
/>
</AutoColumn>
)
}
export default withRouter(CreatePool)
import { Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useEffect, useMemo, useState } from 'react'
import React, { useMemo } from 'react'
import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { useActiveWeb3React } from '../../hooks'
import { ALL_TOKENS, useAllTokens } from '../../hooks/Tokens'
import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks'
import { Link, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import { Link } from '../../theme'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../Question'
import TokenLogo from '../TokenLogo'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { TYPE } from '../../theme'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
......@@ -52,10 +53,6 @@ const CloseIcon = styled.div`
}
`
interface TokenWarningCardProps {
token?: Token
}
const HELP_TEXT = `
The Uniswap V2 smart contracts are designed to support any ERC20 token on Ethereum. Any token can be
loaded into the interface by entering its Ethereum address into the search field or passing it as a URL
......@@ -64,20 +61,21 @@ parameter.
const DUPLICATE_NAME_HELP_TEXT = `${HELP_TEXT} This token has the same name or symbol as another token in your list.`
export default function TokenWarningCard({ token }: TokenWarningCardProps) {
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
token?: Token
}
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
const { chainId } = useActiveWeb3React()
const [dismissed, setDismissed] = useState<boolean>(false)
const isDefaultToken = Boolean(
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
)
useEffect(() => {
setDismissed(false)
}, [token, setDismissed])
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? ''
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
......@@ -95,9 +93,9 @@ export default function TokenWarningCard({ token }: TokenWarningCardProps) {
if (isDefaultToken || !token || dismissed) return null
return (
<Wrapper error={duplicateNameOrSymbol}>
<Wrapper error={duplicateNameOrSymbol} {...rest}>
{duplicateNameOrSymbol ? null : (
<CloseIcon onClick={() => setDismissed(true)}>
<CloseIcon onClick={dismissTokenWarning}>
<CloseColor />
</CloseIcon>
)}
......@@ -123,17 +121,12 @@ export default function TokenWarningCard({ token }: TokenWarningCardProps) {
)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
return null
// temporarily disabled for styling
// return (
// <div style={{ width: '100%', position: 'absolute', top: 'calc(100% + 30px)' }}>
// {Object.keys(tokens).map(field => (
// <div key={field} style={{ marginBottom: 10 }}>
// <TokenWarningCard token={tokens[field]} />
// </div>
// ))}
// </div>
// )
return (
<>
{Object.keys(tokens).map(field =>
tokens[field] ? <TokenWarningCard style={{ marginBottom: 10 }} key={field} token={tokens[field]} /> : null
)}
</>
)
}
......@@ -6,7 +6,7 @@ import { JSBI, Percent, Price, Route, Token, TokenAmount, WETH } from '@uniswap/
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components'
import { ButtonLight, ButtonPrimary } from '../../components/Button'
......@@ -32,7 +32,8 @@ import { useHasPendingApproval, useTransactionAdder } from '../../state/transact
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { Dots, Wrapper } from './styleds'
import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds'
// denominated in bips
const ALLOWED_SLIPPAGE = 50
......@@ -134,12 +135,17 @@ function reducer(
}
}
interface AddLiquidityProps extends RouteComponentProps {
token0: string
token1: string
function useTokenByAddressOrETHAndAutomaticallyAdd(tokenId?: string, chainId?: number): Token | undefined {
const isWETH = tokenId?.toUpperCase() === 'ETH' || tokenId?.toUpperCase() === 'WETH'
const tokenByAddress = useTokenByAddressAndAutomaticallyAdd(isWETH ? null : tokenId)
return isWETH ? WETH[chainId] : tokenByAddress
}
function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
const [token0, token1] = params.tokens.split('-')
const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext)
......@@ -156,8 +162,8 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
// get basic SDK entities
const tokens: { [field in Field]: Token } = {
[Field.INPUT]: useTokenByAddressAndAutomaticallyAdd(fieldData[Field.INPUT].address),
[Field.OUTPUT]: useTokenByAddressAndAutomaticallyAdd(fieldData[Field.OUTPUT].address)
[Field.INPUT]: useTokenByAddressOrETHAndAutomaticallyAdd(fieldData[Field.INPUT].address, chainId),
[Field.OUTPUT]: useTokenByAddressOrETHAndAutomaticallyAdd(fieldData[Field.OUTPUT].address, chainId)
}
// token contracts for approvals and direct sends
......@@ -650,6 +656,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
} ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<AppBody>
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
......@@ -789,7 +796,6 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
</FixedBottom>
)}
</Wrapper>
</AppBody>
)
}
export default withRouter(AddLiquidity)
import React, { Suspense } from 'react'
import { BrowserRouter, Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import styled from 'styled-components'
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import Create from '../components/CreatePool'
import Footer from '../components/Footer'
import Header from '../components/Header'
import NavigationTabs from '../components/NavigationTabs'
import Find from '../components/PoolFinder'
import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import { isAddress } from '../utils'
import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool'
import Pool from './Pool'
import Add from './Pool/AddLiquidity'
import Remove from './Pool/RemoveLiquidity'
import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity'
import Send from './Send'
import Swap from './Swap'
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
const AppWrapper = styled.div`
display: flex;
......@@ -42,14 +38,9 @@ const BodyWrapper = styled.div`
padding-top: 160px;
align-items: center;
flex: 1;
overflow: auto;
overflow-y: auto;
overflow-x: hidden;
z-index: 10;
transition: height 0.3s ease;
& > * {
max-width: calc(420px + 4rem);
width: 90%;
}
${({ theme }) => theme.mediaWidth.upToExtraSmall`
padding: 16px;
......@@ -58,21 +49,7 @@ const BodyWrapper = styled.div`
z-index: 1;
`
const Body = styled.div`
max-width: 420px;
width: 100%;
/* min-height: 340px; */
background: ${({ theme }) => theme.bg1};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px;
box-sizing: border-box;
padding: 1rem;
position: relative;
margin-bottom: 10rem;
`
const StyledRed = styled.div`
const BackgroundGradient = styled.div`
width: 100%;
height: 200vh;
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
......@@ -91,37 +68,8 @@ const StyledRed = styled.div`
}
`
// Redirects to swap but only replace the pathname
function RedirectPathToSwapOnly({ location }: RouteComponentProps) {
return <Redirect to={{ ...location, pathname: '/swap' }} />
}
// Redirects from the /swap/:outputCurrency path to the /swap?outputCurrency=:outputCurrency format
function RedirectToSwap(props: RouteComponentProps<{ outputCurrency: string }>) {
const {
location: { search },
match: {
params: { outputCurrency }
}
} = props
return (
<Redirect
to={{
...props.location,
pathname: '/swap',
search:
search && search.length > 1
? `${search}&outputCurrency=${outputCurrency}`
: `?outputCurrency=${outputCurrency}`
}}
/>
)
}
export default function App() {
return (
<>
<Suspense fallback={null}>
<BrowserRouter>
<Route component={GoogleAnalyticsReporter} />
......@@ -133,60 +81,24 @@ export default function App() {
<BodyWrapper>
<Popups />
<Web3ReactManager>
<Body>
<NavigationTabs />
<Switch>
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/send" component={Send} />
<Route exact strict path="/find" component={Find} />
<Route exact strict path="/create" component={Create} />
<Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool" component={Pool} />
<Route
exact
strict
path={'/add/:tokens'}
component={({ match }) => {
const tokens = match.params.tokens.split('-')
const t0 =
tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined
const t1 =
tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens?.[1]) ? isAddress(tokens[1]) : undefined
if (t0 && t1) {
return <Add token0={t0} token1={t1} />
} else {
return <Redirect to="/pool" />
}
}}
/>
<Route
exact
strict
path={'/remove/:tokens'}
component={({ match }) => {
const tokens = match.params.tokens.split('-')
const t0 =
tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined
const t1 =
tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens?.[1]) ? isAddress(tokens[1]) : undefined
if (t0 && t1) {
return <Remove token0={t0} token1={t1} />
} else {
return <Redirect to="/pool" />
}
}}
/>
<Route exact strict path="/create" component={CreatePool} />
<Route exact strict path="/add/:tokens" component={AddLiquidity} />
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Body>
</Web3ReactManager>
<Footer />
</BodyWrapper>
<StyledRed />
<BackgroundGradient />
</AppWrapper>
</BrowserRouter>
<div id="popover-container" />
</Suspense>
</>
)
}
import React from 'react'
import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs'
export const Body = styled.div`
max-width: 420px;
width: 100%;
/* min-height: 340px; */
background: ${({ theme }) => theme.bg1};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px;
box-sizing: border-box;
padding: 1rem;
position: relative;
margin-bottom: 10rem;
`
/**
* The styled container element that wraps the content of most pages and the tabs.
*/
export default function AppBody({ children }: { children: React.ReactNode }) {
return (
<Body>
<NavigationTabs />
<>{children}</>
</Body>
)
}
import React, { useState, useEffect } from 'react'
import { RouteComponentProps, Redirect } from 'react-router-dom'
import { Token, WETH } from '@uniswap/sdk'
import AppBody from '../AppBody'
import Row, { AutoRow } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo'
import SearchModal from '../../components/SearchModal'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
import { TYPE, Link } from '../../theme'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
import { useToken } from '../../hooks/Tokens'
import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
enum Fields {
TOKEN0 = 0,
TOKEN1 = 1
}
enum STEP {
SELECT_TOKENS = 'SELECT_TOKENS', // choose input and output tokens
READY_TO_CREATE = 'READY_TO_CREATE', // enable 'create' button
SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page
}
export default function CreatePool({ history, location }: RouteComponentProps) {
const { chainId } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
const [token1Address, setToken1Address] = useState<string>()
const token0: Token = useToken(token0Address)
const token1: Token = useToken(token1Address)
const [step, setStep] = useState<string>(STEP.SELECT_TOKENS)
const pair = usePair(token0, token1)
// if both tokens selected but pair doesnt exist, enable button to create pair
useEffect(() => {
if (token0Address && token1Address && pair === null) {
setStep(STEP.READY_TO_CREATE)
}
}, [pair, token0Address, token1Address])
// if theyve clicked create, show add liquidity page
if (step === STEP.SHOW_CREATE_PAGE) {
return <Redirect to={{ ...location, pathname: `/add/${token0Address}-${token1Address}` }} push={true} />
}
return (
<AppBody>
<AutoColumn gap="20px">
<AutoColumn gap="24px">
{!token0Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Text fontSize={20}>Select first token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Row align="flex-end">
<TokenLogo address={token0Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0?.symbol}{' '}
</Text>
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
{token0?.address === 'ETH' && '(default)'}
</TYPE.darkGray>
</Row>
</ButtonDropwdownLight>
)}
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
{!token1Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
disabled={step !== STEP.SELECT_TOKENS}
>
<Text fontSize={20}>Select second token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
<Row>
<TokenLogo address={token1Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1?.symbol}
</Text>
</Row>
</ButtonDropwdownLight>
)}
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
<AutoRow padding="10px" justify="center">
<TYPE.body textAlign="center">
Pool already exists!
<Link onClick={() => history.push('/add/' + token0Address + '-' + token1Address)}> Join the pool.</Link>
</TYPE.body>
</AutoRow>
) : (
<ButtonPrimary disabled={step !== STEP.READY_TO_CREATE} onClick={() => setStep(STEP.SHOW_CREATE_PAGE)}>
<Text fontWeight={500} fontSize={20}>
Create Pool
</Text>
</ButtonPrimary>
)}
</AutoColumn>
<SearchModal
isOpen={showSearch}
filterType="tokens"
onTokenSelect={address => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
}}
onDismiss={() => {
setShowSearch(false)
}}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
showCommonBases={activeField === Fields.TOKEN0}
/>
</AutoColumn>
</AppBody>
)
}
import React, { useState, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { JSBI, Pair } from '@uniswap/sdk'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/Question'
import SearchModal from '../../components/SearchModal'
......@@ -17,6 +17,7 @@ import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks'
import AppBody from '../AppBody'
const Positions = styled.div`
position: relative;
......@@ -34,7 +35,7 @@ function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
return <PositionCard pair={pair} />
}
function Supply({ history }: RouteComponentProps) {
export default function Pool({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext)
const { account } = useActiveWeb3React()
const [showPoolSearch, setShowPoolSearch] = useState(false)
......@@ -58,6 +59,7 @@ function Supply({ history }: RouteComponentProps) {
})
return (
<AppBody>
<AutoColumn gap="lg" justify="center">
<ButtonPrimary
id="join-pool-button"
......@@ -111,6 +113,6 @@ function Supply({ history }: RouteComponentProps) {
</Positions>
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} />
</AutoColumn>
</AppBody>
)
}
export default withRouter(Supply)
import React, { useState, useEffect } from 'react'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { TokenAmount, JSBI, Token, Pair } from '@uniswap/sdk'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import Row from '../Row'
import TokenLogo from '../TokenLogo'
import SearchModal from '../SearchModal'
import PositionCard from '../PositionCard'
import { Link } from '../../theme'
import { Text } from 'rebass'
import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
import React, { useEffect, useState } from 'react'
import { Plus } from 'react-feather'
import { LightCard } from '../Card'
import { AutoColumn, ColumnCenter } from '../Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
import { useToken } from '../../hooks/Tokens'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import PositionCard from '../../components/PositionCard'
import Row from '../../components/Row'
import SearchModal from '../../components/SearchModal'
import TokenLogo from '../../components/TokenLogo'
import { usePair } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { usePairAdder } from '../../state/user/hooks'
import { usePair } from '../../data/Reserves'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { Link } from '../../theme'
import AppBody from '../AppBody'
const Fields = {
TOKEN0: 0,
TOKEN1: 1
enum Fields {
TOKEN0 = 0,
TOKEN1 = 1
}
function PoolFinder({ history }: RouteComponentProps) {
export default function PoolFinder({ history }: RouteComponentProps) {
const { account } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
......@@ -51,7 +50,7 @@ function PoolFinder({ history }: RouteComponentProps) {
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
return (
<>
<AppBody>
<AutoColumn gap="md">
{!token0Address ? (
<ButtonDropwdown
......@@ -168,8 +167,6 @@ function PoolFinder({ history }: RouteComponentProps) {
}}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
/>
</>
</AppBody>
)
}
export default withRouter(PoolFinder)
......@@ -5,6 +5,7 @@ import { JSBI, Percent, Route, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ButtonConfirmed, ButtonPrimary } from '../../components/Button'
......@@ -23,12 +24,13 @@ import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { usePairContract, useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { ClickableText, FixedBottom, MaxButton, Wrapper } from './styleds'
import AppBody from '../AppBody'
import { ClickableText, FixedBottom, MaxButton, Wrapper } from '../Pool/styleds'
import { useApproveCallback, Approval } from '../../hooks/useApproveCallback'
import { Dots } from '../../components/swap/styleds'
......@@ -104,15 +106,25 @@ function reducer(
}
}
export default function RemoveLiquidity({ token0, token1 }: { token0: string; token1: string }) {
function useTokenByAddressOrETHAndAutomaticallyAdd(tokenId?: string, chainId?: number): Token | undefined {
const isWETH = tokenId?.toUpperCase() === 'ETH' || tokenId?.toUpperCase() === 'WETH'
const tokenByAddress = useTokenByAddressAndAutomaticallyAdd(isWETH ? null : tokenId)
return isWETH ? WETH[chainId] : tokenByAddress
}
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
const [token0, token1] = params.tokens.split('-')
const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const inputToken: Token = useToken(token0)
const outputToken: Token = useToken(token1)
const inputToken: Token = useTokenByAddressOrETHAndAutomaticallyAdd(token0, chainId)
const outputToken: Token = useTokenByAddressOrETHAndAutomaticallyAdd(token1, chainId)
// get basic SDK entities
const tokens: { [field in Field]?: Token } = {
......@@ -641,6 +653,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
} and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}`
return (
<AppBody>
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
......@@ -807,5 +820,6 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
</div>
</AutoColumn>
</Wrapper>
</AppBody>
)
}
......@@ -34,6 +34,7 @@ import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapS
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import AppBody from '../AppBody'
export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
......@@ -274,6 +275,9 @@ export default function Send({ location: { search } }: RouteComponentProps) {
: null
return (
<>
{sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
<AppBody>
<Wrapper id="send-page">
<ConfirmationModal
isOpen={showConfirm}
......@@ -432,7 +436,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
{bestTrade && severity > 1 && (
<RowBetween>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}>
<TYPE.main
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
fontSize={14}
>
Price Impact
</TYPE.main>
<RowFixed>
......@@ -497,8 +504,8 @@ export default function Send({ location: { search } }: RouteComponentProps) {
setRawSlippage={setAllowedSlippage}
/>
)}
{sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
</Wrapper>
</AppBody>
</>
)
}
......@@ -30,6 +30,7 @@ import { Field } from '../../state/swap/actions'
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import AppBody from '../AppBody'
export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
......@@ -166,6 +167,9 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<>
<TokenWarningCards tokens={tokens} />
<AppBody>
<Wrapper id="swap-page">
<ConfirmationModal
isOpen={showConfirm}
......@@ -237,7 +241,10 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
{bestTrade && priceImpactSeverity > 1 && (
<RowBetween>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}>
<TYPE.main
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
fontSize={14}
>
Price Impact
</TYPE.main>
<RowFixed>
......@@ -299,8 +306,8 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
setRawSlippage={setAllowedSlippage}
/>
)}
<TokenWarningCards tokens={tokens} />
</Wrapper>
</AppBody>
</>
)
}
// Redirects to swap but only replace the pathname
import React from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom'
export function RedirectPathToSwapOnly({ location }: RouteComponentProps) {
return <Redirect to={{ ...location, pathname: '/swap' }} />
}
// Redirects from the /swap/:outputCurrency path to the /swap?outputCurrency=:outputCurrency format
export function RedirectToSwap(props: RouteComponentProps<{ outputCurrency: string }>) {
const {
location: { search },
match: {
params: { outputCurrency }
}
} = props
return (
<Redirect
to={{
...props.location,
pathname: '/swap',
search:
search && search.length > 1
? `${search}&outputCurrency=${outputCurrency}`
: `?outputCurrency=${outputCurrency}`
}}
/>
)
}
......@@ -22,3 +22,4 @@ export const addSerializedPair = createAction<{ serializedPair: SerializedPair }
export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>(
'removeSerializedPair'
)
export const dismissTokenWarning = createAction<{ chainId: number; tokenAddress: string }>('dismissTokenWarning')
......@@ -8,6 +8,7 @@ import { AppDispatch, AppState } from '../index'
import {
addSerializedPair,
addSerializedToken,
dismissTokenWarning,
removeSerializedToken,
SerializedPair,
SerializedToken,
......@@ -132,6 +133,28 @@ export function usePairAdder(): (pair: Pair) => void {
)
}
/**
* Returns whether a token warning has been dismissed and a callback to dismiss it,
* iff it has not already been dismissed and is a valid token.
*/
export function useTokenWarningDismissal(chainId?: number, token?: Token): [boolean, null | (() => void)] {
const dismissalState = useSelector<AppState, AppState['user']['dismissedTokenWarnings']>(
state => state.user.dismissedTokenWarnings
)
const dispatch = useDispatch<AppDispatch>()
return useMemo(() => {
if (!chainId || !token) return [false, null]
const dismissed: boolean = dismissalState?.[chainId]?.[token.address] === true
const callback = dismissed ? null : () => dispatch(dismissTokenWarning({ chainId, tokenAddress: token.address }))
return [dismissed, callback]
}, [chainId, token, dismissalState, dispatch])
}
const bases = [
...Object.values(WETH),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
......
......@@ -2,6 +2,7 @@ import { createReducer } from '@reduxjs/toolkit'
import {
addSerializedPair,
addSerializedToken,
dismissTokenWarning,
removeSerializedPair,
removeSerializedToken,
SerializedPair,
......@@ -25,6 +26,13 @@ interface UserState {
}
}
// the token warnings that the user has dismissed
dismissedTokenWarnings?: {
[chainId: number]: {
[tokenAddress: string]: true
}
}
pairs: {
[chainId: number]: {
// keyed by token0Address:token1Address
......@@ -79,6 +87,11 @@ export default createReducer(initialState, builder =>
delete state.tokens[chainId][address]
state.timestamp = currentTimestamp()
})
.addCase(dismissTokenWarning, (state, { payload: { chainId, tokenAddress } }) => {
state.dismissedTokenWarnings = state.dismissedTokenWarnings ?? {}
state.dismissedTokenWarnings[chainId] = state.dismissedTokenWarnings[chainId] ?? {}
state.dismissedTokenWarnings[chainId][tokenAddress] = true
})
.addCase(addSerializedPair, (state, { payload: { serializedPair } }) => {
if (
serializedPair.token0.chainId === serializedPair.token1.chainId &&
......
import React from 'react'
/**
* Helper type that returns the props type of another component, excluding
* any of the keys passed as the optional second argument.
*/
type PropsOfExcluding<TComponent, TExcludingProps = void> = TComponent extends React.ComponentType<infer P>
? TExcludingProps extends string | number | symbol
? Omit<P, TExcludingProps>
: P
: unknown
export default PropsOfExcluding
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment