diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1f60bfa57378..f15dff38600d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1,4 +1,16 @@ { + "shareAddress": { + "message": "Share Address" + }, + "shareAddressToConnect": { + "message": "Share your address to connect to $1?" + }, + "shareAddressInfo": { + "message": "Sharing your address with $1 will allow you to interact with this dapp. This permission is to protect your privacy by default." + }, + "privacyModeDefault": { + "message": "Privacy Mode is now enabled by default" + }, "privacyMode": { "message": "Privacy Mode" }, diff --git a/app/images/icons/connect.svg b/app/images/icons/connect.svg new file mode 100644 index 000000000000..24543e8d8079 --- /dev/null +++ b/app/images/icons/connect.svg @@ -0,0 +1,7 @@ + diff --git a/app/images/icons/info.svg b/app/images/icons/info.svg new file mode 100644 index 000000000000..138811bae7e5 --- /dev/null +++ b/app/images/icons/info.svg @@ -0,0 +1,5 @@ + diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index db4d5fd63ab7..7415c5fe9e2c 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -114,6 +114,7 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { async function setupPublicApi (outStream) { const api = { + forceReloadSite: (cb) => cb(null, forceReloadSite()), getSiteMetadata: (cb) => cb(null, getSiteMetadata()), } const dnode = Dnode(api) @@ -306,3 +307,10 @@ async function domIsReady () { // wait for load await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) } + +/** + * Reloads the site + */ +function forceReloadSite () { + window.location.reload() +} diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 24df29c1d818..d480834f5fe1 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -54,6 +54,7 @@ class PreferencesController { useNativeCurrencyAsPrimaryCurrency: true, }, completedOnboarding: false, + migratedPrivacyMode: false, metaMetricsId: null, metaMetricsSendCount: 0, }, opts.initState) @@ -603,6 +604,13 @@ class PreferencesController { return Promise.resolve(true) } + unsetMigratedPrivacyMode () { + this.store.updateState({ + migratedPrivacyMode: false, + }) + return Promise.resolve() + } + // // PRIVATE METHODS // diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 06c4997801c7..00ec0aea1324 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -18,12 +18,12 @@ class ProviderApprovalController extends SafeEventEmitter { */ constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) { super() - this.approvedOrigins = {} this.closePopup = closePopup this.keyringController = keyringController this.openPopup = openPopup this.preferencesController = preferencesController this.store = new ObservableStore({ + approvedOrigins: {}, providerRequests: [], }) } @@ -45,7 +45,7 @@ class ProviderApprovalController extends SafeEventEmitter { } // register the provider request const metadata = await getSiteMetadata(origin) - this._handleProviderRequest(origin, metadata.name, metadata.icon, false, null) + this._handleProviderRequest(origin, metadata.name, metadata.icon) // wait for resolution of request const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) if (approved) { @@ -63,10 +63,10 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} siteTitle - The title of the document requesting full provider access * @param {string} siteImage - The icon of the window requesting full provider access */ - _handleProviderRequest (origin, siteTitle, siteImage, force, tabID) { - this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] }) + _handleProviderRequest (origin, siteTitle, siteImage) { + this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked - if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { + if (this.store.getState().approvedOrigins[origin] && this.caching && isUnlocked) { return } this.openPopup && this.openPopup() @@ -78,11 +78,19 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} origin - origin of the domain that had provider access approved */ approveProviderRequestByOrigin (origin) { - this.closePopup && this.closePopup() - const requests = this.store.getState().providerRequests - const providerRequests = requests.filter(request => request.origin !== origin) - this.store.updateState({ providerRequests }) - this.approvedOrigins[origin] = true + if (this.closePopup) { + this.closePopup() + } + + const { approvedOrigins, providerRequests } = this.store.getState() + const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) + this.store.updateState({ + approvedOrigins: { + ...approvedOrigins, + [origin]: true, + }, + providerRequests: remainingProviderRequests, + }) this.emit(`resolvedRequest:${origin}`, { approved: true }) } @@ -92,19 +100,50 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} origin - origin of the domain that had provider access approved */ rejectProviderRequestByOrigin (origin) { - this.closePopup && this.closePopup() - const requests = this.store.getState().providerRequests - const providerRequests = requests.filter(request => request.origin !== origin) - this.store.updateState({ providerRequests }) - delete this.approvedOrigins[origin] + if (this.closePopup) { + this.closePopup() + } + + const { approvedOrigins, providerRequests } = this.store.getState() + const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) + + // We're cloning and deleting keys here because we don't want to keep unneeded keys + const _approvedOrigins = Object.assign({}, approvedOrigins) + delete _approvedOrigins[origin] + + this.store.putState({ + approvedOrigins: _approvedOrigins, + providerRequests: remainingProviderRequests, + }) this.emit(`resolvedRequest:${origin}`, { approved: false }) } + /** + * Silently approves access to a full Ethereum provider API for the origin + * + * @param {string} origin - origin of the domain that had provider access approved + */ + forceApproveProviderRequestByOrigin (origin) { + const { approvedOrigins, providerRequests } = this.store.getState() + const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) + this.store.updateState({ + approvedOrigins: { + ...approvedOrigins, + [origin]: true, + }, + providerRequests: remainingProviderRequests, + }) + + this.emit(`forceResolvedRequest:${origin}`, { approved: true, forced: true }) + } + /** * Clears any cached approvals for user-approved origins */ clearApprovedOrigins () { - this.approvedOrigins = {} + this.store.updateState({ + approvedOrigins: {}, + }) } /** @@ -115,8 +154,7 @@ class ProviderApprovalController extends SafeEventEmitter { */ shouldExposeAccounts (origin) { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - const result = !privacyMode || Boolean(this.approvedOrigins[origin]) - return result + return !privacyMode || Boolean(this.store.getState().approvedOrigins[origin]) } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 26dde8288328..158fb3079b20 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -454,6 +454,7 @@ module.exports = class MetamaskController extends EventEmitter { setPreference: nodeify(preferencesController.setPreference, preferencesController), completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController), addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController), + unsetMigratedPrivacyMode: nodeify(preferencesController.unsetMigratedPrivacyMode, preferencesController), // BlacklistController whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), @@ -498,6 +499,7 @@ module.exports = class MetamaskController extends EventEmitter { // provider approval approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), + forceApproveProviderRequestByOrigin: providerApprovalController.forceApproveProviderRequestByOrigin.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), } } @@ -1285,6 +1287,8 @@ module.exports = class MetamaskController extends EventEmitter { const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi) this.setupPublicConfig(mux.createStream('publicConfig'), originDomain) + + this.providerApprovalController.on(`forceResolvedRequest:${originDomain}`, publicApi.forceReloadSite) } /** @@ -1465,6 +1469,10 @@ module.exports = class MetamaskController extends EventEmitter { const publicApi = { // wrap with an await remote + forceReloadSite: async () => { + const remote = await getRemote() + return await pify(remote.forceReloadSite)() + }, getSiteMetadata: async () => { const remote = await getRemote() return await pify(remote.getSiteMetadata)() diff --git a/app/scripts/migrations/034.js b/app/scripts/migrations/034.js new file mode 100644 index 000000000000..7c852de96581 --- /dev/null +++ b/app/scripts/migrations/034.js @@ -0,0 +1,33 @@ +const version = 34 +const clone = require('clone') + +/** + * The purpose of this migration is to enable the {@code privacyMode} feature flag and set the user as being migrated + * if it was {@code false}. + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + const { PreferencesController } = state + + if (PreferencesController) { + const featureFlags = PreferencesController.featureFlags || {} + + if (!featureFlags.privacyMode && typeof PreferencesController.migratedPrivacyMode === 'undefined') { + // Mark the state has being migrated and enable Privacy Mode + PreferencesController.migratedPrivacyMode = true + featureFlags.privacyMode = true + } + } + + return state +} diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js deleted file mode 100644 index c08e9fa2484a..000000000000 --- a/app/scripts/popup-core.js +++ /dev/null @@ -1,77 +0,0 @@ -const {EventEmitter} = require('events') -const async = require('async') -const Dnode = require('dnode') -const Eth = require('ethjs') -const EthQuery = require('eth-query') -const launchMetamaskUi = require('../../ui') -const StreamProvider = require('web3-stream-provider') -const {setupMultiplex} = require('./lib/stream-utils.js') - -module.exports = initializePopup - -/** - * Asynchronously initializes the MetaMask popup UI - * - * @param {{ container: Element, connectionStream: * }} config Popup configuration object - * @param {Function} cb Called when initialization is complete - */ -function initializePopup ({ container, connectionStream }, cb) { - // setup app - async.waterfall([ - (cb) => connectToAccountManager(connectionStream, cb), - (backgroundConnection, cb) => launchMetamaskUi({ container, backgroundConnection }, cb), - ], cb) -} - -/** - * Establishes streamed connections to background scripts and a Web3 provider - * - * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection - * @param {Function} cb Called when controller connection is established - */ -function connectToAccountManager (connectionStream, cb) { - // setup communication with background - // setup multiplexing - const mx = setupMultiplex(connectionStream) - // connect features - setupControllerConnection(mx.createStream('controller'), cb) - setupWeb3Connection(mx.createStream('provider')) -} - -/** - * Establishes a streamed connection to a Web3 provider - * - * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection - */ -function setupWeb3Connection (connectionStream) { - const providerStream = new StreamProvider() - providerStream.pipe(connectionStream).pipe(providerStream) - connectionStream.on('error', console.error.bind(console)) - providerStream.on('error', console.error.bind(console)) - global.ethereumProvider = providerStream - global.ethQuery = new EthQuery(providerStream) - global.eth = new Eth(providerStream) -} - -/** - * Establishes a streamed connection to the background account manager - * - * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection - * @param {Function} cb Called when the remote account manager connection is established - */ -function setupControllerConnection (connectionStream, cb) { - // this is a really sneaky way of adding EventEmitter api - // to a bi-directional dnode instance - const eventEmitter = new EventEmitter() - const backgroundDnode = Dnode({ - sendUpdate: function (state) { - eventEmitter.emit('update', state) - }, - }) - connectionStream.pipe(backgroundDnode).pipe(connectionStream) - backgroundDnode.once('remote', function (backgroundConnection) { - // setup push events - backgroundConnection.on = eventEmitter.on.bind(eventEmitter) - cb(null, backgroundConnection) - }) -} diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 2dde14b488bd..a1f904f61bd4 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -1,12 +1,19 @@ -const startPopup = require('./popup-core') const PortStream = require('extension-port-stream') const { getEnvironmentType } = require('./lib/util') -const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN } = require('./lib/enums') +const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_POPUP } = require('./lib/enums') const extension = require('extensionizer') const ExtensionPlatform = require('./platforms/extension') const NotificationManager = require('./lib/notification-manager') const notificationManager = new NotificationManager() const setupSentry = require('./lib/setupSentry') +const {EventEmitter} = require('events') +const Dnode = require('dnode') +const Eth = require('ethjs') +const EthQuery = require('eth-query') +const urlUtil = require('url') +const launchMetaMaskUi = require('../../ui') +const StreamProvider = require('web3-stream-provider') +const {setupMultiplex} = require('./lib/stream-utils.js') const log = require('loglevel') start().catch(log.error) @@ -39,20 +46,8 @@ async function start () { const extensionPort = extension.runtime.connect({ name: windowType }) const connectionStream = new PortStream(extensionPort) - // start ui - const container = document.getElementById('app-content') - startPopup({ container, connectionStream }, (err, store) => { - if (err) return displayCriticalError(err) - - const state = store.getState() - const { metamask: { completedOnboarding } = {} } = state - - if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) { - global.platform.openExtensionInBrowser() - return - } - }) - + const activeTab = await queryCurrentActiveTab(windowType) + initializeUiWithTab(activeTab) function closePopupIfOpen (windowType) { if (windowType !== ENVIRONMENT_TYPE_NOTIFICATION) { @@ -61,11 +56,107 @@ async function start () { } } - function displayCriticalError (err) { + function displayCriticalError (container, err) { container.innerHTML = '