@@ -2,6 +2,7 @@ import React, { useState, useRef, useMemo } from 'react'
2
2
import { Link } from 'react-router-dom'
3
3
import { useTranslation } from 'react-i18next'
4
4
import { ethers } from 'ethers'
5
+ import { BigNumber } from '@uniswap/sdk'
5
6
import styled from 'styled-components'
6
7
import escapeStringRegex from 'escape-string-regexp'
7
8
import { darken } from 'polished'
@@ -11,14 +12,18 @@ import { isMobile } from 'react-device-detect'
11
12
12
13
import { BorderlessInput } from '../../theme'
13
14
import { useTokenContract } from '../../hooks'
14
- import { isAddress , calculateGasMargin } from '../../utils'
15
+ import { isAddress , calculateGasMargin , formatToUsd , formatTokenBalance , formatEthBalance } from '../../utils'
15
16
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
16
17
import Modal from '../Modal'
17
18
import TokenLogo from '../TokenLogo'
18
19
import SearchIcon from '../../assets/images/magnifying-glass.svg'
19
20
import { useTransactionAdder , usePendingApproval } from '../../contexts/Transactions'
20
21
import { useTokenDetails , useAllTokenDetails } from '../../contexts/Tokens'
22
+ import close from '../../assets/images/x.svg'
21
23
import { transparentize } from 'polished'
24
+ import { Spinner } from '../../theme'
25
+ import Circle from '../../assets/images/circle-grey.svg'
26
+ import { useUSDPrice } from '../../contexts/Application'
22
27
23
28
const GAS_MARGIN = ethers . utils . bigNumberify ( 1000 )
24
29
@@ -35,7 +40,6 @@ const SubCurrencySelect = styled.button`
35
40
outline: none;
36
41
cursor: pointer;
37
42
user-select: none;
38
-
39
43
`
40
44
41
45
const InputRow = styled . div `
@@ -52,8 +56,11 @@ const Input = styled(BorderlessInput)`
52
56
`
53
57
54
58
const StyledBorderlessInput = styled ( BorderlessInput ) `
55
- min-height: 1.75rem ;
59
+ min-height: 2.5rem ;
56
60
flex-shrink: 0;
61
+ text-align: left;
62
+ padding-left: 1.6rem;
63
+ background-color: ${ ( { theme } ) => theme . concreteGray } ;
57
64
`
58
65
59
66
const CurrencySelect = styled . button `
@@ -152,10 +159,27 @@ const TokenModal = styled.div`
152
159
width: 100%;
153
160
`
154
161
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
+
155
179
const SearchContainer = styled . div `
156
180
${ ( { 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 } ;
159
183
`
160
184
161
185
const TokenModalInfo = styled . div `
@@ -177,9 +201,8 @@ const TokenList = styled.div`
177
201
const TokenModalRow = styled . div `
178
202
${ ( { theme } ) => theme . flexRowNoWrap }
179
203
align-items: center;
180
- padding: 1rem 1.5rem;
181
- margin: 0.25rem 0.5rem;
182
204
justify-content: space-between;
205
+ padding: 0.8rem 2rem;
183
206
cursor: pointer;
184
207
user-select: none;
185
208
@@ -188,16 +211,55 @@ const TokenModalRow = styled.div`
188
211
}
189
212
190
213
:hover {
191
- background-color: ${ ( { theme } ) => theme . concreteGray } ;
214
+ background-color: ${ ( { theme } ) => theme . tokenRowHover } ;
192
215
}
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;
193
248
`
194
249
195
250
const StyledTokenName = styled . span `
196
251
margin: 0 0.25rem 0 0.25rem;
197
252
`
198
253
254
+ const SpinnerWrapper = styled ( Spinner ) `
255
+ margin: 0 0.25rem 0 0.25rem;
256
+ color: ${ ( { theme } ) => theme . chaliceGray } ;
257
+ opacity: 0.6;
258
+ `
259
+
199
260
export default function CurrencyInputPanel ( {
200
261
onValueChange = ( ) => { } ,
262
+ allBalances,
201
263
renderInput,
202
264
onCurrencySelected = ( ) => { } ,
203
265
title,
@@ -236,7 +298,6 @@ export default function CurrencyInputPanel({
236
298
selectedTokenExchangeAddress ,
237
299
ethers . constants . MaxUint256
238
300
)
239
-
240
301
tokenContract
241
302
. approve ( selectedTokenExchangeAddress , ethers . constants . MaxUint256 , {
242
303
gasLimit : calculateGasMargin ( estimatedGas , GAS_MARGIN )
@@ -337,48 +398,106 @@ export default function CurrencyInputPanel({
337
398
{ ! disableTokenSelect && (
338
399
< CurrencySelectModal
339
400
isOpen = { modalIsOpen }
401
+ // isOpen={true}
340
402
onDismiss = { ( ) => {
341
403
setModalIsOpen ( false )
342
404
} }
343
405
onTokenSelect = { onCurrencySelected }
406
+ allBalances = { allBalances }
344
407
/>
345
408
) }
346
409
</ InputPanel >
347
410
)
348
411
}
349
412
350
- function CurrencySelectModal ( { isOpen, onDismiss, onTokenSelect } ) {
413
+ function CurrencySelectModal ( { isOpen, onDismiss, onTokenSelect, allBalances } ) {
351
414
const { t } = useTranslation ( )
352
415
353
416
const [ searchQuery , setSearchQuery ] = useState ( '' )
354
417
const { exchangeAddress } = useTokenDetails ( searchQuery )
355
418
356
419
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
+
357
448
const tokenList = useMemo ( ( ) => {
358
449
return Object . keys ( allTokens )
359
450
. sort ( ( a , b ) => {
360
451
const aSymbol = allTokens [ a ] . symbol . toLowerCase ( )
361
452
const bSymbol = allTokens [ b ] . symbol . toLowerCase ( )
453
+
362
454
if ( aSymbol === 'ETH' . toLowerCase ( ) || bSymbol === 'ETH' . toLowerCase ( ) ) {
363
455
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH' . toLowerCase ( ) ? - 1 : 1
364
- } else {
365
- return aSymbol < bSymbol ? - 1 : aSymbol > bSymbol ? 1 : 0
366
456
}
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
367
473
} )
368
474
. 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
+ }
369
485
return {
370
486
name : allTokens [ k ] . name ,
371
487
symbol : allTokens [ k ] . symbol ,
372
- address : k
488
+ address : k ,
489
+ balance : balance ,
490
+ usdBalance : usdBalance
373
491
}
374
492
} )
375
- } , [ allTokens ] )
493
+ } , [ allBalances , allTokens , usdAmounts ] )
494
+
376
495
const filteredTokenList = useMemo ( ( ) => {
377
496
return tokenList . filter ( tokenEntry => {
378
497
// check the regex for each field
379
498
const regexMatches = Object . keys ( tokenEntry ) . map ( tokenEntryKey => {
380
499
return (
381
- tokenEntry [ tokenEntryKey ] &&
500
+ typeof tokenEntry [ tokenEntryKey ] === 'string' &&
382
501
! ! tokenEntry [ tokenEntryKey ] . match ( new RegExp ( escapeStringRegex ( searchQuery ) , 'i' ) )
383
502
)
384
503
} )
@@ -397,7 +516,6 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
397
516
if ( isAddress ( searchQuery ) && exchangeAddress === undefined ) {
398
517
return < TokenModalInfo > Searching for Exchange...</ TokenModalInfo >
399
518
}
400
-
401
519
if ( isAddress ( searchQuery ) && exchangeAddress === ethers . constants . AddressZero ) {
402
520
return (
403
521
< >
@@ -408,16 +526,30 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
408
526
</ >
409
527
)
410
528
}
411
-
412
529
if ( ! filteredTokenList . length ) {
413
530
return < TokenModalInfo > { t ( 'noExchange' ) } </ TokenModalInfo >
414
531
}
415
532
416
- return filteredTokenList . map ( ( { address, symbol } ) => {
533
+ return filteredTokenList . map ( ( { address, symbol, name , balance , usdBalance } ) => {
417
534
return (
418
535
< 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 >
421
553
</ TokenModalRow >
422
554
)
423
555
} )
@@ -432,12 +564,33 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
432
564
setSearchQuery ( checksummedInput || input )
433
565
}
434
566
567
+ function clearInputAndDismiss ( ) {
568
+ setSearchQuery ( '' )
569
+ onDismiss ( )
570
+ }
571
+
435
572
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
+ >
437
579
< TokenModal >
580
+ < ModalHeader >
581
+ < p > Select Token</ p >
582
+ < CloseIcon onClick = { clearInputAndDismiss } >
583
+ < img src = { close } alt = { 'close icon' } />
584
+ </ CloseIcon >
585
+ </ ModalHeader >
438
586
< SearchContainer >
439
- < StyledBorderlessInput ref = { inputRef } type = "text" placeholder = { t ( 'searchOrPaste' ) } onChange = { onInput } />
440
587
< img src = { SearchIcon } alt = "search" />
588
+ < StyledBorderlessInput
589
+ ref = { inputRef }
590
+ type = "text"
591
+ placeholder = { isMobile ? t ( 'searchOrPasteMobile' ) : t ( 'searchOrPaste' ) }
592
+ onChange = { onInput }
593
+ />
441
594
</ SearchContainer >
442
595
< TokenList > { renderTokenList ( ) } </ TokenList >
443
596
</ TokenModal >
0 commit comments