diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cec4fd6b..af4c886bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## Current Master +- Adds error messages when passwords don't match in onboarding flow. +- Adds modal notification if a retry in the process of being confirmed is dropped. +- New unlock screen design. +- Design improvements to the add token screen. +- Fix inconsistencies in confirm screen between extension and browser window modes. +- Fix scrolling in deposit ether modal. +- Fix styling of app spinner. +- Font weight changed from 300 to 400. +- New reveal screen design. +- Styling improvements to labels in first time flow and signature request headers. + +## 4.6.1 Mon Apr 30 2018 + +- Fix bug where sending a transaction resulted in an infinite spinner +- Allow transactions with a 0 gwei gas price +- Handle encoding errors in ERC20 symbol + digits +- Fix ShapeShift forms (new + old ui) +- Fix sourcemaps + ## 4.6.0 Thu Apr 26 2018 - Correctly format currency conversion for locally selected preferred currency. diff --git a/MISSION.md b/MISSION.md new file mode 100644 index 000000000..9045828b1 --- /dev/null +++ b/MISSION.md @@ -0,0 +1,14 @@ +# MetaMask Philosophy + +## Mission + +Making it safe and easy for the most people to use the decentralized web to the greatest degree that is empowering to them. + +## Vision + +To realize the highest goals achievable for the human race with the twin powers of peer to peer networks and cryptography. To empower users to hold and use their own keys on these new networks as securely and intelligibly as possible, enabling a new world of peer to peer agreements and economies, in hopes that we may collectively overcome the many great problems that we face together, through the power of strong cooperation. + +## Strategy + +We provide software for users to manage accounts, for sites to easily propose actions to users, and for users to coherently review actions before approving them. We build on this rapidly evolving set of protocols with the goal of empowering the most people to the greatest degree, and aspire to continuously evolve our offering to pursue that goal. + diff --git a/README.md b/README.md index ca25fc0b9..970bd758f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # MetaMask Browser Extension [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension) -[Internal documentation](./docs/jsdocs) - ## Support If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/). +## Introduction + +[Mission Statement](./MISSION.md) + +[Internal documentation](./docs/jsdocs) + ## Developing Compatible Dapps If you're a web dapp developer, we've got two types of guides for you: diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index fa01fea24..40d362f51 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -402,6 +402,9 @@ "infoHelp": { "message": "Info & Help" }, + "initialTransactionConfirmed": { + "message": "Your initial transaction was confirmed by the network. Click OK to go back." + }, "insufficientFunds": { "message": "Insufficient funds." }, @@ -520,6 +523,9 @@ "networks": { "message": "Networks" }, + "nevermind": { + "message": "Nevermind" + }, "newAccount": { "message": "New Account" }, @@ -634,9 +640,15 @@ "rejected": { "message": "Rejected" }, + "reset": { + "message": "Reset" + }, "resetAccount": { "message": "Reset Account" }, + "resetAccountDescription": { + "message": "Resetting your account will clear your transaction history." + }, "restoreFromSeed": { "message": "Restore account?" }, @@ -676,6 +688,9 @@ "ropsten": { "message": "Ropsten Test Network" }, + "rpc": { + "message": "Custom RPC" + }, "currentRpc": { "message": "Current RPC" }, @@ -701,10 +716,10 @@ "save": { "message": "Save" }, - "reprice_title": { - "message": "Reprice Transaction" + "speedUpTitle": { + "message": "Speed Up Transaction" }, - "reprice_subtitle": { + "speedUpSubtitle": { "message": "Increase your gas price to attempt to overwrite and speed up your transaction" }, "saveAsCsvFile": { @@ -891,7 +906,7 @@ "message": "Welcome to the New UI (Beta)" }, "uiWelcomeMessage": { - "message": "You are now using the new Metamask UI. Take a look around, try out new features like sending tokens, and let us know if you have any issues." + "message": "You are now using the new Metamask UI." }, "unapproved": { "message": "Unapproved" diff --git a/app/images/check-icon.svg b/app/images/check-icon.svg new file mode 100644 index 000000000..cafa864e5 --- /dev/null +++ b/app/images/check-icon.svg @@ -0,0 +1,17 @@ + + + + 76BCDB09-52B0-41CB-908F-12F9087A2F1B + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/app/manifest.json b/app/manifest.json index 1a4114a1b..027f9a045 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.6.0", + "version": "4.6.1", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -70,7 +70,7 @@ "externally_connectable": { "matches": [ "https://metamask.io/*" - ] - }, - "content_security_policy": "script-src 'self'; object-src 'self'" + ], + "ids": ["*"] + } } diff --git a/app/scripts/background.js b/app/scripts/background.js index 672a2dcac..610409975 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -315,6 +315,7 @@ function setupController (initState, initLangCode) { // connect to other contexts // extension.runtime.onConnect.addListener(connectRemote) + extension.runtime.onConnectExternal.addListener(connectExternal) const metamaskInternalProcessHash = { [ENVIRONMENT_TYPE_POPUP]: true, @@ -341,9 +342,9 @@ function setupController (initState, initLangCode) { function connectRemote (remotePort) { const processName = remotePort.name const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName] - const portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { + const portStream = new PortStream(remotePort) // communication with popup controller.isClientOpen = true controller.setupTrustedCommunication(portStream, 'MetaMask') @@ -376,12 +377,17 @@ function setupController (initState, initLangCode) { }) } } else { - // communication with page - const originDomain = urlUtil.parse(remotePort.sender.url).hostname - controller.setupUntrustedCommunication(portStream, originDomain) + connectExternal(remotePort) } } + // communication with page or other extension + function connectExternal(remotePort) { + const originDomain = urlUtil.parse(remotePort.sender.url).hostname + const portStream = new PortStream(remotePort) + controller.setupUntrustedCommunication(portStream, originDomain) + } + // // User Interface setup // diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 541f1db73..aff5db984 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -8,6 +8,7 @@ const TxGasUtil = require('./tx-gas-utils') const PendingTransactionTracker = require('./pending-tx-tracker') const NonceTracker = require('./nonce-tracker') const txUtils = require('./lib/util') +const cleanErrorStack = require('../../lib/cleanErrorStack') const log = require('loglevel') /** @@ -118,6 +119,7 @@ class TransactionController extends EventEmitter { @param txParams {object} - txParams for the transaction @param opts {object} - with the key origin to put the origin on the txMeta */ + async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const initialTxMeta = await this.addUnapprovedTransaction(txParams) @@ -130,11 +132,11 @@ class TransactionController extends EventEmitter { case 'submitted': return resolve(finishedTxMeta.hash) case 'rejected': - return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) + return reject(cleanErrorStack(new Error('MetaMask Tx Signature: User denied transaction signature.'))) case 'failed': - return reject(new Error(finishedTxMeta.err.message)) + return reject(cleanErrorStack(new Error(finishedTxMeta.err.message))) default: - return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)) + return reject(cleanErrorStack(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))) } }) }) diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 00e837571..0aae4774b 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -2,6 +2,7 @@ const extend = require('xtend') const EventEmitter = require('events') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') +const log = require('loglevel') const txStateHistoryHelper = require('./lib/tx-state-history-helper') const createId = require('../../lib/random-id') const { getFinalStates } = require('./lib/util') @@ -398,13 +399,19 @@ class TransactionStateManager extends EventEmitter { _setTxStatus (txId, status) { const txMeta = this.getTx(txId) txMeta.status = status - this.emit(`${txMeta.id}:${status}`, txId) - this.emit(`tx:status-update`, txId, status) - if (['submitted', 'rejected', 'failed'].includes(status)) { - this.emit(`${txMeta.id}:finished`, txMeta) - } - this.updateTx(txMeta, `txStateManager: setting status to ${status}`) - this.emit('update:badge') + setTimeout(() => { + try { + this.updateTx(txMeta, `txStateManager: setting status to ${status}`) + this.emit(`${txMeta.id}:${status}`, txId) + this.emit(`tx:status-update`, txId, status) + if (['submitted', 'rejected', 'failed'].includes(status)) { + this.emit(`${txMeta.id}:finished`, txMeta) + } + this.emit('update:badge') + } catch (error) { + log.error(error) + } + }) } /** diff --git a/app/scripts/lib/cleanErrorStack.js b/app/scripts/lib/cleanErrorStack.js new file mode 100644 index 000000000..fe1bfb0ce --- /dev/null +++ b/app/scripts/lib/cleanErrorStack.js @@ -0,0 +1,24 @@ +/** + * Returns error without stack trace for better UI display + * @param {Error} err - error + * @returns {Error} Error with clean stack trace. + */ +function cleanErrorStack(err){ + var name = err.name + name = (name === undefined) ? 'Error' : String(name) + + var msg = err.message + msg = (msg === undefined) ? '' : String(msg) + + if (name === '') { + err.stack = err.message + } else if (msg === '') { + err.stack = err.name + } else { + err.stack = err.name + ': ' + err.message + } + + return err +} + +module.exports = cleanErrorStack diff --git a/app/scripts/lib/get-first-preferred-lang-code.js b/app/scripts/lib/get-first-preferred-lang-code.js index 5473fccf0..1e6a83ba6 100644 --- a/app/scripts/lib/get-first-preferred-lang-code.js +++ b/app/scripts/lib/get-first-preferred-lang-code.js @@ -2,6 +2,12 @@ const extension = require('extensionizer') const promisify = require('pify') const allLocales = require('../../_locales/index.json') +const isSupported = extension.i18n && extension.i18n.getAcceptLanguages +const getPreferredLocales = isSupported ? promisify( + extension.i18n.getAcceptLanguages, + { errorFirst: false } +) : async () => [] + const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-')) /** @@ -12,10 +18,7 @@ const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().r * */ async function getFirstPreferredLangCode () { - const userPreferredLocaleCodes = await promisify( - extension.i18n.getAcceptLanguages, - { errorFirst: false } - )() + const userPreferredLocaleCodes = await getPreferredLocales() const firstPreferredLangCode = userPreferredLocaleCodes .map(code => code.toLowerCase()) .find(code => existingLocaleCodes.includes(code)) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1b1d26886..a570f2567 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -45,6 +45,7 @@ const BN = require('ethereumjs-util').BN const GWEI_BN = new BN('1000000000') const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') +const cleanErrorStack = require('./lib/cleanErrorStack') const log = require('loglevel') module.exports = class MetamaskController extends EventEmitter { @@ -350,7 +351,7 @@ module.exports = class MetamaskController extends EventEmitter { verifySeedPhrase: nodeify(this.verifySeedPhrase, this), clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), - importAccountWithStrategy: this.importAccountWithStrategy.bind(this), + importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), // vault management submitPassword: nodeify(keyringController.submitPassword, keyringController), @@ -608,15 +609,15 @@ module.exports = class MetamaskController extends EventEmitter { * @param {any} args - The data required by that strategy to import an account. * @param {Function} cb - A callback function called with a state update on success. */ - importAccountWithStrategy (strategy, args, cb) { - accountImporter.importAccount(strategy, args) - .then((privateKey) => { - return this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ]) - }) - .then(keyring => keyring.getAccounts()) - .then((accounts) => this.preferencesController.setSelectedAddress(accounts[0])) - .then(() => { cb(null, this.keyringController.fullUpdate()) }) - .catch((reason) => { cb(reason) }) + async importAccountWithStrategy (strategy, args) { + const privateKey = await accountImporter.importAccount(strategy, args) + const keyring = await this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ]) + const accounts = await keyring.getAccounts() + // update accounts in preferences controller + const allAccounts = await this.keyringController.getAccounts() + this.preferencesController.setAddresses(allAccounts) + // set new account as selected + await this.preferencesController.setSelectedAddress(accounts[0]) } // --------------------------------------------------------------------------- @@ -642,9 +643,9 @@ module.exports = class MetamaskController extends EventEmitter { case 'signed': return cb(null, data.rawSig) case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) + return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) } }) } @@ -702,7 +703,7 @@ module.exports = class MetamaskController extends EventEmitter { */ newUnsignedPersonalMessage (msgParams, cb) { if (!msgParams.from) { - return cb(new Error('MetaMask Message Signature: from field is required.')) + return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.'))) } const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) @@ -713,9 +714,9 @@ module.exports = class MetamaskController extends EventEmitter { case 'signed': return cb(null, data.rawSig) case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) + return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) } }) } @@ -781,9 +782,9 @@ module.exports = class MetamaskController extends EventEmitter { case 'signed': return cb(null, data.rawSig) case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) + return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.'))) default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))) } }) } diff --git a/mascara/src/app/first-time/create-password-screen.js b/mascara/src/app/first-time/create-password-screen.js index 99d210ed1..6b284f7c5 100644 --- a/mascara/src/app/first-time/create-password-screen.js +++ b/mascara/src/app/first-time/create-password-screen.js @@ -143,6 +143,7 @@ class CreatePasswordScreen extends Component { autoComplete="new-password" margin="normal" fullWidth + largeLabel /> 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('New UI setup', async function () { + it('switches to first tab', async function () { + const [firstTab] = await driver.getAllWindowHandles() + await driver.switchTo().window(firstTab) + await delay(regularDelayMs) + }) + + it('use the local network', async function () { + const [networkSelector] = await driver.findElements(By.css('#network_component')) + await networkSelector.click() + await delay(regularDelayMs) + + const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`)) + await localhost.click() + await delay(regularDelayMs) + }) + + it('selects the new UI option', async () => { + const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]")) + await button.click() + await delay(regularDelayMs) + + // Close all other tabs + const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() + await driver.switchTo().window(oldUi) + await driver.close() + await driver.switchTo().window(infoPage) + await driver.close() + await driver.switchTo().window(newUi) + await delay(regularDelayMs) + + const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button')) + await continueBtn.click() + await delay(regularDelayMs) + }) + }) + + describe('First time flow starting from an existing seed phrase', () => { + it('imports a seed phrase', async () => { + const [seedPhrase] = await driver.findElements(By.xpath(`//a[contains(text(), 'Import with seed phrase')]`)) + await seedPhrase.click() + await delay(regularDelayMs) + + const [seedTextArea] = await driver.findElements(By.css('textarea.import-account__secret-phrase')) + await seedTextArea.sendKeys(testSeedPhrase) + await delay(regularDelayMs) + + const [password] = await driver.findElements(By.id('password')) + await password.sendKeys('correct horse battery staple') + const [confirmPassword] = await driver.findElements(By.id('confirm-password')) + confirmPassword.sendKeys('correct horse battery staple') + + const [importButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Import')]`)) + await importButton.click() + await delay(regularDelayMs) + }) + + it('clicks through the privacy notice', async () => { + const [nextScreen] = await driver.findElements(By.css('.tou button')) + await nextScreen.click() + await delay(regularDelayMs) + + const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled() + assert.equal(canClickThrough, false, 'disabled continue button') + const element = await driver.findElement(By.linkText('Attributions')) + await driver.executeScript('arguments[0].scrollIntoView(true)', element) + await delay(regularDelayMs) + + const [acceptTos] = await driver.findElements(By.css('.tou button')) + await acceptTos.click() + await delay(regularDelayMs) + }) + }) + + describe('Show account information', () => { + it('shows the correct account address', async () => { + await driver.findElement(By.css('.wallet-view__details-button')).click() + await driver.findElement(By.css('.qr-wrapper')).isDisplayed() + await delay(regularDelayMs) + + const [address] = await driver.findElements(By.css('input.qr-ellip-address')) + assert.equal(await address.getAttribute('value'), testAddress) + + await driver.executeScript("document.querySelector('.account-modal-close').click()") + await delay(largeDelayMs) + }) + + it('shows a QR code for the account', async () => { + await driver.findElement(By.css('.wallet-view__details-button')).click() + await driver.findElement(By.css('.qr-wrapper')).isDisplayed() + await delay(regularDelayMs) + + await driver.executeScript("document.querySelector('.account-modal-close').click()") + await delay(regularDelayMs) + }) + }) + + describe('Log out and log back in', () => { + it('logs out of the account', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) + assert.equal(await logoutButton.getText(), 'Log out') + await logoutButton.click() + await delay(regularDelayMs) + }) + + it('accepts the account password after lock', async () => { + await driver.findElement(By.id('password')).sendKeys('correct horse battery staple') + await driver.findElement(By.id('password')).sendKeys(Key.ENTER) + await delay(largeDelayMs) + }) + }) + + describe('Add an account', () => { + it('choose Create Account from the account menu', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`)) + await createAccount.click() + await delay(regularDelayMs) + }) + + it('set account name', async () => { + const [accountName] = await driver.findElements(By.css('.new-account-create-form input')) + await accountName.sendKeys('2nd account') + await delay(regularDelayMs) + + const [createButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`)) + await createButton.click() + await delay(regularDelayMs) + }) + + it('should show the correct account name', async () => { + const [accountName] = await driver.findElements(By.css('.account-name')) + assert.equal(await accountName.getText(), '2nd account') + await delay(regularDelayMs) + }) + }) + + describe('Switch back to original account', () => { + it('chooses the original account from the account menu', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [originalAccountMenuItem] = await driver.findElements(By.css('.account-menu__name')) + await originalAccountMenuItem.click() + await delay(regularDelayMs) + }) + }) + + describe('Send ETH from inside MetaMask', () => { + it('starts to send a transaction', async function () { + const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`)) + await sendButton.click() + await delay(regularDelayMs) + + const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]')) + const [inputAmount] = await driver.findElements(By.css('.currency-display__input')) + await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') + await inputAmount.sendKeys('1') + + // Set the gas limit + const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button')) + await configureGas.click() + await delay(regularDelayMs) + + const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`)) + await save.click() + await delay(regularDelayMs) + + // Continue to next screen + const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + }) + + it('confirms the transaction', async function () { + const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + const transactions = await driver.findElements(By.css('.tx-list-item')) + assert.equal(transactions.length, 1) + + const txValues = await driver.findElements(By.css('.tx-list-value')) + assert.equal(txValues.length, 1) + assert.equal(await txValues[0].getText(), '1 ETH') + }) + }) + + describe('Send ETH from Faucet', () => { + it('starts a send transaction inside Faucet', async () => { + await driver.executeScript('window.open("https://faucet.metamask.io")') + await delay(waitingNewPageDelayMs) + + const [extension, faucet] = await driver.getAllWindowHandles() + await driver.switchTo().window(faucet) + await delay(regularDelayMs) + + const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`)) + await send1eth.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + + const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(faucet) + await delay(regularDelayMs) + await driver.close() + await delay(regularDelayMs) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + }) + }) + + describe('Add existing token using search', () => { + it('clicks on the Add Token button', async () => { + const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) + await addToken.click() + await delay(regularDelayMs) + }) + + it('picks an existing token', async () => { + const [tokenSearch] = await driver.findElements(By.css('input.add-token__input')) + await tokenSearch.sendKeys('BAT') + await delay(regularDelayMs) + + const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]")) + await token.click() + await delay(regularDelayMs) + + const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + + const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) + await addTokens.click() + await delay(largeDelayMs) + }) + + it('renders the balance for the new token', async () => { + const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount')) + const tokenAmount = await balance.getText() + assert.equal(tokenAmount, '0BAT') + await delay(regularDelayMs) + }) + }) + + describe('Add a custom token from TokenFactory', () => { + it('creates a new token', async () => { + await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') + await delay(waitingNewPageDelayMs) + + const [extension, tokenFactory] = await driver.getAllWindowHandles() + await driver.switchTo().window(tokenFactory) + const [ + totalSupply, + tokenName, + tokenDecimal, + tokenSymbol, + ] = await driver.findElements(By.css('input')) + + await totalSupply.sendKeys('100') + await tokenName.sendKeys('Test') + await tokenDecimal.sendKeys('0') + await tokenSymbol.sendKeys('TST') + + const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`)) + await createToken.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + + const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(tokenFactory) + await delay(regularDelayMs) + const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) + tokenAddress = await tokenContactAddress.getText() + await driver.close() + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + }) + + it('clicks on the Add Token button', async () => { + const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) + await addToken.click() + await delay(regularDelayMs) + }) + + it('picks the new Test token', async () => { + const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]")) + await addCustomToken.click() + await delay(regularDelayMs) + + const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input')) + await newTokenAddress.sendKeys(tokenAddress) + await delay(regularDelayMs) + + const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + + const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) + await addTokens.click() + await delay(regularDelayMs) + }) + + it('renders the balance for the new token', async () => { + const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount')) + const tokenAmount = await balance.getText() + assert.equal(tokenAmount, '100TST') + await delay(regularDelayMs) + }) + }) +}) diff --git a/test/e2e/beta/helpers.js b/test/e2e/beta/helpers.js new file mode 100644 index 000000000..8307fdc50 --- /dev/null +++ b/test/e2e/beta/helpers.js @@ -0,0 +1,55 @@ +const fs = require('fs') +const mkdirp = require('mkdirp') +const pify = require('pify') + +module.exports = { + checkBrowserForConsoleErrors, + loadExtension, + verboseReportOnFailure, +} + +async function loadExtension (driver, extensionId) { + switch (process.env.SELENIUM_BROWSER) { + case 'chrome': { + await driver.get(`chrome-extension://${extensionId}/home.html`) + break + } + case 'firefox': { + await driver.get(`moz-extension://${extensionId}/home.html`) + break + } + } +} + +async function checkBrowserForConsoleErrors (driver) { + const ignoredLogTypes = ['WARNING'] + const ignoredErrorMessages = [ + // React throws error warnings on "dataset", but still sets the data-* properties correctly + 'Warning: Unknown prop `dataset` on ', + // Third-party Favicon 404s show up as errors + 'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)', + // React Development build - known issue blocked by test build sys + 'Warning: It looks like you\'re using a minified copy of the development build of React.', + // Redux Development build - known issue blocked by test build sys + 'This means that you are running a slower development build of Redux.', + ] + const browserLogs = await driver.manage().logs().get('browser') + const errorEntries = browserLogs.filter(entry => !ignoredLogTypes.includes(entry.level.toString())) + const errorObjects = errorEntries.map(entry => entry.toJSON()) + return errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message))) +} + +async function verboseReportOnFailure (driver, test) { + let artifactDir + if (process.env.SELENIUM_BROWSER === 'chrome') { + artifactDir = `./test-artifacts/chrome/${test.title}` + } else if (process.env.SELENIUM_BROWSER === 'firefox') { + artifactDir = `./test-artifacts/firefox/${test.title}` + } + const filepathBase = `${artifactDir}/test-failure` + await pify(mkdirp)(artifactDir) + const screenshot = await driver.takeScreenshot() + await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' }) + const htmlSource = await driver.getPageSource() + await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource) +} diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js new file mode 100644 index 000000000..00863e3b3 --- /dev/null +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -0,0 +1,491 @@ +const path = require('path') +const assert = require('assert') +const webdriver = require('selenium-webdriver') +const { By, Key } = webdriver +const { + delay, + buildChromeWebDriver, + buildFirefoxWebdriver, + installWebExt, + getExtensionIdChrome, + getExtensionIdFirefox, +} = require('../func') +const { + checkBrowserForConsoleErrors, + loadExtension, + verboseReportOnFailure, +} = require('./helpers') + +describe('MetaMask', function () { + let extensionId + let driver + let tokenAddress + + const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' + const tinyDelayMs = 500 + const regularDelayMs = tinyDelayMs * 2 + const largeDelayMs = regularDelayMs * 2 + const waitingNewPageDelayMs = regularDelayMs * 10 + + this.timeout(0) + this.bail(true) + + before(async function () { + switch (process.env.SELENIUM_BROWSER) { + case 'chrome': { + const extPath = path.resolve('dist/chrome') + driver = buildChromeWebDriver(extPath) + extensionId = await getExtensionIdChrome(driver) + await driver.get(`chrome-extension://${extensionId}/popup.html`) + break + } + case 'firefox': { + const extPath = path.resolve('dist/firefox') + driver = buildFirefoxWebdriver() + await installWebExt(driver, extPath) + await delay(700) + extensionId = await getExtensionIdFirefox(driver) + await driver.get(`moz-extension://${extensionId}/popup.html`) + } + } + }) + + 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(this.currentTest) + } + }) + + after(async function () { + await driver.quit() + }) + + describe('New UI setup', async function () { + it('switches to first tab', async function () { + const [firstTab] = await driver.getAllWindowHandles() + await driver.switchTo().window(firstTab) + await delay(regularDelayMs) + }) + + it('use the local network', async function () { + const [networkSelector] = await driver.findElements(By.css('#network_component')) + await networkSelector.click() + await delay(regularDelayMs) + + const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`)) + await localhost.click() + await delay(regularDelayMs) + }) + + it('selects the new UI option', async () => { + const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]")) + await button.click() + await delay(regularDelayMs) + + // Close all other tabs + const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() + await driver.switchTo().window(oldUi) + await driver.close() + await driver.switchTo().window(infoPage) + await driver.close() + await driver.switchTo().window(newUi) + await delay(regularDelayMs) + + const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button')) + await continueBtn.click() + await delay(regularDelayMs) + }) + }) + + describe('Going through the first time flow', () => { + it('accepts a secure password', async () => { + const [passwordBox] = await driver.findElements(By.css('.create-password #create-password')) + const [passwordBoxConfirm] = await driver.findElements(By.css('.create-password #confirm-password')) + const [button] = await driver.findElements(By.css('.create-password button')) + + await passwordBox.sendKeys('correct horse battery staple') + await passwordBoxConfirm.sendKeys('correct horse battery staple') + await button.click() + await delay(regularDelayMs) + }) + + it('clicks through the unique image screen', async () => { + const [nextScreen] = await driver.findElements(By.css('.unique-image button')) + await nextScreen.click() + await delay(regularDelayMs) + }) + + it('clicks through the privacy notice', async () => { + const [nextScreen] = await driver.findElements(By.css('.tou button')) + await nextScreen.click() + await delay(regularDelayMs) + + const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled() + assert.equal(canClickThrough, false, 'disabled continue button') + const [bottomOfTos] = await driver.findElements(By.linkText('Attributions')) + await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos) + await delay(regularDelayMs) + + const [acceptTos] = await driver.findElements(By.css('.tou button')) + await acceptTos.click() + await delay(regularDelayMs) + }) + + let seedPhrase + + it('reveals the seed phrase', async () => { + const [revealSeedPhrase] = await driver.findElements(By.css('.backup-phrase__secret-blocker')) + await revealSeedPhrase.click() + await delay(regularDelayMs) + + seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText() + assert.equal(seedPhrase.split(' ').length, 12) + await delay(regularDelayMs) + + const [nextScreen] = await driver.findElements(By.css('.backup-phrase button')) + await nextScreen.click() + await delay(regularDelayMs) + }) + + it('can retype the seed phrase', async () => { + const words = seedPhrase.split(' ') + + const [word0] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[0]}')]`)) + await word0.click() + await delay(tinyDelayMs) + + const [word1] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[1]}')]`)) + await word1.click() + await delay(tinyDelayMs) + + const [word2] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[2]}')]`)) + await word2.click() + await delay(tinyDelayMs) + + const [word3] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[3]}')]`)) + await word3.click() + await delay(tinyDelayMs) + + const [word4] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[4]}')]`)) + await word4.click() + await delay(tinyDelayMs) + + const [word5] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[5]}')]`)) + await word5.click() + await delay(tinyDelayMs) + + const [word6] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[6]}')]`)) + await word6.click() + await delay(tinyDelayMs) + + const [word7] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[7]}')]`)) + await word7.click() + await delay(tinyDelayMs) + + const [word8] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[8]}')]`)) + await word8.click() + await delay(tinyDelayMs) + + const [word9] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[9]}')]`)) + await word9.click() + await delay(tinyDelayMs) + + const [word10] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[10]}')]`)) + await word10.click() + await delay(tinyDelayMs) + + const [word11] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[11]}')]`)) + await word11.click() + await delay(tinyDelayMs) + + const [confirm] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirm.click() + await delay(regularDelayMs) + }) + + it('clicks through the deposit modal', async () => { + const [closeModal] = await driver.findElements(By.css('.page-container__header-close')) + await closeModal.click() + await delay(regularDelayMs) + }) + }) + + describe('Show account information', () => { + it('shows the QR code for the account', async () => { + await driver.findElement(By.css('.wallet-view__details-button')).click() + await driver.findElement(By.css('.qr-wrapper')).isDisplayed() + await delay(regularDelayMs) + + await driver.executeScript("document.querySelector('.account-modal-close').click()") + await delay(regularDelayMs * 4) + }) + }) + + describe('Log out an log back in', () => { + it('logs out of the account', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) + assert.equal(await logoutButton.getText(), 'Log out') + await logoutButton.click() + await delay(regularDelayMs) + }) + + it('accepts the account password after lock', async () => { + await driver.findElement(By.id('password')).sendKeys('correct horse battery staple') + await driver.findElement(By.id('password')).sendKeys(Key.ENTER) + await delay(regularDelayMs * 4) + }) + }) + + describe('Add account', () => { + it('choose Create Account from the account menu', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`)) + await createAccount.click() + await delay(regularDelayMs) + }) + + it('set account name', async () => { + const [accountName] = await driver.findElements(By.css('.new-account-create-form input')) + await accountName.sendKeys('2nd account') + await delay(regularDelayMs) + + const [create] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`)) + await create.click() + await delay(regularDelayMs) + }) + + it('should correct account name', async () => { + const [accountName] = await driver.findElements(By.css('.account-name')) + assert.equal(await accountName.getText(), '2nd account') + await delay(regularDelayMs) + }) + }) + + describe('Import seed phrase', () => { + it('logs out of the vault', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) + assert.equal(await logoutButton.getText(), 'Log out') + await logoutButton.click() + await delay(regularDelayMs) + }) + + it('imports seed phrase', async () => { + const [restoreSeedLink] = await driver.findElements(By.css('.unlock-page__link--import')) + assert.equal(await restoreSeedLink.getText(), 'Import using account seed phrase') + await restoreSeedLink.click() + await delay(regularDelayMs) + + const [seedTextArea] = await driver.findElements(By.css('textarea')) + await seedTextArea.sendKeys(testSeedPhrase) + await delay(regularDelayMs) + + await driver.findElement(By.id('password-box')).sendKeys('correct horse battery staple') + await driver.findElement(By.id('password-box-confirm')).sendKeys('correct horse battery staple') + await driver.findElement(By.css('button:nth-child(2)')).click() + await delay(regularDelayMs) + }) + + it('balance renders', async () => { + const balance = await driver.findElement(By.css('.balance-display .token-amount')) + const tokenAmount = await balance.getText() + assert.equal(tokenAmount, '100.000 ETH') + await delay(regularDelayMs) + }) + }) + + describe('Send ETH from inside MetaMask', () => { + it('starts to send a transaction', async function () { + const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`)) + await sendButton.click() + await delay(regularDelayMs) + + const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]')) + const [inputAmount] = await driver.findElements(By.css('.currency-display__input')) + await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') + await inputAmount.sendKeys('1') + + // Set the gas limit + const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button')) + await configureGas.click() + await delay(regularDelayMs) + + const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`)) + await save.click() + await delay(regularDelayMs) + + // Continue to next screen + const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + }) + + it('confirms the transaction', async function () { + const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + const transactions = await driver.findElements(By.css('.tx-list-item')) + assert.equal(transactions.length, 1) + + const txValues = await driver.findElements(By.css('.tx-list-value')) + assert.equal(txValues.length, 1) + assert.equal(await txValues[0].getText(), '1 ETH') + }) + }) + + describe('Send ETH from Faucet', () => { + it('starts a send transaction inside Faucet', async () => { + await driver.executeScript('window.open("https://faucet.metamask.io")') + await delay(waitingNewPageDelayMs) + + const [extension, faucet] = await driver.getAllWindowHandles() + await driver.switchTo().window(faucet) + await delay(regularDelayMs) + + const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`)) + await send1eth.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + + const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(faucet) + await delay(regularDelayMs) + await driver.close() + await delay(regularDelayMs) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + }) + }) + + describe('Add existing token using search', () => { + it('clicks on the Add Token button', async () => { + const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) + await addToken.click() + await delay(regularDelayMs) + }) + + it('can pick a token from the existing options', async () => { + const [tokenSearch] = await driver.findElements(By.css('input.add-token__input')) + await tokenSearch.sendKeys('BAT') + await delay(regularDelayMs) + + const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]")) + await token.click() + await delay(regularDelayMs) + + const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + + const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) + await addTokens.click() + await delay(largeDelayMs) + }) + + it('renders the balance for the chosen token', async () => { + const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount')) + const tokenAmount = await balance.getText() + assert.equal(tokenAmount, '0BAT') + await delay(regularDelayMs) + }) + }) + + describe('Add a custom token from TokenFactory', () => { + it('creates a new token', async () => { + await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') + await delay(waitingNewPageDelayMs) + + const [extension, tokenFactory] = await driver.getAllWindowHandles() + await driver.switchTo().window(tokenFactory) + const [ + totalSupply, + tokenName, + tokenDecimal, + tokenSymbol, + ] = await driver.findElements(By.css('input')) + + await totalSupply.sendKeys('100') + await tokenName.sendKeys('Test') + await tokenDecimal.sendKeys('0') + await tokenSymbol.sendKeys('TST') + + const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`)) + await createToken.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + + const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(tokenFactory) + await delay(regularDelayMs) + const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) + tokenAddress = await tokenContactAddress.getText() + await driver.close() + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + }) + + it('clicks on the Add Token button', async () => { + const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) + await addToken.click() + await delay(regularDelayMs) + }) + + it('picks the newly created Test token', async () => { + const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]")) + await addCustomToken.click() + await delay(regularDelayMs) + + const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input')) + await newTokenAddress.sendKeys(tokenAddress) + await delay(regularDelayMs) + + const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + + const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) + await addTokens.click() + await delay(regularDelayMs) + }) + + it('renders the balance for the new token', async () => { + const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount')) + const tokenAmount = await balance.getText() + assert.equal(tokenAmount, '100TST') + await delay(regularDelayMs) + }) + }) +}) diff --git a/test/e2e/beta/run-all.sh b/test/e2e/beta/run-all.sh new file mode 100755 index 000000000..5916d5614 --- /dev/null +++ b/test/e2e/beta/run-all.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +export PATH="$PATH:./node_modules/.bin" + +shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec' +shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec' diff --git a/test/e2e/func.js b/test/e2e/func.js index 8b221ce47..9f06e7f37 100644 --- a/test/e2e/func.js +++ b/test/e2e/func.js @@ -1,5 +1,7 @@ require('chromedriver') require('geckodriver') +const fs = require('fs') +const os = require('os') const path = require('path') const webdriver = require('selenium-webdriver') const Command = require('selenium-webdriver/lib/command').Command @@ -19,10 +21,15 @@ function delay (time) { } function buildChromeWebDriver (extPath) { + const tmpProfile = path.join(os.tmpdir(), fs.mkdtempSync('mm-chrome-profile')); return new webdriver.Builder() .withCapabilities({ chromeOptions: { - args: [`load-extension=${extPath}`], + args: [ + `load-extension=${extPath}`, + `user-data-dir=${tmpProfile}`, + ], + binary: process.env.SELENIUM_CHROME_BINARY, }, }) .build() @@ -53,4 +60,4 @@ async function installWebExt (driver, extension) { .defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install') return await driver.schedule(cmd, 'installWebExt(' + extension + ')') -} \ No newline at end of file +} diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js index 8ec7de16c..a08a34d96 100644 --- a/test/e2e/metamask.spec.js +++ b/test/e2e/metamask.spec.js @@ -121,7 +121,14 @@ describe('Metamask popup page', function () { await delay(300) }) + it('adds a second account', async function () { + await driver.findElement(By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div')).click() + await delay(300) + await driver.findElement(By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span')).click() + }) + it('shows account address', async function () { + await delay(300) accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText() }) diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js index e51c854d2..5a08c90cd 100644 --- a/test/integration/lib/add-token.js +++ b/test/integration/lib/add-token.js @@ -43,7 +43,7 @@ async function runAddTokenFlowTest (assert, done) { assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') // Cancel Add Token - const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.page-container__footer-button') + const cancelAddTokenButton = await queryAsync($, 'button.btn-default.btn--large.page-container__footer-button') assert.ok(cancelAddTokenButton[0], 'cancel add token button present') cancelAddTokenButton.click() @@ -75,15 +75,15 @@ async function runAddTokenFlowTest (assert, done) { tokenWrapper[0].click() // Click Next button - let nextButton = await queryAsync($, 'button.btn-primary--lg') + let nextButton = await queryAsync($, 'button.btn-primary.btn--large') assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') nextButton[0].click() // Confirm Add token const confirmAddToken = await queryAsync($, '.confirm-add-token') assert.ok(confirmAddToken[0], 'confirm add token rendered') - assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found') - $('button.btn-primary--lg')[0].click() + assert.ok($('button.btn-primary.btn--large')[0], 'confirm add token button found') + $('button.btn-primary.btn--large')[0].click() // Verify added token image let heroBalance = await queryAsync($, '.hero-balance') @@ -120,7 +120,7 @@ async function runAddTokenFlowTest (assert, done) { const errorMessage = await queryAsync($, '#custom-symbol-helper-text') assert.ok(errorMessage[0], 'error rendered') - $('button.btn-secondary--lg')[0].click() + $('button.btn-default.btn--large')[0].click() // await timeout(100000) diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js index 3936ac5fa..d5ed7c77c 100644 --- a/test/integration/lib/confirm-sig-requests.js +++ b/test/integration/lib/confirm-sig-requests.js @@ -38,7 +38,7 @@ async function runConfirmSigRequestsTest(assert, done) { let confirmSigRowValue = await queryAsync($, '.request-signature__row-value') assert.equal(confirmSigRowValue[0].textContent, '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0') - let confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') + let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() confirmSigHeadline = await queryAsync($, '.request-signature__headline') @@ -47,7 +47,7 @@ async function runConfirmSigRequestsTest(assert, done) { confirmSigRowValue = await queryAsync($, '.request-signature__row-value') assert.ok(confirmSigRowValue[0].textContent.match(/^\#\sTerms\sof\sUse/)) - confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') + confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() confirmSigHeadline = await queryAsync($, '.request-signature__headline') @@ -57,7 +57,7 @@ async function runConfirmSigRequestsTest(assert, done) { assert.equal(confirmSigRowValue[0].textContent, 'Hi, Alice!') assert.equal(confirmSigRowValue[1].textContent, '1337') - confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') + confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() const txView = await queryAsync($, '.tx-view') diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 3da3f4f95..176907926 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -129,7 +129,7 @@ async function runSendFlowTest(assert, done) { await customizeGas(assert, 0, 21000, '0', '$0.00 USD') await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD') - const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button') + const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') sendButton[0].click() await timeout() @@ -169,13 +169,13 @@ async function runSendFlowTest(assert, done) { sendAmountFieldInputInEdit.val('1.0') reactTriggerChange(sendAmountFieldInputInEdit[0]) - const sendButtonInEdit = await queryAsync($, '.btn-primary--lg.page-container__footer-button') + const sendButtonInEdit = await queryAsync($, '.btn-primary.btn--large.page-container__footer-button') assert.equal(sendButtonInEdit[0].textContent, 'Next', 'next button in edit rendered') selectState.val('send new ui') reactTriggerChange(selectState[0]) - const cancelButtonInEdit = await queryAsync($, '.btn-secondary--lg.page-container__footer-button') + const cancelButtonInEdit = await queryAsync($, '.btn-default.btn--large.page-container__footer-button') cancelButtonInEdit[0].click() // sendButtonInEdit[0].click() diff --git a/test/screens/new-ui.js b/test/screens/new-ui.js index 6b873ac85..e3ba7f6ab 100644 --- a/test/screens/new-ui.js +++ b/test/screens/new-ui.js @@ -11,9 +11,8 @@ const GIFEncoder = require('gifencoder') const pngFileStream = require('png-file-stream') const sizeOfPng = require('image-size/lib/types/png') const By = webdriver.By -const { delay, buildWebDriver } = require('./func') const localesIndex = require('../../app/_locales/index.json') -// const localesIndex = [] +const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('../e2e/func') const eth = new Ethjs(new Ethjs.HttpProvider('http://localhost:8545')) @@ -50,11 +49,10 @@ async function captureAllScreens() { await cleanScreenShotDir() - // setup selenium and install extension const extPath = path.resolve('dist/chrome') - driver = buildWebDriver(extPath) - await driver.get('chrome://extensions-frame') - const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("#container > div.items-container > extensions-item:nth-child(2)").getAttribute("id")') + driver = buildChromeWebDriver(extPath) + const extensionId = await getExtensionIdChrome(driver) + await driver.get(`chrome-extension://${extensionId}/home.html`) await delay(500) tabs = await driver.getAllWindowHandles() @@ -165,7 +163,7 @@ async function captureAllScreens() { await delay(300) await captureLanguageScreenShots('metamask account detail export private key screen - password entered') - await driver.findElement(By.css('.btn-primary--lg.export-private-key__button')).click() + await driver.findElement(By.css('.btn-primary.btn--large.export-private-key__button')).click() await delay(300) await captureLanguageScreenShots('metamask account detail export private key screen - reveal key') diff --git a/ui/app/actions.js b/ui/app/actions.js index 894e31fde..a9372d6f3 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -503,17 +503,23 @@ function requestRevealSeedWords (password) { } function resetAccount () { - return (dispatch) => { - background.resetAccount((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.displayWarning(err.message)) - } + return dispatch => { + dispatch(actions.showLoadingIndication()) - log.info('Transaction history reset for ' + account) - dispatch(actions.showAccountsPage()) - }) - } + return new Promise((resolve, reject) => { + background.resetAccount((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + log.info('Transaction history reset for ' + account) + dispatch(actions.showAccountsPage()) + resolve(account) + }) + }) + } } function addNewKeyring (type, opts) { @@ -1397,16 +1403,24 @@ function markAccountsFound () { function retryTransaction (txId) { log.debug(`background.retryTransaction`) + let newTxId + return (dispatch) => { - background.retryTransaction(txId, (err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - const { selectedAddressTxList } = newState - const { id: newTxId } = selectedAddressTxList[selectedAddressTxList.length - 1] - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.viewPendingTx(newTxId)) + return new Promise((resolve, reject) => { + background.retryTransaction(txId, (err, newState) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } + + const { selectedAddressTxList } = newState + const { id } = selectedAddressTxList[selectedAddressTxList.length - 1] + newTxId = id + resolve(newState) + }) }) + .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then(() => newTxId) } } diff --git a/ui/app/app.js b/ui/app/app.js index 3d2961340..0e8b907df 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -76,7 +76,7 @@ class App extends Component { h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), - h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }), + h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), @@ -137,7 +137,6 @@ class App extends Component { (isLoading || isLoadingNetwork) && h(Loading, { loadingMessage: loadMessage, - fullScreen: true, }), // content diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js index fe3bf363c..e8e798445 100644 --- a/ui/app/components/button/button.component.js +++ b/ui/app/components/button/button.component.js @@ -2,20 +2,15 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -const SECONDARY = 'secondary' +const CLASSNAME_DEFAULT = 'btn-default' const CLASSNAME_PRIMARY = 'btn-primary' -const CLASSNAME_PRIMARY_LARGE = 'btn-primary--lg' const CLASSNAME_SECONDARY = 'btn-secondary' -const CLASSNAME_SECONDARY_LARGE = 'btn-secondary--lg' +const CLASSNAME_LARGE = 'btn--large' -const getClassName = (type, large = false) => { - let output = type === SECONDARY ? CLASSNAME_SECONDARY : CLASSNAME_PRIMARY - - if (large) { - output += ` ${type === SECONDARY ? CLASSNAME_SECONDARY_LARGE : CLASSNAME_PRIMARY_LARGE}` - } - - return output +const typeHash = { + default: CLASSNAME_DEFAULT, + primary: CLASSNAME_PRIMARY, + secondary: CLASSNAME_SECONDARY, } class Button extends Component { @@ -24,7 +19,11 @@ class Button extends Component { return ( { this.props.children } diff --git a/ui/app/components/button/button.stories.js b/ui/app/components/button/button.stories.js index d1e14e869..dec084a25 100644 --- a/ui/app/components/button/button.stories.js +++ b/ui/app/components/button/button.stories.js @@ -13,13 +13,21 @@ storiesOf('Button', module) {text('text', 'Click me')} ) - .add('secondary', () => ( + .add('secondary', () => {text('text', 'Click me')} + ) + .add('default', () => ( + + {text('text', 'Click me')} + )) .add('large primary', () => ( )) + .add('large default', () => ( + + {text('text', 'Click me')} + + )) diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 1ff8eea87..e3529041b 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -308,7 +308,7 @@ CustomizeGasModal.prototype.render = function () { }, [this.context.t('revert')]), h('div.send-v2__customize-gas__buttons', [ - h('button.btn-secondary.send-v2__customize-gas__cancel', { + h('button.btn-default.send-v2__customize-gas__cancel', { onClick: this.props.hideModal, style: { marginRight: '10px', diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index f3fe823f8..e69acff63 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -3,3 +3,5 @@ @import './info-box/index'; @import './pages/index'; + +@import './modals/index'; diff --git a/ui/app/components/loading-screen/loading-screen.component.js b/ui/app/components/loading-screen/loading-screen.component.js index bce2a4aac..6b843cfee 100644 --- a/ui/app/components/loading-screen/loading-screen.component.js +++ b/ui/app/components/loading-screen/loading-screen.component.js @@ -1,7 +1,6 @@ const { Component } = require('react') const h = require('react-hyperscript') const PropTypes = require('prop-types') -const classnames = require('classnames') const Spinner = require('../spinner') class LoadingScreen extends Component { @@ -12,9 +11,7 @@ class LoadingScreen extends Component { render () { return ( - h('.loading-overlay', { - className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }), - }, [ + h('.loading-overlay', [ h('.loading-overlay__container', [ h(Spinner, { color: '#F7C06C', @@ -29,7 +26,6 @@ class LoadingScreen extends Component { LoadingScreen.propTypes = { loadingMessage: PropTypes.string, - fullScreen: PropTypes.bool, } module.exports = LoadingScreen diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js new file mode 100644 index 000000000..14a4da62a --- /dev/null +++ b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../button' + +class ConfirmResetAccount extends Component { + static propTypes = { + hideModal: PropTypes.func.isRequired, + resetAccount: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleReset () { + this.props.resetAccount() + .then(() => this.props.hideModal()) + } + + render () { + const { t } = this.context + + return ( + + + + { `${t('resetAccount')}?` } + + + { t('resetAccountDescription') } + + + + this.props.hideModal()} + > + { t('nevermind') } + + this.handleReset()} + > + { t('reset') } + + + + ) + } +} + +export default ConfirmResetAccount diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js new file mode 100644 index 000000000..9630a5593 --- /dev/null +++ b/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import ConfirmResetAccount from './confirm-reset-account.component' + +const { hideModal, resetAccount } = require('../../../actions') + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + resetAccount: () => dispatch(resetAccount()), + } +} + +export default connect(null, mapDispatchToProps)(ConfirmResetAccount) diff --git a/ui/app/components/modals/confirm-reset-account/index.js b/ui/app/components/modals/confirm-reset-account/index.js new file mode 100644 index 000000000..c812ffc55 --- /dev/null +++ b/ui/app/components/modals/confirm-reset-account/index.js @@ -0,0 +1,2 @@ +import ConfirmResetAccount from './confirm-reset-account.container' +module.exports = ConfirmResetAccount diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/modals/deposit-ether-modal.js index ad5f9b695..2daa7fa1d 100644 --- a/ui/app/components/modals/deposit-ether-modal.js +++ b/ui/app/components/modals/deposit-ether-modal.js @@ -109,7 +109,7 @@ DepositEtherModal.prototype.renderRow = function ({ ]), !hideButton && h('div.deposit-ether-modal__buy-row__button', [ - h('button.btn-primary--lg.deposit-ether-modal__deposit-button', { + h('button.btn-primary.btn--large.deposit-ether-modal__deposit-button', { onClick: onButtonClick, }, [buttonLabel]), ]), diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index 447e43b7a..80ece425f 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -87,14 +87,14 @@ ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, lab ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) { return h('div.export-private-key-buttons', {}, [ !privateKey && this.renderButton( - 'btn-secondary--lg export-private-key__button export-private-key__button--cancel', + 'btn-default btn--large export-private-key__button export-private-key__button--cancel', () => hideModal(), 'Cancel' ), (privateKey - ? this.renderButton('btn-primary--lg export-private-key__button', () => hideModal(), this.context.t('done')) - : this.renderButton('btn-primary--lg export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm')) + ? this.renderButton('btn-primary btn--large export-private-key__button', () => hideModal(), this.context.t('done')) + : this.renderButton('btn-primary btn--large export-private-key__button', () => this.exportAccountAndGetPrivateKey(this.state.password, address), this.context.t('confirm')) ), ]) diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss new file mode 100644 index 000000000..ad6fe16d3 --- /dev/null +++ b/ui/app/components/modals/index.scss @@ -0,0 +1,52 @@ +.modal-container { + width: 100%; + height: 100%; + background-color: #fff; + display: flex; + flex-flow: column; + border-radius: 8px; + + &__title { + font-size: 1.5rem; + font-weight: 500; + padding: 16px 0; + text-align: center; + } + + &__description { + text-align: center; + font-size: .875rem; + } + + &__content { + overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 32px; + + @media screen and (max-width: 575px) { + justify-content: center; + padding: 28px 20px; + } + } + + &__footer { + display: flex; + flex-flow: row; + justify-content: center; + border-top: 1px solid #d2d8dd; + padding: 16px; + flex: 0 0 auto; + + &-button { + min-width: 0; + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + } + } +} diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 43dcd20ae..85e85597a 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -19,7 +19,30 @@ const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') -const ConfirmResetAccount = require('./notification-modals/confirm-reset-account') +const ConfirmResetAccount = require('./confirm-reset-account') +const TransactionConfirmed = require('./transaction-confirmed') +const WelcomeBeta = require('./welcome-beta') +const Notification = require('./notification') + +const modalContainerBaseStyle = { + transform: 'translate3d(-50%, 0, 0px)', + border: '1px solid #CCCFD1', + borderRadius: '8px', + backgroundColor: '#FFFFFF', + boxShadow: '0 2px 22px 0 rgba(0,0,0,0.2)', +} + +const modalContainerLaptopStyle = { + ...modalContainerBaseStyle, + width: '344px', + top: '15%', +} + +const modalContainerMobileStyle = { + ...modalContainerBaseStyle, + width: '309px', + top: '12.5%', +} const accountModalStyle = { mobileModalStyle: { @@ -173,18 +196,18 @@ const MODALS = { BETA_UI_NOTIFICATION_MODAL: { contents: [ - h(NotifcationModal, { - header: 'uiWelcome', - message: 'uiWelcomeMessage', - }), + h(Notification, [ + h(WelcomeBeta), + ]), ], mobileModalStyle: { - width: '95%', - top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', + ...modalContainerMobileStyle, }, laptopModalStyle: { - width: '449px', - top: 'calc(33% + 45px)', + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', }, }, @@ -208,12 +231,13 @@ const MODALS = { CONFIRM_RESET_ACCOUNT: { contents: h(ConfirmResetAccount), mobileModalStyle: { - width: '95%', - top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', + ...modalContainerMobileStyle, }, laptopModalStyle: { - width: '473px', - top: 'calc(33% + 45px)', + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', }, }, @@ -265,6 +289,24 @@ const MODALS = { }, }, + TRANSACTION_CONFIRMED: { + disableBackdropClick: true, + contents: [ + h(Notification, [ + h(TransactionConfirmed), + ]), + ], + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + DEFAULT: { contents: [], mobileModalStyle: {}, @@ -306,7 +348,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal) Modal.prototype.render = function () { const modal = MODALS[this.props.modalState.name || 'DEFAULT'] - const children = modal.contents + const { contents: children, disableBackdropClick = false } = modal const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle'] const contentStyle = modal.contentStyle || {} @@ -326,6 +368,7 @@ Modal.prototype.render = function () { modalStyle, contentStyle, backdropStyle: BACKDROPSTYLE, + closeOnClick: !disableBackdropClick, }, children, ) diff --git a/ui/app/components/modals/notification-modals/confirm-reset-account.js b/ui/app/components/modals/notification-modals/confirm-reset-account.js deleted file mode 100644 index 89fa9bef1..000000000 --- a/ui/app/components/modals/notification-modals/confirm-reset-account.js +++ /dev/null @@ -1,46 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../../actions') -const NotifcationModal = require('../notification-modal') - -class ConfirmResetAccount extends Component { - render () { - const { resetAccount } = this.props - - return h(NotifcationModal, { - header: 'Are you sure you want to reset account?', - message: h('div', [ - - h('span', `Resetting is for developer use only. This button wipes the current account's transaction history, - which is used to calculate the current account nonce. `), - - h('a.notification-modal__link', { - href: 'http://metamask.helpscoutdocs.com/article/36-resetting-an-account', - target: '_blank', - onClick (event) { global.platform.openWindow({ url: event.target.href }) }, - }, 'Read more.'), - - ]), - showCancelButton: true, - showConfirmButton: true, - onConfirm: resetAccount, - - }) - } -} - -ConfirmResetAccount.propTypes = { - resetAccount: PropTypes.func, -} - -const mapDispatchToProps = dispatch => { - return { - resetAccount: () => { - dispatch(actions.resetAccount()) - }, - } -} - -module.exports = connect(null, mapDispatchToProps)(ConfirmResetAccount) diff --git a/ui/app/components/modals/notification/index.js b/ui/app/components/modals/notification/index.js new file mode 100644 index 000000000..d60a3129b --- /dev/null +++ b/ui/app/components/modals/notification/index.js @@ -0,0 +1,2 @@ +import Notification from './notification.container' +module.exports = Notification diff --git a/ui/app/components/modals/notification/notification.component.js b/ui/app/components/modals/notification/notification.component.js new file mode 100644 index 000000000..1af2f3ca8 --- /dev/null +++ b/ui/app/components/modals/notification/notification.component.js @@ -0,0 +1,30 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Button from '../../button' + +const Notification = (props, context) => { + return ( + + { props.children } + + props.onHide()} + > + { context.t('ok') } + + + + ) +} + +Notification.propTypes = { + onHide: PropTypes.func.isRequired, + children: PropTypes.element, +} + +Notification.contextTypes = { + t: PropTypes.func, +} + +export default Notification diff --git a/ui/app/components/modals/notification/notification.container.js b/ui/app/components/modals/notification/notification.container.js new file mode 100644 index 000000000..5b98714da --- /dev/null +++ b/ui/app/components/modals/notification/notification.container.js @@ -0,0 +1,38 @@ +import { connect } from 'react-redux' +import Notification from './notification.component' + +const { hideModal } = require('../../../actions') + +const mapStateToProps = state => { + const { appState: { modal: { modalState: { props } } } } = state + const { onHide } = props + return { + onHide, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { onHide, ...otherStateProps } = stateProps + const { hideModal, ...otherDispatchProps } = dispatchProps + + return { + ...otherStateProps, + ...otherDispatchProps, + ...ownProps, + onHide: () => { + hideModal() + + if (onHide && typeof onHide === 'function') { + onHide() + } + }, + } +} + +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notification) diff --git a/ui/app/components/modals/transaction-confirmed/index.js b/ui/app/components/modals/transaction-confirmed/index.js new file mode 100644 index 000000000..cee8da7f8 --- /dev/null +++ b/ui/app/components/modals/transaction-confirmed/index.js @@ -0,0 +1,2 @@ +import TransactionConfirmed from './transaction-confirmed.component' +module.exports = TransactionConfirmed diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js new file mode 100644 index 000000000..c1c8a2976 --- /dev/null +++ b/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js @@ -0,0 +1,24 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const TransactionConfirmed = (props, context) => { + const { t } = context + + return ( + + + + { `${t('confirmed')}!` } + + + { t('initialTransactionConfirmed') } + + + ) +} + +TransactionConfirmed.contextTypes = { + t: PropTypes.func, +} + +export default TransactionConfirmed diff --git a/ui/app/components/modals/welcome-beta/index.js b/ui/app/components/modals/welcome-beta/index.js new file mode 100644 index 000000000..515c9cdaf --- /dev/null +++ b/ui/app/components/modals/welcome-beta/index.js @@ -0,0 +1,2 @@ +import WelcomeBeta from './welcome-beta.component' +module.exports = WelcomeBeta diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.component.js b/ui/app/components/modals/welcome-beta/welcome-beta.component.js new file mode 100644 index 000000000..61571723a --- /dev/null +++ b/ui/app/components/modals/welcome-beta/welcome-beta.component.js @@ -0,0 +1,23 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const TransactionConfirmed = (props, context) => { + const { t } = context + + return ( + + + { `${t('uiWelcome')}` } + + + { t('uiWelcomeMessage') } + + + ) +} + +TransactionConfirmed.contextTypes = { + t: PropTypes.func, +} + +export default TransactionConfirmed diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js index 0677b4317..1f4b37b53 100644 --- a/ui/app/components/pages/add-token/add-token.component.js +++ b/ui/app/components/pages/add-token/add-token.component.js @@ -323,7 +323,7 @@ class AddToken extends Component { { diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss index 9d0f4be32..cc495dfb0 100644 --- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss +++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss @@ -11,6 +11,10 @@ width: 50%; text-align: center; margin-top: 8px; + + @media screen and (max-width: 575px) { + width: 60%; + } } &__link { diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js index 9db9efc37..65d654b92 100644 --- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js +++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js @@ -87,7 +87,7 @@ export default class ConfirmAddToken extends Component { history.push(ADD_TOKEN_ROUTE)} diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js index 0a3314b2a..0164417ec 100644 --- a/ui/app/components/pages/create-account/import-account/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -51,7 +51,7 @@ class JsonImportSubview extends Component { h('div.new-account-create-form__buttons', {}, [ - h('button.btn-secondary.new-account-create-form__button', { + h('button.btn-default.new-account-create-form__button', { onClick: () => this.props.history.push(DEFAULT_ROUTE), }, [ this.context.t('cancel'), @@ -105,7 +105,7 @@ class JsonImportSubview extends Component { } this.props.importNewJsonAccount([ fileContents, password ]) - // JS runtime requires caught rejections but failures are handled by Redux + // JS runtime requires caught rejections but failures are handled by Redux .catch() } } diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js index df7ac910a..e55a47a3c 100644 --- a/ui/app/components/pages/create-account/import-account/private-key.js +++ b/ui/app/components/pages/create-account/import-account/private-key.js @@ -59,13 +59,13 @@ PrivateKeyImportView.prototype.render = function () { h('div.new-account-import-form__buttons', {}, [ - h('button.btn-secondary--lg.new-account-create-form__button', { + h('button.btn-default.btn--large.new-account-create-form__button', { onClick: () => this.props.history.push(DEFAULT_ROUTE), }, [ this.context.t('cancel'), ]), - h('button.btn-primary--lg.new-account-create-form__button', { + h('button.btn-primary.btn--large.new-account-create-form__button', { onClick: () => this.createNewKeychain(), }, [ this.context.t('import'), @@ -91,7 +91,7 @@ PrivateKeyImportView.prototype.createNewKeychain = function () { const { importNewAccount, history } = this.props importNewAccount('Private Key', [ privateKey ]) - // JS runtime requires caught rejections but failures are handled by Redux + // JS runtime requires caught rejections but failures are handled by Redux .catch() .then(() => history.push(DEFAULT_ROUTE)) } diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js index 03a5ee72d..9c94990e0 100644 --- a/ui/app/components/pages/create-account/new-account.js +++ b/ui/app/components/pages/create-account/new-account.js @@ -38,13 +38,13 @@ class NewAccountCreateForm extends Component { h('div.new-account-create-form__buttons', {}, [ - h('button.btn-secondary--lg.new-account-create-form__button', { + h('button.btn-default.btn--large.new-account-create-form__button', { onClick: () => history.push(DEFAULT_ROUTE), }, [ this.context.t('cancel'), ]), - h('button.btn-primary--lg.new-account-create-form__button', { + h('button.btn-primary.btn--large.new-account-create-form__button', { onClick: () => { createAccount(newAccountName || defaultAccountName) .then(() => history.push(DEFAULT_ROUTE)) diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js index 685c81074..7d7d3f462 100644 --- a/ui/app/components/pages/keychains/reveal-seed.js +++ b/ui/app/components/pages/keychains/reveal-seed.js @@ -106,10 +106,10 @@ class RevealSeedPage extends Component { renderPasswordPromptFooter () { return ( h('.page-container__footer', [ - h('button.btn-secondary--lg.page-container__footer-button', { + h('button.btn-default.btn--large.page-container__footer-button', { onClick: () => this.props.history.push(DEFAULT_ROUTE), }, this.context.t('cancel')), - h('button.btn-primary--lg.page-container__footer-button', { + h('button.btn-primary.btn--large.page-container__footer-button', { onClick: event => this.handleSubmit(event), disabled: this.state.password === '', }, this.context.t('next')), @@ -120,7 +120,7 @@ class RevealSeedPage extends Component { renderRevealSeedFooter () { return ( h('.page-container__footer', [ - h('button.btn-secondary--lg.page-container__footer-button', { + h('button.btn-default.btn--large.page-container__footer-button', { onClick: () => this.props.history.push(DEFAULT_ROUTE), }, this.context.t('close')), ]) diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js index f58ac7ddf..ff42a13de 100644 --- a/ui/app/components/pages/settings/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -217,7 +217,7 @@ class Settings extends Component { ]), h('div.settings__content-item', [ h('div.settings__content-item-col', [ - h('button.btn-primary--lg.settings__button', { + h('button.btn-primary.btn--large.settings__button', { onClick (event) { window.logStateString((err, result) => { if (err) { @@ -242,7 +242,7 @@ class Settings extends Component { h('div.settings__content-item', this.context.t('revealSeedWords')), h('div.settings__content-item', [ h('div.settings__content-item-col', [ - h('button.btn-primary--lg.settings__button--red', { + h('button.btn-primary.btn--large.settings__button--red', { onClick: event => { event.preventDefault() history.push(REVEAL_SEED_ROUTE) @@ -262,7 +262,7 @@ class Settings extends Component { h('div.settings__content-item', this.context.t('useOldUI')), h('div.settings__content-item', [ h('div.settings__content-item-col', [ - h('button.btn-primary--lg.settings__button--orange', { + h('button.btn-primary.btn--large.settings__button--orange', { onClick (event) { event.preventDefault() setFeatureFlagToBeta() @@ -281,7 +281,7 @@ class Settings extends Component { h('div.settings__content-item', this.context.t('resetAccount')), h('div.settings__content-item', [ h('div.settings__content-item-col', [ - h('button.btn-primary--lg.settings__button--orange', { + h('button.btn-primary.btn--large.settings__button--orange', { onClick (event) { event.preventDefault() showResetAccountConfirmationModal() diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js index a2f009d8f..8bc3897da 100644 --- a/ui/app/components/pages/unlock-page/unlock-page.component.js +++ b/ui/app/components/pages/unlock-page/unlock-page.component.js @@ -34,14 +34,7 @@ class UnlockPage extends Component { } } - tryUnlockMetamask (password) { - const { tryUnlockMetamask, history } = this.props - tryUnlockMetamask(password) - .then(() => history.push(DEFAULT_ROUTE)) - .catch(({ message }) => this.setState({ error: message })) - } - - handleSubmit (event) { + async handleSubmit (event) { event.preventDefault() event.stopPropagation() @@ -54,9 +47,14 @@ class UnlockPage extends Component { this.setState({ error: null }) - tryUnlockMetamask(password) - .then(() => history.push(DEFAULT_ROUTE)) - .catch(({ message }) => this.setState({ error: message })) + try { + await tryUnlockMetamask(password) + } catch ({ message }) { + this.setState({ error: message }) + return + } + + history.push(DEFAULT_ROUTE) } handleInputChange ({ target }) { diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index c07c96ccc..5ad35c269 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -291,18 +291,48 @@ ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, curren : value } -ConfirmSendEther.prototype.editTransaction = function (txMeta) { +ConfirmSendEther.prototype.editTransaction = function () { const { editTransaction, history } = this.props + const txMeta = this.gatherTxMeta() editTransaction(txMeta) history.push(SEND_ROUTE) } -ConfirmSendEther.prototype.renderNetworkDisplay = function () { +ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) { const windowType = window.METAMASK_UI_TYPE + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP - return (windowType === ENVIRONMENT_TYPE_NOTIFICATION || windowType === ENVIRONMENT_TYPE_POPUP) - ? h(NetworkDisplay) - : null + if (isTxReprice && isFullScreen) { + return null + } + + return ( + h('.page-container__header-row', [ + h('span.page-container__back-button', { + onClick: () => this.editTransaction(), + style: { + visibility: isTxReprice ? 'hidden' : 'initial', + }, + }, 'Edit'), + !isFullScreen && h(NetworkDisplay), + ]) + ) +} + +ConfirmSendEther.prototype.renderHeader = function (isTxReprice) { + const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm') + const subtitle = isTxReprice + ? this.context.t('speedUpSubtitle') + : this.context.t('pleaseReviewTransaction') + + return ( + h('.page-container__header', [ + this.renderHeaderRow(isTxReprice), + h('.page-container__title', title), + h('.page-container__subtitle', subtitle), + ]) + ) } ConfirmSendEther.prototype.render = function () { @@ -320,6 +350,7 @@ ConfirmSendEther.prototype.render = function () { }, } = this.props const txMeta = this.gatherTxMeta() + const isTxReprice = Boolean(txMeta.lastGasPrice) const txParams = txMeta.txParams || {} const { @@ -338,11 +369,6 @@ ConfirmSendEther.prototype.render = function () { totalInETH, } = this.getData() - const title = txMeta.lastGasPrice ? 'Reprice Transaction' : 'Confirm' - const subtitle = txMeta.lastGasPrice - ? 'Increase your gas fee to attempt to overwrite and speed up your transaction' - : 'Please review your transaction.' - const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency) const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency) @@ -362,19 +388,7 @@ ConfirmSendEther.prototype.render = function () { return ( // Main Send token Card h('.page-container', [ - h('.page-container__header', [ - h('.page-container__header-row', [ - h('span.page-container__back-button', { - onClick: () => this.editTransaction(txMeta), - style: { - visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden', - }, - }, 'Edit'), - this.renderNetworkDisplay(), - ]), - h('.page-container__title', title), - h('.page-container__subtitle', subtitle), - ]), + this.renderHeader(isTxReprice), h('.page-container__content', [ h(SenderToRecipient, { senderName: fromName, diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 656093b3d..ddaa13d22 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -12,6 +12,7 @@ const actions = require('../../actions') const clone = require('clone') const Identicon = require('../identicon') const GasFeeDisplay = require('../send/gas-fee-display-v2.js') +const NetworkDisplay = require('../network-display') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN const { @@ -39,6 +40,11 @@ const { } = require('../../selectors') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') +const { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, +} = require('../../../../app/scripts/lib/enums') + ConfirmSendToken.contextTypes = { t: PropTypes.func, } @@ -430,6 +436,43 @@ ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, curren : value } +ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) { + const windowType = window.METAMASK_UI_TYPE + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP + + if (isTxReprice && isFullScreen) { + return null + } + + return ( + h('.page-container__header-row', [ + h('span.page-container__back-button', { + onClick: () => this.editTransaction(), + style: { + visibility: isTxReprice ? 'hidden' : 'initial', + }, + }, 'Edit'), + !isFullScreen && h(NetworkDisplay), + ]) + ) +} + +ConfirmSendToken.prototype.renderHeader = function (isTxReprice) { + const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm') + const subtitle = isTxReprice + ? this.context.t('speedUpSubtitle') + : this.context.t('pleaseReviewTransaction') + + return ( + h('.page-container__header', [ + this.renderHeaderRow(isTxReprice), + h('.page-container__title', title), + h('.page-container__subtitle', subtitle), + ]) + ) +} + ConfirmSendToken.prototype.render = function () { const txMeta = this.gatherTxMeta() const { @@ -443,25 +486,13 @@ ConfirmSendToken.prototype.render = function () { }, } = this.getData() - this.inputs = [] - const isTxReprice = Boolean(txMeta.lastGasPrice) - const title = isTxReprice ? this.context.t('reprice_title') : this.context.t('confirm') - const subtitle = isTxReprice - ? this.context.t('reprice_subtitle') - : this.context.t('pleaseReviewTransaction') return ( h('div.confirm-screen-container.confirm-send-token', [ // Main Send token Card h('div.page-container', [ - h('div.page-container__header', [ - !txMeta.lastGasPrice && h('button.confirm-screen-back-button', { - onClick: () => this.editTransaction(txMeta), - }, this.context.t('edit')), - h('div.page-container__title', title), - h('div.page-container__subtitle', subtitle), - ]), + this.renderHeader(isTxReprice), h('.page-container__content', [ h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.confirm-screen-account-wrapper', [ diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index 893538bcf..3f8cd8823 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -130,7 +130,6 @@ PendingTx.prototype.render = function () { if (isFetching) { return h(Loading, { - fullScreen: true, loadingMessage: this.context.t('generatingTransaction'), }) } @@ -157,9 +156,7 @@ PendingTx.prototype.render = function () { sendTransaction, }) default: - return h(Loading, { - fullScreen: true, - }) + return h(Loading) } } diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 22ab64426..93d2023b5 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -242,7 +242,7 @@ ShapeshiftForm.prototype.render = function () { ]), - !depositAddress && h('button.btn-primary--lg.shapeshift-form__shapeshift-buy-btn', { + !depositAddress && h('button.btn-primary.btn--large.shapeshift-form__shapeshift-buy-btn', { className: btnClass, disabled: !token, onClick: () => this.onBuyWithShapeShift(), diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js index 474fcf439..ab780dcf4 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/signature-request.js @@ -235,12 +235,12 @@ SignatureRequest.prototype.renderFooter = function () { } return h('div.request-signature__footer', [ - h('button.btn-secondary--lg.request-signature__footer__cancel-button', { + h('button.btn-default.btn--large.request-signature__footer__cancel-button', { onClick: event => { cancel(event).then(() => this.props.history.push(DEFAULT_ROUTE)) }, }, this.context.t('cancel')), - h('button.btn-primary--lg', { + h('button.btn-primary.btn--large', { onClick: event => { sign(event).then(() => this.props.history.push(DEFAULT_ROUTE)) }, diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/text-field/text-field.component.js index b695a449a..2c72d8124 100644 --- a/ui/app/components/text-field/text-field.component.js +++ b/ui/app/components/text-field/text-field.component.js @@ -1,8 +1,15 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import { withStyles } from '@material-ui/core/styles' import { default as MaterialTextField } from '@material-ui/core/TextField' +const inputLabelBase = { + transform: 'none', + transition: 'none', + position: 'initial', + color: '#5b5b5b', +} + const styles = { materialLabel: { '&$materialFocused': { @@ -46,57 +53,57 @@ const styles = { border: '1px solid #2f9ae0', }, }, + largeInputLabel: { + ...inputLabelBase, + fontSize: '1rem', + }, inputLabel: { + ...inputLabelBase, fontSize: '.75rem', - transform: 'none', - transition: 'none', - position: 'initial', - color: '#5b5b5b', }, } -class TextField extends Component { - static defaultProps = { - error: null, - } +const TextField = props => { + const { error, classes, material, startAdornment, largeLabel, ...textFieldProps } = props - static propTypes = { - error: PropTypes.string, - classes: PropTypes.object, - material: PropTypes.bool, - startAdornment: PropTypes.element, - } + return ( + + ) +} - render () { - const { error, classes, material, startAdornment, ...textFieldProps } = this.props +TextField.defaultProps = { + error: null, +} - return ( - - ) - } +TextField.propTypes = { + error: PropTypes.string, + classes: PropTypes.object, + material: PropTypes.bool, + startAdornment: PropTypes.element, + largeLabel: PropTypes.bool, } export default withStyles(styles)(TextField) diff --git a/ui/app/components/text-field/text-field.stories.js b/ui/app/components/text-field/text-field.stories.js index ee3e5faaf..c00873b8a 100644 --- a/ui/app/components/text-field/text-field.stories.js +++ b/ui/app/components/text-field/text-field.stories.js @@ -22,3 +22,32 @@ storiesOf('TextField', module) error="Invalid value" /> ) + .add('Mascara text', () => + + ) + .add('Material text', () => + + ) + .add('Material password', () => + + ) + .add('Material error', () => + + ) diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index bd4ea80a6..ef441ff73 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -1,5 +1,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') +const { compose } = require('recompose') +const { withRouter } = require('react-router-dom') const h = require('react-hyperscript') const connect = require('react-redux').connect const inherits = require('util').inherits @@ -16,13 +18,16 @@ const { conversionUtil, multiplyCurrencies } = require('../conversion-util') const { calcTokenAmount } = require('../token-util') const { getCurrentCurrency } = require('../selectors') +const { CONFIRM_TRANSACTION_ROUTE } = require('../routes') TxListItem.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(TxListItem) - +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TxListItem) function mapStateToProps (state) { return { @@ -216,6 +221,7 @@ TxListItem.prototype.setSelectedToken = function (tokenAddress) { TxListItem.prototype.resubmit = function () { const { transactionId } = this.props this.props.retryTransaction(transactionId) + .then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)) } TxListItem.prototype.render = function () { diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index fb38aaa76..461587cb1 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -7,6 +7,7 @@ const { compose } = require('recompose') const actions = require('./actions') const txHelper = require('../lib/tx-helper') const log = require('loglevel') +const R = require('ramda') const PendingTx = require('./components/pending-tx') const SignatureRequest = require('./components/signature-request') @@ -87,37 +88,74 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) { network, selectedAddressTxList, send, + history, + match: { params: { id: transactionId } = {} }, } = this.props - const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps - const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) - const prevTxData = prevUnconfTxList[prevIndex] || {} - const prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} + + let prevTx + + if (transactionId) { + prevTx = R.find(({ id }) => id + '' === transactionId)(selectedAddressTxList) + } else { + const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps + const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network) + const prevTxData = prevUnconfTxList[prevIndex] || {} + prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {} + } + const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network) - if (unconfTxList.length === 0 && - (prevTx.status === 'dropped' || !send.to && this.getUnapprovedMessagesTotal() === 0)) { + if (prevTx.status === 'dropped') { + this.props.dispatch(actions.showModal({ + name: 'TRANSACTION_CONFIRMED', + onHide: () => history.push(DEFAULT_ROUTE), + })) + + return + } + + if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) { this.props.history.push(DEFAULT_ROUTE) } } -ConfirmTxScreen.prototype.render = function () { - const props = this.props +ConfirmTxScreen.prototype.getTxData = function () { const { network, + index, + unapprovedTxs, + unapprovedMsgs, + unapprovedPersonalMsgs, + unapprovedTypedMessages, + match: { params: { id: transactionId } = {} }, + } = this.props + + const unconfTxList = txHelper( unapprovedTxs, - currentCurrency, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, + network + ) + + log.info(`rendering a combined ${unconfTxList.length} unconf msgs & txs`) + + return transactionId + ? R.find(({ id }) => id + '' === transactionId)(unconfTxList) + : unconfTxList[index] +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { + currentCurrency, conversionRate, blockGasLimit, // provider, // computedBalances, } = props - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) - - var txData = unconfTxList[props.index] || {} + var txData = this.getTxData() || {} var txParams = txData.params || {} // var isNotification = isPopupOrNotification() === 'notification' @@ -136,7 +174,6 @@ ConfirmTxScreen.prototype.render = function () { ]), */ - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) return currentTxView({ // Properties diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 4cbed6093..f93daec04 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -2,10 +2,10 @@ Buttons */ +.btn-default, .btn-primary, -.btn-primary--lg, -.btn-secondary, -.btn-secondary--lg { +.btn-secondary { + height: 44px; background: $white; display: flex; justify-content: center; @@ -20,10 +20,16 @@ width: 100%; text-transform: uppercase; outline: none; + + &--disabled, + &[disabled] { + cursor: auto; + opacity: .5; + pointer-events: none; + } } -.btn-primary, -.btn-primary--lg { +.btn-primary { color: $curious-blue; border: 2px solid $spindle; @@ -35,17 +41,23 @@ &:hover { border-color: $curious-blue; } +} - &--disabled, - &[disabled] { - cursor: auto; - opacity: .5; - pointer-events: none; +.btn-secondary { + color: $monzo; + border: 2px solid lighten($monzo, 40%); + + &:active { + background: lighten($monzo, 55%); + border-color: $monzo; + } + + &:hover { + border-color: $monzo; } } -.btn-secondary, -.btn-secondary--lg { +.btn-default { color: $scorpion; border: 2px solid $dusty-gray; @@ -57,20 +69,9 @@ &:hover { border-color: $scorpion; } - - &--disabled, - &[disabled] { - cursor: auto; - opacity: .5; - pointer-events: none; - } -} - -.btn-primary, .btn-secondary { - height: 44px; } -.btn-primary--lg, .btn-secondary--lg { +.btn--large { height: 54px; } diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss index c18b7fa59..b07d6af17 100644 --- a/ui/app/css/itcss/components/loading-overlay.scss +++ b/ui/app/css/itcss/components/loading-overlay.scss @@ -11,8 +11,8 @@ background: rgba(255, 255, 255, .8); @media screen and (max-width: 575px) { - margin-top: 56px; - height: calc(100% - 56px); + margin-top: 66px; + height: calc(100% - 66px); } @media screen and (min-width: 576px) { diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 2b39eb8db..4e9d0848c 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -42,6 +42,7 @@ function reduceApp (state, action) { open: false, modalState: { name: null, + props: {}, }, previousModalState: { name: null, @@ -88,13 +89,17 @@ function reduceApp (state, action) { // modal methods: case actions.MODAL_OPEN: + const { name, ...modalProps } = action.payload + return extend(appState, { - modal: Object.assign( - state.appState.modal, - { open: true }, - { modalState: action.payload }, - { previousModalState: appState.modal.modalState}, - ), + modal: { + open: true, + modalState: { + name: name, + props: { ...modalProps }, + }, + previousModalState: { ...appState.modal.modalState }, + }, }) case actions.MODAL_CLOSE: @@ -102,7 +107,7 @@ function reduceApp (state, action) { modal: Object.assign( state.appState.modal, { open: false }, - { modalState: { name: null } }, + { modalState: { name: null, props: {} } }, { previousModalState: appState.modal.modalState}, ), }) diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 6736b5571..4fbe8ff11 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -499,7 +499,7 @@ SendTransactionScreen.prototype.renderFooter = function () { return h('div.page-container__footer', [ h(Button, { - type: 'secondary', + type: 'default', large: true, className: 'page-container__footer-button', onClick: () => {