Skip to content

Commit

Permalink
Updates to Token Modal (Uniswap#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianlapham authored and NoahZinsmeister committed Aug 13, 2019
1 parent be2012c commit 677537c
Show file tree
Hide file tree
Showing 25 changed files with 4,180 additions and 954 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ yarn-debug.log*
yarn-error.log*

notes.txt
.idea/
.idea/

.vscode/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"dependencies": {
"@reach/dialog": "^0.2.8",
"@reach/tooltip": "^0.2.0",
"@uniswap/sdk": "^1.0.0-beta.4",
"copy-to-clipboard": "^3.2.0",
"escape-string-regexp": "^2.0.0",
"ethers": "^4.0.28",
"ethers": "^4.0.33",
"i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
Expand Down
4 changes: 3 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"unlock": "Unlock",
"pending": "Pending",
"selectToken": "Select a token",
"searchOrPaste": "Search Token or Paste Address",
"searchOrPaste": "Search Token Name, Symbol, or Address",
"searchOrPasteMobile": "Name, Symbol, or Address",
"noExchange": "No Exchange Found",
"exchangeRate": "Exchange Rate",
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
Expand All @@ -36,6 +37,7 @@
"youWillReceive": "You will receive at least",
"youAreBuying": "You are buying",
"itWillCost": "It will cost at most",
"forAtMost": "for at most",
"insufficientBalance": "Insufficient Balance",
"inputNotValid": "Not a valid input value",
"differentToken": "Must be different token.",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/images/circle-grey.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/images/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
197 changes: 175 additions & 22 deletions src/components/CurrencyInputPanel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useRef, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import { BigNumber } from '@uniswap/sdk'
import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp'
import { darken } from 'polished'
Expand All @@ -11,14 +12,18 @@ import { isMobile } from 'react-device-detect'

import { BorderlessInput } from '../../theme'
import { useTokenContract } from '../../hooks'
import { isAddress, calculateGasMargin } from '../../utils'
import { isAddress, calculateGasMargin, formatToUsd, formatTokenBalance, formatEthBalance } from '../../utils'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import Modal from '../Modal'
import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
import close from '../../assets/images/x.svg'
import { transparentize } from 'polished'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle-grey.svg'
import { useUSDPrice } from '../../contexts/Application'

const GAS_MARGIN = ethers.utils.bigNumberify(1000)

Expand All @@ -35,7 +40,6 @@ const SubCurrencySelect = styled.button`
outline: none;
cursor: pointer;
user-select: none;
`

const InputRow = styled.div`
Expand All @@ -52,8 +56,11 @@ const Input = styled(BorderlessInput)`
`

const StyledBorderlessInput = styled(BorderlessInput)`
min-height: 1.75rem;
min-height: 2.5rem;
flex-shrink: 0;
text-align: left;
padding-left: 1.6rem;
background-color: ${({ theme }) => theme.concreteGray};
`

const CurrencySelect = styled.button`
Expand Down Expand Up @@ -152,10 +159,27 @@ const TokenModal = styled.div`
width: 100%;
`

const ModalHeader = styled.div`
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 2rem;
height: 60px;
`

const CloseIcon = styled.div`
position: absolute;
right: 1.4rem;
&:hover {
cursor: pointer;
}
`

const SearchContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap}
padding: 1rem;
border-bottom: 1px solid ${({ theme }) => theme.mercuryGray};
padding: 0.5rem 2rem;
background-color: ${({ theme }) => theme.concreteGray};
`

const TokenModalInfo = styled.div`
Expand All @@ -177,9 +201,8 @@ const TokenList = styled.div`
const TokenModalRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1.5rem;
margin: 0.25rem 0.5rem;
justify-content: space-between;
padding: 0.8rem 2rem;
cursor: pointer;
user-select: none;
Expand All @@ -188,16 +211,55 @@ const TokenModalRow = styled.div`
}
:hover {
background-color: ${({ theme }) => theme.concreteGray};
background-color: ${({ theme }) => theme.tokenRowHover};
}
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0.8rem 1rem;`}
`

const TokenRowLeft = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items : center;
`

const TokenSymbolGroup = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
margin-left: 1rem;
`

const TokenFullName = styled.div`
color: ${({ theme }) => theme.chaliceGray};
`

const TokenRowBalance = styled.div`
font-size: 1rem;
line-height: 20px;
`

const TokenRowUsd = styled.div`
font-size: 1rem;
line-height: 1.5rem;
color: ${({ theme }) => theme.chaliceGray};
`

const TokenRowRight = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
align-items: flex-end;
`

const StyledTokenName = styled.span`
margin: 0 0.25rem 0 0.25rem;
`

const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
color: ${({ theme }) => theme.chaliceGray};
opacity: 0.6;
`

export default function CurrencyInputPanel({
onValueChange = () => {},
allBalances,
renderInput,
onCurrencySelected = () => {},
title,
Expand Down Expand Up @@ -236,7 +298,6 @@ export default function CurrencyInputPanel({
selectedTokenExchangeAddress,
ethers.constants.MaxUint256
)

tokenContract
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
Expand Down Expand Up @@ -337,48 +398,106 @@ export default function CurrencyInputPanel({
{!disableTokenSelect && (
<CurrencySelectModal
isOpen={modalIsOpen}
// isOpen={true}
onDismiss={() => {
setModalIsOpen(false)
}}
onTokenSelect={onCurrencySelected}
allBalances={allBalances}
/>
)}
</InputPanel>
)
}

function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, allBalances }) {
const { t } = useTranslation()

const [searchQuery, setSearchQuery] = useState('')
const { exchangeAddress } = useTokenDetails(searchQuery)

const allTokens = useAllTokenDetails()

// BigNumber.js instance
const ethPrice = useUSDPrice()

const _usdAmounts = Object.keys(allTokens).map(k => {
if (
ethPrice &&
allBalances &&
allBalances[k] &&
allBalances[k].ethRate &&
!allBalances[k].ethRate.isNaN() &&
allBalances[k].balance
) {
const USDRate = ethPrice.times(allBalances[k].ethRate)
const balanceBigNumber = new BigNumber(allBalances[k].balance.toString())
const usdBalance = balanceBigNumber.times(USDRate).div(new BigNumber(10).pow(allTokens[k].decimals))
return usdBalance
} else {
return null
}
})
const usdAmounts =
_usdAmounts &&
Object.keys(allTokens).reduce(
(accumulator, currentValue, i) => Object.assign({ [currentValue]: _usdAmounts[i] }, accumulator),
{}
)

const tokenList = useMemo(() => {
return Object.keys(allTokens)
.sort((a, b) => {
const aSymbol = allTokens[a].symbol.toLowerCase()
const bSymbol = allTokens[b].symbol.toLowerCase()

if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1
} else {
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
}

if (usdAmounts[a] && !usdAmounts[b]) {
return -1
} else if (usdAmounts[b] && !usdAmounts[a]) {
return 1
}

// check for balance - sort by value
if (usdAmounts[a] && usdAmounts[b]) {
const aUSD = usdAmounts[a]
const bUSD = usdAmounts[b]

return aUSD.gt(bUSD) ? -1 : aUSD.lt(bUSD) ? 1 : 0
}

return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
})
.map(k => {
let balance
let usdBalance
// only update if we have data
if (k === 'ETH' && allBalances && allBalances[k]) {
balance = formatEthBalance(allBalances[k].balance)
usdBalance = usdAmounts[k]
} else if (allBalances && allBalances[k]) {
balance = formatTokenBalance(allBalances[k].balance, allTokens[k].decimals)
usdBalance = usdAmounts[k]
}
return {
name: allTokens[k].name,
symbol: allTokens[k].symbol,
address: k
address: k,
balance: balance,
usdBalance: usdBalance
}
})
}, [allTokens])
}, [allBalances, allTokens, usdAmounts])

const filteredTokenList = useMemo(() => {
return tokenList.filter(tokenEntry => {
// check the regex for each field
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
return (
tokenEntry[tokenEntryKey] &&
typeof tokenEntry[tokenEntryKey] === 'string' &&
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
)
})
Expand All @@ -397,7 +516,6 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
if (isAddress(searchQuery) && exchangeAddress === undefined) {
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
}

if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
return (
<>
Expand All @@ -408,16 +526,30 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
</>
)
}

if (!filteredTokenList.length) {
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
}

return filteredTokenList.map(({ address, symbol }) => {
return filteredTokenList.map(({ address, symbol, name, balance, usdBalance }) => {
return (
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
<TokenLogo address={address} />
<span id="symbol">{symbol}</span>
<TokenRowLeft>
<TokenLogo address={address} size={'2rem'} />
<TokenSymbolGroup>
<span id="symbol">{symbol}</span>
<TokenFullName>{name}</TokenFullName>
</TokenSymbolGroup>
</TokenRowLeft>
<TokenRowRight>
{balance ? (
<TokenRowBalance>{balance && (balance > 0 || balance === '<0.0001') ? balance : '-'}</TokenRowBalance>
) : (
<SpinnerWrapper src={Circle} alt="loader" />
)}
<TokenRowUsd>
{usdBalance ? (usdBalance.lt(0.01) ? '<$0.01' : '$' + formatToUsd(usdBalance)) : ''}
</TokenRowUsd>
</TokenRowRight>
</TokenModalRow>
)
})
Expand All @@ -432,12 +564,33 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
setSearchQuery(checksummedInput || input)
}

function clearInputAndDismiss() {
setSearchQuery('')
onDismiss()
}

return (
<Modal isOpen={isOpen} onDismiss={onDismiss} minHeight={50} initialFocusRef={isMobile ? undefined : inputRef}>
<Modal
isOpen={isOpen}
onDismiss={clearInputAndDismiss}
minHeight={60}
initialFocusRef={isMobile ? undefined : inputRef}
>
<TokenModal>
<ModalHeader>
<p>Select Token</p>
<CloseIcon onClick={clearInputAndDismiss}>
<img src={close} alt={'close icon'} />
</CloseIcon>
</ModalHeader>
<SearchContainer>
<StyledBorderlessInput ref={inputRef} type="text" placeholder={t('searchOrPaste')} onChange={onInput} />
<img src={SearchIcon} alt="search" />
<StyledBorderlessInput
ref={inputRef}
type="text"
placeholder={isMobile ? t('searchOrPasteMobile') : t('searchOrPaste')}
onChange={onInput}
/>
</SearchContainer>
<TokenList>{renderTokenList()}</TokenList>
</TokenModal>
Expand Down
Loading

0 comments on commit 677537c

Please sign in to comment.