1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #927 from MetaMask/AddNotices

Add notices
This commit is contained in:
Dan Finlay 2016-12-16 13:50:07 -08:00 committed by GitHub
commit d24acc5f13
19 changed files with 607 additions and 21 deletions

2
.gitignore vendored
View File

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

View File

@ -9,6 +9,8 @@
## 2.13.11 2016-11-23
- Add support for synchronous RPC method "eth_uninstallFilter".
- Add support for notices.
- Add support for working links.
## 2.13.10 2016-11-22

View File

@ -2,6 +2,7 @@ const extend = require('xtend')
const EthStore = require('eth-store')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const IdentityStore = require('./lib/idStore')
const NoticeController = require('./notice-controller')
const messageManager = require('./lib/message-manager')
const HostStore = require('./lib/remote-store.js').HostStore
const Web3 = require('web3')
@ -17,6 +18,13 @@ module.exports = class MetamaskController {
this.idStore = new IdentityStore({
configManager: this.configManager,
})
// 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.idStore.setStore(this.ethStore)
@ -30,19 +38,20 @@ module.exports = class MetamaskController {
this.checkTOSChange()
this.scheduleConversionInterval()
}
getState () {
return extend(
this.ethStore.getState(),
this.idStore.getState(),
this.configManager.getConfig()
this.configManager.getConfig(),
this.noticeController.getState()
)
}
getApi () {
const idStore = this.idStore
const noticeController = this.noticeController
return {
getState: (cb) => { cb(null, this.getState()) },
@ -77,6 +86,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),
}
}
@ -271,7 +283,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.')
}
}
@ -283,26 +295,28 @@ 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.setConfirmed(true)
cb()
} catch (e) {
cb(e)
} catch (err) {
cb(err)
}
}
resetDisclaimer () {
try {
this.configManager.setConfirmed(false)
} catch (e) {
console.error(e)
} catch (err) {
console.error(err)
}
}
@ -317,8 +331,8 @@ module.exports = class MetamaskController {
conversionDate: this.configManager.getConversionDate(),
}
cb(data)
} catch (e) {
cb(null, e)
} catch (err) {
cb(null, err)
}
}
@ -335,8 +349,8 @@ module.exports = class MetamaskController {
try {
this.configManager.setShouldntShowWarning()
cb()
} catch (e) {
cb(e)
} catch (err) {
cb(err)
}
}
@ -381,8 +395,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)
let 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

@ -11,12 +11,14 @@
"test": "npm run fastTest && npm run ci && npm run lint",
"fastTest": "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 && testem ci -P 2",
"announce": "node development/announcer.js"
"announce": "node development/announcer.js",
"generateNotice": "node development/notice-generator.js"
},
"browserify": {
"transform": [
@ -47,6 +49,8 @@
"ethereumjs-tx": "^1.0.0",
"ethereumjs-util": "^4.4.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",
@ -97,6 +101,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",
@ -116,6 +121,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,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

@ -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

@ -7,6 +7,13 @@ 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,
@ -519,6 +526,43 @@ function goBackToInitView () {
}
}
//
// notice
//
function markNoticeRead (notice) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.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

@ -17,6 +17,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 RevealSeedConfirmation = require('./recover-seed/confirmation')
@ -41,6 +43,7 @@ function mapStateToProps (state) {
isLoading: state.appState.isLoading,
isConfirmed: state.metamask.isConfirmed,
isEthConfirmed: state.metamask.isEthConfirmed,
noActiveNotices: state.metamask.noActiveNotices,
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
currentView: state.appState.currentView,
@ -425,6 +428,10 @@ App.prototype.renderPrimary = function () {
return h(UnlockScreen, {key: 'locked'})
}
if (!props.noActiveNotices) {
return h(NoticeScreen, {key: 'NoticeScreen'})
}
// show current view
switch (props.currentView.name) {
case 'EthStoreWarning':

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)
}

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

@ -229,6 +229,12 @@ function reduceApp (state, action) {
scrollToBottom: false,
})
case actions.SHOW_NOTICE:
return extend(appState, {
transForward: true,
isLoading: false,
})
case actions.REVEAL_ACCOUNT:
return extend(appState, {
scrollToBottom: true,

View File

@ -17,6 +17,8 @@ function reduceMetamask (state, action) {
currentFiat: 'USD',
conversionRate: 0,
conversionDate: 'N/A',
noActiveNotices: true,
lastUnreadNotice: undefined,
}, state.metamask)
switch (action.type) {
@ -26,6 +28,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)