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 { Token } from '@uniswap/sdk'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import React, { useEffect, useMemo, useState } from 'react' import React, { useMemo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { ALL_TOKENS, useAllTokens } from '../../hooks/Tokens' import { ALL_TOKENS, useAllTokens } from '../../hooks/Tokens'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks'
import { Link, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils' import { getEtherscanLink } from '../../utils'
import { Link } from '../../theme' import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../Question' import QuestionHelper from '../Question'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { TYPE } from '../../theme'
const Wrapper = styled.div<{ error: boolean }>` const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)}; background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
...@@ -52,10 +53,6 @@ const CloseIcon = styled.div` ...@@ -52,10 +53,6 @@ const CloseIcon = styled.div`
} }
` `
interface TokenWarningCardProps {
token?: Token
}
const HELP_TEXT = ` const HELP_TEXT = `
The Uniswap V2 smart contracts are designed to support any ERC20 token on Ethereum. Any token can be 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 loaded into the interface by entering its Ethereum address into the search field or passing it as a URL
...@@ -64,20 +61,21 @@ parameter. ...@@ -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.` 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 { chainId } = useActiveWeb3React()
const [dismissed, setDismissed] = useState<boolean>(false)
const isDefaultToken = Boolean( const isDefaultToken = Boolean(
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address] token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
) )
useEffect(() => {
setDismissed(false)
}, [token, setDismissed])
const tokenSymbol = token?.symbol?.toLowerCase() ?? '' const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? '' const tokenName = token?.name?.toLowerCase() ?? ''
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
const allTokens = useAllTokens() const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => { const duplicateNameOrSymbol = useMemo(() => {
...@@ -95,9 +93,9 @@ export default function TokenWarningCard({ token }: TokenWarningCardProps) { ...@@ -95,9 +93,9 @@ export default function TokenWarningCard({ token }: TokenWarningCardProps) {
if (isDefaultToken || !token || dismissed) return null if (isDefaultToken || !token || dismissed) return null
return ( return (
<Wrapper error={duplicateNameOrSymbol}> <Wrapper error={duplicateNameOrSymbol} {...rest}>
{duplicateNameOrSymbol ? null : ( {duplicateNameOrSymbol ? null : (
<CloseIcon onClick={() => setDismissed(true)}> <CloseIcon onClick={dismissTokenWarning}>
<CloseColor /> <CloseColor />
</CloseIcon> </CloseIcon>
)} )}
...@@ -123,17 +121,12 @@ export default function TokenWarningCard({ token }: TokenWarningCardProps) { ...@@ -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 } }) { export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
return null return (
// temporarily disabled for styling <>
// return ( {Object.keys(tokens).map(field =>
// <div style={{ width: '100%', position: 'absolute', top: 'calc(100% + 30px)' }}> tokens[field] ? <TokenWarningCard style={{ marginBottom: 10 }} key={field} token={tokens[field]} /> : null
// {Object.keys(tokens).map(field => ( )}
// <div key={field} style={{ marginBottom: 10 }}> </>
// <TokenWarningCard token={tokens[field]} /> )
// </div>
// ))}
// </div>
// )
} }
...@@ -6,7 +6,7 @@ import { JSBI, Percent, Price, Route, Token, TokenAmount, WETH } from '@uniswap/ ...@@ -6,7 +6,7 @@ import { JSBI, Percent, Price, Route, Token, TokenAmount, WETH } from '@uniswap/
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react' import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { Plus } from 'react-feather' import { Plus } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { RouteComponentProps, withRouter } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { ButtonLight, ButtonPrimary } from '../../components/Button' import { ButtonLight, ButtonPrimary } from '../../components/Button'
...@@ -32,7 +32,8 @@ import { useHasPendingApproval, useTransactionAdder } from '../../state/transact ...@@ -32,7 +32,8 @@ import { useHasPendingApproval, useTransactionAdder } from '../../state/transact
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils' import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { Dots, Wrapper } from './styleds' import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds'
// denominated in bips // denominated in bips
const ALLOWED_SLIPPAGE = 50 const ALLOWED_SLIPPAGE = 50
...@@ -134,12 +135,17 @@ function reducer( ...@@ -134,12 +135,17 @@ function reducer(
} }
} }
interface AddLiquidityProps extends RouteComponentProps { function useTokenByAddressOrETHAndAutomaticallyAdd(tokenId?: string, chainId?: number): Token | undefined {
token0: string const isWETH = tokenId?.toUpperCase() === 'ETH' || tokenId?.toUpperCase() === 'WETH'
token1: string
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 { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
...@@ -156,8 +162,8 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) { ...@@ -156,8 +162,8 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
// get basic SDK entities // get basic SDK entities
const tokens: { [field in Field]: Token } = { const tokens: { [field in Field]: Token } = {
[Field.INPUT]: useTokenByAddressAndAutomaticallyAdd(fieldData[Field.INPUT].address), [Field.INPUT]: useTokenByAddressOrETHAndAutomaticallyAdd(fieldData[Field.INPUT].address, chainId),
[Field.OUTPUT]: useTokenByAddressAndAutomaticallyAdd(fieldData[Field.OUTPUT].address) [Field.OUTPUT]: useTokenByAddressOrETHAndAutomaticallyAdd(fieldData[Field.OUTPUT].address, chainId)
} }
// token contracts for approvals and direct sends // token contracts for approvals and direct sends
...@@ -650,146 +656,146 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) { ...@@ -650,146 +656,146 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
} ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` } ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return ( return (
<Wrapper> <AppBody>
<ConfirmationModal <Wrapper>
isOpen={showConfirm} <ConfirmationModal
onDismiss={() => { isOpen={showConfirm}
setPendingConfirmation(true) onDismiss={() => {
setAttemptingTxn(false) setPendingConfirmation(true)
setShowConfirm(false) setAttemptingTxn(false)
}} setShowConfirm(false)
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/>
<SearchModal
isOpen={showSearch}
onDismiss={() => {
setShowSearch(false)
}}
/>
<AutoColumn gap="20px">
{noLiquidity && (
<ColumnCenter>
<BlueCard>
<AutoColumn gap="10px">
<TYPE.link fontWeight={600} color={'primaryText1'}>
You are the first liquidity provider.
</TYPE.link>
<TYPE.link fontWeight={400} color={'primaryText1'}>
The ratio of tokens you add will set the price of this pool.
</TYPE.link>
<TYPE.link fontWeight={400} color={'primaryText1'}>
Once you are happy with the rate click supply to review.
</TYPE.link>
</AutoColumn>
</BlueCard>
</ColumnCenter>
)}
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onMax(maxAmountInput.toExact(), Field.INPUT)
}} }}
showMaxButton={!atMaxAmountInput} attemptingTxn={attemptingTxn}
token={tokens[Field.INPUT]} pendingConfirmation={pendingConfirmation}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)} hash={txHash ? txHash : ''}
pair={pair} topContent={() => modalHeader()}
label="Input" bottomContent={modalBottom}
id="add-liquidity-input" pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/> />
<ColumnCenter> <SearchModal
<Plus size="16" color={theme.text2} /> isOpen={showSearch}
</ColumnCenter> onDismiss={() => {
<CurrencyInputPanel setShowSearch(false)
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountOutput && onMax(maxAmountOutput?.toExact(), Field.OUTPUT)
}} }}
showMaxButton={!atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
pair={pair}
id="add-liquidity-output"
/> />
{tokens[Field.OUTPUT] && tokens[Field.INPUT] && ( <AutoColumn gap="20px">
<> {noLiquidity && (
<GreyCard padding="0px" borderRadius={'20px'}> <ColumnCenter>
<RowBetween padding="1rem"> <BlueCard>
<TYPE.subHeader fontWeight={500} fontSize={14}> <AutoColumn gap="10px">
{noLiquidity ? 'Initial prices' : 'Prices'} and pool share <TYPE.link fontWeight={600} color={'primaryText1'}>
</TYPE.subHeader> You are the first liquidity provider.
</RowBetween>{' '} </TYPE.link>
<LightCard padding="1rem" borderRadius={'20px'}> <TYPE.link fontWeight={400} color={'primaryText1'}>
<PriceBar /> The ratio of tokens you add will set the price of this pool.
</LightCard> </TYPE.link>
</GreyCard> <TYPE.link fontWeight={400} color={'primaryText1'}>
</> Once you are happy with the rate click supply to review.
)} </TYPE.link>
{isValid ? ( </AutoColumn>
!inputApproved ? ( </BlueCard>
<ButtonLight </ColumnCenter>
onClick={() => { )}
approveAmount(Field.INPUT) <CurrencyInputPanel
}} field={Field.INPUT}
disabled={pendingApprovalInput} value={formattedAmounts[Field.INPUT]}
> onUserInput={onUserInput}
{pendingApprovalInput ? ( onMax={() => {
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots> maxAmountInput && onMax(maxAmountInput.toExact(), Field.INPUT)
) : ( }}
'Approve ' + tokens[Field.INPUT]?.symbol showMaxButton={!atMaxAmountInput}
)} token={tokens[Field.INPUT]}
</ButtonLight> onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
) : !outputApproved ? ( pair={pair}
<ButtonLight label="Input"
onClick={() => { id="add-liquidity-input"
approveAmount(Field.OUTPUT) />
}} <ColumnCenter>
disabled={pendingApprovalOutput} <Plus size="16" color={theme.text2} />
> </ColumnCenter>
{pendingApprovalOutput ? ( <CurrencyInputPanel
<Dots>Approving {tokens[Field.OUTPUT]?.symbol}</Dots> field={Field.OUTPUT}
) : ( value={formattedAmounts[Field.OUTPUT]}
'Approve ' + tokens[Field.OUTPUT]?.symbol onUserInput={onUserInput}
)} onMax={() => {
</ButtonLight> maxAmountOutput && onMax(maxAmountOutput?.toExact(), Field.OUTPUT)
}}
showMaxButton={!atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
pair={pair}
id="add-liquidity-output"
/>
{tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<>
<GreyCard padding="0px" borderRadius={'20px'}>
<RowBetween padding="1rem">
<TYPE.subHeader fontWeight={500} fontSize={14}>
{noLiquidity ? 'Initial prices' : 'Prices'} and pool share
</TYPE.subHeader>
</RowBetween>{' '}
<LightCard padding="1rem" borderRadius={'20px'}>
<PriceBar />
</LightCard>
</GreyCard>
</>
)}
{isValid ? (
!inputApproved ? (
<ButtonLight
onClick={() => {
approveAmount(Field.INPUT)
}}
disabled={pendingApprovalInput}
>
{pendingApprovalInput ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : !outputApproved ? (
<ButtonLight
onClick={() => {
approveAmount(Field.OUTPUT)
}}
disabled={pendingApprovalOutput}
>
{pendingApprovalOutput ? (
<Dots>Approving {tokens[Field.OUTPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.OUTPUT]?.symbol
)}
</ButtonLight>
) : (
<ButtonPrimary
onClick={() => {
setShowConfirm(true)
}}
>
<Text fontSize={20} fontWeight={500}>
Supply
</Text>
</ButtonPrimary>
)
) : ( ) : (
<ButtonPrimary <ButtonPrimary disabled={true}>
onClick={() => {
setShowConfirm(true)
}}
>
<Text fontSize={20} fontWeight={500}> <Text fontSize={20} fontWeight={500}>
Supply {generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'}
</Text> </Text>
</ButtonPrimary> </ButtonPrimary>
) )}
) : ( </AutoColumn>
<ButtonPrimary disabled={true}>
<Text fontSize={20} fontWeight={500}> {!noLiquidity && (
{generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'} <FixedBottom>
</Text> <AutoColumn>
</ButtonPrimary> <PositionCard pair={pair} minimal={true} />
</AutoColumn>
</FixedBottom>
)} )}
</AutoColumn> </Wrapper>
</AppBody>
{!noLiquidity && (
<FixedBottom>
<AutoColumn>
<PositionCard pair={pair} minimal={true} />
</AutoColumn>
</FixedBottom>
)}
</Wrapper>
) )
} }
export default withRouter(AddLiquidity)
import React, { Suspense } from 'react' 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 styled from 'styled-components'
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter' import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import Create from '../components/CreatePool'
import Footer from '../components/Footer' import Footer from '../components/Footer'
import Header from '../components/Header' import Header from '../components/Header'
import NavigationTabs from '../components/NavigationTabs'
import Find from '../components/PoolFinder'
import Popups from '../components/Popups' import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager' import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader' import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import { isAddress } from '../utils' import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool'
import Pool from './Pool' import Pool from './Pool'
import Add from './Pool/AddLiquidity' import PoolFinder from './PoolFinder'
import Remove from './Pool/RemoveLiquidity' import RemoveLiquidity from './RemoveLiquidity'
import Send from './Send' import Send from './Send'
import Swap from './Swap' import Swap from './Swap'
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
const AppWrapper = styled.div` const AppWrapper = styled.div`
display: flex; display: flex;
...@@ -42,14 +38,9 @@ const BodyWrapper = styled.div` ...@@ -42,14 +38,9 @@ const BodyWrapper = styled.div`
padding-top: 160px; padding-top: 160px;
align-items: center; align-items: center;
flex: 1; flex: 1;
overflow: auto; overflow-y: auto;
overflow-x: hidden;
z-index: 10; z-index: 10;
transition: height 0.3s ease;
& > * {
max-width: calc(420px + 4rem);
width: 90%;
}
${({ theme }) => theme.mediaWidth.upToExtraSmall` ${({ theme }) => theme.mediaWidth.upToExtraSmall`
padding: 16px; padding: 16px;
...@@ -58,21 +49,7 @@ const BodyWrapper = styled.div` ...@@ -58,21 +49,7 @@ const BodyWrapper = styled.div`
z-index: 1; z-index: 1;
` `
const Body = styled.div` const BackgroundGradient = 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`
width: 100%; width: 100%;
height: 200vh; height: 200vh;
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`}; background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
...@@ -91,102 +68,37 @@ const StyledRed = styled.div` ...@@ -91,102 +68,37 @@ 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() { export default function App() {
return ( return (
<> <Suspense fallback={null}>
<Suspense fallback={null}> <BrowserRouter>
<BrowserRouter> <Route component={GoogleAnalyticsReporter} />
<Route component={GoogleAnalyticsReporter} /> <Route component={DarkModeQueryParamReader} />
<Route component={DarkModeQueryParamReader} /> <AppWrapper>
<AppWrapper> <HeaderWrapper>
<HeaderWrapper> <Header />
<Header /> </HeaderWrapper>
</HeaderWrapper> <BodyWrapper>
<BodyWrapper> <Popups />
<Popups /> <Web3ReactManager>
<Web3ReactManager> <Switch>
<Body> <Route exact strict path="/swap" component={Swap} />
<NavigationTabs /> <Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Switch> <Route exact strict path="/send" component={Send} />
<Route exact strict path="/swap" component={Swap} /> <Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} /> <Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/send" component={Send} /> <Route exact strict path="/create" component={CreatePool} />
<Route exact strict path="/find" component={Find} /> <Route exact strict path="/add/:tokens" component={AddLiquidity} />
<Route exact strict path="/create" component={Create} /> <Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route exact strict path="/pool" component={Pool} /> <Route component={RedirectPathToSwapOnly} />
<Route </Switch>
exact </Web3ReactManager>
strict <Footer />
path={'/add/:tokens'} </BodyWrapper>
component={({ match }) => { <BackgroundGradient />
const tokens = match.params.tokens.split('-') </AppWrapper>
const t0 = </BrowserRouter>
tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined <div id="popover-container" />
const t1 = </Suspense>
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 component={RedirectPathToSwapOnly} />
</Switch>
</Body>
</Web3ReactManager>
<Footer />
</BodyWrapper>
<StyledRed />
</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 React, { useState, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { JSBI, Pair } from '@uniswap/sdk' 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 Question from '../../components/Question'
import SearchModal from '../../components/SearchModal' import SearchModal from '../../components/SearchModal'
...@@ -17,6 +17,7 @@ import { AutoColumn, ColumnCenter } from '../../components/Column' ...@@ -17,6 +17,7 @@ import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves' import { usePair } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks' import { useAllDummyPairs } from '../../state/user/hooks'
import AppBody from '../AppBody'
const Positions = styled.div` const Positions = styled.div`
position: relative; position: relative;
...@@ -34,7 +35,7 @@ function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) { ...@@ -34,7 +35,7 @@ function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
return <PositionCard pair={pair} /> return <PositionCard pair={pair} />
} }
function Supply({ history }: RouteComponentProps) { export default function Pool({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const [showPoolSearch, setShowPoolSearch] = useState(false) const [showPoolSearch, setShowPoolSearch] = useState(false)
...@@ -58,59 +59,60 @@ function Supply({ history }: RouteComponentProps) { ...@@ -58,59 +59,60 @@ function Supply({ history }: RouteComponentProps) {
}) })
return ( return (
<AutoColumn gap="lg" justify="center"> <AppBody>
<ButtonPrimary <AutoColumn gap="lg" justify="center">
id="join-pool-button" <ButtonPrimary
padding="16px" id="join-pool-button"
onClick={() => { padding="16px"
setShowPoolSearch(true) onClick={() => {
}} setShowPoolSearch(true)
> }}
<Text fontWeight={500} fontSize={20}> >
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool <Text fontWeight={500} fontSize={20}>
</Text> Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
</ButtonPrimary>
<Positions>
<AutoColumn gap="12px">
<RowBetween padding={'0 8px'}>
<Text color={theme.text1} fontWeight={500}>
Your Pooled Liquidity
</Text>
<Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." />
</RowBetween>
{filteredExchangeList?.length === 0 && (
<LightCard
padding="40px
"
>
<TYPE.body color={theme.text3} textAlign="center">
No liquidity found.
</TYPE.body>
</LightCard>
)}
{filteredExchangeList}
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
<Link
id="import-pool-link"
onClick={() => {
history.push('/find')
}}
>
Import it.
</Link>
</Text> </Text>
</AutoColumn> </ButtonPrimary>
<FixedBottom> <Positions>
<ColumnCenter> <AutoColumn gap="12px">
<ButtonSecondary width="136px" padding="8px" borderRadius="10px" onClick={() => history.push('/create')}> <RowBetween padding={'0 8px'}>
+ Create Pool <Text color={theme.text1} fontWeight={500}>
</ButtonSecondary> Your Pooled Liquidity
</ColumnCenter> </Text>
</FixedBottom> <Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." />
</Positions> </RowBetween>
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} /> {filteredExchangeList?.length === 0 && (
</AutoColumn> <LightCard
padding="40px
"
>
<TYPE.body color={theme.text3} textAlign="center">
No liquidity found.
</TYPE.body>
</LightCard>
)}
{filteredExchangeList}
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
<Link
id="import-pool-link"
onClick={() => {
history.push('/find')
}}
>
Import it.
</Link>
</Text>
</AutoColumn>
<FixedBottom>
<ColumnCenter>
<ButtonSecondary width="136px" padding="8px" borderRadius="10px" onClick={() => history.push('/create')}>
+ Create Pool
</ButtonSecondary>
</ColumnCenter>
</FixedBottom>
</Positions>
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} />
</AutoColumn>
</AppBody>
) )
} }
export default withRouter(Supply)
import React, { useState, useEffect } from 'react' import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
import { RouteComponentProps, withRouter } from 'react-router-dom' import React, { useEffect, useState } from 'react'
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 { Plus } from 'react-feather' import { Plus } from 'react-feather'
import { LightCard } from '../Card' import { RouteComponentProps } from 'react-router-dom'
import { AutoColumn, ColumnCenter } from '../Column' import { Text } from 'rebass'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button' import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { useToken } from '../../hooks/Tokens' 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 { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { usePairAdder } from '../../state/user/hooks' 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 = { enum Fields {
TOKEN0: 0, TOKEN0 = 0,
TOKEN1: 1 TOKEN1 = 1
} }
function PoolFinder({ history }: RouteComponentProps) { export default function PoolFinder({ history }: RouteComponentProps) {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false) const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0) const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
...@@ -51,7 +50,7 @@ function PoolFinder({ history }: RouteComponentProps) { ...@@ -51,7 +50,7 @@ function PoolFinder({ history }: RouteComponentProps) {
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)) const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
return ( return (
<> <AppBody>
<AutoColumn gap="md"> <AutoColumn gap="md">
{!token0Address ? ( {!token0Address ? (
<ButtonDropwdown <ButtonDropwdown
...@@ -168,8 +167,6 @@ function PoolFinder({ history }: RouteComponentProps) { ...@@ -168,8 +167,6 @@ function PoolFinder({ history }: RouteComponentProps) {
}} }}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address} 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' ...@@ -5,6 +5,7 @@ import { JSBI, Percent, Route, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react' import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather' import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ButtonConfirmed, ButtonPrimary } from '../../components/Button' import { ButtonConfirmed, ButtonPrimary } from '../../components/Button'
...@@ -23,12 +24,13 @@ import { usePair } from '../../data/Reserves' ...@@ -23,12 +24,13 @@ import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply' import { useTotalSupply } from '../../data/TotalSupply'
import { usePairContract, useActiveWeb3React } from '../../hooks' import { usePairContract, useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens' import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTransactionAdder } from '../../state/transactions/hooks' import { useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenBalance } from '../../state/wallet/hooks' import { useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils' 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 { useApproveCallback, Approval } from '../../hooks/useApproveCallback'
import { Dots } from '../../components/swap/styleds' import { Dots } from '../../components/swap/styleds'
...@@ -104,15 +106,25 @@ function reducer( ...@@ -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 { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [showConfirm, setShowConfirm] = useState<boolean>(false) const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const inputToken: Token = useToken(token0) const inputToken: Token = useTokenByAddressOrETHAndAutomaticallyAdd(token0, chainId)
const outputToken: Token = useToken(token1) const outputToken: Token = useTokenByAddressOrETHAndAutomaticallyAdd(token1, chainId)
// get basic SDK entities // get basic SDK entities
const tokens: { [field in Field]?: Token } = { const tokens: { [field in Field]?: Token } = {
...@@ -641,171 +653,173 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to ...@@ -641,171 +653,173 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
} and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}` } and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}`
return ( return (
<Wrapper> <AppBody>
<ConfirmationModal <Wrapper>
isOpen={showConfirm} <ConfirmationModal
onDismiss={() => { isOpen={showConfirm}
resetModalState() onDismiss={() => {
setShowConfirm(false) resetModalState()
}} setShowConfirm(false)
attemptingTxn={attemptedRemoval} }}
pendingConfirmation={pendingConfirmation} attemptingTxn={attemptedRemoval}
hash={txHash ? txHash : ''} pendingConfirmation={pendingConfirmation}
topContent={modalHeader} hash={txHash ? txHash : ''}
bottomContent={modalBottom} topContent={modalHeader}
pendingText={pendingText} bottomContent={modalBottom}
title="You will receive" pendingText={pendingText}
/> title="You will receive"
<AutoColumn gap="md"> />
<LightCard> <AutoColumn gap="md">
<AutoColumn gap="20px"> <LightCard>
<RowBetween> <AutoColumn gap="20px">
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowAdvanced(!showAdvanced)
}}
>
{showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{derivedPercent?.toFixed(0) === '0' ? '<1' : derivedPercent?.toFixed(0) ?? '0'}%
</Text>
</Row>
{!showAdvanced && (
<Slider
value={parseInt(derivedPercent?.toFixed(0) ?? '0')}
onChange={handleSliderChange}
override={override}
/>
)}
{!showAdvanced && (
<RowBetween> <RowBetween>
<MaxButton onClick={() => handlePresetPercentage(25)} width="20%"> <Text fontWeight={500}>Amount</Text>
25% <ClickableText
</MaxButton> fontWeight={500}
<MaxButton onClick={() => handlePresetPercentage(50)} width="20%"> onClick={() => {
50% setShowAdvanced(!showAdvanced)
</MaxButton> }}
<MaxButton onClick={() => handlePresetPercentage(75)} width="20%"> >
75% {showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
</MaxButton> </ClickableText>
<MaxButton onClick={() => handlePresetPercentage(100)} width="20%">
Max
</MaxButton>
</RowBetween> </RowBetween>
)} <Row style={{ alignItems: 'flex-end' }}>
</AutoColumn> <Text fontSize={72} fontWeight={500}>
</LightCard> {derivedPercent?.toFixed(0) === '0' ? '<1' : derivedPercent?.toFixed(0) ?? '0'}%
{!showAdvanced && ( </Text>
<> </Row>
<ColumnCenter> {!showAdvanced && (
<ArrowDown size="16" color={theme.text2} /> <Slider
</ColumnCenter>{' '} value={parseInt(derivedPercent?.toFixed(0) ?? '0')}
<LightCard> onChange={handleSliderChange}
<AutoColumn gap="10px"> override={override}
/>
)}
{!showAdvanced && (
<RowBetween> <RowBetween>
<Text fontSize={24} fontWeight={500}> <MaxButton onClick={() => handlePresetPercentage(25)} width="20%">
{formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'} 25%
</Text> </MaxButton>
<RowFixed> <MaxButton onClick={() => handlePresetPercentage(50)} width="20%">
<TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} /> 50%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(75)} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(100)} width="20%">
Max
</MaxButton>
</RowBetween>
)}
</AutoColumn>
</LightCard>
{!showAdvanced && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>{' '}
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}> <Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN0]?.symbol} {formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'}
</Text> </Text>
</RowFixed> <RowFixed>
</RowBetween> <TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} />
<RowBetween> <Text fontSize={24} fontWeight={500}>
<Text fontSize={24} fontWeight={500}> {tokens[Field.TOKEN0]?.symbol}
{formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'} </Text>
</Text> </RowFixed>
<RowFixed> </RowBetween>
<TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} /> <RowBetween>
<Text fontSize={24} fontWeight={500}> <Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN1]?.symbol} {formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'}
</Text> </Text>
</RowFixed> <RowFixed>
</RowBetween> <TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} />
</AutoColumn> <Text fontSize={24} fontWeight={500}>
</LightCard> {tokens[Field.TOKEN1]?.symbol}
</> </Text>
)} </RowFixed>
</RowBetween>
{showAdvanced && ( </AutoColumn>
<> </LightCard>
<CurrencyInputPanel </>
field={Field.LIQUIDITY} )}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput} {showAdvanced && (
onMax={onMax} <>
showMaxButton={!atMaxAmount} <CurrencyInputPanel
disableTokenSelect field={Field.LIQUIDITY}
token={pair?.liquidityToken} value={formattedAmounts[Field.LIQUIDITY]}
isExchange={true} onUserInput={onUserInput}
pair={pair} onMax={onMax}
id="liquidity-amount" showMaxButton={!atMaxAmount}
/> disableTokenSelect
<ColumnCenter> token={pair?.liquidityToken}
<ArrowDown size="16" color={theme.text2} /> isExchange={true}
</ColumnCenter> pair={pair}
<CurrencyInputPanel id="liquidity-amount"
field={Field.TOKEN0} />
value={formattedAmounts[Field.TOKEN0]} <ColumnCenter>
onUserInput={onUserInput} <ArrowDown size="16" color={theme.text2} />
onMax={onMax} </ColumnCenter>
showMaxButton={!atMaxAmount} <CurrencyInputPanel
token={tokens[Field.TOKEN0]} field={Field.TOKEN0}
label={'Output'} value={formattedAmounts[Field.TOKEN0]}
disableTokenSelect onUserInput={onUserInput}
id="remove-liquidity-token0" onMax={onMax}
/> showMaxButton={!atMaxAmount}
<ColumnCenter> token={tokens[Field.TOKEN0]}
<Plus size="16" color={theme.text2} /> label={'Output'}
</ColumnCenter> disableTokenSelect
<CurrencyInputPanel id="remove-liquidity-token0"
field={Field.TOKEN1} />
value={formattedAmounts[Field.TOKEN1]} <ColumnCenter>
onUserInput={onUserInput} <Plus size="16" color={theme.text2} />
onMax={onMax} </ColumnCenter>
showMaxButton={!atMaxAmount} <CurrencyInputPanel
token={tokens[Field.TOKEN1]} field={Field.TOKEN1}
label={'Output'} value={formattedAmounts[Field.TOKEN1]}
disableTokenSelect onUserInput={onUserInput}
id="remove-liquidity-token1" onMax={onMax}
/> showMaxButton={!atMaxAmount}
</> token={tokens[Field.TOKEN1]}
)} label={'Output'}
<div style={{ padding: '10px 20px' }}> disableTokenSelect
<RowBetween> id="remove-liquidity-token1"
Price: />
<div> </>
1 {pair?.token0.symbol} ={' '} )}
{independentField === Field.TOKEN0 || independentField === Field.LIQUIDITY <div style={{ padding: '10px 20px' }}>
? route?.midPrice.toSignificant(6) <RowBetween>
: route?.midPrice.invert().toSignificant(6)}{' '} Price:
{pair?.token1.symbol} <div>
</div> 1 {pair?.token0.symbol} ={' '}
</RowBetween> {independentField === Field.TOKEN0 || independentField === Field.LIQUIDITY
</div> ? route?.midPrice.toSignificant(6)
<div style={{ position: 'relative' }}> : route?.midPrice.invert().toSignificant(6)}{' '}
<ButtonPrimary {pair?.token1.symbol}
onClick={() => { </div>
setShowConfirm(true) </RowBetween>
}} </div>
disabled={!isValid} <div style={{ position: 'relative' }}>
> <ButtonPrimary
<Text fontSize={20} fontWeight={500}> onClick={() => {
{inputError || outputError || poolTokenError || generalError || 'Remove'} setShowConfirm(true)
</Text> }}
</ButtonPrimary> disabled={!isValid}
<FixedBottom> >
<PositionCard pair={pair} minimal={true} /> <Text fontSize={20} fontWeight={500}>
</FixedBottom> {inputError || outputError || poolTokenError || generalError || 'Remove'}
</div> </Text>
</AutoColumn> </ButtonPrimary>
</Wrapper> <FixedBottom>
<PositionCard pair={pair} minimal={true} />
</FixedBottom>
</div>
</AutoColumn>
</Wrapper>
</AppBody>
) )
} }
...@@ -34,6 +34,7 @@ import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapS ...@@ -34,6 +34,7 @@ import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapS
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks' import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme' import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices' import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import AppBody from '../AppBody'
export default function Send({ location: { search } }: RouteComponentProps) { export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search) useDefaultsFromURL(search)
...@@ -274,231 +275,237 @@ export default function Send({ location: { search } }: RouteComponentProps) { ...@@ -274,231 +275,237 @@ export default function Send({ location: { search } }: RouteComponentProps) {
: null : null
return ( return (
<Wrapper id="send-page"> <>
<ConfirmationModal {sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
isOpen={showConfirm} <AppBody>
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'} <Wrapper id="send-page">
onDismiss={() => { <ConfirmationModal
resetModal() isOpen={showConfirm}
setShowConfirm(false) title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
}} onDismiss={() => {
attemptingTxn={attemptingTxn} resetModal()
pendingConfirmation={pendingConfirmation} setShowConfirm(false)
hash={txHash} }}
topContent={modalHeader} attemptingTxn={attemptingTxn}
bottomContent={modalBottom} pendingConfirmation={pendingConfirmation}
pendingText={pendingText} hash={txHash}
/> topContent={modalHeader}
{!sendingWithSwap && ( bottomContent={modalBottom}
<AutoColumn justify="center" style={{ marginBottom: '1rem' }}> pendingText={pendingText}
<InputGroup gap="lg" justify="center"> />
<StyledNumerical {!sendingWithSwap && (
id="sending-no-swap-input" <AutoColumn justify="center" style={{ marginBottom: '1rem' }}>
value={formattedAmounts[Field.INPUT]} <InputGroup gap="lg" justify="center">
onUserInput={val => onUserInput(Field.INPUT, val)} <StyledNumerical
/> id="sending-no-swap-input"
<CurrencyInputPanel value={formattedAmounts[Field.INPUT]}
field={Field.INPUT} onUserInput={val => onUserInput(Field.INPUT, val)}
value={formattedAmounts[Field.INPUT]} />
onUserInput={(field, val) => onUserInput(Field.INPUT, val)} <CurrencyInputPanel
onMax={() => { field={Field.INPUT}
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact()) value={formattedAmounts[Field.INPUT]}
}} onUserInput={(field, val) => onUserInput(Field.INPUT, val)}
showMaxButton={!atMaxAmountInput} onMax={() => {
token={tokens[Field.INPUT]} maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
onTokenSelection={address => _onTokenSelect(address)} }}
hideBalance={true} showMaxButton={!atMaxAmountInput}
hideInput={true} token={tokens[Field.INPUT]}
showSendWithSwap={true} onTokenSelection={address => _onTokenSelect(address)}
label={''} hideBalance={true}
id="swap-currency-input" hideInput={true}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address} showSendWithSwap={true}
/> label={''}
</InputGroup> id="swap-currency-input"
<RowBetween style={{ width: 'fit-content' }}> otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
<ButtonSecondary />
width="fit-content" </InputGroup>
style={{ fontSize: '14px' }} <RowBetween style={{ width: 'fit-content' }}>
padding={'4px 8px'} <ButtonSecondary
onClick={() => setSendingWithSwap(true)} width="fit-content"
> style={{ fontSize: '14px' }}
+ Add a swap padding={'4px 8px'}
</ButtonSecondary> onClick={() => setSendingWithSwap(true)}
{account && ( >
<ButtonSecondary + Add a swap
style={{ fontSize: '14px', marginLeft: '8px' }} </ButtonSecondary>
padding={'4px 8px'} {account && (
width="fit-content"
disabled={atMaxAmountInput}
onClick={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
>
Input Max
</ButtonSecondary>
)}
</RowBetween>
</AutoColumn>
)}
<AutoColumn gap={'md'}>
{sendingWithSwap && (
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT && parsedAmounts[Field.INPUT] ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
{sendingWithSwap ? (
<ColumnCenter>
<RowBetween padding="0 1rem 0 12px">
<ArrowWrapper onClick={onSwitchTokens}>
<ArrowDown size="16" color={theme.text2} onClick={onSwitchTokens} />
</ArrowWrapper>
<ButtonSecondary <ButtonSecondary
onClick={() => setSendingWithSwap(false)} style={{ fontSize: '14px', marginLeft: '8px' }}
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }} padding={'4px 8px'}
padding={'4px 6px'} width="fit-content"
disabled={atMaxAmountInput}
onClick={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
> >
Remove Swap Input Max
</ButtonSecondary> </ButtonSecondary>
</RowBetween> )}
</ColumnCenter>
) : (
<CursorPointer>
<AutoColumn style={{ padding: '0 1rem' }}>
<ArrowWrapper>
<ArrowDown
size="16"
onClick={onSwitchTokens}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
)}
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
{sendingWithSwap && (
<RowBetween padding="0 1rem 0 12px">
<ArrowDown size="16" color={theme.text2} />
</RowBetween> </RowBetween>
</AutoColumn>
)}
<AutoColumn gap={'md'}>
{sendingWithSwap && (
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT && parsedAmounts[Field.INPUT] ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
{sendingWithSwap ? (
<ColumnCenter>
<RowBetween padding="0 1rem 0 12px">
<ArrowWrapper onClick={onSwitchTokens}>
<ArrowDown size="16" color={theme.text2} onClick={onSwitchTokens} />
</ArrowWrapper>
<ButtonSecondary
onClick={() => setSendingWithSwap(false)}
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }}
padding={'4px 6px'}
>
Remove Swap
</ButtonSecondary>
</RowBetween>
</ColumnCenter>
) : (
<CursorPointer>
<AutoColumn style={{ padding: '0 1rem' }}>
<ArrowWrapper>
<ArrowDown
size="16"
onClick={onSwitchTokens}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
)}
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
{sendingWithSwap && (
<RowBetween padding="0 1rem 0 12px">
<ArrowDown size="16" color={theme.text2} />
</RowBetween>
)}
</>
)} )}
</>
)}
<AutoColumn gap="lg" justify="center">
<AddressInputPanel
onChange={_onRecipient}
onError={(error: boolean, input) => {
if (error && input !== '') {
setRecipientError('Invalid Recipient')
} else if (error && input === '') {
setRecipientError('Enter a Recipient')
} else {
setRecipientError(null)
}
}}
/>
</AutoColumn>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice showInverted={showInverted} setShowInverted={setShowInverted} trade={bestTrade} />
</RowBetween>
{bestTrade && severity > 1 && ( <AutoColumn gap="lg" justify="center">
<RowBetween> <AddressInputPanel
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}> onChange={_onRecipient}
Price Impact onError={(error: boolean, input) => {
</TYPE.main> if (error && input !== '') {
<RowFixed> setRecipientError('Invalid Recipient')
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} /> } else if (error && input === '') {
<QuestionHelper text="The difference between the market price and estimated price due to trade size." /> setRecipientError('Enter a Recipient')
</RowFixed> } else {
</RowBetween> setRecipientError(null)
)} }
}}
/>
</AutoColumn> </AutoColumn>
</Card> {!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
)} <Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
</AutoColumn> <AutoColumn gap="4px">
<BottomGrouping> <RowBetween align="center">
{!account ? ( <Text fontWeight={500} fontSize={14} color={theme.text2}>
<ButtonLight Price
onClick={() => { </Text>
toggleWalletModal() <TradePrice showInverted={showInverted} setShowInverted={setShowInverted} trade={bestTrade} />
}} </RowBetween>
>
Connect Wallet {bestTrade && severity > 1 && (
</ButtonLight> <RowBetween>
) : noRoute && userHasSpecifiedInputOutput ? ( <TYPE.main
<GreyCard style={{ textAlign: 'center' }}> style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main> fontSize={14}
</GreyCard> >
) : approval === Approval.NOT_APPROVED || approval === Approval.PENDING ? ( Price Impact
<ButtonLight onClick={approveCallback} disabled={approval === Approval.PENDING}> </TYPE.main>
{approval === Approval.PENDING ? ( <RowFixed>
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots> <FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
</RowBetween>
)}
</AutoColumn>
</Card>
)}
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : approval === Approval.NOT_APPROVED || approval === Approval.PENDING ? (
<ButtonLight onClick={approveCallback} disabled={approval === Approval.PENDING}>
{approval === Approval.PENDING ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : ( ) : (
'Approve ' + tokens[Field.INPUT]?.symbol <ButtonError
onClick={() => {
setShowConfirm(true)
}}
id="send-button"
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)}
error={sendingWithSwap && isSwapValid && severity > 2}
>
<Text fontSize={20} fontWeight={500}>
{(sendingWithSwap ? swapError : null) ||
sendAmountError ||
recipientError ||
`Send${severity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)} )}
</ButtonLight> <V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
) : ( </BottomGrouping>
<ButtonError {bestTrade && (
onClick={() => { <AdvancedSwapDetailsDropdown
setShowConfirm(true) trade={bestTrade}
}} rawSlippage={allowedSlippage}
id="send-button" deadline={deadline}
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)} showAdvanced={showAdvanced}
error={sendingWithSwap && isSwapValid && severity > 2} setShowAdvanced={setShowAdvanced}
> priceImpactWithoutFee={priceImpactWithoutFee}
<Text fontSize={20} fontWeight={500}> setDeadline={setDeadline}
{(sendingWithSwap ? swapError : null) || setRawSlippage={setAllowedSlippage}
sendAmountError || />
recipientError || )}
`Send${severity > 2 ? ' Anyway' : ''}`} </Wrapper>
</Text> </AppBody>
</ButtonError> </>
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
{sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
</Wrapper>
) )
} }
...@@ -30,6 +30,7 @@ import { Field } from '../../state/swap/actions' ...@@ -30,6 +30,7 @@ import { Field } from '../../state/swap/actions'
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks' import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
import { CursorPointer, TYPE } from '../../theme' import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices' import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import AppBody from '../AppBody'
export default function Swap({ location: { search } }: RouteComponentProps) { export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search) useDefaultsFromURL(search)
...@@ -166,141 +167,147 @@ export default function Swap({ location: { search } }: RouteComponentProps) { ...@@ -166,141 +167,147 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` } for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return ( return (
<Wrapper id="swap-page"> <>
<ConfirmationModal <TokenWarningCards tokens={tokens} />
isOpen={showConfirm} <AppBody>
title="Confirm Swap" <Wrapper id="swap-page">
onDismiss={() => { <ConfirmationModal
resetModal() isOpen={showConfirm}
setShowConfirm(false) title="Confirm Swap"
}} onDismiss={() => {
attemptingTxn={attemptingTxn} resetModal()
pendingConfirmation={pendingConfirmation} setShowConfirm(false)
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
<AutoColumn gap={'md'}>
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}} }}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)} attemptingTxn={attemptingTxn}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address} pendingConfirmation={pendingConfirmation}
id="swap-currency-input" hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/> />
<CursorPointer> <AutoColumn gap={'md'}>
<AutoColumn style={{ padding: '0 1rem' }}> <>
<ArrowWrapper> <CurrencyInputPanel
<ArrowDown field={Field.INPUT}
size="16" label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
onClick={onSwitchTokens} value={formattedAmounts[Field.INPUT]}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2} showMaxButton={!atMaxAmountInput}
/> token={tokens[Field.INPUT]}
</ArrowWrapper> onUserInput={onUserInput}
</AutoColumn> onMax={() => {
</CursorPointer> maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
<CurrencyInputPanel <CursorPointer>
field={Field.OUTPUT} <AutoColumn style={{ padding: '0 1rem' }}>
value={formattedAmounts[Field.OUTPUT]} <ArrowWrapper>
onUserInput={onUserInput} <ArrowDown
// eslint-disable-next-line @typescript-eslint/no-empty-function size="16"
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'} onClick={onSwitchTokens}
showMaxButton={false} color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
token={tokens[Field.OUTPUT]} />
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)} </ArrowWrapper>
otherSelectedTokenAddress={tokens[Field.INPUT]?.address} </AutoColumn>
id="swap-currency-output" </CursorPointer>
/>
</>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && ( <CurrencyInputPanel
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}> field={Field.OUTPUT}
<AutoColumn gap="4px"> value={formattedAmounts[Field.OUTPUT]}
<RowBetween align="center"> onUserInput={onUserInput}
<Text fontWeight={500} fontSize={14} color={theme.text2}> // eslint-disable-next-line @typescript-eslint/no-empty-function
Price label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
</Text> showMaxButton={false}
<TradePrice trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} /> token={tokens[Field.OUTPUT]}
</RowBetween> onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
</>
{bestTrade && priceImpactSeverity > 1 && ( {!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<RowBetween> <Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}> <AutoColumn gap="4px">
Price Impact <RowBetween align="center">
</TYPE.main> <Text fontWeight={500} fontSize={14} color={theme.text2}>
<RowFixed> Price
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} /> </Text>
<QuestionHelper text="The difference between the market price and estimated price due to trade size." /> <TradePrice trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
</RowFixed> </RowBetween>
</RowBetween>
)} {bestTrade && priceImpactSeverity > 1 && (
</AutoColumn> <RowBetween>
</Card> <TYPE.main
)} style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
</AutoColumn> fontSize={14}
<BottomGrouping> >
{!account ? ( Price Impact
<ButtonLight </TYPE.main>
onClick={() => { <RowFixed>
toggleWalletModal() <FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
}} <QuestionHelper text="The difference between the market price and estimated price due to trade size." />
> </RowFixed>
Connect Wallet </RowBetween>
</ButtonLight> )}
) : noRoute && userHasSpecifiedInputOutput ? ( </AutoColumn>
<GreyCard style={{ textAlign: 'center' }}> </Card>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main> )}
</GreyCard> </AutoColumn>
) : approval === Approval.NOT_APPROVED || approval === Approval.PENDING ? ( <BottomGrouping>
<ButtonLight onClick={approveCallback} disabled={approval === Approval.PENDING}> {!account ? (
{approval === Approval.PENDING ? ( <ButtonLight
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots> onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : approval === Approval.NOT_APPROVED || approval === Approval.PENDING ? (
<ButtonLight onClick={approveCallback} disabled={approval === Approval.PENDING}>
{approval === Approval.PENDING ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : ( ) : (
'Approve ' + tokens[Field.INPUT]?.symbol <ButtonError
onClick={() => {
setShowConfirm(true)
}}
id="swap-button"
disabled={!isValid}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={20} fontWeight={500}>
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)} )}
</ButtonLight> <V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
) : ( </BottomGrouping>
<ButtonError {bestTrade && (
onClick={() => { <AdvancedSwapDetailsDropdown
setShowConfirm(true) trade={bestTrade}
}} rawSlippage={allowedSlippage}
id="swap-button" deadline={deadline}
disabled={!isValid} showAdvanced={showAdvanced}
error={isValid && priceImpactSeverity > 2} setShowAdvanced={setShowAdvanced}
> priceImpactWithoutFee={priceImpactWithoutFee}
<Text fontSize={20} fontWeight={500}> setDeadline={setDeadline}
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`} setRawSlippage={setAllowedSlippage}
</Text> />
</ButtonError> )}
)} </Wrapper>
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} /> </AppBody>
</BottomGrouping> </>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
<TokenWarningCards tokens={tokens} />
</Wrapper>
) )
} }
// 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 } ...@@ -22,3 +22,4 @@ export const addSerializedPair = createAction<{ serializedPair: SerializedPair }
export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>( export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>(
'removeSerializedPair' 'removeSerializedPair'
) )
export const dismissTokenWarning = createAction<{ chainId: number; tokenAddress: string }>('dismissTokenWarning')
...@@ -8,6 +8,7 @@ import { AppDispatch, AppState } from '../index' ...@@ -8,6 +8,7 @@ import { AppDispatch, AppState } from '../index'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
dismissTokenWarning,
removeSerializedToken, removeSerializedToken,
SerializedPair, SerializedPair,
SerializedToken, SerializedToken,
...@@ -132,6 +133,28 @@ export function usePairAdder(): (pair: Pair) => void { ...@@ -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 = [ const bases = [
...Object.values(WETH), ...Object.values(WETH),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'), new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
......
...@@ -2,6 +2,7 @@ import { createReducer } from '@reduxjs/toolkit' ...@@ -2,6 +2,7 @@ import { createReducer } from '@reduxjs/toolkit'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
dismissTokenWarning,
removeSerializedPair, removeSerializedPair,
removeSerializedToken, removeSerializedToken,
SerializedPair, SerializedPair,
...@@ -25,6 +26,13 @@ interface UserState { ...@@ -25,6 +26,13 @@ interface UserState {
} }
} }
// the token warnings that the user has dismissed
dismissedTokenWarnings?: {
[chainId: number]: {
[tokenAddress: string]: true
}
}
pairs: { pairs: {
[chainId: number]: { [chainId: number]: {
// keyed by token0Address:token1Address // keyed by token0Address:token1Address
...@@ -79,6 +87,11 @@ export default createReducer(initialState, builder => ...@@ -79,6 +87,11 @@ export default createReducer(initialState, builder =>
delete state.tokens[chainId][address] delete state.tokens[chainId][address]
state.timestamp = currentTimestamp() 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 } }) => { .addCase(addSerializedPair, (state, { payload: { serializedPair } }) => {
if ( if (
serializedPair.token0.chainId === serializedPair.token1.chainId && 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