1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Set up MVP for popup-based notifications.

This commit is contained in:
Dan Finlay 2016-08-16 15:39:40 -07:00
parent f1986d7a37
commit 5479509618
6 changed files with 162 additions and 131 deletions

11
app/notification.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MetaMask Notification</title>
</head>
<body>
<div id="app-content"></div>
<script src="./scripts/notification.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

View File

@ -69,7 +69,7 @@ function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
extension.runtime.onConnect.addListener(connectRemote) extension.runtime.onConnect.addListener(connectRemote)
function connectRemote (remotePort) { function connectRemote (remotePort) {
var isMetaMaskInternalProcess = (remotePort.name === 'popup') var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification'
var portStream = new PortStream(remotePort) var portStream = new PortStream(remotePort)
if (isMetaMaskInternalProcess) { if (isMetaMaskInternalProcess) {
// communication with popup // communication with popup

View File

@ -18,142 +18,24 @@ const notifications = {
module.exports = notifications module.exports = notifications
window.METAMASK_NOTIFIER = notifications window.METAMASK_NOTIFIER = notifications
setupListeners()
function setupListeners () {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
// notification button press
extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) {
var handlers = notificationHandlers[notificationId]
if (buttonIndex === 0) {
handlers.confirm()
} else {
handlers.cancel()
}
extension.notifications.clear(notificationId)
})
// notification teardown
extension.notifications.onClosed.addListener(function (notificationId) {
delete notificationHandlers[notificationId]
})
}
// creation helper
function createUnlockRequestNotification (opts) { function createUnlockRequestNotification (opts) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 showNotification()
if (!extension.notifications) return console.error('Chrome notifications API missing...')
var message = 'An Ethereum app has requested a signature. Please unlock your account.'
var id = createId()
extension.notifications.create(id, {
type: 'basic',
iconUrl: '/images/icon-128.png',
title: opts.title,
message: message,
})
} }
function createTxNotification (state) { function createTxNotification (state) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 showNotification()
if (!extension.notifications) return console.error('Chrome notifications API missing...')
renderTxNotificationSVG(state, function (err, notificationSvgSource) {
if (err) throw err
showNotification(extend(state, {
title: 'New Unsigned Transaction',
imageUrl: toSvgUri(notificationSvgSource),
}))
})
} }
function createMsgNotification (state) { function createMsgNotification (state) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 showNotification()
if (!extension.notifications) return console.error('Chrome notifications API missing...') }
renderMsgNotificationSVG(state, function (err, notificationSvgSource) { function showNotification() {
if (err) throw err chrome.windows.create({
url:"notification.html",
showNotification(extend(state, { type:"panel",
title: 'New Unsigned Message', width:360,
imageUrl: toSvgUri(notificationSvgSource), height:500,
}))
}) })
} }
function showNotification (state) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
var id = createId()
extension.notifications.create(id, {
type: 'image',
requireInteraction: true,
iconUrl: '/images/icon-128.png',
imageUrl: state.imageUrl,
title: state.title,
message: '',
buttons: [{
title: 'Approve',
}, {
title: 'Reject',
}],
})
notificationHandlers[id] = {
confirm: state.onConfirm,
cancel: state.onCancel,
}
}
function renderTxNotificationSVG (state, cb) {
var content = h(PendingTxDetails, state)
renderNotificationSVG(content, cb)
}
function renderMsgNotificationSVG (state, cb) {
var content = h(PendingMsgDetails, state)
renderNotificationSVG(content, cb)
}
function renderNotificationSVG (content, cb) {
var container = document.createElement('div')
var confirmView = h('div.app-primary', {
style: {
width: '360px',
height: '240px',
padding: '16px',
// background: '#F7F7F7',
background: 'white',
},
}, [
h('style', MetaMaskUiCss()),
content,
])
render(confirmView, container, function ready() {
var rootElement = findDOMNode(this)
var viewSource = rootElement.outerHTML
unmountComponentAtNode(container)
var svgSource = svgWrapper(viewSource)
// insert content into svg wrapper
cb(null, svgSource)
})
}
function svgWrapper (content) {
var wrapperSource = `
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="240">
<foreignObject x="0" y="0" width="100%" height="100%">
<body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body>
</foreignObject>
</svg>
`
return wrapperSource.split('{{content}}').join(content)
}
function toSvgUri (content) {
return 'data:image/svg+xml;utf8,' + encodeURIComponent(content)
}

View File

@ -0,0 +1,85 @@
const url = require('url')
const EventEmitter = require('events').EventEmitter
const async = require('async')
const Dnode = require('dnode')
const Web3 = require('web3')
const MetaMaskNotification = require('../../ui/notification')
const MetaMaskUiCss = require('../../ui/css')
const injectCss = require('inject-css')
const PortStream = require('./lib/port-stream.js')
const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const extension = require('./lib/extension')
// setup app
var css = MetaMaskUiCss()
injectCss(css)
async.parallel({
currentDomain: getCurrentDomain,
accountManager: connectToAccountManager,
}, setupApp)
function connectToAccountManager (cb) {
// setup communication with background
var pluginPort = extension.runtime.connect({name: 'notification'})
var portStream = new PortStream(pluginPort)
// setup multiplexing
var mx = setupMultiplex(portStream)
// connect features
setupControllerConnection(mx.createStream('controller'), cb)
setupWeb3Connection(mx.createStream('provider'))
}
function setupWeb3Connection (stream) {
var remoteProvider = new StreamProvider()
remoteProvider.pipe(stream).pipe(remoteProvider)
stream.on('error', console.error.bind(console))
remoteProvider.on('error', console.error.bind(console))
global.web3 = new Web3(remoteProvider)
}
function setupControllerConnection (stream, cb) {
var eventEmitter = new EventEmitter()
var background = Dnode({
sendUpdate: function (state) {
eventEmitter.emit('update', state)
},
})
stream.pipe(background).pipe(stream)
background.once('remote', function (accountManager) {
// setup push events
accountManager.on = eventEmitter.on.bind(eventEmitter)
cb(null, accountManager)
})
}
function getCurrentDomain (cb) {
const unknown = '<unknown>'
if (!extension.tabs) return cb(null, unknown)
extension.tabs.query({active: true, currentWindow: true}, function (results) {
var activeTab = results[0]
var currentUrl = activeTab && activeTab.url
var currentDomain = url.parse(currentUrl).host
if (!currentUrl) {
return cb(null, unknown)
}
cb(null, currentDomain)
})
}
function setupApp (err, opts) {
if (err) {
alert(err.stack)
throw err
}
var container = document.getElementById('app-content')
MetaMaskNotification({
container: container,
accountManager: opts.accountManager,
currentDomain: opts.currentDomain,
networkVersion: opts.networkVersion,
})
}

View File

@ -108,6 +108,7 @@ const jsFiles = [
'contentscript', 'contentscript',
'background', 'background',
'popup', 'popup',
'notification',
] ]
jsFiles.forEach((jsFile) => { jsFiles.forEach((jsFile) => {
@ -115,9 +116,9 @@ jsFiles.forEach((jsFile) => {
gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, filename: `${jsFile}.js` })) gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, filename: `${jsFile}.js` }))
}) })
gulp.task('dev:js', gulp.parallel('dev:js:inpage','dev:js:contentscript','dev:js:background','dev:js:popup')) gulp.task('dev:js', gulp.parallel('dev:js:inpage','dev:js:contentscript','dev:js:background','dev:js:popup', 'dev:js:notification'))
gulp.task('build:js', gulp.parallel('build:js:inpage','build:js:contentscript','build:js:background','build:js:popup')) gulp.task('build:js', gulp.parallel('build:js:inpage','build:js:contentscript','build:js:background','build:js:popup', 'dev:js:notification'))
// clean dist // clean dist

52
ui/notification.js Normal file
View File

@ -0,0 +1,52 @@
const render = require('react-dom').render
const h = require('react-hyperscript')
const Root = require('./app/root')
const actions = require('./app/actions')
const configureStore = require('./app/store')
module.exports = launchApp
function launchApp (opts) {
var accountManager = opts.accountManager
actions._setAccountManager(accountManager)
// check if we are unlocked first
accountManager.getState(function (err, metamaskState) {
if (err) throw err
startApp(metamaskState, accountManager, opts)
})
}
function startApp (metamaskState, accountManager, opts) {
// parse opts
var store = configureStore({
// metamaskState represents the cross-tab state
metamask: metamaskState,
// appState represents the current tab's popup state
appState: {
currentDomain: opts.currentDomain,
},
// Which blockchain we are using:
networkVersion: opts.networkVersion,
})
// if unconfirmed txs, start on txConf page
if (Object.keys(metamaskState.unconfTxs || {}).length) {
store.dispatch(actions.showConfTxPage())
}
accountManager.on('update', function (metamaskState) {
store.dispatch(actions.updateMetamaskState(metamaskState))
})
// start app
render(
h(Root, {
// inject initial state
store: store,
}
), opts.container)
}