mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
Update designs for Add Token screen
This commit is contained in:
parent
713c77db54
commit
4f6b53c1aa
@ -23,6 +23,9 @@
|
|||||||
"addTokens": {
|
"addTokens": {
|
||||||
"message": "Add Tokens"
|
"message": "Add Tokens"
|
||||||
},
|
},
|
||||||
|
"addAcquiredTokens": {
|
||||||
|
"message": "Add the tokens you've acquired using MetaMask"
|
||||||
|
},
|
||||||
"amount": {
|
"amount": {
|
||||||
"message": "Amount"
|
"message": "Amount"
|
||||||
},
|
},
|
||||||
@ -53,7 +56,7 @@
|
|||||||
"message": "Back"
|
"message": "Back"
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"message": "Balance:"
|
"message": "Balance"
|
||||||
},
|
},
|
||||||
"balances": {
|
"balances": {
|
||||||
"message": "Token balance(s)"
|
"message": "Token balance(s)"
|
||||||
@ -717,6 +720,9 @@
|
|||||||
"search": {
|
"search": {
|
||||||
"message": "Search"
|
"message": "Search"
|
||||||
},
|
},
|
||||||
|
"searchResults": {
|
||||||
|
"message": "Search Results"
|
||||||
|
},
|
||||||
"secretPhrase": {
|
"secretPhrase": {
|
||||||
"message": "Enter your secret twelve word phrase here to restore your vault."
|
"message": "Enter your secret twelve word phrase here to restore your vault."
|
||||||
},
|
},
|
||||||
@ -832,6 +838,9 @@
|
|||||||
"message": "$1 to ETH via ShapeShift",
|
"message": "$1 to ETH via ShapeShift",
|
||||||
"description": "system will fill in deposit type in start of message"
|
"description": "system will fill in deposit type in start of message"
|
||||||
},
|
},
|
||||||
|
"token": {
|
||||||
|
"message": "Token"
|
||||||
|
},
|
||||||
"tokenAddress": {
|
"tokenAddress": {
|
||||||
"message": "Token Address"
|
"message": "Token Address"
|
||||||
},
|
},
|
||||||
|
14
app/images/search.svg
Normal file
14
app/images/search.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="17px" height="17px" viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>search</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Add-Tokens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Metamascara---add-from-token-list-Copy-3" transform="translate(-345.000000, -350.000000)" fill="#9B9B9B" fill-rule="nonzero">
|
||||||
|
<g id="search" transform="translate(345.000000, 350.000000)">
|
||||||
|
<path d="M2.01875,6.90625 C2.01875,4.25 4.25,2.01875 6.90625,2.01875 C9.5625,2.01875 11.6875,4.14375 11.6875,6.90625 C11.6875,9.5625 9.5625,11.6875 6.90625,11.6875 C4.14375,11.6875 2.01875,9.5625 2.01875,6.90625 Z M16.575,15.0875 L12.325,10.8375 C13.175,9.66875 13.6,8.2875 13.6,6.8 C13.70625,3.08125 10.625,0 6.90625,0 C3.08125,0 0,3.08125 0,6.90625 C0,10.73125 3.08125,13.8125 6.90625,13.8125 C8.18125,13.8125 9.45625,13.3875 10.4125,12.75 L14.6625,17 L16.575,15.0875 Z" id="Page-1"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
15
app/images/tokensearch.svg
Normal file
15
app/images/tokensearch.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="65px" height="58px" viewBox="0 0 65 58" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>7FDB75AD-BD4D-497C-B391-69EEB31A0561</title>
|
||||||
|
<desc>Created with sketchtool.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Add-Tokens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Add-tokens" transform="translate(-267.000000, -284.000000)" fill="#B8BAC1">
|
||||||
|
<g id="tokensearch" transform="translate(267.000000, 284.000000)">
|
||||||
|
<path d="M28.5322581,2.80645161 C42.4391613,2.80645161 54.1925806,9.22854839 54.2552581,16.8433871 C54.1925806,24.4591613 42.4391613,30.8821935 28.5322581,30.8821935 C14.6253548,30.8821935 2.87193548,24.4600968 2.80925806,16.8443226 C2.87193548,9.22854839 14.6253548,2.80645161 28.5322581,2.80645161 M28.5322581,36.7289677 C15.7432581,36.7289677 4.78125806,31.2975484 3.05154839,24.5012581 C7.70932258,29.9981613 17.2559355,33.6886452 28.5322581,33.6886452 C39.8085806,33.6886452 49.3551935,29.9981613 54.0129677,24.5012581 C52.2832581,31.2975484 41.3212581,36.7289677 28.5322581,36.7289677 M28.5322581,54.2692903 C15.7432581,54.2692903 4.78125806,48.837871 3.05154839,42.0415806 C7.70932258,47.5384839 17.2559355,51.2289677 28.5322581,51.2289677 C33.2237097,51.2289677 37.6083226,50.5844194 41.471871,49.4403226 C42.1379355,49.243871 42.5270968,48.5675161 42.4110968,47.8818065 C42.4045484,47.8453226 42.398,47.8079032 42.3923871,47.7704839 C42.2642258,46.9603548 41.439129,46.458 40.6533226,46.6946774 C37.0180323,47.792 32.8822581,48.4225161 28.5322581,48.4225161 C15.7432581,48.4225161 4.78125806,42.9910968 3.05154839,36.1948065 C7.70932258,41.6917097 17.2559355,45.3821935 28.5322581,45.3821935 C33.3649677,45.3821935 37.8730645,44.6983548 41.8217419,43.4906452 C42.2763871,43.3503226 42.6066129,42.976129 42.7422581,42.5196129 L42.7534839,42.4812581 C43.0752903,41.4082581 42.0733871,40.4063548 41.004129,40.7403226 C37.2846452,41.9040645 33.0225806,42.5757419 28.5322581,42.5757419 C15.7432581,42.5757419 4.78125806,37.1443226 3.05154839,30.3480323 C7.70932258,35.8449355 17.2559355,39.5354194 28.5322581,39.5354194 C39.8085806,39.5354194 49.3551935,35.8449355 54.0129677,30.3480323 L54.0129677,33.5492581 C54.0129677,34.3846452 54.6902581,35.0619355 55.5256452,35.0619355 C56.3610323,35.0619355 57.0383226,34.3902581 57.0392581,33.5558065 C57.0467419,26.4900968 57.0645161,16.9257097 57.0645161,16.905129 C57.0645161,16.8845484 57.0617097,16.8649032 57.0617097,16.8443226 C57.0617097,16.8237419 57.0645161,16.8031613 57.0645161,16.7825806 L57.0598387,16.7825806 C56.9513226,7.36225806 44.4616774,0 28.5322581,0 C12.6028387,0 0.113193548,7.36225806 0.00467741935,16.7825806 L0,16.7825806 C0,16.8031613 0.00280645161,16.8237419 0.00280645161,16.8443226 C0.00280645161,16.8649032 0,16.8845484 0,16.905129 C0,16.9322581 0.00467741935,19.3420645 0.0102903226,22.6293548 L0,22.6293548 C0,22.7154194 0.00841935484,22.7996129 0.0102903226,22.8838065 C0.0140322581,24.5957419 0.0177741935,26.5247097 0.0196451613,28.476129 L0,28.476129 C0,28.650129 0.0130967742,28.8222581 0.0205806452,28.9953226 C0.0243225806,30.828871 0.0280645161,32.6586774 0.0308709677,34.3229032 L0,34.3229032 C0,34.5857742 0.0140322581,34.8467742 0.0318064516,35.1059032 C0.036483871,37.3108387 0.0392903226,39.1406452 0.0411612903,40.1696774 L0,40.1696774 C0,40.4905484 0.0177741935,40.8086129 0.0458387097,41.123871 L0.0495806452,41.2033871 C0.0645483871,41.3699032 0.0935483871,41.5345484 0.116935484,41.700129 C0.130032258,41.7861935 0.137516129,41.8731935 0.152483871,41.9583226 C0.183354839,42.1416774 0.225451613,42.3240968 0.266612903,42.5055806 C0.29,42.6103548 0.308709677,42.7160645 0.334903226,42.8199032 C0.358290323,42.9078387 0.387290323,42.9939032 0.411612903,43.0818387 C2.00006452,48.7134516 8.12841935,53.3160323 16.5777097,55.5705484 C16.6010968,55.5770968 16.6254194,55.5836452 16.6488065,55.5892581 C16.9350645,55.6650323 17.2213226,55.739871 17.5122581,55.8109677 C20.9099355,56.6538387 24.6322258,57.1215806 28.5322581,57.1215806 C32.4322903,57.1215806 36.1545806,56.6538387 39.5522581,55.8109677 C39.8431935,55.739871 40.1294516,55.6650323 40.4157097,55.5892581 C40.4390968,55.5836452 40.4634194,55.5770968 40.4868065,55.5705484 C41.5766452,55.2796129 42.6253226,54.9475161 43.6319032,54.579871 C44.4682258,54.2739677 44.7675806,53.2627097 44.2652258,52.5274194 C44.2437097,52.4956129 44.2212581,52.462871 44.1997419,52.430129 C43.8423871,51.8950323 43.1688387,51.6873548 42.5645161,51.9090645 C38.4998387,53.3955484 33.6624516,54.2692903 28.5322581,54.2692903" id="Fill-1"></path>
|
||||||
|
<path d="M64.3227484,54.3991355 L60.4535871,50.5299742 C61.4526839,49.1566839 61.9522323,47.5345548 61.9522323,45.7880065 C62.1009742,40.5661355 56.8996839,36.4144581 51.4654581,38.2367806 C48.6131677,39.1928452 46.4821355,41.7401677 46.0611677,44.7187484 C45.3530065,49.7460387 49.205329,54.0249419 54.0894903,54.0249419 C55.5872,54.0249419 57.0849097,53.5244581 58.2074903,52.7770065 L62.0766516,56.6452323 C62.6968774,57.2654581 63.7025226,57.2654581 64.3227484,56.6452323 C64.9429742,56.0250065 64.9429742,55.0193613 64.3227484,54.3991355 M48.3484258,45.9124258 C48.3484258,42.7925871 50.9696516,40.1713613 54.0894903,40.1713613 C57.209329,40.1713613 59.7052,42.6681677 59.7052,45.9124258 C59.7052,49.0332 57.209329,51.529071 54.0894903,51.529071 C50.8452323,51.529071 48.3484258,49.0332 48.3484258,45.9124258" id="Fill-3"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.5 KiB |
161
package-lock.json
generated
161
package-lock.json
generated
@ -67,6 +67,15 @@
|
|||||||
"@babel/types": "7.0.0-beta.31"
|
"@babel/types": "7.0.0-beta.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.0.0-beta.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.47.tgz",
|
||||||
|
"integrity": "sha512-3IaakAC5B4bHJ0aCUKVw0pt+GruavdgWDFbf7TfKh7ZJ8yQuUp7af7MNwf3e+jH8776cjqYmMO1JNDDAE9WfrA==",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "2.5.3",
|
||||||
|
"regenerator-runtime": "0.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/template": {
|
"@babel/template": {
|
||||||
"version": "7.0.0-beta.31",
|
"version": "7.0.0-beta.31",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz",
|
||||||
@ -182,6 +191,74 @@
|
|||||||
"through2": "2.0.3"
|
"through2": "2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@material-ui/core": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BTLp4goHFKGqCVSjSWNSUZp3/fvN36L0B73Z68i4Hs6TRZaApW5M2JyKmWTsCf/hk4PNKTnZMh141qNQFhxzAw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "7.0.0-beta.47",
|
||||||
|
"@types/jss": "9.5.3",
|
||||||
|
"@types/react-transition-group": "2.0.9",
|
||||||
|
"brcast": "3.0.1",
|
||||||
|
"classnames": "2.2.5",
|
||||||
|
"deepmerge": "2.1.0",
|
||||||
|
"dom-helpers": "3.3.1",
|
||||||
|
"hoist-non-react-statics": "2.5.0",
|
||||||
|
"jss": "9.8.1",
|
||||||
|
"jss-camel-case": "6.1.0",
|
||||||
|
"jss-default-unit": "8.0.2",
|
||||||
|
"jss-global": "3.0.0",
|
||||||
|
"jss-nested": "6.0.1",
|
||||||
|
"jss-props-sort": "6.0.0",
|
||||||
|
"jss-vendor-prefixer": "7.0.0",
|
||||||
|
"keycode": "2.2.0",
|
||||||
|
"lodash": "4.17.10",
|
||||||
|
"normalize-scroll-left": "0.1.2",
|
||||||
|
"prop-types": "15.6.1",
|
||||||
|
"react-event-listener": "0.5.3",
|
||||||
|
"react-jss": "8.4.0",
|
||||||
|
"react-popper": "0.10.4",
|
||||||
|
"react-scrollbar-size": "2.1.0",
|
||||||
|
"react-transition-group": "2.2.1",
|
||||||
|
"recompose": "0.27.0",
|
||||||
|
"scroll": "2.0.3",
|
||||||
|
"warning": "3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jss": {
|
||||||
|
"version": "9.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jss/-/jss-9.5.3.tgz",
|
||||||
|
"integrity": "sha512-RQWhcpOVyIhGryKpnUyZARwsgmp+tB82O7c75lC4Tjbmr3hPiCnM1wc+pJipVEOsikYXW0IHgeiQzmxQXbnAIA==",
|
||||||
|
"requires": {
|
||||||
|
"csstype": "2.4.2",
|
||||||
|
"indefinite-observable": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Q89Z26KAfA3lpPGhbF6XMfYAm3jIV3avViy6KOJ2JLzFbeWHOvPQUu5aSJIWXap3gDZC2y1eF5HXEPI2wGqgvw=="
|
||||||
|
},
|
||||||
|
"hoist-non-react-statics": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
|
||||||
|
},
|
||||||
|
"recompose": {
|
||||||
|
"version": "0.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/recompose/-/recompose-0.27.0.tgz",
|
||||||
|
"integrity": "sha512-hivr1EopLhzjchhv2Y7VcLA2H5NGztwV/qfYqmIAhTkNowNQ9PyXdfq9Q8QCa0TMrPM1NtStlUyi5I/p8XfUNQ==",
|
||||||
|
"requires": {
|
||||||
|
"babel-runtime": "6.26.0",
|
||||||
|
"change-emitter": "0.1.6",
|
||||||
|
"fbjs": "0.8.16",
|
||||||
|
"hoist-non-react-statics": "2.5.0",
|
||||||
|
"react-lifecycles-compat": "3.0.2",
|
||||||
|
"symbol-observable": "1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@sentry/cli": {
|
"@sentry/cli": {
|
||||||
"version": "1.30.3",
|
"version": "1.30.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.30.3.tgz",
|
||||||
@ -1365,15 +1442,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jss": {
|
|
||||||
"version": "9.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jss/-/jss-9.5.2.tgz",
|
|
||||||
"integrity": "sha512-EX87yNYcisXO5BU9tT7stB7OGuDJyV3JwtMwhfUprrmHwYKWh9a3vchAy6DYzUSbmTA7bD46h8qata5jP1V7Zw==",
|
|
||||||
"requires": {
|
|
||||||
"csstype": "2.4.2",
|
|
||||||
"indefinite-observable": "1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "8.5.5",
|
"version": "8.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.5.tgz",
|
||||||
@ -17752,78 +17820,6 @@
|
|||||||
"integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=",
|
"integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"material-ui": {
|
|
||||||
"version": "1.0.0-beta.44",
|
|
||||||
"resolved": "https://registry.npmjs.org/material-ui/-/material-ui-1.0.0-beta.44.tgz",
|
|
||||||
"integrity": "sha512-m5SJxvDz77bVKcjyZG/AyG6RBR+UUwkPgvHHLJa2jyAHBNtJMCQ5GVouTXOxaUKlvD5cbO/mcH0YtzugyQTAVg==",
|
|
||||||
"requires": {
|
|
||||||
"@types/jss": "9.5.2",
|
|
||||||
"@types/react-transition-group": "2.0.9",
|
|
||||||
"babel-runtime": "6.26.0",
|
|
||||||
"brcast": "3.0.1",
|
|
||||||
"classnames": "2.2.5",
|
|
||||||
"deepmerge": "2.1.0",
|
|
||||||
"dom-helpers": "3.3.1",
|
|
||||||
"hoist-non-react-statics": "2.5.0",
|
|
||||||
"jss": "9.8.1",
|
|
||||||
"jss-camel-case": "6.1.0",
|
|
||||||
"jss-default-unit": "8.0.2",
|
|
||||||
"jss-global": "3.0.0",
|
|
||||||
"jss-nested": "6.0.1",
|
|
||||||
"jss-props-sort": "6.0.0",
|
|
||||||
"jss-vendor-prefixer": "7.0.0",
|
|
||||||
"keycode": "2.2.0",
|
|
||||||
"lodash": "4.17.10",
|
|
||||||
"normalize-scroll-left": "0.1.2",
|
|
||||||
"prop-types": "15.6.1",
|
|
||||||
"react-event-listener": "0.5.3",
|
|
||||||
"react-jss": "8.4.0",
|
|
||||||
"react-lifecycles-compat": "2.0.2",
|
|
||||||
"react-popper": "0.10.4",
|
|
||||||
"react-scrollbar-size": "2.1.0",
|
|
||||||
"react-transition-group": "2.2.1",
|
|
||||||
"recompose": "0.27.0",
|
|
||||||
"scroll": "2.0.3",
|
|
||||||
"warning": "3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"deepmerge": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-Q89Z26KAfA3lpPGhbF6XMfYAm3jIV3avViy6KOJ2JLzFbeWHOvPQUu5aSJIWXap3gDZC2y1eF5HXEPI2wGqgvw=="
|
|
||||||
},
|
|
||||||
"hoist-non-react-statics": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
|
|
||||||
},
|
|
||||||
"react-lifecycles-compat": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-BPksUj7VMAAFhcCw79sZA0Ow/LTAEjs3Sio1AQcuwLeOP+ua0f/08Su2wyiW+JjDDH6fRqNy3h5CLXh21u1mVg=="
|
|
||||||
},
|
|
||||||
"recompose": {
|
|
||||||
"version": "0.27.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/recompose/-/recompose-0.27.0.tgz",
|
|
||||||
"integrity": "sha512-hivr1EopLhzjchhv2Y7VcLA2H5NGztwV/qfYqmIAhTkNowNQ9PyXdfq9Q8QCa0TMrPM1NtStlUyi5I/p8XfUNQ==",
|
|
||||||
"requires": {
|
|
||||||
"babel-runtime": "6.26.0",
|
|
||||||
"change-emitter": "0.1.6",
|
|
||||||
"fbjs": "0.8.16",
|
|
||||||
"hoist-non-react-statics": "2.5.0",
|
|
||||||
"react-lifecycles-compat": "3.0.3",
|
|
||||||
"symbol-observable": "1.1.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react-lifecycles-compat": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-bOr65SSYgxDgDNqLnDqt+gropXGPNB1Wbyys4tOYiNuP/qYWC4qFM9XH1ruzq+tT6EjE29pJsCr19rclKtpUEg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"math-expression-evaluator": {
|
"math-expression-evaluator": {
|
||||||
"version": "1.2.17",
|
"version": "1.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
||||||
@ -25056,8 +25052,7 @@
|
|||||||
"react-lifecycles-compat": {
|
"react-lifecycles-compat": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz",
|
||||||
"integrity": "sha512-pbZOSMVVkvppW7XRn9fcHK5OgEDnYLwMva7P6TgS44/SN9uGGjfh3Z1c8tomO+y4IsHQ6Fsz2EGwmE7sMeNZgQ==",
|
"integrity": "sha512-pbZOSMVVkvppW7XRn9fcHK5OgEDnYLwMva7P6TgS44/SN9uGGjfh3Z1c8tomO+y4IsHQ6Fsz2EGwmE7sMeNZgQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"react-markdown": {
|
"react-markdown": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@material-ui/core": "^1.0.0",
|
||||||
"abi-decoder": "^1.0.9",
|
"abi-decoder": "^1.0.9",
|
||||||
"asmcrypto.js": "0.22.0",
|
"asmcrypto.js": "0.22.0",
|
||||||
"async": "^2.5.0",
|
"async": "^2.5.0",
|
||||||
@ -136,7 +137,6 @@
|
|||||||
"lodash.shuffle": "^4.2.0",
|
"lodash.shuffle": "^4.2.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"loglevel": "^1.4.1",
|
"loglevel": "^1.4.1",
|
||||||
"material-ui": "1.0.0-beta.44",
|
|
||||||
"metamascara": "^2.0.0",
|
"metamascara": "^2.0.0",
|
||||||
"metamask-logo": "^2.1.4",
|
"metamask-logo": "^2.1.4",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
@ -275,6 +275,10 @@ var actions = {
|
|||||||
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
|
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
|
||||||
|
|
||||||
retryTransaction,
|
retryTransaction,
|
||||||
|
SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
|
||||||
|
CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
|
||||||
|
setPendingTokens,
|
||||||
|
clearPendingTokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = actions
|
module.exports = actions
|
||||||
@ -1929,3 +1933,22 @@ function updateNetworkEndpointType (networkEndpointType) {
|
|||||||
value: networkEndpointType,
|
value: networkEndpointType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPendingTokens (pendingTokens) {
|
||||||
|
const { customToken = {}, selectedTokens = {} } = pendingTokens
|
||||||
|
const { address, symbol, decimals } = customToken
|
||||||
|
const tokens = address && symbol && decimals
|
||||||
|
? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
|
||||||
|
: selectedTokens
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: actions.SET_PENDING_TOKENS,
|
||||||
|
payload: tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingTokens () {
|
||||||
|
return {
|
||||||
|
type: actions.CLEAR_PENDING_TOKENS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,6 +26,7 @@ const UnlockPage = require('./components/pages/unlock-page')
|
|||||||
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
|
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
|
||||||
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||||
const AddTokenPage = require('./components/pages/add-token')
|
const AddTokenPage = require('./components/pages/add-token')
|
||||||
|
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
|
||||||
const CreateAccountPage = require('./components/pages/create-account')
|
const CreateAccountPage = require('./components/pages/create-account')
|
||||||
const NoticeScreen = require('./components/pages/notice')
|
const NoticeScreen = require('./components/pages/notice')
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ const {
|
|||||||
REVEAL_SEED_ROUTE,
|
REVEAL_SEED_ROUTE,
|
||||||
RESTORE_VAULT_ROUTE,
|
RESTORE_VAULT_ROUTE,
|
||||||
ADD_TOKEN_ROUTE,
|
ADD_TOKEN_ROUTE,
|
||||||
|
CONFIRM_ADD_TOKEN_ROUTE,
|
||||||
NEW_ACCOUNT_ROUTE,
|
NEW_ACCOUNT_ROUTE,
|
||||||
SEND_ROUTE,
|
SEND_ROUTE,
|
||||||
CONFIRM_TRANSACTION_ROUTE,
|
CONFIRM_TRANSACTION_ROUTE,
|
||||||
@ -77,6 +79,7 @@ class App extends Component {
|
|||||||
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
|
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
|
||||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
|
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
|
||||||
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
||||||
|
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
|
||||||
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
|
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
|
||||||
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
|
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
|
||||||
])
|
])
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const { Component } = require('react')
|
import React, { Component } from 'react'
|
||||||
const h = require('react-hyperscript')
|
import PropTypes from 'prop-types'
|
||||||
const PropTypes = require('prop-types')
|
import classnames from 'classnames'
|
||||||
const classnames = require('classnames')
|
|
||||||
|
|
||||||
const SECONDARY = 'secondary'
|
const SECONDARY = 'secondary'
|
||||||
const CLASSNAME_PRIMARY = 'btn-primary'
|
const CLASSNAME_PRIMARY = 'btn-primary'
|
||||||
@ -24,10 +23,12 @@ class Button extends Component {
|
|||||||
const { type, large, className, ...buttonProps } = this.props
|
const { type, large, className, ...buttonProps } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
h('button', {
|
<button
|
||||||
className: classnames(getClassName(type, large), className),
|
className={classnames(getClassName(type, large), className)}
|
||||||
...buttonProps,
|
{ ...buttonProps }
|
||||||
}, this.props.children)
|
>
|
||||||
|
{ this.props.children }
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,5 +40,5 @@ Button.propTypes = {
|
|||||||
children: PropTypes.string,
|
children: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Button
|
export default Button
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
const Button = require('./button.component')
|
import Button from './button.component'
|
||||||
module.exports = Button
|
module.exports = Button
|
||||||
|
5
ui/app/components/index.scss
Normal file
5
ui/app/components/index.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import './export-text-container/index';
|
||||||
|
|
||||||
|
@import './info-box/index';
|
||||||
|
|
||||||
|
@import './pages/index';
|
2
ui/app/components/info-box/index.js
Normal file
2
ui/app/components/info-box/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import InfoBox from './info-box.component'
|
||||||
|
module.exports = InfoBox
|
24
ui/app/components/info-box/index.scss
Normal file
24
ui/app/components/info-box/index.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.info-box {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $alabaster;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
color: $mid-gray;
|
||||||
|
|
||||||
|
&__close::after {
|
||||||
|
content: '\00D7';
|
||||||
|
font-size: 29px;
|
||||||
|
font-weight: 200;
|
||||||
|
color: $dusty-gray;
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
49
ui/app/components/info-box/info-box.component.js
Normal file
49
ui/app/components/info-box/info-box.component.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default class InfoBox extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isShowing: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose () {
|
||||||
|
const { onClose } = this.props
|
||||||
|
|
||||||
|
if (onClose) {
|
||||||
|
onClose()
|
||||||
|
} else {
|
||||||
|
this.setState({ isShowing: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { title, description } = this.props
|
||||||
|
|
||||||
|
return !this.state.isShowing
|
||||||
|
? null
|
||||||
|
: (
|
||||||
|
<div className="info-box">
|
||||||
|
<div
|
||||||
|
className="info-box__close"
|
||||||
|
onClick={() => this.handleClose()}
|
||||||
|
/>
|
||||||
|
<div className="info-box__title">{ title }</div>
|
||||||
|
<div className="info-box__description">{ description }</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,431 +0,0 @@
|
|||||||
const inherits = require('util').inherits
|
|
||||||
const Component = require('react').Component
|
|
||||||
const classnames = require('classnames')
|
|
||||||
const h = require('react-hyperscript')
|
|
||||||
const PropTypes = require('prop-types')
|
|
||||||
const connect = require('react-redux').connect
|
|
||||||
const R = require('ramda')
|
|
||||||
const Fuse = require('fuse.js')
|
|
||||||
const contractMap = require('eth-contract-metadata')
|
|
||||||
const TokenBalance = require('../../components/token-balance')
|
|
||||||
const Identicon = require('../../components/identicon')
|
|
||||||
const contractList = Object.entries(contractMap)
|
|
||||||
.map(([ _, tokenData]) => tokenData)
|
|
||||||
.filter(tokenData => Boolean(tokenData.erc20))
|
|
||||||
const fuse = new Fuse(contractList, {
|
|
||||||
shouldSort: true,
|
|
||||||
threshold: 0.45,
|
|
||||||
location: 0,
|
|
||||||
distance: 100,
|
|
||||||
maxPatternLength: 32,
|
|
||||||
minMatchCharLength: 1,
|
|
||||||
keys: [
|
|
||||||
{ name: 'name', weight: 0.5 },
|
|
||||||
{ name: 'symbol', weight: 0.5 },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const actions = require('../../actions')
|
|
||||||
const ethUtil = require('ethereumjs-util')
|
|
||||||
const { tokenInfoGetter } = require('../../token-util')
|
|
||||||
const { DEFAULT_ROUTE } = require('../../routes')
|
|
||||||
|
|
||||||
const emptyAddr = '0x0000000000000000000000000000000000000000'
|
|
||||||
|
|
||||||
AddTokenScreen.contextTypes = {
|
|
||||||
t: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
|
|
||||||
|
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
|
||||||
const { identities, tokens } = state.metamask
|
|
||||||
return {
|
|
||||||
identities,
|
|
||||||
tokens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps (dispatch) {
|
|
||||||
return {
|
|
||||||
addTokens: tokens => dispatch(actions.addTokens(tokens)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inherits(AddTokenScreen, Component)
|
|
||||||
function AddTokenScreen () {
|
|
||||||
this.state = {
|
|
||||||
isShowingConfirmation: false,
|
|
||||||
isShowingInfoBox: true,
|
|
||||||
customAddress: '',
|
|
||||||
customSymbol: '',
|
|
||||||
customDecimals: '',
|
|
||||||
searchQuery: '',
|
|
||||||
selectedTokens: {},
|
|
||||||
errors: {},
|
|
||||||
autoFilled: false,
|
|
||||||
displayedTab: 'SEARCH',
|
|
||||||
}
|
|
||||||
this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
|
|
||||||
this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
|
|
||||||
this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
|
|
||||||
this.onNext = this.onNext.bind(this)
|
|
||||||
Component.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.componentWillMount = function () {
|
|
||||||
this.tokenInfoGetter = tokenInfoGetter()
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.toggleToken = function (address, token) {
|
|
||||||
const { selectedTokens = {}, errors } = this.state
|
|
||||||
const selectedTokensCopy = { ...selectedTokens }
|
|
||||||
|
|
||||||
if (address in selectedTokensCopy) {
|
|
||||||
delete selectedTokensCopy[address]
|
|
||||||
} else {
|
|
||||||
selectedTokensCopy[address] = token
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
selectedTokens: selectedTokensCopy,
|
|
||||||
errors: {
|
|
||||||
...errors,
|
|
||||||
tokenSelector: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.onNext = function () {
|
|
||||||
const { isValid, errors } = this.validate()
|
|
||||||
|
|
||||||
return !isValid
|
|
||||||
? this.setState({ errors })
|
|
||||||
: this.setState({ isShowingConfirmation: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
|
|
||||||
const customAddress = e.target.value.trim()
|
|
||||||
this.setState({ customAddress })
|
|
||||||
if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
|
|
||||||
this.attemptToAutoFillTokenParams(customAddress)
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
customSymbol: '',
|
|
||||||
customDecimals: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.tokenSymbolDidChange = function (e) {
|
|
||||||
const customSymbol = e.target.value.trim()
|
|
||||||
this.setState({ customSymbol })
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
|
|
||||||
const customDecimals = e.target.value.trim()
|
|
||||||
this.setState({ customDecimals })
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.checkExistingAddresses = function (address) {
|
|
||||||
if (!address) return false
|
|
||||||
const tokensList = this.props.tokens
|
|
||||||
const matchesAddress = existingToken => {
|
|
||||||
return existingToken.address.toLowerCase() === address.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return R.any(matchesAddress)(tokensList)
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.validate = function () {
|
|
||||||
const errors = {}
|
|
||||||
const identitiesList = Object.keys(this.props.identities)
|
|
||||||
const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
|
|
||||||
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
|
|
||||||
|
|
||||||
if (customAddress) {
|
|
||||||
const validAddress = ethUtil.isValidAddress(customAddress)
|
|
||||||
if (!validAddress) {
|
|
||||||
errors.customAddress = this.context.t('invalidAddress')
|
|
||||||
}
|
|
||||||
|
|
||||||
const validDecimals = customDecimals !== null
|
|
||||||
&& customDecimals !== ''
|
|
||||||
&& customDecimals >= 0
|
|
||||||
&& customDecimals < 36
|
|
||||||
if (!validDecimals) {
|
|
||||||
errors.customDecimals = this.context.t('decimalsMustZerotoTen')
|
|
||||||
}
|
|
||||||
|
|
||||||
const symbolLen = customSymbol.trim().length
|
|
||||||
const validSymbol = symbolLen > 0 && symbolLen < 10
|
|
||||||
if (!validSymbol) {
|
|
||||||
errors.customSymbol = this.context.t('symbolBetweenZeroTen')
|
|
||||||
}
|
|
||||||
|
|
||||||
const ownAddress = identitiesList.includes(standardAddress)
|
|
||||||
if (ownAddress) {
|
|
||||||
errors.customAddress = this.context.t('personalAddressDetected')
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
|
|
||||||
if (tokenAlreadyAdded) {
|
|
||||||
errors.customAddress = this.context.t('tokenAlreadyAdded')
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
Object.entries(selectedTokens)
|
|
||||||
.reduce((isEmpty, [ symbol, isSelected ]) => (
|
|
||||||
isEmpty && !isSelected
|
|
||||||
), true)
|
|
||||||
) {
|
|
||||||
errors.tokenSelector = this.context.t('mustSelectOne')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: !Object.keys(errors).length,
|
|
||||||
errors,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
|
|
||||||
const { symbol, decimals } = await this.tokenInfoGetter(address)
|
|
||||||
if (symbol && decimals) {
|
|
||||||
this.setState({
|
|
||||||
customSymbol: symbol,
|
|
||||||
customDecimals: decimals,
|
|
||||||
autoFilled: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.renderCustomForm = function () {
|
|
||||||
const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
|
|
||||||
|
|
||||||
return (
|
|
||||||
h('div.add-token__add-custom-form', [
|
|
||||||
h('div', {
|
|
||||||
className: classnames('add-token__add-custom-field', {
|
|
||||||
'add-token__add-custom-field--error': errors.customAddress,
|
|
||||||
}),
|
|
||||||
}, [
|
|
||||||
h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
|
|
||||||
h('input.add-token__add-custom-input', {
|
|
||||||
type: 'text',
|
|
||||||
onChange: this.tokenAddressDidChange,
|
|
||||||
value: customAddress,
|
|
||||||
}),
|
|
||||||
h('div.add-token__add-custom-error-message', errors.customAddress),
|
|
||||||
]),
|
|
||||||
h('div', {
|
|
||||||
className: classnames('add-token__add-custom-field', {
|
|
||||||
'add-token__add-custom-field--error': errors.customSymbol,
|
|
||||||
}),
|
|
||||||
}, [
|
|
||||||
h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
|
|
||||||
h('input.add-token__add-custom-input', {
|
|
||||||
type: 'text',
|
|
||||||
onChange: this.tokenSymbolDidChange,
|
|
||||||
value: customSymbol,
|
|
||||||
disabled: autoFilled,
|
|
||||||
}),
|
|
||||||
h('div.add-token__add-custom-error-message', errors.customSymbol),
|
|
||||||
]),
|
|
||||||
h('div', {
|
|
||||||
className: classnames('add-token__add-custom-field', {
|
|
||||||
'add-token__add-custom-field--error': errors.customDecimals,
|
|
||||||
}),
|
|
||||||
}, [
|
|
||||||
h('div.add-token__add-custom-label', this.context.t('decimal')),
|
|
||||||
h('input.add-token__add-custom-input', {
|
|
||||||
type: 'number',
|
|
||||||
onChange: this.tokenDecimalsDidChange,
|
|
||||||
value: customDecimals,
|
|
||||||
disabled: autoFilled,
|
|
||||||
}),
|
|
||||||
h('div.add-token__add-custom-error-message', errors.customDecimals),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.renderTokenList = function () {
|
|
||||||
const { searchQuery = '', selectedTokens } = this.state
|
|
||||||
const fuseSearchResult = fuse.search(searchQuery)
|
|
||||||
const addressSearchResult = contractList.filter(token => {
|
|
||||||
return token.address.toLowerCase() === searchQuery.toLowerCase()
|
|
||||||
})
|
|
||||||
const results = [...addressSearchResult, ...fuseSearchResult]
|
|
||||||
|
|
||||||
return h('div', [
|
|
||||||
results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')),
|
|
||||||
h('div.add-token__token-icons-container', Array(6).fill(undefined)
|
|
||||||
.map((_, i) => {
|
|
||||||
const { logo, symbol, name, address } = results[i] || {}
|
|
||||||
const tokenAlreadyAdded = this.checkExistingAddresses(address)
|
|
||||||
return Boolean(logo || symbol || name) && (
|
|
||||||
h('div.add-token__token-wrapper', {
|
|
||||||
className: classnames({
|
|
||||||
'add-token__token-wrapper--selected': selectedTokens[address],
|
|
||||||
'add-token__token-wrapper--disabled': tokenAlreadyAdded,
|
|
||||||
}),
|
|
||||||
onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
|
|
||||||
}, [
|
|
||||||
h('div.add-token__token-icon', {
|
|
||||||
style: {
|
|
||||||
backgroundImage: logo && `url(images/contract/${logo})`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
h('div.add-token__token-data', [
|
|
||||||
h('div.add-token__token-symbol', symbol),
|
|
||||||
h('div.add-token__token-name', name),
|
|
||||||
]),
|
|
||||||
// tokenAlreadyAdded && (
|
|
||||||
// h('div.add-token__token-message', 'Already added')
|
|
||||||
// ),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.renderConfirmation = function () {
|
|
||||||
const {
|
|
||||||
customAddress: address,
|
|
||||||
customSymbol: symbol,
|
|
||||||
customDecimals: decimals,
|
|
||||||
selectedTokens,
|
|
||||||
} = this.state
|
|
||||||
|
|
||||||
const { addTokens, history } = this.props
|
|
||||||
|
|
||||||
const customToken = {
|
|
||||||
address,
|
|
||||||
symbol,
|
|
||||||
decimals,
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = address && symbol && decimals
|
|
||||||
? { ...selectedTokens, [address]: customToken }
|
|
||||||
: selectedTokens
|
|
||||||
|
|
||||||
return (
|
|
||||||
h('div.add-token', [
|
|
||||||
h('div.add-token__wrapper', [
|
|
||||||
h('div.add-token__content-container.add-token__confirmation-content', [
|
|
||||||
h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
|
|
||||||
h('div.add-token__confirmation-token-list',
|
|
||||||
Object.entries(tokens)
|
|
||||||
.map(([ address, token ]) => (
|
|
||||||
h('span.add-token__confirmation-token-list-item', [
|
|
||||||
h(Identicon, {
|
|
||||||
className: 'add-token__confirmation-token-icon',
|
|
||||||
diameter: 75,
|
|
||||||
address,
|
|
||||||
}),
|
|
||||||
h(TokenBalance, { token }),
|
|
||||||
])
|
|
||||||
))
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
h('div.add-token__buttons', [
|
|
||||||
h('button.btn-secondary--lg.add-token__cancel-button', {
|
|
||||||
onClick: () => this.setState({ isShowingConfirmation: false }),
|
|
||||||
}, this.context.t('back')),
|
|
||||||
h('button.btn-primary--lg', {
|
|
||||||
onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
|
|
||||||
}, this.context.t('addTokens')),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.displayTab = function (selectedTab) {
|
|
||||||
this.setState({ displayedTab: selectedTab })
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.renderTabs = function () {
|
|
||||||
const { isShowingInfoBox, displayedTab, errors } = this.state
|
|
||||||
|
|
||||||
return displayedTab === 'CUSTOM_TOKEN'
|
|
||||||
? this.renderCustomForm()
|
|
||||||
: h('div', [
|
|
||||||
h('div.add-token__wrapper', [
|
|
||||||
h('div.add-token__content-container', [
|
|
||||||
isShowingInfoBox && h('div.add-token__info-box', [
|
|
||||||
h('div.add-token__info-box__close', {
|
|
||||||
onClick: () => this.setState({ isShowingInfoBox: false }),
|
|
||||||
}),
|
|
||||||
h('div.add-token__info-box__title', this.context.t('whatsThis')),
|
|
||||||
h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
|
|
||||||
h('a.add-token__info-box__copy--blue', {
|
|
||||||
href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
|
|
||||||
target: '_blank',
|
|
||||||
}, this.context.t('learnMore')),
|
|
||||||
]),
|
|
||||||
h('div.add-token__input-container', [
|
|
||||||
h('input.add-token__input', {
|
|
||||||
type: 'text',
|
|
||||||
placeholder: this.context.t('searchTokens'),
|
|
||||||
onChange: e => this.setState({ searchQuery: e.target.value }),
|
|
||||||
}),
|
|
||||||
h('div.add-token__search-input-error-message', errors.tokenSelector),
|
|
||||||
]),
|
|
||||||
this.renderTokenList(),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTokenScreen.prototype.render = function () {
|
|
||||||
const {
|
|
||||||
isShowingConfirmation,
|
|
||||||
displayedTab,
|
|
||||||
} = this.state
|
|
||||||
const { history } = this.props
|
|
||||||
|
|
||||||
return h('div.add-token', [
|
|
||||||
h('div.add-token__header', [
|
|
||||||
h('div.add-token__header__cancel', {
|
|
||||||
onClick: () => history.push(DEFAULT_ROUTE),
|
|
||||||
}, [
|
|
||||||
h('i.fa.fa-angle-left.fa-lg'),
|
|
||||||
h('span', this.context.t('cancel')),
|
|
||||||
]),
|
|
||||||
h('div.add-token__header__title', this.context.t('addTokens')),
|
|
||||||
isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
|
|
||||||
!isShowingConfirmation && h('div.add-token__header__tabs', [
|
|
||||||
|
|
||||||
h('div.add-token__header__tabs__tab', {
|
|
||||||
className: classnames('add-token__header__tabs__tab', {
|
|
||||||
'add-token__header__tabs__selected': displayedTab === 'SEARCH',
|
|
||||||
'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
|
|
||||||
}),
|
|
||||||
onClick: () => this.displayTab('SEARCH'),
|
|
||||||
}, this.context.t('search')),
|
|
||||||
|
|
||||||
h('div.add-token__header__tabs__tab', {
|
|
||||||
className: classnames('add-token__header__tabs__tab', {
|
|
||||||
'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
|
|
||||||
'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
|
|
||||||
}),
|
|
||||||
onClick: () => this.displayTab('CUSTOM_TOKEN'),
|
|
||||||
}, this.context.t('customToken')),
|
|
||||||
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
|
|
||||||
isShowingConfirmation
|
|
||||||
? this.renderConfirmation()
|
|
||||||
: this.renderTabs(),
|
|
||||||
|
|
||||||
!isShowingConfirmation && h('div.add-token__buttons', [
|
|
||||||
h('button.btn-secondary--lg.add-token__cancel-button', {
|
|
||||||
onClick: () => history.push(DEFAULT_ROUTE),
|
|
||||||
}, this.context.t('cancel')),
|
|
||||||
h('button.btn-primary--lg.add-token__confirm-button', {
|
|
||||||
onClick: this.onNext,
|
|
||||||
}, this.context.t('next')),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
}
|
|
356
ui/app/components/pages/add-token/add-token.component.js
Normal file
356
ui/app/components/pages/add-token/add-token.component.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import ethUtil from 'ethereumjs-util'
|
||||||
|
import { checkExistingAddresses } from './util'
|
||||||
|
import { tokenInfoGetter } from '../../../token-util'
|
||||||
|
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
|
||||||
|
import Button from '../../button'
|
||||||
|
import TextField from '../../text-field'
|
||||||
|
import TokenList from './token-list'
|
||||||
|
import TokenSearch from './token-search'
|
||||||
|
|
||||||
|
const emptyAddr = '0x0000000000000000000000000000000000000000'
|
||||||
|
const SEARCH_TAB = 'SEARCH'
|
||||||
|
const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
|
||||||
|
|
||||||
|
class AddToken extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
history: PropTypes.object,
|
||||||
|
setPendingTokens: PropTypes.func,
|
||||||
|
pendingTokens: PropTypes.object,
|
||||||
|
clearPendingTokens: PropTypes.func,
|
||||||
|
tokens: PropTypes.array,
|
||||||
|
identities: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
customAddress: '',
|
||||||
|
customSymbol: '',
|
||||||
|
customDecimals: 0,
|
||||||
|
searchResults: [],
|
||||||
|
selectedTokens: {},
|
||||||
|
tokenSelectorError: null,
|
||||||
|
customAddressError: null,
|
||||||
|
customSymbolError: null,
|
||||||
|
customDecimalsError: null,
|
||||||
|
autoFilled: false,
|
||||||
|
displayedTab: SEARCH_TAB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.tokenInfoGetter = tokenInfoGetter()
|
||||||
|
const { pendingTokens = {} } = this.props
|
||||||
|
const pendingTokenKeys = Object.keys(pendingTokens)
|
||||||
|
|
||||||
|
if (pendingTokenKeys.length > 0) {
|
||||||
|
let selectedTokens = {}
|
||||||
|
let customToken = {}
|
||||||
|
|
||||||
|
pendingTokenKeys.forEach(tokenAddress => {
|
||||||
|
const token = pendingTokens[tokenAddress]
|
||||||
|
const { isCustom } = token
|
||||||
|
|
||||||
|
if (isCustom) {
|
||||||
|
customToken = { ...token }
|
||||||
|
} else {
|
||||||
|
selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
address: customAddress = '',
|
||||||
|
symbol: customSymbol = '',
|
||||||
|
decimals: customDecimals = 0,
|
||||||
|
} = customToken
|
||||||
|
|
||||||
|
const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB
|
||||||
|
this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleToken (token) {
|
||||||
|
const { address } = token
|
||||||
|
const { selectedTokens = {} } = this.state
|
||||||
|
const selectedTokensCopy = { ...selectedTokens }
|
||||||
|
|
||||||
|
if (address in selectedTokensCopy) {
|
||||||
|
delete selectedTokensCopy[address]
|
||||||
|
} else {
|
||||||
|
selectedTokensCopy[address] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedTokens: selectedTokensCopy,
|
||||||
|
tokenSelectorError: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError () {
|
||||||
|
const {
|
||||||
|
tokenSelectorError,
|
||||||
|
customAddressError,
|
||||||
|
customSymbolError,
|
||||||
|
customDecimalsError,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelected () {
|
||||||
|
const { customAddress = '', selectedTokens = {} } = this.state
|
||||||
|
return customAddress || Object.keys(selectedTokens).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNext () {
|
||||||
|
if (this.hasError()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasSelected()) {
|
||||||
|
this.setState({ tokenSelectorError: this.context.t('mustSelectOne') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { setPendingTokens, history } = this.props
|
||||||
|
const {
|
||||||
|
customAddress: address,
|
||||||
|
customSymbol: symbol,
|
||||||
|
customDecimals: decimals,
|
||||||
|
selectedTokens,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const customToken = {
|
||||||
|
address,
|
||||||
|
symbol,
|
||||||
|
decimals,
|
||||||
|
}
|
||||||
|
|
||||||
|
setPendingTokens({ customToken, selectedTokens })
|
||||||
|
history.push(CONFIRM_ADD_TOKEN_ROUTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
async attemptToAutoFillTokenParams (address) {
|
||||||
|
const { symbol, decimals } = await this.tokenInfoGetter(address)
|
||||||
|
|
||||||
|
if (symbol && decimals) {
|
||||||
|
this.setState({
|
||||||
|
customSymbol: symbol,
|
||||||
|
customDecimals: decimals,
|
||||||
|
customSymbolError: null,
|
||||||
|
customDecimalsError: null,
|
||||||
|
autoFilled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCustomAddressChange (value) {
|
||||||
|
const customAddress = value.trim()
|
||||||
|
this.setState({
|
||||||
|
customAddress,
|
||||||
|
customAddressError: null,
|
||||||
|
tokenSelectorError: null,
|
||||||
|
autoFilled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValidAddress = ethUtil.isValidAddress(customAddress)
|
||||||
|
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case !isValidAddress:
|
||||||
|
this.setState({
|
||||||
|
customAddressError: this.context.t('invalidAddress'),
|
||||||
|
customSymbol: '',
|
||||||
|
customDecimals: 0,
|
||||||
|
customSymbolError: null,
|
||||||
|
customDecimalsError: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
case Boolean(this.props.identities[standardAddress]):
|
||||||
|
this.setState({
|
||||||
|
customAddressError: this.context.t('personalAddressDetected'),
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
case checkExistingAddresses(customAddress, this.props.tokens):
|
||||||
|
this.setState({
|
||||||
|
customAddressError: this.context.t('tokenAlreadyAdded'),
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (customAddress !== emptyAddr) {
|
||||||
|
this.attemptToAutoFillTokenParams(customAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCustomSymbolChange (value) {
|
||||||
|
const customSymbol = value.trim()
|
||||||
|
const symbolLength = customSymbol.length
|
||||||
|
let customSymbolError = null
|
||||||
|
|
||||||
|
if (symbolLength <= 0 || symbolLength >= 10) {
|
||||||
|
customSymbolError = this.context.t('symbolBetweenZeroTen')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ customSymbol, customSymbolError })
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCustomDecimalsChange (value) {
|
||||||
|
const customDecimals = value.trim()
|
||||||
|
const validDecimals = customDecimals !== null &&
|
||||||
|
customDecimals !== '' &&
|
||||||
|
customDecimals >= 0 &&
|
||||||
|
customDecimals < 36
|
||||||
|
let customDecimalsError = null
|
||||||
|
|
||||||
|
if (!validDecimals) {
|
||||||
|
customDecimalsError = this.context.t('decimalsMustZerotoTen')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ customDecimals, customDecimalsError })
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCustomTokenForm () {
|
||||||
|
const {
|
||||||
|
customAddress,
|
||||||
|
customSymbol,
|
||||||
|
customDecimals,
|
||||||
|
customAddressError,
|
||||||
|
customSymbolError,
|
||||||
|
customDecimalsError,
|
||||||
|
autoFilled,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="add-token__custom-token-form">
|
||||||
|
<TextField
|
||||||
|
id="custom-address"
|
||||||
|
label="Token Address"
|
||||||
|
type="text"
|
||||||
|
value={customAddress}
|
||||||
|
onChange={e => this.handleCustomAddressChange(e.target.value)}
|
||||||
|
error={customAddressError}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="custom-symbol"
|
||||||
|
label="Token Symbol"
|
||||||
|
type="text"
|
||||||
|
value={customSymbol}
|
||||||
|
onChange={e => this.handleCustomSymbolChange(e.target.value)}
|
||||||
|
error={customSymbolError}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
disabled={autoFilled}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="custom-decimals"
|
||||||
|
label="Decimals of Precision"
|
||||||
|
type="number"
|
||||||
|
value={customDecimals}
|
||||||
|
onChange={e => this.handleCustomDecimalsChange(e.target.value)}
|
||||||
|
error={customDecimalsError}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
disabled={autoFilled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSearchToken () {
|
||||||
|
const { tokenSelectorError, selectedTokens, searchResults } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="add-token__search-token">
|
||||||
|
<TokenSearch
|
||||||
|
onSearch={({ results = [] }) => this.setState({ searchResults: results })}
|
||||||
|
error={tokenSelectorError}
|
||||||
|
/>
|
||||||
|
<div className="add-token__token-list">
|
||||||
|
<TokenList
|
||||||
|
results={searchResults}
|
||||||
|
selectedTokens={selectedTokens}
|
||||||
|
onToggleToken={token => this.handleToggleToken(token)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { displayedTab } = this.state
|
||||||
|
const { history, clearPendingTokens } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container">
|
||||||
|
<div className="page-container__header page-container__header--no-padding-bottom">
|
||||||
|
<div className="page-container__title">
|
||||||
|
{ this.context.t('addTokens') }
|
||||||
|
</div>
|
||||||
|
<div className="page-container__tabs">
|
||||||
|
<div
|
||||||
|
className={classnames('page-container__tab', {
|
||||||
|
'page-container__tab--selected': displayedTab === SEARCH_TAB,
|
||||||
|
})}
|
||||||
|
onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
|
||||||
|
>
|
||||||
|
{ this.context.t('search') }
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classnames('page-container__tab', {
|
||||||
|
'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
|
||||||
|
})}
|
||||||
|
onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
|
||||||
|
>
|
||||||
|
{ this.context.t('customToken') }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="page-container__content">
|
||||||
|
{
|
||||||
|
displayedTab === CUSTOM_TOKEN_TAB
|
||||||
|
? this.renderCustomTokenForm()
|
||||||
|
: this.renderSearchToken()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="page-container__footer">
|
||||||
|
<Button
|
||||||
|
type="secondary"
|
||||||
|
large
|
||||||
|
className="page-container__footer-button"
|
||||||
|
onClick={() => {
|
||||||
|
clearPendingTokens()
|
||||||
|
history.push(DEFAULT_ROUTE)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ this.context.t('cancel') }
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
large
|
||||||
|
className="page-container__footer-button"
|
||||||
|
onClick={() => this.handleNext()}
|
||||||
|
disabled={this.hasError() || !this.hasSelected()}
|
||||||
|
>
|
||||||
|
{ this.context.t('next') }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddToken
|
22
ui/app/components/pages/add-token/add-token.container.js
Normal file
22
ui/app/components/pages/add-token/add-token.container.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { connect } from 'react-redux'
|
||||||
|
import AddToken from './add-token.component'
|
||||||
|
|
||||||
|
const { setPendingTokens, clearPendingTokens } = require('../../../actions')
|
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => {
|
||||||
|
const { identities, tokens, pendingTokens } = metamask
|
||||||
|
return {
|
||||||
|
identities,
|
||||||
|
tokens,
|
||||||
|
pendingTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
|
||||||
|
clearPendingTokens: () => dispatch(clearPendingTokens()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AddToken)
|
2
ui/app/components/pages/add-token/index.js
Normal file
2
ui/app/components/pages/add-token/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import AddToken from './add-token.container'
|
||||||
|
module.exports = AddToken
|
25
ui/app/components/pages/add-token/index.scss
Normal file
25
ui/app/components/pages/add-token/index.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@import './token-list/index';
|
||||||
|
|
||||||
|
.add-token {
|
||||||
|
&__custom-token-form {
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]:hover::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-token {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-list {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
2
ui/app/components/pages/add-token/token-list/index.js
Normal file
2
ui/app/components/pages/add-token/token-list/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import TokenList from './token-list.container'
|
||||||
|
module.exports = TokenList
|
65
ui/app/components/pages/add-token/token-list/index.scss
Normal file
65
ui/app/components/pages/add-token/token-list/index.scss
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@import './token-list-placeholder/index';
|
||||||
|
|
||||||
|
.token-list {
|
||||||
|
&__title {
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tokens-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token {
|
||||||
|
transition: 200ms ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid rgba($malibu-blue, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
border: 2px solid $malibu-blue !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: .4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $white;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba($black, .24);
|
||||||
|
margin-right: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
import TokenListPlaceholder from './token-list-placeholder.component'
|
||||||
|
module.exports = TokenListPlaceholder
|
@ -0,0 +1,19 @@
|
|||||||
|
.token-list-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 36px;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 22px;
|
||||||
|
opacity: .5;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $silver-chalice;
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
color: $curious-blue;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default class TokenListPlaceholder extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className="token-list-placeholder">
|
||||||
|
<img src="images/tokensearch.svg" />
|
||||||
|
<div className="token-list-placeholder__text">
|
||||||
|
{ this.context.t('addAcquiredTokens') }
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
className="token-list-placeholder__link"
|
||||||
|
href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{ this.context.t('learnMore') }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { checkExistingAddresses } from '../util'
|
||||||
|
import TokenListPlaceholder from './token-list-placeholder'
|
||||||
|
|
||||||
|
export default class InfoBox extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
tokens: PropTypes.array,
|
||||||
|
results: PropTypes.array,
|
||||||
|
selectedTokens: PropTypes.object,
|
||||||
|
onToggleToken: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props
|
||||||
|
|
||||||
|
return results.length === 0
|
||||||
|
? <TokenListPlaceholder />
|
||||||
|
: (
|
||||||
|
<div className="token-list">
|
||||||
|
<div className="token-list__title">
|
||||||
|
{ this.context.t('searchResults') }
|
||||||
|
</div>
|
||||||
|
<div className="token-list__tokens-container">
|
||||||
|
{
|
||||||
|
Array(6).fill(undefined)
|
||||||
|
.map((_, i) => {
|
||||||
|
const { logo, symbol, name, address } = results[i] || {}
|
||||||
|
const tokenAlreadyAdded = checkExistingAddresses(address, tokens)
|
||||||
|
|
||||||
|
return Boolean(logo || symbol || name) && (
|
||||||
|
<div
|
||||||
|
className={classnames('token-list__token', {
|
||||||
|
'token-list__token--selected': selectedTokens[address],
|
||||||
|
'token-list__token--disabled': tokenAlreadyAdded,
|
||||||
|
})}
|
||||||
|
onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])}
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="token-list__token-icon"
|
||||||
|
style={{ backgroundImage: logo && `url(images/contract/${logo})` }}>
|
||||||
|
</div>
|
||||||
|
<div className="token-list__token-data">
|
||||||
|
<span className="token-list__token-name">{ `${name} (${symbol})` }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { connect } from 'react-redux'
|
||||||
|
import TokenList from './token-list.component'
|
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => {
|
||||||
|
const { tokens } = metamask
|
||||||
|
return {
|
||||||
|
tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(TokenList)
|
2
ui/app/components/pages/add-token/token-search/index.js
Normal file
2
ui/app/components/pages/add-token/token-search/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import TokenSearch from './token-search.component'
|
||||||
|
module.exports = TokenSearch
|
@ -0,0 +1,85 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import contractMap from 'eth-contract-metadata'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||||
|
import TextField from '../../../text-field'
|
||||||
|
|
||||||
|
const contractList = Object.entries(contractMap)
|
||||||
|
.map(([ _, tokenData]) => tokenData)
|
||||||
|
.filter(tokenData => Boolean(tokenData.erc20))
|
||||||
|
|
||||||
|
const fuse = new Fuse(contractList, {
|
||||||
|
shouldSort: true,
|
||||||
|
threshold: 0.45,
|
||||||
|
location: 0,
|
||||||
|
distance: 100,
|
||||||
|
maxPatternLength: 32,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
keys: [
|
||||||
|
{ name: 'name', weight: 0.5 },
|
||||||
|
{ name: 'symbol', weight: 0.5 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class TokenSearch extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onSearch: PropTypes.func,
|
||||||
|
error: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchQuery: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch (searchQuery) {
|
||||||
|
this.setState({ searchQuery })
|
||||||
|
const fuseSearchResult = fuse.search(searchQuery)
|
||||||
|
const addressSearchResult = contractList.filter(token => {
|
||||||
|
return token.address.toLowerCase() === searchQuery.toLowerCase()
|
||||||
|
})
|
||||||
|
const results = [...addressSearchResult, ...fuseSearchResult]
|
||||||
|
this.props.onSearch({ searchQuery, results })
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAdornment () {
|
||||||
|
return (
|
||||||
|
<InputAdornment
|
||||||
|
position="start"
|
||||||
|
style={{ marginRight: '12px' }}
|
||||||
|
>
|
||||||
|
<img src="images/search.svg" />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { error } = this.props
|
||||||
|
const { searchQuery } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
id="search-tokens"
|
||||||
|
placeholder={this.context.t('searchTokens')}
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => this.handleSearch(e.target.value)}
|
||||||
|
error={error}
|
||||||
|
fullWidth
|
||||||
|
startAdornment={this.renderAdornment()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
13
ui/app/components/pages/add-token/util.js
Normal file
13
ui/app/components/pages/add-token/util.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import R from 'ramda'
|
||||||
|
|
||||||
|
export function checkExistingAddresses (address, tokenList = []) {
|
||||||
|
if (!address) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesAddress = existingToken => {
|
||||||
|
return existingToken.address.toLowerCase() === address.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.any(matchesAddress)(tokenList)
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
|
||||||
|
import Button from '../../button'
|
||||||
|
import Identicon from '../../../components/identicon'
|
||||||
|
import TokenBalance from './token-balance'
|
||||||
|
|
||||||
|
export default class ConfirmAddToken extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
history: PropTypes.object,
|
||||||
|
clearPendingTokens: PropTypes.func,
|
||||||
|
addTokens: PropTypes.func,
|
||||||
|
pendingTokens: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { pendingTokens = {}, history } = this.props
|
||||||
|
|
||||||
|
if (Object.keys(pendingTokens).length === 0) {
|
||||||
|
history.push(DEFAULT_ROUTE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenName (name, symbol) {
|
||||||
|
return typeof name === 'undefined'
|
||||||
|
? symbol
|
||||||
|
: `${name} (${symbol})`
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { history, addTokens, clearPendingTokens, pendingTokens } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container">
|
||||||
|
<div className="page-container__header">
|
||||||
|
<div className="page-container__title">
|
||||||
|
{ this.context.t('addTokens') }
|
||||||
|
</div>
|
||||||
|
<div className="page-container__subtitle">
|
||||||
|
{ this.context.t('likeToAddTokens') }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="page-container__content">
|
||||||
|
<div className="confirm-add-token">
|
||||||
|
<div className="confirm-add-token__header">
|
||||||
|
<div className="confirm-add-token__token">
|
||||||
|
{ this.context.t('token') }
|
||||||
|
</div>
|
||||||
|
<div className="confirm-add-token__balance">
|
||||||
|
{ this.context.t('balance') }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="confirm-add-token__token-list">
|
||||||
|
{
|
||||||
|
Object.entries(pendingTokens)
|
||||||
|
.map(([ address, token ]) => {
|
||||||
|
const { name, symbol } = token
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="confirm-add-token__token-list-item"
|
||||||
|
key={address}
|
||||||
|
>
|
||||||
|
<div className="confirm-add-token__token confirm-add-token__data">
|
||||||
|
<Identicon
|
||||||
|
className="confirm-add-token__token-icon"
|
||||||
|
diameter={48}
|
||||||
|
address={address}
|
||||||
|
/>
|
||||||
|
<div className="confirm-add-token__name">
|
||||||
|
{ this.getTokenName(name, symbol) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="confirm-add-token__balance">
|
||||||
|
<TokenBalance token={token} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="page-container__footer">
|
||||||
|
<Button
|
||||||
|
type="secondary"
|
||||||
|
large
|
||||||
|
className="page-container__footer-button"
|
||||||
|
onClick={() => history.push(ADD_TOKEN_ROUTE)}
|
||||||
|
>
|
||||||
|
{ this.context.t('back') }
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
large
|
||||||
|
className="page-container__footer-button"
|
||||||
|
onClick={() => {
|
||||||
|
addTokens(pendingTokens)
|
||||||
|
.then(() => {
|
||||||
|
clearPendingTokens()
|
||||||
|
history.push(DEFAULT_ROUTE)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ this.context.t('addTokens') }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { connect } from 'react-redux'
|
||||||
|
import ConfirmAddToken from './confirm-add-token.component'
|
||||||
|
|
||||||
|
const { addTokens, clearPendingTokens } = require('../../../actions')
|
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => {
|
||||||
|
const { pendingTokens } = metamask
|
||||||
|
return {
|
||||||
|
pendingTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
addTokens: tokens => dispatch(addTokens(tokens)),
|
||||||
|
clearPendingTokens: () => dispatch(clearPendingTokens()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)
|
2
ui/app/components/pages/confirm-add-token/index.js
Normal file
2
ui/app/components/pages/confirm-add-token/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import ConfirmAddToken from './confirm-add-token.container'
|
||||||
|
module.exports = ConfirmAddToken
|
69
ui/app/components/pages/confirm-add-token/index.scss
Normal file
69
ui/app/components/pages/confirm-add-token/index.scss
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
.confirm-add-token {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
font-size: .75rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__balance {
|
||||||
|
flex: 0 0 30%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
|
||||||
|
.token-balance {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
color: $scorpion;
|
||||||
|
font-size: 43px;
|
||||||
|
line-height: 43px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__symbol {
|
||||||
|
color: $scorpion;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-list-item {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__data {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__token-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
import TokenBalance from './token-balance.container'
|
||||||
|
module.exports = TokenBalance
|
@ -0,0 +1,16 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default class TokenBalance extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
string: PropTypes.string,
|
||||||
|
symbol: PropTypes.string,
|
||||||
|
error: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className="hide-text-overflow">{ this.props.string }</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { compose } from 'recompose'
|
||||||
|
import withTokenTracker from '../../../../helpers/with-token-tracker'
|
||||||
|
import TokenBalance from './token-balance.component'
|
||||||
|
import selectors from '../../../../selectors'
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
userAddress: selectors.getSelectedAddress(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps),
|
||||||
|
withTokenTracker
|
||||||
|
)(TokenBalance)
|
5
ui/app/components/pages/index.scss
Normal file
5
ui/app/components/pages/index.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import './unlock-page/index';
|
||||||
|
|
||||||
|
@import './add-token/index';
|
||||||
|
|
||||||
|
@import './confirm-add-token/index';
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Button from 'material-ui/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
import TextField from '../../text-field'
|
import TextField from '../../text-field'
|
||||||
|
|
||||||
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
|
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
|
||||||
@ -129,6 +129,7 @@ class UnlockPage extends Component {
|
|||||||
error={error}
|
error={error}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
material
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
@ -115,7 +115,7 @@ SignatureRequest.prototype.renderBalance = function () {
|
|||||||
|
|
||||||
return h('div.request-signature__balance', [
|
return h('div.request-signature__balance', [
|
||||||
|
|
||||||
h('div.request-signature__balance-text', [this.context.t('balance')]),
|
h('div.request-signature__balance-text', `${this.context.t('balance')}:`),
|
||||||
|
|
||||||
h('div.request-signature__balance-value', `${balanceInEther} ETH`),
|
h('div.request-signature__balance-value', `${balanceInEther} ETH`),
|
||||||
|
|
||||||
|
@ -1,59 +1,100 @@
|
|||||||
import React from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { withStyles } from 'material-ui/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import { default as MaterialTextField } from 'material-ui/TextField'
|
import { default as MaterialTextField } from '@material-ui/core/TextField'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
cssLabel: {
|
materialLabel: {
|
||||||
'&$cssFocused': {
|
'&$materialFocused': {
|
||||||
color: '#aeaeae',
|
color: '#aeaeae',
|
||||||
},
|
},
|
||||||
'&$cssError': {
|
'&$materialError': {
|
||||||
color: '#aeaeae',
|
color: '#aeaeae',
|
||||||
},
|
},
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
color: '#aeaeae',
|
color: '#aeaeae',
|
||||||
},
|
},
|
||||||
cssFocused: {},
|
materialFocused: {},
|
||||||
cssUnderline: {
|
materialUnderline: {
|
||||||
'&:after': {
|
'&:after': {
|
||||||
backgroundColor: '#f7861c',
|
borderBottom: '2px solid #f7861c',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cssError: {},
|
materialError: {},
|
||||||
|
// Non-material styles
|
||||||
|
formLabel: {
|
||||||
|
'&$formLabelFocused': {
|
||||||
|
color: '#5b5b5b',
|
||||||
|
},
|
||||||
|
'&$materialError': {
|
||||||
|
color: '#5b5b5b',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formLabelFocused: {},
|
||||||
|
inputRoot: {
|
||||||
|
'label + &': {
|
||||||
|
marginTop: '8px',
|
||||||
|
},
|
||||||
|
border: '1px solid #d2d8dd',
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '0 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
'&:focus': {
|
||||||
|
border: '1px solid #2f9ae0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: '.75rem',
|
||||||
|
transform: 'none',
|
||||||
|
transition: 'none',
|
||||||
|
position: 'initial',
|
||||||
|
color: '#5b5b5b',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextField = props => {
|
class TextField extends Component {
|
||||||
const { error, classes, ...textFieldProps } = props
|
static defaultProps = {
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
error: PropTypes.string,
|
||||||
|
classes: PropTypes.object,
|
||||||
|
material: PropTypes.bool,
|
||||||
|
startAdornment: PropTypes.element,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { error, classes, material, startAdornment, ...textFieldProps } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaterialTextField
|
<MaterialTextField
|
||||||
error={Boolean(error)}
|
error={Boolean(error)}
|
||||||
helperText={error}
|
helperText={error}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
|
shrink: material ? undefined : true,
|
||||||
|
className: material ? '' : classes.inputLabel,
|
||||||
FormLabelClasses: {
|
FormLabelClasses: {
|
||||||
root: classes.cssLabel,
|
root: material ? classes.materialLabel : classes.formLabel,
|
||||||
focused: classes.cssFocused,
|
focused: material ? classes.materialFocused : classes.formLabelFocused,
|
||||||
error: classes.cssError,
|
error: classes.materialError,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
|
startAdornment: startAdornment || undefined,
|
||||||
|
disableUnderline: !material,
|
||||||
classes: {
|
classes: {
|
||||||
underline: classes.cssUnderline,
|
root: material ? '' : classes.inputRoot,
|
||||||
|
input: material ? '' : classes.input,
|
||||||
|
underline: material ? classes.materialUnderline : '',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField.defaultProps = {
|
|
||||||
error: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField.propTypes = {
|
|
||||||
error: PropTypes.string,
|
|
||||||
classes: PropTypes.object,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(TextField)
|
export default withStyles(styles)(TextField)
|
||||||
|
@ -1,461 +0,0 @@
|
|||||||
.add-token {
|
|
||||||
width: 498px;
|
|
||||||
max-height: 805px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
position: relative;
|
|
||||||
z-index: 12;
|
|
||||||
font-family: 'Roboto';
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
background-color: $white;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
align-items: center;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
padding: 20px 20px 0px;
|
|
||||||
border-bottom: 1px solid $geyser;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
&__cancel {
|
|
||||||
color: $dodger-blue;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 21px;
|
|
||||||
margin-left: 8px;
|
|
||||||
cursor:pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $tundora;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tabs {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&__tab {
|
|
||||||
height: 54px;
|
|
||||||
padding: 15px 10px;
|
|
||||||
color: $dusty-gray;
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tab:first-of-type {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__unselected:hover {
|
|
||||||
color: $black;
|
|
||||||
border-bottom: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__selected {
|
|
||||||
color: $curious-blue;
|
|
||||||
border-bottom: 3px solid $curious-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info-box {
|
|
||||||
height: 96px;
|
|
||||||
margin: 20px 20px 0px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $alabaster;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 18px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
|
|
||||||
&__close::after {
|
|
||||||
content: '\00D7';
|
|
||||||
font-size: 29px;
|
|
||||||
font-weight: 200;
|
|
||||||
color: $dusty-gray;
|
|
||||||
position: absolute;
|
|
||||||
right: 17px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
color: $mid-gray;
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__copy,
|
|
||||||
&__copy--blue {
|
|
||||||
color: $mid-gray;
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__copy--blue {
|
|
||||||
color: $curious-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__description {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__description + &__description {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-description {
|
|
||||||
font-weight: 400;
|
|
||||||
margin: 20px 0 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input-container {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__search-input-error-message {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -10px;
|
|
||||||
left: 22px;
|
|
||||||
font-size: 12px;
|
|
||||||
width: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: $red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__input,
|
|
||||||
&__add-custom-input {
|
|
||||||
height: 54px;
|
|
||||||
padding: 0px 20px;
|
|
||||||
border: 1px solid $geyser;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 22px 24px;
|
|
||||||
position: relative;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
color: $scorpion;
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $scorpion;
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 21px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footers {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom {
|
|
||||||
color: $scorpion;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 24px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 12px 0;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, .05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: rgba(0, 0, 0, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa {
|
|
||||||
position: absolute;
|
|
||||||
right: 24px;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom-form {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
margin: 40px 0 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom-field {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
|
|
||||||
&--error {
|
|
||||||
.add-token__add-custom-input {
|
|
||||||
border-color: $red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom-error-message {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1px;
|
|
||||||
left: 22px;
|
|
||||||
font-size: 12px;
|
|
||||||
width: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: $red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom-label {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 21px;
|
|
||||||
margin-left: 22px;
|
|
||||||
color: $scorpion;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom-input {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $silver;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__add-custom-field + &__add-custom-field {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-bottom: 30px;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirm-button,
|
|
||||||
&__cancel-button {
|
|
||||||
margin: 0 12px;
|
|
||||||
padding: 10px 13px;
|
|
||||||
height: 54px;
|
|
||||||
width: 133px;
|
|
||||||
margin-right: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-icons-title {
|
|
||||||
color: #5B5D67;
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
margin-left: 24px;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-icons-container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-wrapper {
|
|
||||||
transition: 200ms ease-in-out;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
flex: 0 0 42.5%;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
margin: 0% 2.5% 1.5%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 2px solid rgba($malibu-blue, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
border: 2px solid $malibu-blue !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--disabled {
|
|
||||||
opacity: .4;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-data {
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-name {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-symbol {
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 29px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-icon {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: $white;
|
|
||||||
box-shadow: 0 2px 4px 0 rgba($black, .24);
|
|
||||||
margin-right: 12px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-message {
|
|
||||||
position: absolute;
|
|
||||||
color: $caribbean-green;
|
|
||||||
font-size: 11px;
|
|
||||||
bottom: 0;
|
|
||||||
left: 85px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-token-list {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
|
|
||||||
.token-balance {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
&__amount {
|
|
||||||
color: $scorpion;
|
|
||||||
font-size: 43px;
|
|
||||||
line-height: 43px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__symbol {
|
|
||||||
color: $scorpion;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-title {
|
|
||||||
padding: 30px 120px 12px;
|
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) {
|
|
||||||
padding: 20px 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-content {
|
|
||||||
padding-bottom: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-token-list-item {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
margin: 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-token-list-item + &__confirmation-token-list-item {
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__confirmation-token-icon {
|
|
||||||
margin-right: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) {
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
box-shadow: none !important;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footers {
|
|
||||||
border-bottom: 1px solid $gallery;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-icon {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-symbol {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__token-name {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
border-top: 1px solid $gallery;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,8 +15,9 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: border-color .3s ease;
|
transition: border-color .3s ease;
|
||||||
padding: 0 20px;
|
padding: 0 16px;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
|
width: 100%;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -110,6 +111,7 @@
|
|||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
transition: border-color .3s ease;
|
transition: border-color .3s ease;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: $scorpion;
|
border-color: $scorpion;
|
||||||
@ -126,6 +128,7 @@
|
|||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
transition: border-color .3s ease;
|
transition: border-color .3s ease;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No longer used in flat design, remove when modal buttons done
|
// No longer used in flat design, remove when modal buttons done
|
||||||
|
@ -30,8 +30,6 @@
|
|||||||
|
|
||||||
@import './token-list.scss';
|
@import './token-list.scss';
|
||||||
|
|
||||||
@import './add-token.scss';
|
|
||||||
|
|
||||||
@import './currency-display.scss';
|
@import './currency-display.scss';
|
||||||
|
|
||||||
@import './account-menu.scss';
|
@import './account-menu.scss';
|
||||||
@ -62,4 +60,4 @@
|
|||||||
|
|
||||||
@import './sender-to-recipient.scss';
|
@import './sender-to-recipient.scss';
|
||||||
|
|
||||||
@import '../../../components/export-text-container/export-text-container.scss';
|
@import '../../../components/index';
|
||||||
|
@ -144,8 +144,8 @@ $wallet-view-bg: $alabaster;
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin: 36px auto;
|
margin: 36px auto;
|
||||||
background: none;
|
background: none;
|
||||||
padding: .7rem 2rem;
|
|
||||||
transition: border-color .3s ease;
|
transition: border-color .3s ease;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: $curious-blue;
|
border-color: $curious-blue;
|
||||||
|
@ -1,3 +1 @@
|
|||||||
@import './reveal-seed.scss';
|
@import './reveal-seed.scss';
|
||||||
|
|
||||||
@import '../../../../components/pages/unlock-page/unlock-page.scss';
|
|
||||||
|
@ -74,28 +74,32 @@ input.large-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-container {
|
.page-container {
|
||||||
width: 400px;
|
width: 408px;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
|
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
|
||||||
z-index: 25;
|
z-index: 25;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
border-bottom: 1px solid $geyser;
|
border-bottom: 1px solid $geyser;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&--no-padding-bottom {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header-close {
|
&__header-close {
|
||||||
color: $tundora;
|
color: $tundora;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 16px;
|
||||||
right: 20px;
|
right: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -117,7 +121,7 @@ input.large-input {
|
|||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-top: 1px solid $geyser;
|
border-top: 1px solid $geyser;
|
||||||
padding: 1.6rem;
|
padding: 16px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
.btn-clear,
|
.btn-clear,
|
||||||
@ -128,11 +132,10 @@ input.large-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__footer-button {
|
&__footer-button {
|
||||||
width: 165px;
|
|
||||||
height: 55px;
|
height: 55px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-right: 1.2rem;
|
margin-right: 16px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
@ -162,25 +165,20 @@ input.large-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__tabs {
|
&__tabs {
|
||||||
padding: 0 1.3rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__tab {
|
&__tab {
|
||||||
min-width: 5rem;
|
min-width: 5rem;
|
||||||
padding: .2rem .8rem .9rem;
|
padding: 8px;
|
||||||
color: $dusty-gray;
|
color: $dusty-gray;
|
||||||
font-family: Roboto;
|
font-family: Roboto;
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
line-height: initial;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
margin-right: 1rem;
|
margin-right: 16px;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
@ -189,10 +187,6 @@ input.large-input {
|
|||||||
&--selected {
|
&--selected {
|
||||||
color: $curious-blue;
|
color: $curious-blue;
|
||||||
border-bottom: 3px solid $curious-blue;
|
border-bottom: 3px solid $curious-blue;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $curious-blue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +254,8 @@ input.large-input {
|
|||||||
|
|
||||||
@media screen and (min-width: 576px) {
|
@media screen and (min-width: 576px) {
|
||||||
.page-container {
|
.page-container {
|
||||||
height: 600px;
|
max-height: 82vh;
|
||||||
|
min-height: 570px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,3 +298,9 @@ input.form-control {
|
|||||||
border: 1px solid $monzo;
|
border: 1px solid $monzo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-text-overflow {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
105
ui/app/helpers/with-token-tracker.js
Normal file
105
ui/app/helpers/with-token-tracker.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import TokenTracker from 'eth-token-tracker'
|
||||||
|
|
||||||
|
const withTokenTracker = WrappedComponent => {
|
||||||
|
return class TokenTrackerWrappedComponent extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
userAddress: PropTypes.string.isRequired,
|
||||||
|
token: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
string: '',
|
||||||
|
symbol: '',
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tracker = null
|
||||||
|
this.updateBalance = this.updateBalance.bind(this)
|
||||||
|
this.setError = this.setError.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.createFreshTokenTracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props
|
||||||
|
const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps
|
||||||
|
|
||||||
|
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createFreshTokenTracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.removeListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
createFreshTokenTracker () {
|
||||||
|
this.removeListeners()
|
||||||
|
|
||||||
|
if (!global.ethereumProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userAddress, token } = this.props
|
||||||
|
|
||||||
|
this.tracker = new TokenTracker({
|
||||||
|
userAddress,
|
||||||
|
provider: global.ethereumProvider,
|
||||||
|
tokens: [token],
|
||||||
|
pollingInterval: 8000,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.tracker.on('update', this.updateBalance)
|
||||||
|
this.tracker.on('error', this.setError)
|
||||||
|
|
||||||
|
this.tracker.updateBalances()
|
||||||
|
.then(() => this.updateBalance(this.tracker.serialize()))
|
||||||
|
.catch(error => this.setState({ error: error.message }))
|
||||||
|
}
|
||||||
|
|
||||||
|
setError (error) {
|
||||||
|
this.setState({ error })
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBalance (tokens = []) {
|
||||||
|
const [{ string, symbol }] = tokens
|
||||||
|
this.setState({ string, symbol, error: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListeners () {
|
||||||
|
if (this.tracker) {
|
||||||
|
this.tracker.stop()
|
||||||
|
this.tracker.removeListener('update', this.updateBalance)
|
||||||
|
this.tracker.removeListener('error', this.setError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { string, symbol, error } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WrappedComponent
|
||||||
|
{ ...this.props }
|
||||||
|
string={string}
|
||||||
|
symbol={symbol}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withTokenTracker
|
@ -28,6 +28,7 @@ function reduceMetamask (state, action) {
|
|||||||
contractExchangeRates: {},
|
contractExchangeRates: {},
|
||||||
tokenExchangeRates: {},
|
tokenExchangeRates: {},
|
||||||
tokens: [],
|
tokens: [],
|
||||||
|
pendingTokens: {},
|
||||||
send: {
|
send: {
|
||||||
gasLimit: null,
|
gasLimit: null,
|
||||||
gasPrice: null,
|
gasPrice: null,
|
||||||
@ -356,6 +357,17 @@ function reduceMetamask (state, action) {
|
|||||||
currentLocale: action.value,
|
currentLocale: action.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
case actions.SET_PENDING_TOKENS:
|
||||||
|
return extend(metamaskState, {
|
||||||
|
pendingTokens: { ...action.payload },
|
||||||
|
})
|
||||||
|
|
||||||
|
case actions.CLEAR_PENDING_TOKENS: {
|
||||||
|
return extend(metamaskState, {
|
||||||
|
pendingTokens: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return metamaskState
|
return metamaskState
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ const REVEAL_SEED_ROUTE = '/seed'
|
|||||||
const CONFIRM_SEED_ROUTE = '/confirm-seed'
|
const CONFIRM_SEED_ROUTE = '/confirm-seed'
|
||||||
const RESTORE_VAULT_ROUTE = '/restore-vault'
|
const RESTORE_VAULT_ROUTE = '/restore-vault'
|
||||||
const ADD_TOKEN_ROUTE = '/add-token'
|
const ADD_TOKEN_ROUTE = '/add-token'
|
||||||
|
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
|
||||||
const NEW_ACCOUNT_ROUTE = '/new-account'
|
const NEW_ACCOUNT_ROUTE = '/new-account'
|
||||||
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
|
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
|
||||||
const SEND_ROUTE = '/send'
|
const SEND_ROUTE = '/send'
|
||||||
@ -31,6 +32,7 @@ module.exports = {
|
|||||||
CONFIRM_SEED_ROUTE,
|
CONFIRM_SEED_ROUTE,
|
||||||
RESTORE_VAULT_ROUTE,
|
RESTORE_VAULT_ROUTE,
|
||||||
ADD_TOKEN_ROUTE,
|
ADD_TOKEN_ROUTE,
|
||||||
|
CONFIRM_ADD_TOKEN_ROUTE,
|
||||||
NEW_ACCOUNT_ROUTE,
|
NEW_ACCOUNT_ROUTE,
|
||||||
IMPORT_ACCOUNT_ROUTE,
|
IMPORT_ACCOUNT_ROUTE,
|
||||||
SEND_ROUTE,
|
SEND_ROUTE,
|
||||||
|
Loading…
Reference in New Issue
Block a user