From e9a63d5d5b428e8ace6423652d8691205bb129f0 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Thu, 1 Aug 2019 10:54:33 -0230 Subject: [PATCH] Default Privacy Mode to ON, allow force sharing address (#6904) --- app/_locales/en/messages.json | 12 ++ app/images/icons/connect.svg | 7 + app/images/icons/info.svg | 5 + app/scripts/contentscript.js | 8 ++ app/scripts/controllers/preferences.js | 8 ++ app/scripts/controllers/provider-approval.js | 74 ++++++++--- app/scripts/metamask-controller.js | 8 ++ app/scripts/migrations/034.js | 33 +++++ app/scripts/popup-core.js | 77 ----------- app/scripts/ui.js | 125 +++++++++++++++--- package.json | 1 - .../home-notification.component.js | 110 +++++++++++++++ .../components/app/home-notification/index.js | 1 + .../app/home-notification/index.scss | 106 +++++++++++++++ ui/app/components/app/index.scss | 2 + .../transaction-list.component.js | 3 + .../transaction-view.component.js | 12 +- ui/app/pages/home/home.component.js | 60 ++++++++- ui/app/pages/home/home.container.js | 26 +++- ui/app/store/actions.js | 14 ++ ui/index.js | 1 + 21 files changed, 576 insertions(+), 117 deletions(-) create mode 100644 app/images/icons/connect.svg create mode 100644 app/images/icons/info.svg create mode 100644 app/scripts/migrations/034.js delete mode 100644 app/scripts/popup-core.js create mode 100644 ui/app/components/app/home-notification/home-notification.component.js create mode 100644 ui/app/components/app/home-notification/index.js create mode 100644 ui/app/components/app/home-notification/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1f60bfa57..f15dff386 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 000000000..24543e8d8 --- /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 000000000..138811bae --- /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 db4d5fd63..7415c5fe9 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 24df29c1d..d480834f5 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 06c499780..00ec0aea1 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 26dde8288..158fb3079 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 000000000..7c852de96 --- /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 c08e9fa24..000000000 --- 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 2dde14b48..a1f904f61 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 = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' container.style.height = '80px' log.error(err.stack) throw err } + function initializeUiWithTab (tab) { + const container = document.getElementById('app-content') + initializeUi(tab, container, connectionStream, (err, store) => { + if (err) { + return displayCriticalError(container, err) + } + + const state = store.getState() + const { metamask: { completedOnboarding } = {} } = state + + if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) { + global.platform.openExtensionInBrowser() + } + }) + } +} + +async function queryCurrentActiveTab (windowType) { + return new Promise((resolve) => { + // At the time of writing we only have the `activeTab` permission which means + // that this query will only succeed in the popup context (i.e. after a "browserAction") + if (windowType !== ENVIRONMENT_TYPE_POPUP) { + resolve({}) + return + } + + extension.tabs.query({active: true, currentWindow: true}, (tabs) => { + const [activeTab] = tabs + const {title, url} = activeTab + const origin = url ? urlUtil.parse(url).hostname : null + resolve({ + title, origin, url, + }) + }) + }) +} + +function initializeUi (activeTab, container, connectionStream, cb) { + connectToAccountManager(connectionStream, (err, backgroundConnection) => { + if (err) { + return cb(err) + } + + launchMetaMaskUi({ + activeTab, + container, + backgroundConnection, + }, cb) + }) +} + +/** + * Establishes a connection to the background 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) { + const mx = setupMultiplex(connectionStream) + 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) { + const eventEmitter = new EventEmitter() + const backgroundDnode = Dnode({ + sendUpdate: function (state) { + eventEmitter.emit('update', state) + }, + }) + connectionStream.pipe(backgroundDnode).pipe(connectionStream) + backgroundDnode.once('remote', function (backgroundConnection) { + backgroundConnection.on = eventEmitter.on.bind(eventEmitter) + cb(null, backgroundConnection) + }) } diff --git a/package.json b/package.json index b728b826f..0043a83c5 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@zxing/library": "^0.8.0", "abi-decoder": "^1.2.0", "asmcrypto.js": "^2.3.2", - "async": "^2.5.0", "await-semaphore": "^0.1.1", "babel-runtime": "^6.23.0", "bignumber.js": "^4.1.0", diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js new file mode 100644 index 000000000..cc46eb53a --- /dev/null +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -0,0 +1,110 @@ +import React, { PureComponent } from 'react' +import {Tooltip as ReactTippy} from 'react-tippy' +import PropTypes from 'prop-types' +import Button from '../../ui/button' + +export default class HomeNotification extends PureComponent { + static contextTypes = { + metricsEvent: PropTypes.func, + } + + static defaultProps = { + onAccept: null, + ignoreText: null, + onIgnore: null, + infoText: null, + } + + static propTypes = { + acceptText: PropTypes.string.isRequired, + onAccept: PropTypes.func, + ignoreText: PropTypes.string, + onIgnore: PropTypes.func, + descriptionText: PropTypes.string.isRequired, + infoText: PropTypes.string, + } + + handleAccept = () => { + this.props.onAccept() + } + + handleIgnore = () => { + this.props.onIgnore() + } + + render () { + const { descriptionText, acceptText, onAccept, ignoreText, onIgnore, infoText } = this.props + + return ( +
+
+
+ +
+ { descriptionText } +
+
+ { + infoText ? ( + + {infoText} +

+ )} + offset={-36} + distance={36} + animation="none" + position="top" + arrow + theme="info" + > + +
+ ) : ( + null + ) + } +
+
+ { + (onAccept && acceptText) ? ( + + ) : ( + null + ) + } + { + (onIgnore && ignoreText) ? ( + + ) : ( + null + ) + } +
+
+ ) + } +} diff --git a/ui/app/components/app/home-notification/index.js b/ui/app/components/app/home-notification/index.js new file mode 100644 index 000000000..918a35be2 --- /dev/null +++ b/ui/app/components/app/home-notification/index.js @@ -0,0 +1 @@ +export { default } from './home-notification.component' diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss new file mode 100644 index 000000000..9cc868d46 --- /dev/null +++ b/ui/app/components/app/home-notification/index.scss @@ -0,0 +1,106 @@ +.tippy-tooltip.info-theme { + background: rgba(36, 41, 46, 0.9); + color: $white; + border-radius: 8px; +} + +.home-notification { + background: rgba(36, 41, 46, 0.9); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); + border-radius: 8px; + height: 116px; + padding: 16px; + margin: 8px; + + display: flex; + flex-flow: column; + justify-content: space-between; + + &__header-container { + display: flex; + } + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__text { + font-family: Roboto, 'sans-serif'; + font-style: normal; + font-weight: normal; + font-size: 12px; + color: $white; + margin-left: 10px; + margin-right: 8px; + } + + .fa-info-circle { + color: #6A737D; + } + + &__ignore-button { + border: 2px solid #6A737D; + box-sizing: border-box; + border-radius: 6px; + color: $white; + background-color: inherit; + height: 34px; + width: 155px; + padding: 0; + + &:hover { + border-color: #6A737D; + background-color: #6A737D; + } + + &:active { + background-color: #141618; + } + } + + &__accept-button { + border: 2px solid #6A737D; + box-sizing: border-box; + border-radius: 6px; + color: $white; + background-color: inherit; + height: 34px; + width: 155px; + padding: 0; + + &:hover { + border-color: #6A737D; + background-color: #6A737D; + } + + &:active { + background-color: #141618; + } + } + + &__buttons { + display: flex; + width: 100%; + justify-content: space-between; + flex-direction: row-reverse; + } +} + +.home-notification-tooltip { + &__tooltip-container { + display: flex; + } + + &__content { + font-family: Roboto, 'sans-serif'; + font-style: normal; + font-weight: normal; + font-size: 12px; + color: $white; + text-align: left; + display: inline-block; + width: 200px; + } +} diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index 1236f0c38..9b7da8c2e 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -79,3 +79,5 @@ @import 'gas-customization/gas-price-button-group/index'; @import '../ui/toggle-button/index'; + +@import 'home-notification/index'; diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index 3c096e3fd..157e7200b 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -10,11 +10,13 @@ export default class TransactionList extends PureComponent { } static defaultProps = { + children: null, pendingTransactions: [], completedTransactions: [], } static propTypes = { + children: PropTypes.node, pendingTransactions: PropTypes.array, completedTransactions: PropTypes.array, selectedToken: PropTypes.object, @@ -120,6 +122,7 @@ export default class TransactionList extends PureComponent { return (
{ this.renderTransactions() } + { this.props.children }
) } diff --git a/ui/app/components/app/transaction-view/transaction-view.component.js b/ui/app/components/app/transaction-view/transaction-view.component.js index 7014ca173..fb2c2145c 100644 --- a/ui/app/components/app/transaction-view/transaction-view.component.js +++ b/ui/app/components/app/transaction-view/transaction-view.component.js @@ -10,6 +10,14 @@ export default class TransactionView extends PureComponent { t: PropTypes.func, } + static propTypes = { + children: PropTypes.node, + } + + static defaultProps = { + children: null, + } + render () { return (
@@ -20,7 +28,9 @@ export default class TransactionView extends PureComponent {
- + + { this.props.children } +
) } diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index a3b486c57..1fd12a359 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import Media from 'react-media' import { Redirect } from 'react-router-dom' +import HomeNotification from '../../components/app/home-notification' import WalletView from '../../components/app/wallet-view' import TransactionView from '../../components/app/transaction-view' import ProviderApproval from '../provider-approval' @@ -13,12 +14,30 @@ import { } from '../../helpers/constants/routes' export default class Home extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static defaultProps = { + activeTab: null, + unsetMigratedPrivacyMode: null, + forceApproveProviderRequestByOrigin: null, + } + static propTypes = { + activeTab: PropTypes.shape({ + title: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + }), history: PropTypes.object, forgottenPassword: PropTypes.bool, suggestedTokens: PropTypes.object, unconfirmedTransactionsCount: PropTypes.number, providerRequests: PropTypes.array, + showPrivacyModeNotification: PropTypes.bool.isRequired, + unsetMigratedPrivacyMode: PropTypes.func, + viewingUnconnectedDapp: PropTypes.bool.isRequired, + forceApproveProviderRequestByOrigin: PropTypes.func, } componentWillMount () { @@ -45,10 +64,16 @@ export default class Home extends PureComponent { } render () { + const { t } = this.context const { + activeTab, forgottenPassword, providerRequests, history, + showPrivacyModeNotification, + unsetMigratedPrivacyMode, + viewingUnconnectedDapp, + forceApproveProviderRequestByOrigin, } = this.props if (forgottenPassword) { @@ -68,7 +93,40 @@ export default class Home extends PureComponent { query="(min-width: 576px)" render={() => } /> - { !history.location.pathname.match(/^\/confirm-transaction/) ? : null } + { !history.location.pathname.match(/^\/confirm-transaction/) + ? ( + + { + showPrivacyModeNotification + ? ( + { + window.open('https://medium.com/metamask/42549d4870fa', '_blank', 'noopener') + unsetMigratedPrivacyMode() + }} + /> + ) + : null + } + { + viewingUnconnectedDapp + ? ( + { + forceApproveProviderRequestByOrigin(activeTab.origin) + }} + infoText={t('shareAddressInfo', [activeTab.origin])} + /> + ) + : null + } + + ) + : null } ) diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index a4690a17a..81a3946c5 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -3,26 +3,48 @@ import { compose } from 'recompose' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' +import { + forceApproveProviderRequestByOrigin, + unsetMigratedPrivacyMode, +} from '../../store/actions' +import { getEnvironmentType } from '../../../../app/scripts/lib/util' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' const mapStateToProps = state => { - const { metamask, appState } = state + const { activeTab, metamask, appState } = state const { + approvedOrigins, lostAccounts, suggestedTokens, providerRequests, + migratedPrivacyMode, + featureFlags: { + privacyMode, + } = {}, } = metamask const { forgottenPassword } = appState + const isUnconnected = Boolean(activeTab && privacyMode && !approvedOrigins[activeTab.origin]) + const isPopup = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP + return { lostAccounts, forgottenPassword, suggestedTokens, unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), providerRequests, + showPrivacyModeNotification: migratedPrivacyMode, + activeTab, + viewingUnconnectedDapp: isUnconnected && isPopup, } } +const mapDispatchToProps = (dispatch) => ({ + unsetMigratedPrivacyMode: () => dispatch(unsetMigratedPrivacyMode()), + forceApproveProviderRequestByOrigin: (origin) => dispatch(forceApproveProviderRequestByOrigin(origin)), +}) + export default compose( withRouter, - connect(mapStateToProps) + connect(mapStateToProps, mapDispatchToProps) )(Home) diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 2667dd803..726deb59d 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -324,6 +324,7 @@ var actions = { setUseNativeCurrencyAsPrimaryCurrencyPreference, setShowFiatConversionOnTestnetsPreference, setAutoLogoutTimeLimit, + unsetMigratedPrivacyMode, // Onboarding setCompletedOnboarding, @@ -348,6 +349,7 @@ var actions = { approveProviderRequestByOrigin, rejectProviderRequestByOrigin, + forceApproveProviderRequestByOrigin, clearApprovedOrigins, setFirstTimeFlowType, @@ -2637,6 +2639,12 @@ function approveProviderRequestByOrigin (origin) { } } +function forceApproveProviderRequestByOrigin (origin) { + return () => { + background.forceApproveProviderRequestByOrigin(origin) + } +} + function rejectProviderRequestByOrigin (origin) { return () => { background.rejectProviderRequestByOrigin(origin) @@ -2758,3 +2766,9 @@ function getTokenParams (tokenAddress) { }) } } + +function unsetMigratedPrivacyMode () { + return () => { + background.unsetMigratedPrivacyMode() + } +} diff --git a/ui/index.js b/ui/index.js index 7eb305653..db9292761 100644 --- a/ui/index.js +++ b/ui/index.js @@ -34,6 +34,7 @@ async function startApp (metamaskState, backgroundConnection, opts) { const enLocaleMessages = await fetchLocale('en') const store = configureStore({ + activeTab: opts.activeTab, // metamaskState represents the cross-tab state metamask: metamaskState,