aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbrunobar79 <brunobar79@gmail.com>2018-09-06 04:26:09 +0800
committerbrunobar79 <brunobar79@gmail.com>2018-09-06 04:26:09 +0800
commit8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52 (patch)
tree106d2ab08621f972b4b9140df16e129a84526703
parentb208ce723459a13f9b1fd6837af9d2858ba4cc17 (diff)
parentdc2431fe62bc7e50ebbf864389e9590f29d2136f (diff)
downloadtangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar
tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.gz
tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.bz2
tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.lz
tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.xz
tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.zst
tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.zip
Merge branch 'develop' of github.com:MetaMask/metamask-extension into trezor-v5
-rw-r--r--.babelrc2
-rw-r--r--CHANGELOG.md8
-rw-r--r--app/_locales/cs/messages.json2
-rw-r--r--app/_locales/de/messages.json6
-rw-r--r--app/_locales/en/messages.json34
-rw-r--r--app/_locales/es/messages.json2
-rw-r--r--app/_locales/fr/messages.json2
-rw-r--r--app/_locales/ko/messages.json822
-rw-r--r--app/_locales/ru/messages.json2
-rw-r--r--app/_locales/tml/messages.json2
-rw-r--r--app/_locales/tr/messages.json2
-rw-r--r--app/manifest.json1
-rw-r--r--app/scripts/background.js20
-rw-r--r--app/scripts/controllers/preferences.js120
-rw-r--r--app/scripts/lib/account-tracker.js18
-rw-r--r--app/scripts/lib/ipfsContent.js2
-rw-r--r--app/scripts/metamask-controller.js66
-rw-r--r--development/states/add-token.json1
-rw-r--r--development/states/confirm-new-ui.json1
-rw-r--r--development/states/confirm-sig-requests.json1
-rw-r--r--development/states/currency-localization.json1
-rw-r--r--development/states/first-time.json1
-rw-r--r--development/states/send-edit.json2
-rw-r--r--development/states/send-new-ui.json2
-rw-r--r--development/states/send.json2
-rw-r--r--development/states/tx-list-items.json1
-rw-r--r--old-ui/app/account-detail.js5
-rw-r--r--old-ui/app/add-suggested-token.js202
-rw-r--r--old-ui/app/add-token.js2
-rw-r--r--old-ui/app/app.js6
-rw-r--r--old-ui/app/components/app-bar.js11
-rw-r--r--package-lock.json23
-rw-r--r--package.json13
-rw-r--r--test/e2e/beta/from-import-beta-ui.spec.js6
-rw-r--r--test/e2e/beta/metamask-beta-ui.spec.js221
-rw-r--r--test/integration/lib/add-token.js4
-rw-r--r--test/integration/lib/confirm-sig-requests.js2
-rw-r--r--test/integration/lib/currency-localization.js8
-rw-r--r--test/integration/lib/send-new-ui.js6
-rw-r--r--test/integration/lib/tx-list-items.js21
-rw-r--r--test/unit/app/controllers/metamask-controller-test.js71
-rw-r--r--test/unit/app/controllers/preferences-controller-test.js110
-rw-r--r--ui/app/account-and-transaction-details.js33
-rw-r--r--ui/app/actions.js57
-rw-r--r--ui/app/app.js67
-rw-r--r--ui/app/components/balance-component.js48
-rw-r--r--ui/app/components/buy-button-subview.js267
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js3
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js4
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss8
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js4
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container.component.js5
-rw-r--r--ui/app/components/currency-display/currency-display.component.js26
-rw-r--r--ui/app/components/currency-display/currency-display.container.js19
-rw-r--r--ui/app/components/currency-display/index.js1
-rw-r--r--ui/app/components/currency-display/tests/currency-display.component.test.js27
-rw-r--r--ui/app/components/currency-display/tests/currency-display.container.test.js61
-rw-r--r--ui/app/components/custom-radio-list.js60
-rw-r--r--ui/app/components/dropdowns/components/account-dropdowns.js2
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js12
-rw-r--r--ui/app/components/identicon.js64
-rw-r--r--ui/app/components/index.scss26
-rw-r--r--ui/app/components/menu-bar/index.js1
-rw-r--r--ui/app/components/menu-bar/index.scss23
-rw-r--r--ui/app/components/menu-bar/menu-bar.component.js52
-rw-r--r--ui/app/components/menu-bar/menu-bar.container.js26
-rw-r--r--ui/app/components/modals/account-details-modal.js2
-rw-r--r--ui/app/components/modals/account-modal-container.js4
-rw-r--r--ui/app/components/modals/export-private-key-modal.js48
-rw-r--r--ui/app/components/modals/hide-token-confirmation-modal.js5
-rw-r--r--ui/app/components/page-container/index.scss2
-rw-r--r--ui/app/components/page-container/page-container-header/page-container-header.component.js28
-rw-r--r--ui/app/components/page-container/page-container.component.js84
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js84
-rw-r--r--ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js126
-rw-r--r--ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js29
-rw-r--r--ui/app/components/pages/confirm-add-suggested-token/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.component.js4
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js16
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js14
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js4
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js17
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js3
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js6
-rw-r--r--ui/app/components/pages/home.js239
-rw-r--r--ui/app/components/pages/home/home.component.js77
-rw-r--r--ui/app/components/pages/home/home.container.js30
-rw-r--r--ui/app/components/pages/home/index.js1
-rw-r--r--ui/app/components/pages/settings/settings.js29
-rw-r--r--ui/app/components/pending-msg-details.js56
-rw-r--r--ui/app/components/pending-msg.js73
-rw-r--r--ui/app/components/send/send-content/send-content.component.js3
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js2
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js2
-rw-r--r--ui/app/components/send/send-content/tests/send-content-component.test.js14
-rw-r--r--ui/app/components/send/send.component.js3
-rw-r--r--ui/app/components/send/send.container.js2
-rw-r--r--ui/app/components/send/send.selectors.js5
-rw-r--r--ui/app/components/send/tests/send-component.test.js5
-rw-r--r--ui/app/components/send/tests/send-container.test.js2
-rw-r--r--ui/app/components/send/tests/send-selectors-test-data.js2
-rw-r--r--ui/app/components/send/tests/send-selectors.test.js10
-rw-r--r--ui/app/components/sender-to-recipient/index.scss147
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js143
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.constants.js3
-rw-r--r--ui/app/components/shift-list-item.js3
-rw-r--r--ui/app/components/sidebars/index.js1
-rw-r--r--ui/app/components/sidebars/index.scss74
-rw-r--r--ui/app/components/sidebars/sidebar.component.js49
-rw-r--r--ui/app/components/sidebars/sidebar.constants.js1
-rw-r--r--ui/app/components/sidebars/tests/sidebars-component.test.js88
-rw-r--r--ui/app/components/tabs/tab/tab.component.js13
-rw-r--r--ui/app/components/token-balance.js120
-rw-r--r--ui/app/components/token-balance/index.js1
-rw-r--r--ui/app/components/token-balance/token-balance.component.js23
-rw-r--r--ui/app/components/token-balance/token-balance.container.js (renamed from ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js)4
-rw-r--r--ui/app/components/token-cell.js5
-rw-r--r--ui/app/components/token-currency-display/index.js1
-rw-r--r--ui/app/components/token-currency-display/token-currency-display.component.js54
-rw-r--r--ui/app/components/token-list.js9
-rw-r--r--ui/app/components/transaction-action/index.js1
-rw-r--r--ui/app/components/transaction-action/tests/transaction-action.component.test.js112
-rw-r--r--ui/app/components/transaction-action/transaction-action.component.js52
-rw-r--r--ui/app/components/transaction-list-item/index.js1
-rw-r--r--ui/app/components/transaction-list-item/index.scss117
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.component.js151
-rw-r--r--ui/app/components/transaction-list-item/transaction-list-item.container.js32
-rw-r--r--ui/app/components/transaction-list/index.js1
-rw-r--r--ui/app/components/transaction-list/index.scss46
-rw-r--r--ui/app/components/transaction-list/transaction-list.component.js118
-rw-r--r--ui/app/components/transaction-list/transaction-list.container.js51
-rw-r--r--ui/app/components/transaction-status/index.js1
-rw-r--r--ui/app/components/transaction-status/index.scss28
-rw-r--r--ui/app/components/transaction-status/transaction-status.component.js51
-rw-r--r--ui/app/components/transaction-view-balance/index.js1
-rw-r--r--ui/app/components/transaction-view-balance/index.scss76
-rw-r--r--ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js71
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.component.js96
-rw-r--r--ui/app/components/transaction-view-balance/transaction-view-balance.container.js31
-rw-r--r--ui/app/components/transaction-view/index.js1
-rw-r--r--ui/app/components/transaction-view/index.scss27
-rw-r--r--ui/app/components/transaction-view/transaction-view.component.js27
-rw-r--r--ui/app/components/tx-list-item.js356
-rw-r--r--ui/app/components/tx-list.js171
-rw-r--r--ui/app/components/tx-view.js156
-rw-r--r--ui/app/components/wallet-view.js9
-rw-r--r--ui/app/constants/common.js1
-rw-r--r--ui/app/constants/transactions.js22
-rw-r--r--ui/app/css/itcss/components/hero-balance.scss130
-rw-r--r--ui/app/css/itcss/components/index.scss2
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss43
-rw-r--r--ui/app/css/itcss/components/transaction-list.scss2
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js50
-rw-r--r--ui/app/ducks/tests/confirm-transaction.duck.test.js70
-rw-r--r--ui/app/helpers/confirm-transaction/util.js25
-rw-r--r--ui/app/helpers/confirm-transaction/util.test.js6
-rw-r--r--ui/app/helpers/conversions.util.js37
-rw-r--r--ui/app/helpers/transactions.util.js105
-rw-r--r--ui/app/higher-order-components/with-method-data/index.js1
-rw-r--r--ui/app/higher-order-components/with-method-data/with-method-data.component.js52
-rw-r--r--ui/app/higher-order-components/with-token-tracker/index.js1
-rw-r--r--ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js (renamed from ui/app/helpers/with-token-tracker.js)4
-rw-r--r--ui/app/i18n-provider.js11
-rw-r--r--ui/app/main-container.js49
-rw-r--r--ui/app/new-keychain.js29
-rw-r--r--ui/app/reducers/app.js25
-rw-r--r--ui/app/routes.js2
-rw-r--r--ui/app/selectors.js35
-rw-r--r--ui/app/selectors/tokens.js11
-rw-r--r--ui/app/selectors/transactions.js58
-rw-r--r--ui/app/token-util.js116
-rw-r--r--ui/app/util.js2
173 files changed, 4615 insertions, 2562 deletions
diff --git a/.babelrc b/.babelrc
index fcabd2d1a..9b1d5409b 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,4 +1,4 @@
{
- "presets": [["env"], "react", "stage-0"],
+ "presets": [["env", { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } } ], "react", "stage-0"],
"plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddaa496dd..4b7ea17a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,13 @@
## Current Develop Branch
+- [#4606](https://github.com/MetaMask/metamask-extension/pull/4606): Add new metamask_watchAsset method.
+
## 4.9.3 Wed Aug 15 2018
-- (#4897)[https://github.com/MetaMask/metamask-extension/pull/4897]: QR code scan for recipient addresses.
-- (#4961)[https://github.com/MetaMask/metamask-extension/pull/4961]: Add a download seed phrase link.
-- (#5060)[https://github.com/MetaMask/metamask-extension/pull/5060]: Fix bug where gas was not updating properly.
+- [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses.
+- [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link.
+- [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly.
## 4.9.2 Mon Aug 09 2018
diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json
index 6a4ebc8a5..55344f3e1 100644
--- a/app/_locales/cs/messages.json
+++ b/app/_locales/cs/messages.json
@@ -796,7 +796,7 @@
"message": "Testovací faucet"
},
"to": {
- "message": "Komu: "
+ "message": "Komu"
},
"toETHviaShapeShift": {
"message": "$1 na ETH přes ShapeShift",
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index c06a99250..352d5ad7d 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -384,7 +384,7 @@
"infoHelp": {
"message": "Info & Hilfe"
},
- "insufficientFunds": {
+ "insufficientFunds": {
"message": "Nicht genügend Guthaben."
},
"insufficientTokens": {
@@ -572,7 +572,7 @@
"description": "Wähle diesen Dateityp um damit einen Account zu importieren"
},
"privateKeyWarning": {
- "message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen."
+ "message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen."
},
"privateNetwork": {
"message": "Privates Netzwerk"
@@ -775,7 +775,7 @@
"message": "Testfaucet"
},
"to": {
- "message": "An:"
+ "message": "An"
},
"toETHviaShapeShift": {
"message": "$1 an ETH via ShapeShift",
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index a25a2bd59..14e867b33 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -29,6 +29,9 @@
"addTokens": {
"message": "Add Tokens"
},
+ "addSuggestedTokens": {
+ "message": "Add Suggested Tokens"
+ },
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
@@ -451,6 +454,9 @@
"hideTokenPrompt": {
"message": "Hide Token?"
},
+ "history": {
+ "message": "History"
+ },
"howToDeposit": {
"message": "How would you like to deposit Ether?"
},
@@ -651,7 +657,7 @@
"message": "No transaction history."
},
"noTransactions": {
- "message": "No Transactions"
+ "message": "You have no transactions"
},
"notFound": {
"message": "Not Found"
@@ -702,6 +708,9 @@
"pasteSeed": {
"message": "Paste your seed phrase here!"
},
+ "pending": {
+ "message": "pending"
+ },
"personalAddressDetected": {
"message": "Personal address detected. Input the token contract address."
},
@@ -730,6 +739,9 @@
"qrCode": {
"message": "Show QR Code"
},
+ "queue": {
+ "message": "Queue"
+ },
"readdToken": {
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu."
},
@@ -870,6 +882,12 @@
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
},
+ "showHexData": {
+ "message": "Show Hex Data"
+ },
+ "showHexDataDescription": {
+ "message": "Select this to show the hex data field on the send screen"
+ },
"newPassword8Chars": {
"message": "New Password (min 8 chars)"
},
@@ -897,6 +915,12 @@
"sendTokens": {
"message": "Send Tokens"
},
+ "sentEther": {
+ "message": "sent ether"
+ },
+ "sentTokens": {
+ "message": "sent tokens"
+ },
"separateEachWord": {
"message": "Separate each word with a single space"
},
@@ -910,6 +934,9 @@
"orderOneHere": {
"message": "Order a Trezor or Ledger and keep your funds in cold storage"
},
+ "outgoing": {
+ "message": "Outgoing"
+ },
"searchTokens": {
"message": "Search Tokens"
},
@@ -973,6 +1000,9 @@
"sign": {
"message": "Sign"
},
+ "signatureRequest": {
+ "message": "Signature Request"
+ },
"signed": {
"message": "Signed"
},
@@ -1025,7 +1055,7 @@
"message": "Test Faucet"
},
"to": {
- "message": "To: "
+ "message": "To"
},
"toETHviaShapeShift": {
"message": "$1 to ETH via ShapeShift",
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index ed7f8f681..3e43a7b43 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -772,7 +772,7 @@
"message": "Probar Faucet"
},
"to": {
- "message": "Para:"
+ "message": "Para"
},
"toETHviaShapeShift": {
"message": "$1 a ETH via ShapeShift",
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index 1463e2b5f..6f850d89b 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -490,7 +490,7 @@
"message": "Sélectionner un service"
},
"send": {
- "message": "Envoyé"
+ "message": "Envoyer"
},
"sendTokens": {
"message": "Envoyer des jetons"
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index d3801c4f5..30d032357 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -3,86 +3,125 @@
"message": "수락"
},
"account": {
- "message": "계좌"
+ "message": "계정"
},
"accountDetails": {
- "message": "계좌 상세보기"
+ "message": "계정 상세보기"
},
"accountName": {
- "message": "계좌 이름"
+ "message": "계정 이름"
},
"address": {
"message": "주소"
},
+ "addCustomToken": {
+ "message": "사용자 정의 토큰 추가"
+ },
"addToken": {
"message": "토큰 추가"
},
+ "addTokens": {
+ "message": "토큰 추가"
+ },
+ "addAcquiredTokens": {
+ "message": "메타마스크를 통해 획득한 토큰 추가"
+ },
"amount": {
- "message": "금액"
+ "message": "수량"
},
"amountPlusGas": {
- "message": "금액 + 가스"
+ "message": "수량 + 가스"
},
"appDescription": {
"message": "이더리움 브라우저 확장 프로그램",
- "description": "어플리케이션 내용"
+ "description": "어플리케이션 설명"
},
"appName": {
"message": "메타마스크",
"description": "어플리케이션 이름"
},
+ "approved": {
+ "message": "수락"
+ },
"attemptingConnect": {
- "message": "블록체인에 접속 시도 중입니다."
+ "message": "블록체인에 접속을 시도하는 중입니다."
+ },
+ "attributions": {
+ "message": "속성"
},
"available": {
- "message": "사용 가능한"
+ "message": "사용 가능"
},
"back": {
- "message": "뒤로"
+ "message": "돌아가기"
},
"balance": {
- "message": "잔액:"
+ "message": "잔액"
+ },
+ "balances": {
+ "message": "토큰 잔액"
},
"balanceIsInsufficientGas": {
- "message": "가스가 충분하지 않습니다."
+ "message": "현재 가스 총합에 대해 잔액이 부족합니다"
},
"beta": {
- "message": "베타"
+ "message": "BETA"
},
"betweenMinAndMax": {
"message": "$1 이상 $2 이하여야 합니다.",
- "description": "helper for inputting hex as decimal input"
+ "description": "10진수 입력으로 hex값 입력을 도와줍니다"
+ },
+ "blockiesIdenticon": {
+ "message": "Blockies 아이덴티콘 사용"
},
"borrowDharma": {
- "message": "Dharma에서 빌리기(베타)"
+ "message": "Dharma에서 대여하기(Beta)"
+ },
+ "builtInCalifornia": {
+ "message": "메타마스크는 캘리포니아에서 디자인되고 만들어졌습니다."
},
"buy": {
"message": "구매"
},
"buyCoinbase": {
- "message": "코인베이스에서 구매"
+ "message": "코인베이스에서 구매하기"
},
"buyCoinbaseExplainer": {
- "message": "코인베이스에서 비트코인, 이더리움, 라이트코인을 구매하실 수 있습니다."
+ "message": "코인베이스는 비트코인, 이더리움, 라이트코인을 거래할 수 있는 유명한 거래소입니다."
+ },
+ "ok": {
+ "message": "확인"
},
"cancel": {
"message": "취소"
},
+ "classicInterface": {
+ "message": "예전 인터페이스"
+ },
"clickCopy": {
"message": "클릭하여 복사"
},
+ "close": {
+ "message": "닫기"
+ },
"confirm": {
"message": "승인"
},
+ "confirmed": {
+ "message": "승인됨"
+ },
"confirmContract": {
"message": "컨트랙트 승인"
},
"confirmPassword": {
- "message": "패스워드 승인"
+ "message": "비밀번호 확인"
},
"confirmTransaction": {
"message": "트랜잭션 승인"
},
+ "continue": {
+ "message": "계속"
+ },
"continueToCoinbase": {
"message": "코인베이스로 계속하기"
},
@@ -90,72 +129,93 @@
"message": "컨트랙트 배포"
},
"conversionProgress": {
- "message": "변환중.."
+ "message": "변환 진행중"
},
"copiedButton": {
- "message": "복사되었습니다."
+ "message": "복사됨"
},
"copiedClipboard": {
- "message": "클립보드에 복사되었습니다."
+ "message": "클립보드에 복사되었습니다"
},
"copiedExclamation": {
- "message": "복사되었습니다."
+ "message": "복사됨!"
+ },
+ "copiedSafe": {
+ "message": "안전한 곳에 복사하였습니다"
},
"copy": {
- "message": "복사하기"
+ "message": "복사"
+ },
+ "copyContractAddress": {
+ "message": "컨트랙트 주소 복사"
},
"copyToClipboard": {
- "message": "클립보드에 복사"
+ "message": "클립보드로 복사"
},
"copyButton": {
"message": " 복사 "
},
"copyPrivateKey": {
- "message": "비밀 키 (클릭하여 복사)"
+ "message": "비밀 키입니다 (클릭하여 복사)"
},
"create": {
"message": "생성"
},
"createAccount": {
- "message": "계좌 생성"
+ "message": "계정 생성"
},
"createDen": {
"message": "생성"
},
"crypto": {
"message": "암호화폐",
- "description": "Exchange type (cryptocurrencies)"
+ "description": "거래 유형 (암호화폐)"
+ },
+ "currentConversion": {
+ "message": "선택된 단위"
+ },
+ "currentNetwork": {
+ "message": "현재 네트워크"
},
"customGas": {
"message": "가스 설정"
},
+ "customToken": {
+ "message": "사용자 정의 토큰"
+ },
"customize": {
- "message": "커스터마이즈"
+ "message": "맞춤화 하기"
},
"customRPC": {
- "message": "커스텀 RPC"
+ "message": "사용자 정의 RPC"
+ },
+ "decimalsMustZerotoTen": {
+ "message": "소수점은 0 이상이고 36 이하여야 합니다."
+ },
+ "decimal": {
+ "message": "소수점 정확도"
},
"defaultNetwork": {
"message": "이더리움 트랜잭션의 기본 네트워크는 메인넷입니다."
},
"denExplainer": {
- "message": "DEN은 비밀번호가 암호화 된 MetaMask의 스토리지입니다."
+ "message": "DEN은 비밀번호로 암호화 된 메타마스크의 저장소입니다."
},
"deposit": {
"message": "입금"
},
"depositBTC": {
- "message": "아래 주소로 BTC를 입급해주세요."
+ "message": "다음 주소로 BTC를 입급해주세요."
},
"depositCoin": {
- "message": "아래 주소로 $1를 입금해주세요.",
- "description": "Tells the user what coin they have selected to deposit with shapeshift"
+ "message": "다음 주소로 $1 만큼 입금해주세요.",
+ "description": "사용자에게 shapeshift에서 어떤 코인을 선택해 입금했는지 알려줍니다"
},
"depositEth": {
- "message": "이더 입금"
+ "message": "이더 입금하기"
},
"depositEther": {
- "message": "이더 입금"
+ "message": "이더리움 입금하기"
},
"depositFiat": {
"message": "현금으로 입금하기"
@@ -167,10 +227,10 @@
"message": "ShapeShift를 통해 입금하기"
},
"depositShapeShiftExplainer": {
- "message": "다른 암호화폐를 가지고 있으면, 계좌 생성 필요없이, 거래를 하거나 메타마스크 지갑을 통해 이더를 입금할 수 있습니다."
+ "message": "다른 암호화폐를 가지고 있으면, 계정을 생성할 필요없이 메타마스크 지갑에 이더리움을 바로 거래하거나 입금할 수 있습니다."
},
"details": {
- "message": "상세"
+ "message": "세부사항"
},
"directDeposit": {
"message": "즉시 입금"
@@ -179,70 +239,103 @@
"message": "이더 즉시 입금"
},
"directDepositEtherExplainer": {
- "message": "이더를 이미 보유하고 있다면, 직접 입금을 통해 이더를 즉시 입금하실 수 있습니다."
+ "message": "약간의 이더를 이미 보유하고 있다면, 새로 만든 지갑에 직접 입금하여 이더를 보유할 수 있습니다."
},
"done": {
"message": "완료"
},
+ "downloadStateLogs": {
+ "message": "상태 로그 다운로드"
+ },
+ "dropped": {
+ "message": "중단됨"
+ },
"edit": {
"message": "수정"
},
"editAccountName": {
- "message": "계좌명 수정"
+ "message": "계정 이름 수정"
+ },
+ "editingTransaction": {
+ "message": "트랜젝션을 변경합니다"
+ },
+ "emailUs": {
+ "message": "저자에게 메일 보내기!"
},
"encryptNewDen": {
- "message": "새 DEN 암호화"
+ "message": "새로운 DEN을 암호화"
},
"enterPassword": {
- "message": "패스워드를 입력해주세요."
+ "message": "비밀번호를 입력해주세요"
+ },
+ "enterPasswordConfirm": {
+ "message": "비밀번호를 다시 입력해 주세요"
+ },
+ "enterPasswordContinue": {
+ "message": "계속하기 위해 비밀번호 입력"
+ },
+ "passwordNotLongEnough": {
+ "message": "비밀번호가 충분히 길지 않습니다"
+ },
+ "passwordsDontMatch": {
+ "message": "비밀번호가 맞지 않습니다"
},
"etherscanView": {
- "message": "이더스캔에서 계좌보기"
+ "message": "이더스캔에서 계정보기"
},
"exchangeRate": {
"message": "환율"
},
"exportPrivateKey": {
- "message": "비밀키 내보내기"
+ "message": "개인키 내보내기"
},
"exportPrivateKeyWarning": {
- "message": "Export private keys at your own risk."
+ "message": "개인키 내보내기는 위험을 감수해야 합니다."
},
"failed": {
"message": "실패"
},
"fiat": {
"message": "FIAT",
- "description": "Exchange type"
+ "description": "거래 형식"
},
"fileImportFail": {
- "message": "파일을 가져올 수 없나요? 여기를 클릭해주세요!",
- "description": "Helps user import their account from a JSON file"
+ "message": "파일을 가져올 수 없나요? 이곳을 클릭해주세요!",
+ "description": "JSON 파일로부터 계정 가져오기를 도와줍니다"
+ },
+ "followTwitter": {
+ "message": "트위터에서 팔로우하세요"
},
"from": {
- "message": "보내는 사람"
+ "message": "보내는 이"
+ },
+ "fromToSame": {
+ "message": "보내고 받는 주소는 동일할 수 없습니다"
},
"fromShapeShift": {
"message": "ShapeShift로 부터"
},
"gas": {
"message": "가스",
- "description": "Short indication of gas cost"
+ "description": "가스 가격의 줄임"
},
"gasFee": {
"message": "가스 수수료"
},
"gasLimit": {
- "message": "가스 리밋"
+ "message": "가스 한도"
},
"gasLimitCalculation": {
- "message": "네트워크 성공률을 기반으로 적합한 가스 리밋을 계산합니다."
+ "message": "네트워크 성공률을 기반으로 적합한 가스 한도를 계산합니다."
},
"gasLimitRequired": {
- "message": "가스 리밋이 필요합니다."
+ "message": "가스 한도가 필요합니다."
},
"gasLimitTooLow": {
- "message": "가스 리밋은 21000 이상이여야 합니다."
+ "message": "가스 한도는 최소 21000 이상이여야 합니다."
+ },
+ "generatingSeed": {
+ "message": "시드 생성중..."
},
"gasPrice": {
"message": "가스 가격 (GWEI)"
@@ -253,21 +346,27 @@
"gasPriceRequired": {
"message": "가스 가격이 필요합니다."
},
+ "generatingTransaction": {
+ "message": "트랜잭션 생성중"
+ },
"getEther": {
"message": "이더 얻기"
},
"getEtherFromFaucet": {
- "message": "faucet에서 $1에 달하는 이더를 얻으세요.",
- "description": "Displays network name for Ether faucet"
+ "message": "파우셋에서 $1에 달하는 이더를 얻으세요.",
+ "description": "이더 파우셋에 대한 네트워크 이름을 표시합니다"
},
"greaterThanMin": {
"message": "$1 이상이어야 합니다.",
- "description": "helper for inputting hex as decimal input"
+ "description": "10진수 입력으로 hex값 입력을 도와줍니다"
},
"here": {
"message": "여기",
"description": "as in -click here- for more information (goes with troubleTokenBalances)"
},
+ "hereList": {
+ "message": "리스트가 있습니다!!!!"
+ },
"hide": {
"message": "숨기기"
},
@@ -280,51 +379,96 @@
"howToDeposit": {
"message": "어떤 방법으로 이더를 입금하시겠습니까?"
},
+ "holdEther": {
+ "message": "It allows you to hold ether & tokens, and serves as your bridge to decentralized applications."
+ },
"import": {
- "message": "파일에서 가져오기",
- "description": "Button to import an account from a selected file"
+ "message": "가져오기",
+ "description": "선택된 파일로부터 계정 가져오기 버튼"
},
"importAccount": {
- "message": "계좌 가져오기"
+ "message": "계정 가져오기"
+ },
+ "importAccountMsg": {
+ "message": " 가져온 계정은 메타마스크에서 원래 생성된 계정의 시드구문과 연관성이 없습니다. 가져온 계정에 대해 더 배우기 "
},
"importAnAccount": {
- "message": "계좌 가져오기"
+ "message": "계정 가져오기"
},
"importDen": {
- "message": "기존 DEN 가져오기"
+ "message": "기존의 DEN 가져오기"
},
"imported": {
- "message": "가져오기 완료",
- "description": "status showing that an account has been fully loaded into the keyring"
+ "message": "가져온 계정",
+ "description": "이 상태는 해당 계정이 keyring으로 완전히 적재된 상태임을 표시합니다"
+ },
+ "importUsingSeed": {
+ "message": "계정 시드 구문으로 가져오기"
},
"infoHelp": {
"message": "정보 및 도움말"
},
+ "initialTransactionConfirmed": {
+ "message": "초기 트랜잭션이 네트워크를 통해 확정되었습니다. 확인을 누르고 이전으로 돌아갑니다."
+ },
+ "insufficientFunds": {
+ "message": "충분하지 않은 자금."
+ },
+ "insufficientTokens": {
+ "message": "충분하지 않은 토큰."
+ },
"invalidAddress": {
- "message": "유효하지 않은 주소"
+ "message": "올바르지 않은 주소"
+ },
+ "invalidAddressRecipient": {
+ "message": "수신 주소가 올바르지 않습니다"
},
"invalidGasParams": {
- "message": "유효하지 않은 가스 입력값"
+ "message": "올바르지 않은 가스 입력값"
},
"invalidInput": {
- "message": "유효하지 않은 입력값"
+ "message": "올바르지 않은 입력값"
},
"invalidRequest": {
"message": "유효하지 않은 요청"
},
+ "invalidRPC": {
+ "message": "올바르지 않은 RPC URI"
+ },
+ "jsonFail": {
+ "message": "이상이 있습니다. JSON 파일이 올바른 파일인지 확인해주세요."
+ },
"jsonFile": {
"message": "JSON 파일",
- "description": "format for importing an account"
+ "description": "계정을 가져오기 위한 형식"
+ },
+ "keepTrackTokens": {
+ "message": "메타마스크 계정을 통해 구입한 토큰 기록을 추적 및 보관합니다."
},
"kovan": {
"message": "Kovan 테스트넷"
},
+ "knowledgeDataBase": {
+ "message": "지식 베이스 방문"
+ },
+ "max": {
+ "message": "최대"
+ },
+ "learnMore": {
+ "message": "더 배우기."
+ },
"lessThanMax": {
"message": "$1 이하여야합니다.",
- "description": "helper for inputting hex as decimal input"
+ "description": "10진수 입력으로 hex값 입력을 도와줍니다"
+ },
+ "likeToAddTokens": {
+ "message": "토큰을 추가하시겠습니까?"
+ },
+ "links": {
+ "message": "링크"
},
"limit": {
- "message": "리밋"
+ "message": "한도"
},
"loading": {
"message": "로딩중..."
@@ -335,11 +479,17 @@
"localhost": {
"message": "로컬호스트 8545"
},
+ "login": {
+ "message": "로그인"
+ },
"logout": {
"message": "로그아웃"
},
"loose": {
- "message": "외부 비밀키"
+ "message": "느슨함"
+ },
+ "loweCaseWords": {
+ "message": "시드 단어는 소문자만 가능합니다"
},
"mainnet": {
"message": "이더리움 메인넷"
@@ -347,94 +497,127 @@
"message": {
"message": "메시지"
},
+ "metamaskDescription": {
+ "message": "메타마스크는 이더리움을 위한 안전한 신분 저장소입니다."
+ },
+ "metamaskSeedWords": {
+ "message": "메타마스크 시드 단어"
+ },
"min": {
"message": "최소"
},
"myAccounts": {
- "message": "내 계좌"
+ "message": "내 계정"
+ },
+ "mustSelectOne": {
+ "message": "적어도 하나의 토큰을 선택하세요."
},
"needEtherInWallet": {
- "message": "dApp을 이용하기 위해서는 지갑에 이더가 있어야 합니다."
+ "message": "메타마스크를 통한 dApp을 이용하기 위해서는 지갑에 이더가 있어야 합니다."
},
"needImportFile": {
"message": "가져올 파일을 선택해주세요.",
- "description": "User is important an account and needs to add a file to continue"
+ "description": "사용자는 계정을 가져오기 위해서 파일을 추가후 계속 진행해야 합니다"
},
"needImportPassword": {
- "message": "선택 된 파일에 패스워드를 입력해주세요.",
- "description": "Password and file needed to import an account"
+ "message": "선택 된 파일에 대한 비밀번호를 입력해주세요.",
+ "description": "계정을 가져오기 위해서 비밀번호와 파일이 필요합니다."
+ },
+ "negativeETH": {
+ "message": "음수값의 이더를 보낼 수 없습니다."
},
"networks": {
"message": "네트워크"
},
+ "nevermind": {
+ "message": "상관안함"
+ },
"newAccount": {
- "message": "새 계좌"
+ "message": "새 계정"
},
"newAccountNumberName": {
- "message": "새 계좌 $1",
- "description": "Default name of next account to be created on create account screen"
+ "message": "새 계정 $1",
+ "description": "계정 생성시 볼 수 있는 새 계정의 기본 이름"
},
"newContract": {
"message": "새 컨트랙트"
},
"newPassword": {
- "message": "새 패스워드 (최소 8자 이상)"
+ "message": "새 비밀번호 (최소 8자 이상)"
},
"newRecipient": {
"message": "받는 사람"
},
+ "newRPC": {
+ "message": "새로운 RPC URL"
+ },
"next": {
"message": "다음"
},
"noAddressForName": {
- "message": "이 이름에는 주소가 설정되어 있지 않습니다."
+ "message": "이 이름에 대해 주소가 설정되어 있지 않습니다."
},
"noDeposits": {
- "message": "입금이 없습니다."
+ "message": "입금 내역이 없습니다."
},
"noTransactionHistory": {
"message": "트랜잭션 기록이 없습니다."
},
"noTransactions": {
- "message": "트랜잭션이 없습니다."
+ "message": "트랜잭션이 없습니다"
},
"notStarted": {
- "message": "시작되지 않음."
+ "message": "시작 안됨"
},
"oldUI": {
- "message": "구버전의 UI"
+ "message": "구버전 UI"
},
"oldUIMessage": {
- "message": "구버전 UI로 변경하셨습니다. 우 상단 드랍다운 메뉴에서 새 UI로 변경하실 수 있습니다."
+ "message": "구버전 UI로 변경하셨습니다. 우 상단 드롭다운 메뉴에서 새 UI로 변경하실 수 있습니다."
},
"or": {
"message": "또는",
- "description": "choice between creating or importing a new account"
+ "description": "새 계정을 만들거나 가져오기중에 선택하기"
+ },
+ "password": {
+ "message": "비밀번호"
+ },
+ "passwordCorrect": {
+ "message": "비밀번호가 맞는지 확인해주세요."
},
"passwordMismatch": {
- "message": "패스워드가 일치하지 않습니다.",
- "description": "in password creation process, the two new password fields did not match"
+ "message": "비밀번호가 일치하지 않습니다.",
+ "description": "비밀번호를 생성하는 과정에서 두개의 새 비밀번호 입력란이 일치하지 않습니다"
},
"passwordShort": {
- "message": "패스워드가 너무 짧습니다.",
- "description": "in password creation process, the password is not long enough to be secure"
+ "message": "비밀번호가 짧습니다.",
+ "description": "비밀번호를 생성하는 과정에서 비밀번호가 짧으면 안전하지 않습니다"
},
"pastePrivateKey": {
- "message": "비밀키를 입력해주세요.",
- "description": "For importing an account from a private key"
+ "message": "개인키를 입력해주세요:",
+ "description": "개인키로부터 계정을 가져오는 것입니다"
},
"pasteSeed": {
- "message": "시드 문장들을 붙여넣어주세요."
+ "message": "시드 구문을 이곳에 붙여넣어주세요!"
+ },
+ "personalAddressDetected": {
+ "message": "개인 주소가 탐지됨. 토큰 컨트랙트 주소를 입력하세요."
},
"pleaseReviewTransaction": {
"message": "트랜잭션을 검토해주세요."
},
+ "popularTokens": {
+ "message": "인기있는 토큰"
+ },
+ "privacyMsg": {
+ "message": "개인정보 보호 정책"
+ },
"privateKey": {
- "message": "비밀키",
- "description": "select this type of file to use to import an account"
+ "message": "개인키",
+ "description": "이 형식의 파일을 선택하여 계정을 가져올 수 있습니다"
},
"privateKeyWarning": {
- "message": " 절대 이 키를 노출하지 마십시오. 비밀키가 노출되면 누구나 당신의 계좌에서 자산을 빼갈 수 있습니다."
+ "message": " 절대 이 키를 노출하지 마십시오. 개인키가 노출되면 누구나 당신의 계정에서 자산을 빼갈 수 있습니다."
},
"privateNetwork": {
"message": "프라이빗 네트워크"
@@ -446,28 +629,64 @@
"message": "옵션 메뉴에서 “토큰 추가”를 눌러서 추후에 다시 이 토큰을 추가하실 수 있습니다."
},
"readMore": {
- "message": "더 읽기."
+ "message": "여기서 더 보기."
+ },
+ "readMore2": {
+ "message": "더 보기."
},
"receive": {
"message": "받기"
},
"recipientAddress": {
- "message": "받는 사람 주소"
+ "message": "받는 주소"
},
"refundAddress": {
"message": "환불받을 주소"
},
"rejected": {
- "message": "거부되었음."
+ "message": "거부됨"
+ },
+ "reset": {
+ "message": "초기화"
+ },
+ "resetAccount": {
+ "message": "계정 초기화"
+ },
+ "resetAccountDescription": {
+ "message": "계정을 초기화 하는 경우에 트랜잭션 기록이 삭제됩니다."
+ },
+ "restoreFromSeed": {
+ "message": "계정을 복구하시겠습니까?"
+ },
+ "restoreVault": {
+ "message": "저장소 복구"
},
"required": {
- "message": "필요함."
+ "message": "필요함"
},
"retryWithMoreGas": {
- "message": "더 높은 가스 가격으로 다시 시도해주세요."
+ "message": "더 높은 가스 가격으로 다시 시도해주세요"
+ },
+ "walletSeed": {
+ "message": "지갑 시드값"
+ },
+ "revealSeedWords": {
+ "message": "시드 단어 보이기"
+ },
+ "revealSeedWordsTitle": {
+ "message": "시드 단어"
+ },
+ "revealSeedWordsDescription": {
+ "message": "브라우저를 바꾸거나 컴퓨터를 옮기는 경우 이 시드 구문이 필요하며 이를 통해 계정에 접근할 수 있습니다. 시드 구문을 안전한 곳에 보관하세요."
+ },
+ "revealSeedWordsWarningTitle": {
+ "message": "이 구문을 다른사람과 절대로 공유하지 마세요!"
+ },
+ "revealSeedWordsWarning": {
+ "message": "이 단어모음은 당신의 모든 계정을 훔치는데 사용할 수 있습니다."
},
"revert": {
- "message": "취소"
+ "message": "되돌림"
},
"rinkeby": {
"message": "Rinkeby 테스트넷"
@@ -475,46 +694,122 @@
"ropsten": {
"message": "Ropsten 테스트넷"
},
+ "rpc": {
+ "message": "사용자 정의 RPC"
+ },
+ "currentRpc": {
+ "message": "현재 RPC"
+ },
+ "connectingToMainnet": {
+ "message": "이더리움 메인넷 접속중"
+ },
+ "connectingToRopsten": {
+ "message": "Ropsten 테스트넷 접속중"
+ },
+ "connectingToKovan": {
+ "message": "Kovan 테스트넷 접속중"
+ },
+ "connectingToRinkeby": {
+ "message": "Rinkeby 테스트넷 접속중"
+ },
+ "connectingToUnknown": {
+ "message": "알려지지 않은 네트워크 접속중"
+ },
"sampleAccountName": {
- "message": "예) 나의 새 계좌",
- "description": "Help user understand concept of adding a human-readable name to their account"
+ "message": "예) 나의 새 계정",
+ "description": "각 계정에 대해서 구별하기 쉬운 이름을 지정하여 사용자가 쉽게 이해할 수 있게 합니다"
},
"save": {
"message": "저장"
},
+ "speedUpTitle": {
+ "message": "트랜잭션 속도 향상하기"
+ },
+ "speedUpSubtitle": {
+ "message": "트랜잭션 가스 가격을 늘려서 해당 트랙잭션에 덮어쓰기 하여 속도를 빠르게 합니다"
+ },
+ "saveAsCsvFile": {
+ "message": "CSV 파일로 저장"
+ },
"saveAsFile": {
"message": "파일로 저장",
- "description": "Account export process"
+ "description": "계정 내보내기 절차"
+ },
+ "saveSeedAsFile": {
+ "message": "시드 단어를 파일로 저장하기"
+ },
+ "search": {
+ "message": "검색"
+ },
+ "searchResults": {
+ "message": "검색 결과"
+ },
+ "secretPhrase": {
+ "message": "12개 단어로 구성된 비밀 구문을 입력하여 저장소를 복구하세요."
+ },
+ "newPassword8Chars": {
+ "message": "새 비밀번호 (최소 8문자)"
+ },
+ "seedPhraseReq": {
+ "message": "시드 구문은 12개의 단어입니다"
+ },
+ "select": {
+ "message": "선택"
+ },
+ "selectCurrency": {
+ "message": "통화 선택"
},
"selectService": {
"message": "서비스 선택"
},
+ "selectType": {
+ "message": "형식 선택"
+ },
"send": {
"message": "전송"
},
+ "sendETH": {
+ "message": "ETH 보내기"
+ },
"sendTokens": {
"message": "토큰 전송"
},
+ "onlySendToEtherAddress": {
+ "message": "이더리움 주소로 ETH만 송금하세요."
+ },
+ "onlySendTokensToAccountAddress": {
+ "message": "이더리움계정 주소로 $1 만 보내기.",
+ "description": "토큰 심볼 보이기"
+ },
+ "searchTokens": {
+ "message": "토큰 검색"
+ },
"sendTokensAnywhere": {
- "message": "이더 계좌로 토큰 전송"
+ "message": "이더 계정로 토큰 전송"
},
"settings": {
"message": "설정"
},
+ "info": {
+ "message": "정보"
+ },
"shapeshiftBuy": {
"message": "Shapeshift를 통해서 구매하기"
},
"showPrivateKeys": {
- "message": "비밀키 보기"
+ "message": "개인키 보기"
},
"showQRCode": {
- "message": "QR코드 보기"
+ "message": "QR 코드 보기"
},
"sign": {
"message": "서명"
},
+ "signed": {
+ "message": "서명됨"
+ },
"signMessage": {
- "message": "서명 메시지"
+ "message": "메시지 서명"
},
"signNotice": {
"message": "이 메시지에 대한 서명은 위험할 수 있습니다.\n 완전히 신뢰할 수 있는 사이트에서만 서명해주세요.\n 안전을 위해 추후의 버전에서는 삭제될 기능입니다. "
@@ -523,33 +818,81 @@
"message": "서명 요청"
},
"sigRequested": {
- "message": "서명이 요청되었습니다."
+ "message": "서명이 요청됨"
+ },
+ "spaceBetween": {
+ "message": "단어 사이에는 공백만 올 수 있습니다"
},
"status": {
"message": "상태"
},
+ "stateLogs": {
+ "message": "상태 로그"
+ },
+ "stateLogsDescription": {
+ "message": "상태 로그에는 공개 계정 주소와 송신 트랜잭션 정보가 들어있습니다."
+ },
+ "stateLogError": {
+ "message": "상태 로그 받기 실패."
+ },
"submit": {
"message": "제출"
},
+ "submitted": {
+ "message": "제출됨"
+ },
+ "supportCenter": {
+ "message": "지원 센터에 방문하기"
+ },
+ "symbolBetweenZeroTen": {
+ "message": "심볼은 0에서 10개 사이의 문자여야 합니다."
+ },
"takesTooLong": {
"message": "너무 오래걸리나요?"
},
+ "terms": {
+ "message": "사용 지침"
+ },
"testFaucet": {
- "message": "Faucet 테스트"
+ "message": "파우셋 테스트"
},
"to": {
- "message": "대상"
+ "message": "받는이: "
},
"toETHviaShapeShift": {
"message": "ShapeShift를 통해 $1를 ETH로 바꾸기",
- "description": "system will fill in deposit type in start of message"
+ "description": "시스템이 시작할 때에 입금 유형을 입력해줍니다"
+ },
+ "token": {
+ "message": "토큰"
+ },
+ "tokenAddress": {
+ "message": "토큰 주소"
+ },
+ "tokenAlreadyAdded": {
+ "message": "토큰이 이미 추가되어있습니다."
},
"tokenBalance": {
- "message": "현재 토큰 잔액: "
+ "message": "현재 토큰 잔액:"
+ },
+ "tokenSelection": {
+ "message": "토큰을 검색하거나 유명한 토큰 리스트에서 선택하시기 바랍니다."
+ },
+ "tokenSymbol": {
+ "message": "토큰 기호"
+ },
+ "tokenWarning1": {
+ "message": "메타마스크 계좌를 통해 구입한 토큰을 추적합니다. 다른 계정으로 토큰을 구입한 경우 이곳에 나타나지 않습니다."
},
"total": {
"message": "합계"
},
+ "transactions": {
+ "message": "트랜잭션"
+ },
+ "transactionError": {
+ "message": "트랜잭션 오류. 컨트랙트 코드에서 예외 발생(Exception thrown)."
+ },
"transactionMemo": {
"message": "트랜잭션 메모 (선택사항)"
},
@@ -560,23 +903,29 @@
"message": "전송"
},
"troubleTokenBalances": {
- "message": "토큰 잔액을 가져오는데에 문제가 생겼습니다. (여기)서 상세내용을 볼 수 있습니다.",
- "description": "Followed by a link (here) to view token balances"
+ "message": "토큰 잔액을 가져오는데에 문제가 생겼습니다. 링크에서 상세내용을 볼 수 있습니다.",
+ "description": "토큰 잔액을 보려면 (here) 링크를 따라가세요"
+ },
+ "twelveWords": {
+ "message": "12개의 단어는 메타마스크 계정을 복구하기 위한 유일한 방법입니다.\n안전한 장소에 보관하시기 바랍니다."
},
"typePassword": {
- "message": "패스워드를 입력하세요."
+ "message": "비밀번호를 입력하세요"
},
"uiWelcome": {
- "message": "새 UI에 오신 것을 환영합니다. (베타)"
+ "message": "새로운 UI에 오신 것을 환영합니다. (Beta)"
},
"uiWelcomeMessage": {
- "message": "새 메타마스크 UI를 사용하고 계십니다. 토큰 전송과 같은 새 기능들을 사용해보시면서 문제가 있다면 알려주세요."
+ "message": "새로운 메타마스크 UI를 사용하고 계십니다. 토큰 전송과 같은 새 기능들을 사용해보시면서 문제가 있다면 알려주세요."
+ },
+ "unapproved": {
+ "message": "허가안됨"
},
"unavailable": {
- "message": "유효하지 않은"
+ "message": "유효하지 않음"
},
"unknown": {
- "message": "알려지지 않은"
+ "message": "알려지지 않음"
},
"unknownNetwork": {
"message": "알려지지 않은 프라이빗 네트워크"
@@ -584,19 +933,46 @@
"unknownNetworkId": {
"message": "알려지지 않은 네트워크 ID"
},
+ "unlockMessage": {
+ "message": "우리가 기다리던 분권형 웹입니다"
+ },
+ "uriErrorMsg": {
+ "message": "URI는 HTTP/HTTPS로 시작해야 합니다."
+ },
"usaOnly": {
"message": "USA 거주자 한정",
- "description": "Using this exchange is limited to people inside the USA"
+ "description": "해당 거래소는 USA거주자에 한해서만 사용가능합니다"
},
"usedByClients": {
- "message": "다양한 클라이언트에서 사용되고 있습니다."
+ "message": "다양한 클라이언트에서 사용되고 있습니다"
+ },
+ "useOldUI": {
+ "message": "예전 UI 사용"
+ },
+ "validFileImport": {
+ "message": "가져오기 위해 유효한 파일을 선택해야 합니다."
+ },
+ "vaultCreated": {
+ "message": "저장소가 생성됨"
},
"viewAccount": {
- "message": "계좌 보기"
+ "message": "계정 보기"
+ },
+ "viewOnEtherscan": {
+ "message": "이터스캔에서 보기"
+ },
+ "visitWebSite": {
+ "message": "웹사이트 방문"
},
"warning": {
"message": "경고"
},
+ "welcomeBack": {
+ "message": "환영합니다!"
+ },
+ "welcomeBeta": {
+ "message": "메타마스크 Beta에 오신 것을 환영합니다"
+ },
"whatsThis": {
"message": "이것은 무엇인가요?"
},
@@ -604,6 +980,204 @@
"message": "서명이 요청되고 있습니다."
},
"youSign": {
- "message": "서명 중입니다."
+ "message": "서명 중입니다"
+ },
+ "yourPrivateSeedPhrase": {
+ "message": "개인 시드 구문"
+ },
+ "accessingYourCamera": {
+ "message": "카메라 접근중..."
+ },
+ "accountSelectionRequired": {
+ "message": "계정을 선택하셔야 합니다!"
+ },
+ "approve": {
+ "message": "수락"
+ },
+ "browserNotSupported": {
+ "message": "브라우저가 지원하지 않습니다..."
+ },
+ "bytes": {
+ "message": "바이트"
+ },
+ "chromeRequiredForHardwareWallets": {
+ "message": "하드웨어 지갑을 연결하기 위해서는 구글 크롬에서 메타마스크를 사용하셔야 합니다."
+ },
+ "connectHardwareWallet": {
+ "message": "하드웨어 지갑 연결"
+ },
+ "connect": {
+ "message": "연결"
+ },
+ "connecting": {
+ "message": "연결중..."
+ },
+ "connectToLedger": {
+ "message": "Ledger 연결"
+ },
+ "connectToTrezor": {
+ "message": "Trezor 연결"
+ },
+ "copyAddress": {
+ "message": "클립보드로 주소 복사"
+ },
+ "downloadGoogleChrome": {
+ "message": "구글 크롬 다운로드"
+ },
+ "dontHaveAHardwareWallet": {
+ "message": "하드웨어 지갑이 없나요?"
+ },
+ "ensNameNotFound": {
+ "message": "ENS 이름을 찾을 수 없습니다"
+ },
+ "parameters": {
+ "message": "매개변수"
+ },
+ "forgetDevice": {
+ "message": "장치 연결 해제"
+ },
+ "functionType": {
+ "message": "함수 유형"
+ },
+ "getHelp": {
+ "message": "도움말"
+ },
+ "hardware": {
+ "message": "하드웨어"
+ },
+ "hardwareWalletConnected": {
+ "message": "하드웨어 지갑이 연결됨"
+ },
+ "hardwareWallets": {
+ "message": "하드웨어 지갑 연결"
+ },
+ "hardwareWalletsMsg": {
+ "message": "메타마스크에서 사용할 하드웨어 지갑을 선택해주세요"
+ },
+ "havingTroubleConnecting": {
+ "message": "연결에 문제가 있나요?"
+ },
+ "hexData": {
+ "message": "Hex 데이터"
+ },
+ "invalidSeedPhrase": {
+ "message": "잘못된 시드 구문"
+ },
+ "ledgerAccountRestriction": {
+ "message": "새 계정을 추가하려면 최소 마지막 계정을 사용해야 합니다."
+ },
+ "menu": {
+ "message": "메뉴"
+ },
+ "noConversionRateAvailable": {
+ "message": "변환 비율을 찾을 수 없습니다"
+ },
+ "notFound": {
+ "message": "찾을 수 없음"
+ },
+ "noWebcamFoundTitle": {
+ "message": "웹캠이 없습니다"
+ },
+ "noWebcamFound": {
+ "message": "컴퓨터의 웹캠을 찾을 수 없습니다. 다시 시도해보세요."
+ },
+ "openInTab": {
+ "message": "탭으로 열기"
+ },
+ "origin": {
+ "message": "Origin"
+ },
+ "prev": {
+ "message": "이전"
+ },
+ "restoreAccountWithSeed": {
+ "message": "시드 구문으로 계정 복구하기"
+ },
+ "restore": {
+ "message": "복구"
+ },
+ "remove": {
+ "message": "제거"
+ },
+ "removeAccount": {
+ "message": "계정 제거"
+ },
+ "removeAccountDescription": {
+ "message": "이 계정한 지갑에서 삭제될 것입니다. 지우기 전에 이 계정에 대한 개인 키 혹은 시드 구문을 가지고 있는지 확인하세요. 계정 드랍다운 메뉴를 통해서 계정을 가져오거나 생성할 수 있습니다."
+ },
+ "readyToConnect": {
+ "message": "접속 준비되었나요?"
+ },
+ "separateEachWord": {
+ "message": "각 단어는 공백 한칸으로 분리합니다"
+ },
+ "orderOneHere": {
+ "message": "Trezor 혹은 Ledger를 구입하고 자금을 콜드 스토리지에 저장합니다"
+ },
+ "selectAnAddress": {
+ "message": "주소 선택"
+ },
+ "selectAnAccount": {
+ "message": "계정 선택"
+ },
+ "selectAnAccountHelp": {
+ "message": "메타마스크에서 보기위한 계정 선택"
+ },
+ "selectHdPath": {
+ "message": "HD 경로 지정"
+ },
+ "selectPathHelp": {
+ "message": "하단에서 Ledger지갑 계정을 찾지 못하겠으면 \"Legacy (MEW / MyCrypto)\" 경로로 바꿔보세요"
+ },
+ "step1HardwareWallet": {
+ "message": "1. 하드웨어 지갑 연결"
+ },
+ "step1HardwareWalletMsg": {
+ "message": "하드웨어 지갑을 컴퓨터에 연결해주세요."
+ },
+ "step2HardwareWallet": {
+ "message": "2. 계정 선택"
+ },
+ "step2HardwareWalletMsg": {
+ "message": "보고 싶은 계정을 선택합니다. 한번에 하나의 계정만 선택할 수 있습니다."
+ },
+ "step3HardwareWallet": {
+ "message": "3. dApps을 사용하거나 다른 것을 합니다!"
+ },
+ "step3HardwareWalletMsg": {
+ "message": "다른 이더리움 계정을 사용하듯 하드웨어 계정을 사용합니다. dApps을 로그인하거나, 이더를 보내거나, ERC20토큰 혹은 대체가능하지 않은 토큰 (예를 들어 CryptoKitties)을 사거나 저장하거나 합니다."
+ },
+ "scanInstructions": {
+ "message": "QR 코드를 카메라 앞에 가져다 놓아주세요"
+ },
+ "scanQrCode": {
+ "message": "QR 코드 스캔"
+ },
+ "transfer": {
+ "message": "전송"
+ },
+ "trezorHardwareWallet": {
+ "message": "TREZOR 하드웨어 지갑"
+ },
+ "tryAgain": {
+ "message": "다시 시도하세요"
+ },
+ "unknownFunction": {
+ "message": "알 수 없는 함수"
+ },
+ "unknownQrCode": {
+ "message": "오류: QR 코드를 확인할 수 없습니다"
+ },
+ "unknownCameraErrorTitle": {
+ "message": "이런! 뭔가 잘못되었습니다...."
+ },
+ "unknownCameraError": {
+ "message": "카메라를 접근하는 중 오류가 발생했습니다. 다시 시도해 주세요..."
+ },
+ "unlock": {
+ "message": "잠금 해제"
+ },
+ "youNeedToAllowCameraAccess": {
+ "message": "이 기능을 사용하려면 카메라 접근을 허용해야 합니다."
}
}
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index bb722735d..6344e1beb 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -784,7 +784,7 @@
"message": "Тестовый кран"
},
"to": {
- "message": "Получатель: "
+ "message": "Получатель"
},
"toETHviaShapeShift": {
"message": "$1 в ETH через ShapeShift",
diff --git a/app/_locales/tml/messages.json b/app/_locales/tml/messages.json
index fcc418bac..4f733458e 100644
--- a/app/_locales/tml/messages.json
+++ b/app/_locales/tml/messages.json
@@ -796,7 +796,7 @@
"message": "சோதனை குழாய்"
},
"to": {
- "message": "பெறுநர்: "
+ "message": "பெறுநர்"
},
"toETHviaShapeShift": {
"message": "$ 1 முதல் ETH வரை வடிவம்",
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index 08ba6cde8..8be695108 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -796,7 +796,7 @@
"message": "Test Musluğu"
},
"to": {
- "message": "Kime: "
+ "message": "Kime"
},
"toETHviaShapeShift": {
"message": "ShapeShift üstünden $1'dan ETH'e",
diff --git a/app/manifest.json b/app/manifest.json
index 1e12f35b4..7793c6e38 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -71,7 +71,6 @@
"activeTab",
"webRequest",
"*://*.eth/",
- "*://*.test/",
"notifications"
],
"web_accessible_resources": [
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 1599b8f99..e13d2842a 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -256,6 +256,7 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
+ showWatchAssetUi: showWatchAssetUi,
// initial state
initState,
// initial locale code
@@ -451,9 +452,28 @@ function triggerUi () {
})
}
+/**
+ * Opens the browser popup for user confirmation of watchAsset
+ * then it waits until user interact with the UI
+ */
+function showWatchAssetUi () {
+ triggerUi()
+ return new Promise(
+ (resolve) => {
+ var interval = setInterval(() => {
+ if (!notificationIsOpen) {
+ clearInterval(interval)
+ resolve()
+ }
+ }, 1000)
+ }
+ )
+}
+
// On first install, open a window to MetaMask website to how-it-works.
extension.runtime.onInstalled.addListener(function (details) {
if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
}
})
+
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 707fd7de9..464a37017 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -1,5 +1,6 @@
const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize
+const { isValidAddress } = require('ethereumjs-util')
const extend = require('xtend')
@@ -14,6 +15,7 @@ class PreferencesController {
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
+ * @property {object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature
@@ -26,7 +28,9 @@ class PreferencesController {
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
+ assetImages: {},
tokens: [],
+ suggestedTokens: {},
useBlockie: false,
featureFlags: {},
currentLocale: opts.initLangCode,
@@ -37,6 +41,7 @@ class PreferencesController {
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
+ this.showWatchAssetUi = opts.showWatchAssetUi
this._subscribeProviderType()
}
// PUBLIC METHODS
@@ -51,6 +56,53 @@ class PreferencesController {
this.store.updateState({ useBlockie: val })
}
+ getSuggestedTokens () {
+ return this.store.getState().suggestedTokens
+ }
+
+ getAssetImages () {
+ return this.store.getState().assetImages
+ }
+
+ addSuggestedERC20Asset (tokenOpts) {
+ this._validateERC20AssetParams(tokenOpts)
+ const suggested = this.getSuggestedTokens()
+ const { rawAddress, symbol, decimals, image } = tokenOpts
+ const address = normalizeAddress(rawAddress)
+ const newEntry = { address, symbol, decimals, image }
+ suggested[address] = newEntry
+ this.store.updateState({ suggestedTokens: suggested })
+ }
+
+ /**
+ * RPC engine middleware for requesting new asset added
+ *
+ * @param req
+ * @param res
+ * @param {Function} - next
+ * @param {Function} - end
+ */
+ async requestWatchAsset (req, res, next, end) {
+ if (req.method === 'metamask_watchAsset') {
+ const { type, options } = req.params
+ switch (type) {
+ case 'ERC20':
+ const result = await this._handleWatchAssetERC20(options)
+ if (result instanceof Error) {
+ end(result)
+ } else {
+ res.result = result
+ end()
+ }
+ break
+ default:
+ end(new Error(`Asset of type ${type} not supported`))
+ }
+ } else {
+ next()
+ }
+ }
+
/**
* Getter for the `useBlockie` property
*
@@ -186,6 +238,13 @@ class PreferencesController {
return selected
}
+ removeSuggestedTokens () {
+ return new Promise((resolve, reject) => {
+ this.store.updateState({ suggestedTokens: {} })
+ resolve({})
+ })
+ }
+
/**
* Setter for the `selectedAddress` property
*
@@ -232,11 +291,11 @@ class PreferencesController {
* @returns {Promise<array>} Promises the new array of AddedToken objects.
*
*/
- async addToken (rawAddress, symbol, decimals) {
+ async addToken (rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
-
const tokens = this.store.getState().tokens
+ const assetImages = this.getAssetImages()
const previousEntry = tokens.find((token, index) => {
return token.address === address
})
@@ -247,7 +306,8 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}
- this._updateAccountTokens(tokens)
+ assetImages[address] = image
+ this._updateAccountTokens(tokens, assetImages)
return Promise.resolve(tokens)
}
@@ -260,8 +320,10 @@ class PreferencesController {
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens
+ const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
- this._updateAccountTokens(updatedTokens)
+ delete assetImages[rawAddress]
+ this._updateAccountTokens(updatedTokens, assetImages)
return Promise.resolve(updatedTokens)
}
@@ -322,7 +384,7 @@ class PreferencesController {
/**
* Returns an updated rpcList based on the passed url and the current list.
- * The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the
+ * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
* end of the list. The current list is modified and returned as a promise.
*
* @param {string} _url The rpc url to add to the frequentRpcList.
@@ -338,7 +400,7 @@ class PreferencesController {
if (_url !== 'http://localhost:8545') {
rpcList.push(_url)
}
- if (rpcList.length > 2) {
+ if (rpcList.length > 3) {
rpcList.shift()
}
return Promise.resolve(rpcList)
@@ -387,6 +449,7 @@ class PreferencesController {
//
// PRIVATE METHODS
//
+
/**
* Subscription to network provider type.
*
@@ -405,10 +468,10 @@ class PreferencesController {
* @param {array} tokens Array of tokens to be updated.
*
*/
- _updateAccountTokens (tokens) {
+ _updateAccountTokens (tokens, assetImages) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
- this.store.updateState({ accountTokens, tokens })
+ this.store.updateState({ accountTokens, tokens, assetImages })
}
/**
@@ -438,6 +501,47 @@ class PreferencesController {
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
+
+ /**
+ * Handle the suggestion of an ERC20 asset through `watchAsset`
+ * *
+ * @param {Promise} promise Promise according to addition of ERC20 token
+ *
+ */
+ async _handleWatchAssetERC20 (options) {
+ const { address, symbol, decimals, image } = options
+ const rawAddress = address
+ try {
+ this._validateERC20AssetParams({ rawAddress, symbol, decimals })
+ } catch (err) {
+ return err
+ }
+ const tokenOpts = { rawAddress, decimals, symbol, image }
+ this.addSuggestedERC20Asset(tokenOpts)
+ return this.showWatchAssetUi().then(() => {
+ const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
+ return tokenAddresses.length > 0
+ })
+ }
+
+ /**
+ * Validates that the passed options for suggested token have all required properties.
+ *
+ * @param {Object} opts The options object to validate
+ * @throws {string} Throw a custom error indicating that address, symbol and/or decimals
+ * doesn't fulfill requirements
+ *
+ */
+ _validateERC20AssetParams (opts) {
+ const { rawAddress, symbol, decimals } = opts
+ if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
+ if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
+ const numDecimals = parseInt(decimals, 10)
+ if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
+ throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
+ }
+ if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
+ }
}
module.exports = PreferencesController
diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js
index b7e2c7cbe..3a52d5e8d 100644
--- a/app/scripts/lib/account-tracker.js
+++ b/app/scripts/lib/account-tracker.js
@@ -43,10 +43,24 @@ class AccountTracker {
this._provider = opts.provider
this._query = pify(new EthQuery(this._provider))
this._blockTracker = opts.blockTracker
- // subscribe to latest block
- this._blockTracker.on('latest', this._updateForBlock.bind(this))
// blockTracker.currentBlock may be null
this._currentBlockNumber = this._blockTracker.getCurrentBlock()
+ // bind function for easier listener syntax
+ this._updateForBlock = this._updateForBlock.bind(this)
+ }
+
+ start () {
+ // remove first to avoid double add
+ this._blockTracker.removeListener('latest', this._updateForBlock)
+ // add listener
+ this._blockTracker.addListener('latest', this._updateForBlock)
+ // fetch account balances
+ this._updateAccounts()
+ }
+
+ stop () {
+ // remove listener
+ this._blockTracker.removeListener('latest', this._updateForBlock)
}
/**
diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js
index 5db63f47d..38682b916 100644
--- a/app/scripts/lib/ipfsContent.js
+++ b/app/scripts/lib/ipfsContent.js
@@ -34,7 +34,7 @@ module.exports = function (provider) {
return { cancel: true }
}
- extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
+ extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']})
return {
remove () {
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 29838ad2d..98cb62bfa 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -67,6 +67,10 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
+ // this keeps track of how many "controllerStream" connections are open
+ // the only thing that uses controller connections are open metamask UI instances
+ this.activeControllerConnections = 0
+
// platform-specific api
this.platform = opts.platform
@@ -88,6 +92,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
+ showWatchAssetUi: opts.showWatchAssetUi,
network: this.networkController,
})
@@ -127,6 +132,14 @@ module.exports = class MetamaskController extends EventEmitter {
provider: this.provider,
blockTracker: this.blockTracker,
})
+ // start and stop polling for balances based on activeControllerConnections
+ this.on('controllerConnectionChanged', (activeControllerConnections) => {
+ if (activeControllerConnections > 0) {
+ this.accountTracker.start()
+ } else {
+ this.accountTracker.stop()
+ }
+ })
// key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
@@ -137,19 +150,7 @@ module.exports = class MetamaskController extends EventEmitter {
encryptor: opts.encryptor || undefined,
})
- // If only one account exists, make sure it is selected.
- this.keyringController.memStore.subscribe((state) => {
- const addresses = state.keyrings.reduce((res, keyring) => {
- return res.concat(keyring.accounts)
- }, [])
- if (addresses.length === 1) {
- const address = addresses[0]
- this.preferencesController.setSelectedAddress(address)
- }
- // ensure preferences + identities controller know about all addresses
- this.preferencesController.addAddresses(addresses)
- this.accountTracker.syncWithAddresses(addresses)
- })
+ this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
// detect tokens controller
this.detectTokensController = new DetectTokensController({
@@ -386,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter {
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController),
+ removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
@@ -1209,11 +1211,19 @@ module.exports = class MetamaskController extends EventEmitter {
setupControllerConnection (outStream) {
const api = this.getApi()
const dnode = Dnode(api)
+ // report new active controller connection
+ this.activeControllerConnections++
+ this.emit('controllerConnectionChanged', this.activeControllerConnections)
+ // connect dnode api to remote connection
pump(
outStream,
dnode,
outStream,
(err) => {
+ // report new active controller connection
+ this.activeControllerConnections--
+ this.emit('controllerConnectionChanged', this.activeControllerConnections)
+ // report any error
if (err) log.error(err)
}
)
@@ -1242,6 +1252,7 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(createOriginMiddleware({ origin }))
engine.push(createLoggerMiddleware({ origin }))
engine.push(filterMiddleware)
+ engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
engine.push(createProviderMiddleware({ provider: this.provider }))
// setup connection
@@ -1279,6 +1290,34 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
+ * Handle a KeyringController update
+ * @param {object} state the KC state
+ * @return {Promise<void>}
+ * @private
+ */
+ async _onKeyringControllerUpdate (state) {
+ const {isUnlocked, keyrings} = state
+ const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), [])
+
+ if (!addresses.length) {
+ return
+ }
+
+ // Ensure preferences + identities controller know about all addresses
+ this.preferencesController.addAddresses(addresses)
+ this.accountTracker.syncWithAddresses(addresses)
+
+ const wasLocked = !isUnlocked
+ if (wasLocked) {
+ const oldSelectedAddress = this.preferencesController.getSelectedAddress()
+ if (!addresses.includes(oldSelectedAddress)) {
+ const address = addresses[0]
+ await this.preferencesController.setSelectedAddress(address)
+ }
+ }
+ }
+
+ /**
* A method for emitting the full MetaMask state to all registered listeners.
* @private
*/
@@ -1424,6 +1463,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
+ // TODO: Replace isClientOpen methods with `controllerConnectionChanged` events.
/**
* A method for recording whether the MetaMask user interface is open or not.
* @private
diff --git a/development/states/add-token.json b/development/states/add-token.json
index 84ad5dd4c..d04b3a3ca 100644
--- a/development/states/add-token.json
+++ b/development/states/add-token.json
@@ -123,6 +123,7 @@
"modalState": {},
"previousModalState": {}
},
+ "sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,
diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json
index 2c2e17704..fffee9893 100644
--- a/development/states/confirm-new-ui.json
+++ b/development/states/confirm-new-ui.json
@@ -141,6 +141,7 @@
"accountDetail": {
"subview": "transactions"
},
+ "sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}
diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json
index 829f513a8..5017a4d57 100644
--- a/development/states/confirm-sig-requests.json
+++ b/development/states/confirm-sig-requests.json
@@ -162,6 +162,7 @@
"accountDetail": {
"subview": "transactions"
},
+ "sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}
diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json
index 6848c0840..847ea11a3 100644
--- a/development/states/currency-localization.json
+++ b/development/states/currency-localization.json
@@ -120,6 +120,7 @@
"accountDetail": {
"subview": "transactions"
},
+ "sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}
diff --git a/development/states/first-time.json b/development/states/first-time.json
index f44148973..a31b985a3 100644
--- a/development/states/first-time.json
+++ b/development/states/first-time.json
@@ -48,6 +48,7 @@
"accountDetail": {
"subview": "transactions"
},
+ "sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,
diff --git a/development/states/send-edit.json b/development/states/send-edit.json
index 8e5c25a82..6330b777d 100644
--- a/development/states/send-edit.json
+++ b/development/states/send-edit.json
@@ -22,6 +22,7 @@
"name": "Send Account 4"
}
},
+ "assetImages": {},
"unapprovedTxs": {},
"currentCurrency": "USD",
"conversionRate": 1200.88200327,
@@ -141,6 +142,7 @@
"accountDetail": {
"subview": "transactions"
},
+ "sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}
diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json
index ad2ff3d6e..bb4847155 100644
--- a/development/states/send-new-ui.json
+++ b/development/states/send-new-ui.json
@@ -61,6 +61,7 @@
"name": "Address Book Account 1"
}
],
+ "assetImages": {},
"tokens": [],
"transactions": {},
"selectedAddressTxList": [],
@@ -120,6 +121,7 @@
"accountDetail": {
"subview": "transactions"
},
+ "sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}
diff --git a/development/states/send.json b/development/states/send.json
index 73ac62f65..4c67f8ac6 100644
--- a/development/states/send.json
+++ b/development/states/send.json
@@ -21,6 +21,7 @@
"name": "Account 4"
}
},
+ "assetImages": {},
"unapprovedTxs": {},
"currentCurrency": "USD",
"conversionRate": 16.88200327,
@@ -99,6 +100,7 @@
"accountExport": "none",
"privateKey": ""
},
+ "sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,
diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json
index f22fd0a56..0d2273cb0 100644
--- a/development/states/tx-list-items.json
+++ b/development/states/tx-list-items.json
@@ -118,6 +118,7 @@
"modalState": {},
"previousModalState": {}
},
+ "sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,
diff --git a/old-ui/app/account-detail.js b/old-ui/app/account-detail.js
index c67f0cf71..d240fc38e 100644
--- a/old-ui/app/account-detail.js
+++ b/old-ui/app/account-detail.js
@@ -32,6 +32,7 @@ function mapStateToProps (state) {
currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab,
tokens: state.metamask.tokens,
+ suggestedTokens: state.metamask.suggestedTokens,
computedBalances: state.metamask.computedBalances,
}
}
@@ -49,6 +50,10 @@ AccountDetailScreen.prototype.render = function () {
var account = props.accounts[selected]
const { network, conversionRate, currentCurrency } = props
+ if (Object.keys(props.suggestedTokens).length > 0) {
+ this.props.dispatch(actions.showAddSuggestedTokenPage())
+ }
+
return (
h('.account-detail-section.full-flex-height', [
diff --git a/old-ui/app/add-suggested-token.js b/old-ui/app/add-suggested-token.js
new file mode 100644
index 000000000..ea534b7da
--- /dev/null
+++ b/old-ui/app/add-suggested-token.js
@@ -0,0 +1,202 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../ui/app/actions')
+const Tooltip = require('./components/tooltip.js')
+const ethUtil = require('ethereumjs-util')
+const Copyable = require('./components/copyable')
+const addressSummary = require('./util').addressSummary
+
+
+module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen)
+
+function mapStateToProps (state) {
+ return {
+ identities: state.metamask.identities,
+ suggestedTokens: state.metamask.suggestedTokens,
+ }
+}
+
+inherits(AddSuggestedTokenScreen, Component)
+function AddSuggestedTokenScreen () {
+ this.state = {
+ warning: null,
+ }
+ Component.call(this)
+}
+
+AddSuggestedTokenScreen.prototype.render = function () {
+ const state = this.state
+ const props = this.props
+ const { warning } = state
+ const key = Object.keys(props.suggestedTokens)[0]
+ const { address, symbol, decimals } = props.suggestedTokens[key]
+
+ return (
+ h('.flex-column.flex-grow', [
+
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ h('h2.page-subtitle', 'Add Suggested Token'),
+ ]),
+
+ h('.error', {
+ style: {
+ display: warning ? 'block' : 'none',
+ padding: '0 20px',
+ textAlign: 'center',
+ },
+ }, warning),
+
+ // conf view
+ h('.flex-column.flex-justify-center.flex-grow.select-none', [
+ h('.flex-space-around', {
+ style: {
+ padding: '20px',
+ },
+ }, [
+
+ h('div', [
+ h(Tooltip, {
+ position: 'top',
+ title: 'The contract of the actual token contract. Click for more info.',
+ }, [
+ h('a', {
+ style: { fontWeight: 'bold', paddingRight: '10px'},
+ href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
+ target: '_blank',
+ }, [
+ h('span', 'Token Contract Address '),
+ h('i.fa.fa-question-circle'),
+ ]),
+ ]),
+ ]),
+
+ h('div', {
+ style: { display: 'flex' },
+ }, [
+ h(Copyable, {
+ value: ethUtil.toChecksumAddress(address),
+ }, [
+ h('span#token-address', {
+ style: {
+ width: 'inherit',
+ flex: '1 0 auto',
+ height: '30px',
+ margin: '8px',
+ display: 'flex',
+ },
+ }, addressSummary(address, 24, 4, false)),
+ ]),
+ ]),
+
+ h('div', [
+ h('span', {
+ style: { fontWeight: 'bold', paddingRight: '10px'},
+ }, 'Token Symbol'),
+ ]),
+
+ h('div', { style: {display: 'flex'} }, [
+ h('p#token_symbol', {
+ style: {
+ width: 'inherit',
+ flex: '1 0 auto',
+ height: '30px',
+ margin: '8px',
+ },
+ }, symbol),
+ ]),
+
+ h('div', [
+ h('span', {
+ style: { fontWeight: 'bold', paddingRight: '10px'},
+ }, 'Decimals of Precision'),
+ ]),
+
+ h('div', { style: {display: 'flex'} }, [
+ h('p#token_decimals', {
+ type: 'number',
+ style: {
+ width: 'inherit',
+ flex: '1 0 auto',
+ height: '30px',
+ margin: '8px',
+ },
+ }, decimals),
+ ]),
+
+ h('button', {
+ style: {
+ alignSelf: 'center',
+ margin: '8px',
+ },
+ onClick: (event) => {
+ this.props.dispatch(actions.removeSuggestedTokens())
+ },
+ }, 'Cancel'),
+
+ h('button', {
+ style: {
+ alignSelf: 'center',
+ margin: '8px',
+ },
+ onClick: (event) => {
+ const valid = this.validateInputs({ address, symbol, decimals })
+ if (!valid) return
+
+ this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
+ .then(() => {
+ this.props.dispatch(actions.removeSuggestedTokens())
+ })
+ },
+ }, 'Add'),
+ ]),
+ ]),
+ ])
+ )
+}
+
+AddSuggestedTokenScreen.prototype.componentWillMount = function () {
+ if (typeof global.ethereumProvider === 'undefined') return
+}
+
+AddSuggestedTokenScreen.prototype.validateInputs = function (opts) {
+ let msg = ''
+ const identitiesList = Object.keys(this.props.identities)
+ const { address, symbol, decimals } = opts
+ const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
+
+ const validAddress = ethUtil.isValidAddress(address)
+ if (!validAddress) {
+ msg += 'Address is invalid.'
+ }
+
+ const validDecimals = decimals >= 0 && decimals <= 36
+ if (!validDecimals) {
+ msg += 'Decimals must be at least 0, and not over 36. '
+ }
+
+ const symbolLen = symbol.trim().length
+ const validSymbol = symbolLen > 0 && symbolLen < 10
+ if (!validSymbol) {
+ msg += 'Symbol must be between 0 and 10 characters.'
+ }
+
+ const ownAddress = identitiesList.includes(standardAddress)
+ if (ownAddress) {
+ msg = 'Personal address detected. Input the token contract address.'
+ }
+
+ const isValid = validAddress && validDecimals && !ownAddress
+
+ if (!isValid) {
+ this.setState({
+ warning: msg,
+ })
+ } else {
+ this.setState({ warning: null })
+ }
+
+ return isValid
+}
diff --git a/old-ui/app/add-token.js b/old-ui/app/add-token.js
index e869ac39a..6cf211636 100644
--- a/old-ui/app/add-token.js
+++ b/old-ui/app/add-token.js
@@ -196,7 +196,7 @@ AddTokenScreen.prototype.validateInputs = function () {
msg += 'Address is invalid.'
}
- const validDecimals = decimals >= 0 && decimals < 36
+ const validDecimals = decimals >= 0 && decimals <= 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
}
diff --git a/old-ui/app/app.js b/old-ui/app/app.js
index d3e9e823b..9be21ebad 100644
--- a/old-ui/app/app.js
+++ b/old-ui/app/app.js
@@ -23,6 +23,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
const AddTokenScreen = require('./add-token')
+const AddSuggestedTokenScreen = require('./add-suggested-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
const NewUiAnnouncement = require('./new-ui-annoucement')
@@ -74,6 +75,7 @@ function mapStateToProps (state) {
lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
featureFlags,
+ suggestedTokens: state.metamask.suggestedTokens,
// state needed to get account dropdown temporarily rendering from app bar
identities,
@@ -236,6 +238,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering add-token screen from unlock screen.')
return h(AddTokenScreen, {key: 'add-token'})
+ case 'add-suggested-token':
+ log.debug('rendering add-suggested-token screen from unlock screen.')
+ return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'})
+
case 'config':
log.debug('rendering config screen')
return h(ConfigScreen, {key: 'config'})
diff --git a/old-ui/app/components/app-bar.js b/old-ui/app/components/app-bar.js
index 8ab647efd..234c06a01 100644
--- a/old-ui/app/components/app-bar.js
+++ b/old-ui/app/components/app-bar.js
@@ -350,11 +350,14 @@ module.exports = class AppBar extends Component {
}
}
- renderCommonRpc (rpcList, {rpcTarget}) {
+ renderCommonRpc (rpcList, provider) {
const {dispatch} = this.props
+ const reversedRpcList = rpcList.slice().reverse()
- return rpcList.map((rpc) => {
- if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) {
+ return reversedRpcList.map((rpc) => {
+ const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
+
+ if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) {
return null
} else {
return h(DropdownMenuItem, {
@@ -364,7 +367,7 @@ module.exports = class AppBar extends Component {
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
- rpcTarget === rpc
+ currentRpcTarget
? h('.check', '✓')
: null,
])
diff --git a/package-lock.json b/package-lock.json
index a40bb112c..93c3f13a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16169,6 +16169,14 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
+ "json2mq": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+ "integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=",
+ "requires": {
+ "string-convert": "^0.2.0"
+ }
+ },
"json3": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
@@ -25028,6 +25036,16 @@
"xtend": "^4.0.1"
}
},
+ "react-media": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/react-media/-/react-media-1.8.0.tgz",
+ "integrity": "sha512-XcfqkDQj5/hmJod/kXUAZljJyMVkWrBWOkzwynAR8BXOGlbFLGBwezM0jQHtp2BrSymhf14/XrQrb3gGBnGK4g==",
+ "requires": {
+ "invariant": "^2.2.2",
+ "json2mq": "^0.2.0",
+ "prop-types": "^15.5.10"
+ }
+ },
"react-modal": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.4.4.tgz",
@@ -27885,6 +27903,11 @@
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
+ "string-convert": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+ "integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c="
+ },
"string-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz",
diff --git a/package.json b/package.json
index 01e51fd9d..beb122a3c 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,11 @@
[
"env",
{
- "debug": true
+ "browsers": [
+ ">0.25%",
+ "not ie 11",
+ "not op_mini all"
+ ]
}
],
"stage-0"
@@ -143,7 +147,7 @@
"gulp-eslint": "^4.0.0",
"gulp-sass": "^4.0.0",
"hat": "0.0.3",
- "human-standard-token-abi": "^1.0.2",
+ "human-standard-token-abi": "^2.0.0",
"idb-global": "^2.1.0",
"identicon.js": "^2.3.1",
"iframe": "^1.0.0",
@@ -185,6 +189,7 @@
"react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0",
"react-markdown": "^3.0.0",
+ "react-media": "^1.8.0",
"react-redux": "^5.0.5",
"react-router-dom": "^4.2.2",
"react-select": "^1.0.0",
@@ -246,8 +251,8 @@
"del": "^3.0.0",
"dot-only-hunter": "^1.0.3",
"envify": "^4.0.0",
- "enzyme": "^3.3.0",
- "enzyme-adapter-react-15": "^1.0.5",
+ "enzyme": "^3.4.4",
+ "enzyme-adapter-react-15": "^1.0.6",
"eslint-plugin-chai": "0.0.1",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0",
diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js
index abc684b4f..5ee8f0268 100644
--- a/test/e2e/beta/from-import-beta-ui.spec.js
+++ b/test/e2e/beta/from-import-beta-ui.spec.js
@@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () {
})
it('finds the transaction in the transactions list', async function () {
- const transactions = await findElements(driver, By.css('.tx-list-item'))
+ const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
- const txValues = await findElements(driver, By.css('.tx-list-value'))
+ const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
assert.equal(txValues.length, 1)
- assert.equal(await txValues[0].getText(), '1 ETH')
+ assert.equal(await txValues[0].getText(), '-1 ETH')
})
})
diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js
index dd327accb..c9f759780 100644
--- a/test/e2e/beta/metamask-beta-ui.spec.js
+++ b/test/e2e/beta/metamask-beta-ui.spec.js
@@ -225,19 +225,9 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
}
- await clickWordAndWait(words[0])
- await clickWordAndWait(words[1])
- await clickWordAndWait(words[2])
- await clickWordAndWait(words[3])
- await clickWordAndWait(words[4])
- await clickWordAndWait(words[5])
- await clickWordAndWait(words[6])
- await clickWordAndWait(words[7])
- await clickWordAndWait(words[8])
- await clickWordAndWait(words[9])
- await clickWordAndWait(words[10])
- await clickWordAndWait(words[11])
-
+ for (let i = 0; i < 12; i++) {
+ await clickWordAndWait(words[i])
+ }
} catch (e) {
if (count > 2) {
throw e
@@ -414,12 +404,12 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
- const transactions = await findElements(driver, By.css('.tx-list-item'))
+ const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
- const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
+ const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
+ await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
}
})
})
@@ -457,14 +447,11 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
- const transactions = await findElements(driver, By.css('.tx-list-item'))
+ const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2)
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
-
- const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
+ const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
+ await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
})
})
@@ -487,9 +474,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
+ const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
await txListItem.click()
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
})
it('displays the contract creation data', async () => {
@@ -511,13 +498,15 @@ describe('MetaMask', function () {
it('confirms a deploy contract transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
+ driver.wait(async () => {
+ const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
+ return confirmedTxes.length === 3
+ }, 10000)
- const txAccounts = await findElements(driver, By.css('.tx-list-account'))
- assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
+ const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
+ await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
await delay(regularDelayMs)
})
@@ -538,9 +527,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(largeDelayMs)
- await findElements(driver, By.css('.tx-list-pending-item-container'))
- const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
+ await findElements(driver, By.css('.transaction-list-item'))
+ const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
+ await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@@ -568,15 +557,17 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
+ driver.wait(async () => {
+ const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
+ return confirmedTxes.length === 4
+ }, 10000)
- const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
+ const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
+ await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
- const txAccounts = await findElements(driver, By.css('.tx-list-account'))
- const firstTxAddress = await txAccounts[0].getText()
- assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
+ // const txAccounts = await findElements(driver, By.css('.tx-list-account'))
+ // const firstTxAddress = await txAccounts[0].getText()
+ // assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
})
it('calls and confirms a contract method where ETH is received', async () => {
@@ -590,7 +581,7 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- const txListItem = await findElement(driver, By.css('.tx-list-item'))
+ const txListItem = await findElement(driver, By.css('.transaction-list-item'))
await txListItem.click()
await delay(regularDelayMs)
@@ -598,18 +589,20 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
+ driver.wait(async () => {
+ const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
+ return confirmedTxes.length === 5
+ }, 10000)
- const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
+ const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
+ await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
})
it('renders the correct ETH balance', async () => {
- const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
+ const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
@@ -654,18 +647,17 @@ describe('MetaMask', function () {
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await delay(regularDelayMs)
await driver.switchTo().window(extension)
- await delay(regularDelayMs)
-
+ await delay(largeDelayMs)
})
it('clicks on the Add Token button', async () => {
- const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
+ const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
await addToken.click()
await delay(regularDelayMs)
})
it('picks the newly created Test token', async () => {
- const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
+ const addCustomToken = await findElement(driver, By.xpath("//li[contains(text(), 'Custom Token')]"))
await addCustomToken.click()
await delay(regularDelayMs)
@@ -683,7 +675,7 @@ describe('MetaMask', function () {
})
it('renders the balance for the new token', async () => {
- const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
+ const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
const tokenAmount = await balance.getText()
assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
@@ -752,21 +744,25 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
- const transactions = await findElements(driver, By.css('.tx-list-item'))
+ const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
- const txValues = await findElements(driver, By.css('.tx-list-value'))
+ const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
- await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
+ await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
}
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
- assert.equal(await tx.getText(), 'Confirmed')
+ driver.wait(async () => {
+ const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
+ return confirmedTxes.length === 1
+ }, 10000)
+ const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
+ const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000)
+ assert.equal(await tx.getText(), 'Sent Tokens')
})
})
@@ -789,9 +785,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(largeDelayMs)
- await findElements(driver, By.css('.tx-list-pending-item-container'))
- const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
+ await findElements(driver, By.css('.transaction-list__pending-transactions'))
+ const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
+ await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@@ -838,25 +834,28 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
- const transactions = await findElements(driver, By.css('.tx-list-item'))
- assert.equal(transactions.length, 2)
+ driver.wait(async () => {
+ const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
+ return confirmedTxes.length === 2
+ }, 10000)
- const txValues = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
+ const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
+ await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
+ const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
+ await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
await walletBalance.click()
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
await tokenListItems[0].click()
+ await delay(regularDelayMs)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
- const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
- assert.equal(await tokenBalanceAmount.getText(), '43')
+ const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
+ assert.equal(await tokenBalanceAmount.getText(), '43 TST')
}
})
})
@@ -880,9 +879,14 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
- const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
+ driver.wait(async () => {
+ const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item'))
+ return pendingTxes.length === 1
+ }, 10000)
+
+ const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
+ const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
+ await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
await txListItem.click()
await delay(regularDelayMs)
})
@@ -953,10 +957,15 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
- const txValues = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
- const txStatuses = await findElements(driver, By.css('.tx-list-status'))
- await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
+ driver.wait(async () => {
+ const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
+ return confirmedTxes.length === 3
+ }, 10000)
+
+ const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
+ await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
+ const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
+ await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
})
@@ -1006,9 +1015,69 @@ describe('MetaMask', function () {
})
it('renders the balance for the chosen token', async () => {
- const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
+ const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
await delay(regularDelayMs)
})
})
-}) \ No newline at end of file
+
+ describe('Stores custom RPC history', () => {
+ const customRpcUrls = [
+ 'https://mainnet.infura.io/1',
+ 'https://mainnet.infura.io/2',
+ 'https://mainnet.infura.io/3',
+ 'https://mainnet.infura.io/4',
+ ]
+
+ customRpcUrls.forEach(customRpcUrl => {
+ it('creates custom RPC: ' + customRpcUrl, async () => {
+ const networkDropdown = await findElement(driver, By.css('.network-name'))
+ await networkDropdown.click()
+ await delay(regularDelayMs)
+
+ const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
+ await customRpcButton.click()
+ await delay(regularDelayMs)
+
+ const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]'))
+ await customRpcInput.clear()
+ await customRpcInput.sendKeys(customRpcUrl)
+
+ const customRpcSave = await findElement(driver, By.css('.settings__rpc-save-button'))
+ await customRpcSave.click()
+ await delay(largeDelayMs * 2)
+ })
+ })
+
+ it('selects another provider', async () => {
+ const networkDropdown = await findElement(driver, By.css('.network-name'))
+ await networkDropdown.click()
+ await delay(regularDelayMs)
+
+ const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Main Ethereum Network')]`))
+ await customRpcButton.click()
+ await delay(largeDelayMs * 2)
+ })
+
+ it('finds 3 recent RPCs in history', async () => {
+ const networkDropdown = await findElement(driver, By.css('.network-name'))
+ await networkDropdown.click()
+ await delay(regularDelayMs)
+
+ // oldest selected RPC is not found
+ await assertElementNotPresent(webdriver, driver, By.xpath(`//span[contains(text(), '${customRpcUrls[0]}')]`))
+
+ // only recent 3 are found and in correct order (most recent at the top)
+ const customRpcs = await findElements(driver, By.xpath(`//span[contains(text(), 'https://mainnet.infura.io/')]`))
+
+ assert.equal(customRpcs.length, 3)
+
+ for (let i = 0; i < customRpcs.length; i++) {
+ const linkText = await customRpcs[i].getText()
+ const rpcUrl = customRpcUrls[customRpcUrls.length - i - 1]
+
+ assert.notEqual(linkText.indexOf(rpcUrl), -1)
+ }
+ })
+ })
+})
diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js
index 6de7574c4..bb9d0d10f 100644
--- a/test/integration/lib/add-token.js
+++ b/test/integration/lib/add-token.js
@@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) {
$('button.btn-primary.btn--large')[0].click()
// Verify added token image
- let heroBalance = await queryAsync($, '.hero-balance')
+ let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
@@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) {
// $('button.btn-primary--lg')[0].click()
// Verify added token image
- heroBalance = await queryAsync($, '.hero-balance')
+ heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(heroBalance.find('.identicon')[0], 'token added')
}
diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js
index dcc25c493..3539e97be 100644
--- a/test/integration/lib/confirm-sig-requests.js
+++ b/test/integration/lib/confirm-sig-requests.js
@@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
- const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
+ const pendingRequestItem = $.find('.transaction-list-item')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()
diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js
index d42b7495d..8d5acf5d0 100644
--- a/test/integration/lib/currency-localization.js
+++ b/test/integration/lib/currency-localization.js
@@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) {
await timeout(1000)
reactTriggerChange(selectState[0])
await timeout(1000)
- const txView = await queryAsync($, '.tx-view')
- const heroBalance = await findAsync($(txView), '.hero-balance')
- const fiatAmount = await findAsync($(heroBalance), '.fiat-amount')
- assert.equal(fiatAmount[0].textContent, '₱102,707.97')
+ const txView = await queryAsync($, '.transaction-view')
+ const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
+ const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
+ assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
}
diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js
index 406863ca6..7f3c114e4 100644
--- a/test/integration/lib/send-new-ui.js
+++ b/test/integration/lib/send-new-ui.js
@@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) {
selectState.val('send new ui')
reactTriggerChange(selectState[0])
- const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button')
+ const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button')
assert.ok(sendScreenButton[1], 'send screen button present')
sendScreenButton[1].click()
@@ -124,10 +124,10 @@ async function runSendFlowTest (assert, done) {
selectState.val('send edit')
reactTriggerChange(selectState[0])
- const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
+ const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first()
assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
- const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
+ const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
diff --git a/test/integration/lib/tx-list-items.js b/test/integration/lib/tx-list-items.js
index 9075efe03..7572d1629 100644
--- a/test/integration/lib/tx-list-items.js
+++ b/test/integration/lib/tx-list-items.js
@@ -29,26 +29,23 @@ async function runTxListItemsTest (assert, done) {
assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click()
- const txListItems = await queryAsync($, '.tx-list-item')
+ const txListItems = await queryAsync($, '.transaction-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
- const unapprovedTx = txListItems[0]
- assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
-
const retryTx = txListItems[1]
- const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span')
+ const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry')
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
const approvedTx = txListItems[2]
- const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
- assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
+ const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
+ assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
const unapprovedMsg = txListItems[3]
- const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
+ const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const failedTx = txListItems[4]
- const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
+ const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
const shapeShiftTx = txListItems[5]
@@ -56,10 +53,10 @@ async function runTxListItemsTest (assert, done) {
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
const confirmedTokenTx = txListItems[6]
- const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
- assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address')
+ const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
+ assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
const rejectedTx = txListItems[7]
- const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
+ const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
}
diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js
index a798d41e2..85c78fe1e 100644
--- a/test/unit/app/controllers/metamask-controller-test.js
+++ b/test/unit/app/controllers/metamask-controller-test.js
@@ -814,6 +814,77 @@ describe('MetaMaskController', function () {
})
})
+ describe('#_onKeyringControllerUpdate', function () {
+ it('should do nothing if there are no keyrings in state', async function () {
+ const addAddresses = sinon.fake()
+ const syncWithAddresses = sinon.fake()
+ sandbox.replace(metamaskController, 'preferencesController', {
+ addAddresses,
+ })
+ sandbox.replace(metamaskController, 'accountTracker', {
+ syncWithAddresses,
+ })
+
+ const oldState = metamaskController.getState()
+ await metamaskController._onKeyringControllerUpdate({keyrings: []})
+
+ assert.ok(addAddresses.notCalled)
+ assert.ok(syncWithAddresses.notCalled)
+ assert.deepEqual(metamaskController.getState(), oldState)
+ })
+
+ it('should update selected address if keyrings was locked', async function () {
+ const addAddresses = sinon.fake()
+ const getSelectedAddress = sinon.fake.returns('0x42')
+ const setSelectedAddress = sinon.fake()
+ const syncWithAddresses = sinon.fake()
+ sandbox.replace(metamaskController, 'preferencesController', {
+ addAddresses,
+ getSelectedAddress,
+ setSelectedAddress,
+ })
+ sandbox.replace(metamaskController, 'accountTracker', {
+ syncWithAddresses,
+ })
+
+ const oldState = metamaskController.getState()
+ await metamaskController._onKeyringControllerUpdate({
+ isUnlocked: false,
+ keyrings: [{
+ accounts: ['0x1', '0x2'],
+ }],
+ })
+
+ assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
+ assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
+ assert.deepEqual(setSelectedAddress.args, [['0x1']])
+ assert.deepEqual(metamaskController.getState(), oldState)
+ })
+
+ it('should NOT update selected address if already unlocked', async function () {
+ const addAddresses = sinon.fake()
+ const syncWithAddresses = sinon.fake()
+ sandbox.replace(metamaskController, 'preferencesController', {
+ addAddresses,
+ })
+ sandbox.replace(metamaskController, 'accountTracker', {
+ syncWithAddresses,
+ })
+
+ const oldState = metamaskController.getState()
+ await metamaskController._onKeyringControllerUpdate({
+ isUnlocked: true,
+ keyrings: [{
+ accounts: ['0x1', '0x2'],
+ }],
+ })
+
+ assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
+ assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
+ assert.deepEqual(metamaskController.getState(), oldState)
+ })
+ })
+
})
function deferredPromise () {
diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js
index 9b2c846bd..2c261be90 100644
--- a/test/unit/app/controllers/preferences-controller-test.js
+++ b/test/unit/app/controllers/preferences-controller-test.js
@@ -1,6 +1,7 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
+const sinon = require('sinon')
describe('preferences controller', function () {
let preferencesController
@@ -339,5 +340,114 @@ describe('preferences controller', function () {
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
})
+
+ describe('on watchAsset', function () {
+ var stubNext, stubEnd, stubHandleWatchAssetERC20, asy, req, res
+ const sandbox = sinon.createSandbox()
+
+ beforeEach(() => {
+ req = {params: {}}
+ res = {}
+ asy = {next: () => {}, end: () => {}}
+ stubNext = sandbox.stub(asy, 'next')
+ stubEnd = sandbox.stub(asy, 'end').returns(0)
+ stubHandleWatchAssetERC20 = sandbox.stub(preferencesController, '_handleWatchAssetERC20')
+ })
+ after(() => {
+ sandbox.restore()
+ })
+
+ it('shouldn not do anything if method not corresponds', async function () {
+ const asy = {next: () => {}, end: () => {}}
+ var stubNext = sandbox.stub(asy, 'next')
+ var stubEnd = sandbox.stub(asy, 'end').returns(0)
+ req.method = 'metamask'
+ await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
+ sandbox.assert.notCalled(stubEnd)
+ sandbox.assert.called(stubNext)
+ })
+ it('should do something if method is supported', async function () {
+ const asy = {next: () => {}, end: () => {}}
+ var stubNext = sandbox.stub(asy, 'next')
+ var stubEnd = sandbox.stub(asy, 'end').returns(0)
+ req.method = 'metamask_watchAsset'
+ req.params.type = 'someasset'
+ await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
+ sandbox.assert.called(stubEnd)
+ sandbox.assert.notCalled(stubNext)
+ })
+ it('should through error if method is supported but asset type is not', async function () {
+ req.method = 'metamask_watchAsset'
+ req.params.type = 'someasset'
+ await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
+ sandbox.assert.called(stubEnd)
+ sandbox.assert.notCalled(stubHandleWatchAssetERC20)
+ sandbox.assert.notCalled(stubNext)
+ assert.deepEqual(res, {})
+ })
+ it('should trigger handle add asset if type supported', async function () {
+ const asy = {next: () => {}, end: () => {}}
+ req.method = 'metamask_watchAsset'
+ req.params.type = 'ERC20'
+ await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
+ sandbox.assert.called(stubHandleWatchAssetERC20)
+ })
+ })
+
+ describe('on watchAsset of type ERC20', function () {
+ var req
+
+ const sandbox = sinon.createSandbox()
+ beforeEach(() => {
+ req = {params: {type: 'ERC20'}}
+ })
+ after(() => {
+ sandbox.restore()
+ })
+
+ it('should add suggested token', async function () {
+ const address = '0xabcdef1234567'
+ const symbol = 'ABBR'
+ const decimals = 5
+ const image = 'someimage'
+ req.params.options = { address, symbol, decimals, image }
+
+ sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
+ preferencesController.showWatchAssetUi = async () => {}
+
+ await preferencesController._handleWatchAssetERC20(req.params.options)
+ const suggested = preferencesController.getSuggestedTokens()
+ assert.equal(Object.keys(suggested).length, 1, `one token added ${Object.keys(suggested)}`)
+
+ assert.equal(suggested[address].address, address, 'set address correctly')
+ assert.equal(suggested[address].symbol, symbol, 'set symbol correctly')
+ assert.equal(suggested[address].decimals, decimals, 'set decimals correctly')
+ assert.equal(suggested[address].image, image, 'set image correctly')
+ })
+
+ it('should add token correctly if user confirms', async function () {
+ const address = '0xabcdef1234567'
+ const symbol = 'ABBR'
+ const decimals = 5
+ const image = 'someimage'
+ req.params.options = { address, symbol, decimals, image }
+
+ sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
+ preferencesController.showWatchAssetUi = async () => {
+ await preferencesController.addToken(address, symbol, decimals, image)
+ }
+
+ await preferencesController._handleWatchAssetERC20(req.params.options)
+ const tokens = preferencesController.getTokens()
+ assert.equal(tokens.length, 1, `one token added`)
+ const added = tokens[0]
+ assert.equal(added.address, address, 'set address correctly')
+ assert.equal(added.symbol, symbol, 'set symbol correctly')
+ assert.equal(added.decimals, decimals, 'set decimals correctly')
+
+ const assetImages = preferencesController.getAssetImages()
+ assert.ok(assetImages[address], `set image correctly`)
+ })
+ })
})
diff --git a/ui/app/account-and-transaction-details.js b/ui/app/account-and-transaction-details.js
deleted file mode 100644
index 03101d37a..000000000
--- a/ui/app/account-and-transaction-details.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-// Main Views
-const TxView = require('./components/tx-view')
-const WalletView = require('./components/wallet-view')
-
-module.exports = AccountAndTransactionDetails
-
-inherits(AccountAndTransactionDetails, Component)
-function AccountAndTransactionDetails () {
- Component.call(this)
-}
-
-AccountAndTransactionDetails.prototype.render = function () {
- return h('div.account-and-transaction-details', [
- // wallet
- h(WalletView, {
- style: {
- },
- responsiveDisplayClassname: '.lap-visible',
- }, [
- ]),
-
- // transaction
- h(TxView, {
- style: {
- },
- }, [
- ]),
- ])
-}
-
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 6bcc64e17..6d5b1ef3f 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -227,11 +227,14 @@ var actions = {
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
showConfigPage,
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
+ SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE',
showAddTokenPage,
+ showAddSuggestedTokenPage,
addToken,
addTokens,
removeToken,
updateTokens,
+ removeSuggestedTokens,
UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget,
setProviderType: setProviderType,
@@ -1147,6 +1150,10 @@ function updateAndApproveTx (txData) {
return txData
})
+ .catch((err) => {
+ dispatch(actions.hideLoadingIndication())
+ return Promise.reject(err)
+ })
}
}
@@ -1589,11 +1596,18 @@ function showAddTokenPage (transitionForward = true) {
}
}
-function addToken (address, symbol, decimals) {
+function showAddSuggestedTokenPage (transitionForward = true) {
+ return {
+ type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE,
+ value: transitionForward,
+ }
+}
+
+function addToken (address, symbol, decimals, image) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.addToken(address, symbol, decimals, (err, tokens) => {
+ background.addToken(address, symbol, decimals, image, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
@@ -1643,6 +1657,27 @@ function addTokens (tokens) {
}
}
+function removeSuggestedTokens () {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.removeSuggestedTokens((err, suggestedTokens) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ }
+ dispatch(actions.clearPendingTokens())
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
+ return global.platform.closeCurrentWindow()
+ }
+ resolve(suggestedTokens)
+ })
+ })
+ .then(() => updateMetamaskStateFromBackground())
+ .then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens})))
+ }
+}
+
function updateTokens (newTokens) {
return {
type: actions.UPDATE_TOKENS,
@@ -1650,6 +1685,12 @@ function updateTokens (newTokens) {
}
}
+function clearPendingTokens () {
+ return {
+ type: actions.CLEAR_PENDING_TOKENS,
+ }
+}
+
function goBackToInitView () {
return {
type: actions.BACK_TO_INIT_MENU,
@@ -1812,9 +1853,13 @@ function hideModal (payload) {
}
}
-function showSidebar () {
+function showSidebar ({ transitionName, type }) {
return {
type: actions.SIDEBAR_OPEN,
+ value: {
+ transitionName,
+ type,
+ },
}
}
@@ -2310,9 +2355,3 @@ function setPendingTokens (pendingTokens) {
payload: tokens,
}
}
-
-function clearPendingTokens () {
- return {
- type: actions.CLEAR_PENDING_TOKENS,
- }
-}
diff --git a/ui/app/app.js b/ui/app/app.js
index 4fcf092ca..aa051280b 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -15,10 +15,10 @@ const SendTransactionScreen = require('./components/send/send.container')
const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu
-const WalletView = require('./components/wallet-view')
+const Sidebar = require('./components/sidebars').default
// other views
-const Home = require('./components/pages/home')
+import Home from './components/pages/home'
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
@@ -26,11 +26,11 @@ const RestoreVaultPage = require('./components/pages/keychains/restore-vault').d
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
+const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading-screen')
-const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu')
@@ -51,6 +51,7 @@ const {
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@@ -85,6 +86,7 @@ class App extends Component {
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
+ h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
@@ -102,6 +104,7 @@ class App extends Component {
frequentRpcList,
currentView,
setMouseUserState,
+ sidebar,
} = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
@@ -134,7 +137,12 @@ class App extends Component {
h(AppHeader),
// sidebar
- this.renderSidebar(),
+ h(Sidebar, {
+ sidebarOpen: sidebar.isOpen,
+ hideSidebar: this.props.hideSidebar,
+ transitionName: sidebar.transitionName,
+ type: sidebar.type,
+ }),
// network dropdown
h(NetworkDropdown, {
@@ -154,51 +162,6 @@ class App extends Component {
)
}
- renderSidebar () {
- return h('div', [
- h('style', `
- .sidebar-enter {
- transition: transform 300ms ease-in-out;
- transform: translateX(-100%);
- }
- .sidebar-enter.sidebar-enter-active {
- transition: transform 300ms ease-in-out;
- transform: translateX(0%);
- }
- .sidebar-leave {
- transition: transform 200ms ease-out;
- transform: translateX(0%);
- }
- .sidebar-leave.sidebar-leave-active {
- transition: transform 200ms ease-out;
- transform: translateX(-100%);
- }
- `),
-
- h(ReactCSSTransitionGroup, {
- transitionName: 'sidebar',
- transitionEnterTimeout: 300,
- transitionLeaveTimeout: 200,
- }, [
- // A second instance of Walletview is used for non-mobile viewports
- this.props.sidebarOpen ? h(WalletView, {
- responsiveDisplayClassname: '.sidebar',
- style: {},
- }) : undefined,
-
- ]),
-
- // overlay
- // TODO: add onClick for overlay to close sidebar
- this.props.sidebarOpen ? h('div.sidebar-overlay', {
- style: {},
- onClick: () => {
- this.props.hideSidebar()
- },
- }, []) : undefined,
- ])
- }
-
toggleMetamaskActive () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
@@ -267,7 +230,7 @@ App.propTypes = {
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
- sidebarOpen: PropTypes.bool,
+ sidebar: PropTypes.object,
alertOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
@@ -303,7 +266,7 @@ function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
- sidebarOpen,
+ sidebar,
alertOpen,
alertMessage,
isLoading,
@@ -330,7 +293,7 @@ function mapStateToProps (state) {
return {
// state from plugin
networkDropdownOpen,
- sidebarOpen,
+ sidebar,
alertOpen,
alertMessage,
isLoading,
diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js
index e31552f2d..d63d78c9f 100644
--- a/ui/app/components/balance-component.js
+++ b/ui/app/components/balance-component.js
@@ -4,8 +4,8 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
-const currencyFormatter = require('currency-formatter')
-const currencies = require('currency-formatter/currencies')
+import CurrencyDisplay from './currency-display'
+const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { formatBalance, generateBalanceObject } = require('../util')
@@ -20,8 +20,9 @@ function mapStateToProps (state) {
return {
account,
network,
- conversionRate: state.metamask.conversionRate,
- currentCurrency: state.metamask.currentCurrency,
+ conversionRate: conversionRateSelector(state),
+ currentCurrency: getCurrentCurrency(state),
+ assetImages: getAssetImages(state),
}
}
@@ -32,7 +33,9 @@ function BalanceComponent () {
BalanceComponent.prototype.render = function () {
const props = this.props
- const { token, network } = props
+ const { token, network, assetImages } = props
+ const address = token && token.address
+ const image = assetImages && address ? assetImages[token.address] : undefined
return h('div.balance-container', {}, [
@@ -43,8 +46,9 @@ BalanceComponent.prototype.render = function () {
// }),
h(Identicon, {
diameter: 50,
- address: token && token.address,
+ address,
network,
+ image,
}),
token ? this.renderTokenBalance() : this.renderBalance(),
@@ -80,38 +84,12 @@ BalanceComponent.prototype.renderBalance = function () {
style: {},
}, this.getTokenBalance(formattedBalance, shorten)),
- showFiat ? this.renderFiatValue(formattedBalance) : null,
+ showFiat && h(CurrencyDisplay, {
+ value: balanceValue,
+ }),
])
}
-BalanceComponent.prototype.renderFiatValue = function (formattedBalance) {
-
- const { conversionRate, currentCurrency } = this.props
-
- const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate)
-
- const fiatPrefix = currentCurrency === 'USD' ? '$' : ''
-
- return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix)
-}
-
-BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) {
- const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0
- if (shouldNotRenderFiat) return null
-
- const upperCaseFiatSuffix = fiatSuffix.toUpperCase()
-
- const display = currencies.find(currency => currency.code === upperCaseFiatSuffix)
- ? currencyFormatter.format(Number(fiatDisplayNumber), {
- code: upperCaseFiatSuffix,
- })
- : `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}`
-
- return h('div.fiat-amount', {
- style: {},
- }, display)
-}
-
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)
diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js
deleted file mode 100644
index c6957d2aa..000000000
--- a/ui/app/components/buy-button-subview.js
+++ /dev/null
@@ -1,267 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-const actions = require('../actions')
-const CoinbaseForm = require('./coinbase-form')
-const ShapeshiftForm = require('./shapeshift-form')
-const Loading = require('./loading-screen')
-const AccountPanel = require('./account-panel')
-const RadioList = require('./custom-radio-list')
-const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
-
-BuyButtonSubview.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(BuyButtonSubview)
-
-
-function mapStateToProps (state) {
- return {
- identity: state.appState.identity,
- account: state.metamask.accounts[state.appState.buyView.buyAddress],
- warning: state.appState.warning,
- buyView: state.appState.buyView,
- network: state.metamask.network,
- provider: state.metamask.provider,
- context: state.appState.currentView.context,
- isSubLoading: state.appState.isSubLoading,
- }
-}
-
-inherits(BuyButtonSubview, Component)
-function BuyButtonSubview () {
- Component.call(this)
-}
-
-BuyButtonSubview.prototype.render = function () {
- return (
- h('div', {
- style: {
- width: '100%',
- },
- }, [
- this.headerSubview(),
- this.primarySubview(),
- ])
- )
-}
-
-BuyButtonSubview.prototype.headerSubview = function () {
- const props = this.props
- const isLoading = props.isSubLoading
- return (
-
- h('.flex-column', {
- style: {
- alignItems: 'center',
- },
- }, [
-
- // header bar (back button, label)
- h('.flex-row', {
- style: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- }, [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: this.backButtonContext.bind(this),
- style: {
- position: 'absolute',
- left: '10px',
- },
- }),
- h('h2.text-transform-uppercase.flex-center', {
- style: {
- width: '100vw',
- background: 'rgb(235, 235, 235)',
- color: 'rgb(174, 174, 174)',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, this.context.t('depositEth')),
- ]),
-
- // loading indication
- h('div', {
- style: {
- position: 'absolute',
- top: '57vh',
- left: '49vw',
- },
- }, [
- isLoading && h(Loading),
- ]),
-
- // account panel
- h('div', {
- style: {
- width: '80%',
- },
- }, [
- h(AccountPanel, {
- showFullAddress: true,
- identity: props.identity,
- account: props.account,
- }),
- ]),
-
- h('.flex-row', {
- style: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- }, [
- h('h3.text-transform-uppercase.flex-center', {
- style: {
- paddingLeft: '15px',
- width: '100vw',
- background: 'rgb(235, 235, 235)',
- color: 'rgb(174, 174, 174)',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, this.context.t('selectService')),
- ]),
-
- ])
-
- )
-}
-
-
-BuyButtonSubview.prototype.primarySubview = function () {
- const props = this.props
- const network = props.network
-
- switch (network) {
- case 'loading':
- return
-
- case '1':
- return this.mainnetSubview()
-
- // Ropsten, Rinkeby, Kovan
- case '3':
- case '4':
- case '42':
- const networkName = getNetworkDisplayName(network)
- const label = `${networkName} ${this.context.t('testFaucet')}`
- return (
- h('div.flex-column', {
- style: {
- alignItems: 'center',
- margin: '20px 50px',
- },
- }, [
- h('button.text-transform-uppercase', {
- onClick: () => this.props.dispatch(actions.buyEth({ network })),
- style: {
- marginTop: '15px',
- },
- }, label),
- // Kovan only: Dharma loans beta
- network === '42' ? (
- h('button.text-transform-uppercase', {
- onClick: () => this.navigateTo('https://borrow.dharma.io/'),
- style: {
- marginTop: '15px',
- },
- }, this.context.t('borrowDharma'))
- ) : null,
- ])
- )
-
- default:
- return (
- h('h2.error', this.context.t('unknownNetworkId'))
- )
-
- }
-}
-
-BuyButtonSubview.prototype.mainnetSubview = function () {
- const props = this.props
-
- return (
-
- h('.flex-column', {
- style: {
- alignItems: 'center',
- },
- }, [
-
- h('.flex-row.selected-exchange', {
- style: {
- position: 'relative',
- right: '35px',
- marginTop: '20px',
- marginBottom: '20px',
- },
- }, [
- h(RadioList, {
- defaultFocus: props.buyView.subview,
- labels: [
- 'Coinbase',
- 'ShapeShift',
- ],
- subtext: {
- 'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`,
- 'ShapeShift': this.context.t('crypto'),
- },
- onClick: this.radioHandler.bind(this),
- }),
- ]),
-
- h('h3.text-transform-uppercase', {
- style: {
- paddingLeft: '15px',
- fontFamily: 'Montserrat Light',
- width: '100vw',
- background: 'rgb(235, 235, 235)',
- color: 'rgb(174, 174, 174)',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, props.buyView.subview),
-
- this.formVersionSubview(),
- ])
-
- )
-}
-
-BuyButtonSubview.prototype.formVersionSubview = function () {
- const network = this.props.network
- if (network === '1') {
- if (this.props.buyView.formView.coinbase) {
- return h(CoinbaseForm, this.props)
- } else if (this.props.buyView.formView.shapeshift) {
- return h(ShapeshiftForm, this.props)
- }
- }
-}
-
-BuyButtonSubview.prototype.navigateTo = function (url) {
- global.platform.openWindow({ url })
-}
-
-BuyButtonSubview.prototype.backButtonContext = function () {
- if (this.props.context === 'confTx') {
- this.props.dispatch(actions.showConfTxPage({transForward: false}))
- } else {
- this.props.dispatch(actions.goHome())
- }
-}
-
-BuyButtonSubview.prototype.radioHandler = function (event) {
- switch (event.target.title) {
- case 'Coinbase':
- return this.props.dispatch(actions.coinBaseSubview())
- case 'ShapeShift':
- return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
- }
-}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
index 08923af88..de9aa6eb7 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
@@ -18,6 +18,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
summaryComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -60,6 +61,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle,
identiconAddress,
nonce,
+ assetImage,
summaryComponent,
detailsComponent,
dataComponent,
@@ -85,6 +87,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle={hideSubtitle}
identiconAddress={identiconAddress}
nonce={nonce}
+ assetImage={assetImage}
/>
)
}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
index 70ebdeb20..4965d7b4e 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
@@ -11,7 +11,9 @@ const ConfirmPageContainerError = (props, context) => {
src="/images/alert-red.svg"
className="confirm-page-container-error__icon"
/>
- { `ALERT: ${error}` }
+ <div className="confirm-page-container-error__text">
+ { `ALERT: ${error}` }
+ </div>
</div>
)
}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
index e99b0f631..89ff25578 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
@@ -1,5 +1,5 @@
.confirm-page-container-error {
- height: 32px;
+ min-height: 32px;
border: 1px solid $monzo;
color: $monzo;
background: lighten($monzo, 56%);
@@ -8,10 +8,14 @@
display: flex;
justify-content: flex-start;
align-items: center;
- padding-left: 16px;
+ padding: 8px 16px;
&__icon {
margin-right: 8px;
flex: 0 0 auto;
}
+
+ &__text {
+ overflow: auto;
+ }
}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
index 3b1ee62c5..38b158fd3 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
@@ -4,7 +4,7 @@ import classnames from 'classnames'
import Identicon from '../../../identicon'
const ConfirmPageContainerSummary = props => {
- const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props
+ const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props
return (
<div className={classnames('confirm-page-container-summary', className)}>
@@ -27,6 +27,7 @@ const ConfirmPageContainerSummary = props => {
className="confirm-page-container-summary__identicon"
diameter={36}
address={identiconAddress}
+ image={assetImage}
/>
)
}
@@ -51,6 +52,7 @@ ConfirmPageContainerSummary.propTypes = {
className: PropTypes.string,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
}
export default ConfirmPageContainerSummary
diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js
index 24ff05353..b1582051e 100644
--- a/ui/app/components/confirm-page-container/confirm-page-container.component.js
+++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js
@@ -38,6 +38,7 @@ export default class ConfirmPageContainer extends Component {
detailsComponent: PropTypes.node,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
summaryComponent: PropTypes.node,
warning: PropTypes.string,
// Footer
@@ -70,8 +71,10 @@ export default class ConfirmPageContainer extends Component {
onSubmit,
identiconAddress,
nonce,
+ assetImage,
warning,
} = this.props
+ const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
return (
<div className="page-container">
@@ -84,6 +87,7 @@ export default class ConfirmPageContainer extends Component {
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
+ assetImage={renderAssetImage ? assetImage : undefined}
/>
</ConfirmPageContainerHeader>
{
@@ -101,6 +105,7 @@ export default class ConfirmPageContainer extends Component {
errorKey={errorKey}
identiconAddress={identiconAddress}
nonce={nonce}
+ assetImage={assetImage}
warning={warning}
/>
)
diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js
new file mode 100644
index 000000000..389791b42
--- /dev/null
+++ b/ui/app/components/currency-display/currency-display.component.js
@@ -0,0 +1,26 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { ETH } from '../../constants/common'
+
+export default class CurrencyDisplay extends PureComponent {
+ static propTypes = {
+ className: PropTypes.string,
+ displayValue: PropTypes.string,
+ prefix: PropTypes.string,
+ currency: PropTypes.oneOf([ETH]),
+ }
+
+ render () {
+ const { className, displayValue, prefix } = this.props
+ const text = `${prefix || ''}${displayValue}`
+
+ return (
+ <div
+ className={className}
+ title={text}
+ >
+ { text }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js
new file mode 100644
index 000000000..b8a738c65
--- /dev/null
+++ b/ui/app/components/currency-display/currency-display.container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import CurrencyDisplay from './currency-display.component'
+import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
+
+const mapStateToProps = (state, ownProps) => {
+ const { value, numberOfDecimals = 2, currency } = ownProps
+ const { metamask: { currentCurrency, conversionRate } } = state
+
+ const toCurrency = currency || currentCurrency
+ const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
+ const formattedValue = formatCurrency(convertedValue, toCurrency)
+ const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
+
+ return {
+ displayValue,
+ }
+}
+
+export default connect(mapStateToProps)(CurrencyDisplay)
diff --git a/ui/app/components/currency-display/index.js b/ui/app/components/currency-display/index.js
new file mode 100644
index 000000000..38f08765f
--- /dev/null
+++ b/ui/app/components/currency-display/index.js
@@ -0,0 +1 @@
+export { default } from './currency-display.container'
diff --git a/ui/app/components/currency-display/tests/currency-display.component.test.js b/ui/app/components/currency-display/tests/currency-display.component.test.js
new file mode 100644
index 000000000..d9ef052f1
--- /dev/null
+++ b/ui/app/components/currency-display/tests/currency-display.component.test.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import CurrencyDisplay from '../currency-display.component'
+
+describe('CurrencyDisplay Component', () => {
+ it('should render text with a className', () => {
+ const wrapper = shallow(<CurrencyDisplay
+ displayValue="$123.45"
+ className="currency-display"
+ />)
+
+ assert.ok(wrapper.hasClass('currency-display'))
+ assert.equal(wrapper.text(), '$123.45')
+ })
+
+ it('should render text with a prefix', () => {
+ const wrapper = shallow(<CurrencyDisplay
+ displayValue="$123.45"
+ className="currency-display"
+ prefix="-"
+ />)
+
+ assert.ok(wrapper.hasClass('currency-display'))
+ assert.equal(wrapper.text(), '-$123.45')
+ })
+})
diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js
new file mode 100644
index 000000000..474ce5378
--- /dev/null
+++ b/ui/app/components/currency-display/tests/currency-display.container.test.js
@@ -0,0 +1,61 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../currency-display.container.js', {
+ 'react-redux': {
+ connect: ms => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+})
+
+describe('CurrencyDisplay container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props', () => {
+ const mockState = {
+ metamask: {
+ conversionRate: 280.45,
+ currentCurrency: 'usd',
+ },
+ }
+
+ const tests = [
+ {
+ props: {
+ value: '0x2386f26fc10000',
+ numberOfDecimals: 2,
+ currency: 'usd',
+ },
+ result: {
+ displayValue: '$2.80 USD',
+ },
+ },
+ {
+ props: {
+ value: '0x2386f26fc10000',
+ },
+ result: {
+ displayValue: '$2.80 USD',
+ },
+ },
+ {
+ props: {
+ value: '0x1193461d01595930',
+ currency: 'ETH',
+ numberOfDecimals: 3,
+ },
+ result: {
+ displayValue: '1.266 ETH',
+ },
+ },
+ ]
+
+ tests.forEach(({ props, result }) => {
+ assert.deepEqual(mapStateToProps(mockState, props), result)
+ })
+ })
+ })
+})
diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js
deleted file mode 100644
index a4c525396..000000000
--- a/ui/app/components/custom-radio-list.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-
-module.exports = RadioList
-
-inherits(RadioList, Component)
-function RadioList () {
- Component.call(this)
-}
-
-RadioList.prototype.render = function () {
- const props = this.props
- const activeClass = '.custom-radio-selected'
- const inactiveClass = '.custom-radio-inactive'
- const {
- labels,
- defaultFocus,
- } = props
-
-
- return (
- h('.flex-row', {
- style: {
- fontSize: '12px',
- },
- }, [
- h('.flex-column.custom-radios', {
- style: {
- marginRight: '5px',
- },
- },
- labels.map((lable, i) => {
- let isSelcted = (this.state !== null)
- isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable)
- return h(isSelcted ? activeClass : inactiveClass, {
- title: lable,
- onClick: (event) => {
- this.setState({selected: event.target.title})
- props.onClick(event)
- },
- })
- })
- ),
- h('.text', {},
- labels.map((lable) => {
- if (props.subtext) {
- return h('.flex-row', {}, [
- h('.radio-titles', lable),
- h('.radio-titles-subtext', `- ${props.subtext[lable]}`),
- ])
- } else {
- return h('.radio-titles', lable)
- }
- })
- ),
- ])
- )
-}
-
diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js
index 179b6617f..b497f5c09 100644
--- a/ui/app/components/dropdowns/components/account-dropdowns.js
+++ b/ui/app/components/dropdowns/components/account-dropdowns.js
@@ -459,7 +459,7 @@ const mapDispatchToProps = (dispatch) => {
function mapStateToProps (state) {
return {
keyrings: state.metamask.keyrings,
- sidebarOpen: state.appState.sidebarOpen,
+ sidebarOpen: state.appState.sidebar.isOpen,
}
}
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
index e5363ff56..63a30dd82 100644
--- a/ui/app/components/dropdowns/network-dropdown.js
+++ b/ui/app/components/dropdowns/network-dropdown.js
@@ -272,10 +272,12 @@ NetworkDropdown.prototype.getNetworkName = function () {
NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props
- const rpcTarget = provider.rpcTarget
+ const reversedRpcList = rpcList.slice().reverse()
- return rpcList.map((rpc) => {
- if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
+ return reversedRpcList.map((rpc) => {
+ const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
+
+ if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
return null
} else {
return h(
@@ -291,11 +293,11 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
},
},
[
- rpcTarget === rpc ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
+ currentRpcTarget ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h('i.fa.fa-question-circle.fa-med.menu-icon-circle'),
h('span.network-name-item', {
style: {
- color: rpcTarget === rpc ? '#ffffff' : '#9b9b9b',
+ color: currentRpcTarget ? '#ffffff' : '#9b9b9b',
},
}, rpc),
]
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
index 424048745..076e65b81 100644
--- a/ui/app/components/identicon.js
+++ b/ui/app/components/identicon.js
@@ -26,36 +26,42 @@ function mapStateToProps (state) {
IdenticonComponent.prototype.render = function () {
var props = this.props
- const { className = '', address } = props
+ const { className = '', address, image } = props
var diameter = props.diameter || this.defaultDiameter
-
- return address
- ? (
- h('div', {
- className: `${className} identicon`,
- key: 'identicon-' + address,
- style: {
- display: 'flex',
- flexShrink: 0,
- alignItems: 'center',
- justifyContent: 'center',
- height: diameter,
- width: diameter,
- borderRadius: diameter / 2,
- overflow: 'hidden',
- },
- })
- )
- : (
- h('img.balance-icon', {
- src: './images/eth_logo.svg',
- style: {
- height: diameter,
- width: diameter,
- borderRadius: diameter / 2,
- },
- })
- )
+ const style = {
+ height: diameter,
+ width: diameter,
+ borderRadius: diameter / 2,
+ }
+ if (image) {
+ return h('img', {
+ className: `${className} identicon`,
+ src: image,
+ style: {
+ ...style,
+ },
+ })
+ } else if (address) {
+ return h('div', {
+ className: `${className} identicon`,
+ key: 'identicon-' + address,
+ style: {
+ display: 'flex',
+ flexShrink: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ ...style,
+ overflow: 'hidden',
+ },
+ })
+ } else {
+ return h('img.balance-icon', {
+ src: './images/eth_logo.svg',
+ style: {
+ ...style,
+ },
+ })
+ }
}
IdenticonComponent.prototype.componentDidMount = function () {
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
index 35d38e2a3..cb4065fd9 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -1,23 +1,39 @@
+@import './app-header/index';
+
@import './button-group/index';
-@import './export-text-container/index';
+@import './confirm-page-container/index';
-@import './selected-account/index';
+@import './export-text-container/index';
@import './info-box/index';
-@import './network-display/index';
+@import './menu-bar/index';
-@import './confirm-page-container/index';
+@import './modals/index';
+
+@import './network-display/index';
@import './page-container/index';
@import './pages/index';
-@import './modals/index';
+@import './selected-account/index';
@import './sender-to-recipient/index';
@import './tabs/index';
+@import './transaction-view/index';
+
+@import './transaction-view-balance/index';
+
+@import './transaction-list/index';
+
+@import './transaction-list-item/index';
+
+@import './transaction-status/index';
+
@import './app-header/index';
+
+@import './sidebars/index';
diff --git a/ui/app/components/menu-bar/index.js b/ui/app/components/menu-bar/index.js
new file mode 100644
index 000000000..c5760847f
--- /dev/null
+++ b/ui/app/components/menu-bar/index.js
@@ -0,0 +1 @@
+export { default } from './menu-bar.container'
diff --git a/ui/app/components/menu-bar/index.scss b/ui/app/components/menu-bar/index.scss
new file mode 100644
index 000000000..f699f4090
--- /dev/null
+++ b/ui/app/components/menu-bar/index.scss
@@ -0,0 +1,23 @@
+.menu-bar {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ flex: 0 0 auto;
+ margin-bottom: 16px;
+ padding: 5px;
+ border-bottom: 1px solid #e5e5e5;
+
+ &__sidebar-button {
+ font-size: 1.25rem;
+ cursor: pointer;
+ padding: 10px;
+ }
+
+ &__open-in-browser {
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ padding: 10px;
+ }
+}
diff --git a/ui/app/components/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js
new file mode 100644
index 000000000..eee9feebb
--- /dev/null
+++ b/ui/app/components/menu-bar/menu-bar.component.js
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Tooltip from '../tooltip'
+import SelectedAccount from '../selected-account'
+
+export default class MenuBar extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ hideSidebar: PropTypes.func,
+ isMascara: PropTypes.bool,
+ sidebarOpen: PropTypes.bool,
+ showSidebar: PropTypes.func,
+ }
+
+ render () {
+ const { t } = this.context
+ const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props
+
+ return (
+ <div className="menu-bar">
+ <Tooltip
+ title={t('menu')}
+ position="bottom"
+ >
+ <div
+ className="fa fa-bars menu-bar__sidebar-button"
+ onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
+ />
+ </Tooltip>
+ <SelectedAccount />
+ {
+ !isMascara && (
+ <Tooltip
+ title={t('openInTab')}
+ position="bottom"
+ >
+ <div
+ className="menu-bar__open-in-browser"
+ onClick={() => global.platform.openExtensionInBrowser()}
+ >
+ <img src="images/popout.svg" />
+ </div>
+ </Tooltip>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/menu-bar/menu-bar.container.js b/ui/app/components/menu-bar/menu-bar.container.js
new file mode 100644
index 000000000..ae32882ae
--- /dev/null
+++ b/ui/app/components/menu-bar/menu-bar.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import MenuBar from './menu-bar.component'
+import { showSidebar, hideSidebar } from '../../actions'
+
+const mapStateToProps = state => {
+ const { appState: { sidebar: { isOpen }, isMascara } } = state
+
+ return {
+ sidebarOpen: isOpen,
+ isMascara,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showSidebar: () => {
+ dispatch(showSidebar({
+ transitionName: 'sidebar-right',
+ type: 'wallet-view',
+ }))
+ },
+ hideSidebar: () => dispatch(hideSidebar()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
index 8e252787a..0bbabf38d 100644
--- a/ui/app/components/modals/account-details-modal.js
+++ b/ui/app/components/modals/account-details-modal.js
@@ -61,7 +61,7 @@ AccountDetailsModal.prototype.render = function () {
let exportPrivateKeyFeatureEnabled = true
// This feature is disabled for hardware wallets
- if (keyring.type.search('Hardware') !== -1) {
+ if (keyring && keyring.type.search('Hardware') !== -1) {
exportPrivateKeyFeatureEnabled = false
}
diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js
index a9856b20f..aa0593df8 100644
--- a/ui/app/components/modals/account-modal-container.js
+++ b/ui/app/components/modals/account-modal-container.js
@@ -7,9 +7,9 @@ const actions = require('../../actions')
const { getSelectedIdentity } = require('../../selectors')
const Identicon = require('../identicon')
-function mapStateToProps (state) {
+function mapStateToProps (state, ownProps) {
return {
- selectedIdentity: getSelectedIdentity(state),
+ selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state),
}
}
diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js
index 80ece425f..60a416304 100644
--- a/ui/app/components/modals/export-private-key-modal.js
+++ b/ui/app/components/modals/export-private-key-modal.js
@@ -1,3 +1,4 @@
+const log = require('loglevel')
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
@@ -11,19 +12,33 @@ const ReadOnlyInput = require('../readonly-input')
const copyToClipboard = require('copy-to-clipboard')
const { checksumAddress } = require('../../util')
-function mapStateToProps (state) {
- return {
- warning: state.appState.warning,
- privateKey: state.appState.accountDetail.privateKey,
- network: state.metamask.network,
- selectedIdentity: getSelectedIdentity(state),
- previousModalState: state.appState.modal.previousModalState.name,
+function mapStateToPropsFactory () {
+ let selectedIdentity = null
+ return function mapStateToProps (state) {
+ // We should **not** change the identity displayed here even if it changes from underneath us.
+ // If we do, we will be showing the user one private key and a **different** address and name.
+ // Note that the selected identity **will** change from underneath us when we unlock the keyring
+ // which is the expected behavior that we are side-stepping.
+ selectedIdentity = selectedIdentity || getSelectedIdentity(state)
+ return {
+ warning: state.appState.warning,
+ privateKey: state.appState.accountDetail.privateKey,
+ network: state.metamask.network,
+ selectedIdentity,
+ previousModalState: state.appState.modal.previousModalState.name,
+ }
}
}
function mapDispatchToProps (dispatch) {
return {
- exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)),
+ exportAccount: (password, address) => {
+ return dispatch(actions.exportAccount(password, address))
+ .then((res) => {
+ dispatch(actions.hideWarning())
+ return res
+ })
+ },
showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
hideModal: () => dispatch(actions.hideModal()),
}
@@ -36,6 +51,7 @@ function ExportPrivateKeyModal () {
this.state = {
password: '',
privateKey: null,
+ showWarning: true,
}
}
@@ -43,14 +59,18 @@ ExportPrivateKeyModal.contextTypes = {
t: PropTypes.func,
}
-module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal)
+module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal)
ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
const { exportAccount } = this.props
exportAccount(password, address)
- .then(privateKey => this.setState({ privateKey }))
+ .then(privateKey => this.setState({
+ privateKey,
+ showWarning: false,
+ }))
+ .catch((e) => log.error(e))
}
ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
@@ -110,9 +130,13 @@ ExportPrivateKeyModal.prototype.render = function () {
} = this.props
const { name, address } = selectedIdentity
- const { privateKey } = this.state
+ const {
+ privateKey,
+ showWarning,
+ } = this.state
return h(AccountModalContainer, {
+ selectedIdentity,
showBackButton: previousModalState === 'ACCOUNT_DETAILS',
backButtonAction: () => showAccountDetailModal(),
}, [
@@ -134,7 +158,7 @@ ExportPrivateKeyModal.prototype.render = function () {
this.renderPasswordInput(privateKey),
- !warning ? null : h('span.private-key-password-error', warning),
+ showWarning && warning ? h('span.private-key-password-error', warning) : null,
]),
h('div.private-key-password-warning', this.context.t('privateKeyWarning')),
diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js
index 1518fa9a0..fb38516d3 100644
--- a/ui/app/components/modals/hide-token-confirmation-modal.js
+++ b/ui/app/components/modals/hide-token-confirmation-modal.js
@@ -10,6 +10,7 @@ function mapStateToProps (state) {
return {
network: state.metamask.network,
token: state.appState.modal.modalState.props.token,
+ assetImages: state.metamask.assetImages,
}
}
@@ -40,8 +41,9 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmat
HideTokenConfirmationModal.prototype.render = function () {
- const { token, network, hideToken, hideModal } = this.props
+ const { token, network, hideToken, hideModal, assetImages } = this.props
const { symbol, address } = token
+ const image = assetImages[address]
return h('div.hide-token-confirmation', {}, [
h('div.hide-token-confirmation__container', {
@@ -55,6 +57,7 @@ HideTokenConfirmationModal.prototype.render = function () {
diameter: 45,
address,
network,
+ image,
}),
h('div.hide-token-confirmation__symbol', {}, symbol),
diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss
index 06c3ef709..14cdbacd3 100644
--- a/ui/app/components/page-container/index.scss
+++ b/ui/app/components/page-container/index.scss
@@ -109,7 +109,7 @@
&--selected {
color: $curious-blue;
- border-bottom: 3px solid $curious-blue;
+ border-bottom: 2px solid $curious-blue;
}
}
diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/page-container/page-container-header/page-container-header.component.js
index 5a5de1e5a..a8458604e 100644
--- a/ui/app/components/page-container/page-container-header/page-container-header.component.js
+++ b/ui/app/components/page-container/page-container-header/page-container-header.component.js
@@ -1,8 +1,8 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
+import classnames from 'classnames'
export default class PageContainerHeader extends Component {
-
static propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
@@ -11,8 +11,18 @@ export default class PageContainerHeader extends Component {
onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
- children: PropTypes.node,
- };
+ tabs: PropTypes.node,
+ }
+
+ renderTabs () {
+ const { tabs } = this.props
+
+ return tabs && (
+ <ul className="page-container__tabs">
+ { tabs }
+ </ul>
+ )
+ }
renderHeaderRow () {
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
@@ -31,15 +41,18 @@ export default class PageContainerHeader extends Component {
}
render () {
- const { title, subtitle, onClose, children } = this.props
+ const { title, subtitle, onClose, tabs } = this.props
return (
- <div className="page-container__header">
+ <div className={
+ classnames(
+ 'page-container__header',
+ { 'page-container__header--no-padding-bottom': Boolean(tabs) }
+ )
+ }>
{ this.renderHeaderRow() }
- { children }
-
{
title && <div className="page-container__title">
{ title }
@@ -59,6 +72,7 @@ export default class PageContainerHeader extends Component {
/>
}
+ { this.renderTabs() }
</div>
)
}
diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/page-container/page-container.component.js
index 9bfb99ade..3a2274a29 100644
--- a/ui/app/components/page-container/page-container.component.js
+++ b/ui/app/components/page-container/page-container.component.js
@@ -1,30 +1,82 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import PageContainerHeader from './page-container-header'
import PageContainerFooter from './page-container-footer'
-export default class PageContainer extends Component {
-
+export default class PageContainer extends PureComponent {
static propTypes = {
// PageContainerHeader props
- title: PropTypes.string.isRequired,
- subtitle: PropTypes.string,
+ backButtonString: PropTypes.string,
+ backButtonStyles: PropTypes.object,
+ onBackButtonClick: PropTypes.func,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
- onBackButtonClick: PropTypes.func,
- backButtonStyles: PropTypes.object,
- backButtonString: PropTypes.string,
+ subtitle: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ // Tabs-related props
+ defaultActiveTabIndex: PropTypes.number,
+ tabsComponent: PropTypes.node,
// Content props
- ContentComponent: PropTypes.func,
- contentComponentProps: PropTypes.object,
+ contentComponent: PropTypes.node,
// PageContainerFooter props
- onCancel: PropTypes.func,
cancelText: PropTypes.string,
+ disabled: PropTypes.bool,
+ onCancel: PropTypes.func,
onSubmit: PropTypes.func,
submitText: PropTypes.string,
- disabled: PropTypes.bool,
- };
+ }
+
+ state = {
+ activeTabIndex: this.props.defaultActiveTabIndex || 0,
+ }
+
+ handleTabClick (activeTabIndex) {
+ this.setState({ activeTabIndex })
+ }
+
+ renderTabs () {
+ const { tabsComponent } = this.props
+
+ if (!tabsComponent) {
+ return
+ }
+
+ const numberOfTabs = React.Children.count(tabsComponent.props.children)
+
+ return React.Children.map(tabsComponent.props.children, (child, tabIndex) => {
+ return child && React.cloneElement(child, {
+ onClick: index => this.handleTabClick(index),
+ tabIndex,
+ isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex,
+ key: tabIndex,
+ className: 'page-container__tab',
+ activeClassName: 'page-container__tab--selected',
+ })
+ })
+ }
+
+ renderActiveTabContent () {
+ const { tabsComponent } = this.props
+ const { children } = tabsComponent.props
+ const { activeTabIndex } = this.state
+
+ return children[activeTabIndex]
+ ? children[activeTabIndex].props.children
+ : children.props.children
+ }
+
+ renderContent () {
+ const { contentComponent, tabsComponent } = this.props
+
+ if (contentComponent) {
+ return contentComponent
+ } else if (tabsComponent) {
+ return this.renderActiveTabContent()
+ } else {
+ return null
+ }
+ }
render () {
const {
@@ -35,8 +87,6 @@ export default class PageContainer extends Component {
onBackButtonClick,
backButtonStyles,
backButtonString,
- ContentComponent,
- contentComponentProps,
onCancel,
cancelText,
onSubmit,
@@ -54,9 +104,10 @@ export default class PageContainer extends Component {
onBackButtonClick={onBackButtonClick}
backButtonStyles={backButtonStyles}
backButtonString={backButtonString}
+ tabs={this.renderTabs()}
/>
<div className="page-container__content">
- <ContentComponent { ...contentComponentProps } />
+ { this.renderContent() }
</div>
<PageContainerFooter
onCancel={onCancel}
@@ -68,5 +119,4 @@ export default class PageContainer extends Component {
</div>
)
}
-
}
diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js
index bcb93d401..3612e676c 100644
--- a/ui/app/components/pages/add-token/add-token.component.js
+++ b/ui/app/components/pages/add-token/add-token.component.js
@@ -1,14 +1,14 @@
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'
+import PageContainer from '../../page-container'
+import { Tabs, Tab } from '../../tabs'
const emptyAddr = '0x0000000000000000000000000000000000000000'
const SEARCH_TAB = 'SEARCH'
@@ -206,7 +206,7 @@ class AddToken extends Component {
const validDecimals = customDecimals !== null &&
customDecimals !== '' &&
customDecimals >= 0 &&
- customDecimals < 36
+ customDecimals <= 36
let customDecimalsError = null
if (!validDecimals) {
@@ -285,65 +285,33 @@ class AddToken extends Component {
)
}
+ renderTabs () {
+ return (
+ <Tabs>
+ <Tab name={this.context.t('search')}>
+ { this.renderSearchToken() }
+ </Tab>
+ <Tab name={this.context.t('customToken')}>
+ { this.renderCustomTokenForm() }
+ </Tab>
+ </Tabs>
+ )
+ }
+
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="default"
- 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>
+ <PageContainer
+ title={this.context.t('addTokens')}
+ tabsComponent={this.renderTabs()}
+ onSubmit={() => this.handleNext()}
+ disabled={this.hasError() || !this.hasSelected()}
+ onCancel={() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ }}
+ />
)
}
}
diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
new file mode 100644
index 000000000..c24e1e0ea
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
@@ -0,0 +1,126 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE } from '../../../routes'
+import Button from '../../button'
+import Identicon from '../../../components/identicon'
+import TokenBalance from '../../token-balance'
+
+export default class ConfirmAddSuggestedToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addToken: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ removeSuggestedTokens: PropTypes.func,
+ }
+
+ 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 { addToken, pendingTokens, removeSuggestedTokens, history } = this.props
+ const pendingTokenKey = Object.keys(pendingTokens)[0]
+ const pendingToken = pendingTokens[pendingTokenKey]
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header">
+ <div className="page-container__title">
+ { this.context.t('addSuggestedTokens') }
+ </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, image } = 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}
+ image={image}
+ />
+ <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="default"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ removeSuggestedTokens()
+ .then(() => {
+ history.push(DEFAULT_ROUTE)
+ })
+ }}
+ >
+ { this.context.t('cancel') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ addToken(pendingToken)
+ .then(() => {
+ removeSuggestedTokens()
+ .then(() => {
+ history.push(DEFAULT_ROUTE)
+ })
+ })
+ }}
+ >
+ { this.context.t('addToken') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
new file mode 100644
index 000000000..1f2737e52
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'
+import { withRouter } from 'react-router-dom'
+
+const extend = require('xtend')
+
+const { addToken, removeSuggestedTokens } = require('../../../actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens, suggestedTokens } = metamask
+ const params = extend(pendingTokens, suggestedTokens)
+
+ return {
+ pendingTokens: params,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)),
+ removeSuggestedTokens: () => dispatch(removeSuggestedTokens()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmAddSuggestedToken)
diff --git a/ui/app/components/pages/confirm-add-suggested-token/index.js b/ui/app/components/pages/confirm-add-suggested-token/index.js
new file mode 100644
index 000000000..2ca56b43c
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-suggested-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container'
+module.exports = ConfirmAddSuggestedToken
diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
index 65d654b92..3dcc8cda9 100644
--- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
@@ -2,8 +2,8 @@ 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'
+import Identicon from '../../identicon'
+import TokenBalance from '../../token-balance'
export default class ConfirmAddToken extends Component {
static contextTypes = {
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js
deleted file mode 100644
index 6fb5c8223..000000000
--- a/ui/app/components/pages/confirm-add-token/token-balance/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import TokenBalance from './token-balance.container'
-module.exports = TokenBalance
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
deleted file mode 100644
index 976788d4c..000000000
--- a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
+++ /dev/null
@@ -1,16 +0,0 @@
-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>
- )
- }
-}
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
index 961aa304e..56cfbccc8 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -38,6 +38,7 @@ export default class ConfirmTransactionBase extends Component {
isTxReprice: PropTypes.bool,
methodData: PropTypes.object,
nonce: PropTypes.string,
+ assetImage: PropTypes.string,
sendTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func,
@@ -73,6 +74,7 @@ export default class ConfirmTransactionBase extends Component {
state = {
submitting: false,
+ submitError: null,
}
componentDidUpdate () {
@@ -268,7 +270,7 @@ export default class ConfirmTransactionBase extends Component {
return
}
- this.setState({ submitting: true })
+ this.setState({ submitting: true, submitError: null })
if (onSubmit) {
Promise.resolve(onSubmit(txData))
@@ -280,7 +282,9 @@ export default class ConfirmTransactionBase extends Component {
this.setState({ submitting: false })
history.push(DEFAULT_ROUTE)
})
- .catch(() => this.setState({ submitting: false }))
+ .catch(error => {
+ this.setState({ submitting: false, submitError: error.message })
+ })
}
}
@@ -307,9 +311,10 @@ export default class ConfirmTransactionBase extends Component {
contentComponent,
onEdit,
nonce,
+ assetImage,
warning,
} = this.props
- const { submitting } = this.state
+ const { submitting, submitError } = this.state
const { name } = methodData
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
@@ -331,8 +336,9 @@ export default class ConfirmTransactionBase extends Component {
dataComponent={this.renderData()}
contentComponent={contentComponent}
nonce={nonce}
+ assetImage={assetImage}
identiconAddress={identiconAddress}
- errorMessage={errorMessage}
+ errorMessage={errorMessage || submitError}
errorKey={propsErrorKey || errorKey}
warning={warning}
disabled={!propsValid || !valid || submitting}
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
index 0c0deff18..8f54c8040 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -52,8 +52,9 @@ const mapStateToProps = (state, props) => {
accounts,
selectedAddress,
selectedAddressTxList,
+ assetImages,
} = metamask
-
+ const assetImage = assetImages[txParamsToAddress]
const { balance } = accounts[selectedAddress]
const { name: fromName } = identities[selectedAddress]
const toAddress = propsToAddress || txParamsToAddress
@@ -88,6 +89,7 @@ const mapStateToProps = (state, props) => {
conversionRate,
transactionStatus,
nonce,
+ assetImage,
}
}
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
index 0280f73c6..2c44b6094 100644
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -12,25 +12,27 @@ import {
CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH,
} from '../../../routes'
-import { isConfirmDeployContract } from './confirm-transaction-switch.util'
+import { isConfirmDeployContract } from '../../../helpers/transactions.util'
import {
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM,
-} from './confirm-transaction-switch.constants'
+} from '../../../constants/transactions'
export default class ConfirmTransactionSwitch extends Component {
static propTypes = {
txData: PropTypes.object,
methodData: PropTypes.object,
- fetchingMethodData: PropTypes.bool,
+ fetchingData: PropTypes.bool,
+ isEtherTransaction: PropTypes.bool,
}
redirectToTransaction () {
const {
txData,
methodData: { name },
- fetchingMethodData,
+ fetchingData,
+ isEtherTransaction,
} = this.props
const { id, txParams: { data } = {} } = txData
@@ -39,10 +41,15 @@ export default class ConfirmTransactionSwitch extends Component {
return <Redirect to={{ pathname }} />
}
- if (fetchingMethodData) {
+ if (fetchingData) {
return <Loading />
}
+ if (isEtherTransaction) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
if (data) {
const methodName = name && name.toLowerCase()
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
deleted file mode 100644
index 9db4a2f96..000000000
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const TOKEN_METHOD_TRANSFER = 'transfer'
-export const TOKEN_METHOD_APPROVE = 'approve'
-export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
index 3d7fc78cc..7f2c36af2 100644
--- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
@@ -6,14 +6,16 @@ const mapStateToProps = state => {
confirmTransaction: {
txData,
methodData,
- fetchingMethodData,
+ fetchingData,
+ toSmartContract,
},
} = state
return {
txData,
methodData,
- fetchingMethodData,
+ fetchingData,
+ isEtherTransaction: !toSmartContract,
}
}
diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js
deleted file mode 100644
index 5e3fdc9af..000000000
--- a/ui/app/components/pages/home.js
+++ /dev/null
@@ -1,239 +0,0 @@
-const { Component } = require('react')
-const { connect } = require('react-redux')
-const PropTypes = require('prop-types')
-const { Redirect, withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const h = require('react-hyperscript')
-const actions = require('../../actions')
-const log = require('loglevel')
-
-// init
-const NewKeyChainScreen = require('../../new-keychain')
-// mascara
-const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
-
-// accounts
-const MainContainer = require('../../main-container')
-
-// other views
-const BuyView = require('../../components/buy-button-subview')
-const QrView = require('../../components/qr-code')
-
-// Routes
-const {
- INITIALIZE_BACKUP_PHRASE_ROUTE,
- RESTORE_VAULT_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
- NOTICE_ROUTE,
-} = require('../../routes')
-
-const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
-
-class Home extends Component {
- componentDidMount () {
- const {
- history,
- unconfirmedTransactionsCount = 0,
- } = this.props
-
- // unapprovedTxs and unapproved messages
- if (unconfirmedTransactionsCount > 0) {
- history.push(CONFIRM_TRANSACTION_ROUTE)
- }
- }
-
- render () {
- log.debug('rendering primary')
- const {
- noActiveNotices,
- lostAccounts,
- forgottenPassword,
- currentView,
- activeAddress,
- seedWords,
- } = this.props
-
- // notices
- if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
- return h(Redirect, {
- to: {
- pathname: NOTICE_ROUTE,
- },
- })
- }
-
- // seed words
- if (seedWords) {
- log.debug('rendering seed words')
- return h(Redirect, {
- to: {
- pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
- },
- })
- }
-
- if (forgottenPassword) {
- log.debug('rendering restore vault screen')
- return h(Redirect, {
- to: {
- pathname: RESTORE_VAULT_ROUTE,
- },
- })
- }
-
- // show current view
- switch (currentView.name) {
-
- case 'accountDetail':
- log.debug('rendering main container')
- return h(MainContainer, {key: 'account-detail'})
-
- case 'newKeychain':
- log.debug('rendering new keychain screen')
- return h(NewKeyChainScreen, {key: 'new-keychain'})
-
- case 'buyEth':
- log.debug('rendering buy ether screen')
- return h(BuyView, {key: 'buyEthView'})
-
- case 'onboardingBuyEth':
- log.debug('rendering onboarding buy ether screen')
- return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
-
- case 'qr':
- log.debug('rendering show qr screen')
- return h('div', {
- style: {
- position: 'absolute',
- height: '100%',
- top: '0px',
- left: '0px',
- },
- }, [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
- style: {
- marginLeft: '10px',
- marginTop: '50px',
- },
- }),
- h('div', {
- style: {
- position: 'absolute',
- left: '44px',
- width: '285px',
- },
- }, [
- h(QrView, {key: 'qr'}),
- ]),
- ])
-
- default:
- log.debug('rendering default, account detail screen')
- return h(MainContainer, {key: 'account-detail'})
- }
- }
-}
-
-Home.propTypes = {
- currentCurrency: PropTypes.string,
- isLoading: PropTypes.bool,
- loadingMessage: PropTypes.string,
- network: PropTypes.string,
- provider: PropTypes.object,
- frequentRpcList: PropTypes.array,
- currentView: PropTypes.object,
- sidebarOpen: PropTypes.bool,
- isMascara: PropTypes.bool,
- isOnboarding: PropTypes.bool,
- isUnlocked: PropTypes.bool,
- networkDropdownOpen: PropTypes.bool,
- history: PropTypes.object,
- dispatch: PropTypes.func,
- selectedAddress: PropTypes.string,
- noActiveNotices: PropTypes.bool,
- lostAccounts: PropTypes.array,
- isInitialized: PropTypes.bool,
- forgottenPassword: PropTypes.bool,
- activeAddress: PropTypes.string,
- unapprovedTxs: PropTypes.object,
- seedWords: PropTypes.string,
- unapprovedMsgCount: PropTypes.number,
- unapprovedPersonalMsgCount: PropTypes.number,
- unapprovedTypedMessagesCount: PropTypes.number,
- welcomeScreenSeen: PropTypes.bool,
- isPopup: PropTypes.bool,
- isMouseUser: PropTypes.bool,
- t: PropTypes.func,
- unconfirmedTransactionsCount: PropTypes.number,
-}
-
-function mapStateToProps (state) {
- const { appState, metamask } = state
- const {
- networkDropdownOpen,
- sidebarOpen,
- isLoading,
- loadingMessage,
- } = appState
-
- const {
- accounts,
- address,
- isInitialized,
- noActiveNotices,
- seedWords,
- unapprovedTxs,
- nextUnreadNotice,
- lostAccounts,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- } = metamask
- const selected = address || Object.keys(accounts)[0]
-
- return {
- // state from plugin
- networkDropdownOpen,
- sidebarOpen,
- isLoading,
- loadingMessage,
- noActiveNotices,
- isInitialized,
- isUnlocked: state.metamask.isUnlocked,
- selectedAddress: state.metamask.selectedAddress,
- currentView: state.appState.currentView,
- activeAddress: state.appState.activeAddress,
- transForward: state.appState.transForward,
- isMascara: state.metamask.isMascara,
- isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
- isPopup: state.metamask.isPopup,
- seedWords: state.metamask.seedWords,
- unapprovedTxs,
- unapprovedMsgs: state.metamask.unapprovedMsgs,
- unapprovedMsgCount,
- unapprovedPersonalMsgCount,
- unapprovedTypedMessagesCount,
- menuOpen: state.appState.menuOpen,
- network: state.metamask.network,
- provider: state.metamask.provider,
- forgottenPassword: state.appState.forgottenPassword,
- nextUnreadNotice,
- lostAccounts,
- frequentRpcList: state.metamask.frequentRpcList || [],
- currentCurrency: state.metamask.currentCurrency,
- isMouseUser: state.appState.isMouseUser,
- isRevealingSeedWords: state.metamask.isRevealingSeedWords,
- Qr: state.appState.Qr,
- welcomeScreenSeen: state.metamask.welcomeScreenSeen,
-
- // state needed to get account dropdown temporarily rendering from app bar
- selected,
- unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps)
-)(Home)
diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js
new file mode 100644
index 000000000..d3c71c4f6
--- /dev/null
+++ b/ui/app/components/pages/home/home.component.js
@@ -0,0 +1,77 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Media from 'react-media'
+import { Redirect } from 'react-router-dom'
+import WalletView from '../../wallet-view'
+import TransactionView from '../../transaction-view'
+import {
+ INITIALIZE_BACKUP_PHRASE_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ NOTICE_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+} from '../../../routes'
+
+export default class Home extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ noActiveNotices: PropTypes.bool,
+ lostAccounts: PropTypes.array,
+ forgottenPassword: PropTypes.bool,
+ seedWords: PropTypes.string,
+ suggestedTokens: PropTypes.object,
+ unconfirmedTransactionsCount: PropTypes.number,
+ }
+
+ componentDidMount () {
+ const {
+ history,
+ suggestedTokens = {},
+ unconfirmedTransactionsCount = 0,
+ } = this.props
+
+ // suggested new tokens
+ if (Object.keys(suggestedTokens).length > 0) {
+ history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
+ }
+
+ if (unconfirmedTransactionsCount > 0) {
+ history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+ }
+
+ render () {
+ const {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ seedWords,
+ } = this.props
+
+ // notices
+ if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
+ return <Redirect to={{ pathname: NOTICE_ROUTE }} />
+ }
+
+ // seed words
+ if (seedWords) {
+ return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
+ }
+
+ if (forgottenPassword) {
+ return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
+ }
+
+ return (
+ <div className="main-container">
+ <div className="account-and-transaction-details">
+ <Media
+ query="(min-width: 576px)"
+ render={() => <WalletView />}
+ />
+ <TransactionView />
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/home/home.container.js b/ui/app/components/pages/home/home.container.js
new file mode 100644
index 000000000..58001df6b
--- /dev/null
+++ b/ui/app/components/pages/home/home.container.js
@@ -0,0 +1,30 @@
+import Home from './home.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { unconfirmedTransactionsCountSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask, appState } = state
+ const {
+ noActiveNotices,
+ lostAccounts,
+ seedWords,
+ suggestedTokens,
+ } = metamask
+ const { forgottenPassword } = appState
+
+ return {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ seedWords,
+ suggestedTokens,
+ unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps)
+)(Home)
diff --git a/ui/app/components/pages/home/index.js b/ui/app/components/pages/home/index.js
new file mode 100644
index 000000000..4474ba5b8
--- /dev/null
+++ b/ui/app/components/pages/home/index.js
@@ -0,0 +1 @@
+export { default } from './home.container'
diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js
index ff42a13de..a5ea1b89c 100644
--- a/ui/app/components/pages/settings/settings.js
+++ b/ui/app/components/pages/settings/settings.js
@@ -66,6 +66,30 @@ class Settings extends Component {
])
}
+ renderHexDataOptIn () {
+ const { metamask: { featureFlags: { sendHexData } }, setHexDataFeatureFlag } = this.props
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', this.context.t('showHexData')),
+ h(
+ 'div.settings__content-description',
+ this.context.t('showHexDataDescription')
+ ),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(ToggleButton, {
+ value: sendHexData,
+ onToggle: (value) => setHexDataFeatureFlag(!value),
+ activeLabel: '',
+ inactiveLabel: '',
+ }),
+ ]),
+ ]),
+ ])
+ }
+
renderCurrentConversion () {
const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props
@@ -307,6 +331,7 @@ class Settings extends Component {
!isMascara && this.renderOldUI(),
this.renderResetAccount(),
this.renderBlockieOptIn(),
+ this.renderHexDataOptIn(),
])
)
}
@@ -315,6 +340,7 @@ class Settings extends Component {
Settings.propTypes = {
metamask: PropTypes.object,
setUseBlockie: PropTypes.func,
+ setHexDataFeatureFlag: PropTypes.func,
setCurrentCurrency: PropTypes.func,
setRpcTarget: PropTypes.func,
displayWarning: PropTypes.func,
@@ -349,6 +375,9 @@ const mapDispatchToProps = dispatch => {
setFeatureFlagToBeta: () => {
return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
},
+ setHexDataFeatureFlag: (featureFlagShowState) => {
+ return dispatch(actions.setFeatureFlag('sendHexData', featureFlagShowState))
+ },
showResetAccountConfirmationModal: () => {
return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' }))
},
diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js
deleted file mode 100644
index f16fcb1c7..000000000
--- a/ui/app/components/pending-msg-details.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const connect = require('react-redux').connect
-
-const AccountPanel = require('./account-panel')
-
-PendingMsgDetails.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(PendingMsgDetails)
-
-
-inherits(PendingMsgDetails, Component)
-function PendingMsgDetails () {
- Component.call(this)
-}
-
-PendingMsgDetails.prototype.render = function () {
- var state = this.props
- var msgData = state.txData
-
- var msgParams = msgData.msgParams || {}
- var address = msgParams.from || state.selectedAddress
- var identity = state.identities[address] || { address: address }
- var account = state.accounts[address] || { address: address }
-
- return (
- h('div', {
- key: msgData.id,
- style: {
- margin: '10px 20px',
- },
- }, [
-
- // account that will sign
- h(AccountPanel, {
- showFullAddress: true,
- identity: identity,
- account: account,
- imageifyIdenticons: state.imageifyIdenticons,
- }),
-
- // message data
- h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
- h('.flex-column.flex-space-between', [
- h('label.font-small.allcaps', this.context.t('message')),
- h('span.font-small', msgParams.data),
- ]),
- ]),
-
- ])
- )
-}
diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js
deleted file mode 100644
index 21a7864e4..000000000
--- a/ui/app/components/pending-msg.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const PendingTxDetails = require('./pending-msg-details')
-const connect = require('react-redux').connect
-
-PendingMsg.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(PendingMsg)
-
-
-inherits(PendingMsg, Component)
-function PendingMsg () {
- Component.call(this)
-}
-
-PendingMsg.prototype.render = function () {
- var state = this.props
- var msgData = state.txData
-
- return (
-
- h('div', {
- key: msgData.id,
- style: {
- maxWidth: '350px',
- },
- }, [
-
- // header
- h('h3', {
- style: {
- fontWeight: 'bold',
- textAlign: 'center',
- },
- }, this.context.t('signMessage')),
-
- h('.error', {
- style: {
- margin: '10px',
- },
- }, [
- this.context.t('signNotice'),
- h('a', {
- href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
- style: { color: 'rgb(247, 134, 28)' },
- onClick: (event) => {
- event.preventDefault()
- const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
- global.platform.openWindow({ url })
- },
- }, this.context.t('readMore')),
- ]),
-
- // message details
- h(PendingTxDetails, state),
-
- // sign + cancel
- h('.flex-row.flex-space-around', [
- h('button', {
- onClick: state.cancelMessage,
- }, this.context.t('cancel')),
- h('button', {
- onClick: state.signMessage,
- }, this.context.t('sign')),
- ]),
- ])
-
- )
-}
diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js
index df7bcb7cc..9e0ce9c23 100644
--- a/ui/app/components/send/send-content/send-content.component.js
+++ b/ui/app/components/send/send-content/send-content.component.js
@@ -12,6 +12,7 @@ export default class SendContent extends Component {
static propTypes = {
updateGas: PropTypes.func,
scanQrCode: PropTypes.func,
+ showHexData: PropTypes.bool,
};
render () {
@@ -25,7 +26,7 @@ export default class SendContent extends Component {
/>
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow />
- <SendHexDataRow />
+ { this.props.showHexData ? <SendHexDataRow /> : null }
</div>
</PageContainerContent>
)
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
index 1163dcffc..434db81e5 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
@@ -48,7 +48,7 @@ export default class SendToRow extends Component {
return (
<SendRowWrapper
errorType={'to'}
- label={`${this.context.t('to')}`}
+ label={`${this.context.t('to')}: `}
showError={inError}
>
<EnsInput
diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
index 781371004..591229deb 100644
--- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
@@ -102,7 +102,7 @@ describe('SendToRow Component', function () {
assert.equal(errorType, 'to')
- assert.equal(label, 'to_t')
+ assert.equal(label, 'to_t: ')
assert.equal(showError, false)
})
diff --git a/ui/app/components/send/send-content/tests/send-content-component.test.js b/ui/app/components/send/send-content/tests/send-content-component.test.js
index d5bb6693c..c5a11c8bb 100644
--- a/ui/app/components/send/send-content/tests/send-content-component.test.js
+++ b/ui/app/components/send/send-content/tests/send-content-component.test.js
@@ -8,12 +8,13 @@ import SendAmountRow from '../send-amount-row/send-amount-row.container'
import SendFromRow from '../send-from-row/send-from-row.container'
import SendGasRow from '../send-gas-row/send-gas-row.container'
import SendToRow from '../send-to-row/send-to-row.container'
+import SendHexDataRow from '../send-hex-data-row/send-hex-data-row.container'
describe('SendContent Component', function () {
let wrapper
beforeEach(() => {
- wrapper = shallow(<SendContent />)
+ wrapper = shallow(<SendContent showHexData={true} />)
})
describe('render', () => {
@@ -33,6 +34,17 @@ describe('SendContent Component', function () {
assert(PageContainerContentChild.childAt(1).is(SendToRow))
assert(PageContainerContentChild.childAt(2).is(SendAmountRow))
assert(PageContainerContentChild.childAt(3).is(SendGasRow))
+ assert(PageContainerContentChild.childAt(4).is(SendHexDataRow))
+ })
+
+ it('should not render the SendHexDataRow if props.showHexData is false', () => {
+ wrapper.setProps({ showHexData: false })
+ const PageContainerContentChild = wrapper.find(PageContainerContent).children()
+ assert(PageContainerContentChild.childAt(0).is(SendFromRow))
+ assert(PageContainerContentChild.childAt(1).is(SendToRow))
+ assert(PageContainerContentChild.childAt(2).is(SendAmountRow))
+ assert(PageContainerContentChild.childAt(3).is(SendGasRow))
+ assert.equal(PageContainerContentChild.childAt(4).exists(), false)
})
})
})
diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js
index 0d8ffd179..0dc973632 100644
--- a/ui/app/components/send/send.component.js
+++ b/ui/app/components/send/send.component.js
@@ -193,7 +193,7 @@ export default class SendTransactionScreen extends PersistentForm {
}
render () {
- const { history } = this.props
+ const { history, showHexData } = this.props
return (
<div className="page-container">
@@ -201,6 +201,7 @@ export default class SendTransactionScreen extends PersistentForm {
<SendContent
updateGas={(updateData) => this.updateGas(updateData)}
scanQrCode={_ => this.props.scanQrCode()}
+ showHexData={showHexData}
/>
<SendFooter history={history}/>
</div>
diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js
index 41735de64..6ee8de9aa 100644
--- a/ui/app/components/send/send.container.js
+++ b/ui/app/components/send/send.container.js
@@ -18,6 +18,7 @@ import {
getSelectedTokenToFiatRate,
getSendAmount,
getSendEditingTransactionId,
+ getSendHexDataFeatureFlagState,
getSendFromObject,
getSendTo,
getTokenBalance,
@@ -64,6 +65,7 @@ function mapStateToProps (state) {
recentBlocks: getRecentBlocks(state),
selectedAddress: getSelectedAddress(state),
selectedToken: getSelectedToken(state),
+ showHexData: getSendHexDataFeatureFlagState(state),
to: getSendTo(state),
tokenBalance: getTokenBalance(state),
tokenContract: getSelectedTokenContract(state),
diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js
index ab3f6d34b..22e379693 100644
--- a/ui/app/components/send/send.selectors.js
+++ b/ui/app/components/send/send.selectors.js
@@ -34,6 +34,7 @@ const selectors = {
getSelectedTokenToFiatRate,
getSendAmount,
getSendHexData,
+ getSendHexDataFeatureFlagState,
getSendEditingTransactionId,
getSendErrors,
getSendFrom,
@@ -216,6 +217,10 @@ function getSendHexData (state) {
return state.metamask.send.data
}
+function getSendHexDataFeatureFlagState (state) {
+ return state.metamask.featureFlags.sendHexData
+}
+
function getSendEditingTransactionId (state) {
return state.metamask.send.editingTransactionId
}
diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js
index 6194ec508..d2c2ee926 100644
--- a/ui/app/components/send/tests/send-component.test.js
+++ b/ui/app/components/send/tests/send-component.test.js
@@ -47,6 +47,7 @@ describe('Send Component', function () {
recentBlocks={['mockBlock']}
selectedAddress={'mockSelectedAddress'}
selectedToken={'mockSelectedToken'}
+ showHexData={true}
tokenBalance={'mockTokenBalance'}
tokenContract={'mockTokenContract'}
updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
@@ -328,5 +329,9 @@ describe('Send Component', function () {
}
)
})
+
+ it('should pass showHexData to SendContent', () => {
+ assert.equal(wrapper.find(SendContent).props().showHexData, true)
+ })
})
})
diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js
index 57e332780..85eec6a53 100644
--- a/ui/app/components/send/tests/send-container.test.js
+++ b/ui/app/components/send/tests/send-container.test.js
@@ -39,6 +39,7 @@ proxyquire('../send.container.js', {
getSelectedToken: (s) => `mockSelectedToken:${s}`,
getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`,
+ getSendHexDataFeatureFlagState: (s) => `mockSendHexDataFeatureFlagState:${s}`,
getSendAmount: (s) => `mockAmount:${s}`,
getSendTo: (s) => `mockTo:${s}`,
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
@@ -73,6 +74,7 @@ describe('send container', () => {
recentBlocks: 'mockRecentBlocks:mockState',
selectedAddress: 'mockSelectedAddress:mockState',
selectedToken: 'mockSelectedToken:mockState',
+ showHexData: 'mockSendHexDataFeatureFlagState:mockState',
to: 'mockTo:mockState',
tokenBalance: 'mockTokenBalance:mockState',
tokenContract: 'mockTokenContract:mockState',
diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js
index 8f9c19314..8b939dadb 100644
--- a/ui/app/components/send/tests/send-selectors-test-data.js
+++ b/ui/app/components/send/tests/send-selectors-test-data.js
@@ -2,7 +2,7 @@ module.exports = {
'metamask': {
'isInitialized': true,
'isUnlocked': true,
- 'featureFlags': {'betaUI': true},
+ 'featureFlags': {'betaUI': true, 'sendHexData': true},
'rpcTarget': 'https://rawtestrpc.metamask.io/',
'identities': {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js
index 218da656b..1a47cd209 100644
--- a/ui/app/components/send/tests/send-selectors.test.js
+++ b/ui/app/components/send/tests/send-selectors.test.js
@@ -31,6 +31,7 @@ const {
getSendFrom,
getSendFromBalance,
getSendFromObject,
+ getSendHexDataFeatureFlagState,
getSendMaxModeState,
getSendTo,
getSendToAccounts,
@@ -379,6 +380,15 @@ describe('send selectors', () => {
})
})
+ describe('getSendHexDataFeatureFlagState()', () => {
+ it('should return the sendHexData feature flag state', () => {
+ assert.deepEqual(
+ getSendHexDataFeatureFlagState(mockState),
+ true
+ )
+ })
+ })
+
describe('getSendFrom()', () => {
it('should return the send.from', () => {
assert.deepEqual(
diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/sender-to-recipient/index.scss
index a97393b8f..656e30ddf 100644
--- a/ui/app/components/sender-to-recipient/index.scss
+++ b/ui/app/components/sender-to-recipient/index.scss
@@ -1,5 +1,5 @@
.sender-to-recipient {
- &__container {
+ &--default {
width: 100%;
display: flex;
flex-direction: row;
@@ -8,67 +8,114 @@
position: relative;
flex: 0 0 auto;
height: 42px;
- }
- &__tooltip-wrapper {
- min-width: 0;
- }
+ .sender-to-recipient {
+ &__tooltip-wrapper {
+ min-width: 0;
+ }
- &__tooltip-container {
- max-width: 100%;
- }
+ &__tooltip-container {
+ max-width: 100%;
+ }
- &__sender,
- &__recipient {
- display: flex;
- flex-direction: row;
- align-items: center;
- flex: 1;
- padding: 0 16px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex: 1;
+ padding: 0 16px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
- &__sender {
- padding-right: 30px;
- cursor: pointer;
- }
+ &--sender {
+ padding-right: 30px;
+ cursor: pointer;
+ }
+
+ &--recipient {
+ padding-left: 30px;
+ border-left: 1px solid $geyser;
+
+ &-with-address {
+ cursor: pointer;
+ }
+ }
+ }
- &__recipient {
- padding-left: 30px;
- border-left: 1px solid $geyser;
+ &__arrow-container {
+ position: absolute;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
- &--with-address {
- cursor: pointer;
+ &__arrow-circle {
+ background: $white;
+ padding: 5px;
+ border: 1px solid $geyser;
+ border-radius: 20px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__name {
+ padding-left: 14px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .875rem;
+ }
}
}
- &__arrow-container {
- position: absolute;
- height: 100%;
+ &--cards {
+ width: 100%;
display: flex;
- align-items: center;
+ flex-direction: row;
justify-content: center;
- }
+ position: relative;
+ flex: 0 0 auto;
+ padding: 8px;
- &__arrow-circle {
- background: $white;
- padding: 5px;
- border: 1px solid $geyser;
- border-radius: 20px;
- height: 32px;
- width: 32px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
+ .sender-to-recipient {
+ &__party {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex: 1;
+ border-radius: 4px;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
+ padding: 6px;
+ background: $white;
+ cursor: pointer;
+ min-width: 0;
+ color: $dusty-gray;
+ }
+
+ &__tooltip-wrapper {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
- &__name {
- padding-left: 14px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: .875rem;
+ &__name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: .5rem;
+ }
+
+ &__arrow-container {
+ padding: 0 2px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
}
}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
index cae173b56..445a11d8a 100644
--- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
@@ -1,16 +1,30 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
+import classnames from 'classnames'
import Identicon from '../identicon'
import Tooltip from '../tooltip-v2'
import copyToClipboard from 'copy-to-clipboard'
+import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants'
-export default class SenderToRecipient extends Component {
+const variantHash = {
+ [DEFAULT_VARIANT]: 'sender-to-recipient--default',
+ [CARDS_VARIANT]: 'sender-to-recipient--cards',
+}
+
+export default class SenderToRecipient extends PureComponent {
static propTypes = {
senderName: PropTypes.string,
senderAddress: PropTypes.string,
recipientName: PropTypes.string,
recipientAddress: PropTypes.string,
t: PropTypes.func,
+ variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]),
+ addressOnly: PropTypes.bool,
+ assetImage: PropTypes.string,
+ }
+
+ static defaultProps = {
+ variant: DEFAULT_VARIANT,
}
static contextTypes = {
@@ -22,24 +36,63 @@ export default class SenderToRecipient extends Component {
recipientAddressCopied: false,
}
+ renderSenderIdenticon () {
+ return !this.props.addressOnly && (
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={this.props.senderAddress}
+ diameter={24}
+ />
+ </div>
+ )
+ }
+
+ renderSenderAddress () {
+ const { t } = this.context
+ const { senderName, senderAddress, addressOnly } = this.props
+
+ return (
+ <Tooltip
+ position="bottom"
+ title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ senderAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name">
+ { addressOnly ? `${t('from')}: ${senderAddress}` : senderName }
+ </div>
+ </Tooltip>
+ )
+ }
+
+ renderRecipientIdenticon () {
+ const { recipientAddress, assetImage } = this.props
+
+ return !this.props.addressOnly && (
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={recipientAddress}
+ diameter={24}
+ image={assetImage}
+ />
+ </div>
+ )
+ }
+
renderRecipientWithAddress () {
const { t } = this.context
- const { recipientName, recipientAddress } = this.props
+ const { recipientName, recipientAddress, addressOnly } = this.props
return (
<div
- className="sender-to-recipient__recipient sender-to-recipient__recipient--with-address"
+ className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
onClick={() => {
this.setState({ recipientAddressCopied: true })
copyToClipboard(recipientAddress)
}}
>
- <div className="sender-to-recipient__sender-icon">
- <Identicon
- address={recipientAddress}
- diameter={24}
- />
- </div>
+ { this.renderRecipientIdenticon() }
<Tooltip
position="bottom"
title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
@@ -47,8 +100,12 @@ export default class SenderToRecipient extends Component {
containerClassName="sender-to-recipient__tooltip-container"
onHidden={() => this.setState({ recipientAddressCopied: false })}
>
- <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
- { recipientName || this.context.t('newContract') }
+ <div className="sender-to-recipient__name">
+ {
+ addressOnly
+ ? `${t('to')}: ${recipientAddress}`
+ : (recipientName || this.context.t('newContract'))
+ }
</div>
</Tooltip>
</div>
@@ -57,46 +114,25 @@ export default class SenderToRecipient extends Component {
renderRecipientWithoutAddress () {
return (
- <div className="sender-to-recipient__recipient">
+ <div className="sender-to-recipient__party sender-to-recipient__party--recipient">
<i className="fa fa-file-text-o" />
- <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
+ <div className="sender-to-recipient__name">
{ this.context.t('newContract') }
</div>
</div>
)
}
- render () {
- const { t } = this.context
- const { senderName, senderAddress, recipientAddress } = this.props
-
- return (
- <div className="sender-to-recipient__container">
- <div
- className="sender-to-recipient__sender"
- onClick={() => {
- this.setState({ senderAddressCopied: true })
- copyToClipboard(senderAddress)
- }}
- >
- <div className="sender-to-recipient__sender-icon">
- <Identicon
- address={senderAddress}
- diameter={24}
- />
- </div>
- <Tooltip
- position="bottom"
- title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
- wrapperClassName="sender-to-recipient__tooltip-wrapper"
- containerClassName="sender-to-recipient__tooltip-container"
- onHidden={() => this.setState({ senderAddressCopied: false })}
- >
- <div className="sender-to-recipient__name sender-to-recipient__sender-name">
- { senderName }
- </div>
- </Tooltip>
+ renderArrow () {
+ return this.props.variant === CARDS_VARIANT
+ ? (
+ <div className="sender-to-recipient__arrow-container">
+ <img
+ height={20}
+ src="./images/caret-right.svg"
+ />
</div>
+ ) : (
<div className="sender-to-recipient__arrow-container">
<div className="sender-to-recipient__arrow-circle">
<img
@@ -106,6 +142,25 @@ export default class SenderToRecipient extends Component {
/>
</div>
</div>
+ )
+ }
+
+ render () {
+ const { senderAddress, recipientAddress, variant } = this.props
+
+ return (
+ <div className={classnames(variantHash[variant])}>
+ <div
+ className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
+ onClick={() => {
+ this.setState({ senderAddressCopied: true })
+ copyToClipboard(senderAddress)
+ }}
+ >
+ { this.renderSenderIdenticon() }
+ { this.renderSenderAddress() }
+ </div>
+ { this.renderArrow() }
{
recipientAddress
? this.renderRecipientWithAddress()
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
new file mode 100644
index 000000000..166228932
--- /dev/null
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js
@@ -0,0 +1,3 @@
+// Component design variants
+export const DEFAULT_VARIANT = 'DEFAULT_VARIANT'
+export const CARDS_VARIANT = 'CARDS_VARIANT'
diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js
index 4334aacba..b87bf959e 100644
--- a/ui/app/components/shift-list-item.js
+++ b/ui/app/components/shift-list-item.js
@@ -35,12 +35,13 @@ function ShiftListItem () {
}
ShiftListItem.prototype.render = function () {
- return h('div.tx-list-item.tx-list-clickable', {
+ return h('div.transaction-list-item.tx-list-clickable', {
style: {
paddingTop: '20px',
paddingBottom: '20px',
justifyContent: 'space-around',
alignItems: 'center',
+ flexDirection: 'row',
},
}, [
h('div', {
diff --git a/ui/app/components/sidebars/index.js b/ui/app/components/sidebars/index.js
new file mode 100644
index 000000000..732925f69
--- /dev/null
+++ b/ui/app/components/sidebars/index.js
@@ -0,0 +1 @@
+export { default } from './sidebar.component'
diff --git a/ui/app/components/sidebars/index.scss b/ui/app/components/sidebars/index.scss
new file mode 100644
index 000000000..5ab0664df
--- /dev/null
+++ b/ui/app/components/sidebars/index.scss
@@ -0,0 +1,74 @@
+.sidebar-right-enter {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(-100%);
+}
+
+.sidebar-right-enter.sidebar-right-enter-active {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(0%);
+}
+
+.sidebar-right-leave {
+ transition: transform 200ms ease-out;
+ transform: translateX(0%);
+}
+
+.sidebar-right-leave.sidebar-right-leave-active {
+ transition: transform 200ms ease-out;
+ transform: translateX(-100%);
+}
+
+.sidebar-left-enter {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(100%);
+}
+
+.sidebar-left-enter.sidebar-left-enter-active {
+ transition: transform 300ms ease-in-out;
+ transform: translateX(0%);
+}
+
+.sidebar-left-leave {
+ transition: transform 200ms ease-out;
+ transform: translateX(0%);
+}
+
+.sidebar-left-leave.sidebar-left-leave-active {
+ transition: transform 200ms ease-out;
+ transform: translateX(100%);
+}
+
+.sidebar-left {
+ flex: 1 0 230px;
+ background: rgb(250, 250, 250);
+ z-index: $sidebar-z-index;
+ position: fixed;
+ left: 15%;
+ right: 0;
+ bottom: 0;
+ opacity: 1;
+ visibility: visible;
+ will-change: transform;
+ overflow-y: auto;
+ box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px;
+ width: 85%;
+ height: 100%;
+
+ @media screen and (min-width: 769px) {
+ width: 408px;
+ left: calc(100% - 408px);
+ }
+}
+
+.sidebar-overlay {
+ z-index: $sidebar-overlay-z-index;
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 1;
+ visibility: visible;
+ background-color: rgba(0, 0, 0, .3);
+} \ No newline at end of file
diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/sidebars/sidebar.component.js
new file mode 100644
index 000000000..57cdd7111
--- /dev/null
+++ b/ui/app/components/sidebars/sidebar.component.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
+import WalletView from '../wallet-view'
+import { WALLET_VIEW_SIDEBAR } from './sidebar.constants'
+
+export default class Sidebar extends Component {
+
+ static propTypes = {
+ sidebarOpen: PropTypes.bool,
+ hideSidebar: PropTypes.func,
+ transitionName: PropTypes.string,
+ type: PropTypes.string,
+ };
+
+ renderOverlay () {
+ return <div className="sidebar-overlay" onClick={() => this.props.hideSidebar()} />
+ }
+
+ renderSidebarContent () {
+ const { type } = this.props
+
+ switch (type) {
+ case WALLET_VIEW_SIDEBAR:
+ return <WalletView responsiveDisplayClassname={'sidebar-right' } />
+ default:
+ return null
+ }
+
+ }
+
+ render () {
+ const { transitionName, sidebarOpen } = this.props
+
+ return (
+ <div>
+ <ReactCSSTransitionGroup
+ transitionName={transitionName}
+ transitionEnterTimeout={300}
+ transitionLeaveTimeout={200}
+ >
+ { sidebarOpen ? this.renderSidebarContent() : null }
+ </ReactCSSTransitionGroup>
+ { sidebarOpen ? this.renderOverlay() : null }
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/sidebars/sidebar.constants.js b/ui/app/components/sidebars/sidebar.constants.js
new file mode 100644
index 000000000..1613a8245
--- /dev/null
+++ b/ui/app/components/sidebars/sidebar.constants.js
@@ -0,0 +1 @@
+export const WALLET_VIEW_SIDEBAR = 'wallet-view'
diff --git a/ui/app/components/sidebars/tests/sidebars-component.test.js b/ui/app/components/sidebars/tests/sidebars-component.test.js
new file mode 100644
index 000000000..e2d77518a
--- /dev/null
+++ b/ui/app/components/sidebars/tests/sidebars-component.test.js
@@ -0,0 +1,88 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
+import Sidebar from '../sidebar.component.js'
+
+import WalletView from '../../wallet-view'
+
+const propsMethodSpies = {
+ hideSidebar: sinon.spy(),
+}
+
+describe('Sidebar Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<Sidebar
+ sidebarOpen={false}
+ hideSidebar={propsMethodSpies.hideSidebar}
+ transitionName={'someTransition'}
+ type={'wallet-view'}
+ />)
+ })
+
+ afterEach(() => {
+ propsMethodSpies.hideSidebar.resetHistory()
+ })
+
+ describe('renderOverlay', () => {
+ let renderOverlay
+
+ beforeEach(() => {
+ renderOverlay = shallow(wrapper.instance().renderOverlay())
+ })
+
+ it('should render a overlay element', () => {
+ assert(renderOverlay.hasClass('sidebar-overlay'))
+ })
+
+ it('should pass the correct onClick function to the element', () => {
+ assert.equal(propsMethodSpies.hideSidebar.callCount, 0)
+ renderOverlay.props().onClick()
+ assert.equal(propsMethodSpies.hideSidebar.callCount, 1)
+ })
+ })
+
+ describe('renderSidebarContent', () => {
+ let renderSidebarContent
+
+ beforeEach(() => {
+ wrapper.setProps({ type: 'wallet-view' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ })
+
+ it('should render sidebar content with the correct props', () => {
+ wrapper.setProps({ type: 'wallet-view' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right')
+ })
+
+ it('should not render with an unrecognized type', () => {
+ wrapper.setProps({ type: 'foobar' })
+ renderSidebarContent = wrapper.instance().renderSidebarContent()
+ assert.equal(renderSidebarContent, undefined)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with one child', () => {
+ assert(wrapper.is('div'))
+ assert.equal(wrapper.children().length, 1)
+ })
+
+ it('should render the ReactCSSTransitionGroup without any children', () => {
+ assert(wrapper.children().at(0).is(ReactCSSTransitionGroup))
+ assert.equal(wrapper.children().at(0).children().length, 0)
+ })
+
+ it('should render sidebar content and the overlay if sidebarOpen is true', () => {
+ wrapper.setProps({ sidebarOpen: true })
+ assert.equal(wrapper.children().length, 2)
+ assert(wrapper.children().at(1).hasClass('sidebar-overlay'))
+ assert.equal(wrapper.children().at(0).children().length, 1)
+ assert(wrapper.children().at(0).children().at(0).is(WalletView))
+ })
+ })
+})
diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/tabs/tab/tab.component.js
index a59da8904..9e590391c 100644
--- a/ui/app/components/tabs/tab/tab.component.js
+++ b/ui/app/components/tabs/tab/tab.component.js
@@ -3,13 +3,13 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
const Tab = props => {
- const { name, onClick, isActive, tabIndex } = props
+ const { name, onClick, isActive, tabIndex, className, activeClassName } = props
return (
<li
className={classnames(
- 'tab',
- isActive && 'tab--active',
+ className,
+ { [activeClassName]: isActive },
)}
onClick={event => {
event.preventDefault()
@@ -26,6 +26,13 @@ Tab.propTypes = {
onClick: PropTypes.func,
isActive: PropTypes.bool,
tabIndex: PropTypes.number,
+ className: PropTypes.string,
+ activeClassName: PropTypes.string,
+}
+
+Tab.defaultProps = {
+ className: 'tab',
+ activeClassName: 'tab--active',
}
export default Tab
diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js
deleted file mode 100644
index 99ca7335c..000000000
--- a/ui/app/components/token-balance.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const TokenTracker = require('eth-token-tracker')
-const connect = require('react-redux').connect
-const selectors = require('../selectors')
-const log = require('loglevel')
-
-function mapStateToProps (state) {
- return {
- userAddress: selectors.getSelectedAddress(state),
- }
-}
-
-module.exports = connect(mapStateToProps)(TokenBalance)
-
-
-inherits(TokenBalance, Component)
-function TokenBalance () {
- this.state = {
- string: '',
- symbol: '',
- isLoading: true,
- error: null,
- }
- Component.call(this)
-}
-
-TokenBalance.prototype.render = function () {
- const state = this.state
- const { symbol, string, isLoading } = state
- const { balanceOnly } = this.props
-
- return isLoading
- ? h('span', '')
- : h('span.token-balance', [
- h('span.hide-text-overflow.token-balance__amount', string),
- !balanceOnly && h('span.token-balance__symbol', symbol),
- ])
-}
-
-TokenBalance.prototype.componentDidMount = function () {
- this.createFreshTokenTracker()
-}
-
-TokenBalance.prototype.createFreshTokenTracker = function () {
- if (this.tracker) {
- // Clean up old trackers when refreshing:
- this.tracker.stop()
- this.tracker.removeListener('update', this.balanceUpdater)
- this.tracker.removeListener('error', this.showError)
- }
-
- if (!global.ethereumProvider) return
- const { userAddress, token } = this.props
-
- this.tracker = new TokenTracker({
- userAddress,
- provider: global.ethereumProvider,
- tokens: [token],
- pollingInterval: 8000,
- })
-
-
- // Set up listener instances for cleaning up
- this.balanceUpdater = this.updateBalance.bind(this)
- this.showError = error => {
- this.setState({ error, isLoading: false })
- }
- this.tracker.on('update', this.balanceUpdater)
- this.tracker.on('error', this.showError)
-
- this.tracker.updateBalances()
- .then(() => {
- this.updateBalance(this.tracker.serialize())
- })
- .catch((reason) => {
- log.error(`Problem updating balances`, reason)
- this.setState({ isLoading: false })
- })
-}
-
-TokenBalance.prototype.componentDidUpdate = function (nextProps) {
- const {
- userAddress: oldAddress,
- token: { address: oldTokenAddress },
- } = this.props
- const {
- userAddress: newAddress,
- token: { address: newTokenAddress },
- } = nextProps
-
- if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return
- if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return
-
- this.setState({ isLoading: true })
- this.createFreshTokenTracker()
-}
-
-TokenBalance.prototype.updateBalance = function (tokens = []) {
- if (!this.tracker.running) {
- return
- }
-
- const [{ string, symbol }] = tokens
-
- this.setState({
- string,
- symbol,
- isLoading: false,
- })
-}
-
-TokenBalance.prototype.componentWillUnmount = function () {
- if (!this.tracker) return
- this.tracker.stop()
- this.tracker.removeListener('update', this.balanceUpdater)
- this.tracker.removeListener('error', this.showError)
-}
-
diff --git a/ui/app/components/token-balance/index.js b/ui/app/components/token-balance/index.js
new file mode 100644
index 000000000..f7da15cf8
--- /dev/null
+++ b/ui/app/components/token-balance/index.js
@@ -0,0 +1 @@
+export { default } from './token-balance.container'
diff --git a/ui/app/components/token-balance/token-balance.component.js b/ui/app/components/token-balance/token-balance.component.js
new file mode 100644
index 000000000..2b4f73980
--- /dev/null
+++ b/ui/app/components/token-balance/token-balance.component.js
@@ -0,0 +1,23 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class TokenBalance extends PureComponent {
+ static propTypes = {
+ string: PropTypes.string,
+ symbol: PropTypes.string,
+ error: PropTypes.string,
+ className: PropTypes.string,
+ withSymbol: PropTypes.bool,
+ }
+
+ render () {
+ const { className, string, withSymbol, symbol } = this.props
+
+ return (
+ <div className={classnames('hide-text-overflow', className)}>
+ { string + (withSymbol ? ` ${symbol}` : '') }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/token-balance/token-balance.container.js
index bc1289ce1..adc001f83 100644
--- a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
+++ b/ui/app/components/token-balance/token-balance.container.js
@@ -1,8 +1,8 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
-import withTokenTracker from '../../../../helpers/with-token-tracker'
+import withTokenTracker from '../../higher-order-components/with-token-tracker'
import TokenBalance from './token-balance.component'
-import selectors from '../../../../selectors'
+import selectors from '../../selectors'
const mapStateToProps = state => {
return {
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
index 4100d76a5..477d97597 100644
--- a/ui/app/components/token-cell.js
+++ b/ui/app/components/token-cell.js
@@ -18,7 +18,7 @@ function mapStateToProps (state) {
userAddress: selectors.getSelectedAddress(state),
contractExchangeRates: state.metamask.contractExchangeRates,
conversionRate: state.metamask.conversionRate,
- sidebarOpen: state.appState.sidebarOpen,
+ sidebarOpen: state.appState.sidebar.isOpen,
}
}
@@ -56,8 +56,8 @@ TokenCell.prototype.render = function () {
sidebarOpen,
currentCurrency,
// userAddress,
+ image,
} = props
-
let currentTokenToFiatRate
let currentTokenInFiat
let formattedFiat = ''
@@ -97,6 +97,7 @@ TokenCell.prototype.render = function () {
diameter: 50,
address,
network,
+ image,
}),
h('div.token-list-item__balance-ellipsis', null, [
diff --git a/ui/app/components/token-currency-display/index.js b/ui/app/components/token-currency-display/index.js
new file mode 100644
index 000000000..6065cae1f
--- /dev/null
+++ b/ui/app/components/token-currency-display/index.js
@@ -0,0 +1 @@
+export { default } from './token-currency-display.component'
diff --git a/ui/app/components/token-currency-display/token-currency-display.component.js b/ui/app/components/token-currency-display/token-currency-display.component.js
new file mode 100644
index 000000000..957aec376
--- /dev/null
+++ b/ui/app/components/token-currency-display/token-currency-display.component.js
@@ -0,0 +1,54 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import CurrencyDisplay from '../currency-display/currency-display.component'
+import { getTokenData } from '../../helpers/transactions.util'
+import { calcTokenAmount } from '../../token-util'
+
+export default class TokenCurrencyDisplay extends PureComponent {
+ static propTypes = {
+ transactionData: PropTypes.string,
+ token: PropTypes.object,
+ }
+
+ state = {
+ displayValue: '',
+ }
+
+ componentDidMount () {
+ this.setDisplayValue()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { transactionData } = this.props
+ const { transactionData: prevTransactionData } = prevProps
+
+ if (transactionData !== prevTransactionData) {
+ this.setDisplayValue()
+ }
+ }
+
+ setDisplayValue () {
+ const { transactionData: data, token } = this.props
+ const { decimals = '', symbol = '' } = token
+ const tokenData = getTokenData(data)
+
+ let displayValue
+
+ if (tokenData.params && tokenData.params.length === 2) {
+ const tokenValue = tokenData.params[1].value
+ const tokenAmount = calcTokenAmount(tokenValue, decimals)
+ displayValue = `${tokenAmount} ${symbol}`
+ }
+
+ this.setState({ displayValue })
+ }
+
+ render () {
+ return (
+ <CurrencyDisplay
+ {...this.props}
+ displayValue={this.state.displayValue}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 42351cf89..6a88f30bf 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -13,6 +13,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
tokens: state.metamask.tokens,
userAddress: selectors.getSelectedAddress(state),
+ assetImages: state.metamask.assetImages,
}
}
@@ -44,10 +45,9 @@ function TokenList () {
}
TokenList.prototype.render = function () {
- const { userAddress } = this.props
+ const { userAddress, assetImages } = this.props
const state = this.state
const { tokens, isLoading, error } = state
-
if (isLoading) {
return this.message(this.context.t('loadingTokens'))
}
@@ -74,7 +74,10 @@ TokenList.prototype.render = function () {
])
}
- return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
+ return h('div', tokens.map((tokenData) => {
+ tokenData.image = assetImages[tokenData.address]
+ return h(TokenCell, tokenData)
+ }))
}
diff --git a/ui/app/components/transaction-action/index.js b/ui/app/components/transaction-action/index.js
new file mode 100644
index 000000000..a6e9097f1
--- /dev/null
+++ b/ui/app/components/transaction-action/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-action.component'
diff --git a/ui/app/components/transaction-action/tests/transaction-action.component.test.js b/ui/app/components/transaction-action/tests/transaction-action.component.test.js
new file mode 100644
index 000000000..218792847
--- /dev/null
+++ b/ui/app/components/transaction-action/tests/transaction-action.component.test.js
@@ -0,0 +1,112 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TransactionAction from '../transaction-action.component'
+
+describe('TransactionAction Component', () => {
+ const tOrDefault = key => key
+ global.eth = {
+ getCode: sinon.stub().callsFake(address => {
+ console.log('CALLED')
+ const code = address === 'approveAddress' ? 'contract' : '0x'
+ return Promise.resolve(code)
+ }),
+ }
+
+ describe('Outgoing transaction', () => {
+ it('should render -- when methodData is still fetching', () => {
+ const methodData = { data: {}, done: false, error: null }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(<TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="transaction-action"
+ />, { context: { tOrDefault }})
+
+ assert.equal(wrapper.find('.transaction-action').length, 1)
+ assert.equal(wrapper.text(), '--')
+ })
+
+ it('should render Sent Ether', () => {
+ const methodData = { data: {}, done: true, error: null }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: 'sentEtherAddress',
+ value: '0x2386f26fc10000',
+ },
+ }
+
+ const wrapper = shallow(<TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="transaction-action"
+ />, { context: { tOrDefault }})
+
+ assert.equal(wrapper.find('.transaction-action').length, 1)
+ wrapper.setState({ transactionAction: 'sentEther' })
+ assert.equal(wrapper.text(), 'sentEther')
+ })
+
+ it('should render Approved', () => {
+ const methodData = {
+ data: {
+ name: 'Approve',
+ params: [
+ { type: 'address' },
+ { type: 'uint256' },
+ ],
+ },
+ done: true,
+ error: null,
+ }
+ const transaction = {
+ id: 1,
+ status: 'confirmed',
+ submittedTime: 1534045442919,
+ time: 1534045440641,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x5208',
+ gasPrice: '0x3b9aca00',
+ nonce: '0x96',
+ to: 'approveAddress',
+ value: '0x2386f26fc10000',
+ data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003',
+ },
+ }
+
+ const wrapper = shallow(<TransactionAction
+ methodData={methodData}
+ transaction={transaction}
+ className="transaction-action"
+ />, { context: { tOrDefault }})
+
+ assert.equal(wrapper.find('.transaction-action').length, 1)
+ wrapper.setState({ transactionAction: 'approve' })
+ assert.equal(wrapper.text(), 'approve')
+ })
+ })
+})
diff --git a/ui/app/components/transaction-action/transaction-action.component.js b/ui/app/components/transaction-action/transaction-action.component.js
new file mode 100644
index 000000000..81a1e96d0
--- /dev/null
+++ b/ui/app/components/transaction-action/transaction-action.component.js
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { getTransactionActionKey } from '../../helpers/transactions.util'
+
+export default class TransactionAction extends PureComponent {
+ static contextTypes = {
+ tOrDefault: PropTypes.func,
+ }
+
+ static propTypes = {
+ className: PropTypes.string,
+ transaction: PropTypes.object,
+ methodData: PropTypes.object,
+ }
+
+ state = {
+ transactionAction: '',
+ }
+
+ componentDidMount () {
+ this.getTransactionAction()
+ }
+
+ componentDidUpdate () {
+ this.getTransactionAction()
+ }
+
+ async getTransactionAction () {
+ const { transactionAction } = this.state
+ const { transaction, methodData } = this.props
+ const { data, done } = methodData
+
+ if (!done || transactionAction) {
+ return
+ }
+
+ const actionKey = await getTransactionActionKey(transaction, data)
+ const action = actionKey && this.context.tOrDefault(actionKey)
+ this.setState({ transactionAction: action })
+ }
+
+ render () {
+ const { className, methodData: { done } } = this.props
+ const { transactionAction } = this.state
+
+ return (
+ <div className={className}>
+ { (done && transactionAction) || '--' }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item/index.js b/ui/app/components/transaction-list-item/index.js
new file mode 100644
index 000000000..697cc55e9
--- /dev/null
+++ b/ui/app/components/transaction-list-item/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-list-item.container'
diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss
new file mode 100644
index 000000000..9c53c8960
--- /dev/null
+++ b/ui/app/components/transaction-list-item/index.scss
@@ -0,0 +1,117 @@
+.transaction-list-item {
+ box-sizing: border-box;
+ min-height: 74px;
+ padding: 8px 20px;
+ border-bottom: 1px solid $geyser;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+
+ @media screen and (max-width: $break-small) {
+ padding: 8px 20px 12px;
+ }
+
+ &:hover {
+ background: rgba($alto, .2);
+ }
+
+ &__grid {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 45px 1fr 1fr 1fr;
+ grid-template-areas:
+ "identicon action status primary-amount"
+ "identicon nonce status secondary-amount";
+
+ @media screen and (max-width: $break-small) {
+ grid-template-columns: 45px 5fr 3fr;
+ grid-template-areas:
+ "nonce nonce nonce"
+ "identicon action primary-amount"
+ "identicon status secondary-amount";
+ }
+ }
+
+ &__identicon {
+ grid-area: identicon;
+ grid-row: 1 / span 2;
+ align-self: center;
+
+ @media screen and (max-width: $break-small) {
+ grid-row: 2 / span 2;
+ }
+ }
+
+ &__action {
+ text-transform: capitalize;
+ padding: 0 8px 2px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: action;
+ align-self: end;
+ }
+
+ &__status {
+ grid-area: status;
+ grid-row: 1 / span 2;
+ align-self: center;
+
+ @media screen and (max-width: $break-small) {
+ grid-row: 3;
+ }
+ }
+
+ &__nonce {
+ font-size: .75rem;
+ color: #5e6064;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: nonce;
+ align-self: start;
+
+ @media screen and (max-width: $break-small) {
+ padding-bottom: 4px;
+ }
+ }
+
+ &__amount {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &--primary {
+ text-align: end;
+ grid-area: primary-amount;
+ align-self: end;
+
+ @media screen and (max-width: $break-small) {
+ padding-bottom: 2px;
+ }
+ }
+
+ &--secondary {
+ text-align: end;
+ font-size: .75rem;
+ color: #5e6064;
+ grid-area: secondary-amount;
+ align-self: start;
+ }
+ }
+
+ &__retry {
+ background: #d1edff;
+ border-radius: 12px;
+ font-size: .75rem;
+ padding: 4px 12px;
+ cursor: pointer;
+ margin-top: 8px;
+
+ @media screen and (max-width: $break-small) {
+ font-size: .5rem;
+ }
+ }
+}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js
new file mode 100644
index 000000000..4fddd45ef
--- /dev/null
+++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js
@@ -0,0 +1,151 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Identicon from '../identicon'
+import TransactionStatus from '../transaction-status'
+import TransactionAction from '../transaction-action'
+import CurrencyDisplay from '../currency-display'
+import TokenCurrencyDisplay from '../token-currency-display'
+import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
+import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
+import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
+import { ETH } from '../../constants/common'
+
+export default class TransactionListItem extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ transaction: PropTypes.object,
+ value: PropTypes.string,
+ methodData: PropTypes.object,
+ showRetry: PropTypes.bool,
+ retryTransaction: PropTypes.func,
+ setSelectedToken: PropTypes.func,
+ nonceAndDate: PropTypes.string,
+ token: PropTypes.object,
+ assetImages: PropTypes.object,
+ }
+
+ handleClick = () => {
+ const { transaction, history } = this.props
+ const { id, status, hash, metamaskNetworkId } = transaction
+
+ if (status === UNAPPROVED_STATUS) {
+ history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
+ } else if (hash) {
+ const prefix = prefixForNetwork(metamaskNetworkId)
+ const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
+ global.platform.openWindow({ url: etherscanUrl })
+ }
+ }
+
+ handleRetryClick = event => {
+ event.stopPropagation()
+
+ const {
+ transaction: { txParams: { to } = {} },
+ methodData: { name } = {},
+ setSelectedToken,
+ } = this.props
+
+ if (name === TOKEN_METHOD_TRANSFER) {
+ setSelectedToken(to)
+ }
+
+ this.resubmit()
+ }
+
+ resubmit () {
+ const { transaction: { id }, retryTransaction, history } = this.props
+ retryTransaction(id)
+ .then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
+ }
+
+ renderPrimaryCurrency () {
+ const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
+
+ return token
+ ? (
+ <TokenCurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--primary"
+ token={token}
+ transactionData={data}
+ prefix="-"
+ />
+ ) : (
+ <CurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--primary"
+ value={value}
+ prefix="-"
+ />
+ )
+ }
+
+ renderSecondaryCurrency () {
+ const { token, value } = this.props
+
+ return token
+ ? null
+ : (
+ <CurrencyDisplay
+ className="transaction-list-item__amount transaction-list-item__amount--secondary"
+ prefix="-"
+ value={value}
+ numberOfDecimals={2}
+ currency={ETH}
+ />
+ )
+ }
+
+ render () {
+ const {
+ transaction,
+ methodData,
+ showRetry,
+ nonceAndDate,
+ assetImages,
+ } = this.props
+ const { txParams = {} } = transaction
+
+ return (
+ <div
+ className="transaction-list-item"
+ onClick={this.handleClick}
+ >
+ <div className="transaction-list-item__grid">
+ <Identicon
+ className="transaction-list-item__identicon"
+ address={txParams.to}
+ diameter={34}
+ image={assetImages[txParams.to]}
+ />
+ <TransactionAction
+ transaction={transaction}
+ methodData={methodData}
+ className="transaction-list-item__action"
+ />
+ <div
+ className="transaction-list-item__nonce"
+ title={nonceAndDate}
+ >
+ { nonceAndDate }
+ </div>
+ <TransactionStatus
+ className="transaction-list-item__status"
+ statusKey={transaction.status}
+ />
+ { this.renderPrimaryCurrency() }
+ { this.renderSecondaryCurrency() }
+ </div>
+ {
+ showRetry && methodData.done && (
+ <div
+ className="transaction-list-item__retry"
+ onClick={this.handleRetryClick}
+ >
+ <span>Taking too long? Increase the gas price on your transaction</span>
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js
new file mode 100644
index 000000000..47644241a
--- /dev/null
+++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import withMethodData from '../../higher-order-components/with-method-data'
+import TransactionListItem from './transaction-list-item.component'
+import { setSelectedToken, retryTransaction } from '../../actions'
+import { hexToDecimal } from '../../helpers/conversions.util'
+import { formatDate } from '../../util'
+
+const mapStateToProps = (state, ownProps) => {
+ const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps
+
+ const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
+
+ return {
+ value,
+ nonceAndDate,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
+ retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps),
+ withMethodData,
+)(TransactionListItem)
diff --git a/ui/app/components/transaction-list/index.js b/ui/app/components/transaction-list/index.js
new file mode 100644
index 000000000..688994367
--- /dev/null
+++ b/ui/app/components/transaction-list/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-list.container'
diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss
new file mode 100644
index 000000000..0e8db485c
--- /dev/null
+++ b/ui/app/components/transaction-list/index.scss
@@ -0,0 +1,46 @@
+.transaction-list {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow-y: hidden;
+
+ &__completed-transactions {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ &__header {
+ flex: 0 0 auto;
+ font-size: .875rem;
+ color: $dusty-gray;
+ border-bottom: 1px solid $geyser;
+ padding: 16px 0 8px 20px;
+
+ @media screen and (max-width: $break-small) {
+ padding: 8px 0 8px 16px;
+ }
+ }
+
+ &__transactions {
+ flex: 1;
+ overflow-y: auto;
+ }
+
+ &__pending-transactions {
+ margin-bottom: 16px;
+ }
+
+ &__empty {
+ flex: 1;
+ display: grid;
+ grid-template-rows: 35% 1fr;
+ }
+
+ &__empty-text {
+ grid-row-start: 2;
+ display: flex;
+ justify-content: center;
+ color: $silver;
+ }
+}
diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/transaction-list/transaction-list.component.js
new file mode 100644
index 000000000..c864fea3b
--- /dev/null
+++ b/ui/app/components/transaction-list/transaction-list.component.js
@@ -0,0 +1,118 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TransactionListItem from '../transaction-list-item'
+import ShapeShiftTransactionListItem from '../shift-list-item'
+import { TRANSACTION_TYPE_SHAPESHIFT } from '../../constants/transactions'
+
+export default class TransactionList extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ pendingTransactions: [],
+ completedTransactions: [],
+ transactionToRetry: {},
+ }
+
+ static propTypes = {
+ pendingTransactions: PropTypes.array,
+ completedTransactions: PropTypes.array,
+ transactionToRetry: PropTypes.object,
+ selectedToken: PropTypes.object,
+ updateNetworkNonce: PropTypes.func,
+ assetImages: PropTypes.object,
+ }
+
+ componentDidMount () {
+ this.props.updateNetworkNonce()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { pendingTransactions: prevPendingTransactions = [] } = prevProps
+ const { pendingTransactions = [], updateNetworkNonce } = this.props
+
+ if (pendingTransactions.length > prevPendingTransactions.length) {
+ updateNetworkNonce()
+ }
+ }
+
+ shouldShowRetry = transaction => {
+ const { transactionToRetry } = this.props
+ const { id, submittedTime } = transaction
+ return id === transactionToRetry.id && Date.now() - submittedTime > 30000
+ }
+
+ renderTransactions () {
+ const { t } = this.context
+ const { pendingTransactions = [], completedTransactions = [] } = this.props
+ return (
+ <div className="transaction-list__transactions">
+ {
+ pendingTransactions.length > 0 && (
+ <div className="transaction-list__pending-transactions">
+ <div className="transaction-list__header">
+ { `${t('queue')} (${pendingTransactions.length})` }
+ </div>
+ {
+ pendingTransactions.map((transaction, index) => (
+ this.renderTransaction(transaction, index)
+ ))
+ }
+ </div>
+ )
+ }
+ <div className="transaction-list__completed-transactions">
+ <div className="transaction-list__header">
+ { t('history') }
+ </div>
+ {
+ completedTransactions.length > 0
+ ? completedTransactions.map((transaction, index) => (
+ this.renderTransaction(transaction, index)
+ ))
+ : this.renderEmpty()
+ }
+ </div>
+ </div>
+ )
+ }
+
+ renderTransaction (transaction, index) {
+ const { selectedToken, assetImages } = this.props
+
+ return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
+ ? (
+ <ShapeShiftTransactionListItem
+ { ...transaction }
+ key={`shapeshift${index}`}
+ />
+ ) : (
+ <TransactionListItem
+ transaction={transaction}
+ key={transaction.id}
+ showRetry={this.shouldShowRetry(transaction)}
+ token={selectedToken}
+ assetImages={assetImages}
+ />
+ )
+ }
+
+ renderEmpty () {
+ return (
+ <div className="transaction-list__empty">
+ <div className="transaction-list__empty-text">
+ { this.context.t('noTransactions') }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ return (
+ <div className="transaction-list">
+ { this.renderTransactions() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-list/transaction-list.container.js b/ui/app/components/transaction-list/transaction-list.container.js
new file mode 100644
index 000000000..2e946c67d
--- /dev/null
+++ b/ui/app/components/transaction-list/transaction-list.container.js
@@ -0,0 +1,51 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import TransactionList from './transaction-list.component'
+import {
+ pendingTransactionsSelector,
+ submittedPendingTransactionsSelector,
+ completedTransactionsSelector,
+} from '../../selectors/transactions'
+import { getSelectedAddress, getAssetImages } from '../../selectors'
+import { selectedTokenSelector } from '../../selectors/tokens'
+import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util'
+import { updateNetworkNonce } from '../../actions'
+
+const mapStateToProps = state => {
+ const pendingTransactions = pendingTransactionsSelector(state)
+ const submittedPendingTransactions = submittedPendingTransactionsSelector(state)
+ const networkNonce = state.appState.networkNonce
+
+ return {
+ completedTransactions: completedTransactionsSelector(state),
+ pendingTransactions,
+ transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce),
+ selectedToken: selectedTokenSelector(state),
+ selectedAddress: getSelectedAddress(state),
+ assetImages: getAssetImages(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ updateNetworkNonce: address => dispatch(updateNetworkNonce(address)),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { selectedAddress, ...restStateProps } = stateProps
+ const { updateNetworkNonce, ...restDispatchProps } = dispatchProps
+
+ return {
+ ...restStateProps,
+ ...restDispatchProps,
+ ...ownProps,
+ updateNetworkNonce: () => updateNetworkNonce(selectedAddress),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(TransactionList)
diff --git a/ui/app/components/transaction-status/index.js b/ui/app/components/transaction-status/index.js
new file mode 100644
index 000000000..dece41e9c
--- /dev/null
+++ b/ui/app/components/transaction-status/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-status.component'
diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss
new file mode 100644
index 000000000..35be550f7
--- /dev/null
+++ b/ui/app/components/transaction-status/index.scss
@@ -0,0 +1,28 @@
+.transaction-status {
+ height: 26px;
+ width: 81px;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ color: #5e6064;
+ font-size: .625rem;
+ text-transform: uppercase;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ height: 16px;
+ width: 70px;
+ font-size: .5rem;
+ }
+
+ &--confirmed {
+ background-color: #eafad7;
+ color: #609a1c;
+ }
+
+ &--approved, &--submitted {
+ background-color: #FFF2DB;
+ color: #CA810A;
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/transaction-status/transaction-status.component.js b/ui/app/components/transaction-status/transaction-status.component.js
new file mode 100644
index 000000000..a4c827ae8
--- /dev/null
+++ b/ui/app/components/transaction-status/transaction-status.component.js
@@ -0,0 +1,51 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import {
+ UNAPPROVED_STATUS,
+ REJECTED_STATUS,
+ APPROVED_STATUS,
+ SIGNED_STATUS,
+ SUBMITTED_STATUS,
+ CONFIRMED_STATUS,
+ FAILED_STATUS,
+ DROPPED_STATUS,
+} from '../../constants/transactions'
+
+const statusToClassNameHash = {
+ [UNAPPROVED_STATUS]: 'transaction-status--unapproved',
+ [REJECTED_STATUS]: 'transaction-status--rejected',
+ [APPROVED_STATUS]: 'transaction-status--approved',
+ [SIGNED_STATUS]: 'transaction-status--signed',
+ [SUBMITTED_STATUS]: 'transaction-status--submitted',
+ [CONFIRMED_STATUS]: 'transaction-status--confirmed',
+ [FAILED_STATUS]: 'transaction-status--failed',
+ [DROPPED_STATUS]: 'transaction-status--dropped',
+}
+
+const statusToTextHash = {
+ [APPROVED_STATUS]: 'pending',
+ [SUBMITTED_STATUS]: 'pending',
+}
+
+export default class TransactionStatus extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ statusKey: PropTypes.string,
+ className: PropTypes.string,
+ }
+
+ render () {
+ const { className, statusKey } = this.props
+ const statusText = this.context.t(statusToTextHash[statusKey] || statusKey)
+
+ return (
+ <div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
+ { statusText }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-view-balance/index.js b/ui/app/components/transaction-view-balance/index.js
new file mode 100644
index 000000000..8824737f7
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-view-balance.container'
diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss
new file mode 100644
index 000000000..12045ab6d
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/index.scss
@@ -0,0 +1,76 @@
+.transaction-view-balance {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex: 1;
+ height: 54px;
+
+ &__balance {
+ margin-left: 12px;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: $break-small) {
+ align-items: center;
+ margin: 16px 0;
+ }
+ }
+
+ &__token-balance {
+ margin-left: 12px;
+ font-size: 1.5rem;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 12px;
+ font-size: 1.75rem;
+ }
+ }
+
+ &__primary-balance {
+ font-size: 1.5rem;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 12px;
+ font-size: 1.75rem;
+ }
+ }
+
+ &__secondary-balance {
+ font-size: 1.15rem;
+ color: #a0a0a0;
+ }
+
+ &__balance-container {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ flex-direction: row;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ }
+ }
+
+ &__button {
+ min-width: initial;
+ width: 100px;
+
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ height: initial
+ }
+}
diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
new file mode 100644
index 000000000..bb95cb27e
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import TokenBalance from '../../token-balance'
+import CurrencyDisplay from '../../currency-display'
+import { SEND_ROUTE } from '../../../routes'
+import TransactionViewBalance from '../transaction-view-balance.component'
+
+const propsMethodSpies = {
+ showDepositModal: sinon.spy(),
+}
+
+const historySpies = {
+ push: sinon.spy(),
+}
+
+const t = (str1, str2) => str2 ? str1 + str2 : str1
+
+describe('TransactionViewBalance Component', () => {
+ afterEach(() => {
+ propsMethodSpies.showDepositModal.resetHistory()
+ historySpies.push.resetHistory()
+ })
+
+ it('should render ETH balance properly', () => {
+ const wrapper = shallow(<TransactionViewBalance
+ showDepositModal={propsMethodSpies.showDepositModal}
+ history={historySpies}
+ network="3"
+ ethBalance={123}
+ fiatBalance={456}
+ currentCurrency="usd"
+ />, { context: { t } })
+
+ assert.equal(wrapper.find('.transaction-view-balance').length, 1)
+ assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
+ assert.equal(wrapper.find(CurrencyDisplay).length, 2)
+
+ const buttons = wrapper.find('.transaction-view-balance__buttons')
+ assert.equal(propsMethodSpies.showDepositModal.callCount, 0)
+ buttons.childAt(0).simulate('click')
+ assert.equal(propsMethodSpies.showDepositModal.callCount, 1)
+ assert.equal(historySpies.push.callCount, 0)
+ buttons.childAt(1).simulate('click')
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], SEND_ROUTE)
+ })
+
+ it('should render token balance properly', () => {
+ const token = {
+ address: '0x35865238f0bec9d5ce6abff0fdaebe7b853dfcc5',
+ decimals: '2',
+ symbol: 'ABC',
+ }
+
+ const wrapper = shallow(<TransactionViewBalance
+ showDepositModal={propsMethodSpies.showDepositModal}
+ history={historySpies}
+ network="3"
+ ethBalance={123}
+ fiatBalance={456}
+ currentCurrency="usd"
+ selectedToken={token}
+ />, { context: { t } })
+
+ assert.equal(wrapper.find('.transaction-view-balance').length, 1)
+ assert.equal(wrapper.find('.transaction-view-balance__button').length, 1)
+ assert.equal(wrapper.find(TokenBalance).length, 1)
+ })
+})
diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
new file mode 100644
index 000000000..1b7a29c87
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js
@@ -0,0 +1,96 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../button'
+import Identicon from '../identicon'
+import TokenBalance from '../token-balance'
+import CurrencyDisplay from '../currency-display'
+import { SEND_ROUTE } from '../../routes'
+import { ETH } from '../../constants/common'
+
+export default class TransactionViewBalance extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ showDepositModal: PropTypes.func,
+ selectedToken: PropTypes.object,
+ history: PropTypes.object,
+ network: PropTypes.string,
+ balance: PropTypes.string,
+ assetImage: PropTypes.string,
+ }
+
+ renderBalance () {
+ const { selectedToken, balance } = this.props
+
+ return selectedToken
+ ? (
+ <TokenBalance
+ token={selectedToken}
+ withSymbol
+ className="transaction-view-balance__token-balance"
+ />
+ ) : (
+ <div className="transaction-view-balance__balance">
+ <CurrencyDisplay
+ className="transaction-view-balance__primary-balance"
+ value={balance}
+ currency={ETH}
+ numberOfDecimals={3}
+ />
+ <CurrencyDisplay
+ className="transaction-view-balance__secondary-balance"
+ value={balance}
+ />
+ </div>
+ )
+ }
+
+ renderButtons () {
+ const { t } = this.context
+ const { selectedToken, showDepositModal, history } = this.props
+
+ return (
+ <div className="transaction-view-balance__buttons">
+ {
+ !selectedToken && (
+ <Button
+ type="primary"
+ className="transaction-view-balance__button"
+ onClick={() => showDepositModal()}
+ >
+ { t('deposit') }
+ </Button>
+ )
+ }
+ <Button
+ type="primary"
+ className="transaction-view-balance__button"
+ onClick={() => history.push(SEND_ROUTE)}
+ >
+ { t('send') }
+ </Button>
+ </div>
+ )
+ }
+
+ render () {
+ const { network, selectedToken, assetImage } = this.props
+
+ return (
+ <div className="transaction-view-balance">
+ <div className="transaction-view-balance__balance-container">
+ <Identicon
+ diameter={50}
+ address={selectedToken && selectedToken.address}
+ network={network}
+ image={assetImage}
+ />
+ { this.renderBalance() }
+ </div>
+ { this.renderButtons() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
new file mode 100644
index 000000000..30c5cab16
--- /dev/null
+++ b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import TransactionViewBalance from './transaction-view-balance.component'
+import { getSelectedToken, getSelectedAddress, getSelectedTokenAssetImage } from '../../selectors'
+import { showModal } from '../../actions'
+
+const mapStateToProps = state => {
+ const selectedAddress = getSelectedAddress(state)
+ const { metamask: { network, accounts } } = state
+ const account = accounts[selectedAddress]
+ const { balance } = account
+
+ return {
+ selectedToken: getSelectedToken(state),
+ network,
+ balance,
+ assetImage: getSelectedTokenAssetImage(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(TransactionViewBalance)
diff --git a/ui/app/components/transaction-view/index.js b/ui/app/components/transaction-view/index.js
new file mode 100644
index 000000000..9eb0c3c83
--- /dev/null
+++ b/ui/app/components/transaction-view/index.js
@@ -0,0 +1 @@
+export { default } from './transaction-view.component'
diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/transaction-view/index.scss
new file mode 100644
index 000000000..af9771ce0
--- /dev/null
+++ b/ui/app/components/transaction-view/index.scss
@@ -0,0 +1,27 @@
+.transaction-view {
+ flex: 1 1 66.5%;
+ background: $white;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+
+ &__balance-wrapper {
+ @media screen and (max-width: $break-small) {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ flex: 0 0 auto;
+ padding-top: 16px;
+ }
+
+ @media screen and (min-width: $break-large) {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ margin: 2.3em 2.37em .8em;
+ flex: 0 0 auto;
+ }
+ }
+}
diff --git a/ui/app/components/transaction-view/transaction-view.component.js b/ui/app/components/transaction-view/transaction-view.component.js
new file mode 100644
index 000000000..7014ca173
--- /dev/null
+++ b/ui/app/components/transaction-view/transaction-view.component.js
@@ -0,0 +1,27 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Media from 'react-media'
+import MenuBar from '../menu-bar'
+import TransactionViewBalance from '../transaction-view-balance'
+import TransactionList from '../transaction-list'
+
+export default class TransactionView extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ return (
+ <div className="transaction-view">
+ <Media
+ query="(max-width: 575px)"
+ render={() => <MenuBar />}
+ />
+ <div className="transaction-view__balance-wrapper">
+ <TransactionViewBalance />
+ </div>
+ <TransactionList />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js
deleted file mode 100644
index 474d62638..000000000
--- a/ui/app/components/tx-list-item.js
+++ /dev/null
@@ -1,356 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const { compose } = require('recompose')
-const { withRouter } = require('react-router-dom')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const inherits = require('util').inherits
-const classnames = require('classnames')
-const abi = require('human-standard-token-abi')
-const abiDecoder = require('abi-decoder')
-abiDecoder.addABI(abi)
-const Identicon = require('./identicon')
-const contractMap = require('eth-contract-metadata')
-const { checksumAddress } = require('../util')
-
-const actions = require('../actions')
-const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
-const { calcTokenAmount } = require('../token-util')
-
-const { getCurrentCurrency } = require('../selectors')
-const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
-
-TxListItem.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TxListItem)
-
-function mapStateToProps (state) {
- return {
- tokens: state.metamask.tokens,
- currentCurrency: getCurrentCurrency(state),
- contractExchangeRates: state.metamask.contractExchangeRates,
- selectedAddressTxList: state.metamask.selectedAddressTxList,
- networkNonce: state.appState.networkNonce,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- setSelectedToken: tokenAddress => dispatch(actions.setSelectedToken(tokenAddress)),
- retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)),
- }
-}
-
-inherits(TxListItem, Component)
-function TxListItem () {
- Component.call(this)
-
- this.state = {
- total: null,
- fiatTotal: null,
- isTokenTx: null,
- }
-
- this.unmounted = false
-}
-
-TxListItem.prototype.componentDidMount = async function () {
- const { txParams = {} } = this.props
-
- const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { name: txDataName } = decodedData || {}
- const isTokenTx = txDataName === 'transfer'
-
- const { total, fiatTotal } = isTokenTx
- ? await this.getSendTokenTotal()
- : this.getSendEtherTotal()
-
- if (this.unmounted) {
- return
- }
- this.setState({ total, fiatTotal, isTokenTx })
-}
-
-TxListItem.prototype.componentWillUnmount = function () {
- this.unmounted = true
-}
-
-TxListItem.prototype.getAddressText = function () {
- const {
- address,
- txParams = {},
- isMsg,
- } = this.props
-
- const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { name: txDataName, params = [] } = decodedData || {}
- const { value } = params[0] || {}
- const checksummedAddress = checksumAddress(address)
- const checksummedValue = checksumAddress(value)
-
- let addressText
- if (txDataName === 'transfer' || address) {
- const addressToRender = txDataName === 'transfer' ? checksummedValue : checksummedAddress
- addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}`
- } else if (isMsg) {
- addressText = this.context.t('sigRequest')
- } else {
- addressText = this.context.t('contractDeployment')
- }
-
- return addressText
-}
-
-TxListItem.prototype.getSendEtherTotal = function () {
- const {
- transactionAmount,
- conversionRate,
- address,
- currentCurrency,
- } = this.props
-
- if (!address) {
- return {}
- }
-
- const totalInFiat = conversionUtil(transactionAmount, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- fromDenomination: 'WEI',
- numberOfDecimals: 2,
- conversionRate,
- })
- const totalInETH = conversionUtil(transactionAmount, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- fromDenomination: 'WEI',
- conversionRate,
- numberOfDecimals: 6,
- })
-
- return {
- total: `${totalInETH} ETH`,
- fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`,
- }
-}
-
-TxListItem.prototype.getTokenInfo = async function () {
- const { txParams = {}, tokenInfoGetter, tokens } = this.props
- const toAddress = txParams.to
-
- let decimals
- let symbol
-
- ({ decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {})
-
- if (!decimals && !symbol) {
- ({ decimals, symbol } = contractMap[toAddress] || {})
- }
-
- if (!decimals && !symbol) {
- ({ decimals, symbol } = await tokenInfoGetter(toAddress))
- }
-
- return { decimals, symbol, address: toAddress }
-}
-
-TxListItem.prototype.getSendTokenTotal = async function () {
- const {
- txParams = {},
- conversionRate,
- contractExchangeRates,
- currentCurrency,
- } = this.props
-
- const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { params = [] } = decodedData || {}
- const { value } = params[1] || {}
- const { decimals, symbol, address } = await this.getTokenInfo()
- const total = calcTokenAmount(value, decimals)
-
- let tokenToFiatRate
- let totalInFiat
-
- if (contractExchangeRates[address]) {
- tokenToFiatRate = multiplyCurrencies(
- contractExchangeRates[address],
- conversionRate
- )
-
- totalInFiat = conversionUtil(total, {
- fromNumericBase: 'dec',
- toNumericBase: 'dec',
- fromCurrency: symbol,
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate: tokenToFiatRate,
- })
- }
-
- const showFiat = Boolean(totalInFiat) && currentCurrency.toUpperCase() !== symbol
-
- return {
- total: `${total} ${symbol}`,
- fiatTotal: showFiat && `${totalInFiat} ${currentCurrency.toUpperCase()}`,
- }
-}
-
-TxListItem.prototype.showRetryButton = function () {
- const {
- transactionSubmittedTime,
- selectedAddressTxList,
- transactionId,
- txParams,
- networkNonce,
- } = this.props
- if (!txParams) {
- return false
- }
- let currentTxSharesEarliestNonce = false
- const currentNonce = txParams.nonce
- const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
- const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
- const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted')
- const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
- const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
- lastSubmittedTxWithCurrentNonce.id === transactionId
- if (currentSubmittedTxs.length > 0) {
- currentTxSharesEarliestNonce = currentNonce === networkNonce
- }
-
- return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
-}
-
-TxListItem.prototype.setSelectedToken = function (tokenAddress) {
- this.props.setSelectedToken(tokenAddress)
-}
-
-TxListItem.prototype.resubmit = function () {
- const { transactionId } = this.props
- this.props.retryTransaction(transactionId)
- .then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
-}
-
-TxListItem.prototype.render = function () {
- const {
- transactionStatus,
- onClick,
- transactionId,
- dateString,
- address,
- className,
- txParams,
- } = this.props
- const { total, fiatTotal, isTokenTx } = this.state
-
- return h(`div${className || ''}`, {
- key: transactionId,
- onClick: () => onClick && onClick(transactionId),
- }, [
- h(`div.flex-column.tx-list-item-wrapper`, {}, [
-
- h('div.tx-list-date-wrapper', {
- style: {},
- }, [
- h('span.tx-list-date', {}, [
- dateString,
- ]),
- ]),
-
- h('div.flex-row.tx-list-content-wrapper', {
- style: {},
- }, [
-
- h('div.tx-list-identicon-wrapper', {
- style: {},
- }, [
- h(Identicon, {
- address,
- diameter: 28,
- }),
- ]),
-
- h('div.tx-list-account-and-status-wrapper', {}, [
- h('div.tx-list-account-wrapper', {
- style: {},
- }, [
- h('span.tx-list-account', {}, [
- this.getAddressText(address),
- ]),
- ]),
-
- h('div.tx-list-status-wrapper', {
- style: {},
- }, [
- h('span', {
- className: classnames('tx-list-status', {
- 'tx-list-status--rejected': transactionStatus === 'rejected',
- 'tx-list-status--failed': transactionStatus === 'failed',
- 'tx-list-status--dropped': transactionStatus === 'dropped',
- }),
- },
- this.txStatusIndicator(),
- ),
- ]),
- ]),
-
- h('div.flex-column.tx-list-details-wrapper', {
- style: {},
- }, [
-
- h('span.tx-list-value', total),
-
- fiatTotal && h('span.tx-list-fiat-value', fiatTotal),
-
- ]),
- ]),
-
- this.showRetryButton() && h('.tx-list-item-retry-container', {
- onClick: (event) => {
- event.stopPropagation()
- if (isTokenTx) {
- this.setSelectedToken(txParams.to)
- }
- this.resubmit()
- },
- }, [
- h('span', 'Taking too long? Increase the gas price on your transaction'),
- ]),
-
- ]), // holding on icon from design
- ])
-}
-
-TxListItem.prototype.txStatusIndicator = function () {
- const { transactionStatus } = this.props
-
- let name
-
- if (transactionStatus === 'unapproved') {
- name = this.context.t('unapproved')
- } else if (transactionStatus === 'rejected') {
- name = this.context.t('rejected')
- } else if (transactionStatus === 'approved') {
- name = this.context.t('approved')
- } else if (transactionStatus === 'signed') {
- name = this.context.t('signed')
- } else if (transactionStatus === 'submitted') {
- name = this.context.t('submitted')
- } else if (transactionStatus === 'confirmed') {
- name = this.context.t('confirmed')
- } else if (transactionStatus === 'failed') {
- name = this.context.t('failed')
- } else if (transactionStatus === 'dropped') {
- name = this.context.t('dropped')
- }
- return name
-}
diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js
deleted file mode 100644
index d8c4a9d19..000000000
--- a/ui/app/components/tx-list.js
+++ /dev/null
@@ -1,171 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
-const selectors = require('../selectors')
-const TxListItem = require('./tx-list-item')
-const ShiftListItem = require('./shift-list-item')
-const { formatDate } = require('../util')
-const { showConfTxPage, updateNetworkNonce } = require('../actions')
-const classnames = require('classnames')
-const { tokenInfoGetter } = require('../token-util')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TxList)
-
-TxList.contextTypes = {
- t: PropTypes.func,
-}
-
-function mapStateToProps (state) {
- return {
- txsToRender: selectors.transactionsSelector(state),
- conversionRate: selectors.conversionRateSelector(state),
- selectedAddress: selectors.getSelectedAddress(state),
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
- updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
- }
-}
-
-inherits(TxList, Component)
-function TxList () {
- Component.call(this)
-}
-
-TxList.prototype.componentWillMount = function () {
- this.tokenInfoGetter = tokenInfoGetter()
- this.props.updateNetworkNonce(this.props.selectedAddress)
-}
-
-TxList.prototype.componentDidUpdate = function (prevProps) {
- const oldTxsToRender = prevProps.txsToRender
- const {
- txsToRender: newTxsToRender,
- selectedAddress,
- updateNetworkNonce,
- } = this.props
-
- if (newTxsToRender.length > oldTxsToRender.length) {
- updateNetworkNonce(selectedAddress)
- }
-}
-
-TxList.prototype.render = function () {
- return h('div.flex-column', [
- h('div.flex-row.tx-list-header-wrapper', [
- h('div.flex-row.tx-list-header', [
- h('div', this.context.t('transactions')),
- ]),
- ]),
- h('div.flex-column.tx-list-container', {}, [
- this.renderTransaction(),
- ]),
- ])
-}
-
-TxList.prototype.renderTransaction = function () {
- const { txsToRender, conversionRate } = this.props
-
- return txsToRender.length
- ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate, i))
- : [h(
- 'div.tx-list-item.tx-list-item--empty',
- { key: 'tx-list-none' },
- [ this.context.t('noTransactions') ],
- )]
-}
-
-// TODO: Consider moving TxListItem into a separate component
-TxList.prototype.renderTransactionListItem = function (transaction, conversionRate, index) {
- // console.log({transaction})
- // refer to transaction-list.js:line 58
-
- if (transaction.key === 'shapeshift') {
- return h(ShiftListItem, { ...transaction, key: `shapeshift${index}` })
- }
-
- const props = {
- dateString: formatDate(transaction.time),
- address: transaction.txParams && transaction.txParams.to,
- transactionStatus: transaction.status,
- transactionAmount: transaction.txParams && transaction.txParams.value,
- transactionId: transaction.id,
- transactionHash: transaction.hash,
- transactionNetworkId: transaction.metamaskNetworkId,
- transactionSubmittedTime: transaction.submittedTime,
- }
-
- const {
- address,
- transactionStatus,
- transactionAmount,
- dateString,
- transactionId,
- transactionHash,
- transactionNetworkId,
- transactionSubmittedTime,
- } = props
- const { history } = this.props
-
- const opts = {
- key: transactionId || transactionHash,
- txParams: transaction.txParams,
- isMsg: Boolean(transaction.msgParams),
- transactionStatus,
- transactionId,
- dateString,
- address,
- transactionAmount,
- transactionHash,
- conversionRate,
- tokenInfoGetter: this.tokenInfoGetter,
- transactionSubmittedTime,
- }
-
- const isUnapproved = transactionStatus === 'unapproved'
-
- if (isUnapproved) {
- opts.onClick = () => {
- this.props.showConfTxPage({ id: transactionId })
- history.push(CONFIRM_TRANSACTION_ROUTE)
- }
- opts.transactionStatus = this.context.t('notStarted')
- } else if (transactionHash) {
- opts.onClick = () => this.view(transactionHash, transactionNetworkId)
- }
-
- opts.className = classnames('.tx-list-item', {
- '.tx-list-pending-item-container': isUnapproved,
- '.tx-list-clickable': Boolean(transactionHash) || isUnapproved,
- })
-
- return h(TxListItem, opts)
-}
-
-TxList.prototype.view = function (txHash, network) {
- const url = etherscanLinkFor(txHash, network)
- if (url) {
- navigateTo(url)
- }
-}
-
-function navigateTo (url) {
- global.platform.openWindow({ url })
-}
-
-function etherscanLinkFor (txHash, network) {
- const prefix = prefixForNetwork(network)
- return `https://${prefix}etherscan.io/tx/${txHash}`
-}
diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js
deleted file mode 100644
index 654090da6..000000000
--- a/ui/app/components/tx-view.js
+++ /dev/null
@@ -1,156 +0,0 @@
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const actions = require('../actions')
-const selectors = require('../selectors')
-const { SEND_ROUTE } = require('../routes')
-const { checksumAddress: toChecksumAddress } = require('../util')
-
-const BalanceComponent = require('./balance-component')
-const Tooltip = require('./tooltip')
-const TxList = require('./tx-list')
-const SelectedAccount = require('./selected-account')
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(TxView)
-
-TxView.contextTypes = {
- t: PropTypes.func,
-}
-
-function mapStateToProps (state) {
- const sidebarOpen = state.appState.sidebarOpen
- const isMascara = state.appState.isMascara
-
- const identities = state.metamask.identities
- const accounts = state.metamask.accounts
- const network = state.metamask.network
- const selectedTokenAddress = state.metamask.selectedTokenAddress
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- const checksumAddress = toChecksumAddress(selectedAddress)
- const identity = identities[selectedAddress]
-
- return {
- sidebarOpen,
- selectedAddress,
- checksumAddress,
- selectedTokenAddress,
- selectedToken: selectors.getSelectedToken(state),
- identity,
- network,
- isMascara,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- showSidebar: () => { dispatch(actions.showSidebar()) },
- hideSidebar: () => { dispatch(actions.hideSidebar()) },
- showModal: (payload) => { dispatch(actions.showModal(payload)) },
- showSendPage: () => { dispatch(actions.showSendPage()) },
- showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) },
- }
-}
-
-inherits(TxView, Component)
-function TxView () {
- Component.call(this)
-}
-
-TxView.prototype.renderHeroBalance = function () {
- const { selectedToken } = this.props
-
- return h('div.hero-balance', {}, [
-
- h(BalanceComponent, { token: selectedToken }),
-
- this.renderButtons(),
- ])
-}
-
-TxView.prototype.renderButtons = function () {
- const {selectedToken, showModal, history } = this.props
-
- return !selectedToken
- ? (
- h('div.flex-row.flex-center.hero-balance-buttons', [
- h('button.btn-primary.hero-balance-button', {
- onClick: () => showModal({
- name: 'DEPOSIT_ETHER',
- }),
- }, this.context.t('deposit')),
-
- h('button.btn-primary.hero-balance-button', {
- style: {
- marginLeft: '0.8em',
- },
- onClick: () => history.push(SEND_ROUTE),
- }, this.context.t('send')),
- ])
- )
- : (
- h('div.flex-row.flex-center.hero-balance-buttons', [
- h('button.btn-primary.hero-balance-button', {
- onClick: () => history.push(SEND_ROUTE),
- }, this.context.t('send')),
- ])
- )
-}
-
-TxView.prototype.render = function () {
- const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props
- const { t } = this.context
-
- return h('div.tx-view.flex-column', {
- style: {},
- }, [
-
- h('div.flex-row.phone-visible', {
- style: {
- justifyContent: 'center',
- alignItems: 'center',
- flex: '0 0 auto',
- marginBottom: '16px',
- padding: '5px',
- borderBottom: '1px solid #e5e5e5',
- },
- }, [
-
- h(Tooltip, {
- title: t('menu'),
- position: 'bottom',
- }, [
- h('div.fa.fa-bars', {
- style: {
- fontSize: '1.3em',
- cursor: 'pointer',
- padding: '10px',
- },
- onClick: () => sidebarOpen ? hideSidebar() : showSidebar(),
- }),
- ]),
-
- h(SelectedAccount),
-
- !isMascara && h(Tooltip, {
- title: t('openInTab'),
- position: 'bottom',
- }, [
- h('div.open-in-browser', {
- onClick: () => global.platform.openExtensionInBrowser(),
- }, [h('img', { src: 'images/popout.svg' })]),
- ]),
- ]),
-
- this.renderHeroBalance(),
-
- h(TxList),
-
- ])
-}
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index 8e092364c..6de265110 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -26,11 +26,15 @@ WalletView.contextTypes = {
t: PropTypes.func,
}
+WalletView.defaultProps = {
+ responsiveDisplayClassname: '',
+}
+
function mapStateToProps (state) {
return {
network: state.metamask.network,
- sidebarOpen: state.appState.sidebarOpen,
+ sidebarOpen: state.appState.sidebar.isOpen,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
tokens: state.metamask.tokens,
@@ -131,8 +135,9 @@ WalletView.prototype.render = function () {
}
}
- return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
+ return h('div.wallet-view.flex-column', {
style: {},
+ className: responsiveDisplayClassname,
}, [
// TODO: Separate component: wallet account details
diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js
new file mode 100644
index 000000000..28731ce33
--- /dev/null
+++ b/ui/app/constants/common.js
@@ -0,0 +1 @@
+export const ETH = 'ETH'
diff --git a/ui/app/constants/transactions.js b/ui/app/constants/transactions.js
new file mode 100644
index 000000000..df6c4c8a4
--- /dev/null
+++ b/ui/app/constants/transactions.js
@@ -0,0 +1,22 @@
+export const UNAPPROVED_STATUS = 'unapproved'
+export const REJECTED_STATUS = 'rejected'
+export const APPROVED_STATUS = 'approved'
+export const SIGNED_STATUS = 'signed'
+export const SUBMITTED_STATUS = 'submitted'
+export const CONFIRMED_STATUS = 'confirmed'
+export const FAILED_STATUS = 'failed'
+export const DROPPED_STATUS = 'dropped'
+
+export const TOKEN_METHOD_TRANSFER = 'transfer'
+export const TOKEN_METHOD_APPROVE = 'approve'
+export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
+
+export const SEND_ETHER_ACTION_KEY = 'sentEther'
+export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
+export const APPROVE_ACTION_KEY = 'approve'
+export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
+export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
+export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
+export const UNKNOWN_FUNCTION_KEY = 'unknownFunction'
+
+export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'
diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss
deleted file mode 100644
index eba93ecb4..000000000
--- a/ui/app/css/itcss/components/hero-balance.scss
+++ /dev/null
@@ -1,130 +0,0 @@
-.hero-balance {
-
- @media screen and (max-width: $break-small) {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: center;
- flex: 0 0 auto;
- padding-top: 16px;
- }
-
- @media screen and (min-width: $break-large) {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- margin: 2.3em 2.37em .8em;
- flex: 0 0 auto;
- }
-
- .balance-container {
- display: flex;
- margin: 0;
- justify-content: flex-start;
- align-items: center;
-
- @media screen and (max-width: $break-small) {
- flex-direction: column;
- flex: 0 0 auto;
- max-width: 100%;
- }
-
- @media screen and (min-width: $break-large) {
- flex-direction: row;
- flex-grow: 3;
- min-width: 0;
- }
- }
-
- .balance-display {
- .token-amount {
- color: $black;
- max-width: 100%;
-
- .token-balance {
- display: flex;
- }
- }
-
- @media screen and (max-width: $break-small) {
- max-width: 100%;
- text-align: center;
-
- .token-amount {
- font-size: 1.75rem;
- margin-top: 1rem;
-
- .token-balance {
- flex-direction: column;
- }
- }
-
- .fiat-amount {
- font-size: 115%;
- margin-top: 8.5%;
- color: #a0a0a0;
- }
- }
-
- @media screen and (min-width: $break-large) {
- margin: 0 .8em;
- justify-content: flex-start;
- align-items: flex-start;
- min-width: 0;
-
- .token-amount {
- font-size: 1.5rem;
- }
-
- .fiat-amount {
- margin-top: .25%;
- font-size: 105%;
- }
- }
-
- @media #{$sub-mid-size-breakpoint-range} {
- margin-left: .4em;
- margin-right: .4em;
- justify-content: flex-start;
- align-items: flex-start;
-
- .token-amount {
- font-size: 1rem;
- }
-
- .fiat-amount {
- margin-top: .25%;
- font-size: 1rem;
- }
- }
- }
-
- .hero-balance-buttons {
-
- @media screen and (max-width: $break-small) {
- width: 100%;
- // height: 100px; // needed a round number to set the heights of the buttons inside
- flex: 0 0 auto;
- padding: 16px 0;
- }
-
- @media screen and (min-width: $break-large) {
- flex-grow: 2;
- justify-content: flex-end;
- }
- }
-}
-
-.hero-balance-button {
- min-width: initial;
- width: 6rem;
-
- @media #{$sub-mid-size-breakpoint-range} {
- padding: .4rem;
- width: 4rem;
- display: flex;
- flex: 1;
- justify-content: center;
- }
-}
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 821a6b612..9e2008b54 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -19,8 +19,6 @@
@import './loading-overlay.scss';
// Balances
-@import './hero-balance.scss';
-
@import './wallet-balance.scss';
// Tx List and Sections
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index bbfd85c90..7eb193d6f 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -6,7 +6,6 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma
*/
// Component Colors
-$tx-view-bg: $white;
$wallet-view-bg: $alabaster;
// Main container
@@ -30,32 +29,6 @@ $wallet-view-bg: $alabaster;
min-width: 0;
}
-// tx view
-
-.tx-view {
- flex: 1 1 66.5%;
- background: $tx-view-bg;
- min-width: 0;
-
- // No title on mobile
- @media screen and (max-width: 575px) {
- .identicon-wrapper {
- display: none;
- }
-
- .account-name {
- display: none;
- }
- }
-}
-
-.open-in-browser {
- cursor: pointer;
- display: flex;
- justify-content: center;
- padding: 10px;
-}
-
// wallet view and sidebar
.wallet-view {
@@ -175,7 +148,7 @@ $wallet-view-bg: $alabaster;
}
}
-.wallet-view.sidebar {
+.wallet-view.sidebar-right {
flex: 1 0 230px;
background: rgb(250, 250, 250);
z-index: $sidebar-z-index;
@@ -193,20 +166,6 @@ $wallet-view-bg: $alabaster;
height: calc(100% - 56px);
}
-.sidebar-overlay {
- z-index: $sidebar-overlay-z-index;
- position: fixed;
- // top: 41px;
- height: 100%;
- width: 100%;
- left: 0;
- right: 0;
- bottom: 0;
- opacity: 1;
- visibility: visible;
- background-color: rgba(0, 0, 0, .3);
-}
-
// main-container media queries
@media screen and (min-width: 576px) {
diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss
index 1d45ff13b..3435353d9 100644
--- a/ui/app/css/itcss/components/transaction-list.scss
+++ b/ui/app/css/itcss/components/transaction-list.scss
@@ -243,7 +243,7 @@
}
.tx-list-item {
- border-top: 1px solid rgb(231, 231, 231);
+ border-bottom: 1px solid $geyser;
flex: 0 0 auto;
display: flex;
flex-flow: row nowrap;
diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js
index 1885e12d1..30c32f2bf 100644
--- a/ui/app/ducks/confirm-transaction.duck.js
+++ b/ui/app/ducks/confirm-transaction.duck.js
@@ -5,9 +5,7 @@ import {
} from '../selectors/confirm-transaction'
import {
- getTokenData,
- getMethodData,
- getTransactionAmount,
+ getValueFromWeiHex,
getTransactionFee,
getHexGasTotal,
addFiat,
@@ -16,6 +14,7 @@ import {
hexGreaterThan,
} from '../helpers/confirm-transaction/util'
+import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util'
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
@@ -35,8 +34,9 @@ const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL')
const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
const UPDATE_NONCE = createActionType('UPDATE_NONCE')
-const FETCH_METHOD_DATA_START = createActionType('FETCH_METHOD_DATA_START')
-const FETCH_METHOD_DATA_END = createActionType('FETCH_METHOD_DATA_END')
+const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT')
+const FETCH_DATA_START = createActionType('FETCH_DATA_START')
+const FETCH_DATA_END = createActionType('FETCH_DATA_END')
// Initial state
const initState = {
@@ -55,7 +55,8 @@ const initState = {
ethTransactionTotal: '',
hexGasTotal: '',
nonce: '',
- fetchingMethodData: false,
+ toSmartContract: false,
+ fetchingData: false,
}
// Reducer
@@ -138,15 +139,20 @@ export default function reducer ({ confirmTransaction: confirmState = initState
...confirmState,
nonce: action.payload,
}
- case FETCH_METHOD_DATA_START:
+ case UPDATE_TO_SMART_CONTRACT:
return {
...confirmState,
- fetchingMethodData: true,
+ toSmartContract: action.payload,
}
- case FETCH_METHOD_DATA_END:
+ case FETCH_DATA_START:
return {
...confirmState,
- fetchingMethodData: false,
+ fetchingData: true,
+ }
+ case FETCH_DATA_END:
+ return {
+ ...confirmState,
+ fetchingData: false,
}
case CLEAR_CONFIRM_TRANSACTION:
return initState
@@ -237,9 +243,16 @@ export function updateNonce (nonce) {
}
}
-export function setFetchingMethodData (isFetching) {
+export function updateToSmartContract (toSmartContract) {
+ return {
+ type: UPDATE_TO_SMART_CONTRACT,
+ payload: toSmartContract,
+ }
+}
+
+export function setFetchingData (isFetching) {
return {
- type: isFetching ? FETCH_METHOD_DATA_START : FETCH_METHOD_DATA_END,
+ type: isFetching ? FETCH_DATA_START : FETCH_DATA_END,
}
}
@@ -286,10 +299,10 @@ export function updateTxDataAndCalculate (txData) {
const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
- const fiatTransactionAmount = getTransactionAmount({
+ const fiatTransactionAmount = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
- const ethTransactionAmount = getTransactionAmount({
+ const ethTransactionAmount = getValueFromWeiHex({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
})
@@ -338,19 +351,22 @@ export function setTransactionToConfirm (transactionId) {
dispatch(updateTxDataAndCalculate(txData))
const { txParams } = transaction
+ const { to } = txParams
if (txParams.data) {
const { tokens: existingTokens } = state
const { data, to: tokenAddress } = txParams
try {
- dispatch(setFetchingMethodData(true))
+ dispatch(setFetchingData(true))
const methodData = await getMethodData(data)
dispatch(updateMethodData(methodData))
- dispatch(setFetchingMethodData(false))
+ const toSmartContract = await isSmartContractAddress(to)
+ dispatch(updateToSmartContract(toSmartContract))
+ dispatch(setFetchingData(false))
} catch (error) {
dispatch(updateMethodData({}))
- dispatch(setFetchingMethodData(false))
+ dispatch(setFetchingData(false))
}
const tokenData = getTokenData(data)
diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js
index 111674e33..1bab0add0 100644
--- a/ui/app/ducks/tests/confirm-transaction.duck.test.js
+++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js
@@ -1,6 +1,7 @@
import assert from 'assert'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
+import sinon from 'sinon'
import ConfirmTransactionReducer, * as actions from '../confirm-transaction.duck.js'
@@ -20,7 +21,8 @@ const initialState = {
ethTransactionTotal: '',
hexGasTotal: '',
nonce: '',
- fetchingMethodData: false,
+ toSmartContract: false,
+ fetchingData: false,
}
const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA'
@@ -35,8 +37,9 @@ const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTI
const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL'
const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
-const FETCH_METHOD_DATA_START = 'metamask/confirm-transaction/FETCH_METHOD_DATA_START'
-const FETCH_METHOD_DATA_END = 'metamask/confirm-transaction/FETCH_METHOD_DATA_END'
+const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT'
+const FETCH_DATA_START = 'metamask/confirm-transaction/FETCH_DATA_START'
+const FETCH_DATA_END = 'metamask/confirm-transaction/FETCH_DATA_END'
const CLEAR_CONFIRM_TRANSACTION = 'metamask/confirm-transaction/CLEAR_CONFIRM_TRANSACTION'
describe('Confirm Transaction Duck', () => {
@@ -64,7 +67,8 @@ describe('Confirm Transaction Duck', () => {
ethTransactionTotal: '469.27',
hexGasTotal: '0x1319718a5000',
nonce: '0x0',
- fetchingMethodData: false,
+ toSmartContract: false,
+ fetchingData: false,
},
}
@@ -271,30 +275,43 @@ describe('Confirm Transaction Duck', () => {
)
})
- it('should set fetchingMethodData to true when receiving a FETCH_METHOD_DATA_START action', () => {
+ it('should update nonce when receiving an UPDATE_TO_SMART_CONTRACT action', () => {
assert.deepEqual(
ConfirmTransactionReducer(mockState, {
- type: FETCH_METHOD_DATA_START,
+ type: UPDATE_TO_SMART_CONTRACT,
+ payload: true,
}),
{
...mockState.confirmTransaction,
- fetchingMethodData: true,
+ toSmartContract: true,
}
)
})
- it('should set fetchingMethodData to false when receiving a FETCH_METHOD_DATA_END action', () => {
+ it('should set fetchingData to true when receiving a FETCH_DATA_START action', () => {
assert.deepEqual(
- ConfirmTransactionReducer({ confirmTransaction: { fetchingMethodData: true } }, {
- type: FETCH_METHOD_DATA_END,
+ ConfirmTransactionReducer(mockState, {
+ type: FETCH_DATA_START,
}),
{
- fetchingMethodData: false,
+ ...mockState.confirmTransaction,
+ fetchingData: true,
}
)
})
- it('should clear confirmTransaction when receiving a FETCH_METHOD_DATA_END action', () => {
+ it('should set fetchingData to false when receiving a FETCH_DATA_END action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({ confirmTransaction: { fetchingData: true } }, {
+ type: FETCH_DATA_END,
+ }),
+ {
+ fetchingData: false,
+ }
+ )
+ })
+
+ it('should clear confirmTransaction when receiving a FETCH_DATA_END action', () => {
assert.deepEqual(
ConfirmTransactionReducer(mockState, {
type: CLEAR_CONFIRM_TRANSACTION,
@@ -460,24 +477,24 @@ describe('Confirm Transaction Duck', () => {
)
})
- it('should create an action to set fetchingMethodData to true', () => {
+ it('should create an action to set fetchingData to true', () => {
const expectedAction = {
- type: FETCH_METHOD_DATA_START,
+ type: FETCH_DATA_START,
}
assert.deepEqual(
- actions.setFetchingMethodData(true),
+ actions.setFetchingData(true),
expectedAction
)
})
- it('should create an action to set fetchingMethodData to false', () => {
+ it('should create an action to set fetchingData to false', () => {
const expectedAction = {
- type: FETCH_METHOD_DATA_END,
+ type: FETCH_DATA_END,
}
assert.deepEqual(
- actions.setFetchingMethodData(false),
+ actions.setFetchingData(false),
expectedAction
)
})
@@ -495,6 +512,18 @@ describe('Confirm Transaction Duck', () => {
})
describe('Thunk actions', done => {
+ beforeEach(() => {
+ global.eth = {
+ getCode: sinon.stub().callsFake(
+ address => Promise.resolve(address && address.match(/isContract/) ? 'not-0x' : '0x')
+ ),
+ }
+ })
+
+ afterEach(() => {
+ global.eth.getCode.resetHistory()
+ })
+
it('updates txData and gas on an existing transaction in confirmTransaction', () => {
const mockState = {
metamask: {
@@ -505,7 +534,7 @@ describe('Confirm Transaction Duck', () => {
ethTransactionAmount: '1',
ethTransactionFee: '0.000021',
ethTransactionTotal: '1.000021',
- fetchingMethodData: false,
+ fetchingData: false,
fiatTransactionAmount: '469.26',
fiatTransactionFee: '0.01',
fiatTransactionTotal: '469.27',
@@ -581,7 +610,7 @@ describe('Confirm Transaction Duck', () => {
ethTransactionAmount: '1',
ethTransactionFee: '0.000021',
ethTransactionTotal: '1.000021',
- fetchingMethodData: false,
+ fetchingData: false,
fiatTransactionAmount: '469.26',
fiatTransactionFee: '0.01',
fiatTransactionTotal: '469.27',
@@ -667,6 +696,7 @@ describe('Confirm Transaction Duck', () => {
.then(() => {
const storeActions = store.getActions()
assert.equal(storeActions.length, expectedActions.length)
+
storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
done()
})
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
index 76e80a8ac..d1a4994e4 100644
--- a/ui/app/helpers/confirm-transaction/util.js
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -1,15 +1,8 @@
import currencyFormatter from 'currency-formatter'
import currencies from 'currency-formatter/currencies'
-import abi from 'human-standard-token-abi'
-import abiDecoder from 'abi-decoder'
import ethUtil from 'ethereumjs-util'
import BigNumber from 'bignumber.js'
-abiDecoder.addABI(abi)
-
-import MethodRegistry from 'eth-method-registry'
-const registry = new MethodRegistry({ provider: global.ethereumProvider })
-
import {
conversionUtil,
addCurrencies,
@@ -19,22 +12,6 @@ import {
import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
-export function getTokenData (data = {}) {
- return abiDecoder.decodeMethod(data)
-}
-
-export async function getMethodData (data = {}) {
- const prefixedData = ethUtil.addHexPrefix(data)
- const fourBytePrefix = prefixedData.slice(0, 10)
- const sig = await registry.lookup(fourBytePrefix)
- const parsedResult = registry.parse(sig)
-
- return {
- name: parsedResult.name,
- params: parsedResult.args,
- }
-}
-
export function increaseLastGasPrice (lastGasPrice) {
return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
@@ -76,7 +53,7 @@ export function addFiat (...args) {
})
}
-export function getTransactionAmount ({
+export function getValueFromWeiHex ({
value,
toCurrency,
conversionRate,
diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js
index a9c8fae34..4c1a3e16b 100644
--- a/ui/app/helpers/confirm-transaction/util.test.js
+++ b/ui/app/helpers/confirm-transaction/util.test.js
@@ -92,9 +92,9 @@ describe('Confirm Transaction utils', () => {
})
})
- describe('getTransactionAmount', () => {
+ describe('getValueFromWeiHex', () => {
it('should get the transaction amount in ETH', () => {
- const ethTransactionAmount = utils.getTransactionAmount({
+ const ethTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
})
@@ -102,7 +102,7 @@ describe('Confirm Transaction utils', () => {
})
it('should get the transaction amount in fiat', () => {
- const fiatTransactionAmount = utils.getTransactionAmount({
+ const fiatTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
})
diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js
new file mode 100644
index 000000000..1dec216fa
--- /dev/null
+++ b/ui/app/helpers/conversions.util.js
@@ -0,0 +1,37 @@
+import { conversionUtil } from '../conversion-util'
+
+export function hexToDecimal (hexValue) {
+ return conversionUtil(hexValue, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+}
+
+export function getEthFromWeiHex ({
+ value,
+ conversionRate,
+}) {
+ return getValueFromWeiHex({
+ value,
+ conversionRate,
+ toCurrency: 'ETH',
+ numberOfDecimals: 6,
+ })
+}
+
+export function getValueFromWeiHex ({
+ value,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency,
+ numberOfDecimals,
+ fromDenomination: 'WEI',
+ conversionRate,
+ })
+}
diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js
new file mode 100644
index 000000000..54df54aa8
--- /dev/null
+++ b/ui/app/helpers/transactions.util.js
@@ -0,0 +1,105 @@
+import ethUtil from 'ethereumjs-util'
+import MethodRegistry from 'eth-method-registry'
+import abi from 'human-standard-token-abi'
+import abiDecoder from 'abi-decoder'
+
+import {
+ TOKEN_METHOD_TRANSFER,
+ TOKEN_METHOD_APPROVE,
+ TOKEN_METHOD_TRANSFER_FROM,
+ SEND_ETHER_ACTION_KEY,
+ DEPLOY_CONTRACT_ACTION_KEY,
+ APPROVE_ACTION_KEY,
+ SEND_TOKEN_ACTION_KEY,
+ TRANSFER_FROM_ACTION_KEY,
+ SIGNATURE_REQUEST_KEY,
+ UNKNOWN_FUNCTION_KEY,
+} from '../constants/transactions'
+
+abiDecoder.addABI(abi)
+
+export function getTokenData (data = {}) {
+ return abiDecoder.decodeMethod(data)
+}
+
+const registry = new MethodRegistry({ provider: global.ethereumProvider })
+
+export async function getMethodData (data = {}) {
+ const prefixedData = ethUtil.addHexPrefix(data)
+ const fourBytePrefix = prefixedData.slice(0, 10)
+ const sig = await registry.lookup(fourBytePrefix)
+ const parsedResult = registry.parse(sig)
+
+ return {
+ name: parsedResult.name,
+ params: parsedResult.args,
+ }
+}
+
+export function isConfirmDeployContract (txData = {}) {
+ const { txParams = {} } = txData
+ return !txParams.to
+}
+
+export async function getTransactionActionKey (transaction, methodData) {
+ const { txParams: { data, to } = {}, msgParams } = transaction
+
+ if (msgParams) {
+ return SIGNATURE_REQUEST_KEY
+ }
+
+ if (isConfirmDeployContract(transaction)) {
+ return DEPLOY_CONTRACT_ACTION_KEY
+ }
+
+ if (data) {
+ const toSmartContract = await isSmartContractAddress(to)
+
+ if (!toSmartContract) {
+ return SEND_ETHER_ACTION_KEY
+ }
+
+ const { name } = methodData
+ const methodName = name && name.toLowerCase()
+
+ if (!methodName) {
+ return UNKNOWN_FUNCTION_KEY
+ }
+
+ switch (methodName) {
+ case TOKEN_METHOD_TRANSFER:
+ return SEND_TOKEN_ACTION_KEY
+ case TOKEN_METHOD_APPROVE:
+ return APPROVE_ACTION_KEY
+ case TOKEN_METHOD_TRANSFER_FROM:
+ return TRANSFER_FROM_ACTION_KEY
+ default:
+ return name
+ }
+ } else {
+ return SEND_ETHER_ACTION_KEY
+ }
+}
+
+export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') {
+ if (!transactions.length) {
+ return {}
+ }
+
+ return transactions.reduce((acc, current) => {
+ const { submittedTime, txParams: { nonce: currentNonce } = {} } = current
+
+ if (currentNonce === nonce) {
+ return acc.submittedTime
+ ? submittedTime > acc.submittedTime ? current : acc
+ : current
+ } else {
+ return acc
+ }
+ }, {})
+}
+
+export async function isSmartContractAddress (address) {
+ const code = await global.eth.getCode(address)
+ return code && code !== '0x'
+}
diff --git a/ui/app/higher-order-components/with-method-data/index.js b/ui/app/higher-order-components/with-method-data/index.js
new file mode 100644
index 000000000..f511e1ae7
--- /dev/null
+++ b/ui/app/higher-order-components/with-method-data/index.js
@@ -0,0 +1 @@
+export { default } from './with-method-data.component'
diff --git a/ui/app/higher-order-components/with-method-data/with-method-data.component.js b/ui/app/higher-order-components/with-method-data/with-method-data.component.js
new file mode 100644
index 000000000..fed7d9865
--- /dev/null
+++ b/ui/app/higher-order-components/with-method-data/with-method-data.component.js
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { getMethodData } from '../../helpers/transactions.util'
+
+export default function withMethodData (WrappedComponent) {
+ return class MethodDataWrappedComponent extends PureComponent {
+ static propTypes = {
+ transaction: PropTypes.object,
+ }
+
+ static defaultProps = {
+ transaction: {},
+ }
+
+ state = {
+ methodData: {},
+ done: false,
+ error: null,
+ }
+
+ componentDidMount () {
+ this.fetchMethodData()
+ }
+
+ async fetchMethodData () {
+ const { transaction } = this.props
+ const { txParams: { data = '' } = {} } = transaction
+
+ if (data) {
+ try {
+ const methodData = await getMethodData(data)
+ this.setState({ methodData, done: true })
+ } catch (error) {
+ this.setState({ done: true, error })
+ }
+ } else {
+ this.setState({ done: true })
+ }
+ }
+
+ render () {
+ const { methodData, done, error } = this.state
+
+ return (
+ <WrappedComponent
+ { ...this.props }
+ methodData={{ data: methodData, done, error }}
+ />
+ )
+ }
+ }
+}
diff --git a/ui/app/higher-order-components/with-token-tracker/index.js b/ui/app/higher-order-components/with-token-tracker/index.js
new file mode 100644
index 000000000..d401e81f1
--- /dev/null
+++ b/ui/app/higher-order-components/with-token-tracker/index.js
@@ -0,0 +1 @@
+export { default } from './with-token-tracker.component'
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js
index 8608b15f4..36f6a6efd 100644
--- a/ui/app/helpers/with-token-tracker.js
+++ b/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from 'eth-token-tracker'
-const withTokenTracker = WrappedComponent => {
+export default function withTokenTracker (WrappedComponent) {
return class TokenTrackerWrappedComponent extends Component {
static propTypes = {
userAddress: PropTypes.string.isRequired,
@@ -104,5 +104,3 @@ const withTokenTracker = WrappedComponent => {
}
}
}
-
-module.exports = withTokenTracker
diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js
index d46911f7c..3419474c4 100644
--- a/ui/app/i18n-provider.js
+++ b/ui/app/i18n-provider.js
@@ -6,6 +6,11 @@ const { compose } = require('recompose')
const t = require('../i18n-helper').getMessage
class I18nProvider extends Component {
+ tOrDefault = (key, defaultValue, ...args) => {
+ const { localeMessages: { current, en } = {} } = this.props
+ return t(current, key, ...args) || t(en, key, ...args) || defaultValue
+ }
+
getChildContext () {
const { localeMessages } = this.props
const { current, en } = localeMessages
@@ -13,6 +18,10 @@ class I18nProvider extends Component {
t (key, ...args) {
return t(current, key, ...args) || t(en, key, ...args) || `[${key}]`
},
+ tOrDefault: this.tOrDefault,
+ tOrKey (key, ...args) {
+ return this.tOrDefault(key, key, ...args)
+ },
}
}
@@ -28,6 +37,8 @@ I18nProvider.propTypes = {
I18nProvider.childContextTypes = {
t: PropTypes.func,
+ tOrDefault: PropTypes.func,
+ tOrKey: PropTypes.func,
}
const mapStateToProps = state => {
diff --git a/ui/app/main-container.js b/ui/app/main-container.js
deleted file mode 100644
index 8a0708025..000000000
--- a/ui/app/main-container.js
+++ /dev/null
@@ -1,49 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const AccountAndTransactionDetails = require('./account-and-transaction-details')
-const Settings = require('./components/pages/settings')
-const log = require('loglevel')
-
-import UnlockScreen from './components/pages/unlock-page'
-
-module.exports = MainContainer
-
-inherits(MainContainer, Component)
-function MainContainer () {
- Component.call(this)
-}
-
-MainContainer.prototype.render = function () {
- // 3. summarize:
- // switch statement goes inside MainContainer,
- // or a method in renderPrimary
- // - pass resulting h() to MainContainer
- // - error checking in separate func
- // - router in separate func
- const contents = {
- component: AccountAndTransactionDetails,
- key: 'account-detail',
- style: {},
- }
-
- if (this.props.isUnlocked === false) {
- switch (this.props.currentViewName) {
- case 'config':
- log.debug('rendering config screen from unlock screen.')
- return h(Settings, {key: 'config'})
- default:
- log.debug('rendering locked screen')
- return h('.unlock-screen-container', {}, h(UnlockScreen, { key: 'locked' }))
- }
- }
-
- return h('div.main-container', {
- style: contents.style,
- }, [
- h(contents.component, {
- key: contents.key,
- }, []),
- ])
-}
-
diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js
deleted file mode 100644
index cc9633166..000000000
--- a/ui/app/new-keychain.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-
-module.exports = connect(mapStateToProps)(NewKeychain)
-
-function mapStateToProps (state) {
- return {}
-}
-
-inherits(NewKeychain, Component)
-function NewKeychain () {
- Component.call(this)
-}
-
-NewKeychain.prototype.render = function () {
- // const props = this.props
-
- return (
- h('div', {
- style: {
- background: 'blue',
- },
- }, [
- h('h1', `Here's a list!!!!`),
- ])
- )
-}
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index c246e7904..5c86d397d 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -48,7 +48,11 @@ function reduceApp (state, action) {
name: null,
},
},
- sidebarOpen: false,
+ sidebar: {
+ isOpen: false,
+ transitionName: '',
+ type: '',
+ },
alertOpen: false,
alertMessage: null,
qrCodeData: null,
@@ -88,12 +92,18 @@ function reduceApp (state, action) {
// sidebar methods
case actions.SIDEBAR_OPEN:
return extend(appState, {
- sidebarOpen: true,
+ sidebar: {
+ ...action.value,
+ isOpen: true,
+ },
})
case actions.SIDEBAR_CLOSE:
return extend(appState, {
- sidebarOpen: false,
+ sidebar: {
+ ...appState.sidebar,
+ isOpen: false,
+ },
})
// alert methods
@@ -209,6 +219,15 @@ function reduceApp (state, action) {
transForward: action.value,
})
+ case actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'add-suggested-token',
+ context: appState.currentView.context,
+ },
+ transForward: action.value,
+ })
+
case actions.SHOW_IMPORT_PAGE:
return extend(appState, {
currentView: {
diff --git a/ui/app/routes.js b/ui/app/routes.js
index f6b2a7a55..76afed5db 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -7,6 +7,7 @@ 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 CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
@@ -41,6 +42,7 @@ module.exports = {
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index d86462275..fb4517628 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -1,6 +1,9 @@
-const valuesFor = require('./util').valuesFor
const abi = require('human-standard-token-abi')
+import {
+ transactionsSelector,
+} from './selectors/transactions'
+
const {
multiplyCurrencies,
} = require('./conversion-util')
@@ -11,6 +14,8 @@ const selectors = {
getSelectedAccount,
getSelectedToken,
getSelectedTokenExchangeRate,
+ getSelectedTokenAssetImage,
+ getAssetImages,
getTokenExchangeRate,
conversionRateSelector,
transactionsSelector,
@@ -68,6 +73,18 @@ function getSelectedTokenExchangeRate (state) {
return contractExchangeRates[address] || 0
}
+function getSelectedTokenAssetImage (state) {
+ const assetImages = state.metamask.assetImages || {}
+ const selectedToken = getSelectedToken(state) || {}
+ const { address } = selectedToken
+ return assetImages[address]
+}
+
+function getAssetImages (state) {
+ const assetImages = state.metamask.assetImages || {}
+ return assetImages
+}
+
function getTokenExchangeRate (state, address) {
const contractExchangeRates = state.metamask.contractExchangeRates
return contractExchangeRates[address] || 0
@@ -101,22 +118,6 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress)
}
-function transactionsSelector (state) {
- const { network, selectedTokenAddress } = state.metamask
- const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
- const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
- const transactions = state.metamask.selectedAddressTxList || []
- const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
-
- // console.log({txsToRender, selectedTokenAddress})
- return selectedTokenAddress
- ? txsToRender
- .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
- .sort((a, b) => b.time - a.time)
- : txsToRender
- .sort((a, b) => b.time - a.time)
-}
-
function getGasIsLoading (state) {
return state.appState.gasIsLoading
}
diff --git a/ui/app/selectors/tokens.js b/ui/app/selectors/tokens.js
new file mode 100644
index 000000000..47b6e0192
--- /dev/null
+++ b/ui/app/selectors/tokens.js
@@ -0,0 +1,11 @@
+import { createSelector } from 'reselect'
+
+export const selectedTokenAddressSelector = state => state.metamask.selectedTokenAddress
+export const tokenSelector = state => state.metamask.tokens
+export const selectedTokenSelector = createSelector(
+ tokenSelector,
+ selectedTokenAddressSelector,
+ (tokens = [], selectedTokenAddress = '') => {
+ return tokens.find(({ address }) => address === selectedTokenAddress)
+ }
+)
diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js
new file mode 100644
index 000000000..3e9843722
--- /dev/null
+++ b/ui/app/selectors/transactions.js
@@ -0,0 +1,58 @@
+import { createSelector } from 'reselect'
+import { valuesFor } from '../util'
+import {
+ UNAPPROVED_STATUS,
+ APPROVED_STATUS,
+ SUBMITTED_STATUS,
+} from '../constants/transactions'
+
+import { selectedTokenAddressSelector } from './tokens'
+
+export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList
+export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
+export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList
+
+const pendingStatusHash = {
+ [UNAPPROVED_STATUS]: true,
+ [APPROVED_STATUS]: true,
+ [SUBMITTED_STATUS]: true,
+}
+
+export const transactionsSelector = createSelector(
+ selectedTokenAddressSelector,
+ unapprovedMsgsSelector,
+ shapeShiftTxListSelector,
+ selectedAddressTxListSelector,
+ (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => {
+ const unapprovedMsgsList = valuesFor(unapprovedMsgs)
+ const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList)
+
+ return selectedTokenAddress
+ ? txsToRender
+ .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
+ .sort((a, b) => b.time - a.time)
+ : txsToRender
+ .sort((a, b) => b.time - a.time)
+ }
+)
+
+export const pendingTransactionsSelector = createSelector(
+ transactionsSelector,
+ (transactions = []) => (
+ transactions.filter(transaction => transaction.status in pendingStatusHash)
+ )
+)
+
+export const submittedPendingTransactionsSelector = createSelector(
+ transactionsSelector,
+ (transactions = []) => (
+ transactions.filter(transaction => transaction.status === SUBMITTED_STATUS)
+ )
+)
+
+export const completedTransactionsSelector = createSelector(
+ transactionsSelector,
+ (transactions = []) => (
+ transactions.filter(transaction => !(transaction.status in pendingStatusHash))
+ )
+)
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
index 8798ed266..3d61ad1ca 100644
--- a/ui/app/token-util.js
+++ b/ui/app/token-util.js
@@ -1,55 +1,113 @@
const log = require('loglevel')
const util = require('./util')
const BigNumber = require('bignumber.js')
+import contractMap from 'eth-contract-metadata'
-function tokenInfoGetter () {
- const tokens = {}
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
- return async (address) => {
- if (tokens[address]) {
- return tokens[address]
+const DEFAULT_SYMBOL = ''
+const DEFAULT_DECIMALS = '0'
+
+async function getSymbolFromContract (tokenAddress) {
+ const token = util.getContractAtAddress(tokenAddress)
+
+ try {
+ const result = await token.symbol()
+ return result[0]
+ } catch (error) {
+ log.warn(`symbol() call for token at address ${tokenAddress} resulted in error:`, error)
+ }
+}
+
+async function getDecimalsFromContract (tokenAddress) {
+ const token = util.getContractAtAddress(tokenAddress)
+
+ try {
+ const result = await token.decimals()
+ const decimalsBN = result[0]
+ return decimalsBN && decimalsBN.toString()
+ } catch (error) {
+ log.warn(`decimals() call for token at address ${tokenAddress} resulted in error:`, error)
+ }
+}
+
+function getContractMetadata (tokenAddress) {
+ return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]
+}
+
+async function getSymbol (tokenAddress) {
+ let symbol = await getSymbolFromContract(tokenAddress)
+
+ if (!symbol) {
+ const contractMetadataInfo = getContractMetadata(tokenAddress)
+
+ if (contractMetadataInfo) {
+ symbol = contractMetadataInfo.symbol
}
+ }
- tokens[address] = await getSymbolAndDecimals(address)
+ return symbol
+}
- return tokens[address]
+async function getDecimals (tokenAddress) {
+ let decimals = await getDecimalsFromContract(tokenAddress)
+
+ if (!decimals || decimals === '0') {
+ const contractMetadataInfo = getContractMetadata(tokenAddress)
+
+ if (contractMetadataInfo) {
+ decimals = contractMetadataInfo.decimals
+ }
}
+
+ return decimals
}
-async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
+export async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
const existingToken = existingTokens.find(({ address }) => tokenAddress === address)
+
if (existingToken) {
- return existingToken
+ return {
+ symbol: existingToken.symbol,
+ decimals: existingToken.decimals,
+ }
}
- let result = []
+ let symbol, decimals
+
try {
- const token = util.getContractAtAddress(tokenAddress)
+ symbol = await getSymbol(tokenAddress)
+ decimals = await getDecimals(tokenAddress)
+ } catch (error) {
+ log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error)
+ }
- result = await Promise.all([
- token.symbol(),
- token.decimals(),
- ])
- } catch (err) {
- log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err)
+ return {
+ symbol: symbol || DEFAULT_SYMBOL,
+ decimals: decimals || DEFAULT_DECIMALS,
}
+}
+
+export function tokenInfoGetter () {
+ const tokens = {}
- const [ symbol = [], decimals = [] ] = result
+ return async (address) => {
+ if (tokens[address]) {
+ return tokens[address]
+ }
- return {
- symbol: symbol[0] || null,
- decimals: decimals[0] && decimals[0].toString() || null,
+ tokens[address] = await getSymbolAndDecimals(address)
+
+ return tokens[address]
}
}
-function calcTokenAmount (value, decimals) {
+export function calcTokenAmount (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
return new BigNumber(String(value)).div(multiplier).toNumber()
}
-
-
-module.exports = {
- tokenInfoGetter,
- calcTokenAmount,
- getSymbolAndDecimals,
-}
diff --git a/ui/app/util.js b/ui/app/util.js
index ade4fec8a..37c0fb698 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -9,7 +9,7 @@ const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
// formatData :: ( date: <Unix Timestamp> ) -> String
function formatDate (date) {
- return vreme.format(new Date(date), 'March 16 2014 14:30')
+ return vreme.format(new Date(date), '3/16/2014 at 14:30')
}
var valueTable = {