Commit 28d8f0b0 authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

Improvement(token import): update import flow and style tweaks (#1273)

* update import flow and style tweaks

* update tests

* Update tsconfig.json
parent 8bed3900
describe('Warning', () => {
beforeEach(() => {
cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a')
})
it('Check that warning is displayed', () => {
cy.get('.token-warning-container').should('be.visible')
})
it('Check that warning hides after button dismissal', () => {
cy.get('.token-dismiss-button').should('be.disabled')
cy.get('.understand-checkbox').click()
cy.get('.token-dismiss-button').should('not.be.disabled')
cy.get('.token-dismiss-button').click()
cy.get('.token-warning-container').should('not.be.visible')
})
})
......@@ -79,7 +79,7 @@ export default function CurrencySearchModal({
/>
) : modalView === CurrencyModalView.importToken && importToken ? (
<ImportToken
token={importToken}
tokens={[importToken]}
onDismiss={onDismiss}
onBack={() =>
setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search)
......
import React, { CSSProperties } from 'react'
import { Token } from '@uniswap/sdk'
import { RowBetween, RowFixed, AutoRow } from 'components/Row'
import { AutoRow, RowFixed } from 'components/Row'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import { TYPE } from 'theme'
......@@ -13,9 +13,15 @@ import styled from 'styled-components'
import { useIsUserAddedToken, useIsTokenActive } from 'hooks/Tokens'
import { CheckCircle } from 'react-feather'
const TokenSection = styled.div`
padding: 8px 20px;
const TokenSection = styled.div<{ dim?: boolean }>`
padding: 4px 20px;
height: 56px;
display: grid;
grid-template-columns: auto minmax(auto, 1fr) auto;
grid-gap: 16px;
align-items: center;
opacity: ${({ dim }) => (dim ? '0.6' : '1')};
`
const CheckIcon = styled(CheckCircle)`
......@@ -61,20 +67,16 @@ export default function ImportRow({
return (
<TokenSection style={style}>
<RowBetween>
<AutoRow style={{ opacity: dim ? '0.6' : '1' }}>
<CurrencyLogo currency={token} size={'24px'} />
<AutoColumn gap="4px">
<CurrencyLogo currency={token} size={'24px'} style={{ opacity: dim ? '0.6' : '1' }} />
<AutoColumn gap="4px" style={{ opacity: dim ? '0.6' : '1' }}>
<AutoRow>
<TYPE.body ml="8px" fontWeight={500}>
{token.symbol}
</TYPE.body>
<TYPE.body fontWeight={500}>{token.symbol}</TYPE.body>
<TYPE.darkGray ml="8px" fontWeight={300}>
<NameOverflow title={token.name}>{token.name}</NameOverflow>
</TYPE.darkGray>
</AutoRow>
{list && list.logoURI && (
<RowFixed style={{ marginLeft: '8px' }}>
<RowFixed>
<TYPE.small mr="4px" color={theme.text3}>
via {list.name}
</TYPE.small>
......@@ -82,7 +84,6 @@ export default function ImportRow({
</RowFixed>
)}
</AutoColumn>
</AutoRow>
{!isActive && !isAdded ? (
<ButtonPrimary
width="fit-content"
......@@ -102,7 +103,7 @@ export default function ImportRow({
<TYPE.main color={theme.green1}>Active</TYPE.main>
</RowFixed>
)}
</RowBetween>
{/* </RowBetween> */}
</TokenSection>
)
}
......@@ -39,13 +39,13 @@ const AddressText = styled(TYPE.blue)`
`
interface ImportProps {
token: Token
onBack: () => void
onDismiss: () => void
handleCurrencySelect: (currency: Currency) => void
tokens: Token[]
onBack?: () => void
onDismiss?: () => void
handleCurrencySelect?: (currency: Currency) => void
}
export function ImportToken({ token, onBack, onDismiss, handleCurrencySelect }: ImportProps) {
export function ImportToken({ tokens, onBack, onDismiss, handleCurrencySelect }: ImportProps) {
const theme = useTheme()
const { chainId } = useActiveWeb3React()
......@@ -57,20 +57,26 @@ export function ImportToken({ token, onBack, onDismiss, handleCurrencySelect }:
// use for showing import source on inactive tokens
const inactiveTokenList = useCombinedInactiveList()
const list = chainId && inactiveTokenList?.[chainId]?.[token.address]?.list
// higher warning severity if either is not on a list
const fromLists =
(chainId && inactiveTokenList?.[chainId]?.[tokens[0]?.address]?.list) ||
(chainId && inactiveTokenList?.[chainId]?.[tokens[1]?.address]?.list)
return (
<Wrapper>
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
<RowBetween>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
<TYPE.mediumHeader>Import Token</TYPE.mediumHeader>
<CloseIcon onClick={onDismiss} />
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div></div>}
<TYPE.mediumHeader>Import {tokens.length > 1 ? 'Tokens' : 'Token'}</TYPE.mediumHeader>
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div></div>}
</RowBetween>
</PaddedColumn>
<SectionBreak />
<PaddedColumn gap="md">
<Card backgroundColor={theme.bg2}>
{tokens.map(token => {
const list = chainId && inactiveTokenList?.[chainId]?.[token.address]?.list
return (
<Card backgroundColor={theme.bg2} key={'import' + token.address} className=".token-warning-container">
<AutoColumn gap="10px">
<AutoRow align="center">
<CurrencyLogo currency={token} size={'24px'} />
......@@ -103,40 +109,51 @@ export function ImportToken({ token, onBack, onDismiss, handleCurrencySelect }:
)}
</AutoColumn>
</Card>
<Card style={{ backgroundColor: list ? transparentize(0.8, theme.yellow2) : transparentize(0.8, theme.red1) }}>
)
})}
<Card
style={{ backgroundColor: fromLists ? transparentize(0.8, theme.yellow2) : transparentize(0.8, theme.red1) }}
>
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
<AlertTriangle stroke={list ? theme.yellow2 : theme.red1} size={32} />
<TYPE.body fontWeight={600} fontSize={20} color={list ? theme.yellow2 : theme.red1}>
<AlertTriangle stroke={fromLists ? theme.yellow2 : theme.red1} size={32} />
<TYPE.body fontWeight={600} fontSize={20} color={fromLists ? theme.yellow2 : theme.red1}>
Trade at your own risk!
</TYPE.body>
</AutoColumn>
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
<TYPE.body fontWeight={400} color={list ? theme.yellow2 : theme.red1}>
<TYPE.body fontWeight={400} color={fromLists ? theme.yellow2 : theme.red1}>
Anyone can create a token, including creating fake versions of existing tokens that claim to represent
projects.
</TYPE.body>
<TYPE.body fontWeight={600} color={list ? theme.yellow2 : theme.red1}>
<TYPE.body fontWeight={600} color={fromLists ? theme.yellow2 : theme.red1}>
If you purchase this token, you may not be able to sell it back.
</TYPE.body>
</AutoColumn>
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
<Checkbox name="confirmed" type="checkbox" checked={confirmed} onChange={() => setConfirmed(!confirmed)} />
<TYPE.body ml="10px" fontSize="16px" color={list ? theme.yellow2 : theme.red1} fontWeight={500}>
<Checkbox
className=".understand-checkbox"
name="confirmed"
type="checkbox"
checked={confirmed}
onChange={() => setConfirmed(!confirmed)}
/>
<TYPE.body ml="10px" fontSize="16px" color={fromLists ? theme.yellow2 : theme.red1} fontWeight={500}>
I understand
</TYPE.body>
</AutoRow>
</Card>
<ButtonPrimary
disabled={!confirmed}
altDisabledStyle={true}
borderRadius="20px"
padding="10px 1rem"
onClick={() => {
addToken(token)
handleCurrencySelect(token)
tokens.map(token => addToken(token))
handleCurrencySelect && handleCurrencySelect(tokens[0])
}}
className=".token-dismiss-button"
>
Import
</ButtonPrimary>
......
import { Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useCallback, useMemo, useState } from 'react'
import styled from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, shortenAddress } from '../../utils'
import CurrencyLogo from '../CurrencyLogo'
import React, { useCallback } from 'react'
import Modal from '../Modal'
import { AutoRow, RowBetween } from '../Row'
import { AutoColumn } from '../Column'
import { AlertTriangle } from 'react-feather'
import { ButtonError } from '../Button'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme }) => transparentize(0.6, theme.bg3)};
padding: 0.75rem;
border-radius: 20px;
`
const WarningContainer = styled.div`
max-width: 420px;
width: 100%;
padding: 1rem;
background: rgba(242, 150, 2, 0.05);
border: 1px solid #f3841e;
border-radius: 20px;
overflow: auto;
`
const StyledWarningIcon = styled(AlertTriangle)`
stroke: ${({ theme }) => theme.red2};
`
interface TokenWarningCardProps {
token?: Token
}
function TokenWarningCard({ token }: TokenWarningCardProps) {
const { chainId } = useActiveWeb3React()
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? ''
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
if (!token || !chainId) return false
return Object.keys(allTokens).some(tokenAddress => {
const userToken = allTokens[tokenAddress]
if (userToken.equals(token)) {
return false
}
return userToken.symbol?.toLowerCase() === tokenSymbol || userToken.name?.toLowerCase() === tokenName
})
}, [token, chainId, allTokens, tokenSymbol, tokenName])
if (!token) return null
return (
<Wrapper error={duplicateNameOrSymbol}>
<AutoRow gap="6px">
<AutoColumn gap="24px">
<CurrencyLogo currency={token} size={'16px'} />
<div> </div>
</AutoColumn>
<AutoColumn gap="10px" justify="flex-start">
<TYPE.main>
{token && token.name && token.symbol && token.name !== token.symbol
? `${token.name} (${token.symbol})`
: token.name || token.symbol}{' '}
</TYPE.main>
{chainId && (
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
<TYPE.blue title={token.address}>{shortenAddress(token.address)} (View on Etherscan)</TYPE.blue>
</ExternalLink>
)}
</AutoColumn>
</AutoRow>
</Wrapper>
)
}
import { ImportToken } from 'components/SearchModal/ImportToken'
export default function TokenWarningModal({
isOpen,
......@@ -92,62 +12,11 @@ export default function TokenWarningModal({
tokens: Token[]
onConfirm: () => void
}) {
const [understandChecked, setUnderstandChecked] = useState(false)
const toggleUnderstand = useCallback(() => setUnderstandChecked(uc => !uc), [])
const handleDismiss = useCallback(() => null, [])
return (
<Modal isOpen={isOpen} onDismiss={handleDismiss} maxHeight={90}>
<WarningContainer className="token-warning-container">
<AutoColumn gap="lg">
<AutoRow gap="6px">
<StyledWarningIcon />
<TYPE.main color={'red2'}>Token imported</TYPE.main>
</AutoRow>
<TYPE.body color={'red2'}>
Anyone can create an ERC20 token on Ethereum with <em>any</em> name, including creating fake versions of
existing tokens and tokens that claim to represent projects that do not have a token.
</TYPE.body>
<TYPE.body color={'red2'}>
This interface can load arbitrary tokens by token addresses. Please take extra caution and do your research
when interacting with arbitrary ERC20 tokens.
</TYPE.body>
<TYPE.body color={'red2'}>
If you purchase an arbitrary token, <strong>you may not be able to sell it back.</strong>
</TYPE.body>
{tokens.map(token => {
return <TokenWarningCard key={token.address} token={token} />
})}
<RowBetween>
<div>
<label style={{ cursor: 'pointer', userSelect: 'none' }}>
<input
type="checkbox"
className="understand-checkbox"
checked={understandChecked}
onChange={toggleUnderstand}
/>{' '}
I understand
</label>
</div>
<ButtonError
disabled={!understandChecked}
error={true}
width={'140px'}
padding="0.5rem 1rem"
className="token-dismiss-button"
style={{
borderRadius: '10px'
}}
onClick={() => {
onConfirm()
}}
>
<TYPE.body color="white">Continue</TYPE.body>
</ButtonError>
</RowBetween>
</AutoColumn>
</WarningContainer>
<ImportToken tokens={tokens} handleCurrencySelect={onConfirm} />
</Modal>
)
}
......@@ -24,7 +24,7 @@ import SwapHeader from '../../components/swap/SwapHeader'
import { INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useCurrency, useDefaultTokens } from '../../hooks/Tokens'
import { useCurrency, useAllTokens } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import useENSAddress from '../../hooks/useENSAddress'
import { useSwapCallback } from '../../hooks/useSwapCallback'
......@@ -66,13 +66,13 @@ export default function Swap() {
setDismissTokenWarning(true)
}, [])
// dismiss warning if all imported tokens are in default list
const defaultTokens = useDefaultTokens()
// dismiss warning if all imported tokens are in active lists
const defaultTokens = useAllTokens()
const importTokensNotInDefault =
urlLoadedTokens &&
urlLoadedTokens.filter((token: Token) => {
return !Boolean(token.address in defaultTokens)
}).length > 0
})
const { account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
......@@ -294,14 +294,13 @@ export default function Swap() {
return (
<>
<TokenWarningModal
isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning && importTokensNotInDefault}
tokens={urlLoadedTokens}
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
tokens={importTokensNotInDefault}
onConfirm={handleConfirmTokenWarning}
/>
<SwapPoolTabs active={'swap'} />
<AppBody>
<SwapHeader />
{/* <Separator /> */}
<Wrapper id="swap-page">
<ConfirmSwapModal
isOpen={showConfirm}
......
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