Skip to content

Commit 677537c

Browse files
ianlaphamNoahZinsmeister
authored andcommitted
Updates to Token Modal (Uniswap#399)
1 parent be2012c commit 677537c

File tree

25 files changed

+4180
-954
lines changed

25 files changed

+4180
-954
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ yarn-debug.log*
2424
yarn-error.log*
2525

2626
notes.txt
27-
.idea/
27+
.idea/
28+
29+
.vscode/

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
"dependencies": {
88
"@reach/dialog": "^0.2.8",
99
"@reach/tooltip": "^0.2.0",
10+
"@uniswap/sdk": "^1.0.0-beta.4",
1011
"copy-to-clipboard": "^3.2.0",
1112
"escape-string-regexp": "^2.0.0",
12-
"ethers": "^4.0.28",
13+
"ethers": "^4.0.33",
1314
"i18next": "^15.0.9",
1415
"i18next-browser-languagedetector": "^3.0.1",
1516
"i18next-xhr-backend": "^2.0.1",

public/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"unlock": "Unlock",
1919
"pending": "Pending",
2020
"selectToken": "Select a token",
21-
"searchOrPaste": "Search Token or Paste Address",
21+
"searchOrPaste": "Search Token Name, Symbol, or Address",
22+
"searchOrPasteMobile": "Name, Symbol, or Address",
2223
"noExchange": "No Exchange Found",
2324
"exchangeRate": "Exchange Rate",
2425
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
@@ -36,6 +37,7 @@
3637
"youWillReceive": "You will receive at least",
3738
"youAreBuying": "You are buying",
3839
"itWillCost": "It will cost at most",
40+
"forAtMost": "for at most",
3941
"insufficientBalance": "Insufficient Balance",
4042
"inputNotValid": "Not a valid input value",
4143
"differentToken": "Must be different token.",

src/assets/images/circle-grey.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/images/x.svg

Lines changed: 1 addition & 0 deletions
Loading

src/components/CurrencyInputPanel/index.js

Lines changed: 175 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useRef, useMemo } from 'react'
22
import { Link } from 'react-router-dom'
33
import { useTranslation } from 'react-i18next'
44
import { ethers } from 'ethers'
5+
import { BigNumber } from '@uniswap/sdk'
56
import styled from 'styled-components'
67
import escapeStringRegex from 'escape-string-regexp'
78
import { darken } from 'polished'
@@ -11,14 +12,18 @@ import { isMobile } from 'react-device-detect'
1112

1213
import { BorderlessInput } from '../../theme'
1314
import { useTokenContract } from '../../hooks'
14-
import { isAddress, calculateGasMargin } from '../../utils'
15+
import { isAddress, calculateGasMargin, formatToUsd, formatTokenBalance, formatEthBalance } from '../../utils'
1516
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
1617
import Modal from '../Modal'
1718
import TokenLogo from '../TokenLogo'
1819
import SearchIcon from '../../assets/images/magnifying-glass.svg'
1920
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
2021
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
22+
import close from '../../assets/images/x.svg'
2123
import { transparentize } from 'polished'
24+
import { Spinner } from '../../theme'
25+
import Circle from '../../assets/images/circle-grey.svg'
26+
import { useUSDPrice } from '../../contexts/Application'
2227

2328
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
2429

@@ -35,7 +40,6 @@ const SubCurrencySelect = styled.button`
3540
outline: none;
3641
cursor: pointer;
3742
user-select: none;
38-
3943
`
4044

4145
const InputRow = styled.div`
@@ -52,8 +56,11 @@ const Input = styled(BorderlessInput)`
5256
`
5357

5458
const StyledBorderlessInput = styled(BorderlessInput)`
55-
min-height: 1.75rem;
59+
min-height: 2.5rem;
5660
flex-shrink: 0;
61+
text-align: left;
62+
padding-left: 1.6rem;
63+
background-color: ${({ theme }) => theme.concreteGray};
5764
`
5865

5966
const CurrencySelect = styled.button`
@@ -152,10 +159,27 @@ const TokenModal = styled.div`
152159
width: 100%;
153160
`
154161

162+
const ModalHeader = styled.div`
163+
position: relative;
164+
display: flex;
165+
flex-direction: row;
166+
align-items: center;
167+
padding: 0 2rem;
168+
height: 60px;
169+
`
170+
171+
const CloseIcon = styled.div`
172+
position: absolute;
173+
right: 1.4rem;
174+
&:hover {
175+
cursor: pointer;
176+
}
177+
`
178+
155179
const SearchContainer = styled.div`
156180
${({ theme }) => theme.flexRowNoWrap}
157-
padding: 1rem;
158-
border-bottom: 1px solid ${({ theme }) => theme.mercuryGray};
181+
padding: 0.5rem 2rem;
182+
background-color: ${({ theme }) => theme.concreteGray};
159183
`
160184

161185
const TokenModalInfo = styled.div`
@@ -177,9 +201,8 @@ const TokenList = styled.div`
177201
const TokenModalRow = styled.div`
178202
${({ theme }) => theme.flexRowNoWrap}
179203
align-items: center;
180-
padding: 1rem 1.5rem;
181-
margin: 0.25rem 0.5rem;
182204
justify-content: space-between;
205+
padding: 0.8rem 2rem;
183206
cursor: pointer;
184207
user-select: none;
185208
@@ -188,16 +211,55 @@ const TokenModalRow = styled.div`
188211
}
189212
190213
:hover {
191-
background-color: ${({ theme }) => theme.concreteGray};
214+
background-color: ${({ theme }) => theme.tokenRowHover};
192215
}
216+
217+
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0.8rem 1rem;`}
218+
`
219+
220+
const TokenRowLeft = styled.div`
221+
${({ theme }) => theme.flexRowNoWrap}
222+
align-items : center;
223+
`
224+
225+
const TokenSymbolGroup = styled.div`
226+
${({ theme }) => theme.flexColumnNoWrap};
227+
margin-left: 1rem;
228+
`
229+
230+
const TokenFullName = styled.div`
231+
color: ${({ theme }) => theme.chaliceGray};
232+
`
233+
234+
const TokenRowBalance = styled.div`
235+
font-size: 1rem;
236+
line-height: 20px;
237+
`
238+
239+
const TokenRowUsd = styled.div`
240+
font-size: 1rem;
241+
line-height: 1.5rem;
242+
color: ${({ theme }) => theme.chaliceGray};
243+
`
244+
245+
const TokenRowRight = styled.div`
246+
${({ theme }) => theme.flexColumnNoWrap};
247+
align-items: flex-end;
193248
`
194249

195250
const StyledTokenName = styled.span`
196251
margin: 0 0.25rem 0 0.25rem;
197252
`
198253

254+
const SpinnerWrapper = styled(Spinner)`
255+
margin: 0 0.25rem 0 0.25rem;
256+
color: ${({ theme }) => theme.chaliceGray};
257+
opacity: 0.6;
258+
`
259+
199260
export default function CurrencyInputPanel({
200261
onValueChange = () => {},
262+
allBalances,
201263
renderInput,
202264
onCurrencySelected = () => {},
203265
title,
@@ -236,7 +298,6 @@ export default function CurrencyInputPanel({
236298
selectedTokenExchangeAddress,
237299
ethers.constants.MaxUint256
238300
)
239-
240301
tokenContract
241302
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, {
242303
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
@@ -337,48 +398,106 @@ export default function CurrencyInputPanel({
337398
{!disableTokenSelect && (
338399
<CurrencySelectModal
339400
isOpen={modalIsOpen}
401+
// isOpen={true}
340402
onDismiss={() => {
341403
setModalIsOpen(false)
342404
}}
343405
onTokenSelect={onCurrencySelected}
406+
allBalances={allBalances}
344407
/>
345408
)}
346409
</InputPanel>
347410
)
348411
}
349412

350-
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
413+
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, allBalances }) {
351414
const { t } = useTranslation()
352415

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

356419
const allTokens = useAllTokenDetails()
420+
421+
// BigNumber.js instance
422+
const ethPrice = useUSDPrice()
423+
424+
const _usdAmounts = Object.keys(allTokens).map(k => {
425+
if (
426+
ethPrice &&
427+
allBalances &&
428+
allBalances[k] &&
429+
allBalances[k].ethRate &&
430+
!allBalances[k].ethRate.isNaN() &&
431+
allBalances[k].balance
432+
) {
433+
const USDRate = ethPrice.times(allBalances[k].ethRate)
434+
const balanceBigNumber = new BigNumber(allBalances[k].balance.toString())
435+
const usdBalance = balanceBigNumber.times(USDRate).div(new BigNumber(10).pow(allTokens[k].decimals))
436+
return usdBalance
437+
} else {
438+
return null
439+
}
440+
})
441+
const usdAmounts =
442+
_usdAmounts &&
443+
Object.keys(allTokens).reduce(
444+
(accumulator, currentValue, i) => Object.assign({ [currentValue]: _usdAmounts[i] }, accumulator),
445+
{}
446+
)
447+
357448
const tokenList = useMemo(() => {
358449
return Object.keys(allTokens)
359450
.sort((a, b) => {
360451
const aSymbol = allTokens[a].symbol.toLowerCase()
361452
const bSymbol = allTokens[b].symbol.toLowerCase()
453+
362454
if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
363455
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1
364-
} else {
365-
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
366456
}
457+
458+
if (usdAmounts[a] && !usdAmounts[b]) {
459+
return -1
460+
} else if (usdAmounts[b] && !usdAmounts[a]) {
461+
return 1
462+
}
463+
464+
// check for balance - sort by value
465+
if (usdAmounts[a] && usdAmounts[b]) {
466+
const aUSD = usdAmounts[a]
467+
const bUSD = usdAmounts[b]
468+
469+
return aUSD.gt(bUSD) ? -1 : aUSD.lt(bUSD) ? 1 : 0
470+
}
471+
472+
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
367473
})
368474
.map(k => {
475+
let balance
476+
let usdBalance
477+
// only update if we have data
478+
if (k === 'ETH' && allBalances && allBalances[k]) {
479+
balance = formatEthBalance(allBalances[k].balance)
480+
usdBalance = usdAmounts[k]
481+
} else if (allBalances && allBalances[k]) {
482+
balance = formatTokenBalance(allBalances[k].balance, allTokens[k].decimals)
483+
usdBalance = usdAmounts[k]
484+
}
369485
return {
370486
name: allTokens[k].name,
371487
symbol: allTokens[k].symbol,
372-
address: k
488+
address: k,
489+
balance: balance,
490+
usdBalance: usdBalance
373491
}
374492
})
375-
}, [allTokens])
493+
}, [allBalances, allTokens, usdAmounts])
494+
376495
const filteredTokenList = useMemo(() => {
377496
return tokenList.filter(tokenEntry => {
378497
// check the regex for each field
379498
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
380499
return (
381-
tokenEntry[tokenEntryKey] &&
500+
typeof tokenEntry[tokenEntryKey] === 'string' &&
382501
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
383502
)
384503
})
@@ -397,7 +516,6 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
397516
if (isAddress(searchQuery) && exchangeAddress === undefined) {
398517
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
399518
}
400-
401519
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
402520
return (
403521
<>
@@ -408,16 +526,30 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
408526
</>
409527
)
410528
}
411-
412529
if (!filteredTokenList.length) {
413530
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
414531
}
415532

416-
return filteredTokenList.map(({ address, symbol }) => {
533+
return filteredTokenList.map(({ address, symbol, name, balance, usdBalance }) => {
417534
return (
418535
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
419-
<TokenLogo address={address} />
420-
<span id="symbol">{symbol}</span>
536+
<TokenRowLeft>
537+
<TokenLogo address={address} size={'2rem'} />
538+
<TokenSymbolGroup>
539+
<span id="symbol">{symbol}</span>
540+
<TokenFullName>{name}</TokenFullName>
541+
</TokenSymbolGroup>
542+
</TokenRowLeft>
543+
<TokenRowRight>
544+
{balance ? (
545+
<TokenRowBalance>{balance && (balance > 0 || balance === '<0.0001') ? balance : '-'}</TokenRowBalance>
546+
) : (
547+
<SpinnerWrapper src={Circle} alt="loader" />
548+
)}
549+
<TokenRowUsd>
550+
{usdBalance ? (usdBalance.lt(0.01) ? '<$0.01' : '$' + formatToUsd(usdBalance)) : ''}
551+
</TokenRowUsd>
552+
</TokenRowRight>
421553
</TokenModalRow>
422554
)
423555
})
@@ -432,12 +564,33 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
432564
setSearchQuery(checksummedInput || input)
433565
}
434566

567+
function clearInputAndDismiss() {
568+
setSearchQuery('')
569+
onDismiss()
570+
}
571+
435572
return (
436-
<Modal isOpen={isOpen} onDismiss={onDismiss} minHeight={50} initialFocusRef={isMobile ? undefined : inputRef}>
573+
<Modal
574+
isOpen={isOpen}
575+
onDismiss={clearInputAndDismiss}
576+
minHeight={60}
577+
initialFocusRef={isMobile ? undefined : inputRef}
578+
>
437579
<TokenModal>
580+
<ModalHeader>
581+
<p>Select Token</p>
582+
<CloseIcon onClick={clearInputAndDismiss}>
583+
<img src={close} alt={'close icon'} />
584+
</CloseIcon>
585+
</ModalHeader>
438586
<SearchContainer>
439-
<StyledBorderlessInput ref={inputRef} type="text" placeholder={t('searchOrPaste')} onChange={onInput} />
440587
<img src={SearchIcon} alt="search" />
588+
<StyledBorderlessInput
589+
ref={inputRef}
590+
type="text"
591+
placeholder={isMobile ? t('searchOrPasteMobile') : t('searchOrPaste')}
592+
onChange={onInput}
593+
/>
441594
</SearchContainer>
442595
<TokenList>{renderTokenList()}</TokenList>
443596
</TokenModal>

0 commit comments

Comments
 (0)