Implement some cross-browser practices (#473)

* Add mozilla plugin key to manifest

* Move all chrome references into platform-checking module

Addresses #453

* Add chrome global back to linter blacklist

* Add tests
This commit is contained in:
Dan Finlay 2016-07-21 10:45:32 -07:00 committed by GitHub
parent cdd7e40545
commit 6658bea8d4
14 changed files with 139 additions and 35 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
app/scripts/lib/extension-instance.js

View File

@ -23,7 +23,6 @@
],
"globals": {
"chrome": true,
"document": false,
"navigator": false,
"web3": true,

View File

@ -37,6 +37,11 @@
"all_frames": false
}
],
"applications": {
"gecko": {
"id": "MOZILLA_EXTENSION_ID"
}
},
"permissions": [
"notifications",
"storage",

View File

@ -9,6 +9,7 @@ const createMsgNotification = require('./lib/notifications.js').createMsgNotific
const messageManager = require('./lib/message-manager')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const MetamaskController = require('./metamask-controller')
const extension = require('./lib/extension')
const STORAGE_KEY = 'metamask-config'
@ -65,7 +66,7 @@ function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
// connect to other contexts
//
chrome.runtime.onConnect.addListener(connectRemote)
extension.runtime.onConnect.addListener(connectRemote)
function connectRemote (remotePort) {
var isMetaMaskInternalProcess = (remotePort.name === 'popup')
var portStream = new PortStream(remotePort)
@ -133,8 +134,8 @@ function updateBadge (state) {
if (count) {
label = String(count)
}
chrome.browserAction.setBadgeText({ text: label })
chrome.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
extension.browserAction.setBadgeText({ text: label })
extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
}
function loadData () {

View File

@ -25,11 +25,12 @@
// if (e.data) {
// var data = JSON.parse(e.data);
// if (data && data.command === 'reload') {
// chrome.runtime.reload();
// extension.runtime.reload();
// }
// }
// };
const extension = require('./lib/extension')
window.LiveReloadOptions = { host: 'localhost' };
(function e (t, n, r) { function s (o, u) { if (!n[o]) { if (!t[o]) { var a = typeof require === 'function' && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); var f = new Error("Cannot find module '" + o + "'"); throw f.code = 'MODULE_NOT_FOUND', f } var l = n[o] = {exports: {}}; t[o][0].call(l.exports, function (e) { var n = t[o][1][e]; return s(n ? n : e) }, l, l.exports, e, t, n, r) } return n[o].exports } var i = typeof require === 'function' && require; for (var o = 0; o < r.length; o++)s(r[o]); return s })({1: [function (require, module, exports) {

View File

@ -1,6 +1,7 @@
const LocalMessageDuplexStream = require('./lib/local-message-stream.js')
const PortStream = require('./lib/port-stream.js')
const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('./lib/extension')
if (shouldInjectWeb3()) {
setupInjection()
@ -10,7 +11,7 @@ if (shouldInjectWeb3()) {
function setupInjection(){
// inject in-page script
var scriptTag = document.createElement('script')
scriptTag.src = chrome.extension.getURL('scripts/inpage.js')
scriptTag.src = extension.extension.getURL('scripts/inpage.js')
scriptTag.onload = function () { this.parentNode.removeChild(this) }
var container = document.head || document.documentElement
// append as first child
@ -25,7 +26,7 @@ function setupStreams(){
target: 'inpage',
})
pageStream.on('error', console.error.bind(console))
var pluginPort = chrome.runtime.connect({name: 'contentscript'})
var pluginPort = extension.runtime.connect({name: 'contentscript'})
var pluginStream = new PortStream(pluginPort)
pluginStream.on('error', console.error.bind(console))
@ -49,4 +50,4 @@ function setupStreams(){
function shouldInjectWeb3(){
var shouldInject = (window.location.href.indexOf('.pdf') === -1)
return shouldInject
}
}

View File

@ -0,0 +1,38 @@
const apis = [
'alarms',
'bookmarks',
'browserAction',
'commands',
'contextMenus',
'cookies',
'downloads',
'events',
'extension',
'extensionTypes',
'history',
'i18n',
'idle',
'notifications',
'pageAction',
'runtime',
'storage',
'tabs',
'webNavigation',
'webRequest',
'windows',
]
function Extension () {
const _this = this
let global = window
if (window.chrome) {
global = window.chrome
}
apis.forEach(function (api) {
_this[api] = global[api]
})
}
module.exports = Extension

View File

@ -0,0 +1,14 @@
/* Extension.js
*
* A module for unifying browser differences in the WebExtension API.
*
* Initially implemented because Chrome hides all of their WebExtension API
* behind a global `chrome` variable, but we'd like to start grooming
* the code-base for cross-browser extension support.
*
* You can read more about the WebExtension API here:
* https://developer.mozilla.org/en-US/Add-ons/WebExtensions
*/
const Extension = require('./extension-instance')
module.exports = new Extension()

View File

@ -7,6 +7,7 @@ const h = require('react-hyperscript')
const PendingTxDetails = require('../../../ui/app/components/pending-tx-details')
const PendingMsgDetails = require('../../../ui/app/components/pending-msg-details')
const MetaMaskUiCss = require('../../../ui/css')
const extension = require('./extension')
var notificationHandlers = {}
const notifications = {
@ -20,34 +21,34 @@ window.METAMASK_NOTIFIER = notifications
setupListeners()
function setupListeners () {
// guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!chrome.notifications) return console.error('Chrome notifications API missing...')
// 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
chrome.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) {
extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) {
var handlers = notificationHandlers[notificationId]
if (buttonIndex === 0) {
handlers.confirm()
} else {
handlers.cancel()
}
chrome.notifications.clear(notificationId)
extension.notifications.clear(notificationId)
})
// notification teardown
chrome.notifications.onClosed.addListener(function (notificationId) {
extension.notifications.onClosed.addListener(function (notificationId) {
delete notificationHandlers[notificationId]
})
}
// creation helper
function createUnlockRequestNotification (opts) {
// guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!chrome.notifications) return console.error('Chrome notifications API missing...')
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
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()
chrome.notifications.create(id, {
extension.notifications.create(id, {
type: 'basic',
iconUrl: '/images/icon-128.png',
title: opts.title,
@ -56,8 +57,8 @@ function createUnlockRequestNotification (opts) {
}
function createTxNotification (state) {
// guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!chrome.notifications) return console.error('Chrome notifications API missing...')
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
renderTxNotificationSVG(state, function (err, notificationSvgSource) {
if (err) throw err
@ -70,8 +71,8 @@ function createTxNotification (state) {
}
function createMsgNotification (state) {
// guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!chrome.notifications) return console.error('Chrome notifications API missing...')
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
renderMsgNotificationSVG(state, function (err, notificationSvgSource) {
if (err) throw err
@ -84,11 +85,11 @@ function createMsgNotification (state) {
}
function showNotification (state) {
// guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!chrome.notifications) return console.error('Chrome notifications API missing...')
// 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()
chrome.notifications.create(id, {
extension.notifications.create(id, {
type: 'image',
requireInteraction: true,
iconUrl: '/images/icon-128.png',

View File

@ -6,6 +6,7 @@ const messageManager = require('./lib/message-manager')
const HostStore = require('./lib/remote-store.js').HostStore
const Web3 = require('web3')
const ConfigManager = require('./lib/config-manager')
const extension = require('./lib/extension')
module.exports = class MetamaskController {
@ -239,19 +240,19 @@ module.exports = class MetamaskController {
// called from popup
setRpcTarget (rpcTarget) {
this.configManager.setRpcTarget(rpcTarget)
chrome.runtime.reload()
extension.runtime.reload()
this.idStore.getNetwork()
}
setProviderType (type) {
this.configManager.setProviderType(type)
chrome.runtime.reload()
extension.runtime.reload()
this.idStore.getNetwork()
}
useEtherscanProvider () {
this.configManager.useEtherscanProvider()
chrome.runtime.reload()
extension.runtime.reload()
}
}

View File

@ -9,6 +9,7 @@ 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()
@ -21,7 +22,7 @@ async.parallel({
function connectToAccountManager (cb) {
// setup communication with background
var pluginPort = chrome.runtime.connect({name: 'popup'})
var pluginPort = extension.runtime.connect({name: 'popup'})
var portStream = new PortStream(pluginPort)
// setup multiplexing
var mx = setupMultiplex(portStream)
@ -55,8 +56,8 @@ function setupControllerConnection (stream, cb) {
function getCurrentDomain (cb) {
const unknown = '<unknown>'
if (!chrome.tabs) return cb(null, unknown)
chrome.tabs.query({active: true, currentWindow: true}, function (results) {
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
@ -68,9 +69,9 @@ function getCurrentDomain (cb) {
}
function clearNotifications(){
chrome.notifications.getAll(function (object) {
extension.notifications.getAll(function (object) {
for (let notification in object){
chrome.notifications.clear(notification)
extension.notifications.clear(notification)
}
})
}

View File

@ -0,0 +1,39 @@
var assert = require('assert')
var sinon = require('sinon')
const ethUtil = require('ethereumjs-util')
var path = require('path')
var Extension = require(path.join(__dirname, '..', '..', 'app', 'scripts', 'lib', 'extension-instance.js'))
describe('extension', function() {
describe('with chrome global', function() {
let extension
beforeEach(function() {
window.chrome = {
alarms: 'foo'
}
extension = new Extension()
})
it('should use the chrome global apis', function() {
assert.equal(extension.alarms, 'foo')
})
})
describe('without chrome global', function() {
let extension
beforeEach(function() {
window.chrome = undefined
window.alarms = 'foo'
extension = new Extension()
})
it('should use the global apis', function() {
assert.equal(extension.alarms, 'foo')
})
})
})

View File

@ -7,6 +7,7 @@ const addressSummary = require('../util').addressSummary
const explorerLink = require('../../lib/explorer-link')
const CopyButton = require('./copyButton')
const vreme = new (require('vreme'))
const extension = require('../../../app/scripts/lib/extension')
const TransactionIcon = require('./transaction-list-item-icon')
@ -49,7 +50,7 @@ TransactionListItem.prototype.render = function () {
if (!transaction.hash || !isLinkable) return
var url = explorerLink(transaction.hash, parseInt(network))
chrome.tabs.create({ url })
extension.tabs.create({ url })
},
style: {
padding: '20px 0',

View File

@ -3,6 +3,7 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const extension = require('../../app/scripts/lib/extension')
module.exports = connect(mapStateToProps)(InfoScreen)
@ -19,7 +20,7 @@ InfoScreen.prototype.render = function () {
var state = this.props
var manifest
try {
manifest = chrome.runtime.getManifest()
manifest = extension.runtime.getManifest()
} catch (e) {
manifest = { version: '2.0.0' }
}
@ -105,7 +106,7 @@ InfoScreen.prototype.render = function () {
h('a.info', {
target: '_blank',
style: { width: '85vw' },
onClick () { chrome.tabs.create({url: 'mailto:help@metamask.io?subject=Feedback'}) },
onClick () { extension.tabs.create({url: 'mailto:help@metamask.io?subject=Feedback'}) },
}, 'Email us any questions or comments!'),
]),
@ -124,5 +125,5 @@ InfoScreen.prototype.render = function () {
}
InfoScreen.prototype.navigateTo = function (url) {
chrome.tabs.create({ url })
extension.tabs.create({ url })
}