diff --git a/test/e2e/ethereum-on.spec.js b/test/e2e/ethereum-on.spec.js
index ca062ca26..c50615521 100644
--- a/test/e2e/ethereum-on.spec.js
+++ b/test/e2e/ethereum-on.spec.js
@@ -113,7 +113,8 @@ describe('MetaMask', function () {
let extension
let popup
let dapp
- it('switches to a dapp', async () => {
+
+ it('connects to the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
@@ -126,19 +127,27 @@ describe('MetaMask', function () {
const windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
- popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
- dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
+ dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
+
+ await driver.switchTo().window(popup)
await delay(regularDelayMs)
- const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
- await approveButton.click()
+ const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
+ await accountButton.click()
+
+ const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
+ await submitButton.click()
+
+ await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
})
it('has the ganache network id within the dapp', async () => {
const networkDiv = await findElement(driver, By.css('#network'))
+ await delay(regularDelayMs)
assert.equal(await networkDiv.getText(), '5777')
})
diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js
index 90cf35710..143e759ee 100644
--- a/test/e2e/metamask-responsive-ui.spec.js
+++ b/test/e2e/metamask-responsive-ui.spec.js
@@ -134,7 +134,7 @@ describe('MetaMask', function () {
it('show account details dropdown menu', async () => {
await driver.findElement(By.css('div.menu-bar__open-in-browser')).click()
const options = await driver.findElements(By.css('div.menu.account-details-dropdown div.menu__item'))
- assert.equal(options.length, 3) // HD Wallet type does not have to show the Remove Account option
+ assert.equal(options.length, 4) // HD Wallet type does not have to show the Remove Account option
await delay(regularDelayMs)
})
})
diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js
index b5a8220b0..a22c0c1ca 100644
--- a/test/e2e/metamask-ui.spec.js
+++ b/test/e2e/metamask-ui.spec.js
@@ -432,7 +432,7 @@ describe('MetaMask', function () {
await delay(largeDelayMs)
})
- it('starts a send transaction inside the dapp', async () => {
+ it('connects the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
@@ -445,15 +445,22 @@ describe('MetaMask', function () {
windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
- popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
- dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
+ dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
+
+ await driver.switchTo().window(popup)
await delay(regularDelayMs)
- const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
- await approveButton.click()
+ const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
+ await accountButton.click()
+
+ const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
+ await submitButton.click()
+
+ await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(dapp)
- await delay(2000)
+ await delay(regularDelayMs)
})
it('initiates a send from the dapp', async () => {
diff --git a/test/e2e/permissions.spec.js b/test/e2e/permissions.spec.js
new file mode 100644
index 000000000..b7147d7a2
--- /dev/null
+++ b/test/e2e/permissions.spec.js
@@ -0,0 +1,201 @@
+const assert = require('assert')
+const webdriver = require('selenium-webdriver')
+const { By, until } = webdriver
+const {
+ delay,
+} = require('./func')
+const {
+ checkBrowserForConsoleErrors,
+ findElement,
+ findElements,
+ openNewPage,
+ verboseReportOnFailure,
+ waitUntilXWindowHandles,
+ switchToWindowWithTitle,
+ setupFetchMocking,
+ prepareExtensionForTesting,
+} = require('./helpers')
+const enLocaleMessages = require('../../app/_locales/en/messages.json')
+
+describe('MetaMask', function () {
+ let driver
+ let publicAddress
+
+ const tinyDelayMs = 200
+ const regularDelayMs = tinyDelayMs * 2
+ const largeDelayMs = regularDelayMs * 2
+
+ this.timeout(0)
+ this.bail(true)
+
+ before(async function () {
+ const result = await prepareExtensionForTesting()
+ driver = result.driver
+ await setupFetchMocking(driver)
+ })
+
+ afterEach(async function () {
+ if (process.env.SELENIUM_BROWSER === 'chrome') {
+ const errors = await checkBrowserForConsoleErrors(driver)
+ if (errors.length) {
+ const errorReports = errors.map(err => err.message)
+ const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
+ console.error(new Error(errorMessage))
+ }
+ }
+ if (this.currentTest.state === 'failed') {
+ await verboseReportOnFailure(driver, this.currentTest)
+ }
+ })
+
+ after(async function () {
+ await driver.quit()
+ })
+
+ describe('Going through the first time flow, but skipping the seed phrase challenge', () => {
+ it('clicks the continue button on the welcome screen', async () => {
+ await findElement(driver, By.css('.welcome-page__header'))
+ const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
+ welcomeScreenBtn.click()
+ await delay(largeDelayMs)
+ })
+
+ it('clicks the "Create New Wallet" option', async () => {
+ const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Create a Wallet')]`))
+ customRpcButton.click()
+ await delay(largeDelayMs)
+ })
+
+ it('clicks the "No thanks" option on the metametrics opt-in screen', async () => {
+ const optOutButton = await findElement(driver, By.css('.btn-default'))
+ optOutButton.click()
+ await delay(largeDelayMs)
+ })
+
+ it('accepts a secure password', async () => {
+ const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
+ const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
+ const button = await findElement(driver, By.css('.first-time-flow__form button'))
+
+ await passwordBox.sendKeys('correct horse battery staple')
+ await passwordBoxConfirm.sendKeys('correct horse battery staple')
+
+ const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox'))
+ await tosCheckBox.click()
+
+ await button.click()
+ await delay(largeDelayMs)
+ })
+
+ it('skips the seed phrase challenge', async () => {
+ const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
+ await button.click()
+ await delay(regularDelayMs)
+
+ const detailsButton = await findElement(driver, By.css('.account-details__details-button'))
+ await detailsButton.click()
+ await delay(regularDelayMs)
+ })
+
+ it('gets the current accounts address', async () => {
+ const addressInput = await findElement(driver, By.css('.qr-ellip-address'))
+ publicAddress = await addressInput.getAttribute('value')
+ const accountModal = await driver.findElement(By.css('span .modal'))
+
+ await driver.executeScript("document.querySelector('.account-modal-close').click()")
+
+ await driver.wait(until.stalenessOf(accountModal))
+ await delay(regularDelayMs)
+ })
+ })
+
+ describe('sets permissions', () => {
+ let extension
+ let popup
+ let dapp
+
+ it('connects to the dapp', async () => {
+ await openNewPage(driver, 'http://127.0.0.1:8080/')
+ await delay(regularDelayMs)
+
+ const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
+ await connectButton.click()
+
+ await waitUntilXWindowHandles(driver, 3)
+ const windowHandles = await driver.getAllWindowHandles()
+
+ extension = windowHandles[0]
+ dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
+
+ await driver.switchTo().window(popup)
+
+ await delay(regularDelayMs)
+
+ const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
+ await accountButton.click()
+
+ const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
+ await submitButton.click()
+
+ await waitUntilXWindowHandles(driver, 2)
+ await driver.switchTo().window(extension)
+ await delay(regularDelayMs)
+ })
+
+ it('shows connected sites', async () => {
+ const connectedSites = await findElement(driver, By.xpath(`//button[contains(text(), 'Connected Sites')]`))
+ await connectedSites.click()
+
+ await findElement(driver, By.css('.connected-sites__title'))
+
+ const domains = await findElements(driver, By.css('.connected-sites-list__domain'))
+ assert.equal(domains.length, 1)
+
+ const domainName = await findElement(driver, By.css('.connected-sites-list__domain-name'))
+ assert.equal(await domainName.getText(), 'E2E Test Dapp')
+
+ await domains[0].click()
+
+ const permissionDescription = await findElement(driver, By.css('.connected-sites-list__permission-description'))
+ assert.equal(await permissionDescription.getText(), 'View the address of the selected account')
+ })
+
+ it('can get accounts within the dapp', async () => {
+ await driver.switchTo().window(dapp)
+ await delay(regularDelayMs)
+
+ const getAccountsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'eth_accounts')]`))
+ await getAccountsButton.click()
+
+ const getAccountsResult = await findElement(driver, By.css('#getAccountsResult'))
+ assert.equal((await getAccountsResult.getText()).toLowerCase(), publicAddress.toLowerCase())
+ })
+
+ it('can disconnect all accounts', async () => {
+ await driver.switchTo().window(extension)
+
+ const disconnectAllButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Disconnect All')]`))
+ await disconnectAllButton.click()
+
+ const disconnectModal = await driver.findElement(By.css('span .modal'))
+
+ const disconnectAllModalButton = await findElement(driver, By.css('.disconnect-all-modal .btn-danger'))
+ await disconnectAllModalButton.click()
+
+ await driver.wait(until.stalenessOf(disconnectModal))
+ await delay(regularDelayMs)
+ })
+
+ it('can no longer get accounts within the dapp', async () => {
+ await driver.switchTo().window(dapp)
+ await delay(regularDelayMs)
+
+ const getAccountsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'eth_accounts')]`))
+ await getAccountsButton.click()
+
+ const getAccountsResult = await findElement(driver, By.css('#getAccountsResult'))
+ assert.equal(await getAccountsResult.getText(), 'Not able to get accounts')
+ })
+ })
+})
diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh
index 33c2428da..55a06fc88 100755
--- a/test/e2e/run-all.sh
+++ b/test/e2e/run-all.sh
@@ -60,6 +60,14 @@ concurrently --kill-others \
'yarn dapp' \
'sleep 5 && mocha test/e2e/ethereum-on.spec'
+concurrently --kill-others \
+ --names 'ganache,dapp,e2e' \
+ --prefix '[{time}][{name}]' \
+ --success first \
+ 'yarn ganache:start' \
+ 'yarn dapp' \
+ 'sleep 5 && mocha test/e2e/permissions.spec'
+
export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
concurrently --kill-others \
--names 'ganache,sendwithprivatedapp,e2e' \
diff --git a/test/e2e/signature-request.spec.js b/test/e2e/signature-request.spec.js
index a5be61baf..f36b6ac51 100644
--- a/test/e2e/signature-request.spec.js
+++ b/test/e2e/signature-request.spec.js
@@ -119,12 +119,13 @@ describe('MetaMask', function () {
})
})
- describe('provider listening for events', () => {
+ describe('successfuly signs typed data', () => {
let extension
let popup
let dapp
let windowHandles
- it('switches to a dapp', async () => {
+
+ it('connects to the dapp', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
@@ -134,18 +135,24 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 3)
- windowHandles = await driver.getAllWindowHandles()
+ const windowHandles = await driver.getAllWindowHandles()
extension = windowHandles[0]
- popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
- dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
+ dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ popup = windowHandles.find(handle => handle !== extension && handle !== dapp)
+
+ await driver.switchTo().window(popup)
await delay(regularDelayMs)
- const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
- await approveButton.click()
+ const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account'))
+ await accountButton.click()
+
+ const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`))
+ await submitButton.click()
+
+ await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(dapp)
- await delay(regularDelayMs)
})
it('creates a sign typed data signature request', async () => {
@@ -153,6 +160,7 @@ describe('MetaMask', function () {
await signTypedMessage.click()
await delay(largeDelayMs)
+ await delay(regularDelayMs)
windowHandles = await driver.getAllWindowHandles()
await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
await delay(regularDelayMs)
diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js
index f49e518c3..7aea39a36 100644
--- a/test/unit/app/controllers/preferences-controller-test.js
+++ b/test/unit/app/controllers/preferences-controller-test.js
@@ -1,6 +1,7 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
+const { addInternalMethodPrefix } = require('../../../../app/scripts/controllers/permissions')
const sinon = require('sinon')
describe('preferences controller', function () {
@@ -375,7 +376,7 @@ describe('preferences controller', function () {
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubEnd)
sandbox.assert.notCalled(stubNext)
- req.method = 'wallet_watchAsset'
+ req.method = addInternalMethodPrefix('watchAsset')
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.calledTwice(stubEnd)
diff --git a/test/unit/app/controllers/provider-approval-test.js b/test/unit/app/controllers/provider-approval-test.js
deleted file mode 100644
index eeb9e813b..000000000
--- a/test/unit/app/controllers/provider-approval-test.js
+++ /dev/null
@@ -1,330 +0,0 @@
-const assert = require('assert')
-const sinon = require('sinon')
-const ProviderApprovalController = require('../../../../app/scripts/controllers/provider-approval')
-
-const mockLockedKeyringController = {
- memStore: {
- getState: () => ({
- isUnlocked: false,
- }),
- },
-}
-
-const mockUnlockedKeyringController = {
- memStore: {
- getState: () => ({
- isUnlocked: true,
- }),
- },
-}
-
-describe('ProviderApprovalController', () => {
- describe('#_handleProviderRequest', () => {
- it('should add a pending provider request when unlocked', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
-
- controller._handleProviderRequest(metadata)
- assert.deepEqual(controller._getMergedState(), {
- approvedOrigins: {},
- providerRequests: [metadata],
- })
- })
-
- it('should add a pending provider request when locked', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockLockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- assert.deepEqual(controller._getMergedState(), {
- approvedOrigins: {},
- providerRequests: [metadata],
- })
- })
-
- it('should add a 2nd pending provider request when unlocked', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = [{
- hostname: 'https://example1.com',
- origin: 'example1.com',
- siteTitle: 'Example 1',
- siteImage: 'https://example1.com/logo.svg',
- }, {
- hostname: 'https://example2.com',
- origin: 'example2.com',
- siteTitle: 'Example 2',
- siteImage: 'https://example2.com/logo.svg',
- }]
-
- controller._handleProviderRequest(metadata[0])
- controller._handleProviderRequest(metadata[1])
- assert.deepEqual(controller._getMergedState(), {
- approvedOrigins: {},
- providerRequests: metadata,
- })
- })
-
- it('should add a 2nd pending provider request when locked', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockLockedKeyringController,
- })
-
- const metadata = [{
- hostname: 'https://example1.com',
- origin: 'example1.com',
- siteTitle: 'Example 1',
- siteImage: 'https://example1.com/logo.svg',
- }, {
- hostname: 'https://example2.com',
- origin: 'example2.com',
- siteTitle: 'Example 2',
- siteImage: 'https://example2.com/logo.svg',
- }]
-
- controller._handleProviderRequest(metadata[0])
- controller._handleProviderRequest(metadata[1])
- assert.deepEqual(controller._getMergedState(), {
- approvedOrigins: {},
- providerRequests: metadata,
- })
- })
-
- it('should call openPopup when unlocked and when given', () => {
- const openPopup = sinon.spy()
- const controller = new ProviderApprovalController({
- openPopup,
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- assert.ok(openPopup.calledOnce)
- })
-
- it('should call openPopup when locked and when given', () => {
- const openPopup = sinon.spy()
- const controller = new ProviderApprovalController({
- openPopup,
- keyringController: mockLockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- assert.ok(openPopup.calledOnce)
- })
-
- it('should NOT call openPopup when unlocked and when the domain has already been approved', () => {
- const openPopup = sinon.spy()
- const controller = new ProviderApprovalController({
- openPopup,
- keyringController: mockUnlockedKeyringController,
- })
-
- controller.store.updateState({
- approvedOrigins: {
- 'example.com': {
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- },
- },
- })
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- assert.ok(openPopup.notCalled)
- })
- })
-
- describe('#approveProviderRequestByOrigin', () => {
- it('should mark the origin as approved and remove the provider request', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- controller.approveProviderRequestByOrigin('example.com')
- assert.deepEqual(controller._getMergedState(), {
- providerRequests: [],
- approvedOrigins: {
- 'example.com': {
- hostname: 'https://example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- },
- },
- })
- })
-
- it('should mark the origin as approved and multiple requests for the same domain', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- controller._handleProviderRequest(metadata)
- controller.approveProviderRequestByOrigin('example.com')
- assert.deepEqual(controller._getMergedState(), {
- providerRequests: [],
- approvedOrigins: {
- 'example.com': {
- hostname: 'https://example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- },
- },
- })
- })
-
- it('should mark the origin as approved without a provider request', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- controller.approveProviderRequestByOrigin('example.com')
- assert.deepEqual(controller._getMergedState(), {
- providerRequests: [],
- approvedOrigins: {
- 'example.com': {
- hostname: null,
- siteTitle: null,
- siteImage: null,
- },
- },
- })
- })
- })
-
- describe('#rejectProviderRequestByOrigin', () => {
- it('should remove the origin from approved', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- controller.approveProviderRequestByOrigin('example.com')
- controller.rejectProviderRequestByOrigin('example.com')
- assert.deepEqual(controller._getMergedState(), {
- providerRequests: [],
- approvedOrigins: {},
- })
- })
-
- it('should reject the origin even without a pending request', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- controller.rejectProviderRequestByOrigin('example.com')
- assert.deepEqual(controller._getMergedState(), {
- providerRequests: [],
- approvedOrigins: {},
- })
- })
- })
-
- describe('#clearApprovedOrigins', () => {
- it('should clear the approved origins', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- controller.approveProviderRequestByOrigin('example.com')
- controller.clearApprovedOrigins()
- assert.deepEqual(controller._getMergedState(), {
- providerRequests: [],
- approvedOrigins: {},
- })
- })
- })
-
- describe('#shouldExposeAccounts', () => {
- it('should return true for an approved origin', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- controller.approveProviderRequestByOrigin('example.com')
- assert.ok(controller.shouldExposeAccounts('example.com'))
- })
-
- it('should return false for an origin not yet approved', () => {
- const controller = new ProviderApprovalController({
- keyringController: mockUnlockedKeyringController,
- })
-
- const metadata = {
- hostname: 'https://example.com',
- origin: 'example.com',
- siteTitle: 'Example',
- siteImage: 'https://example.com/logo.svg',
- }
- controller._handleProviderRequest(metadata)
- controller.approveProviderRequestByOrigin('example.com')
- assert.ok(!controller.shouldExposeAccounts('bad.website'))
- })
- })
-})
diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js
index d0a989e71..d398c7e04 100644
--- a/test/unit/app/controllers/transactions/tx-controller-test.js
+++ b/test/unit/app/controllers/transactions/tx-controller-test.js
@@ -48,6 +48,7 @@ describe('Transaction Controller', function () {
ethTx.sign(fromAccount.key)
resolve()
}),
+ getPermittedAccounts: () => {},
})
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
})
@@ -176,13 +177,15 @@ describe('Transaction Controller', function () {
describe('#addUnapprovedTransaction', function () {
const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d'
- let getSelectedAddress
+ let getSelectedAddress, getPermittedAccounts
beforeEach(function () {
getSelectedAddress = sinon.stub(txController, 'getSelectedAddress').returns(selectedAddress)
+ getPermittedAccounts = sinon.stub(txController, 'getPermittedAccounts').returns([selectedAddress])
})
afterEach(function () {
getSelectedAddress.restore()
+ getPermittedAccounts.restore()
})
it('should add an unapproved transaction and return a valid txMeta', function (done) {
diff --git a/ui/app/components/app/account-details/account-details.component.js b/ui/app/components/app/account-details/account-details.component.js
index 55078cee0..e75e777a6 100644
--- a/ui/app/components/app/account-details/account-details.component.js
+++ b/ui/app/components/app/account-details/account-details.component.js
@@ -19,9 +19,11 @@ export default class AccountDetails extends Component {
static propTypes = {
hideSidebar: PropTypes.func,
showAccountDetailModal: PropTypes.func,
+ showConnectedSites: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
checksummedAddress: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
+ history: PropTypes.object.isRequired,
}
state = {
@@ -48,6 +50,7 @@ export default class AccountDetails extends Component {
const {
hideSidebar,
showAccountDetailModal,
+ showConnectedSites,
label,
checksummedAddress,
name,
@@ -65,14 +68,19 @@ export default class AccountDetails extends Component {
{label}
-
-
+
+
{name}
-
+
+
+
+
list.concat(keyring.accounts), [])
@@ -71,6 +76,8 @@ export default class AccountMenu extends PureComponent {
const keyring = keyrings.find(kr => {
return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address)
})
+ const addressDomains = addressConnectedDomainMap[identity.address] || {}
+ const iconAndNameForOpenDomain = addressDomains[originOfCurrentTab]
return (
+ { iconAndNameForOpenDomain
+ ? (
+ - {
+ e.stopPropagation()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Account Options',
+ name: 'Opened Connected Sites',
+ },
+ })
+ history.push(CONNECTED_ROUTE)
+ }}
+ text={this.context.t('connectedSites')}
+ icon={(
+
+ )}
+ />
{
isRemovable
? (
diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss
index 1afbebd00..7578aa204 100644
--- a/ui/app/components/app/index.scss
+++ b/ui/app/components/app/index.scss
@@ -38,7 +38,7 @@
@import '../../pages/index';
-@import 'provider-page-container/index';
+@import 'permission-page-container/index';
@import 'selected-account/index';
@@ -64,18 +64,12 @@
@import 'transaction-status/index';
-@import 'app-header/index';
-
@import 'sidebars/index';
@import '../ui/unit-input/index';
@import 'gas-customization/gas-modal-page-container/index';
-@import 'gas-customization/gas-modal-page-container/index';
-
-@import 'gas-customization/gas-modal-page-container/index';
-
@import 'gas-customization/index';
@import 'gas-customization/gas-price-button-group/index';
@@ -87,3 +81,7 @@
@import 'multiple-notifications/index';
@import 'signature-request/index';
+
+@import 'connected-sites-list/index';
+
+@import '../ui/icon-with-fallback/index';
diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js
index f0fdd3bd5..6c45160fd 100644
--- a/ui/app/components/app/modal/modal.component.js
+++ b/ui/app/components/app/modal/modal.component.js
@@ -16,6 +16,7 @@ export default class Modal extends PureComponent {
submitType: PropTypes.string,
submitText: PropTypes.string,
submitDisabled: PropTypes.bool,
+ hideFooter: PropTypes.bool,
// Cancel button (left button)
onCancel: PropTypes.func,
cancelType: PropTypes.string,
@@ -41,6 +42,7 @@ export default class Modal extends PureComponent {
cancelText,
contentClass,
containerClass,
+ hideFooter,
} = this.props
return (
@@ -61,27 +63,32 @@ export default class Modal extends PureComponent {
{ children }
-
- {
- onCancel && (
+ { !hideFooter
+ ? (
+
+ {
+ onCancel && (
+
+ )
+ }
- )
- }
-
-
+
+ )
+ : null
+ }
)
}
diff --git a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js
deleted file mode 100644
index ceaa20a95..000000000
--- a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { PureComponent } from 'react'
-import PropTypes from 'prop-types'
-import Modal, { ModalContent } from '../../modal'
-
-export default class ClearApprovedOrigins extends PureComponent {
- static propTypes = {
- hideModal: PropTypes.func.isRequired,
- clearApprovedOrigins: PropTypes.func.isRequired,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- }
-
- handleClear = () => {
- const { clearApprovedOrigins, hideModal } = this.props
- clearApprovedOrigins()
- hideModal()
- }
-
- render () {
- const { t } = this.context
-
- return (
- this.props.hideModal()}
- submitText={t('ok')}
- cancelText={t('nevermind')}
- submitType="secondary"
- >
-
-
- )
- }
-}
diff --git a/ui/app/components/app/modals/clear-approved-origins/index.js b/ui/app/components/app/modals/clear-approved-origins/index.js
deleted file mode 100644
index b3e321995..000000000
--- a/ui/app/components/app/modals/clear-approved-origins/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './clear-approved-origins.container'
diff --git a/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js b/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js
new file mode 100644
index 000000000..4fe5c7227
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Modal from '../../modal'
+import Button from '../../../ui/button'
+
+
+export default class DisconnectAccount extends PureComponent {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ disconnectAccount: PropTypes.func.isRequired,
+ accountLabel: PropTypes.string.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const { t } = this.context
+ const { hideModal, disconnectAccount, accountLabel } = this.props
+
+ return (
+ hideModal()}
+ hideFooter
+ >
+
+
+ { t('disconnectAccountModalDescription', [ accountLabel ]) }
+
+
+
+
+
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js b/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js
new file mode 100644
index 000000000..b0511bb47
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js
@@ -0,0 +1,44 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
+import DisconnectAccount from './disconnect-account.component'
+import { getCurrentAccountWithSendEtherInfo } from '../../../../selectors/selectors'
+import { removePermissionsFor } from '../../../../store/actions'
+
+const mapStateToProps = state => {
+ return {
+ ...state.appState.modal.modalState.props || {},
+ accountLabel: getCurrentAccountWithSendEtherInfo(state).name,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ disconnectAccount: (domainKey, domain) => {
+ const permissionMethodNames = domain.permissions.map(perm => perm.parentCapability)
+ dispatch(removePermissionsFor({ [domainKey]: permissionMethodNames }))
+ },
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const {
+ domainKey,
+ domain,
+ } = stateProps
+ const {
+ disconnectAccount: dispatchDisconnectAccount,
+ } = dispatchProps
+
+ return {
+ ...ownProps,
+ ...stateProps,
+ ...dispatchProps,
+ disconnectAccount: () => dispatchDisconnectAccount(domainKey, domain),
+ }
+}
+
+export default compose(
+ withModalProps,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(DisconnectAccount)
diff --git a/ui/app/components/app/modals/disconnect-account/index.js b/ui/app/components/app/modals/disconnect-account/index.js
new file mode 100644
index 000000000..43bfac9fd
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-account/index.js
@@ -0,0 +1 @@
+export { default } from './disconnect-account.container'
diff --git a/ui/app/components/app/modals/disconnect-account/index.scss b/ui/app/components/app/modals/disconnect-account/index.scss
new file mode 100644
index 000000000..861b7cec2
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-account/index.scss
@@ -0,0 +1,25 @@
+.disconnect-account-modal {
+ &__description {
+ color: #24292E;
+ margin-bottom: 24px;
+ }
+
+ &__cancel-button {
+ border: none;
+ margin-top: 12px;
+ }
+}
+
+.disconnect-account-modal-container {
+ .modal-container__header-text {
+ @extend %header--18;
+ }
+
+ .modal-container__content {
+ padding-bottom: 18px;
+
+ @media screen and (max-width: 575px) {
+ padding-bottom: 18px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/app/components/app/modals/disconnect-all/disconnect-all.component.js b/ui/app/components/app/modals/disconnect-all/disconnect-all.component.js
new file mode 100644
index 000000000..2d29fd9ea
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-all/disconnect-all.component.js
@@ -0,0 +1,54 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Modal from '../../modal'
+import Button from '../../../ui/button'
+import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes'
+
+export default class DisconnectAll extends PureComponent {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ disconnectAll: PropTypes.func.isRequired,
+ history: PropTypes.object.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const { t } = this.context
+ const { hideModal, disconnectAll, history } = this.props
+
+ return (
+ hideModal()}
+ hideFooter
+ >
+
+
+ { t('disconnectAllModalDescription') }
+
+
+
+
+
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js b/ui/app/components/app/modals/disconnect-all/disconnect-all.container.js
similarity index 53%
rename from ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js
rename to ui/app/components/app/modals/disconnect-all/disconnect-all.container.js
index 2276bc7e7..2415c3fa9 100644
--- a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js
+++ b/ui/app/components/app/modals/disconnect-all/disconnect-all.container.js
@@ -1,16 +1,20 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
-import ClearApprovedOriginsComponent from './clear-approved-origins.component'
-import { clearApprovedOrigins } from '../../../../store/actions'
+import DisconnectAll from './disconnect-all.component'
+import { clearPermissions } from '../../../../store/actions'
const mapDispatchToProps = dispatch => {
return {
- clearApprovedOrigins: () => dispatch(clearApprovedOrigins()),
+ disconnectAll: () => {
+ dispatch(clearPermissions())
+ },
}
}
export default compose(
withModalProps,
+ withRouter,
connect(null, mapDispatchToProps)
-)(ClearApprovedOriginsComponent)
+)(DisconnectAll)
diff --git a/ui/app/components/app/modals/disconnect-all/index.js b/ui/app/components/app/modals/disconnect-all/index.js
new file mode 100644
index 000000000..7fdfac530
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-all/index.js
@@ -0,0 +1 @@
+export { default } from './disconnect-all.container'
diff --git a/ui/app/components/app/modals/disconnect-all/index.scss b/ui/app/components/app/modals/disconnect-all/index.scss
new file mode 100644
index 000000000..8f69baade
--- /dev/null
+++ b/ui/app/components/app/modals/disconnect-all/index.scss
@@ -0,0 +1,38 @@
+.disconnect-all-modal {
+ height: 160px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ &__description {
+ color: #24292E;
+ margin-bottom: 24px;
+ }
+
+ &__cancel-button {
+ border: none;
+ margin-top: 12px;
+ }
+
+ .btn-secondary {
+ border: none;
+ }
+}
+
+.disconnect-all-modal-container {
+ .modal-container__header-text {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: bold;
+ font-size: 18px;
+ color: #24292E;
+ }
+
+ .modal-container__content {
+ padding-bottom: 18px;
+
+ @media screen and (max-width: 575px) {
+ padding-bottom: 18px;
+ }
+ }
+}
diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss
index da7a27b84..dbf47265f 100644
--- a/ui/app/components/app/modals/index.scss
+++ b/ui/app/components/app/modals/index.scss
@@ -11,3 +11,9 @@
@import './add-to-addressbook-modal/index';
@import './edit-approval-permission/index';
+
+@import './disconnect-account/index';
+
+@import './disconnect-all/index';
+
+@import './new-account-modal/index';
diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js
index 02690722b..0409e901f 100644
--- a/ui/app/components/app/modals/modal.js
+++ b/ui/app/components/app/modals/modal.js
@@ -24,11 +24,13 @@ import CancelTransaction from './cancel-transaction'
import MetaMetricsOptInModal from './metametrics-opt-in-modal'
import RejectTransactions from './reject-transactions'
-import ClearApprovedOrigins from './clear-approved-origins'
import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
import ConfirmDeleteNetwork from './confirm-delete-network'
import AddToAddressBookModal from './add-to-addressbook-modal'
import EditApprovalPermission from './edit-approval-permission'
+import NewAccountModal from './new-account-modal'
+import DisconnectAccount from './disconnect-account'
+import DisconnectAll from './disconnect-all'
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
@@ -139,6 +141,87 @@ const MODALS = {
},
},
+ NEW_ACCOUNT: {
+ contents: ,
+ mobileModalStyle: {
+ width: '95%',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ borderRadius: '10px',
+ },
+ laptopModalStyle: {
+ width: '375px',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ borderRadius: '10px',
+ },
+ contentStyle: {
+ borderRadius: '10px',
+ },
+ },
+
+ DISCONNECT_ACCOUNT: {
+ contents: ,
+ mobileModalStyle: {
+ width: '95%',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ borderRadius: '10px',
+ },
+ laptopModalStyle: {
+ width: '375px',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ borderRadius: '10px',
+ },
+ contentStyle: {
+ borderRadius: '10px',
+ },
+ },
+
+ DISCONNECT_ALL: {
+ contents: ,
+ mobileModalStyle: {
+ width: '95%',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ borderRadius: '10px',
+ },
+ laptopModalStyle: {
+ width: '375px',
+ top: '10%',
+ boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ borderRadius: '10px',
+ },
+ contentStyle: {
+ borderRadius: '10px',
+ },
+ },
+
ACCOUNT_DETAILS: {
contents: ,
...accountModalStyle,
@@ -161,19 +244,6 @@ const MODALS = {
},
},
- CLEAR_APPROVED_ORIGINS: {
- contents: ,
- mobileModalStyle: {
- ...modalContainerMobileStyle,
- },
- laptopModalStyle: {
- ...modalContainerLaptopStyle,
- },
- contentStyle: {
- borderRadius: '8px',
- },
- },
-
METAMETRICS_OPT_IN_MODAL: {
contents: ,
mobileModalStyle: {
diff --git a/ui/app/components/app/modals/new-account-modal/index.js b/ui/app/components/app/modals/new-account-modal/index.js
new file mode 100644
index 000000000..2c8b78890
--- /dev/null
+++ b/ui/app/components/app/modals/new-account-modal/index.js
@@ -0,0 +1 @@
+export { default } from './new-account-modal.container'
diff --git a/ui/app/components/app/modals/new-account-modal/index.scss b/ui/app/components/app/modals/new-account-modal/index.scss
new file mode 100644
index 000000000..d6c2d0ac1
--- /dev/null
+++ b/ui/app/components/app/modals/new-account-modal/index.scss
@@ -0,0 +1,37 @@
+.new-account-modal {
+ @extend %col-nowrap;
+ @extend %modal;
+
+ &__content {
+ @extend %col-nowrap;
+ padding: 1.5rem;
+ border-bottom: 1px solid $Grey-100;
+
+ &__header {
+ @extend %h3;
+ }
+ }
+
+ &__input-label {
+ color: $Grey-600;
+ margin-top: 1.25rem;
+ }
+
+ &__input {
+ @extend %input;
+ margin-top: 0.75rem;
+
+ &::placeholder {
+ color: $Grey-300;
+ }
+ }
+
+ &__footer {
+ @extend %row-nowrap;
+ padding: 1rem;
+
+ button + button {
+ margin-left: 1rem;
+ }
+ }
+}
diff --git a/ui/app/components/app/modals/new-account-modal/new-account-modal.component.js b/ui/app/components/app/modals/new-account-modal/new-account-modal.component.js
new file mode 100644
index 000000000..26224ae63
--- /dev/null
+++ b/ui/app/components/app/modals/new-account-modal/new-account-modal.component.js
@@ -0,0 +1,78 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../ui/button/button.component'
+
+export default class NewAccountModal extends Component {
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ newAccountNumber: PropTypes.number.isRequired,
+ onSave: PropTypes.func.isRequired,
+ }
+
+ state = {
+ alias: '',
+ }
+
+ onChange = e => {
+ this.setState({
+ alias: e.target.value,
+ })
+ }
+
+ onSubmit = () => {
+ this.props.onSave(this.state.alias)
+ .then(this.props.hideModal)
+ }
+
+ onKeyPress = e => {
+ if (e.key === 'Enter' && this.state.alias) {
+ this.onSubmit()
+ }
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+
+
+
+ {t('newAccount')}
+
+
+ {t('accountName')}
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/ui/app/components/app/modals/new-account-modal/new-account-modal.container.js b/ui/app/components/app/modals/new-account-modal/new-account-modal.container.js
new file mode 100644
index 000000000..812e98dbd
--- /dev/null
+++ b/ui/app/components/app/modals/new-account-modal/new-account-modal.container.js
@@ -0,0 +1,44 @@
+import { connect } from 'react-redux'
+import NewAccountModal from './new-account-modal.component'
+import actions from '../../../../store/actions'
+
+function mapStateToProps (state) {
+ return {
+ ...state.appState.modal.modalState.props || {},
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ hideModal: () => dispatch(actions.hideModal()),
+ createAccount: newAccountName => {
+ return dispatch(actions.addNewAccount())
+ .then(newAccountAddress => {
+ if (newAccountName) {
+ dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
+ }
+ return newAccountAddress
+ })
+ },
+ }
+}
+
+function mergeProps (stateProps, dispatchProps) {
+ const {
+ onCreateNewAccount,
+ } = stateProps
+ const {
+ createAccount,
+ } = dispatchProps
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ onSave: (newAccountName) => {
+ return createAccount(newAccountName)
+ .then(newAccountAddress => onCreateNewAccount(newAccountAddress))
+ },
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(NewAccountModal)
diff --git a/ui/app/components/app/permission-page-container/index.js b/ui/app/components/app/permission-page-container/index.js
new file mode 100644
index 000000000..ea3b8daaa
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/index.js
@@ -0,0 +1,3 @@
+export {default} from './permission-page-container.container'
+export {default as PermissionPageContainerContent} from './permission-page-container-content'
+export {default as PermissionPageContainerHeader} from './permission-page-container-header'
diff --git a/ui/app/components/app/permission-page-container/index.scss b/ui/app/components/app/permission-page-container/index.scss
new file mode 100644
index 000000000..7979867fa
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/index.scss
@@ -0,0 +1,281 @@
+.permission-approval-container {
+ display: flex;
+ border: none;
+ box-shadow: none;
+ margin-top: 45px;
+ width: 466px;
+ min-height: 468px;
+
+ &__header {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ border-bottom: 1px solid $geyser;
+ padding: 9px;
+ }
+
+ &__title {
+ @extend %header--18;
+ line-height: 25px;
+ text-align: center;
+ position: fixed;
+ left: 0;
+ width: 100%;
+ }
+
+ &__content {
+ display: flex;
+ overflow-y: auto;
+ flex: 1;
+ flex-direction: column;
+ color: #7C808E;
+
+ &--redirect {
+ margin-top: 60px;
+ }
+
+ h1, h2 {
+ color: #4A4A4A;
+ display: flex;
+ justify-content: center;
+ text-align: center;
+ }
+
+ h2 {
+ font-size: 16px;
+ line-height: 18px;
+ padding: 20px;
+ }
+
+ h1 {
+ font-size: 22px;
+ line-height: 26px;
+ padding: 20px;
+ }
+
+ p {
+ padding: 0 40px;
+ text-align: center;
+ font-size: 12px;
+ line-height: 18px;
+ }
+
+ a, a:hover {
+ color: $dodger-blue;
+ }
+
+ section {
+ h1 {
+ padding: 30px 0px 0px 0px;
+ }
+
+ h2 {
+ padding: 0px 0px 20px 0px;
+ }
+ }
+
+ &__requested {
+ text-align: left;
+ }
+
+ &__revoke-note {
+ margin-top: 24px;
+ }
+
+ &__checkbox {
+ margin-right: 10px;
+ }
+
+ &__permission {
+ margin-top: 18px;
+
+ i {
+ color: #6A737D;
+ }
+ label {
+ margin-left: 6px;
+ color: #24292E;
+ }
+ }
+
+ .permission-approval-visual {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ position: relative;
+ margin: 0 32px;
+ margin-top: 40px;
+
+ section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+ }
+
+ h1 {
+ font-size: 14px;
+ line-height: 18px;
+ padding: 8px 0 0;
+ }
+
+ h2 {
+ font-size: 12px;
+ line-height: 17px;
+ color: #6A737D;
+ padding: 0;
+ }
+
+ &__check {
+ width: 40px;
+ height: 40px;
+ background: white url("/images/permissions-check.svg") no-repeat;
+ margin-top: 24px;
+ z-index: 1;
+ }
+
+ &__reject {
+ background: white;
+ z-index: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ i {
+ color: #D73A49;
+ transform: scale(3);
+ }
+ }
+
+ &__broken-line {
+ z-index: 0;
+ position: absolute;
+ top: 43px;
+ }
+
+ &__identicon, .icon-with-fallback__identicon {
+ width: 32px;
+ height: 32px;
+ z-index: 1;
+
+ &--default {
+ background-color: #777A87;
+ color: white;
+ width: 64px;
+ height: 64px;
+ border-radius: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ z-index: 1;
+ }
+ }
+
+ &__identicon-container, .icon-with-fallback__identicon-container {
+ padding: 1rem;
+ flex: 1;
+ position: relative;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__identicon-border, .icon-with-fallback__identicon-border {
+ height: 64px;
+ width: 64px;
+ border-radius: 50%;
+ border: 1px solid white;
+ position: absolute;
+ background: #FFFFFF;
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25);
+ }
+
+ &:before {
+ border-top: 2px dashed #CDD1E4;
+ content: "";
+ margin: 0 auto;
+ position: absolute;
+ top: 32px;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ width: 65%;
+ z-index: -1;
+ }
+
+ &__account-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &__label {
+ @extend %content-text;
+ line-height: 20px;
+ color: #000000;
+ }
+
+ &__address {
+ @extend %font;
+ font-size: 12px;
+ line-height: 17px;
+ color: #6A737D;
+ }
+ }
+ }
+
+ .secure-badge {
+ display: flex;
+ justify-content: center;
+ padding: 25px;
+ }
+ }
+
+ &__permissions-header {
+ @extend %content-text;
+ line-height: 20px;
+ color: #6A737D;
+
+ &--redirect {
+ text-align: center;
+ }
+ }
+
+ &__permissions-container {
+ display: flex;
+ flex-direction: column;
+ margin-top: 33px;
+ }
+
+ .page-container__footer {
+ border-top: none;
+ align-items: center;
+
+ header {
+ width: 300px;
+ }
+ }
+
+ &__permissions-header-redirect {
+ text-align: center;
+ }
+
+ @media screen and (max-width: 575px) {
+ width: 100%;
+ margin-top: 25px;
+ padding: 10px;
+
+ &__title {
+ position: initial;
+ }
+
+ &__content-approval-visual {
+ margin-top: 16px;
+ }
+
+ .page-container__footer header {
+ padding: 0;
+ }
+ }
+}
diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/index.js b/ui/app/components/app/permission-page-container/permission-page-container-content/index.js
new file mode 100644
index 000000000..899d168f9
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/permission-page-container-content/index.js
@@ -0,0 +1 @@
+export {default} from './permission-page-container-content.component'
diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js
new file mode 100644
index 000000000..d6a62dbbf
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js
@@ -0,0 +1,163 @@
+import PropTypes from 'prop-types'
+import React, { PureComponent } from 'react'
+import Identicon from '../../../ui/identicon'
+import IconWithFallBack from '../../../ui/icon-with-fallback'
+import classnames from 'classnames'
+
+export default class PermissionPageContainerContent extends PureComponent {
+
+ static propTypes = {
+ requestMetadata: PropTypes.object.isRequired,
+ domainMetadata: PropTypes.object.isRequired,
+ selectedPermissions: PropTypes.object.isRequired,
+ permissionsDescriptions: PropTypes.object.isRequired,
+ onPermissionToggle: PropTypes.func.isRequired,
+ selectedAccount: PropTypes.object,
+ redirect: PropTypes.bool,
+ permissionRejected: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ redirect: null,
+ permissionRejected: null,
+ selectedAccount: {},
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ renderAccountInfo = (account) => {
+ return (
+
+
+ { account.label }
+
+
+ { account.truncatedAddress }
+
+
+ )
+ }
+
+ renderPermissionApprovalVisual = () => {
+ const {
+ requestMetadata, domainMetadata, selectedAccount, redirect, permissionRejected,
+ } = this.props
+
+ return (
+
+
+
+ { redirect ? null : {domainMetadata.name}
}
+ { redirect ? null : {requestMetadata.origin}
}
+
+ { permissionRejected
+ ?
+ :
+ }
+
+
+
+ { redirect ? null : this.renderAccountInfo(selectedAccount) }
+
+
+ )
+ }
+
+ renderRequestedPermissions () {
+ const {
+ selectedPermissions, permissionsDescriptions, onPermissionToggle,
+ } = this.props
+ const { t } = this.context
+
+ const items = Object.keys(selectedPermissions).map((methodName) => {
+
+ // the request will almost certainly be reject by rpc-cap if this happens
+ if (!permissionsDescriptions[methodName]) {
+ console.warn(`Unknown permission requested: ${methodName}`)
+ }
+ const description = permissionsDescriptions[methodName] || methodName
+ // don't allow deselecting eth_accounts
+ const isDisabled = methodName === 'eth_accounts'
+
+ return (
+ {
+ if (!isDisabled) {
+ onPermissionToggle(methodName)
+ }
+ }}
+ >
+ { selectedPermissions[methodName]
+ ?
+ :
+ }
+
+
+ )
+ })
+
+ return (
+
+ {items}
+
{ t('revokeInPermissions') }
+
+ )
+ }
+
+ render () {
+ const { domainMetadata, redirect, permissionRejected } = this.props
+ const { t } = this.context
+
+ let titleArgs
+ if (redirect && permissionRejected) {
+ titleArgs = [ 'cancelledConnectionWithMetaMask' ]
+ } else if (redirect) {
+ titleArgs = [ 'connectingWithMetaMask' ]
+ } else if (domainMetadata.extensionId) {
+ titleArgs = [ 'externalExtension', [domainMetadata.extensionId] ]
+ } else {
+ titleArgs = [ 'likeToConnect', [domainMetadata.name] ]
+ }
+
+ return (
+
+
+ { t(...titleArgs) }
+
+ {this.renderPermissionApprovalVisual()}
+ { !redirect
+ ? (
+
+
+ { domainMetadata.extensionId
+ ? t('thisWillAllowExternalExtension', [domainMetadata.extensionId])
+ : t('thisWillAllow', [domainMetadata.name])
+ }
+
+ { this.renderRequestedPermissions() }
+
+ )
+ : (
+
+ { t('redirectingBackToDapp') }
+
+ )
+ }
+
+ )
+ }
+}
diff --git a/ui/app/components/app/permission-page-container/permission-page-container-header/index.js b/ui/app/components/app/permission-page-container/permission-page-container-header/index.js
new file mode 100644
index 000000000..45ef9036b
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/permission-page-container-header/index.js
@@ -0,0 +1 @@
+export {default} from './permission-page-container-header.component'
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js b/ui/app/components/app/permission-page-container/permission-page-container-header/permission-page-container-header.component.js
similarity index 58%
rename from ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js
rename to ui/app/components/app/permission-page-container/permission-page-container-header/permission-page-container-header.component.js
index 41bf6c3dd..8ba3444ba 100644
--- a/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js
+++ b/ui/app/components/app/permission-page-container/permission-page-container-header/permission-page-container-header.component.js
@@ -1,10 +1,10 @@
import React, {PureComponent} from 'react'
import NetworkDisplay from '../../network-display'
-export default class ProviderPageContainerHeader extends PureComponent {
+export default class PermissionPageContainerHeader extends PureComponent {
render () {
return (
-
+
)
diff --git a/ui/app/components/app/permission-page-container/permission-page-container.component.js b/ui/app/components/app/permission-page-container/permission-page-container.component.js
new file mode 100644
index 000000000..b7cbcd6ba
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/permission-page-container.component.js
@@ -0,0 +1,151 @@
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import deepEqual from 'fast-deep-equal'
+import { PermissionPageContainerContent } from '.'
+import { PageContainerFooter } from '../../ui/page-container'
+
+export default class PermissionPageContainer extends Component {
+
+ static propTypes = {
+ approvePermissionsRequest: PropTypes.func.isRequired,
+ rejectPermissionsRequest: PropTypes.func.isRequired,
+ selectedIdentity: PropTypes.object,
+ permissionsDescriptions: PropTypes.object.isRequired,
+ request: PropTypes.object,
+ redirect: PropTypes.bool,
+ permissionRejected: PropTypes.bool,
+ requestMetadata: PropTypes.object,
+ targetDomainMetadata: PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ redirect: null,
+ permissionRejected: null,
+ request: {},
+ requestMetadata: {},
+ selectedIdentity: {},
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ };
+
+ state = {
+ selectedPermissions: this.getRequestedMethodState(
+ this.getRequestedMethodNames(this.props)
+ ),
+ }
+
+ componentDidUpdate () {
+ const newMethodNames = this.getRequestedMethodNames(this.props)
+
+ if (!deepEqual(Object.keys(this.state.selectedPermissions), newMethodNames)) {
+ // this should be a new request, so just overwrite
+ this.setState({
+ selectedPermissions: this.getRequestedMethodState(newMethodNames),
+ })
+ }
+ }
+
+ getRequestedMethodState (methodNames) {
+ return methodNames.reduce(
+ (acc, methodName) => {
+ acc[methodName] = true
+ return acc
+ },
+ {}
+ )
+ }
+
+ getRequestedMethodNames (props) {
+ return Object.keys(props.request.permissions || {})
+ }
+
+ onPermissionToggle = methodName => {
+ this.setState({
+ selectedPermissions: {
+ ...this.state.selectedPermissions,
+ [methodName]: !this.state.selectedPermissions[methodName],
+ },
+ })
+ }
+
+ componentDidMount () {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Auth',
+ action: 'Connect',
+ name: 'Tab Opened',
+ },
+ })
+ }
+
+ onCancel = () => {
+ const { request, rejectPermissionsRequest } = this.props
+ rejectPermissionsRequest(request.metadata.id)
+ }
+
+ onSubmit = () => {
+ const {
+ request: _request, approvePermissionsRequest, rejectPermissionsRequest, selectedIdentity,
+ } = this.props
+
+ const request = {
+ ..._request,
+ permissions: { ..._request.permissions },
+ }
+
+ Object.keys(this.state.selectedPermissions).forEach(key => {
+ if (!this.state.selectedPermissions[key]) {
+ delete request.permissions[key]
+ }
+ })
+
+ if (Object.keys(request.permissions).length > 0) {
+ approvePermissionsRequest(request, [selectedIdentity.address])
+ } else {
+ rejectPermissionsRequest(request.metadata.id)
+ }
+ }
+
+ render () {
+ const {
+ requestMetadata,
+ targetDomainMetadata,
+ permissionsDescriptions,
+ selectedIdentity,
+ redirect,
+ permissionRejected,
+ } = this.props
+
+ return (
+
+
+ { !redirect
+ ? (
+
this.onCancel()}
+ cancelText={this.context.t('cancel')}
+ onSubmit={() => this.onSubmit()}
+ submitText={this.context.t('submit')}
+ submitButtonType="confirm"
+ buttonSizeLarge={false}
+ />
+ )
+ : null
+ }
+
+ )
+ }
+}
diff --git a/ui/app/components/app/permission-page-container/permission-page-container.container.js b/ui/app/components/app/permission-page-container/permission-page-container.container.js
new file mode 100644
index 000000000..f83393c70
--- /dev/null
+++ b/ui/app/components/app/permission-page-container/permission-page-container.container.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import PermissionPageContainer from './permission-page-container.component'
+import {
+ getPermissionsDescriptions,
+ getDomainMetadata,
+} from '../../../selectors/selectors'
+
+const mapStateToProps = (state, ownProps) => {
+ const { request, cachedOrigin } = ownProps
+ const { metadata: requestMetadata = {} } = request || {}
+
+ const domainMetadata = getDomainMetadata(state)
+ const origin = requestMetadata.origin || cachedOrigin
+ const targetDomainMetadata = (domainMetadata[origin] || { name: origin, icon: null })
+
+ return {
+ permissionsDescriptions: getPermissionsDescriptions(state),
+ requestMetadata,
+ targetDomainMetadata,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps)
+)(PermissionPageContainer)
diff --git a/ui/app/components/app/provider-page-container/index.js b/ui/app/components/app/provider-page-container/index.js
deleted file mode 100644
index 927c35940..000000000
--- a/ui/app/components/app/provider-page-container/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export {default} from './provider-page-container.component'
-export {default as ProviderPageContainerContent} from './provider-page-container-content'
-export {default as ProviderPageContainerHeader} from './provider-page-container-header'
diff --git a/ui/app/components/app/provider-page-container/index.scss b/ui/app/components/app/provider-page-container/index.scss
deleted file mode 100644
index 8d35ac179..000000000
--- a/ui/app/components/app/provider-page-container/index.scss
+++ /dev/null
@@ -1,121 +0,0 @@
-.provider-approval-container {
- display: flex;
-
- &__header {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- border-bottom: 1px solid $geyser;
- padding: 9px;
- }
-
- &__content {
- display: flex;
- overflow-y: auto;
- flex: 1;
- flex-direction: column;
- justify-content: space-between;
- color: #7C808E;
-
- h1, h2 {
- color: #4A4A4A;
- display: flex;
- justify-content: center;
- text-align: center;
- }
-
- h2 {
- font-size: 16px;
- line-height: 18px;
- padding: 20px;
- }
-
- h1 {
- font-size: 22px;
- line-height: 26px;
- padding: 20px;
- }
-
- p {
- padding: 0 40px;
- text-align: center;
- font-size: 12px;
- line-height: 18px;
- }
-
- a, a:hover {
- color: $dodger-blue;
- }
-
- .provider-approval-visual {
- display: flex;
- flex-direction: row;
- justify-content: space-evenly;
- position: relative;
- margin: 0 32px;
-
- section {
- display: flex;
- flex-direction: column;
- align-items: center;
- flex: 1;
- }
-
- h1 {
- font-size: 14px;
- line-height: 18px;
- padding: 8px 0 0;
- }
-
- h2 {
- font-size: 10px;
- line-height: 14px;
- padding: 0;
- color: #A2A4AC;
- }
-
- &__check {
- width: 40px;
- height: 40px;
- background: white url("/images/provider-approval-check.svg") no-repeat;
- margin-top: 14px;
- }
-
- &__identicon {
- width: 64px;
- height: 64px;
-
- &--default {
- background-color: #777A87;
- color: white;
- width: 64px;
- height: 64px;
- border-radius: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: bold;
- }
- }
-
- &:before {
- border-top: 2px dashed #CDD1E4;
- content: "";
- margin: 0 auto;
- position: absolute;
- top: 32px;
- left: 0;
- bottom: 0;
- right: 0;
- width: 65%;
- z-index: -1;
- }
- }
-
- .secure-badge {
- display: flex;
- justify-content: center;
- padding: 25px;
- }
- }
-}
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/index.js b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js
deleted file mode 100644
index 73e491adc..000000000
--- a/ui/app/components/app/provider-page-container/provider-page-container-content/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export {default} from './provider-page-container-content.container'
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
deleted file mode 100644
index 4f94015b1..000000000
--- a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import PropTypes from 'prop-types'
-import React, {PureComponent} from 'react'
-import Identicon from '../../../ui/identicon'
-
-export default class ProviderPageContainerContent extends PureComponent {
- static propTypes = {
- origin: PropTypes.string.isRequired,
- selectedIdentity: PropTypes.object.isRequired,
- siteImage: PropTypes.string,
- siteTitle: PropTypes.string,
- hostname: PropTypes.string,
- extensionId: PropTypes.string,
- }
-
- static contextTypes = {
- t: PropTypes.func,
- };
-
- renderConnectVisual = (title, identifier) => {
- const { selectedIdentity, siteImage } = this.props
-
- return (
-
-
- {siteImage ? (
-
- ) : (
-
- {title.charAt(0).toUpperCase()}
-
- )}
- {title}
- {identifier}
-
-
-
-
- {selectedIdentity.name}
-
-
- )
- }
-
- render () {
- const { siteTitle, hostname, extensionId } = this.props
- const { t } = this.context
-
- const title = extensionId ?
- 'External Extension' :
- siteTitle || hostname
-
- const identifier = extensionId ?
- `Extension ID: '${extensionId}'` :
- hostname
-
- return (
-
-
- {t('connectRequest')}
- {this.renderConnectVisual(title, identifier)}
- {t('providerRequest', [title])}
-
- {t('providerRequestInfo')}
-
-
- {t('learnMore')}.
-
-
-
-
-
-
-
- )
- }
-}
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
deleted file mode 100644
index 4dbdddd16..000000000
--- a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { connect } from 'react-redux'
-import ProviderPageContainerContent from './provider-page-container-content.component'
-import { getSelectedIdentity } from '../../../../selectors/selectors'
-
-const mapStateToProps = (state) => {
- return {
- selectedIdentity: getSelectedIdentity(state),
- }
-}
-
-export default connect(mapStateToProps)(ProviderPageContainerContent)
diff --git a/ui/app/components/app/provider-page-container/provider-page-container-header/index.js b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js
deleted file mode 100644
index 430627d3a..000000000
--- a/ui/app/components/app/provider-page-container/provider-page-container-header/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export {default} from './provider-page-container-header.component'
diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js
deleted file mode 100644
index 7d152e4cb..000000000
--- a/ui/app/components/app/provider-page-container/provider-page-container.component.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import PropTypes from 'prop-types'
-import React, {PureComponent} from 'react'
-import { ProviderPageContainerContent, ProviderPageContainerHeader } from '.'
-import { PageContainerFooter } from '../../ui/page-container'
-import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums'
-import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
-
-export default class ProviderPageContainer extends PureComponent {
- static propTypes = {
- approveProviderRequestByOrigin: PropTypes.func.isRequired,
- rejectProviderRequestByOrigin: PropTypes.func.isRequired,
- origin: PropTypes.string.isRequired,
- siteImage: PropTypes.string,
- siteTitle: PropTypes.string,
- hostname: PropTypes.string,
- extensionId: PropTypes.string,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- };
-
- componentDidMount () {
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
- window.addEventListener('beforeunload', this._beforeUnload)
- }
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Popup Opened',
- },
- })
- }
-
- _beforeUnload = () => {
- const { origin, rejectProviderRequestByOrigin } = this.props
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Cancel Connect Request Via Notification Close',
- },
- })
- this._removeBeforeUnload()
- rejectProviderRequestByOrigin(origin)
- }
-
- _removeBeforeUnload () {
- window.removeEventListener('beforeunload', this._beforeUnload)
- }
-
- componentWillUnmount () {
- this._removeBeforeUnload()
- }
-
- onCancel = () => {
- const { origin, rejectProviderRequestByOrigin } = this.props
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Canceled',
- },
- })
- this._removeBeforeUnload()
- rejectProviderRequestByOrigin(origin)
- }
-
- onSubmit = () => {
- const { approveProviderRequestByOrigin, origin } = this.props
- this.context.metricsEvent({
- eventOpts: {
- category: 'Auth',
- action: 'Connect',
- name: 'Confirmed',
- },
- })
- this._removeBeforeUnload()
- approveProviderRequestByOrigin(origin)
- }
-
- render () {
- const {origin, siteImage, siteTitle, hostname, extensionId} = this.props
-
- return (
-
-
-
-
this.onCancel()}
- cancelText={this.context.t('cancel')}
- onSubmit={() => this.onSubmit()}
- submitText={this.context.t('connect')}
- submitButtonType="confirm"
- />
-
- )
- }
-}
diff --git a/ui/app/components/app/wallet-view/wallet-view.component.js b/ui/app/components/app/wallet-view/wallet-view.component.js
index ceccfea51..dc3abf259 100644
--- a/ui/app/components/app/wallet-view/wallet-view.component.js
+++ b/ui/app/components/app/wallet-view/wallet-view.component.js
@@ -7,7 +7,7 @@ import AccountDetails from '../account-details'
const { checksumAddress } = require('../../../helpers/utils/util')
const TokenList = require('../token-list')
-const { ADD_TOKEN_ROUTE } = require('../../../helpers/constants/routes')
+const { ADD_TOKEN_ROUTE, CONNECTED_ROUTE } = require('../../../helpers/constants/routes')
export default class WalletView extends Component {
static contextTypes = {
@@ -91,6 +91,18 @@ export default class WalletView extends Component {
)
}
+ showConnectedSites = () => {
+ const {
+ sidebarOpen,
+ hideSidebar,
+ history,
+ } = this.props
+ history.push(CONNECTED_ROUTE)
+ if (sidebarOpen) {
+ hideSidebar()
+ }
+ }
+
render () {
const {
responsiveDisplayClassname,
@@ -124,6 +136,7 @@ export default class WalletView extends Component {
label={label}
checksummedAddress={checksummedAddress}
name={identities[selectedAddress].name}
+ showConnectedSites={this.showConnectedSites}
/>
{this.renderWalletBalance()}
diff --git a/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js
new file mode 100644
index 000000000..13b3e93d7
--- /dev/null
+++ b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js
@@ -0,0 +1,42 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+
+export default class IconWithFallback extends PureComponent {
+ static propTypes = {
+ icon: PropTypes.string,
+ name: PropTypes.string,
+ }
+
+ static defaultProps = {
+ name: '',
+ icon: null,
+ }
+
+ state = {
+ iconError: false,
+ }
+
+ render () {
+ const { icon, name } = this.props
+
+ return (
+
+
+ { !this.state.iconError && icon
+ ? (
+
this.setState({ iconError: true })}
+ />
+ )
+ : (
+
+ { name.length ? name.charAt(0).toUpperCase() : '' }
+
+ )
+ }
+
+ )
+ }
+}
diff --git a/ui/app/components/ui/icon-with-fallback/index.js b/ui/app/components/ui/icon-with-fallback/index.js
new file mode 100644
index 000000000..8c1f9a154
--- /dev/null
+++ b/ui/app/components/ui/icon-with-fallback/index.js
@@ -0,0 +1 @@
+export { default } from './icon-with-fallback.component'
diff --git a/ui/app/components/ui/icon-with-fallback/index.scss b/ui/app/components/ui/icon-with-fallback/index.scss
new file mode 100644
index 000000000..02ffe371d
--- /dev/null
+++ b/ui/app/components/ui/icon-with-fallback/index.scss
@@ -0,0 +1,30 @@
+.icon-with-fallback {
+ &__identicon-container {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 32px;
+ width: 32px;
+ }
+
+ &__identicon-border {
+ height: 32px;
+ width: 32px;
+ border-radius: 50%;
+ border: 1px solid #F2F3F4;
+ position: absolute;
+ background: #FFFFFF;
+ }
+
+ &__identicon {
+ width: 24px;
+ height: 24px;
+ z-index: 1;
+
+ &--default {
+ z-index: 1;
+ color: black;
+ }
+ }
+}
diff --git a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js
index da8da45d1..338df83d8 100644
--- a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js
+++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js
@@ -14,6 +14,7 @@ export default class PageContainerFooter extends Component {
disabled: PropTypes.bool,
submitButtonType: PropTypes.string,
hideCancel: PropTypes.bool,
+ buttonSizeLarge: PropTypes.bool,
}
static contextTypes = {
@@ -31,6 +32,7 @@ export default class PageContainerFooter extends Component {
submitButtonType,
hideCancel,
cancelButtonType,
+ buttonSizeLarge = false,
} = this.props
return (
@@ -40,7 +42,7 @@ export default class PageContainerFooter extends Component {
{!hideCancel && (