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,146 +656,146 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
} ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setPendingConfirmation(true)
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)
<AppBody>
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
}}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
pair={pair}
label="Input"
id="add-liquidity-input"
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountOutput && onMax(maxAmountOutput?.toExact(), Field.OUTPUT)
<SearchModal
isOpen={showSearch}
onDismiss={() => {
setShowSearch(false)
}}
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>
<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}
token={tokens[Field.INPUT]}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
pair={pair}
label="Input"
id="add-liquidity-input"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
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] && (
<>
<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
onClick={() => {
setShowConfirm(true)
}}
>
<ButtonPrimary disabled={true}>
<Text fontSize={20} fontWeight={500}>
Supply
{generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'}
</Text>
</ButtonPrimary>
)
) : (
<ButtonPrimary disabled={true}>
<Text fontSize={20} fontWeight={500}>
{generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'}
</Text>
</ButtonPrimary>
)}
</AutoColumn>
{!noLiquidity && (
<FixedBottom>
<AutoColumn>
<PositionCard pair={pair} minimal={true} />
</AutoColumn>
</FixedBottom>
)}
</AutoColumn>
{!noLiquidity && (
<FixedBottom>
<AutoColumn>
<PositionCard pair={pair} minimal={true} />
</AutoColumn>
</FixedBottom>
)}
</Wrapper>
</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,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() {
return (
<>
<Suspense fallback={null}>
<BrowserRouter>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<AppWrapper>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<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="/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 component={RedirectPathToSwapOnly} />
</Switch>
</Body>
</Web3ReactManager>
<Footer />
</BodyWrapper>
<StyledRed />
</AppWrapper>
</BrowserRouter>
<div id="popover-container" />
</Suspense>
</>
<Suspense fallback={null}>
<BrowserRouter>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<AppWrapper>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<BodyWrapper>
<Popups />
<Web3ReactManager>
<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={PoolFinder} />
<Route exact strict path="/pool" component={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>
</Web3ReactManager>
<Footer />
</BodyWrapper>
<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,59 +59,60 @@ function Supply({ history }: RouteComponentProps) {
})
return (
<AutoColumn gap="lg" justify="center">
<ButtonPrimary
id="join-pool-button"
padding="16px"
onClick={() => {
setShowPoolSearch(true)
}}
>
<Text fontWeight={500} fontSize={20}>
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
</Text>
</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>
<AppBody>
<AutoColumn gap="lg" justify="center">
<ButtonPrimary
id="join-pool-button"
padding="16px"
onClick={() => {
setShowPoolSearch(true)
}}
>
<Text fontWeight={500} fontSize={20}>
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
</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>
</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>
</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 { 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,171 +653,173 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
} and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}`
return (
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
resetModalState()
setShowConfirm(false)
}}
attemptingTxn={attemptedRemoval}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
title="You will receive"
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<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 && (
<AppBody>
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
resetModalState()
setShowConfirm(false)
}}
attemptingTxn={attemptedRemoval}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
title="You will receive"
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<MaxButton onClick={() => handlePresetPercentage(25)} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(50)} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(75)} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(100)} width="20%">
Max
</MaxButton>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowAdvanced(!showAdvanced)
}}
>
{showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
</ClickableText>
</RowBetween>
)}
</AutoColumn>
</LightCard>
{!showAdvanced && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>{' '}
<LightCard>
<AutoColumn gap="10px">
<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>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} />
<MaxButton onClick={() => handlePresetPercentage(25)} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(50)} width="20%">
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}>
{tokens[Field.TOKEN0]?.symbol}
{formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} />
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN0]?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN1]?.symbol}
{formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'}
</Text>
</RowFixed>
</RowBetween>
</AutoColumn>
</LightCard>
</>
)}
{showAdvanced && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN0}
value={formattedAmounts[Field.TOKEN0]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN0]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-token0"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN1}
value={formattedAmounts[Field.TOKEN1]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN1]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-token1"
/>
</>
)}
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {pair?.token0.symbol} ={' '}
{independentField === Field.TOKEN0 || independentField === Field.LIQUIDITY
? route?.midPrice.toSignificant(6)
: route?.midPrice.invert().toSignificant(6)}{' '}
{pair?.token1.symbol}
</div>
</RowBetween>
</div>
<div style={{ position: 'relative' }}>
<ButtonPrimary
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
{inputError || outputError || poolTokenError || generalError || 'Remove'}
</Text>
</ButtonPrimary>
<FixedBottom>
<PositionCard pair={pair} minimal={true} />
</FixedBottom>
</div>
</AutoColumn>
</Wrapper>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN1]?.symbol}
</Text>
</RowFixed>
</RowBetween>
</AutoColumn>
</LightCard>
</>
)}
{showAdvanced && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN0}
value={formattedAmounts[Field.TOKEN0]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN0]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-token0"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN1}
value={formattedAmounts[Field.TOKEN1]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN1]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-token1"
/>
</>
)}
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {pair?.token0.symbol} ={' '}
{independentField === Field.TOKEN0 || independentField === Field.LIQUIDITY
? route?.midPrice.toSignificant(6)
: route?.midPrice.invert().toSignificant(6)}{' '}
{pair?.token1.symbol}
</div>
</RowBetween>
</div>
<div style={{ position: 'relative' }}>
<ButtonPrimary
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
{inputError || outputError || poolTokenError || generalError || 'Remove'}
</Text>
</ButtonPrimary>
<FixedBottom>
<PositionCard pair={pair} minimal={true} />
</FixedBottom>
</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,231 +275,237 @@ export default function Send({ location: { search } }: RouteComponentProps) {
: null
return (
<Wrapper id="send-page">
<ConfirmationModal
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
resetModal()
setShowConfirm(false)
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
{!sendingWithSwap && (
<AutoColumn justify="center" style={{ marginBottom: '1rem' }}>
<InputGroup gap="lg" justify="center">
<StyledNumerical
id="sending-no-swap-input"
value={formattedAmounts[Field.INPUT]}
onUserInput={val => onUserInput(Field.INPUT, val)}
/>
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={(field, val) => onUserInput(Field.INPUT, val)}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={address => _onTokenSelect(address)}
hideBalance={true}
hideInput={true}
showSendWithSwap={true}
label={''}
id="swap-currency-input"
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
/>
</InputGroup>
<RowBetween style={{ width: 'fit-content' }}>
<ButtonSecondary
width="fit-content"
style={{ fontSize: '14px' }}
padding={'4px 8px'}
onClick={() => setSendingWithSwap(true)}
>
+ Add a swap
</ButtonSecondary>
{account && (
<ButtonSecondary
style={{ fontSize: '14px', marginLeft: '8px' }}
padding={'4px 8px'}
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>
<>
{sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
<AppBody>
<Wrapper id="send-page">
<ConfirmationModal
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
resetModal()
setShowConfirm(false)
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
{!sendingWithSwap && (
<AutoColumn justify="center" style={{ marginBottom: '1rem' }}>
<InputGroup gap="lg" justify="center">
<StyledNumerical
id="sending-no-swap-input"
value={formattedAmounts[Field.INPUT]}
onUserInput={val => onUserInput(Field.INPUT, val)}
/>
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={(field, val) => onUserInput(Field.INPUT, val)}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={address => _onTokenSelect(address)}
hideBalance={true}
hideInput={true}
showSendWithSwap={true}
label={''}
id="swap-currency-input"
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
/>
</InputGroup>
<RowBetween style={{ width: 'fit-content' }}>
<ButtonSecondary
width="fit-content"
style={{ fontSize: '14px' }}
padding={'4px 8px'}
onClick={() => setSendingWithSwap(true)}
>
+ Add a swap
</ButtonSecondary>
{account && (
<ButtonSecondary
onClick={() => setSendingWithSwap(false)}
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }}
padding={'4px 6px'}
style={{ fontSize: '14px', marginLeft: '8px' }}
padding={'4px 8px'}
width="fit-content"
disabled={atMaxAmountInput}
onClick={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
>
Remove Swap
Input Max
</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>
)}
<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 && (
<RowBetween>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}>
Price Impact
</TYPE.main>
<RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
</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>
</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>
{!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 && (
<RowBetween>
<TYPE.main
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
fontSize={14}
>
Price Impact
</TYPE.main>
<RowFixed>
<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>
) : (
<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>
)}
<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>
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</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,141 +167,147 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<Wrapper id="swap-page">
<ConfirmationModal
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
resetModal()
setShowConfirm(false)
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
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())
<>
<TokenWarningCards tokens={tokens} />
<AppBody>
<Wrapper id="swap-page">
<ConfirmationModal
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
resetModal()
setShowConfirm(false)
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
<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>
<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)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
</>
<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>
{!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 trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
</RowBetween>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
</>
{bestTrade && priceImpactSeverity > 1 && (
<RowBetween>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}>
Price Impact
</TYPE.main>
<RowFixed>
<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>
{!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 trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
</RowBetween>
{bestTrade && priceImpactSeverity > 1 && (
<RowBetween>
<TYPE.main
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
fontSize={14}
>
Price Impact
</TYPE.main>
<RowFixed>
<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="swap-button"
disabled={!isValid}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={20} fontWeight={500}>
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
</ButtonLight>
) : (
<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>
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
<TokenWarningCards tokens={tokens} />
</Wrapper>
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</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