Merge branch 'develop' into testing

feature/default_network_editable
Thomas 7 years ago
commit 5e80bc4cb9
  1. 42
      .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. 383
      app/_locales/zh_CN/messages.json
  10. 2
      app/scripts/controllers/transactions/index.js
  11. 133
      mascara/src/app/first-time/create-password-screen.js
  12. 146
      mascara/src/app/first-time/import-seed-phrase-screen.js
  13. 34
      mascara/src/app/first-time/index.css
  14. 56
      mascara/src/app/first-time/index.js
  15. 17730
      package-lock.json
  16. 18
      package.json
  17. 314
      test/e2e/chrome/metamask.spec.js
  18. 323
      test/e2e/firefox/metamask.spec.js
  19. 8
      test/e2e/func.js
  20. 145
      test/e2e/metamask.spec.js
  21. 32
      test/integration/lib/mascara-first-time.js
  22. 2
      test/integration/lib/tx-list-items.js
  23. 7
      test/screens/new-ui.js
  24. 2
      ui/app/actions.js
  25. 130
      ui/app/app.js
  26. 106
      ui/app/components/app-header/app-header.component.js
  27. 38
      ui/app/components/app-header/app-header.container.js
  28. 2
      ui/app/components/app-header/index.js
  29. 43
      ui/app/components/button/button.component.js
  30. 41
      ui/app/components/button/button.stories.js
  31. 2
      ui/app/components/button/index.js
  32. 2
      ui/app/components/export-text-container/export-text-container.scss
  33. 2
      ui/app/components/pages/unlock-page/index.js
  34. 181
      ui/app/components/pages/unlock-page/unlock-page.component.js
  35. 33
      ui/app/components/pages/unlock-page/unlock-page.container.js
  36. 51
      ui/app/components/pages/unlock-page/unlock-page.scss
  37. 194
      ui/app/components/pages/unlock.js
  38. 2
      ui/app/components/text-field/index.js
  39. 59
      ui/app/components/text-field/text-field.component.js
  40. 24
      ui/app/components/text-field/text-field.stories.js
  41. 6
      ui/app/components/wallet-view.js
  42. 1
      ui/app/css/itcss/components/account-dropdown.scss
  43. 3
      ui/app/css/itcss/components/account-menu.scss
  44. 1
      ui/app/css/itcss/components/add-token.scss
  45. 1
      ui/app/css/itcss/components/buttons.scss
  46. 4
      ui/app/css/itcss/components/confirm.scss
  47. 1
      ui/app/css/itcss/components/currency-display.scss
  48. 118
      ui/app/css/itcss/components/header.scss
  49. 1
      ui/app/css/itcss/components/menu.scss
  50. 2
      ui/app/css/itcss/components/modal.scss
  51. 2
      ui/app/css/itcss/components/newui-sections.scss
  52. 4
      ui/app/css/itcss/components/pages/index.scss
  53. 9
      ui/app/css/itcss/components/pages/unlock.scss
  54. 2
      ui/app/css/itcss/components/request-signature.scss
  55. 41
      ui/app/css/itcss/components/sections.scss
  56. 3
      ui/app/css/itcss/components/send.scss
  57. 4
      ui/app/css/itcss/components/settings.scss
  58. 97
      ui/app/css/itcss/components/welcome-screen.scss
  59. 7
      ui/app/css/itcss/generic/index.scss
  60. 2
      ui/app/main-container.js
  61. 11
      ui/app/send-v2.js
  62. 141
      ui/app/unlock.js

@ -18,10 +18,15 @@ workflows:
- test-deps:
requires:
- prep-deps-npm
- test-e2e:
- test-e2e-chrome:
requires:
- prep-deps-npm
- prep-build
- test-e2e-firefox:
requires:
- prep-deps-npm
- prep-deps-firefox
- prep-build
- test-unit:
requires:
- prep-deps-npm
@ -48,7 +53,8 @@ workflows:
- test-lint
- test-deps
- test-unit
- test-e2e
- test-e2e-chrome
- test-e2e-firefox
- test-integration-mascara-chrome
- test-integration-mascara-firefox
- test-integration-flat-chrome
@ -160,7 +166,7 @@ jobs:
name: Test
command: npx nsp check
test-e2e:
test-e2e-chrome:
docker:
- image: circleci/node:8-browsers
steps:
@ -171,7 +177,34 @@ jobs:
key: build-cache-{{ .Revision }}
- run:
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:
path: test-artifacts
destination: test-artifacts
@ -335,3 +368,4 @@ jobs:
- run:
name: All Tests 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",
"description": "status showing that an account has been fully loaded into the keyring"
},
"importUsingSeed": {
"message": "Import using account seed phrase"
},
"infoHelp": {
"message": "Info & Help"
},
@ -632,7 +635,7 @@
"message": "Reset Account"
},
"restoreFromSeed": {
"message": "Restore from seed phrase"
"message": "Restore account?"
},
"restoreVault": {
"message": "Restore Vault"
@ -721,7 +724,7 @@
"message": "New Password (min 8 chars)"
},
"seedPhraseReq": {
"message": "seed phrases are 12 words long"
"message": "Seed phrases are 12 words long"
},
"select": {
"message": "Select"
@ -896,6 +899,9 @@
"unknownNetworkId": {
"message": "Unknown network ID"
},
"unlockMessage": {
"message": "The decentralized web awaits"
},
"uriErrorMsg": {
"message": "URIs require the appropriate HTTP/HTTPS prefix."
},
@ -924,6 +930,9 @@
"warning": {
"message": "Warning"
},
"welcomeBack": {
"message": "Welcome Back!"
},
"welcomeBeta": {
"message": "Welcome to MetaMask Beta"
},

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

@ -114,7 +114,7 @@ class TransactionController extends EventEmitter {
/**
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} weather the nonce has been used in a transaction confirmed in a block
@returns {boolean} whether the nonce has been used in a transaction confirmed in a block
@param {object} txMeta - the txMeta object
*/
async isNonceTaken (txMeta) {

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

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

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

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

17730
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:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"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:run": "mocha test/e2e/metamask.spec --recursive",
"test:e2e:chrome": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:chrome'",
"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:run": "node test/screens/new-ui.js",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
@ -42,7 +44,8 @@
"announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.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": {
"transform": [
@ -80,7 +83,7 @@
"currency-formatter": "^1.4.2",
"debounce": "^1.0.0",
"debounce-stream": "^2.0.0",
"deep-extend": "^0.5.0",
"deep-extend": "^0.5.1",
"detect-node": "^2.0.3",
"disc": "^1.3.2",
"dnode": "^1.2.2",
@ -133,6 +136,7 @@
"lodash.shuffle": "^4.2.0",
"lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1",
"material-ui": "1.0.0-beta.44",
"metamascara": "^2.0.0",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
@ -191,6 +195,9 @@
},
"devDependencies": {
"@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-eslint": "^8.0.0",
"babel-plugin-transform-async-to-generator": "^6.24.1",
@ -208,6 +215,7 @@
"compression": "^1.7.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"deep-freeze-strict": "^1.1.1",
"del": "^3.0.0",
"envify": "^4.0.0",
@ -218,9 +226,11 @@
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.6.0",
"file-loader": "^1.1.11",
"fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
"ganache-core": "^2.1.0",
"geckodriver": "^1.11.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"gulp-babel": "^7.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('geckodriver')
const webdriver = require('selenium-webdriver')
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()
.withCapabilities({
chromeOptions: {
args: [`load-extension=${extPath}`],
},
})
.forBrowser('chrome')
.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 reactTriggerChange = require('react-trigger-change')
const {
timeout,
findAsync,
@ -11,6 +10,11 @@ async function runFirstTimeUsageTest (assert, done) {
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)
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')
// enter password
const pwBox = (await findAsync(app, '.first-time-flow__input'))[0]
const confBox = (await findAsync(app, '.first-time-flow__input'))[1]
pwBox.value = PASSWORD
confBox.value = PASSWORD
reactTriggerChange(pwBox)
reactTriggerChange(confBox)
const pwBox = (await findAsync(app, '#create-password'))[0]
const confBox = (await findAsync(app, '#confirm-password'))[0]
nativeInputValueSetter.call(pwBox, PASSWORD)
pwBox.dispatchEvent(new Event('input', { bubbles: true}))
nativeInputValueSetter.call(confBox, PASSWORD)
confBox.dispatchEvent(new Event('input', { bubbles: true}))
// Create Password
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')
lock.click()
const pwBox2 = (await findAsync(app, '#password-box'))[0]
pwBox2.value = PASSWORD
await timeout(1000)
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()
const detail2 = (await findAsync(app, '.wallet-view'))[0]

@ -21,7 +21,7 @@ async function runTxListItemsTest(assert, done) {
selectState.val('tx list items')
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')
metamaskLogo[0].click()

@ -39,8 +39,7 @@ async function captureAllScreens() {
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')
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")')
await driver.get(`chrome-extension://${extensionId}/home.html`)
await delay(500)
tabs = await driver.getAllWindowHandles()
@ -75,8 +74,8 @@ async function captureAllScreens() {
await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password')
const passwordBox = await driver.findElement(By.css('input[type=password]:nth-of-type(1)'))
const passwordBoxConfirm = await driver.findElement(By.css('input[type=password]:nth-of-type(2)'))
const passwordBox = await driver.findElement(By.css('input#create-password'))
const passwordBoxConfirm = await driver.findElement(By.css('input#confirm-password'))
passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789')
await delay(500)

@ -317,6 +317,7 @@ function tryUnlockMetamask (password) {
background.verifySeedPhrase(err => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
resolve()
@ -330,6 +331,7 @@ function tryUnlockMetamask (password) {
.catch(err => {
dispatch(actions.unlockFailed(err.message))
dispatch(actions.hideLoadingIndication())
return Promise.reject(err)
})
}
}

@ -1,7 +1,7 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const { Route, Switch, withRouter } = require('react-router-dom')
const { Route, Switch, withRouter, matchPath } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript')
const actions = require('./actions')
@ -22,7 +22,7 @@ const Home = require('./components/pages/home')
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
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 RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
@ -30,8 +30,6 @@ const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
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 NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu')
@ -39,6 +37,8 @@ const AccountMenu = require('./components/account-menu')
// Global Modals
const Modal = require('./components/modals/index').Modal
const AppHeader = require('./components/app-header')
// Routes
const {
DEFAULT_ROUTE,
@ -69,11 +69,11 @@ class App extends Component {
return (
h(Switch, [
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: SETTINGS_ROUTE, component: Settings }),
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: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
@ -83,6 +83,15 @@ class App extends Component {
)
}
renderAppHeader () {
const { location } = this.props
const isInitializing = matchPath(location.pathname, {
path: INITIALIZE_ROUTE, exact: false,
})
return isInitializing ? null : h(AppHeader)
}
render () {
const {
isLoading,
@ -119,8 +128,7 @@ class App extends Component {
// global modal
h(Modal, {}, []),
// app bar
this.renderAppBar(),
this.renderAppHeader(),
// sidebar
this.renderSidebar(),
@ -197,110 +205,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 () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box

@ -0,0 +1,106 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../../../app/scripts/lib/enums')
const { DEFAULT_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()
}
renderAccountMenu () {
const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props
return isUnlocked && (
<div
className="account-menu__icon"
onClick={toggleAccountMenu}
>
<Identicon
address={selectedAddress}
diameter={32}
/>
</div>
)
}
render () {
const {
network,
provider,
history,
location,
isUnlocked,
} = this.props
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
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

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

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

@ -0,0 +1,181 @@
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,
setNetworkEndpoints: PropTypes.func,
}
export default UnlockPage

@ -0,0 +1,33 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
const {
tryUnlockMetamask,
forgotPassword,
markPasswordForgotten,
setNetworkEndpoints,
} = 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()),
setNetworkEndpoints: type => dispatch(setNetworkEndpoints(type)),
}
}
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)

@ -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,
keyrings,
showAccountDetailModal,
sidebarOpen,
hideSidebar,
history,
} = this.props
@ -182,7 +183,10 @@ WalletView.prototype.render = function () {
h(TokenList),
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')),
])
}

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

@ -40,7 +40,6 @@
font-size: 12px;
line-height: 23px;
padding: 0 24px;
font-weight: 300;
}
&__item-icon {
@ -113,7 +112,6 @@
&__name {
color: $white;
font-size: 18px;
font-weight: 300;
}
&__balance {
@ -124,7 +122,6 @@
&__action {
font-size: 16px;
line-height: 18px;
font-weight: 300;
cursor: pointer;
}
}

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

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

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

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

@ -1,15 +1,15 @@
.app-header {
align-items: center;
visibility: visible;
background: $gallery;
position: relative;
z-index: $header-z-index;
display: flex;
flex-flow: column nowrap;
width: 100%;
flex: 0 0 auto;
@media screen and (max-width: 575px) {
padding: 12px;
width: 100%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .08);
z-index: $mobile-header-z-index;
}
@ -17,48 +17,75 @@
@media screen and (min-width: 576px) {
height: 75px;
justify-content: center;
&--back-drop {
&::after {
content: '';
position: absolute;
width: 100%;
height: 32px;
background: $gallery;
bottom: -32px;
}
}
}
.metafox-icon {
&__metafox {
cursor: pointer;
}
}
.app-header--initialized {
@media screen and (min-width: 576px) {
&::after {
content: '';
position: absolute;
width: 100%;
height: 32px;
background: $gallery;
bottom: -32px;
&__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;
}
}
}
.app-header-contents {
display: flex;
justify-content: space-between;
flex-flow: row nowrap;
width: 100%;
height: 6.9vh;
&__contents {
display: flex;
justify-content: space-between;
flex-flow: row nowrap;
width: 100%;
@media screen and (max-width: 575px) {
height: 100%;
}
@media screen and (max-width: 575px) {
height: 100%;
}
@media screen and (min-width: 576px) {
width: 85vw;
@media screen and (min-width: 576px) {
width: 85vw;
}
@media screen and (min-width: 769px) {
width: 80vw;
}
@media screen and (min-width: 1281px) {
width: 62vw;
}
}
@media screen and (min-width: 769px) {
width: 80vw;
&__logo-container {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
}
@media screen and (min-width: 1281px) {
width: 62vw;
&__account-menu-container {
display: flex;
flex-flow: row nowrap;
align-items: center;
.identicon {
cursor: pointer;
}
}
}
@ -76,20 +103,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 {
text-transform: uppercase;
color: #aeaeae;
@ -102,20 +115,3 @@ h2.page-subtitle {
flex-direction: row;
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;
}
}

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

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

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

@ -1,3 +1,3 @@
@import './unlock.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;
font-family: Roboto;
font-size: 22px;
font-weight: 300;
line-height: 29px;
z-index: 3;
}
@ -125,7 +124,6 @@
color: $tundora;
font-family: Roboto;
font-size: 18px;
font-weight: 300;
line-height: 24px;
text-align: center;
margin-top: 20px;

@ -95,19 +95,6 @@ textarea.twelve-word-phrase {
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 {
font-size: 14px;
height: 30px;
@ -118,34 +105,6 @@ textarea.twelve-word-phrase {
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-section {

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

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

@ -1,59 +1,60 @@
.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;
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;
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;
}
}
height: 100%;
align-items: center;
justify-content: center;
&__copy {
font-size: 1em;
width: 400px;
max-width: 90vw;
text-align: center;
&__header {
font-size: 1.65em;
margin-bottom: 14px;
@media screen and (max-width: 575px) {
font-size: 0.9em;
}
}
@media screen and (max-width: 575px) {
font-size: 1.5em;
}
}
&__button {
height: 54px;
width: 198px;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14);
color: #FFFFFF;
font-size: 20px;
font-weight: 500;
line-height: 26px;
&__copy {
font-size: 1em;
width: 400px;
max-width: 90vw;
text-align: center;
text-transform: uppercase;
margin: 35px 0 14px;
transition: 200ms ease-in-out;
background-color: rgba(247, 134, 28, 0.9);
@media screen and (max-width: 575px) {
font-size: .9em;
}
}
}
&__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 {
font-family: Roboto, Arial;
color: #4d4d4d;
font-weight: 300;
font-weight: 400;
background: #f7f7f7;
width: 100%;
height: 100%;
@ -205,10 +205,8 @@ input.large-input {
}
&__content {
height: 100%;
overflow-y: auto;
min-height: 250px;
max-height: 400px;
flex: 1;
}
&__warning-container {
@ -256,6 +254,7 @@ input.large-input {
overflow-y: auto;
background-color: $white;
border-radius: 0;
flex: 1;
}
}

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

@ -31,6 +31,7 @@ const {
} = require('./components/send/send-utils')
const { isValidAddress } = require('./util')
const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes')
const Button = require('./components/button')
SendTransactionScreen.contextTypes = {
t: PropTypes.func,
@ -497,13 +498,19 @@ SendTransactionScreen.prototype.renderFooter = function () {
const noErrors = !amountError && toError === null
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: () => {
clearSend()
history.push(DEFAULT_ROUTE)
},
}, 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,
onClick: event => this.onSubmit(event),
}, 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,
})
}
Loading…
Cancel
Save