mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Merge pull request #4308 from MetaMask/i4232-addtoken
Update designs for Add Token screen
This commit is contained in:
commit
b5bbfd3264
@ -23,6 +23,9 @@
|
||||
"addTokens": {
|
||||
"message": "Add Tokens"
|
||||
},
|
||||
"addAcquiredTokens": {
|
||||
"message": "Add the tokens you've acquired using MetaMask"
|
||||
},
|
||||
"amount": {
|
||||
"message": "Amount"
|
||||
},
|
||||
@ -53,7 +56,7 @@
|
||||
"message": "Back"
|
||||
},
|
||||
"balance": {
|
||||
"message": "Balance:"
|
||||
"message": "Balance"
|
||||
},
|
||||
"balances": {
|
||||
"message": "Token balance(s)"
|
||||
@ -717,6 +720,9 @@
|
||||
"search": {
|
||||
"message": "Search"
|
||||
},
|
||||
"searchResults": {
|
||||
"message": "Search Results"
|
||||
},
|
||||
"secretPhrase": {
|
||||
"message": "Enter your secret twelve word phrase here to restore your vault."
|
||||
},
|
||||
@ -832,6 +838,9 @@
|
||||
"message": "$1 to ETH via ShapeShift",
|
||||
"description": "system will fill in deposit type in start of message"
|
||||
},
|
||||
"token": {
|
||||
"message": "Token"
|
||||
},
|
||||
"tokenAddress": {
|
||||
"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/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": {
|
||||
"version": "7.0.0-beta.31",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz",
|
||||
@ -182,6 +191,74 @@
|
||||
"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": {
|
||||
"version": "1.30.3",
|
||||
"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": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.5.tgz",
|
||||
@ -17752,78 +17820,6 @@
|
||||
"integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=",
|
||||
"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": {
|
||||
"version": "1.2.17",
|
||||
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
|
||||
@ -25056,8 +25052,7 @@
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz",
|
||||
"integrity": "sha512-pbZOSMVVkvppW7XRn9fcHK5OgEDnYLwMva7P6TgS44/SN9uGGjfh3Z1c8tomO+y4IsHQ6Fsz2EGwmE7sMeNZgQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-pbZOSMVVkvppW7XRn9fcHK5OgEDnYLwMva7P6TgS44/SN9uGGjfh3Z1c8tomO+y4IsHQ6Fsz2EGwmE7sMeNZgQ=="
|
||||
},
|
||||
"react-markdown": {
|
||||
"version": "3.1.4",
|
||||
|
@ -63,6 +63,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^1.0.0",
|
||||
"abi-decoder": "^1.0.9",
|
||||
"asmcrypto.js": "0.22.0",
|
||||
"async": "^2.5.0",
|
||||
@ -136,7 +137,6 @@
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"loglevel": "^1.4.1",
|
||||
"material-ui": "1.0.0-beta.44",
|
||||
"metamascara": "^2.0.0",
|
||||
"metamask-logo": "^2.1.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
|
@ -6,6 +6,9 @@ module.exports = function(config) {
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: process.cwd(),
|
||||
|
||||
// Uncomment to allow for longer timeouts
|
||||
// browserNoActivityTimeout: 100000000,
|
||||
|
||||
browserConsoleLogOptions: {
|
||||
terminal: false,
|
||||
},
|
||||
|
@ -22,6 +22,11 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
selectState.val('add token')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
// Used to set values on TextField input component
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype, 'value'
|
||||
).set
|
||||
|
||||
// Check that no tokens have been added
|
||||
assert.ok($('.token-list-item').length === 0, 'no tokens added')
|
||||
|
||||
@ -31,14 +36,14 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
addTokenButton[0].click()
|
||||
|
||||
// Verify Add Token screen
|
||||
let addTokenWrapper = await queryAsync($, '.add-token__wrapper')
|
||||
let addTokenWrapper = await queryAsync($, '.page-container')
|
||||
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
|
||||
|
||||
let addTokenTitle = await queryAsync($, '.add-token__header__title')
|
||||
let addTokenTitle = await queryAsync($, '.page-container__title')
|
||||
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
|
||||
|
||||
// Cancel Add Token
|
||||
const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.add-token__cancel-button')
|
||||
const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.page-container__footer-button')
|
||||
assert.ok(cancelAddTokenButton[0], 'cancel add token button present')
|
||||
cancelAddTokenButton.click()
|
||||
|
||||
@ -50,20 +55,22 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
addTokenButton[0].click()
|
||||
|
||||
// Verify Add Token Screen
|
||||
addTokenWrapper = await queryAsync($, '.add-token__wrapper')
|
||||
addTokenTitle = await queryAsync($, '.add-token__header__title')
|
||||
addTokenWrapper = await queryAsync($, '.page-container')
|
||||
addTokenTitle = await queryAsync($, '.page-container__title')
|
||||
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
|
||||
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
|
||||
|
||||
// Search for token
|
||||
const searchInput = await queryAsync($, 'input.add-token__input')
|
||||
searchInput.val('a')
|
||||
reactTriggerChange(searchInput[0])
|
||||
const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0]
|
||||
searchInput.focus()
|
||||
await timeout(1000)
|
||||
nativeInputValueSetter.call(searchInput, 'a')
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true}))
|
||||
|
||||
// Click token to add
|
||||
const tokenWrapper = await queryAsync($, 'div.add-token__token-wrapper')
|
||||
const tokenWrapper = await queryAsync($, 'div.token-list__token')
|
||||
assert.ok(tokenWrapper[0], 'token found')
|
||||
const tokenImageProp = tokenWrapper.find('.add-token__token-icon').css('background-image')
|
||||
const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image')
|
||||
const tokenImageUrl = tokenImageProp.slice(5, -2)
|
||||
tokenWrapper[0].click()
|
||||
|
||||
@ -73,11 +80,8 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
nextButton[0].click()
|
||||
|
||||
// Confirm Add token
|
||||
assert.equal(
|
||||
$('.add-token__description')[0].textContent,
|
||||
'Token balance(s)',
|
||||
'confirm add token rendered'
|
||||
)
|
||||
const confirmAddToken = await queryAsync($, '.confirm-add-token')
|
||||
assert.ok(confirmAddToken[0], 'confirm add token rendered')
|
||||
assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
|
||||
$('button.btn-primary--lg')[0].click()
|
||||
|
||||
@ -91,39 +95,46 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
assert.ok(addTokenButton[0], 'add token button present')
|
||||
addTokenButton[0].click()
|
||||
|
||||
const addTokenTabs = await queryAsync($, '.add-token__header__tabs__tab')
|
||||
addTokenWrapper = await queryAsync($, '.page-container')
|
||||
const addTokenTabs = await queryAsync($, '.page-container__tab')
|
||||
assert.equal(addTokenTabs.length, 2, 'expected number of tabs')
|
||||
assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present')
|
||||
assert.ok(addTokenTabs[1], 'add custom token tab present')
|
||||
addTokenTabs[1].click()
|
||||
await timeout(1000)
|
||||
|
||||
// Input token contract address
|
||||
const customInput = await queryAsync($, 'input.add-token__add-custom-input')
|
||||
customInput.val('0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
|
||||
reactTriggerChange(customInput[0])
|
||||
const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0]
|
||||
customInput.focus()
|
||||
await timeout(1000)
|
||||
nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
|
||||
customInput.dispatchEvent(new Event('input', { bubbles: true}))
|
||||
|
||||
|
||||
// Click Next button
|
||||
nextButton = await queryAsync($, 'button.btn-primary--lg')
|
||||
assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
|
||||
nextButton[0].click()
|
||||
// nextButton = await queryAsync($, 'button.btn-primary--lg')
|
||||
// assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
|
||||
// nextButton[0].click()
|
||||
|
||||
// Verify symbol length error since contract address won't return symbol
|
||||
const errorMessage = await queryAsync($, '.add-token__add-custom-error-message')
|
||||
// // Verify symbol length error since contract address won't return symbol
|
||||
const errorMessage = await queryAsync($, '#custom-symbol-helper-text')
|
||||
assert.ok(errorMessage[0], 'error rendered')
|
||||
|
||||
$('button.btn-secondary--lg')[0].click()
|
||||
|
||||
// // Confirm Add token
|
||||
// await timeout(100000)
|
||||
|
||||
// Confirm Add token
|
||||
// assert.equal(
|
||||
// $('.add-token__description')[0].textContent,
|
||||
// $('.page-container__subtitle')[0].textContent,
|
||||
// 'Would you like to add these tokens?',
|
||||
// 'confirm add token rendered'
|
||||
// )
|
||||
// assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
|
||||
// $('button.btn-primary--lg')[0].click()
|
||||
|
||||
// // Verify added token image
|
||||
// heroBalance = await queryAsync($, '.hero-balance')
|
||||
// assert.ok(heroBalance, 'rendered hero balance')
|
||||
// assert.ok(heroBalance.find('.identicon')[0], 'token added')
|
||||
// Verify added token image
|
||||
heroBalance = await queryAsync($, '.hero-balance')
|
||||
assert.ok(heroBalance, 'rendered hero balance')
|
||||
assert.ok(heroBalance.find('.identicon')[0], 'token added')
|
||||
}
|
||||
|
@ -275,6 +275,10 @@ var actions = {
|
||||
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
|
||||
|
||||
retryTransaction,
|
||||
SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
|
||||
CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
|
||||
setPendingTokens,
|
||||
clearPendingTokens,
|
||||
}
|
||||
|
||||
module.exports = actions
|
||||
@ -1929,3 +1933,22 @@ function updateNetworkEndpointType (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 RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||
const AddTokenPage = require('./components/pages/add-token')
|
||||
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
|
||||
const CreateAccountPage = require('./components/pages/create-account')
|
||||
const NoticeScreen = require('./components/pages/notice')
|
||||
|
||||
@ -47,6 +48,7 @@ const {
|
||||
REVEAL_SEED_ROUTE,
|
||||
RESTORE_VAULT_ROUTE,
|
||||
ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_TOKEN_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
SEND_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
@ -77,6 +79,7 @@ class App extends Component {
|
||||
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
|
||||
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: DEFAULT_ROUTE, exact, component: Home }),
|
||||
])
|
||||
|
@ -1,7 +1,6 @@
|
||||
const { Component } = require('react')
|
||||
const h = require('react-hyperscript')
|
||||
const PropTypes = require('prop-types')
|
||||
const classnames = require('classnames')
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const SECONDARY = 'secondary'
|
||||
const CLASSNAME_PRIMARY = 'btn-primary'
|
||||
@ -24,10 +23,12 @@ class Button extends Component {
|
||||
const { type, large, className, ...buttonProps } = this.props
|
||||
|
||||
return (
|
||||
h('button', {
|
||||
className: classnames(getClassName(type, large), className),
|
||||
...buttonProps,
|
||||
}, this.props.children)
|
||||
<button
|
||||
className={classnames(getClassName(type, large), className)}
|
||||
{ ...buttonProps }
|
||||
>
|
||||
{ this.props.children }
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -39,5 +40,5 @@ Button.propTypes = {
|
||||
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
|
||||
|
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')),
|
||||
]),
|
||||
])
|
||||
}
|
351
ui/app/components/pages/add-token/add-token.component.js
Normal file
351
ui/app/components/pages/add-token/add-token.component.js
Normal file
@ -0,0 +1,351 @@
|
||||
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 = 0 } = await this.tokenInfoGetter(address)
|
||||
|
||||
const autoFilled = Boolean(symbol && decimals)
|
||||
this.setState({ autoFilled })
|
||||
this.handleCustomSymbolChange(symbol || '')
|
||||
this.handleCustomDecimalsChange(decimals)
|
||||
}
|
||||
|
||||
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 PropTypes from 'prop-types'
|
||||
import Button from 'material-ui/Button'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import TextField from '../../text-field'
|
||||
|
||||
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
|
||||
@ -129,6 +129,7 @@ class UnlockPage extends Component {
|
||||
error={error}
|
||||
autoFocus
|
||||
autoComplete="current-password"
|
||||
material
|
||||
fullWidth
|
||||
/>
|
||||
</form>
|
||||
|
@ -115,7 +115,7 @@ SignatureRequest.prototype.renderBalance = function () {
|
||||
|
||||
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`),
|
||||
|
||||
|
@ -1,59 +1,102 @@
|
||||
import React from 'react'
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { withStyles } from 'material-ui/styles'
|
||||
import { default as MaterialTextField } from 'material-ui/TextField'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { default as MaterialTextField } from '@material-ui/core/TextField'
|
||||
|
||||
const styles = {
|
||||
cssLabel: {
|
||||
'&$cssFocused': {
|
||||
materialLabel: {
|
||||
'&$materialFocused': {
|
||||
color: '#aeaeae',
|
||||
},
|
||||
'&$cssError': {
|
||||
'&$materialError': {
|
||||
color: '#aeaeae',
|
||||
},
|
||||
fontWeight: '400',
|
||||
color: '#aeaeae',
|
||||
},
|
||||
cssFocused: {},
|
||||
cssUnderline: {
|
||||
materialFocused: {},
|
||||
materialUnderline: {
|
||||
'&:after': {
|
||||
backgroundColor: '#f7861c',
|
||||
borderBottom: '2px solid #f7861c',
|
||||
},
|
||||
},
|
||||
cssError: {},
|
||||
materialError: {},
|
||||
// Non-material styles
|
||||
formLabel: {
|
||||
'&$formLabelFocused': {
|
||||
color: '#5b5b5b',
|
||||
},
|
||||
'&$materialError': {
|
||||
color: '#5b5b5b',
|
||||
},
|
||||
},
|
||||
formLabelFocused: {},
|
||||
inputFocused: {},
|
||||
inputRoot: {
|
||||
'label + &': {
|
||||
marginTop: '8px',
|
||||
},
|
||||
border: '1px solid #d2d8dd',
|
||||
height: '48px',
|
||||
borderRadius: '4px',
|
||||
padding: '0 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'&$inputFocused': {
|
||||
border: '1px solid #2f9ae0',
|
||||
},
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: '.75rem',
|
||||
transform: 'none',
|
||||
transition: 'none',
|
||||
position: 'initial',
|
||||
color: '#5b5b5b',
|
||||
},
|
||||
}
|
||||
|
||||
const TextField = props => {
|
||||
const { error, classes, ...textFieldProps } = props
|
||||
class TextField extends Component {
|
||||
static defaultProps = {
|
||||
error: null,
|
||||
}
|
||||
|
||||
return (
|
||||
<MaterialTextField
|
||||
error={Boolean(error)}
|
||||
helperText={error}
|
||||
InputLabelProps={{
|
||||
FormLabelClasses: {
|
||||
root: classes.cssLabel,
|
||||
focused: classes.cssFocused,
|
||||
error: classes.cssError,
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
underline: classes.cssUnderline,
|
||||
},
|
||||
}}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
static propTypes = {
|
||||
error: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
material: PropTypes.bool,
|
||||
startAdornment: PropTypes.element,
|
||||
}
|
||||
|
||||
TextField.defaultProps = {
|
||||
error: null,
|
||||
}
|
||||
render () {
|
||||
const { error, classes, material, startAdornment, ...textFieldProps } = this.props
|
||||
|
||||
TextField.propTypes = {
|
||||
error: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
return (
|
||||
<MaterialTextField
|
||||
error={Boolean(error)}
|
||||
helperText={error}
|
||||
InputLabelProps={{
|
||||
shrink: material ? undefined : true,
|
||||
className: material ? '' : classes.inputLabel,
|
||||
FormLabelClasses: {
|
||||
root: material ? classes.materialLabel : classes.formLabel,
|
||||
focused: material ? classes.materialFocused : classes.formLabelFocused,
|
||||
error: classes.materialError,
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: startAdornment || undefined,
|
||||
disableUnderline: !material,
|
||||
classes: {
|
||||
root: material ? '' : classes.inputRoot,
|
||||
input: material ? '' : classes.input,
|
||||
underline: material ? classes.materialUnderline : '',
|
||||
focused: material ? '' : classes.inputFocused,
|
||||
},
|
||||
}}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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-weight: 500;
|
||||
transition: border-color .3s ease;
|
||||
padding: 0 20px;
|
||||
padding: 0 16px;
|
||||
min-width: 140px;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
outline: none;
|
||||
}
|
||||
@ -110,6 +111,7 @@
|
||||
font-size: .85rem;
|
||||
font-weight: 400;
|
||||
transition: border-color .3s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: $scorpion;
|
||||
@ -126,6 +128,7 @@
|
||||
font-size: .85rem;
|
||||
font-weight: 400;
|
||||
transition: border-color .3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// No longer used in flat design, remove when modal buttons done
|
||||
|
@ -30,8 +30,6 @@
|
||||
|
||||
@import './token-list.scss';
|
||||
|
||||
@import './add-token.scss';
|
||||
|
||||
@import './currency-display.scss';
|
||||
|
||||
@import './account-menu.scss';
|
||||
@ -62,4 +60,4 @@
|
||||
|
||||
@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;
|
||||
margin: 36px auto;
|
||||
background: none;
|
||||
padding: .7rem 2rem;
|
||||
transition: border-color .3s ease;
|
||||
width: 150px;
|
||||
|
||||
&:hover {
|
||||
border-color: $curious-blue;
|
||||
|
@ -1,3 +1 @@
|
||||
@import './reveal-seed.scss';
|
||||
|
||||
@import '../../../../components/pages/unlock-page/unlock-page.scss';
|
||||
|
@ -74,28 +74,32 @@ input.large-input {
|
||||
}
|
||||
|
||||
.page-container {
|
||||
width: 400px;
|
||||
width: 408px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
|
||||
z-index: 25;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-radius: 7px;
|
||||
border-radius: 8px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
|
||||
&--no-padding-bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
color: $tundora;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
@ -117,7 +121,7 @@ input.large-input {
|
||||
flex-flow: row;
|
||||
justify-content: center;
|
||||
border-top: 1px solid $geyser;
|
||||
padding: 1.6rem;
|
||||
padding: 16px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.btn-clear,
|
||||
@ -128,11 +132,10 @@ input.large-input {
|
||||
}
|
||||
|
||||
&__footer-button {
|
||||
width: 165px;
|
||||
height: 55px;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
margin-right: 1.2rem;
|
||||
margin-right: 16px;
|
||||
border-radius: 2px;
|
||||
|
||||
&:last-of-type {
|
||||
@ -162,25 +165,20 @@ input.large-input {
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
padding: 0 1.3rem;
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
min-width: 5rem;
|
||||
padding: .2rem .8rem .9rem;
|
||||
padding: 8px;
|
||||
color: $dusty-gray;
|
||||
font-family: Roboto;
|
||||
font-size: 1.1rem;
|
||||
line-height: initial;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
margin-right: 1rem;
|
||||
|
||||
&:hover {
|
||||
color: $black;
|
||||
}
|
||||
margin-right: 16px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
@ -189,10 +187,6 @@ input.large-input {
|
||||
&--selected {
|
||||
color: $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) {
|
||||
.page-container {
|
||||
height: 600px;
|
||||
max-height: 82vh;
|
||||
min-height: 570px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
@ -303,3 +298,9 @@ input.form-control {
|
||||
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: {},
|
||||
tokenExchangeRates: {},
|
||||
tokens: [],
|
||||
pendingTokens: {},
|
||||
send: {
|
||||
gasLimit: null,
|
||||
gasPrice: null,
|
||||
@ -356,6 +357,17 @@ function reduceMetamask (state, action) {
|
||||
currentLocale: action.value,
|
||||
})
|
||||
|
||||
case actions.SET_PENDING_TOKENS:
|
||||
return extend(metamaskState, {
|
||||
pendingTokens: { ...action.payload },
|
||||
})
|
||||
|
||||
case actions.CLEAR_PENDING_TOKENS: {
|
||||
return extend(metamaskState, {
|
||||
pendingTokens: {},
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
return metamaskState
|
||||
|
||||
|
@ -6,6 +6,7 @@ const REVEAL_SEED_ROUTE = '/seed'
|
||||
const CONFIRM_SEED_ROUTE = '/confirm-seed'
|
||||
const RESTORE_VAULT_ROUTE = '/restore-vault'
|
||||
const ADD_TOKEN_ROUTE = '/add-token'
|
||||
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
|
||||
const NEW_ACCOUNT_ROUTE = '/new-account'
|
||||
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
|
||||
const SEND_ROUTE = '/send'
|
||||
@ -31,6 +32,7 @@ module.exports = {
|
||||
CONFIRM_SEED_ROUTE,
|
||||
RESTORE_VAULT_ROUTE,
|
||||
ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_TOKEN_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
IMPORT_ACCOUNT_ROUTE,
|
||||
SEND_ROUTE,
|
||||
|
Loading…
Reference in New Issue
Block a user