1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Merge branch 'dev' into TxManager

This commit is contained in:
Frankie 2016-12-21 14:31:07 -08:00 committed by GitHub
commit 6f7c23fd28
38 changed files with 813 additions and 362 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist
npm-debug.log
node_modules
temp
.tmp
@ -13,4 +14,4 @@ app/.DS_Store
development/bundle.js
builds.zip
test/integration/bundle.js
npm-debug.log
development/states.js

View File

@ -3,7 +3,23 @@
## Current Master
- Add a check for when a tx is included in a block.
## 2.14.1 2016-12-20
- Temporarily disable extension reload detection causing infinite reload bug.
- Implemented basic checking for valid RPC URIs.
## 2.14.0 2016-12-16
- Removed Morden testnet provider from provider menu.
- Add support for notices.
- Fix broken reload detection.
- Fix transaction forever cached-as-pending bug.
## 2.13.11 2016-11-23
- Add support for synchronous RPC method "eth_uninstallFilter".
- Forgotten password prompts now send users directly to seed word restoration.
## 2.13.10 2016-11-22

View File

@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "2.13.10",
"version": "2.14.1",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",

View File

@ -1,6 +1,5 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask'
const MORDEN_RPC_URL = 'https://morden.infura.io/metamask'
const DEFAULT_RPC_URL = TESTNET_RPC_URL
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
@ -11,6 +10,6 @@ module.exports = {
default: DEFAULT_RPC_URL,
mainnet: MAINET_RPC_URL,
testnet: TESTNET_RPC_URL,
morden: MORDEN_RPC_URL,
morden: TESTNET_RPC_URL,
},
}

View File

@ -2,8 +2,8 @@
cleanContextForImports()
require('web3/dist/web3.min.js')
const LocalMessageDuplexStream = require('post-message-stream')
const PingStream = require('ping-pong-stream/ping')
const endOfStream = require('end-of-stream')
// const PingStream = require('ping-pong-stream/ping')
// const endOfStream = require('end-of-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
@ -40,13 +40,15 @@ reloadStream.once('data', triggerReload)
// setup ping timeout autoreload
// LocalMessageDuplexStream does not self-close, so reload if pingStream fails
var pingChannel = inpageProvider.multiStream.createStream('pingpong')
var pingStream = new PingStream({ objectMode: true })
// var pingChannel = inpageProvider.multiStream.createStream('pingpong')
// var pingStream = new PingStream({ objectMode: true })
// wait for first successful reponse
metamaskStream.once('_data', function () {
pingStream.pipe(pingChannel).pipe(pingStream)
})
endOfStream(pingStream, triggerReload)
// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully
// metamaskStream.once('data', function(){
// pingStream.pipe(pingChannel).pipe(pingStream)
// })
// endOfStream(pingStream, triggerReload)
// set web3 defaultAcount
inpageProvider.publicConfigStore.subscribe(function (state) {

View File

@ -2,8 +2,9 @@ const ethUtil = require('ethereumjs-util')
const bip39 = require('bip39')
const EventEmitter = require('events').EventEmitter
const filter = require('promise-filter')
const encryptor = require('browser-passworder')
const normalize = require('./lib/sig-util').normalize
const encryptor = require('./lib/encryptor')
const messageManager = require('./lib/message-manager')
const IdStoreMigrator = require('./lib/idStore-migrator')
const BN = ethUtil.BN
@ -106,6 +107,7 @@ module.exports = class KeyringController extends EventEmitter {
conversionDate: this.configManager.getConversionDate(),
keyringTypes: this.keyringTypes.map(krt => krt.type),
identities: this.identities,
lostAccounts: this.configManager.getLostAccounts(),
}
}
@ -430,11 +432,17 @@ module.exports = class KeyringController extends EventEmitter {
// may be completed without interruption.
migrateOldVaultIfAny (password) {
const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault()
if (!shouldMigrate) {
return Promise.resolve()
}
return this.idStoreMigrator.migratedVaultForPassword(password)
.then((serialized) => {
.then((result) => {
this.password = password
if (serialized && shouldMigrate) {
if (result && shouldMigrate) {
const { serialized, lostAccounts } = result
this.configManager.setLostAccounts(lostAccounts)
return this.restoreKeyring(serialized)
.then(keyring => keyring.getAccounts())
.then((accounts) => {

View File

@ -33,15 +33,15 @@ class HdKeyring extends EventEmitter {
this.mnemonic = null
this.root = null
if ('mnemonic' in opts) {
if (opts.mnemonic) {
this._initFromMnemonic(opts.mnemonic)
}
if ('numberOfAccounts' in opts) {
this.addAccounts(opts.numberOfAccounts)
if (opts.numberOfAccounts) {
return this.addAccounts(opts.numberOfAccounts)
}
return Promise.resolve()
return Promise.resolve([])
}
addAccounts (numberOfAccounts = 1) {

View File

@ -380,3 +380,14 @@ ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) {
data.gasMultiplier = gasMultiplier
this.setData(data)
}
ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
var data = this.getData()
data.lostAccounts = lostAccounts
this.setData(data)
}
ConfigManager.prototype.getLostAccounts = function () {
var data = this.getData()
return data.lostAccounts || []
}

View File

@ -1,156 +0,0 @@
module.exports = {
// Simple encryption methods:
encrypt,
decrypt,
// More advanced encryption methods:
keyFromPassword,
encryptWithKey,
decryptWithKey,
// Buffer <-> String methods
convertArrayBufferViewtoString,
convertStringToArrayBufferView,
// Buffer <-> Hex string methods
serializeBufferForStorage,
serializeBufferFromStorage,
// Buffer <-> base64 string methods
encodeBufferToBase64,
decodeBase64ToBuffer,
generateSalt,
}
// Takes a Pojo, returns cypher text.
function encrypt (password, dataObj) {
const salt = this.generateSalt()
return keyFromPassword(password + salt)
.then(function (passwordDerivedKey) {
return encryptWithKey(passwordDerivedKey, dataObj)
})
.then(function (payload) {
payload.salt = salt
return JSON.stringify(payload)
})
}
function encryptWithKey (key, dataObj) {
var data = JSON.stringify(dataObj)
var dataBuffer = convertStringToArrayBufferView(data)
var vector = global.crypto.getRandomValues(new Uint8Array(16))
return global.crypto.subtle.encrypt({
name: 'AES-GCM',
iv: vector,
}, key, dataBuffer).then(function (buf) {
var buffer = new Uint8Array(buf)
var vectorStr = encodeBufferToBase64(vector)
var vaultStr = encodeBufferToBase64(buffer)
return {
data: vaultStr,
iv: vectorStr,
}
})
}
// Takes encrypted text, returns the restored Pojo.
function decrypt (password, text) {
const payload = JSON.parse(text)
const salt = payload.salt
return keyFromPassword(password + salt)
.then(function (key) {
return decryptWithKey(key, payload)
})
}
function decryptWithKey (key, payload) {
const encryptedData = decodeBase64ToBuffer(payload.data)
const vector = decodeBase64ToBuffer(payload.iv)
return crypto.subtle.decrypt({name: 'AES-GCM', iv: vector}, key, encryptedData)
.then(function (result) {
const decryptedData = new Uint8Array(result)
const decryptedStr = convertArrayBufferViewtoString(decryptedData)
const decryptedObj = JSON.parse(decryptedStr)
return decryptedObj
})
.catch(function (reason) {
throw new Error('Incorrect password')
})
}
function convertStringToArrayBufferView (str) {
var bytes = new Uint8Array(str.length)
for (var i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i)
}
return bytes
}
function convertArrayBufferViewtoString (buffer) {
var str = ''
for (var i = 0; i < buffer.byteLength; i++) {
str += String.fromCharCode(buffer[i])
}
return str
}
function keyFromPassword (password) {
var passBuffer = convertStringToArrayBufferView(password)
return global.crypto.subtle.digest('SHA-256', passBuffer)
.then(function (passHash) {
return global.crypto.subtle.importKey('raw', passHash, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt'])
})
}
function serializeBufferFromStorage (str) {
var stripStr = (str.slice(0, 2) === '0x') ? str.slice(2) : str
var buf = new Uint8Array(stripStr.length / 2)
for (var i = 0; i < stripStr.length; i += 2) {
var seg = stripStr.substr(i, 2)
buf[i / 2] = parseInt(seg, 16)
}
return buf
}
// Should return a string, ready for storage, in hex format.
function serializeBufferForStorage (buffer) {
var result = '0x'
var len = buffer.length || buffer.byteLength
for (var i = 0; i < len; i++) {
result += unprefixedHex(buffer[i])
}
return result
}
function unprefixedHex (num) {
var hex = num.toString(16)
while (hex.length < 2) {
hex = '0' + hex
}
return hex
}
function encodeBufferToBase64 (buf) {
var b64encoded = btoa(String.fromCharCode.apply(null, buf))
return b64encoded
}
function decodeBase64ToBuffer (base64) {
var buf = new Uint8Array(atob(base64).split('')
.map(function (c) {
return c.charCodeAt(0)
}))
return buf
}
function generateSalt (byteCount = 32) {
var view = new Uint8Array(byteCount)
global.crypto.getRandomValues(view)
var b64encoded = btoa(String.fromCharCode.apply(null, view))
return b64encoded
}

View File

@ -1,5 +1,8 @@
const IdentityStore = require('./idStore')
const HdKeyring = require('../keyrings/hd')
const sigUtil = require('./sig-util')
const normalize = sigUtil.normalize
const denodeify = require('denodeify')
module.exports = class IdentityStoreMigrator {
@ -23,15 +26,13 @@ module.exports = class IdentityStoreMigrator {
return Promise.resolve(null)
}
return new Promise((resolve, reject) => {
this.idStore.submitPassword(password, (err) => {
if (err) return reject(err)
try {
resolve(this.serializeVault())
} catch (e) {
reject(e)
}
})
const idStore = this.idStore
const submitPassword = denodeify(idStore.submitPassword.bind(idStore))
return submitPassword(password)
.then(() => {
const serialized = this.serializeVault()
return this.checkForLostAccounts(serialized)
})
}
@ -45,6 +46,28 @@ module.exports = class IdentityStoreMigrator {
}
}
checkForLostAccounts (serialized) {
const hd = new HdKeyring()
return hd.deserialize(serialized.data)
.then((hexAccounts) => {
const newAccounts = hexAccounts.map(normalize)
const oldAccounts = this.idStore._getAddresses().map(normalize)
const lostAccounts = oldAccounts.reduce((result, account) => {
if (newAccounts.includes(account)) {
return result
} else {
result.push(account)
return result
}
}, [])
return {
serialized,
lostAccounts,
}
})
}
hasOldVault () {
const wallet = this.configManager.getWallet()
return wallet

View File

@ -2,6 +2,7 @@ const extend = require('xtend')
const EthStore = require('eth-store')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const KeyringController = require('./keyring-controller')
const NoticeController = require('./notice-controller')
const messageManager = require('./lib/message-manager')
const TxManager = require('./transaction-manager')
const HostStore = require('./lib/remote-store.js').HostStore
@ -23,6 +24,13 @@ module.exports = class MetamaskController {
txManager: this.txManager,
getNetwork: this.getStateNetwork.bind(this),
})
// notices
this.noticeController = new NoticeController({
configManager: this.configManager,
})
this.noticeController.updateNoticesList()
// to be uncommented when retrieving notices from a remote server.
// this.noticeController.startPolling()
this.provider = this.initializeProvider(opts)
this.ethStore = new EthStore(this.provider)
this.keyringController.setStore(this.ethStore)
@ -57,12 +65,15 @@ module.exports = class MetamaskController {
this.configManager.getConfig(),
this.keyringController.getState(),
this.txManager.getState()
this.noticeController.getState()
)
}
getApi () {
const keyringController = this.keyringController
const txManager = this.txManager
const noticeController = this.noticeController
return {
getState: (cb) => { cb(null, this.getState()) },
setRpcTarget: this.setRpcTarget.bind(this),
@ -101,6 +112,9 @@ module.exports = class MetamaskController {
buyEth: this.buyEth.bind(this),
// shapeshift
createShapeShiftTx: this.createShapeShiftTx.bind(this),
// notices
checkNotices: noticeController.updateNoticesList.bind(noticeController),
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
}
}
@ -292,7 +306,7 @@ module.exports = class MetamaskController {
setTOSHash (hash) {
try {
this.configManager.setTOSHash(hash)
} catch (e) {
} catch (err) {
console.error('Error in setting terms of service hash.')
}
}
@ -304,17 +318,19 @@ module.exports = class MetamaskController {
this.resetDisclaimer()
this.setTOSHash(global.TOS_HASH)
}
} catch (e) {
} catch (err) {
console.error('Error in checking TOS change.')
}
}
// disclaimer
agreeToDisclaimer (cb) {
try {
this.configManager.setConfirmedDisclaimer(true)
cb()
} catch (e) {
cb(e)
} catch (err) {
cb(err)
}
}
@ -337,8 +353,8 @@ module.exports = class MetamaskController {
conversionDate: this.configManager.getConversionDate(),
}
cb(data)
} catch (e) {
cb(null, e)
} catch (err) {
cb(null, err)
}
}
@ -411,8 +427,8 @@ module.exports = class MetamaskController {
try {
this.configManager.setGasMultiplier(gasMultiplier)
cb()
} catch (e) {
cb(e)
} catch (err) {
cb(err)
}
}

View File

@ -0,0 +1,96 @@
const EventEmitter = require('events').EventEmitter
const hardCodedNotices = require('../../development/notices.json')
module.exports = class NoticeController extends EventEmitter {
constructor (opts) {
super()
this.configManager = opts.configManager
this.noticePoller = null
}
getState () {
var lastUnreadNotice = this.getLatestUnreadNotice()
return {
lastUnreadNotice: lastUnreadNotice,
noActiveNotices: !lastUnreadNotice,
}
}
getNoticesList () {
var data = this.configManager.getData()
if ('noticesList' in data) {
return data.noticesList
} else {
return []
}
}
setNoticesList (list) {
var data = this.configManager.getData()
data.noticesList = list
this.configManager.setData(data)
return Promise.resolve(true)
}
markNoticeRead (notice, cb) {
cb = cb || function (err) { if (err) throw err }
try {
var notices = this.getNoticesList()
var id = notice.id
notices[id].read = true
this.setNoticesList(notices)
const latestNotice = this.getLatestUnreadNotice()
cb(null, latestNotice)
} catch (err) {
cb(err)
}
}
updateNoticesList () {
return this._retrieveNoticeData().then((newNotices) => {
var oldNotices = this.getNoticesList()
var combinedNotices = this._mergeNotices(oldNotices, newNotices)
return Promise.resolve(this.setNoticesList(combinedNotices))
})
}
getLatestUnreadNotice () {
var notices = this.getNoticesList()
var filteredNotices = notices.filter((notice) => {
return notice.read === false
})
return filteredNotices[filteredNotices.length - 1]
}
startPolling () {
if (this.noticePoller) {
clearInterval(this.noticePoller)
}
this.noticePoller = setInterval(() => {
this.noticeController.updateNoticesList()
}, 300000)
}
_mergeNotices (oldNotices, newNotices) {
var noticeMap = this._mapNoticeIds(oldNotices)
newNotices.forEach((notice) => {
if (noticeMap.indexOf(notice.id) === -1) {
oldNotices.push(notice)
}
})
return oldNotices
}
_mapNoticeIds (notices) {
return notices.map((notice) => notice.id)
}
_retrieveNoticeData () {
// Placeholder for the API.
return Promise.resolve(hardCodedNotices)
}
}

View File

@ -0,0 +1,36 @@
var fsp = require('fs-promise')
var path = require('path')
var prompt = require('prompt')
var open = require('open')
var extend = require('extend')
var notices = require('./notices.json')
var id = 0
var date = new Date().toDateString()
var notice = {
read: false,
date: date,
}
fsp.readdir('notices')
.then((files) => {
files.forEach(file => { id ++ })
Promise.resolve()
}).then(() => {
fsp.writeFile(`notices/notice_${id}.md`,'Message goes here. Please write out your notice and save before proceeding at the command line.')
.then(() => {
open(`notices/notice_${id}.md`)
prompt.start()
prompt.get(['title'], (err, result) => {
notice.title = result.title
fsp.readFile(`notices/notice_${id}.md`)
.then((body) => {
notice.body = body.toString()
notice.id = id
notices.push(notice)
return fsp.writeFile(`development/notices.json`, JSON.stringify(notices))
})
})
})
})

1
development/notices.json Normal file
View File

@ -0,0 +1 @@
[{"read":false,"date":"Fri Dec 16 2016","title":"Ending Morden Support","body":"Due to [recent events](https://blog.ethereum.org/2016/11/20/from-morden-to-ropsten/), MetaMask is now deprecating support for the Morden Test Network.\n\nUsers will still be able to access Morden through a locally hosted node, but we will no longer be providing hosted access to this network through [Infura](http://infura.io/).\n\nPlease use the new Ropsten Network as your new default test network.\n\nYou can fund your Ropsten account using the buy button on your account page.\n\nBest wishes!\nThe MetaMask Team\n\n","id":0}]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,62 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"identities": {
"0x24a1d059462456aa332d6da9117aa7f91a46f2ac": {
"address": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac",
"name": "Account 1"
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 8.3533002,
"conversionDate": 1481671082,
"noActiveNotices": false,
"lastUnreadNotice": {
"read": false,
"date": "Tue Dec 13 2016",
"title": "MultiVault Support",
"body": "# Multi\n# Line\n## Support\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi tincidunt dapibus justo a auctor. Sed luctus metus non mi laoreet, sit amet placerat nibh ultricies. Cras fringilla, urna sit amet sodales porttitor, lacus risus lacinia lorem, non euismod magna felis id ex. Nam iaculis, ante nec imperdiet suscipit, nisi quam fringilla nisl, sed fringilla turpis lectus et nibh. Pellentesque sed neque pretium nulla elementum lacinia eu eget felis. Nulla facilisi. Pellentesque id mi tempor, tempus sapien id, ultricies nibh. Integer faucibus elit non orci dapibus porttitor. Pellentesque rutrum hendrerit sapien ut lacinia. Nunc elementum eget arcu eu volutpat. Integer ullamcorper aliquam metus, eu malesuada tellus vestibulum a.\n",
"id": 0
},
"network": "3",
"accounts": {
"0x24a1d059462456aa332d6da9117aa7f91a46f2ac": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac"
}
},
"transactions": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac",
"seedWords": null,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree"
]
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"detailView": null,
"context": "0x24a1d059462456aa332d6da9117aa7f91a46f2ac"
},
"accountDetail": {
"subview": "transactions"
},
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -0,0 +1,40 @@
{
"metamask": {
"isInitialized": false,
"isUnlocked": false,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 8.18703468,
"conversionDate": 1481755832,
"network": "3",
"accounts": {},
"transactions": [],
"provider": {
"type": "testnet"
},
"isDisclaimerConfirmed": false,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree"
]
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accounts",
"detailView": null
},
"accountDetail": {
"subview": "transactions"
},
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

12
notices/notice_0.md Normal file
View File

@ -0,0 +1,12 @@
Due to [recent events](https://blog.ethereum.org/2016/11/20/from-morden-to-ropsten/), MetaMask is now deprecating support for the Morden Test Network.
Users will still be able to access Morden through a locally hosted node, but we will no longer be providing hosted access to this network through [Infura](http://infura.io/).
Please use the new Ropsten Network as your new default test network.
You can fund your Ropsten account using the buy button on your account page.
Best wishes!
The MetaMask Team

View File

@ -12,12 +12,14 @@
"test": "npm run fastTest && npm run ci && npm run lint",
"fastTest": "METAMASK_ENV=test mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"ui": "node development/genStates.js && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"genStates": "node development/genStates.js",
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"buildMock": "browserify ./mock-dev.js -o ./development/bundle.js",
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
"testem": "npm run buildMock && testem",
"ci": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
"announce": "node development/announcer.js"
"announce": "node development/announcer.js",
"generateNotice": "node development/notice-generator.js"
},
"browserify": {
"transform": [
@ -35,10 +37,12 @@
"dependencies": {
"async": "^1.5.2",
"bip39": "^2.2.0",
"browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4",
"clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0",
"debounce": "^1.0.0",
"denodeify": "^1.2.1",
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
"ensnare": "^1.0.0",
@ -50,6 +54,8 @@
"ethereumjs-util": "^4.4.0",
"ethereumjs-wallet": "^0.6.0",
"express": "^4.14.0",
"extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0",
"gulp-eslint": "^2.0.0",
"hat": "0.0.3",
"identicon.js": "^1.2.1",
@ -85,9 +91,10 @@
"textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.1",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "0.17.0-beta",
"web3-provider-engine": "^8.1.5",
"web3-provider-engine": "^8.1.14",
"web3-stream-provider": "^2.0.6",
"xtend": "^4.0.1"
},
@ -101,6 +108,7 @@
"chai": "^3.5.0",
"deep-freeze-strict": "^1.1.1",
"del": "^2.2.0",
"fs-promise": "^1.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-brfs": "^0.1.0",
"gulp-if": "^2.0.1",
@ -120,6 +128,8 @@
"mocha-jsdom": "^1.1.0",
"mocha-sinon": "^1.1.5",
"nock": "^8.0.0",
"open": "0.0.5",
"prompt": "^1.0.0",
"qs": "^6.2.0",
"qunit": "^0.9.1",
"sinon": "^1.17.3",

View File

@ -1,71 +0,0 @@
var encryptor = require('../../../app/scripts/lib/encryptor')
QUnit.module('encryptor')
QUnit.test('encryptor:serializeBufferForStorage', function (assert) {
assert.expect(1)
var buf = new Buffer(2)
buf[0] = 16
buf[1] = 1
var output = encryptor.serializeBufferForStorage(buf)
var expect = '0x1001'
assert.equal(expect, output)
})
QUnit.test('encryptor:serializeBufferFromStorage', function (assert) {
assert.expect(2)
var input = '0x1001'
var output = encryptor.serializeBufferFromStorage(input)
assert.equal(output[0], 16)
assert.equal(output[1], 1)
})
QUnit.test('encryptor:encrypt & decrypt', function(assert) {
var done = assert.async();
var password, data, encrypted
password = 'a sample passw0rd'
data = { foo: 'data to encrypt' }
encryptor.encrypt(password, data)
.then(function(encryptedStr) {
assert.equal(typeof encryptedStr, 'string', 'returns a string')
return encryptor.decrypt(password, encryptedStr)
})
.then(function (decryptedObj) {
assert.deepEqual(decryptedObj, data, 'decrypted what was encrypted')
done()
})
.catch(function(reason) {
assert.ifError(reason, 'threw an error')
done(reason)
})
})
QUnit.test('encryptor:encrypt & decrypt with wrong password', function(assert) {
var done = assert.async();
var password, data, encrypted, wrongPassword
password = 'a sample passw0rd'
wrongPassword = 'a wrong password'
data = { foo: 'data to encrypt' }
encryptor.encrypt(password, data)
.then(function(encryptedStr) {
assert.equal(typeof encryptedStr, 'string', 'returns a string')
return encryptor.decrypt(wrongPassword, encryptedStr)
})
.then(function (decryptedObj) {
assert.equal(!decryptedObj, true, 'Wrong password should not decrypt')
done()
})
.catch(function(reason) {
done()
})
})

View File

@ -2,11 +2,14 @@ var KeyringController = require('../../../app/scripts/keyring-controller')
var ConfigManager = require('../../../app/scripts/lib/config-manager')
var oldStyleVault = require('../mocks/oldVault.json')
var badStyleVault = require('../mocks/badVault.json')
var STORAGE_KEY = 'metamask-config'
var PASSWORD = '12345678'
var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase()
var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
QUnit.module('Old Style Vaults', {
beforeEach: function () {
@ -45,6 +48,63 @@ QUnit.test('keyringController:submitPassword', function (assert) {
this.keyringController.submitPassword(PASSWORD)
.then((state) => {
assert.ok(state.identities[FIRST_ADDRESS])
assert.equal(state.lostAccounts.length, 0, 'no lost accounts')
done()
})
})
QUnit.test('keyringController:setLocked', function (assert) {
var done = assert.async()
var self = this
this.keyringController.setLocked()
.then(function() {
assert.notOk(self.keyringController.password, 'password should be deallocated')
assert.deepEqual(self.keyringController.keyrings, [], 'keyrings should be deallocated')
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
QUnit.module('Old Style Vaults with bad HD seed', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.keyringController = new KeyringController({
configManager: this.configManager,
getNetwork: () => { return '2' },
})
this.ethStore = {
addAccount: () => {},
removeAccount: () => {},
}
this.keyringController.setStore(this.ethStore)
}
})
QUnit.test('keyringController:isInitialized', function (assert) {
assert.ok(this.keyringController.getState().isInitialized, 'vault is initialized')
})
QUnit.test('keyringController:submitPassword', function (assert) {
var done = assert.async()
this.keyringController.submitPassword(PASSWORD)
.then((state) => {
assert.ok(state.identities[BAD_STYLE_FIRST_ADDRESS])
assert.equal(state.lostAccounts.length, 1, 'one lost account')
assert.equal(state.lostAccounts[0], '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase())
assert.deepEqual(this.configManager.getLostAccounts(), state.lostAccounts, 'persisted')
done()
})
})

View File

@ -0,0 +1 @@
{"meta":{"version":4},"data":{"fiatCurrency":"USD","conversionRate":8.34908448,"conversionDate":1481227505,"isConfirmed":true,"wallet":"{\"encSeed\":{\"encStr\":\"Te2KyAGY3S01bgUJ+7d4y3BOvr/8TKrXrkRZ29cGI6dgyedtN+YgTQxElC2td/pzuoXm7KeSfr+yAoFCvMgqFAJwRcX3arHOsMFQie8kp8mL5I65zwdg/HB2QecB4OJHytrxgApv2zZiKEo0kbu2cs8zYIn5wNlCBIHwgylYmHpUDIJcO1B4zg==\",\"nonce\":\"xnxqk4iy70bjt721F+KPLV4PNfBFNyct\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"vNrSjekRKLmaGFf77Uca9+aAebmDlvrBwtAV8YthpQ4OX/mXtLSycmnLsYdk4schaByfJvrm6/Mf9fxzOSaScJk+XvKw5XqNXedkDHtbWrmNnxFpuT+9tuB8Nupr3D9GZK9PgXhJD99/7Bn6Wk7/ne+PIDmbtdmx/SWmrdo3pg==\",\"nonce\":\"zqWq/gtJ5zfUVRWQQJkP/zoYjer6Rozj\"},\"hdIndex\":1,\"encPrivKeys\":{\"e15d894becb0354c501ae69429b05143679f39e0\":{\"key\":\"jBLQ9v1l5LOEY1C3kI8z7LpbJKHP1vpVfPAlz90MNSfa8Oe+XlxKQAGYs8Zb4fWm\",\"nonce\":\"fJyrSRo1t0RMNqp2MsneoJnYJWHQnSVY\"}},\"addresses\":[\"e15d894becb0354c501ae69429b05143679f39e0\"]}},\"encHdRootPriv\":{\"encStr\":\"mbvwiFBQGbjj4BJLmdeYzfYi8jb7gtFtwiCQOPfvmyz4h2/KMbHNGzumM16qRKpifioQXkhnBulMIQHaYg0Jwv1MoFsqHxHmuIAT+QP5XvJjz0MRl6708pHowmIVG+R8CZNTLqzE7XS8YkZ4ElRpTvLEM8Wngi5Sg287mQMP9w==\",\"nonce\":\"i5Tp2lQe92rXQzNhjZcu9fNNhfux6Wf4\"},\"salt\":\"FQpA8D9R/5qSp9WtQ94FILyfWZHMI6YZw6RmBYqK0N0=\",\"version\":2}","config":{"provider":{"type":"testnet"},"selectedAccount":"0xe15d894becb0354c501ae69429b05143679f39e0"},"isEthConfirmed":true,"transactions":[],"TOSHash":"a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b","gasMultiplier":1}}

View File

@ -1,12 +1,12 @@
const assert = require('assert')
const extend = require('xtend')
const STORAGE_KEY = 'metamask-persistance-key'
var configManagerGen = require('../lib/mock-config-manager')
var configManager
const rp = require('request-promise')
const nock = require('nock')
var configManagerGen = require('../lib/mock-config-manager')
const STORAGE_KEY = 'metamask-persistance-key'
describe('config-manager', function() {
var configManager
beforeEach(function() {
window.localStorage = {} // Hacking localStorage support into JSDom

View File

@ -21,6 +21,10 @@ const mockVault = {
account: '0x5d8de92c205279c10e5669f797b853ccef4f739a',
}
const badVault = {
seed: 'radar blur cabbage chef fix engine embark joy scheme fiction master release',
}
describe('IdentityStore to KeyringController migration', function() {
// The stars of the show:
@ -83,33 +87,8 @@ describe('IdentityStore to KeyringController migration', function() {
keyringController.configManager.setWallet('something')
const state = keyringController.getState()
assert(state.isInitialized, 'old vault counted as initialized.')
assert.equal(state.lostAccounts.length, 0, 'no lost accounts')
})
/*
it('should use the password to migrate the old vault', function(done) {
this.timeout(5000)
console.log('calling submitPassword')
console.dir(keyringController)
keyringController.submitPassword(password, function (err, state) {
assert.ifError(err, 'submitPassword threw error')
function log(str, dat) { console.log(str + ': ' + JSON.stringify(dat)) }
let newAccounts = keyringController.getAccounts()
log('new accounts: ', newAccounts)
let newAccount = ethUtil.addHexPrefix(newAccounts[0])
assert.equal(ethUtil.addHexPrefix(newAccount), mockVault.account, 'restored the correct account')
const newSeed = keyringController.keyrings[0].mnemonic
log('keyringController keyrings', keyringController.keyrings)
assert.equal(newSeed, mockVault.seed, 'seed phrase transferred.')
assert(configManager.getVault(), 'new type of vault is persisted')
done()
})
})
*/
})
})

View File

@ -99,21 +99,6 @@ describe('KeyringController', function() {
})
})
describe('#migrateOldVaultIfAny', function() {
it('should return and init a new vault', function(done) {
keyringController.migrateOldVaultIfAny(password)
.then(() => {
assert(keyringController.configManager.getVault(), 'now has a vault')
assert(keyringController.password, 'has a password set')
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
})
describe('#createNickname', function() {
it('should add the address to the identities hash', function() {
const fakeAddress = '0x12345678'

View File

@ -0,0 +1,115 @@
const assert = require('assert')
const extend = require('xtend')
const rp = require('request-promise')
const nock = require('nock')
const configManagerGen = require('../lib/mock-config-manager')
const NoticeController = require('../../app/scripts/notice-controller')
const STORAGE_KEY = 'metamask-persistance-key'
// Hacking localStorage support into JSDom
window.localStorage = {}
describe('notice-controller', function() {
var noticeController
beforeEach(function() {
let configManager = configManagerGen()
noticeController = new NoticeController({
configManager: configManager,
})
})
describe('notices', function() {
describe('#getNoticesList', function() {
it('should return an empty array when new', function() {
var testList = [{
id:0,
read:false,
title:"Futuristic Notice"
}]
var result = noticeController.getNoticesList()
assert.equal(result.length, 0)
})
})
describe('#setNoticesList', function() {
it('should set data appropriately', function () {
var testList = [{
id:0,
read:false,
title:"Futuristic Notice"
}]
noticeController.setNoticesList(testList)
var testListId = noticeController.getNoticesList()[0].id
assert.equal(testListId, 0)
})
})
describe('#updateNoticeslist', function() {
it('should integrate the latest changes from the source', function() {
var testList = [{
id:55,
read:false,
title:"Futuristic Notice"
}]
noticeController.setNoticesList(testList)
noticeController.updateNoticesList().then(() => {
var newList = noticeController.getNoticesList()
assert.ok(newList[0].id === 55)
assert.ok(newList[1])
})
})
it('should not overwrite any existing fields', function () {
var testList = [{
id:0,
read:false,
title:"Futuristic Notice"
}]
noticeController.setNoticesList(testList)
noticeController.updateNoticesList().then(() => {
var newList = noticeController.getNoticesList()
assert.equal(newList[0].id, 0)
assert.equal(newList[0].title, "Futuristic Notice")
assert.equal(newList.length, 1)
})
})
})
describe('#markNoticeRead', function () {
it('should mark a notice as read', function () {
var testList = [{
id:0,
read:false,
title:"Futuristic Notice"
}]
noticeController.setNoticesList(testList)
noticeController.markNoticeRead(testList[0])
var newList = noticeController.getNoticesList()
assert.ok(newList[0].read)
})
})
describe('#getLatestUnreadNotice', function () {
it('should retrieve the latest unread notice', function () {
var testList = [
{id:0,read:true,title:"Past Notice"},
{id:1,read:false,title:"Current Notice"},
{id:2,read:false,title:"Future Notice"},
]
noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice()
assert.equal(latestUnread.id, 2)
})
it('should return undefined if no unread notices exist.', function () {
var testList = [
{id:0,read:true,title:"Past Notice"},
{id:1,read:true,title:"Current Notice"},
{id:2,read:true,title:"Future Notice"},
]
noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice()
assert.ok(!latestUnread)
})
})
})
})

View File

@ -13,12 +13,21 @@ var actions = {
// remote state
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
updateMetamaskState: updateMetamaskState,
// notices
MARK_NOTICE_READ: 'MARK_NOTICE_READ',
markNoticeRead: markNoticeRead,
SHOW_NOTICE: 'SHOW_NOTICE',
showNotice: showNotice,
CLEAR_NOTICES: 'CLEAR_NOTICES',
clearNotices: clearNotices,
// intialize screen
AGREE_TO_DISCLAIMER: 'AGREE_TO_DISCLAIMER',
agreeToDisclaimer: agreeToDisclaimer,
CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
FORGOT_PASSWORD: 'FORGOT_PASSWORD',
forgotPassword: forgotPassword,
SHOW_INIT_MENU: 'SHOW_INIT_MENU',
SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
@ -177,13 +186,13 @@ function tryUnlockMetamask (password) {
}
}
function transitionForward() {
function transitionForward () {
return {
type: this.TRANSITION_FORWARD,
}
}
function transitionBackward() {
function transitionBackward () {
return {
type: this.TRANSITION_BACKWARD,
}
@ -380,6 +389,12 @@ function showRestoreVault () {
}
}
function forgotPassword () {
return {
type: actions.FORGOT_PASSWORD,
}
}
function showInitializeMenu () {
return {
type: actions.SHOW_INIT_MENU,
@ -539,6 +554,43 @@ function goBackToInitView () {
}
}
//
// notice
//
function markNoticeRead (notice) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
background.markNoticeRead(notice, (err, notice) => {
dispatch(this.hideLoadingIndication())
if (err) {
return dispatch(actions.showWarning(err))
}
if (notice) {
return dispatch(actions.showNotice(notice))
} else {
dispatch(this.clearNotices())
return {
type: actions.SHOW_ACCOUNTS_PAGE,
}
}
})
}
}
function showNotice (notice) {
return {
type: actions.SHOW_NOTICE,
value: notice,
}
}
function clearNotices () {
return {
type: actions.CLEAR_NOTICES,
}
}
//
// config
//

View File

@ -15,6 +15,8 @@ const AccountsScreen = require('./accounts')
const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send')
const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./notice')
// other views
const ConfigScreen = require('./config')
const InfoScreen = require('./info')
@ -40,6 +42,7 @@ function mapStateToProps (state) {
// state from plugin
isLoading: state.appState.isLoading,
isDisclaimerConfirmed: state.metamask.isDisclaimerConfirmed,
noActiveNotices: state.metamask.noActiveNotices,
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
currentView: state.appState.currentView,
@ -240,15 +243,6 @@ App.prototype.renderNetworkDropdown = function () {
provider: props.provider,
}),
h(DropMenuItem, {
label: 'Morden Test Network',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => props.dispatch(actions.setProviderType('morden')),
icon: h('.menu-icon.red-dot'),
activeNetworkRender: props.network,
provider: props.provider,
}),
h(DropMenuItem, {
label: 'Localhost 8545',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
@ -372,6 +366,10 @@ App.prototype.renderPrimary = function () {
}
}
if (!props.noActiveNotices) {
return h(NoticeScreen, {key: 'NoticeScreen'})
}
// show current view
switch (props.currentView.name) {

View File

@ -44,9 +44,6 @@ DropMenuItem.prototype.activeNetworkRender = function () {
case 'Ropsten Test Network':
if (provider.type === 'testnet') return h('.check', '✓')
break
case 'Morden Test Network':
if (provider.type === 'morden') return h('.check', '✓')
break
case 'Localhost 8545':
if (activeNetwork === 'http://localhost:8545') return h('.check', '✓')
break

View File

@ -40,9 +40,6 @@ Network.prototype.render = function () {
} else if (parseInt(networkNumber) === 3) {
hoverText = 'Ropsten Test Network'
iconName = 'ropsten-test-network'
} else if (parseInt(networkNumber) === 2) {
hoverText = 'Morden Test Network'
iconName = 'morden-test-network'
} else {
hoverText = 'Unknown Private Network'
iconName = 'unknown-private-network'
@ -77,15 +74,6 @@ Network.prototype.render = function () {
}},
'Ropsten Test Net'),
])
case 'morden-test-network':
return h('.network-indicator', [
h('.menu-icon.red-dot'),
h('.network-name', {
style: {
color: '#ff6666',
}},
'Morden Test Net'),
])
default:
return h('.network-indicator', [
h('i.fa.fa-question-circle.fa-lg', {

View File

@ -4,11 +4,13 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const currencies = require('./conversion.json').rows
const validUrl = require('valid-url')
module.exports = connect(mapStateToProps)(ConfigScreen)
function mapStateToProps (state) {
return {
metamask: state.metamask,
warning: state.appState.warning,
}
}
@ -20,6 +22,7 @@ function ConfigScreen () {
ConfigScreen.prototype.render = function () {
var state = this.props
var metamaskState = state.metamask
var warning = state.warning
return (
h('.flex-column.flex-grow', [
@ -34,6 +37,14 @@ ConfigScreen.prototype.render = function () {
h('h2.page-subtitle', 'Settings'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
@ -57,7 +68,7 @@ ConfigScreen.prototype.render = function () {
if (event.key === 'Enter') {
var element = event.target
var newRpc = element.value
state.dispatch(actions.setRpcTarget(newRpc))
rpcValidation(newRpc, state)
}
},
}),
@ -69,7 +80,7 @@ ConfigScreen.prototype.render = function () {
event.preventDefault()
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
state.dispatch(actions.setRpcTarget(newRpc))
rpcValidation(newRpc, state)
},
}, 'Save'),
]),
@ -99,6 +110,19 @@ ConfigScreen.prototype.render = function () {
)
}
function rpcValidation (newRpc, state) {
if (validUrl.isWebUri(newRpc)) {
state.dispatch(actions.setRpcTarget(newRpc))
} else {
var appendedRpc = `http://${newRpc}`
if (validUrl.isWebUri(appendedRpc)) {
state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.'))
} else {
state.dispatch(actions.displayWarning('Invalid RPC URI'))
}
}
}
function currentConversionInformation (metamaskState, state) {
var currentFiat = metamaskState.currentFiat
var conversionDate = metamaskState.conversionDate
@ -133,7 +157,7 @@ function currentProviderDisplay (metamaskState) {
case 'testnet':
title = 'Current Network'
value = 'Morden Test Network'
value = 'Ropsten Test Network'
break
default:

View File

@ -6,6 +6,8 @@ const actions = require('../actions')
const ReactMarkdown = require('react-markdown')
const fs = require('fs')
const path = require('path')
const linker = require('extension-link-enabler')
const findDOMNode = require('react-dom').findDOMNode
const disclaimer = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'USER_AGREEMENT.md')).toString()
module.exports = connect(mapStateToProps)(DisclaimerScreen)
@ -98,3 +100,13 @@ DisclaimerScreen.prototype.render = function () {
])
)
}
DisclaimerScreen.prototype.componentDidMount = function () {
var node = findDOMNode(this)
linker.setupListener(node)
}
DisclaimerScreen.prototype.componentWillUnmount = function () {
var node = findDOMNode(this)
linker.teardownListener(node)
}

View File

@ -21,7 +21,6 @@ function mapStateToProps (state) {
// state from plugin
currentView: state.appState.currentView,
warning: state.appState.warning,
forgottenPassword: state.metamask.isInitialized,
}
}
@ -118,17 +117,6 @@ InitializeMenuScreen.prototype.renderMenu = function (state) {
},
}, 'Create'),
state.forgottenPassword ? h('.flex-row.flex-center.flex-grow', [
h('p.pointer', {
onClick: this.backToUnlockView.bind(this),
style: {
fontSize: '0.8em',
color: 'rgb(247, 134, 28)',
textDecoration: 'underline',
},
}, 'Return to Login'),
]) : null,
h('.flex-row.flex-center.flex-grow', [
h('p.pointer', {
onClick: this.showRestoreVault.bind(this),
@ -159,10 +147,6 @@ InitializeMenuScreen.prototype.showRestoreVault = function () {
this.props.dispatch(actions.showRestoreVault())
}
InitializeMenuScreen.prototype.backToUnlockView = function () {
this.props.dispatch(actions.backToUnlockView())
}
InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () {
var passwordBox = document.getElementById('password-box')
var password = passwordBox.value

View File

@ -14,6 +14,7 @@ function RestoreVaultScreen () {
function mapStateToProps (state) {
return {
warning: state.appState.warning,
forgottenPassword: state.appState.forgottenPassword,
}
}
@ -100,14 +101,17 @@ RestoreVaultScreen.prototype.render = function () {
}, 'OK'),
]),
])
)
}
RestoreVaultScreen.prototype.showInitializeMenu = function () {
if (this.props.forgottenPassword) {
this.props.dispatch(actions.backToUnlockView())
} else {
this.props.dispatch(actions.showInitializeMenu())
}
}
RestoreVaultScreen.prototype.createOnEnter = function (event) {

118
ui/app/notice.js Normal file
View File

@ -0,0 +1,118 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const ReactMarkdown = require('react-markdown')
const connect = require('react-redux').connect
const actions = require('./actions')
const linker = require('extension-link-enabler')
const findDOMNode = require('react-dom').findDOMNode
module.exports = connect(mapStateToProps)(Notice)
function mapStateToProps (state) {
return {
lastUnreadNotice: state.metamask.lastUnreadNotice,
}
}
inherits(Notice, Component)
function Notice () {
Component.call(this)
}
Notice.prototype.render = function () {
const props = this.props
const title = props.lastUnreadNotice.title
const date = props.lastUnreadNotice.date
return (
h('.flex-column.flex-center.flex-grow', [
h('h3.flex-center.text-transform-uppercacse.terms-header', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
width: '100%',
fontSize: '20px',
textAlign: 'center',
padding: 6,
},
}, [
title,
]),
h('h5.flex-center.text-transform-uppercacse.terms-header', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: 24,
width: '100%',
fontSize: '20px',
textAlign: 'center',
padding: 6,
},
}, [
date,
]),
h('style', `
.markdown {
overflow-x: hidden;
}
.markdown h1, .markdown h2, .markdown h3 {
margin: 10px 0;
font-weight: bold;
}
.markdown strong {
font-weight: bold;
}
.markdown em {
font-style: italic;
}
.markdown p {
margin: 10px 0;
}
.markdown a {
color: #df6b0e;
}
`),
h('div.markdown', {
style: {
background: 'rgb(235, 235, 235)',
height: '310px',
padding: '6px',
width: '90%',
overflowY: 'scroll',
scroll: 'auto',
},
}, [
h(ReactMarkdown, {
source: props.lastUnreadNotice.body,
skipHtml: true,
}),
]),
h('button', {
onClick: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
style: {
marginTop: '18px',
},
}, 'Continue'),
])
)
}
Notice.prototype.componentDidMount = function () {
var node = findDOMNode(this)
linker.setupListener(node)
}
Notice.prototype.componentWillUnmount = function () {
var node = findDOMNode(this)
linker.teardownListener(node)
}

View File

@ -72,6 +72,16 @@ function reduceApp (state, action) {
name: 'restoreVault',
},
transForward: true,
forgottenPassword: true,
})
case actions.FORGOT_PASSWORD:
return extend(appState, {
currentView: {
name: 'restoreVault',
},
transForward: false,
forgottenPassword: true,
})
case actions.SHOW_INIT_MENU:
@ -169,7 +179,7 @@ function reduceApp (state, action) {
return extend(appState, {
warning: null,
transForward: true,
forgottenPassword: !appState.forgottenPassword,
forgottenPassword: false,
currentView: {
name: 'UnlockScreen',
},
@ -248,6 +258,12 @@ function reduceApp (state, action) {
forgottenPassword: false,
})
case actions.SHOW_NOTICE:
return extend(appState, {
transForward: true,
isLoading: false,
})
case actions.REVEAL_ACCOUNT:
return extend(appState, {
scrollToBottom: true,

View File

@ -16,6 +16,8 @@ function reduceMetamask (state, action) {
currentFiat: 'USD',
conversionRate: 0,
conversionDate: 'N/A',
noActiveNotices: true,
lastUnreadNotice: undefined,
}, state.metamask)
switch (action.type) {
@ -25,6 +27,17 @@ function reduceMetamask (state, action) {
delete newState.seedWords
return newState
case actions.SHOW_NOTICE:
return extend(metamaskState, {
noActiveNotices: false,
lastUnreadNotice: action.value,
})
case actions.CLEAR_NOTICES:
return extend(metamaskState, {
noActiveNotices: true,
})
case actions.UPDATE_METAMASK_STATE:
return extend(metamaskState, action.value)

View File

@ -70,7 +70,7 @@ UnlockScreen.prototype.render = function () {
h('.flex-row.flex-center.flex-grow', [
h('p.pointer', {
onClick: () => this.props.dispatch(actions.goBackToInitView()),
onClick: () => this.props.dispatch(actions.forgotPassword()),
style: {
fontSize: '0.8em',
color: 'rgb(247, 134, 28)',