Skip to content

Commit

Permalink
improvement(token warnings): show better warnings for imported tokens (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
ianlapham authored Jul 30, 2020
1 parent 1b07e95 commit 2150450
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 98 deletions.
19 changes: 19 additions & 0 deletions cypress/integration/token-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"ajv": "^6.12.3",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2",
"cypress": "^4.5.0",
"cypress": "^4.11.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
Expand Down
164 changes: 81 additions & 83 deletions src/components/TokenWarningCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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(() => {
Expand All @@ -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>
)
}
8 changes: 5 additions & 3 deletions src/pages/AppBody.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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%;
Expand All @@ -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>
}
19 changes: 14 additions & 5 deletions src/pages/Swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -424,7 +434,6 @@ export default function Swap() {
</BottomGrouping>
</Wrapper>
</AppBody>

<AdvancedSwapDetailsDropdown trade={trade} />
</>
)
Expand Down
20 changes: 15 additions & 5 deletions src/state/user/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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'
Expand All @@ -19,6 +19,8 @@ import {
updateUserExpertMode,
updateUserSlippageTolerance
} from './actions'
import { useDefaultTokenList } from '../lists/hooks'
import { isDefaultToken } from '../../utils'

function serializeToken(token: Token): SerializedToken {
return {
Expand Down Expand Up @@ -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])
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/theme/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function colors(darkMode: boolean): Colors {

// other
red1: '#FF6871',
red2: '#F82D3A',
green1: '#27AE60',
yellow1: '#FFE270',
yellow2: '#F3841E'
Expand Down
1 change: 1 addition & 0 deletions src/theme/styled.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface Colors {

// other
red1: Color
red2: Color
green1: Color
yellow1: Color
yellow2: Color
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down

1 comment on commit 2150450

@vercel
Copy link

@vercel vercel bot commented on 2150450 Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.