Merge remote-tracking branch 'upstream/develop' into develop

feature/default_network_editable
Bruno 7 years ago
commit 3dd58463ca
  1. 57
      .circleci/config.yml
  2. 5
      .nsprc
  3. 10
      .storybook/README.md
  4. 2
      .storybook/addons.js
  5. 11
      .storybook/config.js
  6. 21
      .storybook/decorators.js
  7. 37
      .storybook/webpack.config.js
  8. 13
      app/_locales/en/messages.json
  9. 8
      app/_locales/sl/messages.json
  10. 383
      app/_locales/zh_CN/messages.json
  11. 1
      app/scripts/contentscript.js
  12. 26
      app/scripts/controllers/network/enums.js
  13. 121
      app/scripts/controllers/network/network.js
  14. 36
      app/scripts/controllers/network/util.js
  15. 15
      app/scripts/controllers/transactions/index.js
  16. 21
      app/scripts/controllers/transactions/lib/tx-state-history-helper.js
  17. 4
      app/scripts/controllers/transactions/tx-state-manager.js
  18. 9
      app/scripts/first-time-state.js
  19. 42
      app/scripts/lib/setupRaven.js
  20. 2
      app/scripts/metamask-controller.js
  21. 96
      docs/send-screen-QA-checklist.md
  22. 133
      mascara/src/app/first-time/create-password-screen.js
  23. 4
      mascara/src/app/first-time/import-account-screen.js
  24. 146
      mascara/src/app/first-time/import-seed-phrase-screen.js
  25. 34
      mascara/src/app/first-time/index.css
  26. 56
      mascara/src/app/first-time/index.js
  27. 2
      old-ui/app/accounts/import/json.js
  28. 2
      old-ui/app/accounts/import/private-key.js
  29. 3
      old-ui/app/app.js
  30. 22103
      package-lock.json
  31. 28
      package.json
  32. 314
      test/e2e/chrome/metamask.spec.js
  33. 323
      test/e2e/firefox/metamask.spec.js
  34. 8
      test/e2e/func.js
  35. 145
      test/e2e/metamask.spec.js
  36. 32
      test/integration/lib/mascara-first-time.js
  37. 2
      test/integration/lib/tx-list-items.js
  38. 7
      test/screens/new-ui.js
  39. 30
      test/unit/network-contoller-test.js
  40. 30
      test/unit/tx-controller-test.js
  41. 137
      test/unit/tx-state-history-helper-test.js
  42. 46
      test/unit/tx-state-history-helper.js
  43. 9
      test/unit/tx-state-manager-test.js
  44. 64
      ui/app/actions.js
  45. 122
      ui/app/app.js
  46. 140
      ui/app/components/app-header/app-header.component.js
  47. 38
      ui/app/components/app-header/app-header.container.js
  48. 2
      ui/app/components/app-header/index.js
  49. 43
      ui/app/components/button/button.component.js
  50. 41
      ui/app/components/button/button.stories.js
  51. 2
      ui/app/components/button/index.js
  52. 2
      ui/app/components/buy-button-subview.js
  53. 2
      ui/app/components/export-text-container/export-text-container.scss
  54. 2
      ui/app/components/loading-screen/index.js
  55. 13
      ui/app/components/loading-screen/loading-screen.component.js
  56. 2
      ui/app/components/pages/create-account/import-account/json.js
  57. 2
      ui/app/components/pages/create-account/import-account/private-key.js
  58. 2
      ui/app/components/pages/settings/settings.js
  59. 2
      ui/app/components/pages/unlock-page/index.js
  60. 180
      ui/app/components/pages/unlock-page/unlock-page.component.js
  61. 31
      ui/app/components/pages/unlock-page/unlock-page.container.js
  62. 51
      ui/app/components/pages/unlock-page/unlock-page.scss
  63. 194
      ui/app/components/pages/unlock.js
  64. 14
      ui/app/components/pending-tx/confirm-send-ether.js
  65. 2
      ui/app/components/pending-tx/index.js
  66. 2
      ui/app/components/spinner/index.js
  67. 78
      ui/app/components/spinner/spinner.component.js
  68. 2
      ui/app/components/text-field/index.js
  69. 59
      ui/app/components/text-field/text-field.component.js
  70. 24
      ui/app/components/text-field/text-field.stories.js
  71. 6
      ui/app/components/wallet-view.js
  72. 2
      ui/app/conf-tx.js
  73. 1
      ui/app/css/itcss/components/account-dropdown.scss
  74. 7
      ui/app/css/itcss/components/account-menu.scss
  75. 1
      ui/app/css/itcss/components/add-token.scss
  76. 1
      ui/app/css/itcss/components/buttons.scss
  77. 4
      ui/app/css/itcss/components/confirm.scss
  78. 1
      ui/app/css/itcss/components/currency-display.scss
  79. 114
      ui/app/css/itcss/components/header.scss
  80. 21
      ui/app/css/itcss/components/loading-overlay.scss
  81. 1
      ui/app/css/itcss/components/menu.scss
  82. 2
      ui/app/css/itcss/components/modal.scss
  83. 2
      ui/app/css/itcss/components/newui-sections.scss
  84. 4
      ui/app/css/itcss/components/pages/index.scss
  85. 9
      ui/app/css/itcss/components/pages/unlock.scss
  86. 2
      ui/app/css/itcss/components/request-signature.scss
  87. 41
      ui/app/css/itcss/components/sections.scss
  88. 3
      ui/app/css/itcss/components/send.scss
  89. 4
      ui/app/css/itcss/components/settings.scss
  90. 97
      ui/app/css/itcss/components/welcome-screen.scss
  91. 7
      ui/app/css/itcss/generic/index.scss
  92. 2
      ui/app/first-time/init-menu.js
  93. 2
      ui/app/main-container.js
  94. 5
      ui/app/select-app.js
  95. 11
      ui/app/send-v2.js
  96. 141
      ui/app/unlock.js
  97. 9
      ui/index.js

@ -15,9 +15,17 @@ workflows:
- test-lint: - test-lint:
requires: requires:
- prep-deps-npm - prep-deps-npm
- test-e2e: - test-deps:
requires: requires:
- prep-deps-npm - prep-deps-npm
- test-e2e-chrome:
requires:
- prep-deps-npm
- prep-build
- test-e2e-firefox:
requires:
- prep-deps-npm
- prep-deps-firefox
- prep-build - prep-build
- test-unit: - test-unit:
requires: requires:
@ -43,8 +51,10 @@ workflows:
- all-tests-pass: - all-tests-pass:
requires: requires:
- test-lint - test-lint
- test-deps
- test-unit - test-unit
- test-e2e - test-e2e-chrome
- test-e2e-firefox
- test-integration-mascara-chrome - test-integration-mascara-chrome
- test-integration-mascara-firefox - test-integration-mascara-firefox
- test-integration-flat-chrome - test-integration-flat-chrome
@ -145,7 +155,18 @@ jobs:
name: Test name: Test
command: npm run lint command: npm run lint
test-e2e: test-deps:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- run:
name: Test
command: npx nsp check
test-e2e-chrome:
docker: docker:
- image: circleci/node:8-browsers - image: circleci/node:8-browsers
steps: steps:
@ -156,7 +177,34 @@ jobs:
key: build-cache-{{ .Revision }} key: build-cache-{{ .Revision }}
- run: - run:
name: Test name: Test
command: npm run test:e2e command: npm run test:e2e:chrome
- store_artifacts:
path: test-artifacts
destination: test-artifacts
test-e2e-firefox:
environment:
browsers: '["Firefox"]'
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-firefox-{{ .Revision }}
- run:
name: Install firefox
command: >
sudo rm -r /opt/firefox
&& sudo mv firefox /opt/firefox58
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
name: test:e2e:firefox
command: npm run test:e2e:firefox
- store_artifacts: - store_artifacts:
path: test-artifacts path: test-artifacts
destination: test-artifacts destination: test-artifacts
@ -320,3 +368,4 @@ jobs:
- run: - run:
name: All Tests Passed name: All Tests Passed
command: echo 'weew - everything passed!' command: echo 'weew - everything passed!'

@ -1,3 +1,6 @@
{ {
"exceptions": ["https://nodesecurity.io/advisories/566"] "exceptions": [
"https://nodesecurity.io/advisories/566",
"https://nodesecurity.io/advisories/157"
]
} }

@ -0,0 +1,10 @@
# Storybook
We're currently using [Storybook](https://storybook.js.org/) as part of our design system. To run Storybook and test some of our UI components, clone the repo and run the following:
```
npm install
npm run storybook
```
You should then see:
> info Storybook started on => http://localhost:6006/
In your browser, navigate to http://localhost:6006/ to see the Storybook application. From here, you'll be able to easily view components and even modify some of their properties.

@ -0,0 +1,2 @@
import '@storybook/addon-knobs/register'
import '@storybook/addon-actions/register'

@ -0,0 +1,11 @@
import { configure } from '@storybook/react'
import '../ui/app/css/index.scss'
const req = require.context('../ui/app/components', true, /\.stories\.js$/)
function loadStories () {
require('./decorators')
req.keys().forEach((filename) => req(filename))
}
configure(loadStories, module)

@ -0,0 +1,21 @@
import React from 'react'
import { addDecorator } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import { withKnobs } from '@storybook/addon-knobs/react'
const styles = {
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}
const CenterDecorator = story => (
<div style={styles}>
{ story() }
</div>
)
addDecorator((story, context) => withInfo()(story)(context))
addDecorator(withKnobs)
addDecorator(CenterDecorator)

@ -0,0 +1,37 @@
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=\d+\.\d+\.\d+)?$/,
loaders: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
},
}],
},
{
test: /\.scss$/,
loaders: [
'style-loader',
'css-loader',
'resolve-url-loader',
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
],
},
resolve: {
alias: {
'./fonts/Font_Awesome': path.resolve(__dirname, '../fonts/Font_Awesome'),
},
},
}

@ -393,6 +393,9 @@
"message": "Imported", "message": "Imported",
"description": "status showing that an account has been fully loaded into the keyring" "description": "status showing that an account has been fully loaded into the keyring"
}, },
"importUsingSeed": {
"message": "Import using account seed phrase"
},
"infoHelp": { "infoHelp": {
"message": "Info & Help" "message": "Info & Help"
}, },
@ -632,7 +635,7 @@
"message": "Reset Account" "message": "Reset Account"
}, },
"restoreFromSeed": { "restoreFromSeed": {
"message": "Restore from seed phrase" "message": "Restore account?"
}, },
"restoreVault": { "restoreVault": {
"message": "Restore Vault" "message": "Restore Vault"
@ -721,7 +724,7 @@
"message": "New Password (min 8 chars)" "message": "New Password (min 8 chars)"
}, },
"seedPhraseReq": { "seedPhraseReq": {
"message": "seed phrases are 12 words long" "message": "Seed phrases are 12 words long"
}, },
"select": { "select": {
"message": "Select" "message": "Select"
@ -896,6 +899,9 @@
"unknownNetworkId": { "unknownNetworkId": {
"message": "Unknown network ID" "message": "Unknown network ID"
}, },
"unlockMessage": {
"message": "The decentralized web awaits"
},
"uriErrorMsg": { "uriErrorMsg": {
"message": "URIs require the appropriate HTTP/HTTPS prefix." "message": "URIs require the appropriate HTTP/HTTPS prefix."
}, },
@ -924,6 +930,9 @@
"warning": { "warning": {
"message": "Warning" "message": "Warning"
}, },
"welcomeBack": {
"message": "Welcome Back!"
},
"welcomeBeta": { "welcomeBeta": {
"message": "Welcome to MetaMask Beta" "message": "Welcome to MetaMask Beta"
}, },

@ -181,7 +181,7 @@
"message": "DEN je vaša šifrirana shramba v MetaMasku." "message": "DEN je vaša šifrirana shramba v MetaMasku."
}, },
"deposit": { "deposit": {
"message": "Vplačilo" "message": "Vplačaj"
}, },
"depositBTC": { "depositBTC": {
"message": "Vplačajte vaš BTC na spodnji naslov:" "message": "Vplačajte vaš BTC na spodnji naslov:"
@ -507,10 +507,10 @@
"message": "Ni se začelo" "message": "Ni se začelo"
}, },
"oldUI": { "oldUI": {
"message": "Starejši uporabniški vmesnik" "message": "Star UI"
}, },
"oldUIMessage": { "oldUIMessage": {
"message": "Vrnili ste se v starejši uporabniški vmesnik. V novega se lahko vrnete z možnostjo v spustnem meniju v zgornjem desnem kotu." "message": "Vrnili ste se v star uporabniški vmesnik. V novega se lahko vrnete z možnostjo v spustnem meniju v zgornjem desnem kotu."
}, },
"or": { "or": {
"message": "ali", "message": "ali",
@ -759,7 +759,7 @@
"message": "Vpišite vaše geslo" "message": "Vpišite vaše geslo"
}, },
"uiWelcome": { "uiWelcome": {
"message": "Dobrodošli v novem uporabniškem vmesniku (Beta)" "message": "Dobrodošli v nov UI (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "Zdaj uporabljate novi MetaMask uporabniški vmesnik. Razglejte se, preizkusite nove funkcije, kot so pošiljanje žetonov, in nas obvestite, če imate kakšne težave." "message": "Zdaj uporabljate novi MetaMask uporabniški vmesnik. Razglejte se, preizkusite nove funkcije, kot so pošiljanje žetonov, in nas obvestite, če imate kakšne težave."

@ -14,9 +14,15 @@
"address": { "address": {
"message": "地址" "message": "地址"
}, },
"addCustomToken": {
"message": "添加自定义代币"
},
"addToken": { "addToken": {
"message": "添加代币" "message": "添加代币"
}, },
"addTokens": {
"message": "添加代币"
},
"amount": { "amount": {
"message": "数量" "message": "数量"
}, },
@ -31,9 +37,15 @@
"message": "MetaMask", "message": "MetaMask",
"description": "The name of the application" "description": "The name of the application"
}, },
"approved": {
"message": "批准"
},
"attemptingConnect": { "attemptingConnect": {
"message": "正在尝试连接区块链。" "message": "正在尝试连接区块链。"
}, },
"attributions": {
"message": "来源"
},
"available": { "available": {
"message": "可用" "message": "可用"
}, },
@ -43,6 +55,9 @@
"balance": { "balance": {
"message": "余额:" "message": "余额:"
}, },
"balances": {
"message": "代币余额"
},
"balanceIsInsufficientGas": { "balanceIsInsufficientGas": {
"message": "当前余额不足以支付 Gas" "message": "当前余额不足以支付 Gas"
}, },
@ -53,9 +68,15 @@
"message": "必须大于等于 $1 并且小于等于 $2 。", "message": "必须大于等于 $1 并且小于等于 $2 。",
"description": "helper for inputting hex as decimal input" "description": "helper for inputting hex as decimal input"
}, },
"blockiesIdenticon": {
"message": "使用区块Identicon"
},
"borrowDharma": { "borrowDharma": {
"message": "Borrow With Dharma (Beta)" "message": "Borrow With Dharma (Beta)"
}, },
"builtInCalifornia": {
"message": "MetaMask在加利福尼亚设计和制造。"
},
"buy": { "buy": {
"message": "购买" "message": "购买"
}, },
@ -65,15 +86,27 @@
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase 是世界上最流行的买卖比特币,以太币和莱特币的交易所。" "message": "Coinbase 是世界上最流行的买卖比特币,以太币和莱特币的交易所。"
}, },
"ok": {
"message": "确认"
},
"cancel": { "cancel": {
"message": "取消" "message": "取消"
}, },
"classicInterface": {
"message": "使用经典接口"
},
"clickCopy": { "clickCopy": {
"message": "点击复制" "message": "点击复制"
}, },
"close": {
"message": "关闭"
},
"confirm": { "confirm": {
"message": "确认" "message": "确认"
}, },
"confirmed": {
"message": "确认"
},
"confirmContract": { "confirmContract": {
"message": "确认合约" "message": "确认合约"
}, },
@ -83,6 +116,9 @@
"confirmTransaction": { "confirmTransaction": {
"message": "确认交易" "message": "确认交易"
}, },
"continue": {
"message": "继续"
},
"continueToCoinbase": { "continueToCoinbase": {
"message": "继续访问 Coinbase" "message": "继续访问 Coinbase"
}, },
@ -99,7 +135,10 @@
"message": "已复制到剪贴板" "message": "已复制到剪贴板"
}, },
"copiedExclamation": { "copiedExclamation": {
"message": "已复制!" "message": "已复制"
},
"copiedSafe": {
"message": "我已将它复制保存到某个安全的地方"
}, },
"copy": { "copy": {
"message": "复制" "message": "复制"
@ -126,15 +165,30 @@
"message": "加密", "message": "加密",
"description": "Exchange type (cryptocurrencies)" "description": "Exchange type (cryptocurrencies)"
}, },
"currentConversion": {
"message": "当前汇率"
},
"currentNetwork": {
"message": "当前网络"
},
"customGas": { "customGas": {
"message": "自定义 Gas" "message": "自定义 Gas"
}, },
"customToken": {
"message": "自定义代币"
},
"customize": { "customize": {
"message": "自定义" "message": "自定义"
}, },
"customRPC": { "customRPC": {
"message": "自定义 RPC" "message": "自定义 RPC"
}, },
"decimalsMustZerotoTen": {
"message": "小数位最小为0并且不超过36位."
},
"decimal": {
"message": "精确小数点"
},
"defaultNetwork": { "defaultNetwork": {
"message": "默认以太坊交易网络为主网。" "message": "默认以太坊交易网络为主网。"
}, },
@ -184,18 +238,39 @@
"done": { "done": {
"message": "完成" "message": "完成"
}, },
"downloadStateLogs": {
"message": "下载日志"
},
"dropped": {
"message": "丢弃"
},
"edit": { "edit": {
"message": "编辑" "message": "编辑"
}, },
"editAccountName": { "editAccountName": {
"message": "编辑账户名称" "message": "编辑账户名称"
}, },
"emailUs": {
"message": "联系我们"
},
"encryptNewDen": { "encryptNewDen": {
"message": "加密你的新 DEN" "message": "加密你的新 DEN"
}, },
"enterPassword": { "enterPassword": {
"message": "请输入密码" "message": "请输入密码"
}, },
"enterPasswordConfirm": {
"message": "请输入密码以确认"
},
"enterPasswordContinue": {
"message": "请输入密码以继续"
},
"passwordNotLongEnough": {
"message": "密码长度不足"
},
"passwordsDontMatch": {
"message": "密码不匹配"
},
"etherscanView": { "etherscanView": {
"message": "在 Etherscan 上查看账户" "message": "在 Etherscan 上查看账户"
}, },
@ -219,9 +294,15 @@
"message": "文件导入失败? 点击这里!", "message": "文件导入失败? 点击这里!",
"description": "Helps user import their account from a JSON file" "description": "Helps user import their account from a JSON file"
}, },
"followTwitter": {
"message": "关注我们的Twitter"
},
"from": { "from": {
"message": "来自" "message": "来自"
}, },
"fromToSame": {
"message": "发送和接受地址不能相同"
},
"fromShapeShift": { "fromShapeShift": {
"message": "来自 ShapeShift" "message": "来自 ShapeShift"
}, },
@ -244,6 +325,9 @@
"gasLimitTooLow": { "gasLimitTooLow": {
"message": "Gas Limit 至少要 21000" "message": "Gas Limit 至少要 21000"
}, },
"generatingSeed": {
"message": "生成密钥中..."
},
"gasPrice": { "gasPrice": {
"message": "Gas Price (GWEI)" "message": "Gas Price (GWEI)"
}, },
@ -253,6 +337,9 @@
"gasPriceRequired": { "gasPriceRequired": {
"message": "Gas Price 必填" "message": "Gas Price 必填"
}, },
"generatingTransaction": {
"message": "生成 交易"
},
"getEther": { "getEther": {
"message": "获取 Ether" "message": "获取 Ether"
}, },
@ -268,6 +355,9 @@
"message": "这里", "message": "这里",
"description": "as in -click here- for more information (goes with troubleTokenBalances)" "description": "as in -click here- for more information (goes with troubleTokenBalances)"
}, },
"hereList": {
"message": "Here's a list!!!!"
},
"hide": { "hide": {
"message": "隐藏" "message": "隐藏"
}, },
@ -280,6 +370,9 @@
"howToDeposit": { "howToDeposit": {
"message": "你想怎样转入 Ether?" "message": "你想怎样转入 Ether?"
}, },
"holdEther": {
"message": "它允许你保存ether和代币,并作为你使用Dapp的桥梁."
},
"import": { "import": {
"message": "导入", "message": "导入",
"description": "Button to import an account from a selected file" "description": "Button to import an account from a selected file"
@ -287,6 +380,9 @@
"importAccount": { "importAccount": {
"message": "导入账户" "message": "导入账户"
}, },
"importAccountMsg": {
"message":" Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts "
},
"importAnAccount": { "importAnAccount": {
"message": "导入一个账户" "message": "导入一个账户"
}, },
@ -294,46 +390,82 @@
"message": "导入存在的 DEN" "message": "导入存在的 DEN"
}, },
"imported": { "imported": {
"message": "已导入私钥", "message": "已导入",
"description": "status showing that an account has been fully loaded into the keyring" "description": "status showing that an account has been fully loaded into the keyring"
}, },
"infoHelp": { "infoHelp": {
"message": "信息 & 帮助" "message": "信息 & 帮助"
}, },
"insufficientFunds": {
"message": "余额不足."
},
"insufficientTokens": {
"message": "代币余额不足."
},
"invalidAddress": { "invalidAddress": {
"message": "错误的地址" "message": "无效地址"
},
"invalidAddressRecipient": {
"message": "收款地址不合法"
}, },
"invalidGasParams": { "invalidGasParams": {
"message": "错误的 Gas 参数" "message": "无效 Gas 参数"
}, },
"invalidInput": { "invalidInput": {
"message": "错误的输入。" "message": "无效输入."
}, },
"invalidRequest": { "invalidRequest": {
"message": "无效请求" "message": "无效请求"
}, },
"invalidRPC": {
"message": "无效 RPC URI"
},
"jsonFail": {
"message": "Something went wrong. Please make sure your JSON file is properly formatted."
},
"jsonFile": { "jsonFile": {
"message": "JSON 文件", "message": "JSON 文件",
"description": "format for importing an account" "description": "format for importing an account"
}, },
"keepTrackTokens": {
"message": "Keep track of the tokens you’ve bought with your MetaMask account."
},
"kovan": { "kovan": {
"message": "Kovan 测试网络" "message": "Kovan 测试网络"
}, },
"knowledgeDataBase": {
"message": "浏览我们的知识库"
},
"max": {
"message": "最大"
},
"learnMore": {
"message": "查看更多."
},
"lessThanMax": { "lessThanMax": {
"message": "必须小于等于 $1.", "message": "必须小于等于 $1.",
"description": "helper for inputting hex as decimal input" "description": "helper for inputting hex as decimal input"
}, },
"likeToAddTokens": {
"message": "你想添加这些代币吗?"
},
"links": {
"message": "链接"
},
"limit": { "limit": {
"message": "限定" "message": "限"
}, },
"loading": { "loading": {
"message": "加载..." "message": "加载..."
}, },
"loadingTokens": { "loadingTokens": {
"message": "加载代币..." "message": "加载代币..."
}, },
"localhost": { "localhost": {
"message": "本地主机 8545" "message": "Localhost 8545"
},
"login": {
"message": "登录"
}, },
"logout": { "logout": {
"message": "登出" "message": "登出"
@ -341,17 +473,29 @@
"loose": { "loose": {
"message": "疏松" "message": "疏松"
}, },
"loweCaseWords": {
"message": "助记词只有小写字符"
},
"mainnet": { "mainnet": {
"message": "以太坊主网络" "message": "以太坊主网络"
}, },
"message": { "message": {
"message": "消息" "message": "消息"
}, },
"metamaskDescription": {
"message": "MetaMask is a secure identity vault for Ethereum."
},
"metamaskSeedWords": {
"message": "MetaMask 助记词"
},
"min": { "min": {
"message": "最小" "message": "最小"
}, },
"myAccounts": { "myAccounts": {
"message": "我的账户" "message": "My Accounts"
},
"mustSelectOne": {
"message": "至少选择一种代币."
}, },
"needEtherInWallet": { "needEtherInWallet": {
"message": "使用 MetaMask 与 DAPP 交互,需要你的钱包里有 Ether。" "message": "使用 MetaMask 与 DAPP 交互,需要你的钱包里有 Ether。"
@ -361,9 +505,12 @@
"description": "User is important an account and needs to add a file to continue" "description": "User is important an account and needs to add a file to continue"
}, },
"needImportPassword": { "needImportPassword": {
"message": "必须为已选择的文件输入密码。", "message": "必须为已选择的文件输入密码。",
"description": "Password and file needed to import an account" "description": "Password and file needed to import an account"
}, },
"negativeETH": {
"message": "Can not send negative amounts of ETH."
},
"networks": { "networks": {
"message": "网络" "message": "网络"
}, },
@ -383,8 +530,11 @@
"newRecipient": { "newRecipient": {
"message": "新收款人" "message": "新收款人"
}, },
"newRPC": {
"message": "新 RPC URL"
},
"next": { "next": {
"message": "下一个" "message": "下一"
}, },
"noAddressForName": { "noAddressForName": {
"message": "此 ENS 名字还没有指定地址。" "message": "此 ENS 名字还没有指定地址。"
@ -405,12 +555,18 @@
"message": "旧版界面" "message": "旧版界面"
}, },
"oldUIMessage": { "oldUIMessage": {
"message": "你已经切换到旧版界面。 你可以通过右上方下拉菜单中的选项切换回新的用户界面。" "message": "你已经切换到旧版界面。 你可以通过右上方下拉菜单中的选项切换回新的用户界面。"
}, },
"or": { "or": {
"message": "或", "message": "或",
"description": "choice between creating or importing a new account" "description": "choice between creating or importing a new account"
}, },
"password": {
"message": "密码"
},
"passwordCorrect": {
"message": "Please make sure your password is correct."
},
"passwordMismatch": { "passwordMismatch": {
"message": "密码不匹配", "message": "密码不匹配",
"description": "in password creation process, the two new password fields did not match" "description": "in password creation process, the two new password fields did not match"
@ -426,15 +582,24 @@
"pasteSeed": { "pasteSeed": {
"message": "请粘贴你的助记词!" "message": "请粘贴你的助记词!"
}, },
"personalAddressDetected": {
"message": "检测到个人地址。请输入代币合约地址。"
},
"pleaseReviewTransaction": { "pleaseReviewTransaction": {
"message": "请检查你的交易。" "message": "请检查你的交易。"
}, },
"popularTokens": {
"message": "常用代币"
},
"privacyMsg": {
"message": "隐私政策"
},
"privateKey": { "privateKey": {
"message": "私钥", "message": "私钥",
"description": "select this type of file to use to import an account" "description": "select this type of file to use to import an account"
}, },
"privateKeyWarning": { "privateKeyWarning": {
"message": "注意:永远不要公开这个私钥。任何拥有你的私钥的人都可以窃取你帐户中的任何资产。" "message": "注意:永远不要公开这个私钥。任何拥有你的私钥的人都可以窃取你帐户中的任何资产。"
}, },
"privateNetwork": { "privateNetwork": {
"message": "私有网络" "message": "私有网络"
@ -443,11 +608,14 @@
"message": "显示二维码" "message": "显示二维码"
}, },
"readdToken": { "readdToken": {
"message": "之后你还可以通过帐户选项菜单中的“添加代币”来添加此代币。" "message": "之后你还可以通过帐户选项菜单中的“添加代币”来添加此代币。"
}, },
"readMore": { "readMore": {
"message": "了解更多。" "message": "了解更多。"
}, },
"readMore2": {
"message": "了解更多。"
},
"receive": { "receive": {
"message": "接收" "message": "接收"
}, },
@ -460,12 +628,39 @@
"rejected": { "rejected": {
"message": "拒绝" "message": "拒绝"
}, },
"resetAccount": {
"message": "重设账户"
},
"restoreFromSeed": {
"message": "从助记词还原"
},
"restoreVault": {
"message": "还原保险柜"
},
"required": { "required": {
"message": "必填" "message": "必填"
}, },
"retryWithMoreGas": { "retryWithMoreGas": {
"message": "使用更高的 Gas Price 重试" "message": "使用更高的 Gas Price 重试"
}, },
"walletSeed": {
"message": "钱包助记词"
},
"revealSeedWords": {
"message": "显示助记词"
},
"revealSeedWordsTitle": {
"message": "助记词"
},
"revealSeedWordsDescription": {
"message": "如果您更换浏览器或计算机,则需要使用此助记词访问您的帐户。请将它们保存在安全秘密的地方。"
},
"revealSeedWordsWarningTitle": {
"message": "不要对任何人展示助记词!"
},
"revealSeedWordsWarning": {
"message": "助记词可以用来窃取您的所有帐户."
},
"revert": { "revert": {
"message": "还原" "message": "还原"
}, },
@ -475,6 +670,24 @@
"ropsten": { "ropsten": {
"message": "Ropsten 测试网络" "message": "Ropsten 测试网络"
}, },
"currentRpc": {
"message": "当前 RPC"
},
"connectingToMainnet": {
"message": "正在连接到以太坊主网"
},
"connectingToRopsten": {
"message": "正在连接到Ropsten测试网络"
},
"connectingToKovan": {
"message": "正在连接到Kovan测试网络"
},
"connectingToRinkeby": {
"message": "正在连接到Rinkeby测试网络"
},
"connectingToUnknown": {
"message": "正在连接到未知网络"
},
"sampleAccountName": { "sampleAccountName": {
"message": "例如:我的账户", "message": "例如:我的账户",
"description": "Help user understand concept of adding a human-readable name to their account" "description": "Help user understand concept of adding a human-readable name to their account"
@ -482,25 +695,70 @@
"save": { "save": {
"message": "保存" "message": "保存"
}, },
"reprice_title": {
"message": "重新出价交易"
},
"reprice_subtitle": {
"message": "提高 GAS 价格尝试覆盖并加速交易"
},
"saveAsCsvFile": {
"message": "另存为CSV文件"
},
"saveAsFile": { "saveAsFile": {
"message": "保存文件", "message": "保存文件",
"description": "Account export process" "description": "Account export process"
}, },
"saveSeedAsFile": {
"message": "保存助记词为文件"
},
"search": {
"message": "搜索"
},
"secretPhrase": {
"message": "输入12位助记词以恢复金库."
},
"newPassword8Chars": {
"message": "新密码(至少8位)"
},
"seedPhraseReq": {
"message": "助记词为12个单词"
},
"select": {
"message": "选择"
},
"selectCurrency": {
"message": "选择货币"
},
"selectService": { "selectService": {
"message": "选择服务" "message": "选择服务"
}, },
"selectType": {
"message": "选择类型"
},
"send": { "send": {
"message": "发送" "message": "发送"
}, },
"sendETH": {
"message": "发送 ETH"
},
"sendTokens": { "sendTokens": {
"message": "发送代币" "message": "发送 代币"
},
"onlySendToEtherAddress": {
"message": "只发送 ETH 给一个以太坊地址"
},
"searchTokens": {
"message": "搜索代币"
}, },
"sendTokensAnywhere": { "sendTokensAnywhere": {
"message": "发送代币给拥有以太坊账户的任何人" "message": "将代币发送给拥有以太坊地址的任何人"
}, },
"settings": { "settings": {
"message": "设置" "message": "设置"
}, },
"info": {
"message": "信息"
},
"shapeshiftBuy": { "shapeshiftBuy": {
"message": "使用 Shapeshift 购买" "message": "使用 Shapeshift 购买"
}, },
@ -513,6 +771,9 @@
"sign": { "sign": {
"message": "签名" "message": "签名"
}, },
"signed": {
"message": "已签名"
},
"signMessage": { "signMessage": {
"message": "签署消息" "message": "签署消息"
}, },
@ -525,15 +786,39 @@
"sigRequested": { "sigRequested": {
"message": "签名已请求" "message": "签名已请求"
}, },
"spaceBetween": {
"message": "单词之间只能有一个空格"
},
"status": { "status": {
"message": "状态" "message": "状态"
}, },
"stateLogs": {
"message": "状态日志"
},
"stateLogsDescription": {
"message": "状态日志包含您的账户地址和已发送的交易。"
},
"stateLogError": {
"message": "检索状态日志时出错。"
},
"submit": { "submit": {
"message": "提交" "message": "提交"
}, },
"submitted": {
"message": "已提交"
},
"supportCenter": {
"message": "访问我们的支持中心"
},
"symbolBetweenZeroTen": {
"message": "符号应该有0-10个字符."
},
"takesTooLong": { "takesTooLong": {
"message": "花费太长时间?" "message": "花费太长时间?"
}, },
"terms": {
"message": "使用条款"
},
"testFaucet": { "testFaucet": {
"message": "测试水管" "message": "测试水管"
}, },
@ -544,33 +829,60 @@
"message": "$1 ETH 通过 ShapeShift", "message": "$1 ETH 通过 ShapeShift",
"description": "system will fill in deposit type in start of message" "description": "system will fill in deposit type in start of message"
}, },
"tokenAddress": {
"message": "代币地址"
},
"tokenAlreadyAdded": {
"message": "代币已经被添加."
},
"tokenBalance": { "tokenBalance": {
"message": "代币余额:" "message": "代币余额:"
}, },
"tokenSelection": {
"message": "搜索代币或从我们的常用代币列表中进行选择"
},
"tokenSymbol": {
"message": "代币符号"
},
"tokenWarning1": {
"message": "Keep track of the tokens you’ve bought with your MetaMask account. If you bought tokens using a different account, those tokens will not appear here."
},
"total": { "total": {
"message": "总量" "message": "总量"
}, },
"transactions": {
"message": "交易"
},
"transactionError": {
"message": "交易出错. 合约代码执行异常."
},
"transactionMemo": { "transactionMemo": {
"message": "交易备注 (可选)" "message": "交易备注(可选)"
}, },
"transactionNumber": { "transactionNumber": {
"message": "交易号" "message": "交易 number"
}, },
"transfers": { "transfers": {
"message": "Transfers" "message": "交易"
}, },
"troubleTokenBalances": { "troubleTokenBalances": {
"message": "无法加载代币余额。你可以再这里查看 ", "message": "我们无法加载您的代币余额。你可以查看它们",
"description": "Followed by a link (here) to view token balances" "description": "Followed by a link (here) to view token balances"
}, },
"twelveWords": {
"message": "这12个单词是恢复MetaMask帐户的唯一方法。.\n将它们存放在安全和秘密的地方。."
},
"typePassword": { "typePassword": {
"message": "请输入密码" "message": "输入你的密码"
}, },
"uiWelcome": { "uiWelcome": {
"message": "欢迎使用新版界面 (Beta)" "message": "欢迎使用新版界面 (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "你现在正在使用新的 Metamask 界面。 尝试发送代币等新功能,有任何问题请告知我们。" "message": "你现在正在使用新的 Metamask 界面。 尝试发送代币等新功能,有任何问题请告知我们。"
},
"unapproved": {
"message": "未批准"
}, },
"unavailable": { "unavailable": {
"message": "不可用" "message": "不可用"
@ -582,7 +894,10 @@
"message": "未知私有网络" "message": "未知私有网络"
}, },
"unknownNetworkId": { "unknownNetworkId": {
"message": "未知网络 ID" "message": "未知网络ID"
},
"uriErrorMsg": {
"message": "URIs require the appropriate HTTP/HTTPS prefix."
}, },
"usaOnly": { "usaOnly": {
"message": "只限于美国", "message": "只限于美国",
@ -591,12 +906,27 @@
"usedByClients": { "usedByClients": {
"message": "可用于各种不同的客户端" "message": "可用于各种不同的客户端"
}, },
"useOldUI": {
"message": "使用旧版 UI"
},
"validFileImport": {
"message": "您必须选择一个有效的文件进行导入."
},
"vaultCreated": {
"message": "已创建保险库"
},
"viewAccount": { "viewAccount": {
"message": "查看账户" "message": "查看账户"
}, },
"visitWebSite": {
"message": "访问我们的网站"
},
"warning": { "warning": {
"message": "警告" "message": "警告"
}, },
"welcomeBeta": {
"message": "欢迎使用 MetaMask 测试版"
},
"whatsThis": { "whatsThis": {
"message": "这是什么?" "message": "这是什么?"
}, },
@ -605,5 +935,8 @@
}, },
"youSign": { "youSign": {
"message": "正在签名" "message": "正在签名"
},
"yourPrivateSeedPhrase": {
"message": "你的私有助记词"
} }
} }

@ -174,6 +174,7 @@ function blacklistedDomainCheck () {
'uscourts.gov', 'uscourts.gov',
'dropbox.com', 'dropbox.com',
'webbyawards.com', 'webbyawards.com',
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
] ]
var currentUrl = window.location.href var currentUrl = window.location.href
var currentRegex var currentRegex

@ -13,20 +13,6 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby'
const KOVAN_DISPLAY_NAME = 'Kovan' const KOVAN_DISPLAY_NAME = 'Kovan'
const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network'
const MAINNET_RPC_URL = 'https://mainnet.infura.io/metamask'
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
const LOCALHOST_RPC_URL = 'http://localhost:8545'
const MAINNET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2'
const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2'
const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2'
const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2'
const DEFAULT_NETWORK = 'rinkeby'
const OLD_UI_NETWORK_TYPE = 'network'
const BETA_UI_NETWORK_TYPE = 'networkBeta'
module.exports = { module.exports = {
ROPSTEN, ROPSTEN,
@ -41,16 +27,4 @@ module.exports = {
RINKEBY_DISPLAY_NAME, RINKEBY_DISPLAY_NAME,
KOVAN_DISPLAY_NAME, KOVAN_DISPLAY_NAME,
MAINNET_DISPLAY_NAME, MAINNET_DISPLAY_NAME,
MAINNET_RPC_URL,
ROPSTEN_RPC_URL,
KOVAN_RPC_URL,
RINKEBY_RPC_URL,
LOCALHOST_RPC_URL,
MAINNET_RPC_URL_BETA,
ROPSTEN_RPC_URL_BETA,
KOVAN_RPC_URL_BETA,
RINKEBY_RPC_URL_BETA,
DEFAULT_NETWORK,
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
} }

@ -14,52 +14,40 @@ const {
RINKEBY, RINKEBY,
KOVAN, KOVAN,
MAINNET, MAINNET,
OLD_UI_NETWORK_TYPE, LOCALHOST,
DEFAULT_NETWORK,
} = require('./enums') } = require('./enums')
const { getNetworkEndpoints } = require('./util') const LOCALHOST_RPC_URL = 'http://localhost:8545'
const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET]
const env = process.env.METAMASK_ENV
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const testMode = (METAMASK_DEBUG || env === 'test')
const defaultProviderConfig = {
type: testMode ? RINKEBY : MAINNET,
}
module.exports = class NetworkController extends EventEmitter { module.exports = class NetworkController extends EventEmitter {
constructor (config) { constructor (opts = {}) {
super() super()
this._networkEndpointVersion = OLD_UI_NETWORK_TYPE // parse options
this._networkEndpoints = getNetworkEndpoints(OLD_UI_NETWORK_TYPE) const providerConfig = opts.provider || defaultProviderConfig
this._defaultRpc = this._networkEndpoints[DEFAULT_NETWORK] // create stores
this.providerStore = new ObservableStore(providerConfig)
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.networkStore = new ObservableStore('loading') this.networkStore = new ObservableStore('loading')
this.providerStore = new ObservableStore(config.provider)
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
// create event emitter proxy
this._proxy = createEventEmitterProxy() this._proxy = createEventEmitterProxy()
this.on('networkDidChange', this.lookupNetwork) this.on('networkDidChange', this.lookupNetwork)
} }
async setNetworkEndpoints (version) {
if (version === this._networkEndpointVersion) {
return
}
this._networkEndpointVersion = version
this._networkEndpoints = getNetworkEndpoints(version)
this._defaultRpc = this._networkEndpoints[DEFAULT_NETWORK]
const { type } = this.getProviderConfig()
return this.setProviderType(type, true)
}
initializeProvider (_providerParams) { initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams this._baseProviderParams = _providerParams
const { type, rpcTarget } = this.providerStore.getState() const { type, rpcTarget } = this.providerStore.getState()
// map rpcTarget to rpcUrl this._configureProvider({ type, rpcTarget })
const opts = {
type,
rpcUrl: rpcTarget,
}
this._configureProvider(opts)
this._proxy.on('block', this._logBlock.bind(this)) this._proxy.on('block', this._logBlock.bind(this))
this._proxy.on('error', this.verifyNetwork.bind(this)) this._proxy.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this._proxy) this.ethQuery = new EthQuery(this._proxy)
@ -96,45 +84,27 @@ module.exports = class NetworkController extends EventEmitter {
}) })
} }
setRpcTarget (rpcUrl) { setRpcTarget (rpcTarget) {
this.providerStore.updateState({ const providerConfig = {
type: 'rpc', type: 'rpc',
rpcTarget: rpcUrl, rpcTarget,
})
this._switchNetwork({ rpcUrl })
}
getCurrentRpcAddress () {
const provider = this.getProviderConfig()
if (!provider) return null
return this.getRpcAddressForType(provider.type)
}
async setProviderType (type, forceUpdate = false) {
assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`)
// skip if type already matches
if (type === this.getProviderConfig().type && !forceUpdate) {
return
} }
this.providerStore.updateState(providerConfig)
this._switchNetwork(providerConfig)
}
const rpcTarget = this.getRpcAddressForType(type) async setProviderType (type) {
assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`)
this.providerStore.updateState({ type, rpcTarget }) assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`)
this._switchNetwork({ type }) const providerConfig = { type }
this.providerStore.updateState(providerConfig)
this._switchNetwork(providerConfig)
} }
getProviderConfig () { getProviderConfig () {
return this.providerStore.getState() return this.providerStore.getState()
} }
getRpcAddressForType (type, provider = this.getProviderConfig()) {
if (this._networkEndpoints[type]) {
return this._networkEndpoints[type]
}
return provider && provider.rpcTarget ? provider.rpcTarget : this._defaultRpc
}
// //
// Private // Private
// //
@ -146,32 +116,27 @@ module.exports = class NetworkController extends EventEmitter {
} }
_configureProvider (opts) { _configureProvider (opts) {
// type-based rpc endpoints const { type, rpcTarget } = opts
const { type } = opts // infura type-based endpoints
if (type) { const isInfura = INFURA_PROVIDER_TYPES.includes(type)
// type-based infura rpc endpoints if (isInfura) {
const isInfura = INFURA_PROVIDER_TYPES.includes(type) this._configureInfuraProvider(opts)
opts.rpcUrl = this.getRpcAddressForType(type) // other type-based rpc endpoints
if (isInfura) { } else if (type === LOCALHOST) {
this._configureInfuraProvider(opts) this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL })
// other type-based rpc endpoints
} else {
this._configureStandardProvider(opts)
}
// url-based rpc endpoints // url-based rpc endpoints
} else if (type === 'rpc'){
this._configureStandardProvider({ rpcUrl: rpcTarget })
} else { } else {
this._configureStandardProvider(opts) throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`)
} }
} }
_configureInfuraProvider (opts) { _configureInfuraProvider ({ type }) {
log.info('_configureInfuraProvider', opts) log.info('_configureInfuraProvider', type)
const infuraProvider = createInfuraProvider({ const infuraProvider = createInfuraProvider({ network: type })
network: opts.type,
})
const infuraSubprovider = new SubproviderFromProvider(infuraProvider) const infuraSubprovider = new SubproviderFromProvider(infuraProvider)
const providerParams = extend(this._baseProviderParams, { const providerParams = extend(this._baseProviderParams, {
rpcUrl: opts.rpcUrl,
engineParams: { engineParams: {
pollingInterval: 8000, pollingInterval: 8000,
blockTrackerProvider: infuraProvider, blockTrackerProvider: infuraProvider,

@ -3,7 +3,6 @@ const {
RINKEBY, RINKEBY,
KOVAN, KOVAN,
MAINNET, MAINNET,
LOCALHOST,
ROPSTEN_CODE, ROPSTEN_CODE,
RINKEYBY_CODE, RINKEYBY_CODE,
KOVAN_CODE, KOVAN_CODE,
@ -11,17 +10,6 @@ const {
RINKEBY_DISPLAY_NAME, RINKEBY_DISPLAY_NAME,
KOVAN_DISPLAY_NAME, KOVAN_DISPLAY_NAME,
MAINNET_DISPLAY_NAME, MAINNET_DISPLAY_NAME,
MAINNET_RPC_URL,
ROPSTEN_RPC_URL,
KOVAN_RPC_URL,
RINKEBY_RPC_URL,
LOCALHOST_RPC_URL,
MAINNET_RPC_URL_BETA,
ROPSTEN_RPC_URL_BETA,
KOVAN_RPC_URL_BETA,
RINKEBY_RPC_URL_BETA,
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
} = require('./enums') } = require('./enums')
const networkToNameMap = { const networkToNameMap = {
@ -34,32 +22,8 @@ const networkToNameMap = {
[KOVAN_CODE]: KOVAN_DISPLAY_NAME, [KOVAN_CODE]: KOVAN_DISPLAY_NAME,
} }
const networkEndpointsMap = {
[OLD_UI_NETWORK_TYPE]: {
[LOCALHOST]: LOCALHOST_RPC_URL,
[MAINNET]: MAINNET_RPC_URL,
[ROPSTEN]: ROPSTEN_RPC_URL,
[KOVAN]: KOVAN_RPC_URL,
[RINKEBY]: RINKEBY_RPC_URL,
},
[BETA_UI_NETWORK_TYPE]: {
[LOCALHOST]: LOCALHOST_RPC_URL,
[MAINNET]: MAINNET_RPC_URL_BETA,
[ROPSTEN]: ROPSTEN_RPC_URL_BETA,
[KOVAN]: KOVAN_RPC_URL_BETA,
[RINKEBY]: RINKEBY_RPC_URL_BETA,
},
}
const getNetworkDisplayName = key => networkToNameMap[key] const getNetworkDisplayName = key => networkToNameMap[key]
const getNetworkEndpoints = (networkType = OLD_UI_NETWORK_TYPE) => {
return {
...networkEndpointsMap[networkType],
}
}
module.exports = { module.exports = {
getNetworkDisplayName, getNetworkDisplayName,
getNetworkEndpoints,
} }

@ -111,6 +111,21 @@ class TransactionController extends EventEmitter {
this.txStateManager.wipeTransactions(address) this.txStateManager.wipeTransactions(address)
} }
/**
Check if a txMeta in the list with the same nonce has been confirmed in a block
if the txParams dont have a nonce will return false
@returns {boolean} whether the nonce has been used in a transaction confirmed in a block
@param {object} txMeta - the txMeta object
*/
async isNonceTaken (txMeta) {
const { from, nonce } = txMeta.txParams
if ('nonce' in txMeta.txParams) {
const sameNonceTxList = this.txStateManager.getFilteredTxList({from, nonce, status: 'confirmed'})
return (sameNonceTxList.length >= 1)
}
return false
}
/** /**
add a new unapproved transaction to the pipeline add a new unapproved transaction to the pipeline

@ -25,26 +25,31 @@ function migrateFromSnapshotsToDiffs (longHistory) {
} }
/** /**
generates an array of history objects sense the previous state. Generates an array of history objects sense the previous state.
The object has the keys opp(the operation preformed), The object has the keys
path(the key and if a nested object then each key will be seperated with a `/`) op (the operation performed),
value path (the key and if a nested object then each key will be seperated with a `/`)
with the first entry having the note value
with the first entry having the note and a timestamp when the change took place
@param previousState {object} - the previous state of the object @param previousState {object} - the previous state of the object
@param newState {object} - the update object @param newState {object} - the update object
@param note {string} - a optional note for the state change @param note {string} - a optional note for the state change
@reurns {array} @returns {array}
*/ */
function generateHistoryEntry (previousState, newState, note) { function generateHistoryEntry (previousState, newState, note) {
const entry = jsonDiffer.compare(previousState, newState) const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry // Add a note to the first op, since it breaks if we append it to the entry
if (note && entry[0]) entry[0].note = note if (entry[0]) {
if (note) entry[0].note = note
entry[0].timestamp = Date.now()
}
return entry return entry
} }
/** /**
Recovers previous txMeta state obj Recovers previous txMeta state obj
@return {object} @returns {object}
*/ */
function replayHistory (_shortHistory) { function replayHistory (_shortHistory) {
const shortHistory = clone(_shortHistory) const shortHistory = clone(_shortHistory)

@ -159,7 +159,7 @@ class TransactionStateManager extends EventEmitter {
/** /**
updates the txMeta in the list and adds a history entry updates the txMeta in the list and adds a history entry
@param txMeta {Object} - the txMeta to update @param txMeta {Object} - the txMeta to update
@param [note] {string} - a not about the update for history @param [note] {string} - a note about the update for history
*/ */
updateTx (txMeta, note) { updateTx (txMeta, note) {
// validate txParams // validate txParams
@ -263,7 +263,7 @@ class TransactionStateManager extends EventEmitter {
*/ */
getTxsByMetaData (key, value, txList = this.getTxList()) { getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => { return txList.filter((txMeta) => {
if (txMeta.txParams[key]) { if (key in txMeta.txParams) {
return txMeta.txParams[key] === value return txMeta.txParams[key] === value
} else { } else {
return txMeta[key] === value return txMeta[key] === value

@ -1,7 +1,3 @@
// test and development environment variables
const env = process.env.METAMASK_ENV
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const { DEFAULT_NETWORK, MAINNET } = require('./controllers/network/enums')
/** /**
* @typedef {Object} FirstTimeState * @typedef {Object} FirstTimeState
@ -14,11 +10,6 @@ const { DEFAULT_NETWORK, MAINNET } = require('./controllers/network/enums')
*/ */
const initialState = { const initialState = {
config: {}, config: {},
NetworkController: {
provider: {
type: (METAMASK_DEBUG || env === 'test') ? DEFAULT_NETWORK : MAINNET,
},
},
} }
module.exports = initialState module.exports = initialState

@ -25,7 +25,7 @@ function setupRaven(opts) {
const report = opts.data const report = opts.data
try { try {
// handle error-like non-error exceptions // handle error-like non-error exceptions
nonErrorException(report) rewriteErrorLikeExceptions(report)
// simplify certain complex error messages (e.g. Ethjs) // simplify certain complex error messages (e.g. Ethjs)
simplifyErrorMessages(report) simplifyErrorMessages(report)
// modify report urls // modify report urls
@ -42,27 +42,35 @@ function setupRaven(opts) {
return Raven return Raven
} }
function nonErrorException(report) { function rewriteErrorLikeExceptions(report) {
// handle errors that lost their error-ness in serialization // handle errors that lost their error-ness in serialization (e.g. dnode)
if (report.message.includes('Non-Error exception captured with keys: message')) { rewriteErrorMessages(report, (errorMessage) => {
if (!(report.extra && report.extra.__serialized__)) return if (!errorMessage.includes('Non-Error exception captured with keys:')) return errorMessage
report.message = `Non-Error Exception: ${report.extra.__serialized__.message}` if (!(report.extra && report.extra.__serialized__ && report.extra.__serialized__.message)) return errorMessage
} return `Non-Error Exception: ${report.extra.__serialized__.message}`
})
} }
function simplifyErrorMessages(report) { function simplifyErrorMessages(report) {
rewriteErrorMessages(report, (errorMessage) => {
// simplify ethjs error messages
errorMessage = extractEthjsErrorMessage(errorMessage)
// simplify 'Transaction Failed: known transaction'
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
// cut the hash from the error message
errorMessage = 'Transaction Failed: known transaction'
}
return errorMessage
})
}
function rewriteErrorMessages(report, rewriteFn) {
// rewrite top level message
report.message = rewriteFn(report.message)
// rewrite each exception message
if (report.exception && report.exception.values) { if (report.exception && report.exception.values) {
report.exception.values.forEach(item => { report.exception.values.forEach(item => {
let errorMessage = item.value item.value = rewriteFn(item.value)
// simplify ethjs error messages
errorMessage = extractEthjsErrorMessage(errorMessage)
// simplify 'Transaction Failed: known transaction'
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
// cut the hash from the error message
errorMessage = 'Transaction Failed: known transaction'
}
// finalize
item.value = errorMessage
}) })
} }
} }

@ -355,7 +355,6 @@ module.exports = class MetamaskController extends EventEmitter {
submitPassword: nodeify(keyringController.submitPassword, keyringController), submitPassword: nodeify(keyringController.submitPassword, keyringController),
// network management // network management
setNetworkEndpoints: nodeify(networkController.setNetworkEndpoints, networkController),
setProviderType: nodeify(networkController.setProviderType, networkController), setProviderType: nodeify(networkController.setProviderType, networkController),
setCustomRpc: nodeify(this.setCustomRpc, this), setCustomRpc: nodeify(this.setCustomRpc, this),
@ -382,6 +381,7 @@ module.exports = class MetamaskController extends EventEmitter {
updateTransaction: nodeify(txController.updateTransaction, txController), updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this), retryTransaction: nodeify(this.retryTransaction, this),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
// messageManager // messageManager
signMessage: nodeify(this.signMessage, this), signMessage: nodeify(this.signMessage, this),

@ -0,0 +1,96 @@
# Send screen QA checklist:
This checklist can be to guide QA of the send screen. It can also be used to guide e2e tests for the send screen.
Once all of these are QA verified on master, resolutions to any bugs related to the send screen should include and update to this list.
Additional features or functionality on the send screen should include an update to this list.
## Send Eth mode
- [ ] **Header** _It should:_
- [ ] have title "Send ETH"
- [ ] have sub title "Only send ETH to an Ethereum address."
- [ ] return user to main screen when top right X is clicked
- [ ] **From row** _It should:_
- [ ] show the currently selected account by default
- [ ] show a dropdown with all of the users accounts
- [ ] contain the following info for each account: identicon, account name, balance in ETH, balance in current currency
- [ ] change the account selected in the dropdown (but not the app-wide selected account) when one in the dropdown is clicked
- [ ] close the dropdown, without changing the dropdown selected account, when the dropdown is open and then a click happens outside it
- [ ] **To row** _It should:_
- [ ] Show a placeholder with the text 'Recipient Address' by default
- [ ] Show, when clicked, a dropdown list of all 'to accounts': the users accounts, plus any other accounts they have previously sent to
- [ ] Show account address, and account name if it exists, of each item in the dropdown list
- [ ] Show a dropdown list of all to accounts (see above) whose address matches an address currently being typed in
- [ ] Set the input text to the address of an account clicked in the dropdown list, and also hide the dropdown
- [ ] Hide the dropdown without changing what is in the input if the user clicks outside the dropdown list while it is open
- [ ] Select the text in the input (i.e. the address) if an address is displayed and then clicked
- [ ] Show a 'required' error if the dropdown is opened but no account is selected
- [ ] Show an 'invalid address' error if text is entered in the input that cannot be a valid hex address or ens address
- [ ] Support ens names. (enter dinodan.eth on mainnet) After entering the plain text address, the hex address should appear in the input with a green checkmark beside
- [ ] Should show a 'no such address' error if a non-existent ens address is entered
- [ ] **Amount row** _It should:_
- [ ] allow user to enter any rational number >= 0
- [ ] allow user to copy and paste into the field
- [ ] show an insufficient funds error if an amount > balance - gas fee
- [ ] display 'ETH' after the number amount. The position of 'ETH' should change as the length of the input amount text changes
- [ ] display the value of the amount of ETH in the current currency, formatted in that currency
- [ ] show a 'max' but if amount < balance - gas fee
- [ ] show no max button or error if amount === balance - gas fee
- [ ] set the amount to balance - gas fee if the 'max' button is clicked
- [ ] **Gas Fee Display row** _It should:_
- [ ] Default to the fee given by the estimated gas price
- [ ] display the fee in ETH and the current currency
- [ ] update when changes are made using the customize gas modal
- [ ] **Cancel button** _It should:_
- [ ] Take the user back to the main screen
- [ ] **submit button** _It should:_
- [ ] be disabled if no recipient address is provided or if any field is in error
- [ ] sign a transaction with the info in the above form, and display the details of that transaction on the confirm screen
## Send token mode
- [ ] **Header** _It should:_
- [ ] have title "Send Tokens"
- [ ] have sub title "Only send [token symbol] to an Ethereum address."
- [ ] return user to main screen when top right X is clicked
- [ ] **From row** _It should:_
- [ ] Behave the same as 'Send ETH mode' (see above)
- [ ] **To row** _It should:_
- [ ] Behave the same as 'Send ETH mode' (see above)
- [ ] **Amount row** _It should:_
- [ ] allow user to enter any rational number >= 0
- [ ] allow user to copy and paste into the field
- [ ] show an 'insufficient tokens' error if an amount > token balance
- [ ] show an 'insufficient funds' error if an gas fee > eth balance
- [ ] display [token symbol] after the number amount. The position of [token symbol] should change as the length of the input amount text changes
- [ ] display the value of the amount of tokens in the current currency, formatted in that currency
- [ ] show a 'max' but if amount < token balance
- [ ] show no max button or error if amount === token balance
- [ ] set the amount to token balance if the 'max' button is clicked
- [ ] **Gas Fee Display row** _It should:_
- [ ] Behave the same as 'Send ETH mode' (see above)
- [ ] **Cancel button** _It should:_
- [ ] Take the user back to the main screen
- [ ] **submit button** _It should:_
- [ ] be disabled if no recipient address is provided or if any field is in error
- [ ] sign a token transaction with the info in the above form, and display the details of that transaction on the confirm screen
## Edit send Eth mode
- [ ] Say 'Editing transaction' in the header
- [ ] display a button to go back to the confirmation screen without applying update
- [ ] say 'update transaction' on the submit button
- [ ] update the existing transaction, instead of signing a new one, when clicking the submit button
- [ ] Otherwise, behave the same as 'Send ETH mode' (see above)
## Edit send token mode
- [ ] Behave the same as 'Edit send Eth mode' (see above)
## Specific cases to test
- [ ] Send eth to a hex address
- [ ] Send eth to an ENS address
- [ ] Donate to the faucet at https://faucet.metamask.io/ and edit the transaction before confirming
- [ ] Send a token that is available on the 'Add Token' screen search to a hex address
- [ ] Create a custom token at https://tokenfactory.surge.sh/ and send it to a hex address
- [ ] Send a token to an ENS address
- [ ] Create a token transaction using https://tokenfactory.surge.sh/#/, and edit the transaction before confirming
- [ ] Send each of MKR, EOS and ICON using myetherwallet, and edit the transaction before confirming

@ -13,8 +13,13 @@ import {
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_NOTICE_ROUTE, INITIALIZE_NOTICE_ROUTE,
} from '../../../../ui/app/routes' } from '../../../../ui/app/routes'
import TextField from '../../../../ui/app/components/text-field'
class CreatePasswordScreen extends Component { class CreatePasswordScreen extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = { static propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
createAccount: PropTypes.func.isRequired, createAccount: PropTypes.func.isRequired,
@ -27,6 +32,8 @@ class CreatePasswordScreen extends Component {
state = { state = {
password: '', password: '',
confirmPassword: '', confirmPassword: '',
passwordError: null,
confirmPasswordError: null,
} }
constructor (props) { constructor (props) {
@ -69,82 +76,37 @@ class CreatePasswordScreen extends Component {
.then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)) .then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE))
} }
renderFields () { handlePasswordChange (password) {
const { isMascara, history } = this.props const { confirmPassword } = this.state
let confirmPasswordError = null
let passwordError = null
return ( if (password && password.length < 8) {
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}> passwordError = this.context.t('passwordNotLongEnough')
<div className={classnames({ }
'first-view-main': !isMascara,
'first-view-main__mascara': isMascara, if (confirmPassword && password !== confirmPassword) {
})}> confirmPasswordError = this.context.t('passwordsDontMatch')
{isMascara && <div className="mascara-info first-view-phone-invisible"> }
<Mascot
animationEventEmitter={this.animationEventEmitter} this.setState({ password, passwordError, confirmPasswordError })
width="225" }
height="225"
/> handleConfirmPasswordChange (confirmPassword) {
<div className="info"> const { password } = this.state
MetaMask is a secure identity vault for Ethereum. let confirmPasswordError = null
</div>
<div className="info"> if (password !== confirmPassword) {
It allows you to hold ether & tokens, and interact with decentralized applications. confirmPasswordError = this.context.t('passwordsDontMatch')
</div> }
</div>}
<div className="create-password"> this.setState({ confirmPassword, confirmPasswordError })
<div className="create-password__title">
Create Password
</div>
<input
className="first-time-flow__input"
type="password"
placeholder="New Password (min 8 characters)"
onChange={e => this.setState({password: e.target.value})}
/>
<input
className="first-time-flow__input create-password__confirm-input"
type="password"
placeholder="Confirm Password"
onChange={e => this.setState({confirmPassword: e.target.value})}
/>
<button
className="first-time-flow__button"
disabled={!this.isValid()}
onClick={this.createAccount}
>
Create
</button>
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
}}
>
Import with seed phrase
</a>
{ /* }
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_ACCOUNT_ROUTE)
}}
>
Import an account
</a>
{ */ }
<Breadcrumbs total={3} currentIndex={0} />
</div>
</div>
</div>
)
} }
render () { render () {
const { history, isMascara } = this.props const { history, isMascara } = this.props
const { passwordError, confirmPasswordError } = this.state
const { t } = this.context
return ( return (
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}> <div className={classnames({ 'first-view-main-wrapper': !isMascara })}>
@ -169,17 +131,30 @@ class CreatePasswordScreen extends Component {
<div className="create-password__title"> <div className="create-password__title">
Create Password Create Password
</div> </div>
<input <TextField
className="first-time-flow__input" id="create-password"
label={t('newPassword')}
type="password" type="password"
placeholder="New Password (min 8 characters)" className="first-time-flow__input"
onChange={e => this.setState({password: e.target.value})} value={this.state.password}
onChange={event => this.handlePasswordChange(event.target.value)}
error={passwordError}
autoFocus
autoComplete="new-password"
margin="normal"
fullWidth
/> />
<input <TextField
className="first-time-flow__input create-password__confirm-input" id="confirm-password"
label={t('confirmPassword')}
type="password" type="password"
placeholder="Confirm Password" className="first-time-flow__input"
onChange={e => this.setState({confirmPassword: e.target.value})} value={this.state.confirmPassword}
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
error={confirmPasswordError}
autoComplete="confirm-password"
margin="normal"
fullWidth
/> />
<button <button
className="first-time-flow__button" className="first-time-flow__button"

@ -70,10 +70,14 @@ class ImportAccountScreen extends Component {
switch (this.state.selectedOption) { switch (this.state.selectedOption) {
case OPTIONS.JSON_FILE: case OPTIONS.JSON_FILE:
return importNewAccount('JSON File', [ jsonFile, password ]) return importNewAccount('JSON File', [ jsonFile, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(next) .then(next)
case OPTIONS.PRIVATE_KEY: case OPTIONS.PRIVATE_KEY:
default: default:
return importNewAccount('Private Key', [ privateKey ]) return importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(next) .then(next)
} }
} }

@ -1,29 +1,33 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import classnames from 'classnames'
import { import {
createNewVaultAndRestore, createNewVaultAndRestore,
hideWarning,
displayWarning,
unMarkPasswordForgotten, unMarkPasswordForgotten,
} from '../../../../ui/app/actions' } from '../../../../ui/app/actions'
import { DEFAULT_ROUTE, INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes' import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
import TextField from '../../../../ui/app/components/text-field'
class ImportSeedPhraseScreen extends Component { class ImportSeedPhraseScreen extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = { static propTypes = {
warning: PropTypes.string, warning: PropTypes.string,
createNewVaultAndRestore: PropTypes.func.isRequired, createNewVaultAndRestore: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired,
displayWarning: PropTypes.func,
leaveImportSeedScreenState: PropTypes.func, leaveImportSeedScreenState: PropTypes.func,
history: PropTypes.object, history: PropTypes.object,
isLoading: PropTypes.bool,
}; };
state = { state = {
seedPhrase: '', seedPhrase: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
seedPhraseError: null,
passwordError: null,
confirmPasswordError: null,
} }
parseSeedPhrase = (seedPhrase) => { parseSeedPhrase = (seedPhrase) => {
@ -32,39 +36,47 @@ class ImportSeedPhraseScreen extends Component {
.join(' ') .join(' ')
} }
onChange = ({ seedPhrase, password, confirmPassword }) => { handleSeedPhraseChange (seedPhrase) {
const { let seedPhraseError = null
password: prevPassword,
confirmPassword: prevConfirmPassword,
} = this.state
const { displayWarning, hideWarning } = this.props
let warning = null
if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
warning = 'Seed Phrases are 12 words long' seedPhraseError = this.context.t('seedPhraseReq')
} else if (password && password.length < 8) {
warning = 'Passwords require a mimimum length of 8'
} else if ((password || prevPassword) !== (confirmPassword || prevConfirmPassword)) {
warning = 'Confirmed password does not match'
} }
if (warning) { this.setState({ seedPhrase, seedPhraseError })
displayWarning(warning) }
} else {
hideWarning() handlePasswordChange (password) {
const { confirmPassword } = this.state
let confirmPasswordError = null
let passwordError = null
if (password && password.length < 8) {
passwordError = this.context.t('passwordNotLongEnough')
}
if (confirmPassword && password !== confirmPassword) {
confirmPasswordError = this.context.t('passwordsDontMatch')
}
this.setState({ password, passwordError, confirmPasswordError })
}
handleConfirmPasswordChange (confirmPassword) {
const { password } = this.state
let confirmPasswordError = null
if (password !== confirmPassword) {
confirmPasswordError = this.context.t('passwordsDontMatch')
} }
seedPhrase && this.setState({ seedPhrase }) this.setState({ confirmPassword, confirmPasswordError })
password && this.setState({ password })
confirmPassword && this.setState({ confirmPassword })
} }
onClick = () => { onClick = () => {
const { password, seedPhrase } = this.state const { password, seedPhrase } = this.state
const { const {
createNewVaultAndRestore, createNewVaultAndRestore,
displayWarning,
leaveImportSeedScreenState, leaveImportSeedScreenState,
history, history,
} = this.props } = this.props
@ -74,10 +86,23 @@ class ImportSeedPhraseScreen extends Component {
.then(() => history.push(INITIALIZE_NOTICE_ROUTE)) .then(() => history.push(INITIALIZE_NOTICE_ROUTE))
} }
hasError () {
const { passwordError, confirmPasswordError, seedPhraseError } = this.state
return passwordError || confirmPasswordError || seedPhraseError
}
render () { render () {
const { seedPhrase, password, confirmPassword } = this.state const {
const { warning, isLoading } = this.props seedPhrase,
const importDisabled = warning || !seedPhrase || !password || !confirmPassword || isLoading password,
confirmPassword,
seedPhraseError,
passwordError,
confirmPasswordError,
} = this.state
const { t } = this.context
const { isLoading } = this.props
const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
return ( return (
<div className="first-view-main-wrapper"> <div className="first-view-main-wrapper">
@ -103,45 +128,40 @@ class ImportSeedPhraseScreen extends Component {
<label className="import-account__input-label">Wallet Seed</label> <label className="import-account__input-label">Wallet Seed</label>
<textarea <textarea
className="import-account__secret-phrase" className="import-account__secret-phrase"
onChange={e => this.onChange({seedPhrase: e.target.value})} onChange={e => this.handleSeedPhraseChange(e.target.value)}
value={this.state.seedPhrase} value={this.state.seedPhrase}
placeholder="Separate each word with a single space" placeholder="Separate each word with a single space"
/> />
</div> </div>
<span <span className="error">
className="error" { seedPhraseError }
>
{this.props.warning}
</span> </span>
<div className="import-account__input-wrapper"> <TextField
<label className="import-account__input-label">New Password</label> id="password"
<input label={t('newPassword')}
className="first-time-flow__input" type="password"
type="password" className="first-time-flow__input"
placeholder="New Password (min 8 characters)" value={this.state.password}
onChange={e => this.onChange({password: e.target.value})} onChange={event => this.handlePasswordChange(event.target.value)}
/> error={passwordError}
</div> autoComplete="new-password"
<div className="import-account__input-wrapper"> margin="normal"
<label />
className={classnames('import-account__input-label', { <TextField
'import-account__input-label__disabled': password.length < 8, id="confirm-password"
})} label={t('confirmPassword')}
>Confirm Password</label> type="password"
<input className="first-time-flow__input"
className={classnames('first-time-flow__input', { value={this.state.confirmPassword}
'first-time-flow__input__disabled': password.length < 8, onChange={event => this.handleConfirmPasswordChange(event.target.value)}
})} error={confirmPasswordError}
type="password" autoComplete="confirm-password"
placeholder="Confirm Password" margin="normal"
onChange={e => this.onChange({confirmPassword: e.target.value})} />
disabled={password.length < 8}
/>
</div>
<button <button
className="first-time-flow__button" className="first-time-flow__button"
onClick={() => !importDisabled && this.onClick()} onClick={() => !disabled && this.onClick()}
disabled={importDisabled} disabled={disabled}
> >
Import Import
</button> </button>
@ -159,7 +179,5 @@ export default connect(
dispatch(unMarkPasswordForgotten()) dispatch(unMarkPasswordForgotten())
}, },
createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)), createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
displayWarning: (warning) => dispatch(displayWarning(warning)),
hideWarning: () => dispatch(hideWarning()),
}) })
)(ImportSeedPhraseScreen) )(ImportSeedPhraseScreen)

@ -21,6 +21,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
background: #f7861c; background: #f7861c;
flex: 0 0 auto;
} }
.alpha-warning, .alpha-warning,
@ -173,10 +174,7 @@
} }
.first-time-flow__input { .first-time-flow__input {
width: initial !important; width: 100%;
font-size: 14px !important;
line-height: 18px !important;
padding: 12px !important;
} }
.tou__body { .tou__body {
@ -247,7 +245,7 @@
} }
.create-password__confirm-input { .create-password__confirm-input {
margin-top: 15px; margin-top: 16px;
} }
.create-password__import-link { .create-password__import-link {
@ -519,10 +517,6 @@ button.backup-phrase__confirm-seed-option:hover {
margin-top: 30px; margin-top: 30px;
} }
.first-time-flow__input--error {
border: 1px solid #FF001F !important;
}
.import-account__input-error-message { .import-account__input-error-message {
margin-top: 10px; margin-top: 10px;
width: 422px; width: 422px;
@ -543,7 +537,13 @@ button.backup-phrase__confirm-seed-option:hover {
} }
.import-account__input { .import-account__input {
width: 325px !important; width: 350px;
}
@media only screen and (max-width: 575px) {
.import-account__input {
width: 100%;
}
} }
.import-account__file-input { .import-account__file-input {
@ -680,20 +680,6 @@ button.backup-phrase__confirm-seed-option:hover {
.first-time-flow__input { .first-time-flow__input {
width: 350px; width: 350px;
font-size: 18px;
line-height: 24px;
padding: 15px;
border: 1px solid #CDCDCD;
background-color: #FFFFFF;
}
.first-time-flow__input__disabled {
opacity: 0.5;
}
.first-time-flow__input::placeholder {
color: #9B9B9B;
font-weight: 200;
} }
.first-time-flow__button { .first-time-flow__button {

@ -3,6 +3,8 @@ import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import { withRouter, Switch, Route } from 'react-router-dom' import { withRouter, Switch, Route } from 'react-router-dom'
import { compose } from 'recompose' import { compose } from 'recompose'
import classnames from 'classnames'
import CreatePasswordScreen from './create-password-screen' import CreatePasswordScreen from './create-password-screen'
import UniqueImageScreen from './unique-image-screen' import UniqueImageScreen from './unique-image-screen'
import NoticeScreen from './notice-screen' import NoticeScreen from './notice-screen'
@ -33,6 +35,7 @@ class FirstTimeFlow extends Component {
isUnlocked: PropTypes.bool, isUnlocked: PropTypes.bool,
history: PropTypes.object, history: PropTypes.object,
welcomeScreenSeen: PropTypes.bool, welcomeScreenSeen: PropTypes.bool,
isPopup: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -41,23 +44,44 @@ class FirstTimeFlow extends Component {
noActiveNotices: false, noActiveNotices: false,
}; };
renderAppBar () {
const { welcomeScreenSeen } = this.props
return (
<div className="alpha-warning__container">
<h2 className={classnames({
'alpha-warning': welcomeScreenSeen,
'alpha-warning-welcome-screen': !welcomeScreenSeen,
})}
>
Please be aware that this version is still under development
</h2>
</div>
)
}
render () { render () {
const { isPopup } = this.props
return ( return (
<div className="first-time-flow"> <div className="flex-column flex-grow">
<Switch> { !isPopup && this.renderAppBar() }
<Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} /> <div className="first-time-flow">
<Route <Switch>
exact <Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} />
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE} <Route
component={ImportSeedPhraseScreen} exact
/> path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
<Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImageScreen} /> component={ImportSeedPhraseScreen}
<Route exact path={INITIALIZE_NOTICE_ROUTE} component={NoticeScreen} /> />
<Route exact path={INITIALIZE_BACKUP_PHRASE_ROUTE} component={BackupPhraseScreen} /> <Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImageScreen} />
<Route exact path={INITIALIZE_CONFIRM_SEED_ROUTE} component={ConfirmSeed} /> <Route exact path={INITIALIZE_NOTICE_ROUTE} component={NoticeScreen} />
<Route exact path={INITIALIZE_CREATE_PASSWORD_ROUTE} component={CreatePasswordScreen} /> <Route exact path={INITIALIZE_BACKUP_PHRASE_ROUTE} component={BackupPhraseScreen} />
<Route exact path={INITIALIZE_ROUTE} component={WelcomeScreen} /> <Route exact path={INITIALIZE_CONFIRM_SEED_ROUTE} component={ConfirmSeed} />
</Switch> <Route exact path={INITIALIZE_CREATE_PASSWORD_ROUTE} component={CreatePasswordScreen} />
<Route exact path={INITIALIZE_ROUTE} component={WelcomeScreen} />
</Switch>
</div>
</div> </div>
) )
} }
@ -73,6 +97,7 @@ const mapStateToProps = ({ metamask }) => {
isMascara, isMascara,
isUnlocked, isUnlocked,
welcomeScreenSeen, welcomeScreenSeen,
isPopup,
} = metamask } = metamask
return { return {
@ -84,6 +109,7 @@ const mapStateToProps = ({ metamask }) => {
forgottenPassword, forgottenPassword,
isUnlocked, isUnlocked,
welcomeScreenSeen, welcomeScreenSeen,
isPopup,
} }
} }

@ -96,6 +96,8 @@ class JsonImportSubview extends Component {
} }
this.props.importNewAccount([ fileContents, password ]) this.props.importNewAccount([ fileContents, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
} }
} }

@ -64,4 +64,6 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
const input = document.getElementById('private-key-box') const input = document.getElementById('private-key-box')
const privateKey = input.value const privateKey = input.value
this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
} }

@ -35,7 +35,6 @@ const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums')
module.exports = connect(mapStateToProps)(App) module.exports = connect(mapStateToProps)(App)
@ -409,7 +408,6 @@ App.prototype.renderDropdown = function () {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { onClick: () => {
this.props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL')) this.props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
.then(() => this.props.dispatch(actions.setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
}, },
}, 'Try Beta!'), }, 'Try Beta!'),
]) ])
@ -472,7 +470,6 @@ App.prototype.renderPrimary = function () {
onClick: () => { onClick: () => {
global.platform.openExtensionInBrowser() global.platform.openExtensionInBrowser()
props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL')) props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
.then(() => props.dispatch(actions.setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
}, },
style: { style: {
fontSize: '0.8em', fontSize: '0.8em',

22103
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -13,8 +13,10 @@
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara", "test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"test:integration:build": "gulp build:scss", "test:integration:build": "gulp build:scss",
"test:e2e": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run'", "test:e2e:chrome": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:chrome'",
"test:e2e:run": "mocha test/e2e/metamask.spec --recursive", "test:e2e:firefox": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:firefox'",
"test:e2e:run:chrome": "SELENIUM_BROWSER=chrome mocha test/e2e/chrome/metamask.spec --bail --recursive",
"test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/firefox/metamask.spec --bail --recursive",
"test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'", "test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'",
"test:screens:run": "node test/screens/new-ui.js", "test:screens:run": "node test/screens/new-ui.js",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload", "test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
@ -42,7 +44,8 @@
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.js", "version:bump": "node development/run-version-bump.js",
"generateNotice": "node notices/notice-generator.js", "generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js" "deleteNotice": "node notices/notice-delete.js",
"storybook": "start-storybook -p 6006 -c .storybook"
}, },
"browserify": { "browserify": {
"transform": [ "transform": [
@ -76,10 +79,11 @@
"classnames": "^2.2.5", "classnames": "^2.2.5",
"clone": "^2.1.1", "clone": "^2.1.1",
"copy-to-clipboard": "^3.0.8", "copy-to-clipboard": "^3.0.8",
"css-loader": "^0.28.11",
"currency-formatter": "^1.4.2", "currency-formatter": "^1.4.2",
"debounce": "^1.0.0", "debounce": "^1.0.0",
"debounce-stream": "^2.0.0", "debounce-stream": "^2.0.0",
"deep-extend": "^0.5.0", "deep-extend": "^0.5.1",
"detect-node": "^2.0.3", "detect-node": "^2.0.3",
"disc": "^1.3.2", "disc": "^1.3.2",
"dnode": "^1.2.2", "dnode": "^1.2.2",
@ -87,7 +91,6 @@
"ensnare": "^1.0.0", "ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.3.0",
"eth-contract-metadata": "^1.1.5", "eth-contract-metadata": "^1.1.5",
"eth-hd-keyring": "^1.2.1", "eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.6", "eth-json-rpc-filters": "^1.2.6",
@ -111,6 +114,7 @@
"extensionizer": "^1.0.0", "extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4", "fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"file-loader": "^1.1.11",
"fuse.js": "^3.2.0", "fuse.js": "^3.2.0",
"gulp": "github:gulpjs/gulp#4.0", "gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^5.0.0", "gulp-autoprefixer": "^5.0.0",
@ -132,6 +136,7 @@
"lodash.shuffle": "^4.2.0", "lodash.shuffle": "^4.2.0",
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1", "loglevel": "^1.4.1",
"material-ui": "1.0.0-beta.44",
"metamascara": "^2.0.0", "metamascara": "^2.0.0",
"metamask-logo": "^2.1.4", "metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
@ -191,6 +196,9 @@
}, },
"devDependencies": { "devDependencies": {
"@sentry/cli": "^1.30.3", "@sentry/cli": "^1.30.3",
"@storybook/addon-info": "^3.4.2",
"@storybook/addon-knobs": "^3.4.2",
"@storybook/react": "^3.4.2",
"babel-core": "^6.24.1", "babel-core": "^6.24.1",
"babel-eslint": "^8.0.0", "babel-eslint": "^8.0.0",
"babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1",
@ -204,10 +212,11 @@
"brfs": "^1.4.3", "brfs": "^1.4.3",
"browserify": "^16.1.1", "browserify": "^16.1.1",
"chai": "^4.1.0", "chai": "^4.1.0",
"chromedriver": "^2.34.1", "chromedriver": "2.36.0",
"compression": "^1.7.1", "compression": "^1.7.1",
"coveralls": "^3.0.0", "coveralls": "^3.0.0",
"cross-env": "^5.1.4", "cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"deep-freeze-strict": "^1.1.1", "deep-freeze-strict": "^1.1.1",
"del": "^3.0.0", "del": "^3.0.0",
"envify": "^4.0.0", "envify": "^4.0.0",
@ -218,9 +227,11 @@
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.6.0", "eth-json-rpc-middleware": "^1.6.0",
"file-loader": "^1.1.11",
"fs-promise": "^2.0.3", "fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0", "ganache-cli": "^6.1.0",
"ganache-core": "^2.1.0", "ganache-core": "^2.1.0",
"geckodriver": "^1.11.0",
"gifencoder": "^1.1.0", "gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed", "gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"gulp-babel": "^7.0.0", "gulp-babel": "^7.0.0",
@ -255,8 +266,10 @@
"mocha-sinon": "^2.0.0", "mocha-sinon": "^2.0.0",
"nock": "^9.0.14", "nock": "^9.0.14",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"nsp": "^3.2.1",
"nyc": "^11.0.3", "nyc": "^11.0.3",
"open": "0.0.5", "open": "0.0.5",
"path": "^0.12.7",
"png-file-stream": "^1.0.0", "png-file-stream": "^1.0.0",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"qs": "^6.2.0", "qs": "^6.2.0",
@ -266,11 +279,14 @@
"react-test-renderer": "^15.6.2", "react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"redux-test-utils": "^0.2.2", "redux-test-utils": "^0.2.2",
"resolve-url-loader": "^2.3.0",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sass-loader": "^7.0.1",
"selenium-webdriver": "^3.5.0", "selenium-webdriver": "^3.5.0",
"shell-parallel": "^1.0.3", "shell-parallel": "^1.0.3",
"sinon": "^5.0.0", "sinon": "^5.0.0",
"source-map": "^0.7.2", "source-map": "^0.7.2",
"style-loader": "^0.21.0",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1", "tape": "^4.5.1",
"testem": "^2.0.0", "testem": "^2.0.0",

@ -0,0 +1,314 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const assert = require('assert')
const pify = require('pify')
const webdriver = require('selenium-webdriver')
const until = require('selenium-webdriver/lib/until')
const By = webdriver.By
const { delay, buildChromeWebDriver } = require('../func')
describe('Metamask popup page', function () {
let driver, accountAddress, tokenAddress, extensionId
this.timeout(0)
before(async function () {
const extPath = path.resolve('dist/chrome')
driver = buildChromeWebDriver(extPath)
await driver.get('chrome://extensions')
await delay(500)
})
afterEach(async function () {
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('Setup', function () {
it('switches to Chrome extensions list', async function () {
const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
await delay(300)
})
it(`selects MetaMask's extension id and opens it in the current tab`, async function () {
extensionId = await getExtensionId()
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(500)
})
it('sets provider type to localhost', async function () {
await driver.wait(until.elementLocated(By.css('#app-content')), 300)
await setProviderType('localhost')
})
})
describe('Account Creation', () => {
it('matches MetaMask title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('shows privacy notice', async () => {
await driver.wait(async () => {
const privacyHeader = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > h3')).getText()
assert.equal(privacyHeader, 'PRIVACY NOTICE', 'shows privacy notice')
return privacyHeader === 'PRIVACY NOTICE'
}, 300)
await driver.findElement(By.css('button')).click()
})
it('show terms of use', async () => {
await driver.wait(async () => {
const terms = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > h3')).getText()
assert.equal(terms, 'TERMS OF USE', 'shows terms of use')
return terms === 'TERMS OF USE'
})
})
it('checks if the TOU button is disabled', async () => {
const button = await driver.findElement(By.css('button')).isEnabled()
assert.equal(button, false, 'disabled continue button')
const element = await driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
})
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => {
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button'))
const buttonEnabled = await button.isEnabled()
assert.equal(buttonEnabled, true, 'enabled continue button')
await button.click()
})
it('accepts password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))
const button = await driver.findElements(By.css('button'))
await passwordBox.sendKeys('123456789')
await passwordBoxConfirm.sendKeys('123456789')
await button[0].click()
await delay(500)
})
it('shows value was created and seed phrase', async () => {
await delay(300)
const seedPhrase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
const continueAfterSeedPhrase = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > button:nth-child(4)'))
assert.equal(await continueAfterSeedPhrase.getText(), `I'VE COPIED IT SOMEWHERE SAFE`)
await continueAfterSeedPhrase.click()
await delay(300)
})
it('shows account address', async function () {
accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText()
})
it('logs out of the vault', async () => {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(500)
const logoutButton = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
assert.equal(await logoutButton.getText(), 'Log Out')
await logoutButton.click()
})
it('accepts account password after lock', async () => {
await delay(500)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.css('button')).click()
await delay(500)
})
it('shows QR code option', async () => {
await delay(300)
await driver.findElement(By.css('.fa-ellipsis-h')).click()
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
await delay(300)
})
it('checks QR code address is the same as account details address', async () => {
const QRaccountAddress = await driver.findElement(By.css('.ellip-address')).getText()
assert.equal(accountAddress.toLowerCase(), QRaccountAddress)
await driver.findElement(By.css('.fa-arrow-left')).click()
await delay(500)
})
})
describe('Import Ganache seed phrase', function () {
it('logs out', async function () {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(200)
const logOut = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
assert.equal(await logOut.getText(), 'Log Out')
await logOut.click()
await delay(300)
})
it('restores from seed phrase', async function () {
const restoreSeedLink = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div.flex-row.flex-center.flex-grow > p'))
assert.equal(await restoreSeedLink.getText(), 'Restore from seed phrase')
await restoreSeedLink.click()
await delay(100)
})
it('adds seed phrase', async function () {
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const seedTextArea = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > textarea'))
await seedTextArea.sendKeys(testSeedPhrase)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.id('password-box-confirm')).sendKeys('123456789')
await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > button:nth-child(2)')).click()
await delay(500)
})
it('balance renders', async function () {
await delay(200)
const balance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'))
assert.equal(await balance.getText(), '100.000')
await delay(200)
})
it('sends transaction', async function () {
const sendButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)'))
assert.equal(await sendButton.getText(), 'SEND')
await sendButton.click()
await delay(200)
})
it('adds recipient address and amount', async function () {
const sendTranscationScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)')).getText()
assert.equal(sendTranscationScreen, 'SEND TRANSACTION')
const inputAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input'))
const inputAmmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmmount.sendKeys('10')
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > button')).click()
await delay(300)
})
it('confirms transaction', async function () {
await delay(300)
await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')).click()
await delay(500)
})
it('finds the transaction in the transactions list', async function () {
const tranasactionAmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'))
assert.equal(await tranasactionAmount.getText(), '10.0')
})
})
describe('Token Factory', function () {
it('navigates to token factory', async function () {
await driver.get('http://tokenfactory.surge.sh/')
})
it('navigates to create token contract link', async function () {
const createToken = await driver.findElement(By.css('#bs-example-navbar-collapse-1 > ul > li:nth-child(3) > a'))
await createToken.click()
})
it('adds input for token', async function () {
const totalSupply = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(5) > input'))
const tokenName = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(6) > input'))
const tokenDecimal = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(7) > input'))
const tokenSymbol = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(8) > input'))
const createToken = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > button'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
await createToken.click()
await delay(1000)
})
it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[windowHandles.length - 1])
const metamaskSubmit = await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input'))
await metamaskSubmit.click()
await delay(1000)
})
it('switches back to Token Factory to grab the token contract address', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[0])
const tokenContactAddress = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await delay(500)
})
it('navigates back to MetaMask popup in the tab', async function () {
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(700)
})
})
describe('Add Token', function () {
it('switches to the add token screen', async function () {
const tokensTab = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div > div.inactiveForm.pointer'))
assert.equal(await tokensTab.getText(), 'TOKENS')
await tokensTab.click()
await delay(300)
})
it('navigates to the add token screen', async function () {
const addTokenButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div.full-flex-height > div > button'))
assert.equal(await addTokenButton.getText(), 'ADD TOKEN')
await addTokenButton.click()
})
it('checks add token screen rendered', async function () {
const addTokenScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.section-title.flex-row.flex-center > h2'))
assert.equal(await addTokenScreen.getText(), 'ADD TOKEN')
})
it('adds token parameters', async function () {
const tokenContractAddress = await driver.findElement(By.css('#token-address'))
await tokenContractAddress.sendKeys(tokenAddress)
await delay(300)
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > button')).click()
await delay(100)
})
it('checks the token balance', async function () {
const tokenBalance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > ol > li:nth-child(2) > h3'))
assert.equal(await tokenBalance.getText(), '100 TST')
})
})
async function getExtensionId () {
const extension = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")')
return extension
}
async function setProviderType (type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure (test) {
const artifactDir = `./test-artifacts/chrome/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
})

@ -0,0 +1,323 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const assert = require('assert')
const pify = require('pify')
const webdriver = require('selenium-webdriver')
const Command = require('selenium-webdriver/lib/command').Command
const By = webdriver.By
const { delay, buildFirefoxWebdriver } = require('../func')
describe('', function () {
let driver, accountAddress, tokenAddress, extensionId
this.timeout(0)
before(async function () {
const extPath = path.resolve('dist/firefox')
driver = buildFirefoxWebdriver()
installWebExt(driver, extPath)
await delay(700)
})
afterEach(async function () {
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('Setup', function () {
it('switches to Firefox addon list', async function () {
await driver.get('about:debugging#addons')
await delay(1000)
})
it(`selects MetaMask's extension id and opens it in the current tab`, async function () {
const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText()
await driver.get(`moz-extension://${extensionId}/popup.html`)
await delay(500)
})
it('sets provider type to localhost', async function () {
await setProviderType('localhost')
await delay(300)
})
})
describe('Account Creation', () => {
it('matches MetaMask title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('shows privacy notice', async () => {
await delay(300)
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
await driver.findElement(By.css('button')).click()
await delay(300)
})
it('show terms of use', async () => {
await delay(300)
const terms = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(terms, 'TERMS OF USE', 'shows terms of use')
await delay(300)
})
it('checks if the TOU button is disabled', async () => {
const button = await driver.findElement(By.css('button')).isEnabled()
assert.equal(button, false, 'disabled continue button')
const element = await driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
})
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => {
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button'))
await delay(300)
const buttonEnabled = await button.isEnabled()
assert.equal(buttonEnabled, true, 'enabled continue button')
await delay(200)
await button.click()
})
it('accepts password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))
const button = await driver.findElements(By.css('button'))
await passwordBox.sendKeys('123456789')
await passwordBoxConfirm.sendKeys('123456789')
await button[0].click()
await delay(500)
})
it('shows value was created and seed phrase', async () => {
await delay(300)
const seedPhrase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
const continueAfterSeedPhrase = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > button:nth-child(4)'))
assert.equal(await continueAfterSeedPhrase.getText(), `I'VE COPIED IT SOMEWHERE SAFE`)
await continueAfterSeedPhrase.click()
await delay(300)
})
it('shows account address', async function () {
accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText()
})
it('logs out of the vault', async () => {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(500)
const logoutButton = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
assert.equal(await logoutButton.getText(), 'Log Out')
await logoutButton.click()
})
it('accepts account password after lock', async () => {
await delay(500)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.id('password-box')).sendKeys(webdriver.Key.ENTER)
await delay(500)
})
it('shows QR code option', async () => {
await delay(300)
await driver.findElement(By.css('.fa-ellipsis-h')).click()
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
await delay(300)
})
it('checks QR code address is the same as account details address', async () => {
const QRaccountAddress = await driver.findElement(By.css('.ellip-address')).getText()
assert.equal(accountAddress.toLowerCase(), QRaccountAddress)
await driver.findElement(By.css('.fa-arrow-left')).click()
await delay(500)
})
})
describe('Import Ganache seed phrase', function () {
it('logs out', async function () {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(200)
const logOut = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
assert.equal(await logOut.getText(), 'Log Out')
await logOut.click()
await delay(300)
})
it('restores from seed phrase', async function () {
const restoreSeedLink = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div.flex-row.flex-center.flex-grow > p'))
assert.equal(await restoreSeedLink.getText(), 'Restore from seed phrase')
await restoreSeedLink.click()
await delay(100)
})
it('adds seed phrase', async function () {
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const seedTextArea = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > textarea'))
await seedTextArea.sendKeys(testSeedPhrase)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.id('password-box-confirm')).sendKeys('123456789')
await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > button:nth-child(2)')).click()
await delay(500)
})
it('balance renders', async function () {
await delay(200)
const balance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'))
assert.equal(await balance.getText(), '100.000')
await delay(200)
})
it('sends transaction', async function () {
const sendButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)'))
assert.equal(await sendButton.getText(), 'SEND')
await sendButton.click()
await delay(200)
})
it('adds recipient address and amount', async function () {
const sendTranscationScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)')).getText()
assert.equal(sendTranscationScreen, 'SEND TRANSACTION')
const inputAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input'))
const inputAmmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmmount.sendKeys('10')
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > button')).click()
await delay(300)
})
it('confirms transaction', async function () {
await delay(300)
await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')).click()
await delay(500)
})
it('finds the transaction in the transactions list', async function () {
const tranasactionAmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'))
assert.equal(await tranasactionAmount.getText(), '10.0')
})
})
describe('Token Factory', function () {
it('navigates to token factory', async function () {
await driver.get('http://tokenfactory.surge.sh/')
})
it('navigates to create token contract link', async function () {
const createToken = await driver.findElement(By.css('#bs-example-navbar-collapse-1 > ul > li:nth-child(3) > a'))
await createToken.click()
})
it('adds input for token', async function () {
const totalSupply = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(5) > input'))
const tokenName = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(6) > input'))
const tokenDecimal = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(7) > input'))
const tokenSymbol = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(8) > input'))
const createToken = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > button'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
await createToken.click()
await delay(1000)
})
// There is an issue with blank confirmation window, but the button is still there and the driver is able to clicked (?.?)
it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[windowHandles.length - 1])
const metamaskSubmit = await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input'))
await metamaskSubmit.click()
await delay(1000)
})
it('switches back to Token Factory to grab the token contract address', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[0])
const tokenContactAddress = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await delay(500)
})
it('navigates back to MetaMask popup in the tab', async function () {
await driver.get(`moz-extension://${extensionId}/popup.html`)
await delay(700)
})
})
describe('Add Token', function () {
it('switches to the add token screen', async function () {
const tokensTab = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div > div.inactiveForm.pointer'))
assert.equal(await tokensTab.getText(), 'TOKENS')
await tokensTab.click()
await delay(300)
})
it('navigates to the add token screen', async function () {
const addTokenButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div.full-flex-height > div > button'))
assert.equal(await addTokenButton.getText(), 'ADD TOKEN')
await addTokenButton.click()
})
it('checks add token screen rendered', async function () {
const addTokenScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.section-title.flex-row.flex-center > h2'))
assert.equal(await addTokenScreen.getText(), 'ADD TOKEN')
})
it('adds token parameters', async function () {
const tokenContractAddress = await driver.findElement(By.css('#token-address'))
await tokenContractAddress.sendKeys(tokenAddress)
await delay(300)
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > button')).click()
await delay(100)
})
it('checks the token balance', async function () {
const tokenBalance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > ol > li:nth-child(2) > h3'))
assert.equal(await tokenBalance.getText(), '100 TST')
})
})
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/firefox/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
})
async function installWebExt (driver, extension) {
const cmd = await new Command('moz-install-web-ext')
.setParameter('path', path.resolve(extension))
.setParameter('temporary', true)
await driver.getExecutor()
.defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install')
return await driver.schedule(cmd, 'installWebExt(' + extension + ')')
}

@ -1,4 +1,5 @@
require('chromedriver') require('chromedriver')
require('geckodriver')
const webdriver = require('selenium-webdriver') const webdriver = require('selenium-webdriver')
exports.delay = function delay (time) { exports.delay = function delay (time) {
@ -6,13 +7,16 @@ exports.delay = function delay (time) {
} }
exports.buildWebDriver = function buildWebDriver (extPath) { exports.buildChromeWebDriver = function buildChromeWebDriver (extPath) {
return new webdriver.Builder() return new webdriver.Builder()
.withCapabilities({ .withCapabilities({
chromeOptions: { chromeOptions: {
args: [`load-extension=${extPath}`], args: [`load-extension=${extPath}`],
}, },
}) })
.forBrowser('chrome')
.build() .build()
} }
exports.buildFirefoxWebdriver = function buildFirefoxWebdriver (extPath) {
return new webdriver.Builder().build()
}

@ -1,145 +0,0 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const assert = require('assert')
const pify = require('pify')
const webdriver = require('selenium-webdriver')
const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
describe('Metamask popup page', function () {
let driver
this.seedPhase
this.accountAddress
this.timeout(0)
before(async function () {
const extPath = path.resolve('dist/chrome')
driver = buildWebDriver(extPath)
await driver.get('chrome://extensions-frame')
const elems = await driver.findElements(By.css('.extension-list-item-wrapper'))
const extensionId = await elems[1].getAttribute('id')
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(500)
})
afterEach(async function () {
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('#onboarding', () => {
it('should open Metamask.io', async function () {
const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
await delay(300)
await setProviderType('localhost')
await delay(300)
})
it('should match title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('should show privacy notice', async () => {
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
driver.findElement(By.css('button')).click()
await delay(300)
})
it('should show terms of use', async () => {
await delay(300)
const terms = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(terms, 'TERMS OF USE', 'shows terms of use')
await delay(300)
})
it('should be unable to continue without scolling throught the terms of use', async () => {
const button = await driver.findElement(By.css('button')).isEnabled()
assert.equal(button, false, 'disabled continue button')
const element = driver.findElement(By.linkText(
'Attributions'
))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
})
it('should be able to continue when scrolled to the bottom of terms of use', async () => {
const button = await driver.findElement(By.css('button'))
const buttonEnabled = await button.isEnabled()
await delay(500)
assert.equal(buttonEnabled, true, 'enabled continue button')
await button.click()
await delay(300)
})
it('should accept password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))
const button = driver.findElement(By.css('button'))
passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789')
await delay(500)
await button.click()
})
it('should show value was created and seed phrase', async () => {
await delay(700)
this.seedPhase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
const continueAfterSeedPhrase = await driver.findElement(By.css('button'))
await continueAfterSeedPhrase.click()
await delay(300)
})
it('should show lock account', async () => {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(500)
await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')).click()
})
it('should accept account password after lock', async () => {
await delay(500)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.css('button')).click()
await delay(500)
})
it('should show QR code option', async () => {
await delay(300)
await driver.findElement(By.css('.fa-ellipsis-h')).click()
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
await delay(300)
})
it('should show the account address', async () => {
this.accountAddress = await driver.findElement(By.css('.ellip-address')).getText()
await driver.findElement(By.css('.fa-arrow-left')).click()
await delay(500)
})
})
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
})

@ -1,5 +1,4 @@
const PASSWORD = 'password123' const PASSWORD = 'password123'
const reactTriggerChange = require('react-trigger-change')
const { const {
timeout, timeout,
findAsync, findAsync,
@ -11,6 +10,11 @@ async function runFirstTimeUsageTest (assert, done) {
const app = await queryAsync($, '#app-content') const app = await queryAsync($, '#app-content')
// Used to set values on TextField input component
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set
await skipNotices(app) await skipNotices(app)
const welcomeButton = (await findAsync(app, '.welcome-screen__button'))[0] const welcomeButton = (await findAsync(app, '.welcome-screen__button'))[0]
@ -21,12 +25,14 @@ async function runFirstTimeUsageTest (assert, done) {
assert.equal(title, 'Create Password', 'create password screen') assert.equal(title, 'Create Password', 'create password screen')
// enter password // enter password
const pwBox = (await findAsync(app, '.first-time-flow__input'))[0] const pwBox = (await findAsync(app, '#create-password'))[0]
const confBox = (await findAsync(app, '.first-time-flow__input'))[1] const confBox = (await findAsync(app, '#confirm-password'))[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD nativeInputValueSetter.call(pwBox, PASSWORD)
reactTriggerChange(pwBox) pwBox.dispatchEvent(new Event('input', { bubbles: true}))
reactTriggerChange(confBox)
nativeInputValueSetter.call(confBox, PASSWORD)
confBox.dispatchEvent(new Event('input', { bubbles: true}))
// Create Password // Create Password
const createButton = (await findAsync(app, 'button.first-time-flow__button'))[0] const createButton = (await findAsync(app, 'button.first-time-flow__button'))[0]
@ -71,10 +77,16 @@ async function runFirstTimeUsageTest (assert, done) {
assert.ok(lock, 'Lock menu item found') assert.ok(lock, 'Lock menu item found')
lock.click() lock.click()
const pwBox2 = (await findAsync(app, '#password-box'))[0] await timeout(1000)
pwBox2.value = PASSWORD
const pwBox2 = (await findAsync(app, '#password'))[0]
pwBox2.focus()
await timeout(1000)
nativeInputValueSetter.call(pwBox2, PASSWORD)
pwBox2.dispatchEvent(new Event('input', { bubbles: true}))
const createButton2 = (await findAsync(app, 'button.primary'))[0] const createButton2 = (await findAsync(app, 'button[type="submit"]'))[0]
createButton2.click() createButton2.click()
const detail2 = (await findAsync(app, '.wallet-view'))[0] const detail2 = (await findAsync(app, '.wallet-view'))[0]

@ -21,7 +21,7 @@ async function runTxListItemsTest(assert, done) {
selectState.val('tx list items') selectState.val('tx list items')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
const metamaskLogo = await queryAsync($, '.left-menu-wrapper') const metamaskLogo = await queryAsync($, '.app-header__logo-container')
assert.ok(metamaskLogo[0], 'metamask logo present') assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click() metamaskLogo[0].click()

@ -39,8 +39,7 @@ async function captureAllScreens() {
const extPath = path.resolve('dist/chrome') const extPath = path.resolve('dist/chrome')
driver = buildWebDriver(extPath) driver = buildWebDriver(extPath)
await driver.get('chrome://extensions-frame') await driver.get('chrome://extensions-frame')
const elems = await driver.findElements(By.css('.extension-list-item-wrapper')) const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("#container > div.items-container > extensions-item:nth-child(2)").getAttribute("id")')
const extensionId = await elems[1].getAttribute('id')
await driver.get(`chrome-extension://${extensionId}/home.html`) await driver.get(`chrome-extension://${extensionId}/home.html`)
await delay(500) await delay(500)
tabs = await driver.getAllWindowHandles() tabs = await driver.getAllWindowHandles()
@ -75,8 +74,8 @@ async function captureAllScreens() {
await driver.findElement(By.css('button')).click() await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password') await captureLanguageScreenShots('create password')
const passwordBox = await driver.findElement(By.css('input[type=password]:nth-of-type(1)')) const passwordBox = await driver.findElement(By.css('input#create-password'))
const passwordBoxConfirm = await driver.findElement(By.css('input[type=password]:nth-of-type(2)')) const passwordBoxConfirm = await driver.findElement(By.css('input#confirm-password'))
passwordBox.sendKeys('123456789') passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789') passwordBoxConfirm.sendKeys('123456789')
await delay(500) await delay(500)

@ -3,17 +3,15 @@ const nock = require('nock')
const NetworkController = require('../../app/scripts/controllers/network') const NetworkController = require('../../app/scripts/controllers/network')
const { const {
getNetworkDisplayName, getNetworkDisplayName,
getNetworkEndpoints,
} = require('../../app/scripts/controllers/network/util') } = require('../../app/scripts/controllers/network/util')
const { createTestProviderTools } = require('../stub/provider') const { createTestProviderTools } = require('../stub/provider')
const providerResultStub = {} const providerResultStub = {}
const provider = createTestProviderTools({ scaffold: providerResultStub }).provider
describe('# Network Controller', function () { describe('# Network Controller', function () {
let networkController let networkController
const noop = () => {} const noop = () => {}
const networkControllerProviderInit = { const networkControllerProviderConfig = {
getAccounts: noop, getAccounts: noop,
} }
@ -24,11 +22,9 @@ describe('# Network Controller', function () {
.post('/metamask') .post('/metamask')
.reply(200) .reply(200)
networkController = new NetworkController({ networkController = new NetworkController()
provider,
})
networkController.initializeProvider(networkControllerProviderInit, provider) networkController.initializeProvider(networkControllerProviderConfig)
}) })
afterEach(function () { afterEach(function () {
@ -38,7 +34,7 @@ describe('# Network Controller', function () {
describe('network', function () { describe('network', function () {
describe('#provider', function () { describe('#provider', function () {
it('provider should be updatable without reassignment', function () { it('provider should be updatable without reassignment', function () {
networkController.initializeProvider(networkControllerProviderInit, provider) networkController.initializeProvider(networkControllerProviderConfig)
const proxy = networkController._proxy const proxy = networkController._proxy
proxy.setTarget({ test: true, on: () => {} }) proxy.setTarget({ test: true, on: () => {} })
assert.ok(proxy.test) assert.ok(proxy.test)
@ -59,12 +55,6 @@ describe('# Network Controller', function () {
}) })
}) })
describe('#getRpcAddressForType', function () {
it('should return the right rpc address', function () {
const rpcTarget = networkController.getRpcAddressForType('mainnet')
assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress')
})
})
describe('#setProviderType', function () { describe('#setProviderType', function () {
it('should update provider.type', function () { it('should update provider.type', function () {
networkController.setProviderType('mainnet') networkController.setProviderType('mainnet')
@ -76,16 +66,11 @@ describe('# Network Controller', function () {
const loading = networkController.isNetworkLoading() const loading = networkController.isNetworkLoading()
assert.ok(loading, 'network is loading') assert.ok(loading, 'network is loading')
}) })
it('should set the right rpcTarget', function () {
networkController.setProviderType('mainnet')
const rpcTarget = networkController.getProviderConfig().rpcTarget
assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress')
})
}) })
}) })
}) })
describe('# Network utils', () => { describe('Network utils', () => {
it('getNetworkDisplayName should return the correct network name', () => { it('getNetworkDisplayName should return the correct network name', () => {
const tests = [ const tests = [
{ {
@ -114,9 +99,4 @@ describe('# Network utils', () => {
tests.forEach(({ input, expected }) => assert.equal(getNetworkDisplayName(input), expected)) tests.forEach(({ input, expected }) => assert.equal(getNetworkDisplayName(input), expected))
}) })
it('getNetworkEndpoints should return the correct endpoints', () => {
assert.equal(getNetworkEndpoints('networkBeta').ropsten, 'https://ropsten.infura.io/metamask2')
assert.equal(getNetworkEndpoints('network').rinkeby, 'https://rinkeby.infura.io/metamask')
})
}) })

@ -40,6 +40,36 @@ describe('Transaction Controller', function () {
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
}) })
describe('#isNonceTaken', function () {
it('should return true', function (done) {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
])
txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}})
.then((isNonceTaken) => {
assert(isNonceTaken)
done()
}).catch(done)
})
it('should return false', function (done) {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
])
txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}})
.then((isNonceTaken) => {
assert(!isNonceTaken)
done()
}).catch(done)
})
})
describe('#getState', function () { describe('#getState', function () {
it('should return a state object with the right keys and datat types', function () { it('should return a state object with the right keys and datat types', function () {
const exposedState = txController.getState() const exposedState = txController.getState()

@ -1,26 +1,129 @@
const assert = require('assert') const assert = require('assert')
const clone = require('clone')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json')
describe('deepCloneFromTxMeta', function () { describe ('Transaction state history helper', function () {
it('should clone deep', function () {
const input = { describe('#snapshotFromTxMeta', function () {
foo: { it('should clone deep', function () {
bar: { const input = {
bam: 'baz' foo: {
bar: {
bam: 'baz'
}
} }
} }
} const output = txStateHistoryHelper.snapshotFromTxMeta(input)
const output = txStateHistoryHelper.snapshotFromTxMeta(input) assert('foo' in output, 'has a foo key')
assert('foo' in output, 'has a foo key') assert('bar' in output.foo, 'has a bar key')
assert('bar' in output.foo, 'has a bar key') assert('bam' in output.foo.bar, 'has a bar key')
assert('bam' in output.foo.bar, 'has a bar key') assert.equal(output.foo.bar.bam, 'baz', 'has a baz value')
assert.equal(output.foo.bar.bam, 'baz', 'has a baz value') })
it('should remove the history key', function () {
const input = { foo: 'bar', history: 'remembered' }
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
assert(typeof output.history, 'undefined', 'should remove history')
})
}) })
it('should remove the history key', function () { describe('#migrateFromSnapshotsToDiffs', function () {
const input = { foo: 'bar', history: 'remembered' } it('migrates history to diffs and can recover original values', function () {
const output = txStateHistoryHelper.snapshotFromTxMeta(input) testVault.data.TransactionController.transactions.forEach((tx, index) => {
assert(typeof output.history, 'undefined', 'should remove history') const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history)
newHistory.forEach((newEntry, index) => {
if (index === 0) {
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj')
} else {
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj')
}
const oldEntry = tx.history[index]
const historySubset = newHistory.slice(0, index + 1)
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset)
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs')
})
})
})
})
describe('#replayHistory', function () {
it('replaying history does not mutate the original obj', function () {
const initialState = { test: true, message: 'hello', value: 1 }
const diff1 = [{
"op": "replace",
"path": "/message",
"value": "haay",
}]
const diff2 = [{
"op": "replace",
"path": "/value",
"value": 2,
}]
const history = [initialState, diff1, diff2]
const beforeStateSnapshot = JSON.stringify(initialState)
const latestState = txStateHistoryHelper.replayHistory(history)
const afterStateSnapshot = JSON.stringify(initialState)
assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state')
assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run')
})
})
describe('#generateHistoryEntry', function () {
function generateHistoryEntryTest(note) {
const prevState = {
someValue: 'value 1',
foo: {
bar: {
bam: 'baz'
}
}
}
const nextState = {
newPropRoot: 'new property - root',
someValue: 'value 2',
foo: {
newPropFirstLevel: 'new property - first level',
bar: {
bam: 'baz'
}
}
}
const before = new Date().getTime()
const result = txStateHistoryHelper.generateHistoryEntry(prevState, nextState, note)
const after = new Date().getTime()
assert.ok(Array.isArray(result))
assert.equal(result.length, 3)
const expectedEntry1 = { op: 'add', path: '/foo/newPropFirstLevel', value: 'new property - first level' }
assert.equal(result[0].op, expectedEntry1.op)
assert.equal(result[0].path, expectedEntry1.path)
assert.equal(result[0].value, expectedEntry1.value)
assert.equal(result[0].value, expectedEntry1.value)
if (note)
assert.equal(result[0].note, note)
assert.ok(result[0].timestamp >= before && result[0].timestamp <= after)
const expectedEntry2 = { op: 'replace', path: '/someValue', value: 'value 2' }
assert.deepEqual(result[1], expectedEntry2)
const expectedEntry3 = { op: 'add', path: '/newPropRoot', value: 'new property - root' }
assert.deepEqual(result[2], expectedEntry3)
}
it('should generate history entries', function () {
generateHistoryEntryTest()
})
it('should add note to first entry', function () {
generateHistoryEntryTest('custom note')
})
}) })
}) })

@ -1,46 +0,0 @@
const assert = require('assert')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json')
describe('tx-state-history-helper', function () {
it('migrates history to diffs and can recover original values', function () {
testVault.data.TransactionController.transactions.forEach((tx, index) => {
const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history)
newHistory.forEach((newEntry, index) => {
if (index === 0) {
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj')
} else {
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj')
}
const oldEntry = tx.history[index]
const historySubset = newHistory.slice(0, index + 1)
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset)
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs')
})
})
})
it('replaying history does not mutate the original obj', function () {
const initialState = { test: true, message: 'hello', value: 1 }
const diff1 = [{
"op": "replace",
"path": "/message",
"value": "haay",
}]
const diff2 = [{
"op": "replace",
"path": "/value",
"value": 2,
}]
const history = [initialState, diff1, diff2]
const beforeStateSnapshot = JSON.stringify(initialState)
const latestState = txStateHistoryHelper.replayHistory(history)
const afterStateSnapshot = JSON.stringify(initialState)
assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state')
assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run')
})
})

@ -176,14 +176,21 @@ describe('TransactionStateManager', function () {
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state') assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
// modify value and updateTx // modify value and updateTx
updatedTx.txParams.gasPrice = desiredGasPrice updatedTx.txParams.gasPrice = desiredGasPrice
const before = new Date().getTime()
txStateManager.updateTx(updatedTx) txStateManager.updateTx(updatedTx)
const after = new Date().getTime()
// check updated value // check updated value
const result = txStateManager.getTx('1') const result = txStateManager.getTx('1')
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated') assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
// validate history was updated // validate history was updated
assert.equal(result.history.length, 2, 'two history items (initial + diff)') assert.equal(result.history.length, 2, 'two history items (initial + diff)')
assert.equal(result.history[1].length, 1, 'two history state items (initial + diff)')
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice } const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)') assert.deepEqual(result.history[1][0].op, expectedEntry.op, 'two history items (initial + diff) operation')
assert.deepEqual(result.history[1][0].path, expectedEntry.path, 'two history items (initial + diff) path')
assert.deepEqual(result.history[1][0].value, expectedEntry.value, 'two history items (initial + diff) value')
assert.ok(result.history[1][0].timestamp >= before && result.history[1][0].timestamp <= after)
}) })
}) })

@ -1,4 +1,5 @@
const abi = require('human-standard-token-abi') const abi = require('human-standard-token-abi')
const pify = require('pify')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
const { getTokenAddressFromTokenObject } = require('./util') const { getTokenAddressFromTokenObject } = require('./util')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
@ -270,7 +271,6 @@ var actions = {
SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
// Network // Network
setNetworkEndpoints,
updateNetworkEndpointType, updateNetworkEndpointType,
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE', UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
@ -316,6 +316,7 @@ function tryUnlockMetamask (password) {
background.verifySeedPhrase(err => { background.verifySeedPhrase(err => {
if (err) { if (err) {
dispatch(actions.displayWarning(err.message)) dispatch(actions.displayWarning(err.message))
return reject(err)
} }
resolve() resolve()
@ -329,6 +330,7 @@ function tryUnlockMetamask (password) {
.catch(err => { .catch(err => {
dispatch(actions.unlockFailed(err.message)) dispatch(actions.unlockFailed(err.message))
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
return Promise.reject(err)
}) })
} }
} }
@ -523,31 +525,26 @@ function addNewKeyring (type, opts) {
} }
function importNewAccount (strategy, args) { function importNewAccount (strategy, args) {
return (dispatch) => { return async (dispatch) => {
dispatch(actions.showLoadingIndication('This may take a while, be patient.')) let newState
log.debug(`background.importAccountWithStrategy`) dispatch(actions.showLoadingIndication('This may take a while, please be patient.'))
return new Promise((resolve, reject) => { try {
background.importAccountWithStrategy(strategy, args, (err) => { log.debug(`background.importAccountWithStrategy`)
if (err) { await pify(background.importAccountWithStrategy).call(background, strategy, args)
dispatch(actions.displayWarning(err.message)) log.debug(`background.getState`)
return reject(err) newState = await pify(background.getState).call(background)
} } catch (err) {
log.debug(`background.getState`) dispatch(actions.hideLoadingIndication())
background.getState((err, newState) => { dispatch(actions.displayWarning(err.message))
dispatch(actions.hideLoadingIndication()) throw err
if (err) { }
dispatch(actions.displayWarning(err.message)) dispatch(actions.hideLoadingIndication())
return reject(err) dispatch(actions.updateMetamaskState(newState))
} dispatch({
dispatch(actions.updateMetamaskState(newState)) type: actions.SHOW_ACCOUNT_DETAIL,
dispatch({ value: newState.selectedAddress,
type: actions.SHOW_ACCOUNT_DETAIL,
value: newState.selectedAddress,
})
resolve(newState)
})
})
}) })
return newState
} }
} }
@ -1926,23 +1923,6 @@ function setLocaleMessages (localeMessages) {
} }
} }
function setNetworkEndpoints (networkEndpointType) {
return dispatch => {
log.debug('background.setNetworkEndpoints')
return new Promise((resolve, reject) => {
background.setNetworkEndpoints(networkEndpointType, err => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.updateNetworkEndpointType(networkEndpointType))
resolve(networkEndpointType)
})
})
}
}
function updateNetworkEndpointType (networkEndpointType) { function updateNetworkEndpointType (networkEndpointType) {
return { return {
type: actions.UPDATE_NETWORK_ENDPOINT_TYPE, type: actions.UPDATE_NETWORK_ENDPOINT_TYPE,

@ -22,16 +22,14 @@ const Home = require('./components/pages/home')
const Authenticated = require('./components/pages/authenticated') const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized') const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings') const Settings = require('./components/pages/settings')
const UnlockPage = require('./components/pages/unlock') const UnlockPage = require('./components/pages/unlock-page')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault') const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token') const AddTokenPage = require('./components/pages/add-token')
const CreateAccountPage = require('./components/pages/create-account') const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice') const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading') const Loading = require('./components/loading-screen')
const NetworkIndicator = require('./components/network')
const Identicon = require('./components/identicon')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown') const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu') const AccountMenu = require('./components/account-menu')
@ -39,6 +37,8 @@ const AccountMenu = require('./components/account-menu')
// Global Modals // Global Modals
const Modal = require('./components/modals/index').Modal const Modal = require('./components/modals/index').Modal
const AppHeader = require('./components/app-header')
// Routes // Routes
const { const {
DEFAULT_ROUTE, DEFAULT_ROUTE,
@ -69,11 +69,11 @@ class App extends Component {
return ( return (
h(Switch, [ h(Switch, [
h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }), h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }),
h(Initialized, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }), h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
h(Initialized, { path: SETTINGS_ROUTE, component: Settings }),
h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }), h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
h(Initialized, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }), h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
@ -119,8 +119,7 @@ class App extends Component {
// global modal // global modal
h(Modal, {}, []), h(Modal, {}, []),
// app bar h(AppHeader),
this.renderAppBar(),
// sidebar // sidebar
this.renderSidebar(), this.renderSidebar(),
@ -135,6 +134,7 @@ class App extends Component {
(isLoading || isLoadingNetwork) && h(Loading, { (isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage, loadingMessage: loadMessage,
fullScreen: true,
}), }),
// content // content
@ -196,110 +196,6 @@ class App extends Component {
]) ])
} }
renderAppBar () {
const {
isUnlocked,
network,
provider,
networkDropdownOpen,
showNetworkDropdown,
hideNetworkDropdown,
isInitialized,
welcomeScreenSeen,
isPopup,
betaUI,
} = this.props
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
if (isMascara && isOnboarding) {
return null
}
// Do not render header if user is in mascara buy ether
if (isMascara && props.currentView.name === 'buyEth') {
return null
}
return (
h('.full-width', {
style: {},
}, [
(isInitialized || welcomeScreenSeen || isPopup || !betaUI) && h('.app-header.flex-row.flex-space-between', {
className: classnames({
'app-header--initialized': !isOnboarding,
}),
}, [
h('div.app-header-contents', {}, [
h('div.left-menu-wrapper', {
onClick: () => props.history.push(DEFAULT_ROUTE),
}, [
// mini logo
h('img.metafox-icon', {
height: 42,
width: 42,
src: '/images/metamask-fox.svg',
}),
// metamask name
h('.flex-row', [
h('h1', this.context.t('appName')),
h('div.beta-label', this.context.t('beta')),
]),
]),
betaUI && isInitialized && h('div.header__right-actions', [
h('div.network-component-wrapper', {
style: {},
}, [
// Network Indicator
h(NetworkIndicator, {
network,
provider,
disabled: this.props.location.pathname === CONFIRM_TRANSACTION_ROUTE,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
return networkDropdownOpen === false
? showNetworkDropdown()
: hideNetworkDropdown()
},
}),
]),
isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [
h(Identicon, {
address: this.props.selectedAddress,
diameter: 32,
}),
]),
]),
]),
]),
!isInitialized && !isPopup && betaUI && h('.alpha-warning__container', {}, [
h('h2', {
className: classnames({
'alpha-warning': welcomeScreenSeen,
'alpha-warning-welcome-screen': !welcomeScreenSeen,
}),
}, 'Please be aware that this version is still under development'),
]),
])
)
}
toggleMetamaskActive () { toggleMetamaskActive () {
if (!this.props.isUnlocked) { if (!this.props.isUnlocked) {
// currently inactive: redirect to password box // currently inactive: redirect to password box

@ -0,0 +1,140 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { matchPath } from 'react-router-dom'
const {
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
} = require('../../../../app/scripts/lib/enums')
const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes')
const Identicon = require('../identicon')
const NetworkIndicator = require('../network')
class AppHeader extends Component {
static propTypes = {
history: PropTypes.object,
location: PropTypes.object,
network: PropTypes.string,
provider: PropTypes.object,
networkDropdownOpen: PropTypes.bool,
showNetworkDropdown: PropTypes.func,
hideNetworkDropdown: PropTypes.func,
toggleAccountMenu: PropTypes.func,
selectedAddress: PropTypes.string,
isUnlocked: PropTypes.bool,
}
static contextTypes = {
t: PropTypes.func,
}
handleNetworkIndicatorClick (event) {
event.preventDefault()
event.stopPropagation()
const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
return networkDropdownOpen === false
? showNetworkDropdown()
: hideNetworkDropdown()
}
isConfirming () {
const { location } = this.props
return Boolean(matchPath(location.pathname, {
path: CONFIRM_TRANSACTION_ROUTE, exact: false,
}))
}
renderAccountMenu () {
const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props
return isUnlocked && (
<div
className={classnames('account-menu__icon', {
'account-menu__icon--disabled': this.isConfirming(),
})}
onClick={() => this.isConfirming() || toggleAccountMenu()}
>
<Identicon
address={selectedAddress}
diameter={32}
/>
</div>
)
}
hideAppHeader () {
const { location } = this.props
const isInitializing = Boolean(matchPath(location.pathname, {
path: INITIALIZE_ROUTE, exact: false,
}))
if (isInitializing) {
return true
}
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
return true
}
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) {
return true
}
}
render () {
const {
network,
provider,
history,
location,
isUnlocked,
} = this.props
if (this.hideAppHeader()) {
return null
}
return (
<div
className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
<div className="app-header__contents">
<div
className="app-header__logo-container"
onClick={() => history.push(DEFAULT_ROUTE)}
>
<img
className="app-header__metafox"
src="/images/metamask-fox.svg"
height={42}
width={42}
/>
<div className="flex-row">
<h1>{ this.context.t('appName') }</h1>
<div className="app-header__beta-label">
{ this.context.t('beta') }
</div>
</div>
</div>
<div className="app-header__account-menu-container">
<div className="network-component-wrapper">
<NetworkIndicator
network={network}
provider={provider}
onClick={event => this.handleNetworkIndicatorClick(event)}
disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE}
/>
</div>
{ this.renderAccountMenu() }
</div>
</div>
</div>
)
}
}
export default AppHeader

@ -0,0 +1,38 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import AppHeader from './app-header.component'
const actions = require('../../actions')
const mapStateToProps = state => {
const { appState, metamask } = state
const { networkDropdownOpen } = appState
const {
network,
provider,
selectedAddress,
isUnlocked,
} = metamask
return {
networkDropdownOpen,
network,
provider,
selectedAddress,
isUnlocked,
}
}
const mapDispatchToProps = dispatch => {
return {
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(AppHeader)

@ -0,0 +1,2 @@
import AppHeader from './app-header.container'
module.exports = AppHeader

@ -0,0 +1,43 @@
const { Component } = require('react')
const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const classnames = require('classnames')
const SECONDARY = 'secondary'
const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_PRIMARY_LARGE = 'btn-primary--lg'
const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_SECONDARY_LARGE = 'btn-secondary--lg'
const getClassName = (type, large = false) => {
let output = type === SECONDARY ? CLASSNAME_SECONDARY : CLASSNAME_PRIMARY
if (large) {
output += ` ${type === SECONDARY ? CLASSNAME_SECONDARY_LARGE : CLASSNAME_PRIMARY_LARGE}`
}
return output
}
class Button extends Component {
render () {
const { type, large, className, ...buttonProps } = this.props
return (
h('button', {
className: classnames(getClassName(type, large), className),
...buttonProps,
}, this.props.children)
)
}
}
Button.propTypes = {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.string,
}
module.exports = Button

@ -0,0 +1,41 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import Button from './'
import { text } from '@storybook/addon-knobs/react'
storiesOf('Button', module)
.add('primary', () =>
<Button
onClick={action('clicked')}
type="primary"
>
{text('text', 'Click me')}
</Button>
)
.add('secondary', () => (
<Button
onClick={action('clicked')}
type="secondary"
>
{text('text', 'Click me')}
</Button>
))
.add('large primary', () => (
<Button
onClick={action('clicked')}
type="primary"
large
>
{text('text', 'Click me')}
</Button>
))
.add('large secondary', () => (
<Button
onClick={action('clicked')}
type="secondary"
large
>
{text('text', 'Click me')}
</Button>
))

@ -0,0 +1,2 @@
const Button = require('./button.component')
module.exports = Button

@ -6,7 +6,7 @@ const connect = require('react-redux').connect
const actions = require('../actions') const actions = require('../actions')
const CoinbaseForm = require('./coinbase-form') const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form') const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading') const Loading = require('./loading-screen')
const AccountPanel = require('./account-panel') const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list') const RadioList = require('./custom-radio-list')
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util') const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')

@ -37,7 +37,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 14px; font-size: 12px;
cursor: pointer; cursor: pointer;
color: $curious-blue; color: $curious-blue;

@ -0,0 +1,2 @@
const LoadingScreen = require('./loading-screen.component')
module.exports = LoadingScreen

@ -2,8 +2,9 @@ const { Component } = require('react')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const classnames = require('classnames') const classnames = require('classnames')
const Spinner = require('../spinner')
class LoadingIndicator extends Component { class LoadingScreen extends Component {
renderMessage () { renderMessage () {
const { loadingMessage } = this.props const { loadingMessage } = this.props
return loadingMessage && h('span', loadingMessage) return loadingMessage && h('span', loadingMessage)
@ -14,9 +15,9 @@ class LoadingIndicator extends Component {
h('.loading-overlay', { h('.loading-overlay', {
className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }), className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }),
}, [ }, [
h('.flex-center.flex-column', [ h('.loading-overlay__container', [
h('img', { h(Spinner, {
src: 'images/loading.svg', color: '#F7C06C',
}), }),
this.renderMessage(), this.renderMessage(),
@ -26,9 +27,9 @@ class LoadingIndicator extends Component {
} }
} }
LoadingIndicator.propTypes = { LoadingScreen.propTypes = {
loadingMessage: PropTypes.string, loadingMessage: PropTypes.string,
fullScreen: PropTypes.bool, fullScreen: PropTypes.bool,
} }
module.exports = LoadingIndicator module.exports = LoadingScreen

@ -105,6 +105,8 @@ class JsonImportSubview extends Component {
} }
this.props.importNewJsonAccount([ fileContents, password ]) this.props.importNewJsonAccount([ fileContents, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
} }
} }

@ -91,5 +91,7 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
const { importNewAccount, history } = this.props const { importNewAccount, history } = this.props
importNewAccount('Private Key', [ privateKey ]) importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(() => history.push(DEFAULT_ROUTE)) .then(() => history.push(DEFAULT_ROUTE))
} }

@ -12,7 +12,6 @@ const SimpleDropdown = require('../../dropdowns/simple-dropdown')
const ToggleButton = require('react-toggle-button') const ToggleButton = require('react-toggle-button')
const { REVEAL_SEED_ROUTE } = require('../../../routes') const { REVEAL_SEED_ROUTE } = require('../../../routes')
const locales = require('../../../../../app/_locales/index.json') const locales = require('../../../../../app/_locales/index.json')
const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/controllers/network/enums')
const getInfuraCurrencyOptions = () => { const getInfuraCurrencyOptions = () => {
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
@ -349,7 +348,6 @@ const mapDispatchToProps = dispatch => {
updateCurrentLocale: key => dispatch(actions.updateCurrentLocale(key)), updateCurrentLocale: key => dispatch(actions.updateCurrentLocale(key)),
setFeatureFlagToBeta: () => { setFeatureFlagToBeta: () => {
return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
.then(() => dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
}, },
showResetAccountConfirmationModal: () => { showResetAccountConfirmationModal: () => {
return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' })) return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' }))

@ -0,0 +1,2 @@
import UnlockPage from './unlock-page.container'
module.exports = UnlockPage

@ -0,0 +1,180 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from 'material-ui/Button'
import TextField from '../../text-field'
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
const { getEnvironmentType } = require('../../../../../app/scripts/lib/util')
const getCaretCoordinates = require('textarea-caret')
const EventEmitter = require('events').EventEmitter
const Mascot = require('../../mascot')
const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes')
class UnlockPage extends Component {
static contextTypes = {
t: PropTypes.func,
}
constructor (props) {
super(props)
this.state = {
password: '',
error: null,
}
this.animationEventEmitter = new EventEmitter()
}
componentWillMount () {
const { isUnlocked, history } = this.props
if (isUnlocked) {
history.push(DEFAULT_ROUTE)
}
}
tryUnlockMetamask (password) {
const { tryUnlockMetamask, history } = this.props
tryUnlockMetamask(password)
.then(() => history.push(DEFAULT_ROUTE))
.catch(({ message }) => this.setState({ error: message }))
}
handleSubmit (event) {
event.preventDefault()
event.stopPropagation()
const { password } = this.state
const { tryUnlockMetamask, history } = this.props
if (password === '') {
return
}
this.setState({ error: null })
tryUnlockMetamask(password)
.then(() => history.push(DEFAULT_ROUTE))
.catch(({ message }) => this.setState({ error: message }))
}
handleInputChange ({ target }) {
this.setState({ password: target.value, error: null })
// tell mascot to look at page action
const element = target
const boundingRect = element.getBoundingClientRect()
const coordinates = getCaretCoordinates(element, element.selectionEnd)
this.animationEventEmitter.emit('point', {
x: boundingRect.left + coordinates.left - element.scrollLeft,
y: boundingRect.top + coordinates.top - element.scrollTop,
})
}
renderSubmitButton () {
const style = {
backgroundColor: '#f7861c',
color: 'white',
marginTop: '20px',
height: '60px',
fontWeight: '400',
boxShadow: 'none',
borderRadius: '4px',
}
return (
<Button
type="submit"
style={style}
disabled={!this.state.password}
fullWidth
variant="raised"
size="large"
onClick={event => this.handleSubmit(event)}
disableRipple
>
{ this.context.t('login') }
</Button>
)
}
render () {
const { error } = this.state
return (
<div className="unlock-page__container">
<div className="unlock-page">
<div className="unlock-page__mascot-container">
<Mascot
animationEventEmitter={this.animationEventEmitter}
width="120"
height="120"
/>
</div>
<h1 className="unlock-page__title">
{ this.context.t('welcomeBack') }
</h1>
<div>{ this.context.t('unlockMessage') }</div>
<form
className="unlock-page__form"
onSubmit={event => this.handleSubmit(event)}
>
<TextField
id="password"
label="Password"
type="password"
value={this.state.password}
onChange={event => this.handleInputChange(event)}
error={error}
autoFocus
autoComplete="current-password"
fullWidth
/>
</form>
{ this.renderSubmitButton() }
<div className="unlock-page__links">
<div
className="unlock-page__link"
onClick={() => {
this.props.markPasswordForgotten()
this.props.history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
}}
>
{ this.context.t('restoreFromSeed') }
</div>
<div
className="unlock-page__link unlock-page__link--import"
onClick={() => {
this.props.markPasswordForgotten()
this.props.history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
}}
>
{ this.context.t('importUsingSeed') }
</div>
</div>
</div>
</div>
)
}
}
UnlockPage.propTypes = {
forgotPassword: PropTypes.func,
tryUnlockMetamask: PropTypes.func,
markPasswordForgotten: PropTypes.func,
history: PropTypes.object,
isUnlocked: PropTypes.bool,
t: PropTypes.func,
useOldInterface: PropTypes.func,
}
export default UnlockPage

@ -0,0 +1,31 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
const {
tryUnlockMetamask,
forgotPassword,
markPasswordForgotten,
} = require('../../../actions')
import UnlockPage from './unlock-page.component'
const mapStateToProps = state => {
const { metamask: { isUnlocked } } = state
return {
isUnlocked,
}
}
const mapDispatchToProps = dispatch => {
return {
forgotPassword: () => dispatch(forgotPassword()),
tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
markPasswordForgotten: () => dispatch(markPasswordForgotten()),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(UnlockPage)

@ -0,0 +1,51 @@
.unlock-page {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
width: 357px;
padding: 30px;
font-weight: 400;
color: $silver-chalice;
&__container {
background: $white;
display: flex;
align-self: stretch;
justify-content: center;
flex: 1 0 auto;
}
&__mascot-container {
margin-top: 24px;
}
&__title {
margin-top: 5px;
font-size: 2rem;
font-weight: 800;
color: $tundora;
}
&__form {
width: 100%;
margin: 56px 0 8px;
}
&__links {
margin-top: 25px;
width: 100%;
}
&__link {
cursor: pointer;
&--import {
color: $ecstasy;
}
&--use-classic {
margin-top: 10px;
}
}
}

@ -1,194 +0,0 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const connect = require('../../metamask-connect')
const h = require('react-hyperscript')
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const {
tryUnlockMetamask,
forgotPassword,
markPasswordForgotten,
setNetworkEndpoints,
setFeatureFlag,
} = require('../../actions')
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
const getCaretCoordinates = require('textarea-caret')
const EventEmitter = require('events').EventEmitter
const Mascot = require('../mascot')
const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/controllers/network/enums')
const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../routes')
class UnlockScreen extends Component {
constructor (props) {
super(props)
this.state = {
error: null,
}
this.animationEventEmitter = new EventEmitter()
}
componentWillMount () {
const { isUnlocked, history } = this.props
if (isUnlocked) {
history.push(DEFAULT_ROUTE)
}
}
componentDidMount () {
const passwordBox = document.getElementById('password-box')
if (passwordBox) {
passwordBox.focus()
}
}
tryUnlockMetamask (password) {
const { tryUnlockMetamask, history } = this.props
tryUnlockMetamask(password)
.then(() => history.push(DEFAULT_ROUTE))
.catch(({ message }) => this.setState({ error: message }))
}
onSubmit (event) {
const input = document.getElementById('password-box')
const password = input.value
this.tryUnlockMetamask(password)
}
onKeyPress (event) {
if (event.key === 'Enter') {
this.submitPassword(event)
}
}
submitPassword (event) {
var element = event.target
var password = element.value
// reset input
element.value = ''
this.tryUnlockMetamask(password)
}
inputChanged (event) {
// tell mascot to look at page action
var element = event.target
var boundingRect = element.getBoundingClientRect()
var coordinates = getCaretCoordinates(element, element.selectionEnd)
this.animationEventEmitter.emit('point', {
x: boundingRect.left + coordinates.left - element.scrollLeft,
y: boundingRect.top + coordinates.top - element.scrollTop,
})
}
render () {
const { error } = this.state
return (
h('.unlock-screen', [
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('h1', {
style: {
fontSize: '1.4em',
textTransform: 'uppercase',
color: '#7F8082',
},
}, this.props.t('appName')),
h('input.large-input', {
type: 'password',
id: 'password-box',
placeholder: 'enter password',
style: {
background: 'white',
},
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
h('.error', {
style: {
display: error ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, error),
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
style: {
margin: 10,
},
}, this.props.t('login')),
h('p.pointer', {
onClick: () => {
this.props.markPasswordForgotten()
this.props.history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
},
style: {
fontSize: '0.8em',
color: 'rgb(247, 134, 28)',
textDecoration: 'underline',
},
}, this.props.t('restoreFromSeed')),
h('p.pointer', {
onClick: () => {
this.props.useOldInterface()
.then(() => this.props.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))
},
style: {
fontSize: '0.8em',
color: '#aeaeae',
textDecoration: 'underline',
marginTop: '32px',
},
}, this.props.t('classicInterface')),
])
)
}
}
UnlockScreen.propTypes = {
forgotPassword: PropTypes.func,
tryUnlockMetamask: PropTypes.func,
markPasswordForgotten: PropTypes.func,
history: PropTypes.object,
isUnlocked: PropTypes.bool,
t: PropTypes.func,
useOldInterface: PropTypes.func,
setNetworkEndpoints: PropTypes.func,
}
const mapStateToProps = state => {
const { metamask: { isUnlocked } } = state
return {
isUnlocked,
}
}
const mapDispatchToProps = dispatch => {
return {
forgotPassword: () => dispatch(forgotPassword()),
tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
markPasswordForgotten: () => dispatch(markPasswordForgotten()),
useOldInterface: () => dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')),
setNetworkEndpoints: type => dispatch(setNetworkEndpoints(type)),
}
}
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(UnlockScreen)

@ -28,6 +28,10 @@ const currencies = require('currency-formatter/currencies')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
const {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
} = require('../../../../app/scripts/lib/enums')
ConfirmSendEther.contextTypes = { ConfirmSendEther.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
@ -293,6 +297,14 @@ ConfirmSendEther.prototype.editTransaction = function (txMeta) {
history.push(SEND_ROUTE) history.push(SEND_ROUTE)
} }
ConfirmSendEther.prototype.renderNetworkDisplay = function () {
const windowType = window.METAMASK_UI_TYPE
return (windowType === ENVIRONMENT_TYPE_NOTIFICATION || windowType === ENVIRONMENT_TYPE_POPUP)
? h(NetworkDisplay)
: null
}
ConfirmSendEther.prototype.render = function () { ConfirmSendEther.prototype.render = function () {
const { const {
currentCurrency, currentCurrency,
@ -358,7 +370,7 @@ ConfirmSendEther.prototype.render = function () {
visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden', visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden',
}, },
}, 'Edit'), }, 'Edit'),
window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay), this.renderNetworkDisplay(),
]), ]),
h('.page-container__title', title), h('.page-container__title', title),
h('.page-container__subtitle', subtitle), h('.page-container__subtitle', subtitle),

@ -12,7 +12,7 @@ const { getSymbolAndDecimals } = require('../../token-util')
const ConfirmSendEther = require('./confirm-send-ether') const ConfirmSendEther = require('./confirm-send-ether')
const ConfirmSendToken = require('./confirm-send-token') const ConfirmSendToken = require('./confirm-send-token')
const ConfirmDeployContract = require('./confirm-deploy-contract') const ConfirmDeployContract = require('./confirm-deploy-contract')
const Loading = require('../loading') const Loading = require('../loading-screen')
const TX_TYPES = { const TX_TYPES = {
DEPLOY_CONTRACT: 'deploy_contract', DEPLOY_CONTRACT: 'deploy_contract',

@ -0,0 +1,2 @@
const Spinner = require('./spinner.component')
module.exports = Spinner

@ -0,0 +1,78 @@
import React from 'react'
import PropTypes from 'prop-types'
const Spinner = ({ className = '', color = '#000000' }) => {
return (
<div className={`spinner ${className}`}>
<svg className="lds-spinner" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style={{background: 'none'}}>
<g transform="rotate(0 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(30 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(60 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(90 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(120 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(150 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(180 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(210 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(240 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(270 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(300 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(330 50 50)">
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite" />
</rect>
</g>
</svg>
</div>
)
}
Spinner.propTypes = {
className: PropTypes.string,
color: PropTypes.string,
}
module.exports = Spinner

@ -0,0 +1,2 @@
import TextField from './text-field.component'
module.exports = TextField

@ -0,0 +1,59 @@
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from 'material-ui/styles'
import { default as MaterialTextField } from 'material-ui/TextField'
const styles = {
cssLabel: {
'&$cssFocused': {
color: '#aeaeae',
},
'&$cssError': {
color: '#aeaeae',
},
fontWeight: '400',
color: '#aeaeae',
},
cssFocused: {},
cssUnderline: {
'&:after': {
backgroundColor: '#f7861c',
},
},
cssError: {},
}
const TextField = props => {
const { error, classes, ...textFieldProps } = props
return (
<MaterialTextField
error={Boolean(error)}
helperText={error}
InputLabelProps={{
FormLabelClasses: {
root: classes.cssLabel,
focused: classes.cssFocused,
error: classes.cssError,
},
}}
InputProps={{
classes: {
underline: classes.cssUnderline,
},
}}
{...textFieldProps}
/>
)
}
TextField.defaultProps = {
error: null,
}
TextField.propTypes = {
error: PropTypes.string,
classes: PropTypes.object,
}
export default withStyles(styles)(TextField)

@ -0,0 +1,24 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import TextField from './'
storiesOf('TextField', module)
.add('text', () =>
<TextField
label="Text"
type="text"
/>
)
.add('password', () =>
<TextField
label="Password"
type="password"
/>
)
.add('error', () =>
<TextField
type="text"
label="Name"
error="Invalid value"
/>
)

@ -102,6 +102,7 @@ WalletView.prototype.render = function () {
selectedIdentity, selectedIdentity,
keyrings, keyrings,
showAccountDetailModal, showAccountDetailModal,
sidebarOpen,
hideSidebar, hideSidebar,
history, history,
} = this.props } = this.props
@ -182,7 +183,10 @@ WalletView.prototype.render = function () {
h(TokenList), h(TokenList),
h('button.btn-primary.wallet-view__add-token-button', { h('button.btn-primary.wallet-view__add-token-button', {
onClick: () => history.push(ADD_TOKEN_ROUTE), onClick: () => {
history.push(ADD_TOKEN_ROUTE)
sidebarOpen && hideSidebar()
},
}, this.context.t('addToken')), }, this.context.t('addToken')),
]) ])
} }

@ -13,7 +13,7 @@ const SignatureRequest = require('./components/signature-request')
// const PendingMsg = require('./components/pending-msg') // const PendingMsg = require('./components/pending-msg')
// const PendingPersonalMsg = require('./components/pending-personal-msg') // const PendingPersonalMsg = require('./components/pending-personal-msg')
// const PendingTypedMsg = require('./components/pending-typed-msg') // const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading') const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes') const { DEFAULT_ROUTE } = require('./routes')
module.exports = compose( module.exports = compose(

@ -50,7 +50,6 @@
font-family: Roboto; font-family: Roboto;
line-height: 16px; line-height: 16px;
font-size: 12px; font-size: 12px;
font-weight: 300;
} }
&__account-primary-balance { &__account-primary-balance {

@ -23,6 +23,10 @@
&__icon { &__icon {
margin-left: 20px; margin-left: 20px;
cursor: pointer; cursor: pointer;
&--disabled {
cursor: initial;
}
} }
&__header { &__header {
@ -40,7 +44,6 @@
font-size: 12px; font-size: 12px;
line-height: 23px; line-height: 23px;
padding: 0 24px; padding: 0 24px;
font-weight: 300;
} }
&__item-icon { &__item-icon {
@ -113,7 +116,6 @@
&__name { &__name {
color: $white; color: $white;
font-size: 18px; font-size: 18px;
font-weight: 300;
} }
&__balance { &__balance {
@ -124,7 +126,6 @@
&__action { &__action {
font-size: 16px; font-size: 16px;
line-height: 18px; line-height: 18px;
font-weight: 300;
cursor: pointer; cursor: pointer;
} }
} }

@ -377,7 +377,6 @@
&__amount { &__amount {
color: $scorpion; color: $scorpion;
font-size: 43px; font-size: 43px;
font-weight: 300;
line-height: 43px; line-height: 43px;
margin-right: 8px; margin-right: 8px;
} }

@ -18,6 +18,7 @@
padding: 0 20px; padding: 0 20px;
min-width: 140px; min-width: 140px;
text-transform: uppercase; text-transform: uppercase;
outline: none;
} }
.btn-primary, .btn-primary,

@ -175,7 +175,6 @@
margin-top: 12px; margin-top: 12px;
text-align: center; text-align: center;
font-size: 40px; font-size: 40px;
font-weight: 300;
line-height: 53px; line-height: 53px;
flex: 0 0 auto; flex: 0 0 auto;
} }
@ -235,7 +234,6 @@ section .confirm-screen-account-number,
padding-left: 35px; padding-left: 35px;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
font-weight: 300;
&:not(:last-of-type) { &:not(:last-of-type) {
border-bottom: 1px solid $alto; border-bottom: 1px solid $alto;
@ -336,7 +334,6 @@ section .confirm-screen-account-number,
border-width: 0; border-width: 0;
box-shadow: none; box-shadow: none;
flex: 1 0 auto; flex: 1 0 auto;
font-weight: 300;
margin: 0 5px; margin: 0 5px;
} }
@ -353,6 +350,5 @@ section .confirm-screen-account-number,
box-shadow: none; box-shadow: none;
cursor: pointer; cursor: pointer;
flex: 1 0 auto; flex: 1 0 auto;
font-weight: 300;
margin: 0 5px; margin: 0 5px;
} }

@ -7,7 +7,6 @@
color: $scorpion; color: $scorpion;
font-family: Roboto; font-family: Roboto;
font-size: 16px; font-size: 16px;
font-weight: 300;
padding: 8px 10px; padding: 8px 10px;
position: relative; position: relative;

@ -1,15 +1,15 @@
.app-header { .app-header {
align-items: center; align-items: center;
visibility: visible;
background: $gallery; background: $gallery;
position: relative; position: relative;
z-index: $header-z-index; z-index: $header-z-index;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
width: 100%;
flex: 0 0 auto;
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
padding: 12px; padding: 12px;
width: 100%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); box-shadow: 0 0 0 1px rgba(0, 0, 0, .08);
z-index: $mobile-header-z-index; z-index: $mobile-header-z-index;
} }
@ -17,48 +17,71 @@
@media screen and (min-width: 576px) { @media screen and (min-width: 576px) {
height: 75px; height: 75px;
justify-content: center; justify-content: center;
&--back-drop {
&::after {
content: '';
position: absolute;
width: 100%;
height: 32px;
background: $gallery;
bottom: -32px;
}
}
} }
.metafox-icon { &__metafox {
cursor: pointer; cursor: pointer;
} }
}
.app-header--initialized {
@media screen and (min-width: 576px) { &__beta-label {
&::after { font-family: Roboto;
content: ''; text-transform: uppercase;
position: absolute; font-weight: 500;
width: 100%; font-size: .8rem;
height: 32px; color: $buttercup;
background: $gallery; margin-left: 5px;
bottom: -32px; line-height: initial;
@media screen and (max-width: 575px) {
display: none;
} }
} }
}
.app-header-contents { &__contents {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-flow: row nowrap; flex-flow: row nowrap;
width: 100%; width: 100%;
height: 6.9vh;
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
height: 100%; height: 100%;
} }
@media screen and (min-width: 576px) { @media screen and (min-width: 576px) {
width: 85vw; width: 85vw;
}
@media screen and (min-width: 769px) {
width: 80vw;
}
@media screen and (min-width: 1281px) {
width: 62vw;
}
} }
@media screen and (min-width: 769px) { &__logo-container {
width: 80vw; display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
} }
@media screen and (min-width: 1281px) { &__account-menu-container {
width: 62vw; display: flex;
flex-flow: row nowrap;
align-items: center;
} }
} }
@ -76,20 +99,6 @@
} }
} }
.beta-label {
font-family: Roboto;
text-transform: uppercase;
font-weight: 500;
font-size: .8rem;
color: $buttercup;
margin-left: 5px;
line-height: initial;
@media screen and (max-width: 575px) {
display: none;
}
}
h2.page-subtitle { h2.page-subtitle {
text-transform: uppercase; text-transform: uppercase;
color: #aeaeae; color: #aeaeae;
@ -102,20 +111,3 @@ h2.page-subtitle {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
.left-menu-wrapper {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
}
.header__right-actions {
display: flex;
flex-flow: row nowrap;
align-items: center;
.identicon {
cursor: pointer;
}
}

@ -26,4 +26,25 @@
width: 100vw; width: 100vw;
margin-top: 0; margin-top: 0;
} }
&__container {
position: absolute;
top: 33%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
&__message {
margin-top: 32px;
font-weight: 400;
font-size: 20px;
color: $manatee;
}
}
.spinner {
height: 58px;
width: 58px;
} }

@ -11,7 +11,6 @@
flex-flow: row nowrap; flex-flow: row nowrap;
align-items: center; align-items: center;
position: relative; position: relative;
font-weight: 300;
z-index: 201; z-index: 201;
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {

@ -368,7 +368,6 @@
resize: none; resize: none;
padding: 9px 13px 8px; padding: 9px 13px 8px;
text-transform: uppercase; text-transform: uppercase;
font-weight: 300;
} }
@ -796,7 +795,6 @@
.simple-dropdown { .simple-dropdown {
color: #5B5D67; color: #5B5D67;
font-size: 16px; font-size: 16px;
font-weight: 300;
line-height: 21px; line-height: 21px;
border: 1px solid #D8D8D8; border: 1px solid #D8D8D8;
background-color: #FFFFFF; background-color: #FFFFFF;

@ -117,7 +117,6 @@ $wallet-view-bg: $alabaster;
font-size: 14px; font-size: 14px;
line-height: 12px; line-height: 12px;
padding: 4px 12px; padding: 4px 12px;
font-weight: 300;
cursor: pointer; cursor: pointer;
flex: 0 0 auto; flex: 0 0 auto;
@ -264,7 +263,6 @@ $wallet-view-bg: $alabaster;
// wallet view // wallet view
.account-name { .account-name {
font-size: 24px; font-size: 24px;
font-weight: 300;
color: $black; color: $black;
margin-top: 8px; margin-top: 8px;
margin-bottom: .9rem; margin-bottom: .9rem;

@ -1,3 +1,3 @@
@import './unlock.scss';
@import './reveal-seed.scss'; @import './reveal-seed.scss';
@import '../../../../components/pages/unlock-page/unlock-page.scss';

@ -1,9 +0,0 @@
.unlock-page {
box-shadow: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgb(247, 247, 247);
width: 100%;
}

@ -48,7 +48,6 @@
color: #5B5D67; color: #5B5D67;
font-family: Roboto; font-family: Roboto;
font-size: 22px; font-size: 22px;
font-weight: 300;
line-height: 29px; line-height: 29px;
z-index: 3; z-index: 3;
} }
@ -125,7 +124,6 @@
color: $tundora; color: $tundora;
font-family: Roboto; font-family: Roboto;
font-size: 18px; font-size: 18px;
font-weight: 300;
line-height: 24px; line-height: 24px;
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;

@ -95,19 +95,6 @@ textarea.twelve-word-phrase {
margin: -2px 8px 0px -8px; margin: -2px 8px 0px -8px;
} }
.unlock-screen #metamask-mascot-container {
margin-top: 24px;
}
.unlock-screen h1 {
margin-top: -28px;
margin-bottom: 42px;
}
.unlock-screen input[type=password] {
width: 260px;
}
.sizing-input { .sizing-input {
font-size: 14px; font-size: 14px;
height: 30px; height: 30px;
@ -118,34 +105,6 @@ textarea.twelve-word-phrase {
display: flex; display: flex;
} }
/* Webkit */
.unlock-screen input::-webkit-input-placeholder {
text-align: center;
font-size: 1.2em;
}
/* Firefox 18- */
.unlock-screen input:-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
/* Firefox 19+ */
.unlock-screen input::-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
/* IE */
.unlock-screen input:-ms-input-placeholder {
text-align: center;
font-size: 1.2em;
}
/* accounts */ /* accounts */
.accounts-section { .accounts-section {

@ -507,7 +507,6 @@
&__copy { &__copy {
color: $gray; color: $gray;
font-size: 14px; font-size: 14px;
font-weight: 300;
line-height: 19px; line-height: 19px;
text-align: center; text-align: center;
margin-top: 10px; margin-top: 10px;
@ -641,7 +640,6 @@
font-family: Roboto; font-family: Roboto;
font-size: 16px; font-size: 16px;
line-height: 21px; line-height: 21px;
font-weight: 300;
} }
} }
@ -832,7 +830,6 @@
color: $tundora; color: $tundora;
font-family: Roboto; font-family: Roboto;
font-size: 20px; font-size: 20px;
font-weight: 300;
line-height: 26px; line-height: 26px;
margin-top: 17px; margin-top: 17px;
} }

@ -3,8 +3,6 @@
background: $white; background: $white;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
height: auto;
overflow: auto;
} }
.settings__header { .settings__header {
@ -29,6 +27,8 @@
.settings__content { .settings__content {
padding: 0 25px; padding: 0 25px;
height: auto;
overflow: auto;
} }
.settings__content-row { .settings__content-row {

@ -1,59 +1,60 @@
.welcome-screen { .welcome-screen {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
font-family: Roboto;
font-weight: 400;
width: 100%;
flex: 1 0 auto;
padding: 70px 0;
background: $white;
@media screen and (max-width: 575px) {
padding: 0;
}
&__info {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
justify-content: center;
align-items: center;
font-family: Roboto;
font-weight: 400;
width: 100%; width: 100%;
flex: 1 0 auto; height: 100%;
padding: 70px 0; align-items: center;
background: $white; justify-content: center;
@media screen and (max-width: 575px) {
padding: 0;
}
&__info {
display: flex;
flex-flow: column;
width: 100%;
height: 100%;
align-items: center;
&__header {
font-size: 1.65em;
margin-bottom: 14px;
@media screen and (max-width: 575px) {
font-size: 1.5em;
}
}
&__copy { &__header {
font-size: 1em; font-size: 1.65em;
width: 400px; margin-bottom: 14px;
max-width: 90vw;
text-align: center;
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
font-size: 0.9em; font-size: 1.5em;
} }
}
} }
&__button { &__copy {
height: 54px; font-size: 1em;
width: 198px; width: 400px;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); max-width: 90vw;
color: #FFFFFF;
font-size: 20px;
font-weight: 500;
line-height: 26px;
text-align: center; text-align: center;
text-transform: uppercase;
margin: 35px 0 14px; @media screen and (max-width: 575px) {
transition: 200ms ease-in-out; font-size: .9em;
background-color: rgba(247, 134, 28, 0.9); }
} }
}
&__button {
height: 54px;
width: 198px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14);
color: #fff;
font-size: 20px;
font-weight: 500;
line-height: 26px;
text-align: center;
text-transform: uppercase;
margin: 35px 0 14px;
transition: 200ms ease-in-out;
background-color: rgba(247, 134, 28, .9);
}
} }

@ -12,7 +12,7 @@ html,
body { body {
font-family: Roboto, Arial; font-family: Roboto, Arial;
color: #4d4d4d; color: #4d4d4d;
font-weight: 300; font-weight: 400;
background: #f7f7f7; background: #f7f7f7;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -205,10 +205,8 @@ input.large-input {
} }
&__content { &__content {
height: 100%;
overflow-y: auto; overflow-y: auto;
min-height: 250px; flex: 1;
max-height: 400px;
} }
&__warning-container { &__warning-container {
@ -256,6 +254,7 @@ input.large-input {
overflow-y: auto; overflow-y: auto;
background-color: $white; background-color: $white;
border-radius: 0; border-radius: 0;
flex: 1;
} }
} }

@ -10,7 +10,6 @@ const getCaretCoordinates = require('textarea-caret')
const { RESTORE_VAULT_ROUTE, DEFAULT_ROUTE } = require('../routes') const { RESTORE_VAULT_ROUTE, DEFAULT_ROUTE } = require('../routes')
const { getEnvironmentType } = require('../../../app/scripts/lib/util') const { getEnvironmentType } = require('../../../app/scripts/lib/util')
const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums') const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums')
const { OLD_UI_NETWORK_TYPE } = require('../../../app/scripts/controllers/network/enums')
class InitializeMenuScreen extends Component { class InitializeMenuScreen extends Component {
constructor (props) { constructor (props) {
@ -190,7 +189,6 @@ class InitializeMenuScreen extends Component {
showOldUI () { showOldUI () {
this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
.then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
} }
} }

@ -3,7 +3,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const AccountAndTransactionDetails = require('./account-and-transaction-details') const AccountAndTransactionDetails = require('./account-and-transaction-details')
const Settings = require('./components/pages/settings') const Settings = require('./components/pages/settings')
const UnlockScreen = require('./components/pages/unlock') const UnlockScreen = require('./components/pages/unlock-page')
const log = require('loglevel') const log = require('loglevel')
module.exports = MainContainer module.exports = MainContainer

@ -6,8 +6,7 @@ const { HashRouter } = require('react-router-dom')
const App = require('./app') const App = require('./app')
const OldApp = require('../../old-ui/app/app') const OldApp = require('../../old-ui/app/app')
const { autoAddToBetaUI } = require('./selectors') const { autoAddToBetaUI } = require('./selectors')
const { setFeatureFlag, setNetworkEndpoints } = require('./actions') const { setFeatureFlag } = require('./actions')
const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums')
const I18nProvider = require('./i18n-provider') const I18nProvider = require('./i18n-provider')
function mapStateToProps (state) { function mapStateToProps (state) {
@ -24,11 +23,9 @@ function mapDispatchToProps (dispatch) {
return { return {
setFeatureFlagWithModal: () => { setFeatureFlagWithModal: () => {
return dispatch(setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL')) return dispatch(setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
.then(() => dispatch(setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
}, },
setFeatureFlagWithoutModal: () => { setFeatureFlagWithoutModal: () => {
return dispatch(setFeatureFlag('betaUI', true)) return dispatch(setFeatureFlag('betaUI', true))
.then(() => dispatch(setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
}, },
} }
} }

@ -31,6 +31,7 @@ const {
} = require('./components/send/send-utils') } = require('./components/send/send-utils')
const { isValidAddress } = require('./util') const { isValidAddress } = require('./util')
const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes') const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes')
const Button = require('./components/button')
SendTransactionScreen.contextTypes = { SendTransactionScreen.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
@ -497,13 +498,19 @@ SendTransactionScreen.prototype.renderFooter = function () {
const noErrors = !amountError && toError === null const noErrors = !amountError && toError === null
return h('div.page-container__footer', [ return h('div.page-container__footer', [
h('button.btn-secondary--lg.page-container__footer-button', { h(Button, {
type: 'secondary',
large: true,
className: 'page-container__footer-button',
onClick: () => { onClick: () => {
clearSend() clearSend()
history.push(DEFAULT_ROUTE) history.push(DEFAULT_ROUTE)
}, },
}, this.context.t('cancel')), }, this.context.t('cancel')),
h('button.btn-primary--lg.page-container__footer-button', { h(Button, {
type: 'primary',
large: true,
className: 'page-container__footer-button',
disabled: !noErrors || !gasTotal || missingTokenBalance, disabled: !noErrors || !gasTotal || missingTokenBalance,
onClick: event => this.onSubmit(event), onClick: event => this.onSubmit(event),
}, this.context.t('next')), }, this.context.t('next')),

@ -1,141 +0,0 @@
const inherits = require('util').inherits
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const getCaretCoordinates = require('textarea-caret')
const EventEmitter = require('events').EventEmitter
const { OLD_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums')
const { getEnvironmentType } = require('../../app/scripts/lib/util')
const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums')
const Mascot = require('./components/mascot')
UnlockScreen.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps)(UnlockScreen)
inherits(UnlockScreen, Component)
function UnlockScreen () {
Component.call(this)
this.animationEventEmitter = new EventEmitter()
}
function mapStateToProps (state) {
return {
warning: state.appState.warning,
}
}
UnlockScreen.prototype.render = function () {
const state = this.props
const warning = state.warning
return (
h('.unlock-screen', [
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('h1', {
style: {
fontSize: '1.4em',
textTransform: 'uppercase',
color: '#7F8082',
},
}, this.context.t('appName')),
h('input.large-input', {
type: 'password',
id: 'password-box',
placeholder: 'enter password',
style: {
background: 'white',
},
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
style: {
margin: 10,
},
}, this.context.t('login')),
h('p.pointer', {
onClick: () => {
this.props.dispatch(actions.markPasswordForgotten())
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
},
style: {
fontSize: '0.8em',
color: 'rgb(247, 134, 28)',
textDecoration: 'underline',
},
}, this.context.t('restoreFromSeed')),
h('p.pointer', {
onClick: () => {
this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
.then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
},
style: {
fontSize: '0.8em',
color: '#aeaeae',
textDecoration: 'underline',
marginTop: '32px',
},
}, this.context.t('classicInterface')),
])
)
}
UnlockScreen.prototype.componentDidMount = function () {
document.getElementById('password-box').focus()
}
UnlockScreen.prototype.onSubmit = function (event) {
const input = document.getElementById('password-box')
const password = input.value
this.props.dispatch(actions.tryUnlockMetamask(password))
}
UnlockScreen.prototype.onKeyPress = function (event) {
if (event.key === 'Enter') {
this.submitPassword(event)
}
}
UnlockScreen.prototype.submitPassword = function (event) {
var element = event.target
var password = element.value
// reset input
element.value = ''
this.props.dispatch(actions.tryUnlockMetamask(password))
}
UnlockScreen.prototype.inputChanged = function (event) {
// tell mascot to look at page action
var element = event.target
var boundingRect = element.getBoundingClientRect()
var coordinates = getCaretCoordinates(element, element.selectionEnd)
this.animationEventEmitter.emit('point', {
x: boundingRect.left + coordinates.left - element.scrollLeft,
y: boundingRect.top + coordinates.top - element.scrollTop,
})
}

@ -5,11 +5,6 @@ const actions = require('./app/actions')
const configureStore = require('./app/store') const configureStore = require('./app/store')
const txHelper = require('./lib/tx-helper') const txHelper = require('./lib/tx-helper')
const { fetchLocale } = require('./i18n-helper') const { fetchLocale } = require('./i18n-helper')
const {
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
} = require('../app/scripts/controllers/network/enums')
const log = require('loglevel') const log = require('loglevel')
module.exports = launchMetamaskUi module.exports = launchMetamaskUi
@ -55,10 +50,6 @@ async function startApp (metamaskState, accountManager, opts) {
networkVersion: opts.networkVersion, networkVersion: opts.networkVersion,
}) })
const useBetaUi = metamaskState.featureFlags.betaUI
const networkEndpointType = useBetaUi ? BETA_UI_NETWORK_TYPE : OLD_UI_NETWORK_TYPE
store.dispatch(actions.setNetworkEndpoints(networkEndpointType))
// if unconfirmed txs, start on txConf page // if unconfirmed txs, start on txConf page
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
const numberOfUnapprivedTx = unapprovedTxsAll.length const numberOfUnapprivedTx = unapprovedTxsAll.length

Loading…
Cancel
Save