Commit 21504507 authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

improvement(token warnings): show better warnings for imported tokens (#1005)

* add updated ui warnings for imported tokens

* remove useless styling

* update to surpress on default tokens

* add integration tests for warning cards on token import

* remove callbacks as props in token warning card
parent 1b07e958
describe('Warning', () => {
beforeEach(() => {
cy.clearLocalStorage()
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').click()
cy.get('.token-warning-container').should('not.be.visible')
})
it('Check supression persists across sessions.', () => {
cy.get('.token-warning-container').should('be.visible')
cy.get('.token-dismiss-button').click()
cy.reload()
cy.get('.token-warning-container').should('not.be.visible')
})
})
......@@ -2,66 +2,41 @@ import { Currency, Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useMemo } from 'react'
import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useDefaultTokenList } from '../../state/lists/hooks'
import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper'
import CurrencyLogo from '../CurrencyLogo'
import { AutoRow, RowBetween } from '../Row'
import { AutoColumn } from '../Column'
import { AlertTriangle } from 'react-feather'
import { ButtonError } from '../Button'
import { useTokenWarningDismissal } from '../../state/user/hooks'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
position: relative;
padding: 1rem;
/* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
border-radius: 10px;
margin-bottom: 20px;
display: grid;
grid-template-rows: 14px auto auto;
grid-row-gap: 14px;
background: ${({ theme }) => transparentize(0.6, theme.white)};
padding: 0.75rem;
border-radius: 20px;
`
const Row = styled.div`
display: flex;
align-items: center;
justify-items: flex-start;
& > * {
margin-right: 6px;
}
`
const CloseColor = styled(Close)`
color: #aeaeae;
`
const CloseIcon = styled.div`
position: absolute;
right: 1rem;
top: 12px;
&:hover {
cursor: pointer;
opacity: 0.6;
}
& > * {
height: 16px;
width: 16px;
}
const WarningContainer = styled.div`
max-width: 420px;
width: 100%;
padding: 1rem;
background: rgba(242, 150, 2, 0.05);
border: 1px solid #f3841e;
box-sizing: border-box;
border-radius: 20px;
margin-bottom: 2rem;
`
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
parameter.
const StyledWarningIcon = styled(AlertTriangle)`
stroke: ${({ theme }) => theme.red2};
`
const DUPLICATE_NAME_HELP_TEXT = `${HELP_TEXT} This token has the same name or symbol as another token in your list.`
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
token?: Token
}
......@@ -74,8 +49,6 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? ''
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
......@@ -90,52 +63,77 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
})
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
if (isDefault || !token || dismissed) return null
if (isDefault || !token) return null
return (
<Wrapper error={duplicateNameOrSymbol} {...rest}>
{duplicateNameOrSymbol ? null : (
<CloseIcon onClick={dismissTokenWarning}>
<CloseColor />
</CloseIcon>
)}
<Row>
<TYPE.subHeader>{duplicateNameOrSymbol ? 'Duplicate token name or symbol' : 'Imported token'}</TYPE.subHeader>
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
</Row>
<Row>
<CurrencyLogo currency={token} />
<div style={{ fontWeight: 500 }}>
{token && token.name && token.symbol && token.name !== token.symbol
? `${token.name} (${token.symbol})`
: token.name || token.symbol}
</div>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
(View on Etherscan)
</ExternalLink>
</Row>
<Row>
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>
</Row>
<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>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
<TYPE.blue> (View on Etherscan)</TYPE.blue>
</ExternalLink>
</AutoColumn>
</AutoRow>
</Wrapper>
)
}
const WarningContainer = styled.div`
max-width: 420px;
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
`
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
const { chainId } = useActiveWeb3React()
const [dismissedToken0, dismissToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
const [dismissedToken1, dismissToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
return (
<WarningContainer>
{Object.keys(currencies).map(field =>
currencies[field] instanceof Token ? (
<TokenWarningCard style={{ marginBottom: 14 }} key={field} token={currencies[field]} />
) : null
)}
<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 and name any ERC20 token on Ethereum, 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'}>
Similar to Etherscan, this site can load arbitrary tokens via token addresses. Please do your own research
before interacting with any ERC20 token.
</TYPE.body>
{Object.keys(currencies).map(field => {
const dismissed = field === Field.INPUT ? dismissedToken0 : dismissedToken1
return currencies[field] instanceof Token && !dismissed ? (
<TokenWarningCard key={field} token={currencies[field]} />
) : null
})}
<RowBetween>
<div />
<ButtonError
error={true}
width={'140px'}
padding="0.5rem 1rem"
style={{
borderRadius: '10px'
}}
onClick={() => {
dismissToken0 && dismissToken0()
dismissToken1 && dismissToken1()
}}
>
<TYPE.body color="white" className="token-dismiss-button">
I understand
</TYPE.body>
</ButtonError>
<div />
</RowBetween>
</AutoColumn>
</WarningContainer>
)
}
import React from 'react'
import styled from 'styled-components'
export const BodyWrapper = styled.div`
export const BodyWrapper = styled.div<{ disabled?: boolean }>`
position: relative;
max-width: 420px;
width: 100%;
......@@ -10,11 +10,13 @@ export const BodyWrapper = styled.div`
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px;
padding: 1rem;
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
pointer-events: ${({ disabled }) => disabled && 'none'};
`
/**
* The styled container element that wraps the content of most pages and the tabs.
*/
export default function AppBody({ children }: { children: React.ReactNode }) {
return <BodyWrapper>{children}</BodyWrapper>
export default function AppBody({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) {
return <BodyWrapper disabled={disabled}>{children}</BodyWrapper>
}
......@@ -37,7 +37,12 @@ import {
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { useExpertModeManager, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import {
useExpertModeManager,
useUserDeadline,
useUserSlippageTolerance,
useTokenWarningDismissal
} from '../../state/user/hooks'
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
......@@ -47,7 +52,7 @@ import { ClickableText } from '../Pool/styleds'
export default function Swap() {
useDefaultsFromURLSearch()
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
......@@ -241,10 +246,15 @@ export default function Swap() {
currencies[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${currencies[Field.OUTPUT]?.symbol}`
const [dismissedToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
const [dismissedToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
const showWarning =
(!dismissedToken0 && !!currencies[Field.INPUT]) || (!dismissedToken1 && !!currencies[Field.OUTPUT])
return (
<>
<TokenWarningCards currencies={currencies} />
<AppBody>
{showWarning && <TokenWarningCards currencies={currencies} />}
<AppBody disabled={!!showWarning}>
<SwapPoolTabs active={'swap'} />
<Wrapper id="swap-page">
<ConfirmationModal
......@@ -424,7 +434,6 @@ export default function Swap() {
</BottomGrouping>
</Wrapper>
</AppBody>
<AdvancedSwapDetailsDropdown trade={trade} />
</>
)
......
import { ChainId, Pair, Token } from '@uniswap/sdk'
import { ChainId, Pair, Token, Currency } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
......@@ -19,6 +19,8 @@ import {
updateUserExpertMode,
updateUserSlippageTolerance
} from './actions'
import { useDefaultTokenList } from '../lists/hooks'
import { isDefaultToken } from '../../utils'
function serializeToken(token: Token): SerializedToken {
return {
......@@ -165,22 +167,30 @@ 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)] {
export function useTokenWarningDismissal(chainId?: number, token?: Currency): [boolean, null | (() => void)] {
const dismissalState = useSelector<AppState, AppState['user']['dismissedTokenWarnings']>(
state => state.user.dismissedTokenWarnings
)
const dispatch = useDispatch<AppDispatch>()
// get default list, mark as dismissed if on list
const defaultList = useDefaultTokenList()
const isDefault = isDefaultToken(defaultList, token)
return useMemo(() => {
if (!chainId || !token) return [false, null]
const dismissed: boolean = dismissalState?.[chainId]?.[token.address] === true
const dismissed: boolean =
token instanceof Token ? dismissalState?.[chainId]?.[token.address] === true || isDefault : true
const callback = dismissed ? null : () => dispatch(dismissTokenWarning({ chainId, tokenAddress: token.address }))
const callback =
dismissed || !(token instanceof Token)
? null
: () => dispatch(dismissTokenWarning({ chainId, tokenAddress: token.address }))
return [dismissed, callback]
}, [chainId, token, dismissalState, dispatch])
}, [chainId, token, dismissalState, isDefault, dispatch])
}
/**
......
......@@ -75,6 +75,7 @@ export function colors(darkMode: boolean): Colors {
// other
red1: '#FF6871',
red2: '#F82D3A',
green1: '#27AE60',
yellow1: '#FFE270',
yellow2: '#F3841E'
......
......@@ -39,6 +39,7 @@ export interface Colors {
// other
red1: Color
red2: Color
green1: Color
yellow1: Color
yellow2: Color
......
......@@ -5690,7 +5690,7 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@*, cypress@^4.5.0:
cypress@*, cypress@^4.11.0:
version "4.11.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.11.0.tgz#054b0b85fd3aea793f186249ee1216126d5f0a7e"
integrity sha512-6Yd598+KPATM+dU1Ig0g2hbA+R/o1MAKt0xIejw4nZBVLSplCouBzqeKve6XsxGU6n4HMSt/+QYsWfFcoQeSEw==
......
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