diff options
author | Whymarrh Whitby <whymarrh.whitby@gmail.com> | 2019-08-01 21:24:33 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-01 21:24:33 +0800 |
commit | e9a63d5d5b428e8ace6423652d8691205bb129f0 (patch) | |
tree | 69c214f85143b61f22b6f19bf313a608c32c0999 | |
parent | 4d88e1cf862c3ae174780cd888d7703685db23e7 (diff) | |
download | tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.gz tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.bz2 tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.lz tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.xz tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.zst tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.zip |
Default Privacy Mode to ON, allow force sharing address (#6904)
21 files changed, 576 insertions, 117 deletions
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 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M8.00002 9.57037C8.93767 9.57037 9.69778 8.81026 9.69778 7.8726C9.69778 6.93495 8.93767 6.17484 8.00002 6.17484C7.06236 6.17484 6.30225 6.93495 6.30225 7.8726C6.30225 8.81026 7.06236 9.57037 8.00002 9.57037Z" fill="white"/> + <path d="M11.0582 11.6586C10.872 11.6586 10.6857 11.5876 10.5437 11.4455C10.2595 11.1614 10.2595 10.7007 10.5437 10.4165C11.2232 9.73704 11.5975 8.83356 11.5975 7.87259C11.5975 6.91161 11.2232 6.00813 10.5437 5.32865C10.2595 5.04448 10.2595 4.58381 10.5437 4.29964C10.8278 4.01554 11.2886 4.01554 11.5727 4.29964C12.527 5.25398 13.0527 6.52293 13.0527 7.87259C13.0527 9.22224 12.527 10.4912 11.5727 11.4455C11.4306 11.5876 11.2444 11.6586 11.0582 11.6586Z" fill="white"/> + <path d="M4.94175 11.6586C4.75553 11.6586 4.56929 11.5876 4.42724 11.4455C3.4729 10.4912 2.94727 9.22224 2.94727 7.87259C2.94727 6.52293 3.4729 5.25398 4.42724 4.29964C4.71135 4.01554 5.17215 4.01554 5.45626 4.29964C5.74043 4.58381 5.74043 5.04448 5.45626 5.32865C4.77672 6.00813 4.4025 6.91161 4.4025 7.87259C4.4025 8.83356 4.77672 9.73704 5.45626 10.4165C5.74043 10.7007 5.74043 11.1614 5.45626 11.4455C5.3142 11.5876 5.12798 11.6586 4.94175 11.6586Z" fill="white"/> + <path d="M13.1451 13.7453C12.9589 13.7453 12.7727 13.6742 12.6306 13.5322C12.3464 13.248 12.3464 12.7873 12.6306 12.5031C15.1839 9.94985 15.1839 5.79538 12.6306 3.24209C12.3464 2.95792 12.3464 2.49725 12.6306 2.21308C12.9147 1.92897 13.3755 1.92897 13.6596 2.21308C16.7803 5.33374 16.7803 10.4115 13.6596 13.5322C13.5176 13.6742 13.3313 13.7453 13.1451 13.7453Z" fill="white"/> + <path d="M2.855 13.7453C2.66878 13.7453 2.48255 13.6742 2.3405 13.5322C-0.780166 10.4115 -0.780166 5.33374 2.3405 2.21308C2.62461 1.92897 3.08541 1.92897 3.36951 2.21308C3.65368 2.49725 3.65368 2.95792 3.36951 3.24209C0.816221 5.79538 0.816221 9.94985 3.36951 12.5031C3.65368 12.7873 3.65368 13.248 3.36951 13.5322C3.22745 13.6742 3.04123 13.7453 2.855 13.7453Z" fill="white"/> +</svg> 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 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 2.00001C4.68613 2.00001 1.99984 4.6863 1.99984 8C1.99984 11.3137 4.68613 14 7.99984 14C11.3135 14 13.9998 11.3137 13.9998 8C13.9998 4.6863 11.3135 2.00001 7.99984 2.00001ZM0.666504 8C0.666504 3.94992 3.94975 0.666672 7.99984 0.666672C12.0499 0.666672 15.3332 3.94992 15.3332 8C15.3332 12.0501 12.0499 15.3333 7.99984 15.3333C3.94975 15.3333 0.666504 12.0501 0.666504 8Z" fill="#6A737D"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 7.33334C8.36803 7.33334 8.6665 7.63181 8.6665 8V10.6667C8.6665 11.0349 8.36803 11.3333 7.99984 11.3333C7.63165 11.3333 7.33317 11.0349 7.33317 10.6667V8C7.33317 7.63181 7.63165 7.33334 7.99984 7.33334Z" fill="#6A737D"/> + <path d="M8.6665 5.33334C8.6665 5.70153 8.36803 6 7.99984 6C7.63165 6 7.33317 5.70153 7.33317 5.33334C7.33317 4.96515 7.63165 4.66667 7.99984 4.66667C8.36803 4.66667 8.6665 4.96515 8.6665 5.33334Z" fill="#6A737D"/> +</svg> 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 = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>' 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 ( + <div className="home-notification"> + <div className="home-notification__header"> + <div className="home-notification__header-container"> + <img + className="home-notification__icon" + alt="" + src="images/icons/connect.svg" + /> + <div className="home-notification__text"> + { descriptionText } + </div> + </div> + { + infoText ? ( + <ReactTippy + style={{ + display: 'flex', + }} + html={( + <p className="home-notification-tooltip__content"> + {infoText} + </p> + )} + offset={-36} + distance={36} + animation="none" + position="top" + arrow + theme="info" + > + <img + alt="" + src="images/icons/info.svg" + /> + </ReactTippy> + ) : ( + null + ) + } + </div> + <div className="home-notification__buttons"> + { + (onAccept && acceptText) ? ( + <Button + type="primary" + className="home-notification__accept-button" + onClick={this.handleAccept} + > + { acceptText } + </Button> + ) : ( + null + ) + } + { + (onIgnore && ignoreText) ? ( + <Button + type="secondary" + className="home-notification__ignore-button" + onClick={this.handleIgnore} + > + { ignoreText } + </Button> + ) : ( + null + ) + } + </div> + </div> + ) + } +} 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 ( <div className="transaction-list"> { this.renderTransactions() } + { this.props.children } </div> ) } 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 ( <div className="transaction-view"> @@ -20,7 +28,9 @@ export default class TransactionView extends PureComponent { <div className="transaction-view__balance-wrapper"> <TransactionViewBalance /> </div> - <TransactionList /> + <TransactionList> + { this.props.children } + </TransactionList> </div> ) } 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={() => <WalletView />} /> - { !history.location.pathname.match(/^\/confirm-transaction/) ? <TransactionView /> : null } + { !history.location.pathname.match(/^\/confirm-transaction/) + ? ( + <TransactionView> + { + showPrivacyModeNotification + ? ( + <HomeNotification + descriptionText={t('privacyModeDefault')} + acceptText={t('learnMore')} + onAccept={() => { + window.open('https://medium.com/metamask/42549d4870fa', '_blank', 'noopener') + unsetMigratedPrivacyMode() + }} + /> + ) + : null + } + { + viewingUnconnectedDapp + ? ( + <HomeNotification + descriptionText={t('shareAddressToConnect', [activeTab.origin])} + acceptText={t('shareAddress')} + onAccept={() => { + forceApproveProviderRequestByOrigin(activeTab.origin) + }} + infoText={t('shareAddressInfo', [activeTab.origin])} + /> + ) + : null + } + </TransactionView> + ) + : null } </div> </div> ) 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, |