From 215045076040f9a46cf36597e3835cf7aac8d39a Mon Sep 17 00:00:00 2001 From: Ian Lapham Date: Thu, 30 Jul 2020 15:21:37 -0400 Subject: [PATCH] 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 --- cypress/integration/token-warning.ts | 19 +++ package.json | 2 +- src/components/TokenWarningCard/index.tsx | 164 +++++++++++----------- src/pages/AppBody.tsx | 8 +- src/pages/Swap/index.tsx | 19 ++- src/state/user/hooks.tsx | 20 ++- src/theme/index.tsx | 1 + src/theme/styled.d.ts | 1 + yarn.lock | 2 +- 9 files changed, 138 insertions(+), 98 deletions(-) create mode 100644 cypress/integration/token-warning.ts diff --git a/cypress/integration/token-warning.ts b/cypress/integration/token-warning.ts new file mode 100644 index 00000000000..c6f027501d5 --- /dev/null +++ b/cypress/integration/token-warning.ts @@ -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') + }) +}) diff --git a/package.json b/package.json index c745e3cc375..ee1383e5063 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/TokenWarningCard/index.tsx b/src/components/TokenWarningCard/index.tsx index b5cbcc93437..7432251f651 100644 --- a/src/components/TokenWarningCard/index.tsx +++ b/src/components/TokenWarningCard/index.tsx @@ -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 { 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 ( - {duplicateNameOrSymbol ? null : ( - - - - )} - - {duplicateNameOrSymbol ? 'Duplicate token name or symbol' : 'Imported token'} - - - - -
- {token && token.name && token.symbol && token.name !== token.symbol - ? `${token.name} (${token.symbol})` - : token.name || token.symbol} -
- - (View on Etherscan) - -
- - Verify this is the correct token before making any transactions. - + + + +
+
+ + + {token && token.name && token.symbol && token.name !== token.symbol + ? `${token.name} (${token.symbol})` + : token.name || token.symbol} + + + (View on Etherscan) + + +
) } -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 ( - - {Object.keys(currencies).map(field => - currencies[field] instanceof Token ? ( - - ) : null - )} + + + + + Token imported + + + 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. + + + Similar to Etherscan, this site can load arbitrary tokens via token addresses. Please do your own research + before interacting with any ERC20 token. + + {Object.keys(currencies).map(field => { + const dismissed = field === Field.INPUT ? dismissedToken0 : dismissedToken1 + return currencies[field] instanceof Token && !dismissed ? ( + + ) : null + })} + +
+ { + dismissToken0 && dismissToken0() + dismissToken1 && dismissToken1() + }} + > + + I understand + + +
+ + ) } diff --git a/src/pages/AppBody.tsx b/src/pages/AppBody.tsx index 2e9c4a368ed..3c80b439945 100644 --- a/src/pages/AppBody.tsx +++ b/src/pages/AppBody.tsx @@ -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%; @@ -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 {children} +export default function AppBody({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) { + return {children} } diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 1b42d86b851..5d5dbdc5665 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -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 ( <> - - + {showWarning && } + - ) diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index 89fed7fe44d..41b922c6e04 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -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' @@ -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( state => state.user.dismissedTokenWarnings ) const dispatch = useDispatch() + // 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]) } /** diff --git a/src/theme/index.tsx b/src/theme/index.tsx index abf74bb0591..9b91a4270ea 100644 --- a/src/theme/index.tsx +++ b/src/theme/index.tsx @@ -75,6 +75,7 @@ export function colors(darkMode: boolean): Colors { // other red1: '#FF6871', + red2: '#F82D3A', green1: '#27AE60', yellow1: '#FFE270', yellow2: '#F3841E' diff --git a/src/theme/styled.d.ts b/src/theme/styled.d.ts index 6db2dc3dc16..d045ebc4ef5 100644 --- a/src/theme/styled.d.ts +++ b/src/theme/styled.d.ts @@ -39,6 +39,7 @@ export interface Colors { // other red1: Color + red2: Color green1: Color yellow1: Color yellow2: Color diff --git a/yarn.lock b/yarn.lock index e9247839a8d..ecc57d8e20c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==