From cb0af67f743d242afa3bdb518847f77d3c2cc260 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 22 Aug 2018 11:23:54 -0700 Subject: [PATCH 01/19] metamask controller - force account tracker to update balances on network change --- app/scripts/metamask-controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 29838ad2d..1e2df6368 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -127,6 +127,10 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, }) + // ensure accountTracker updates balances after network change + this.networkController.on('networkDidChange', () => { + this.accountTracker._updateAccounts() + }) // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] From 70c45ae8be4827415c218a8b527fd0a6d420a7ba Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 Oct 2018 01:14:25 -0400 Subject: [PATCH 02/19] enable fetch debugging --- app/scripts/background.js | 3 +++ app/scripts/lib/setupFetchDebugging.js | 34 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 app/scripts/lib/setupFetchDebugging.js diff --git a/app/scripts/background.js b/app/scripts/background.js index 0343e134c..509a0001d 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -2,6 +2,9 @@ * @file The entry point for the web extension singleton process. */ +// this needs to run before anything else +require('./lib/setupFetchDebugging')() + const urlUtil = require('url') const endOfStream = require('end-of-stream') const pump = require('pump') diff --git a/app/scripts/lib/setupFetchDebugging.js b/app/scripts/lib/setupFetchDebugging.js new file mode 100644 index 000000000..dd87b65a6 --- /dev/null +++ b/app/scripts/lib/setupFetchDebugging.js @@ -0,0 +1,34 @@ +module.exports = setupFetchDebugging + +// +// This is a utility to help resolve cases where `window.fetch` throws a +// `TypeError: Failed to Fetch` without any stack or context for the request +// https://github.com/getsentry/sentry-javascript/pull/1293 +// + +function setupFetchDebugging() { + if (!global.fetch) return + const originalFetch = global.fetch + + global.fetch = wrappedFetch + + async function wrappedFetch(...args) { + const initialStack = getCurrentStack() + try { + return await originalFetch.call(window, ...args) + } catch (err) { + console.warn('FetchDebugger - fetch encountered an Error', err) + console.warn('FetchDebugger - overriding stack to point of original call') + err.stack = initialStack + throw err + } + } +} + +function getCurrentStack() { + try { + throw new Error('Fake error for generating stack trace') + } catch (err) { + return err.stack + } +} From bd357280410d026c4839576658a20f9b95b0de88 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 16 Oct 2018 16:36:11 -0230 Subject: [PATCH 03/19] Lower i18n-helper#getMessage log level from error to warning --- ui/i18n-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/i18n-helper.js b/ui/i18n-helper.js index c6a7d0bf1..db07049e1 100644 --- a/ui/i18n-helper.js +++ b/ui/i18n-helper.js @@ -13,7 +13,7 @@ const getMessage = (locale, key, substitutions) => { return null } if (!locale[key]) { - log.error(`Translator - Unable to find value for key "${key}"`) + log.warn(`Translator - Unable to find value for key "${key}"`) return null } const entry = locale[key] From 222da7f52351a7f5d872860baf4f6d080161bbd6 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 16 Oct 2018 17:03:18 -0230 Subject: [PATCH 04/19] Update wording on export privkey modal --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 13b0da230..63ab58e0a 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1189,7 +1189,7 @@ "message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret." }, "typePassword": { - "message": "Type Your Password" + "message": "Type your MetaMask password" }, "uiWelcome": { "message": "Welcome to the New UI (Beta)" From e96188ce9d1fa2920a384410e8080c25dc5a62cc Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 16 Oct 2018 19:05:21 -0230 Subject: [PATCH 05/19] Fix wording of private key warning shown during export --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 63ab58e0a..f54dfd013 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -798,7 +798,7 @@ "description": "select this type of file to use to import an account" }, "privateKeyWarning": { - "message": "Warning: Never disclose this key. Anyone with your private keys can take steal any assets held in your account." + "message": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account." }, "privateNetwork": { "message": "Private Network" From badebe017fe28b58ac742082368484c3a4b1c1bc Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Wed, 17 Oct 2018 07:03:29 +0800 Subject: [PATCH 06/19] Adds toggle for primary currency (#5421) * Add UnitInput component * Add CurrencyInput component * Add UserPreferencedCurrencyInput component * Add UserPreferencedCurrencyDisplay component * Add updatePreferences action * Add styles for CurrencyInput, CurrencyDisplay, and UnitInput * Update SettingsTab page with Primary Currency toggle * Refactor currency displays and inputs to use UserPreferenced displays and inputs * Add TokenInput component * Add UserPreferencedTokenInput component * Use TokenInput in the send screen * Fix unit tests * Fix e2e and integration tests * Remove send/CurrencyDisplay component * Replace diamond unicode character with Eth logo. Fix typos --- app/_locales/en/messages.json | 11 +- app/images/eth.svg | 14 + app/scripts/controllers/preferences.js | 30 ++ app/scripts/metamask-controller.js | 1 + development/states/add-token.json | 5 +- development/states/confirm-sig-requests.json | 5 +- development/states/currency-localization.json | 5 +- development/states/first-time.json | 5 +- development/states/send-new-ui.json | 5 +- development/states/tx-list-items.json | 5 +- test/e2e/beta/from-import-beta-ui.spec.js | 2 +- test/e2e/beta/metamask-beta-ui.spec.js | 12 +- test/integration/lib/send-new-ui.js | 20 +- .../unit/components/balance-component-test.js | 44 --- ui/app/actions.js | 36 ++ ui/app/components/account-menu/index.js | 11 +- ui/app/components/balance-component.js | 30 +- .../confirm-detail-row.component.js | 66 +++- .../confirm-detail-row/index.scss | 12 +- .../confirm-detail-row.component.test.js | 38 +-- ...onfirm-page-container-content.component.js | 7 +- ...onfirm-page-container-summary.component.js | 19 +- .../confirm-page-container.component.js | 5 +- .../currency-display.component.js | 11 +- .../currency-display.container.js | 25 +- ui/app/components/currency-display/index.scss | 10 + .../tests/currency-display.container.test.js | 21 +- .../currency-input.component.js | 120 +++++++ .../currency-input.container.js | 27 ++ ui/app/components/currency-input/index.js | 1 + ui/app/components/currency-input/index.scss | 7 + .../tests/currency-input.component.test.js | 239 ++++++++++++++ .../tests/currency-input.container.test.js | 55 ++++ ui/app/components/index.scss | 6 + .../cancel-transaction-gas-fee.component.js | 12 +- ...ncel-transaction-gas-fee.component.test.js | 9 +- ...onfirm-token-transaction-base.component.js | 59 +++- .../confirm-transaction-base.component.js | 87 +++-- .../confirm-transaction-base.container.js | 8 +- .../pages/settings/settings-tab/index.scss | 18 + .../settings-tab/settings-tab.component.js | 53 +++ .../settings-tab/settings-tab.container.js | 7 + .../account-list-item.component.js | 29 +- .../tests/account-list-item-component.test.js | 16 +- .../send/currency-display/currency-display.js | 186 ----------- .../components/send/currency-display/index.js | 1 - .../tests/currency-display.test.js | 91 ------ .../send-amount-row.component.js | 45 ++- .../tests/send-amount-row-component.test.js | 27 +- .../gas-fee-display.component.js | 35 +- .../test/gas-fee-display.component.test.js | 12 +- ui/app/components/token-input/index.js | 1 + .../tests/token-input.component.test.js | 308 ++++++++++++++++++ .../tests/token-input.container.test.js | 129 ++++++++ .../token-input/token-input.component.js | 136 ++++++++ .../token-input/token-input.container.js | 27 ++ .../transaction-breakdown.component.js | 15 +- .../transaction-list-item.component.js | 14 +- .../token-view-balance.component.test.js | 4 +- .../transaction-view-balance.component.js | 14 +- ui/app/components/unit-input/index.js | 1 + ui/app/components/unit-input/index.scss | 44 +++ .../tests/unit-input.component.test.js | 146 +++++++++ .../unit-input/unit-input.component.js | 104 ++++++ .../index.js | 1 + ...erenced-currency-display.component.test.js | 34 ++ ...erenced-currency-display.container.test.js | 105 ++++++ ...-preferenced-currency-display.component.js | 45 +++ ...-preferenced-currency-display.container.js | 52 +++ .../user-preferenced-currency-input/index.js | 1 + ...eferenced-currency-input.component.test.js | 32 ++ ...eferenced-currency-input.container.test.js | 31 ++ ...er-preferenced-currency-input.component.js | 20 ++ ...er-preferenced-currency-input.container.js | 13 + .../user-preferenced-token-input/index.js | 1 + ...-preferenced-token-input.component.test.js | 32 ++ ...-preferenced-token-input.container.test.js | 31 ++ .../user-preferenced-token-input.component.js | 20 ++ .../user-preferenced-token-input.container.js | 13 + ui/app/constants/common.js | 3 + ui/app/ducks/confirm-transaction.duck.js | 60 ++-- .../tests/confirm-transaction.duck.test.js | 44 +-- ui/app/helpers/conversions.util.js | 19 ++ ui/app/reducers/metamask.js | 9 + ui/app/selectors.js | 5 + 85 files changed, 2466 insertions(+), 653 deletions(-) create mode 100644 app/images/eth.svg delete mode 100644 test/unit/components/balance-component-test.js create mode 100644 ui/app/components/currency-display/index.scss create mode 100644 ui/app/components/currency-input/currency-input.component.js create mode 100644 ui/app/components/currency-input/currency-input.container.js create mode 100644 ui/app/components/currency-input/index.js create mode 100644 ui/app/components/currency-input/index.scss create mode 100644 ui/app/components/currency-input/tests/currency-input.component.test.js create mode 100644 ui/app/components/currency-input/tests/currency-input.container.test.js delete mode 100644 ui/app/components/send/currency-display/currency-display.js delete mode 100644 ui/app/components/send/currency-display/index.js delete mode 100644 ui/app/components/send/currency-display/tests/currency-display.test.js create mode 100644 ui/app/components/token-input/index.js create mode 100644 ui/app/components/token-input/tests/token-input.component.test.js create mode 100644 ui/app/components/token-input/tests/token-input.container.test.js create mode 100644 ui/app/components/token-input/token-input.component.js create mode 100644 ui/app/components/token-input/token-input.container.js create mode 100644 ui/app/components/unit-input/index.js create mode 100644 ui/app/components/unit-input/index.scss create mode 100644 ui/app/components/unit-input/tests/unit-input.component.test.js create mode 100644 ui/app/components/unit-input/unit-input.component.js create mode 100644 ui/app/components/user-preferenced-currency-display/index.js create mode 100644 ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js create mode 100644 ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js create mode 100644 ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js create mode 100644 ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js create mode 100644 ui/app/components/user-preferenced-currency-input/index.js create mode 100644 ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js create mode 100644 ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js create mode 100644 ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js create mode 100644 ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js create mode 100644 ui/app/components/user-preferenced-token-input/index.js create mode 100644 ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js create mode 100644 ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js create mode 100644 ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js create mode 100644 ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f54dfd013..690864ef1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -361,6 +361,9 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "eth": { + "message": "ETH" + }, "etherscanView": { "message": "View account on Etherscan" }, @@ -380,7 +383,7 @@ "message": "Failed" }, "fiat": { - "message": "FIAT", + "message": "Fiat", "description": "Exchange type" }, "fileImportFail": { @@ -790,6 +793,12 @@ "prev": { "message": "Prev" }, + "primaryCurrencySetting": { + "message": "Primary Currency" + }, + "primaryCurrencySettingDescription": { + "message": "Select ETH to prioritize displaying values in ETH. Select Fiat to prioritize displaying values in your selected currency." + }, "privacyMsg": { "message": "Privacy Policy" }, diff --git a/app/images/eth.svg b/app/images/eth.svg new file mode 100644 index 000000000..6375b790f --- /dev/null +++ b/app/images/eth.svg @@ -0,0 +1,14 @@ + + + + +deposit-eth +Created with Sketch. + + + + diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index fd6a4866d..8eb2bce0c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -38,6 +38,9 @@ class PreferencesController { lostIdentities: {}, seedWords: null, forgottenPassword: false, + preferences: { + useETHAsPrimaryCurrency: true, + }, }, opts.initState) this.diagnostics = opts.diagnostics @@ -463,6 +466,33 @@ class PreferencesController { getFeatureFlags () { return this.store.getState().featureFlags } + + /** + * Updates the `preferences` property, which is an object. These are user-controlled features + * found in the settings page. + * @param {string} preference The preference to enable or disable. + * @param {boolean} value Indicates whether or not the preference should be enabled or disabled. + * @returns {Promise} Promises a new object; the updated preferences object. + */ + setPreference (preference, value) { + const currentPreferences = this.getPreferences() + const updatedPreferences = { + ...currentPreferences, + [preference]: value, + } + + this.store.updateState({ preferences: updatedPreferences }) + return Promise.resolve(updatedPreferences) + } + + /** + * A getter for the `preferences` property + * @returns {object} A key-boolean map of user-selected preferences. + */ + getPreferences () { + return this.store.getState().preferences + } + // // PRIVATE METHODS // diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 493877345..ebf1749c6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -387,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter { setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), + setPreference: nodeify(preferencesController.setPreference, preferencesController), // BlacklistController whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), diff --git a/development/states/add-token.json b/development/states/add-token.json index d04b3a3ca..6a525f2b3 100644 --- a/development/states/add-token.json +++ b/development/states/add-token.json @@ -107,7 +107,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index 5017a4d57..c7103cd13 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -150,7 +150,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 847ea11a3..7dea42ade 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -108,7 +108,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/first-time.json b/development/states/first-time.json index a31b985a3..3206b67a3 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -37,7 +37,10 @@ "shapeShiftTxList": [], "lostAccounts": [], "tokens": [], - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index bb4847155..d9924dd74 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -109,7 +109,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index 0d2273cb0..cbffa98e5 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -102,7 +102,10 @@ "shapeShiftTxList": [{"depositAddress":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke","depositType":"BTC","key":"shapeshift","response":{"status":"no_deposits","address":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke"},"time":1522377459106}], "lostAccounts": [], "send": {}, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index 32aaa29a6..b782a1c40 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -286,7 +286,7 @@ describe('Using MetaMask with an existing account', function () { await delay(regularDelayMs) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) - const inputAmount = await findElement(driver, By.css('.currency-display__input')) + const inputAmount = await findElement(driver, By.css('.unit-input__input')) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('1') diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index 8d1ecac0d..12cf91227 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -383,7 +383,7 @@ describe('MetaMask', function () { await delay(regularDelayMs) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) - const inputAmount = await findElement(driver, By.css('.currency-display__input')) + const inputAmount = await findElement(driver, By.css('.unit-input__input')) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('1') @@ -702,7 +702,7 @@ describe('MetaMask', function () { await delay(regularDelayMs) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) - const inputAmount = await findElement(driver, By.css('.currency-display__input')) + const inputAmount = await findElement(driver, By.css('.unit-input__input')) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('50') @@ -834,8 +834,8 @@ describe('MetaMask', function () { await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) - assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary')) + assert.equal(await gasFeeInputs[0].getText(), '0.0006') }) it('submits the transaction', async function () { @@ -957,8 +957,8 @@ describe('MetaMask', function () { await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) - assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary')) + assert.equal(await gasFeeInputs[0].getText(), '0.0006') }) it('submits the transaction', async function () { diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index ac1cc2e14..e13016e68 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -40,7 +40,7 @@ async function customizeGas (assert, price, limit, ethFee, usdFee) { const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') assert.equal( - (await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(), + (await findAsync(sendGasField, '.currency-display-component'))[0].textContent, ethFee, 'send gas field should show customized gas total' ) @@ -97,9 +97,9 @@ async function runSendFlowTest (assert, done) { assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address') const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)') - sendAmountField.find('.currency-display')[0].click() + sendAmountField.find('.unit-input')[0].click() - const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input') + const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input') sendAmountFieldInput.val('5.1') reactTriggerChange(sendAmountField.find('input')[0]) @@ -112,9 +112,9 @@ async function runSendFlowTest (assert, done) { errorMessage = $('.send-v2__error') assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected') - await customizeGas(assert, 0, 21000, '0', '$0.00 USD') - await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD') - await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD') + await customizeGas(assert, 0, 21000, '0 ETH', '$0.00 USD') + await customizeGas(assert, 1, 21000, '0.000021 ETH', '$0.03 USD') + await customizeGas(assert, 500, 60000, '0.03 ETH', '$36.03 USD') const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') @@ -130,11 +130,11 @@ async function runSendFlowTest (assert, done) { const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last() assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name') - const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat') + const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__secondary') const confirmScreenGas = confirmScreenRowFiats[0] assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas') const confirmScreenTotal = confirmScreenRowFiats[1] - assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total') + assert.equal(confirmScreenTotal.textContent, '$2,405.37', 'confirm screen should show correct total') const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button') confirmScreenBackButton[0].click() @@ -150,9 +150,9 @@ async function runSendFlowTest (assert, done) { sendToFieldInputInEdit.val('0xd85a4b6a394794842887b8284293d69163007bbb') const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)') - sendAmountFieldInEdit.find('.currency-display')[0].click() + sendAmountFieldInEdit.find('.unit-input')[0].click() - const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input') + const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.unit-input__input') sendAmountFieldInputInEdit.val('1.0') reactTriggerChange(sendAmountFieldInputInEdit[0]) diff --git a/test/unit/components/balance-component-test.js b/test/unit/components/balance-component-test.js deleted file mode 100644 index aa9763b72..000000000 --- a/test/unit/components/balance-component-test.js +++ /dev/null @@ -1,44 +0,0 @@ -const assert = require('assert') -const h = require('react-hyperscript') -const { createMockStore } = require('redux-test-utils') -const { shallowWithStore } = require('../../lib/render-helpers') -const BalanceComponent = require('../../../ui/app/components/balance-component') -const mockState = { - metamask: { - accounts: { abc: {} }, - network: 1, - selectedAddress: 'abc', - }, -} - -describe('BalanceComponent', function () { - let balanceComponent - let store - let component - beforeEach(function () { - store = createMockStore(mockState) - component = shallowWithStore(h(BalanceComponent), store) - balanceComponent = component.dive() - }) - - it('shows token balance and convert to fiat value based on conversion rate', function () { - const formattedBalance = '1.23 ETH' - - const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false) - const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 2) - - assert.equal('1.23 ETH', tokenBalance) - assert.equal(2.46, fiatDisplayNumber) - }) - - it('shows only the token balance when conversion rate is not available', function () { - const formattedBalance = '1.23 ETH' - - const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false) - const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 0) - - assert.equal('1.23 ETH', tokenBalance) - assert.equal('N/A', fiatDisplayNumber) - }) - -}) diff --git a/ui/app/actions.js b/ui/app/actions.js index eea581d33..f8a375e2f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -305,6 +305,12 @@ var actions = { updateFeatureFlags, UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS', + // Preferences + setPreference, + updatePreferences, + UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', + setUseETHAsPrimaryCurrencyPreference, + setMouseUserState, SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', @@ -2298,6 +2304,36 @@ function updateFeatureFlags (updatedFeatureFlags) { } } +function setPreference (preference, value) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.setPreference(preference, value, (err, updatedPreferences) => { + dispatch(actions.hideLoadingIndication()) + + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.updatePreferences(updatedPreferences)) + resolve(updatedPreferences) + }) + }) + } +} + +function updatePreferences (value) { + return { + type: actions.UPDATE_PREFERENCES, + value, + } +} + +function setUseETHAsPrimaryCurrencyPreference (value) { + return setPreference('useETHAsPrimaryCurrency', value) +} + function setNetworkNonce (networkNonce) { return { type: actions.SET_NETWORK_NONCE, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index bcada41e3..c9c5b60e1 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -8,11 +8,11 @@ const h = require('react-hyperscript') const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const Identicon = require('../identicon') -const { formatBalance } = require('../../util') const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') const { getEnvironmentType } = require('../../../../app/scripts/lib/util') const Tooltip = require('../tooltip') - +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' +import { PRIMARY } from '../../constants/common' const { SETTINGS_ROUTE, @@ -163,7 +163,6 @@ AccountMenu.prototype.renderAccounts = function () { const isSelected = identity.address === selectedAddress const balanceValue = accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() const keyring = keyrings.find((kr) => { @@ -189,7 +188,11 @@ AccountMenu.prototype.renderAccounts = function () { h('div.account-menu__account-info', [ h('div.account-menu__name', identity.name || ''), - h('div.account-menu__balance', formattedBalance), + h(UserPreferencedCurrencyDisplay, { + className: 'account-menu__balance', + value: balanceValue, + type: PRIMARY, + }), ]), this.renderKeyringType(keyring), diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index d63d78c9f..e1fcf08e0 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -4,10 +4,11 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenBalance = require('./token-balance') const Identicon = require('./identicon') -import CurrencyDisplay from './currency-display' +import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../constants/common' const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') -const { formatBalance, generateBalanceObject } = require('../util') +const { formatBalance } = require('../util') module.exports = connect(mapStateToProps)(BalanceComponent) @@ -65,7 +66,7 @@ BalanceComponent.prototype.renderTokenBalance = function () { BalanceComponent.prototype.renderBalance = function () { const props = this.props - const { shorten, account } = props + const { account } = props const balanceValue = account && account.balance const needsParse = 'needsParse' in props ? props.needsParse : true const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' @@ -80,25 +81,20 @@ BalanceComponent.prototype.renderBalance = function () { } return h('div.flex-column.balance-display', {}, [ - h('div.token-amount', { - style: {}, - }, this.getTokenBalance(formattedBalance, shorten)), - - showFiat && h(CurrencyDisplay, { + h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, { value: balanceValue, + type: PRIMARY, + ethNumberOfDecimals: 3, + })), + + showFiat && h(UserPreferencedCurrencyDisplay, { + value: balanceValue, + type: SECONDARY, + ethNumberOfDecimals: 3, }), ]) } -BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { - const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3) - - const balanceValue = shorten ? balanceObj.shortBalance : balanceObj.balance - const label = balanceObj.label - - return `${balanceValue} ${label}` -} - BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) { if (formattedBalance === 'None') return formattedBalance if (conversionRate === 0) return 'N/A' diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js index f0703dde2..c7262d2a9 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -1,16 +1,19 @@ import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' const ConfirmDetailRow = props => { const { label, - fiatText, - ethText, + primaryText, + secondaryText, onHeaderClick, - fiatTextColor, + primaryValueTextColor, headerText, headerTextClassName, + value, } = props return ( @@ -25,28 +28,57 @@ const ConfirmDetailRow = props => { > { headerText } -
- { fiatText } -
-
- { ethText } -
+ { + primaryText + ? ( +
+ { primaryText } +
+ ) : ( + + ) + } + { + secondaryText + ? ( +
+ { secondaryText } +
+ ) : ( + + ) + } ) } ConfirmDetailRow.propTypes = { - label: PropTypes.string, - fiatText: PropTypes.string, - ethText: PropTypes.string, - fiatTextColor: PropTypes.string, - onHeaderClick: PropTypes.func, headerText: PropTypes.string, headerTextClassName: PropTypes.string, + label: PropTypes.string, + onHeaderClick: PropTypes.func, + primaryValueTextColor: PropTypes.string, + primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + secondaryText: PropTypes.string, + value: PropTypes.string, } export default ConfirmDetailRow diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss index dd6f87c17..580a41fde 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss @@ -18,18 +18,14 @@ min-width: 0; } - &__fiat { + &__primary { font-size: 1.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + justify-content: flex-end; } - &__eth { + &__secondary { color: $oslo-gray; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + justify-content: flex-end; } &__header-text { diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js index 6f2489071..c8507985d 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -12,17 +12,19 @@ describe('Confirm Detail Row Component', function () { let wrapper beforeEach(() => { - wrapper = shallow() + wrapper = shallow( + + ) }) describe('render', () => { @@ -38,16 +40,16 @@ describe('Confirm Detail Row Component', function () { assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText') }) - it('should render the fiatText as a child of the confirm-detail-row__fiat', () => { - assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText') + it('should render the primaryText as a child of the confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__primary').childAt(0).text(), 'mockFiatText') }) - it('should render the ethText as a child of the confirm-detail-row__eth', () => { - assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText') + it('should render the ethText as a child of the confirm-detail-row__secondary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__secondary').childAt(0).text(), 'mockEthText') }) - it('should set the fiatTextColor on confirm-detail-row__fiat', () => { - assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor') + it('should set the fiatTextColor on confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__primary').props().style.color, 'mockColor') }) it('should assure the confirm-detail-row__header-text classname is correct', () => { @@ -58,7 +60,5 @@ describe('Confirm Detail Row Component', function () { wrapper.find('.confirm-detail-row__header-text').props().onClick() assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1)) }) - - }) }) diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 74e95ece6..1dca81560 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -17,9 +17,10 @@ export default class ConfirmPageContainerContent extends Component { nonce: PropTypes.string, assetImage: PropTypes.string, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitleComponent: PropTypes.node, summaryComponent: PropTypes.node, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - titleComponent: PropTypes.func, + titleComponent: PropTypes.node, warning: PropTypes.string, } @@ -54,7 +55,9 @@ export default class ConfirmPageContainerContent extends Component { errorKey, errorMessage, title, + titleComponent, subtitle, + subtitleComponent, hideSubtitle, identiconAddress, nonce, @@ -80,7 +83,9 @@ export default class ConfirmPageContainerContent extends Component { })} action={action} title={title} + titleComponent={titleComponent} subtitle={subtitle} + subtitleComponent={subtitleComponent} hideSubtitle={hideSubtitle} identiconAddress={identiconAddress} nonce={nonce} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 38b158fd3..89ceb015f 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -4,7 +4,18 @@ import classnames from 'classnames' import Identicon from '../../../identicon' const ConfirmPageContainerSummary = props => { - const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props + const { + action, + title, + titleComponent, + subtitle, + subtitleComponent, + hideSubtitle, + className, + identiconAddress, + nonce, + assetImage, + } = props return (
@@ -32,12 +43,12 @@ const ConfirmPageContainerSummary = props => { ) }
- { title } + { titleComponent || title }
{ hideSubtitle ||
- { subtitle } + { subtitleComponent || subtitle }
} @@ -47,7 +58,9 @@ const ConfirmPageContainerSummary = props => { ConfirmPageContainerSummary.propTypes = { action: PropTypes.string, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + titleComponent: PropTypes.node, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitleComponent: PropTypes.node, hideSubtitle: PropTypes.bool, className: PropTypes.string, identiconAddress: PropTypes.string, diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js index 36d5a1f58..8b2e47cbb 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js @@ -16,8 +16,9 @@ export default class ConfirmPageContainer extends Component { onEdit: PropTypes.func, showEdit: PropTypes.bool, subtitle: PropTypes.string, + subtitleComponent: PropTypes.node, title: PropTypes.string, - titleComponent: PropTypes.func, + titleComponent: PropTypes.node, // Sender to Recipient fromAddress: PropTypes.string, fromName: PropTypes.string, @@ -65,6 +66,7 @@ export default class ConfirmPageContainer extends Component { title, titleComponent, subtitle, + subtitleComponent, hideSubtitle, summaryComponent, detailsComponent, @@ -101,6 +103,7 @@ export default class ConfirmPageContainer extends Component { title={title} titleComponent={titleComponent} subtitle={subtitle} + subtitleComponent={subtitleComponent} hideSubtitle={hideSubtitle} summaryComponent={summaryComponent} detailsComponent={detailsComponent} diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js index e4eb58a2a..5f5717be3 100644 --- a/ui/app/components/currency-display/currency-display.component.js +++ b/ui/app/components/currency-display/currency-display.component.js @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' import { ETH, GWEI } from '../../constants/common' export default class CurrencyDisplay extends PureComponent { @@ -7,6 +8,8 @@ export default class CurrencyDisplay extends PureComponent { className: PropTypes.string, displayValue: PropTypes.string, prefix: PropTypes.string, + prefixComponent: PropTypes.node, + style: PropTypes.object, // Used in container currency: PropTypes.oneOf([ETH]), denomination: PropTypes.oneOf([GWEI]), @@ -16,15 +19,17 @@ export default class CurrencyDisplay extends PureComponent { } render () { - const { className, displayValue, prefix } = this.props + const { className, displayValue, prefix, prefixComponent, style } = this.props const text = `${prefix || ''}${displayValue}` return (
- { text } + { prefixComponent} + { text }
) } diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js index 6644a1099..b387229b5 100644 --- a/ui/app/components/currency-display/currency-display.container.js +++ b/ui/app/components/currency-display/currency-display.container.js @@ -2,10 +2,26 @@ import { connect } from 'react-redux' import CurrencyDisplay from './currency-display.component' import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util' -const mapStateToProps = (state, ownProps) => { - const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps +const mapStateToProps = state => { const { metamask: { currentCurrency, conversionRate } } = state + return { + currentCurrency, + conversionRate, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { currentCurrency, conversionRate, ...restStateProps } = stateProps + const { + value, + numberOfDecimals = 2, + currency, + denomination, + hideLabel, + ...restOwnProps + } = ownProps + const toCurrency = currency || currentCurrency const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination, @@ -14,8 +30,11 @@ const mapStateToProps = (state, ownProps) => { const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}` return { + ...restStateProps, + ...dispatchProps, + ...restOwnProps, displayValue, } } -export default connect(mapStateToProps)(CurrencyDisplay) +export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay) diff --git a/ui/app/components/currency-display/index.scss b/ui/app/components/currency-display/index.scss new file mode 100644 index 000000000..8c0196102 --- /dev/null +++ b/ui/app/components/currency-display/index.scss @@ -0,0 +1,10 @@ +.currency-display-component { + display: flex; + align-items: center; + + &__text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js index 5265bbb04..b9f98c543 100644 --- a/ui/app/components/currency-display/tests/currency-display.container.test.js +++ b/ui/app/components/currency-display/tests/currency-display.container.test.js @@ -1,12 +1,13 @@ import assert from 'assert' import proxyquire from 'proxyquire' -let mapStateToProps +let mapStateToProps, mergeProps proxyquire('../currency-display.container.js', { 'react-redux': { - connect: ms => { + connect: (ms, md, mp) => { mapStateToProps = ms + mergeProps = mp return () => ({}) }, }, @@ -22,6 +23,20 @@ describe('CurrencyDisplay container', () => { }, } + assert.deepEqual(mapStateToProps(mockState), { + conversionRate: 280.45, + currentCurrency: 'usd', + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + conversionRate: 280.45, + currentCurrency: 'usd', + } + const tests = [ { props: { @@ -98,7 +113,7 @@ describe('CurrencyDisplay container', () => { ] tests.forEach(({ props, result }) => { - assert.deepEqual(mapStateToProps(mockState, props), result) + assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result) }) }) }) diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/currency-input/currency-input.component.js new file mode 100644 index 000000000..54cd0e1ac --- /dev/null +++ b/ui/app/components/currency-input/currency-input.component.js @@ -0,0 +1,120 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import UnitInput from '../unit-input' +import CurrencyDisplay from '../currency-display' +import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util' +import { ETH } from '../../constants/common' + +/** + * Component that allows user to enter currency values as a number, and props receive a converted + * hex value in WEI. props.value, used as a default or forced value, should be a hex value, which + * gets converted into a decimal value depending on the currency (ETH or Fiat). + */ +export default class CurrencyInput extends PureComponent { + static propTypes = { + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + suffix: PropTypes.string, + useFiat: PropTypes.bool, + value: PropTypes.string, + } + + constructor (props) { + super(props) + + const { value: hexValue } = props + const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + + this.state = { + decimalValue, + hexValue, + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsHexValue } = prevProps + const { value: propsHexValue } = this.props + const { hexValue: stateHexValue } = this.state + + if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { + const decimalValue = this.getDecimalValue(this.props) + this.setState({ hexValue: propsHexValue, decimalValue }) + } + } + + getDecimalValue (props) { + const { value: hexValue, useFiat, currentCurrency, conversionRate } = props + const decimalValueString = useFiat + ? getValueFromWeiHex({ + value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, + }) + : getValueFromWeiHex({ + value: hexValue, toCurrency: ETH, numberOfDecimals: 6, + }) + + return Number(decimalValueString) || 0 + } + + handleChange = decimalValue => { + const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props + + const hexValue = useFiat + ? getWeiHexFromDecimalValue({ + value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true, + }) + : getWeiHexFromDecimalValue({ + value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate, + }) + + this.setState({ hexValue, decimalValue }) + onChange(hexValue) + } + + handleBlur = () => { + this.props.onBlur(this.state.hexValue) + } + + renderConversionComponent () { + const { useFiat, currentCurrency } = this.props + const { hexValue } = this.state + let currency, numberOfDecimals + + if (useFiat) { + // Display ETH + currency = ETH + numberOfDecimals = 6 + } else { + // Display Fiat + currency = currentCurrency + numberOfDecimals = 2 + } + + return ( + + ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + + { this.renderConversionComponent() } + + ) + } +} diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js new file mode 100644 index 000000000..18e5533de --- /dev/null +++ b/ui/app/components/currency-input/currency-input.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import CurrencyInput from './currency-input.component' +import { ETH } from '../../constants/common' + +const mapStateToProps = state => { + const { metamask: { currentCurrency, conversionRate } } = state + + return { + currentCurrency, + conversionRate, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { currentCurrency } = stateProps + const { useFiat } = ownProps + const suffix = useFiat ? currentCurrency.toUpperCase() : ETH + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + suffix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(CurrencyInput) diff --git a/ui/app/components/currency-input/index.js b/ui/app/components/currency-input/index.js new file mode 100644 index 000000000..d8069fb67 --- /dev/null +++ b/ui/app/components/currency-input/index.js @@ -0,0 +1 @@ +export { default } from './currency-input.container' diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/currency-input/index.scss new file mode 100644 index 000000000..fcb2db461 --- /dev/null +++ b/ui/app/components/currency-input/index.scss @@ -0,0 +1,7 @@ +.currency-input { + &__conversion-component { + font-size: 12px; + line-height: 12px; + padding-left: 1px; + } +} diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/currency-input/tests/currency-input.component.test.js new file mode 100644 index 000000000..8de0ef863 --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.component.test.js @@ -0,0 +1,239 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import CurrencyInput from '../currency-input.component' +import UnitInput from '../../unit-input' +import CurrencyDisplay from '../../currency-display' + +describe('CurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UnitInput).length, 1) + }) + + it('should render properly with a suffix', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render properly with an ETH value', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD') + }) + + it('should render properly with a fiat value', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH') + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should call onChange and onBlur on input changes with the hex value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 0) + assert.equal(currencyInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000')) + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD') + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000')) + }) + + it('should call onChange and onBlur on input changes with the hex value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 0) + assert.equal(currencyInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('f602f2234d0ea')) + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH') + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('f602f2234d0ea')) + }) + + it('should change the state and pass in a new decimalValue when props.value changes', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = shallow( + + + + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).dive() + assert.equal(currencyInputInstance.state('decimalValue'), 0) + assert.equal(currencyInputInstance.state('hexValue'), undefined) + assert.equal(currencyInputInstance.find(UnitInput).props().value, 0) + + currencyInputInstance.setProps({ value: '1ec05e43e72400' }) + currencyInputInstance.update() + assert.equal(currencyInputInstance.state('decimalValue'), 2) + assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400') + assert.equal(currencyInputInstance.find(UnitInput).props().value, 2) + }) + }) +}) diff --git a/ui/app/components/currency-input/tests/currency-input.container.test.js b/ui/app/components/currency-input/tests/currency-input.container.test.js new file mode 100644 index 000000000..e77945e4d --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.container.test.js @@ -0,0 +1,55 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../currency-input.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('CurrencyInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + conversionRate: 280.45, + currentCurrency: 'usd', + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + conversionRate: 280.45, + currentCurrency: 'usd', + } + const mockDispatchProps = {} + + assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), { + conversionRate: 280.45, + currentCurrency: 'usd', + useFiat: true, + suffix: 'USD', + }) + + assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), { + conversionRate: 280.45, + currentCurrency: 'usd', + suffix: 'ETH', + }) + }) + }) +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 21b65bf55..bf34fd732 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -6,6 +6,10 @@ @import './confirm-page-container/index'; +@import './currency-input/index'; + +@import './currency-display/index'; + @import './error-message/index'; @import './export-text-container/index'; @@ -49,3 +53,5 @@ @import './app-header/index'; @import './sidebars/index'; + +@import './unit-input/index'; diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js index b082db1d0..b973f221c 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js +++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import CurrencyDisplay from '../../../currency-display' -import { ETH } from '../../../../constants/common' +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../constants/common' export default class CancelTransaction extends PureComponent { static propTypes = { @@ -13,15 +13,15 @@ export default class CancelTransaction extends PureComponent { return (
- -
) diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js index 994c2a577..014815503 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js +++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component' -import CurrencyDisplay from '../../../../currency-display' +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' describe('CancelTransactionGasFee Component', () => { it('should render', () => { @@ -13,12 +13,11 @@ describe('CancelTransactionGasFee Component', () => { ) assert.ok(wrapper) - assert.equal(wrapper.find(CurrencyDisplay).length, 2) - const ethDisplay = wrapper.find(CurrencyDisplay).at(0) - const fiatDisplay = wrapper.find(CurrencyDisplay).at(1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) + const ethDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(0) + const fiatDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(1) assert.equal(ethDisplay.props().value, '0x3b9aca00') - assert.equal(ethDisplay.props().currency, 'ETH') assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth') assert.equal(fiatDisplay.props().value, '0x3b9aca00') diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js index acaed383a..7f1fb4e49 100644 --- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -1,12 +1,15 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ConfirmTransactionBase from '../confirm-transaction-base' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' import { formatCurrency, convertTokenToFiat, addFiat, roundExponential, } from '../../../helpers/confirm-transaction/util' +import { getWeiHexFromDecimalValue } from '../../../helpers/conversions.util' +import { ETH, PRIMARY } from '../../../constants/common' export default class ConfirmTokenTransactionBase extends Component { static contextTypes = { @@ -36,19 +39,48 @@ export default class ConfirmTokenTransactionBase extends Component { }) } - getSubtitle () { - const { currentCurrency, contractExchangeRate } = this.props + renderSubtitleComponent () { + const { contractExchangeRate, tokenAmount } = this.props - if (typeof contractExchangeRate === 'undefined') { - return this.context.t('noConversionRateAvailable') - } else { - const fiatTransactionAmount = this.getFiatTransactionAmount() - const roundedFiatTransactionAmount = roundExponential(fiatTransactionAmount) - return formatCurrency(roundedFiatTransactionAmount, currentCurrency) - } + const decimalEthValue = (tokenAmount * contractExchangeRate) || 0 + const hexWeiValue = getWeiHexFromDecimalValue({ + value: decimalEthValue, + fromCurrency: ETH, + fromDenomination: ETH, + }) + + return typeof contractExchangeRate === 'undefined' + ? ( + + { this.context.t('noConversionRateAvailable') } + + ) : ( + + ) } - getFiatTotalTextOverride () { + renderPrimaryTotalTextOverride () { + const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props + const tokensText = `${tokenAmount} ${tokenSymbol}` + + return ( +
+ { `${tokensText} + ` } + + { ethTransactionTotal } +
+ ) + } + + getSecondaryTotalTextOverride () { const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props if (typeof contractExchangeRate === 'undefined') { @@ -67,7 +99,6 @@ export default class ConfirmTokenTransactionBase extends Component { tokenAddress, tokenSymbol, tokenAmount, - ethTransactionTotal, ...restProps } = this.props @@ -78,9 +109,9 @@ export default class ConfirmTokenTransactionBase extends Component { toAddress={toAddress} identiconAddress={tokenAddress} title={tokensText} - subtitle={this.getSubtitle()} - ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`} - fiatTotalTextOverride={this.getFiatTotalTextOverride()} + subtitleComponent={this.renderSubtitleComponent()} + primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()} + secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()} {...restProps} /> ) diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 707dad62d..c92867afe 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -1,7 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' -import { formatCurrency } from '../../../helpers/confirm-transaction/util' import { isBalanceSufficient } from '../../send/send.utils' import { DEFAULT_ROUTE } from '../../../routes' import { @@ -9,6 +8,8 @@ import { TRANSACTION_ERROR_KEY, } from '../../../constants/error-keys' import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' export default class ConfirmTransactionBase extends Component { static contextTypes = { @@ -36,7 +37,9 @@ export default class ConfirmTransactionBase extends Component { fiatTransactionTotal: PropTypes.string, fromAddress: PropTypes.string, fromName: PropTypes.string, - hexGasTotal: PropTypes.string, + hexTransactionAmount: PropTypes.string, + hexTransactionFee: PropTypes.string, + hexTransactionTotal: PropTypes.string, isTxReprice: PropTypes.bool, methodData: PropTypes.object, nonce: PropTypes.string, @@ -59,8 +62,8 @@ export default class ConfirmTransactionBase extends Component { detailsComponent: PropTypes.node, errorKey: PropTypes.string, errorMessage: PropTypes.string, - ethTotalTextOverride: PropTypes.string, - fiatTotalTextOverride: PropTypes.string, + primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + secondaryTotalTextOverride: PropTypes.string, hideData: PropTypes.bool, hideDetails: PropTypes.bool, hideSubtitle: PropTypes.bool, @@ -70,8 +73,10 @@ export default class ConfirmTransactionBase extends Component { onEditGas: PropTypes.func, onSubmit: PropTypes.func, subtitle: PropTypes.string, + subtitleComponent: PropTypes.node, summaryComponent: PropTypes.node, title: PropTypes.string, + titleComponent: PropTypes.node, valid: PropTypes.bool, warning: PropTypes.string, } @@ -105,7 +110,7 @@ export default class ConfirmTransactionBase extends Component { const { balance, conversionRate, - hexGasTotal, + hexTransactionFee, txData: { simulationFails, txParams: { @@ -116,7 +121,7 @@ export default class ConfirmTransactionBase extends Component { const insufficientBalance = balance && !isBalanceSufficient({ amount, - gasTotal: hexGasTotal || '0x0', + gasTotal: hexTransactionFee || '0x0', balance, conversionRate, }) @@ -153,13 +158,10 @@ export default class ConfirmTransactionBase extends Component { renderDetails () { const { detailsComponent, - fiatTransactionFee, - ethTransactionFee, - currentCurrency, - fiatTransactionTotal, - ethTransactionTotal, - fiatTotalTextOverride, - ethTotalTextOverride, + primaryTotalTextOverride, + secondaryTotalTextOverride, + hexTransactionFee, + hexTransactionTotal, hideDetails, } = this.props @@ -167,16 +169,13 @@ export default class ConfirmTransactionBase extends Component { return null } - const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency) - return ( detailsComponent || (
this.handleEditGas()} @@ -185,11 +184,12 @@ export default class ConfirmTransactionBase extends Component {
@@ -311,6 +311,43 @@ export default class ConfirmTransactionBase extends Component { } } + renderTitleComponent () { + const { title, titleComponent, hexTransactionAmount } = this.props + + // Title string passed in by props takes priority + if (title) { + return null + } + + return titleComponent || ( + + ) + } + + renderSubtitleComponent () { + const { subtitle, subtitleComponent, hexTransactionAmount } = this.props + + // Subtitle string passed in by props takes priority + if (subtitle) { + return null + } + + return subtitleComponent || ( + + ) + } + render () { const { isTxReprice, @@ -319,12 +356,9 @@ export default class ConfirmTransactionBase extends Component { toName, toAddress, methodData, - ethTransactionAmount, - fiatTransactionAmount, valid: propsValid = true, errorMessage, errorKey: propsErrorKey, - currentCurrency, action, title, subtitle, @@ -341,7 +375,6 @@ export default class ConfirmTransactionBase extends Component { const { submitting, submitError } = this.state const { name } = methodData - const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) const { valid, errorKey } = this.getErrorKey() return ( @@ -352,8 +385,10 @@ export default class ConfirmTransactionBase extends Component { toAddress={toAddress} showEdit={onEdit && !isTxReprice} action={action || name || this.context.t('unknownFunction')} - title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} - subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} + title={title} + titleComponent={this.renderTitleComponent()} + subtitle={subtitle} + subtitleComponent={this.renderSubtitleComponent()} hideSubtitle={hideSubtitle} summaryComponent={summaryComponent} detailsComponent={this.renderDetails()} diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index b34067686..c366d5137 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -36,7 +36,9 @@ const mapStateToProps = (state, props) => { fiatTransactionAmount, fiatTransactionFee, fiatTransactionTotal, - hexGasTotal, + hexTransactionAmount, + hexTransactionFee, + hexTransactionTotal, tokenData, methodData, txData, @@ -87,7 +89,9 @@ const mapStateToProps = (state, props) => { fiatTransactionAmount, fiatTransactionFee, fiatTransactionTotal, - hexGasTotal, + hexTransactionAmount, + hexTransactionFee, + hexTransactionTotal, txData, tokenData, methodData, diff --git a/ui/app/components/pages/settings/settings-tab/index.scss b/ui/app/components/pages/settings/settings-tab/index.scss index 76a0cec6f..3bf840c86 100644 --- a/ui/app/components/pages/settings/settings-tab/index.scss +++ b/ui/app/components/pages/settings/settings-tab/index.scss @@ -48,4 +48,22 @@ border-color: $ecstasy; } } + + &__radio-buttons { + display: flex; + align-items: center; + } + + &__radio-button { + display: flex; + align-items: center; + + &:not(:last-child) { + margin-right: 16px; + } + } + + &__radio-label { + padding-left: 4px; + } } diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js index 9da624f56..a9e2a723e 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js @@ -55,6 +55,8 @@ export default class SettingsTab extends PureComponent { sendHexData: PropTypes.bool, currentCurrency: PropTypes.string, conversionDate: PropTypes.number, + useETHAsPrimaryCurrency: PropTypes.bool, + setUseETHAsPrimaryCurrencyPreference: PropTypes.func, } state = { @@ -339,6 +341,56 @@ export default class SettingsTab extends PureComponent { ) } + renderUseEthAsPrimaryCurrency () { + const { t } = this.context + const { useETHAsPrimaryCurrency, setUseETHAsPrimaryCurrencyPreference } = this.props + + return ( +
+
+ { t('primaryCurrencySetting') } +
+ { t('primaryCurrencySettingDescription') } +
+
+
+
+
+
+ setUseETHAsPrimaryCurrencyPreference(true)} + checked={Boolean(useETHAsPrimaryCurrency)} + /> + +
+
+ setUseETHAsPrimaryCurrencyPreference(false)} + checked={!useETHAsPrimaryCurrency} + /> + +
+
+
+
+
+ ) + } + render () { const { warning, isMascara } = this.props @@ -346,6 +398,7 @@ export default class SettingsTab extends PureComponent {
{ warning &&
{ warning }
} { this.renderCurrentConversion() } + { this.renderUseEthAsPrimaryCurrency() } { this.renderCurrentLocale() } { this.renderNewRpcUrl() } { this.renderStateLogs() } diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js index 665b56f5c..de30f309c 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js @@ -11,7 +11,9 @@ import { updateCurrentLocale, setFeatureFlag, showModal, + setUseETHAsPrimaryCurrencyPreference, } from '../../../../actions' +import { preferencesSelector } from '../../../../selectors' const mapStateToProps = state => { const { appState: { warning }, metamask } = state @@ -24,6 +26,7 @@ const mapStateToProps = state => { isMascara, currentLocale, } = metamask + const { useETHAsPrimaryCurrency } = preferencesSelector(state) return { warning, @@ -34,6 +37,7 @@ const mapStateToProps = state => { useBlockie, sendHexData, provider, + useETHAsPrimaryCurrency, } } @@ -50,6 +54,9 @@ const mapDispatchToProps = dispatch => { }, setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), + setUseETHAsPrimaryCurrencyPreference: value => { + return dispatch(setUseETHAsPrimaryCurrencyPreference(value)) + }, } } diff --git a/ui/app/components/send/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js index 9f4a96e61..14bb7471f 100644 --- a/ui/app/components/send/account-list-item/account-list-item.component.js +++ b/ui/app/components/send/account-list-item/account-list-item.component.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { checksumAddress } from '../../../util' import Identicon from '../../identicon' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' export default class AccountListItem extends Component { @@ -25,8 +26,6 @@ export default class AccountListItem extends Component { const { account, className, - conversionRate, - currentCurrency, displayAddress = false, displayBalance = true, handleClick, @@ -57,16 +56,20 @@ export default class AccountListItem extends Component { { checksumAddress(address) }
} - {displayBalance && } + { + displayBalance && ( +
+ + +
+ ) + }
) } diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js index ef152d2e7..f88c0dbd0 100644 --- a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js @@ -4,7 +4,7 @@ import { shallow } from 'enzyme' import sinon from 'sinon' import proxyquire from 'proxyquire' import Identicon from '../../../identicon' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' const utilsMethodStubs = { checksumAddress: sinon.stub().returns('mockCheckSumAddress'), @@ -114,17 +114,11 @@ describe('AccountListItem Component', function () { it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { wrapper.setProps({ displayBalance: true }) - assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) assert.deepEqual( - wrapper.find(CurrencyDisplay).props(), + wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(), { - className: 'account-list-item__account-balances', - conversionRate: 4, - convertedBalanceClassName: 'account-list-item__account-secondary-balance', - convertedCurrency: 'mockCurrentyCurrency', - primaryBalanceClassName: 'account-list-item__account-primary-balance', - primaryCurrency: 'ETH', - readOnly: true, + type: 'PRIMARY', value: 'mockBalance', } ) @@ -132,7 +126,7 @@ describe('AccountListItem Component', function () { it('should not render a CurrencyDisplay if displayBalance is false', () => { wrapper.setProps({ displayBalance: false }) - assert.equal(wrapper.find(CurrencyDisplay).length, 0) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0) }) }) }) diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js deleted file mode 100644 index 2b8eaa41f..000000000 --- a/ui/app/components/send/currency-display/currency-display.js +++ /dev/null @@ -1,186 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') -const { removeLeadingZeroes } = require('../send.utils') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') -const ethUtil = require('ethereumjs-util') -const PropTypes = require('prop-types') - -CurrencyDisplay.contextTypes = { - t: PropTypes.func, -} - -module.exports = CurrencyDisplay - -inherits(CurrencyDisplay, Component) -function CurrencyDisplay () { - Component.call(this) -} - -function toHexWei (value) { - return conversionUtil(value, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - toDenomination: 'WEI', - }) -} - -CurrencyDisplay.prototype.componentWillMount = function () { - this.setState({ - valueToRender: this.getValueToRender(this.props), - }) -} - -CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) { - const currentValueToRender = this.getValueToRender(this.props) - const newValueToRender = this.getValueToRender(nextProps) - if (currentValueToRender !== newValueToRender) { - this.setState({ - valueToRender: newValueToRender, - }) - } -} - -CurrencyDisplay.prototype.getAmount = function (value) { - const { selectedToken } = this.props - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'}) - - return selectedToken - ? sendAmount - : toHexWei(value) -} - -CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { - if (value === '0x0') return readOnly ? '0' : '' - const { decimals, symbol } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - return selectedToken - ? conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - toCurrency: symbol, - conversionRate: multiplier, - invertConversionRate: true, - }) - : conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromDenomination: 'WEI', - numberOfDecimals: 9, - conversionRate, - }) -} - -CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { - const { primaryCurrency, convertedCurrency, conversionRate } = this.props - - if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { - if (nonFormattedValue !== 0) { - return null - } - } - - let convertedValue = conversionUtil(nonFormattedValue, { - fromNumericBase: 'dec', - fromCurrency: primaryCurrency, - toCurrency: convertedCurrency, - numberOfDecimals: 2, - conversionRate, - }) - - convertedValue = Number(convertedValue).toFixed(2) - const upperCaseCurrencyCode = convertedCurrency.toUpperCase() - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(convertedValue), { - code: upperCaseCurrencyCode, - }) - : convertedValue - } - -CurrencyDisplay.prototype.handleChange = function (newVal) { - this.setState({ valueToRender: removeLeadingZeroes(newVal) }) - this.props.onChange(this.getAmount(newVal)) -} - -CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { - const valueString = String(valueToRender) - const valueLength = valueString.length || 1 - const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 - return (valueLength + decimalPointDeficit + 0.75) + 'ch' -} - -CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) { - const { - convertedBalanceClassName = 'currency-display__converted-value', - convertedCurrency, - } = this.props - return h('div', { - className: convertedBalanceClassName, - }, convertedValueToRender == null - ? this.context.t('noConversionRateAvailable') - : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}` -) - } - -CurrencyDisplay.prototype.render = function () { - const { - className = 'currency-display', - primaryBalanceClassName = 'currency-display__input', - primaryCurrency, - readOnly = false, - inError = false, - onBlur, - step, - } = this.props - const { valueToRender } = this.state - - const convertedValueToRender = this.getConvertedValueToRender(valueToRender) - - return h('div', { - className, - style: { - borderColor: inError ? 'red' : null, - }, - onClick: () => { - this.currencyInput && this.currencyInput.focus() - }, - }, [ - - h('div.currency-display__primary-row', [ - - h('div.currency-display__input-wrapper', [ - - h('input', { - className: primaryBalanceClassName, - value: `${valueToRender}`, - placeholder: '0', - type: 'number', - readOnly, - ...(!readOnly ? { - onChange: e => this.handleChange(e.target.value), - onBlur: () => onBlur(this.getAmount(valueToRender)), - } : {}), - ref: input => { this.currencyInput = input }, - style: { - width: this.getInputWidth(valueToRender, readOnly), - }, - min: 0, - step, - }), - - h('span.currency-display__currency-symbol', primaryCurrency), - - ]), - - ]), this.onlyRenderConversions(convertedValueToRender), - - ]) - -} - diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js deleted file mode 100644 index 5dc269c5a..000000000 --- a/ui/app/components/send/currency-display/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './currency-display.js' diff --git a/ui/app/components/send/currency-display/tests/currency-display.test.js b/ui/app/components/send/currency-display/tests/currency-display.test.js deleted file mode 100644 index c9560b81c..000000000 --- a/ui/app/components/send/currency-display/tests/currency-display.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react' -import assert from 'assert' -import sinon from 'sinon' -import { shallow, mount } from 'enzyme' -import CurrencyDisplay from '../currency-display' - -describe('', () => { - - const token = { - address: '0xTest', - symbol: 'TST', - decimals: '13', - } - - it('retuns ETH value for wei value', () => { - const wrapper = mount(, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getValueToRender({ - // 1000000000000000000 - value: 'DE0B6B3A7640000', - }) - - assert.equal(value, 1) - }) - - it('returns value of token based on token decimals', () => { - const wrapper = mount(, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getValueToRender({ - selectedToken: token, - // 1000000000000000000 - value: 'DE0B6B3A7640000', - }) - - assert.equal(value, 100000) - }) - - it('returns hex value with decimal adjustment', () => { - - const wrapper = mount( - , {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getAmount(1) - // 10000000000000 - assert.equal(value, '9184e72a000') - }) - - it('#getConvertedValueToRender converts input value based on conversionRate', () => { - - const wrapper = mount( - , {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getConvertedValueToRender(32) - - assert.equal(value, 64) - }) - - it('#onlyRenderConversions renders single element for converted currency and value', () => { - const wrapper = mount( - , {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().onlyRenderConversions(10) - assert.equal(value.props.className, 'currency-display__converted-value') - assert.equal(value.props.children, '10 TEST') - }) - - it('simulates change value in input', () => { - const handleChangeSpy = sinon.spy() - - const wrapper = shallow( - , {context: {t: str => str + '_t'}}) - - const input = wrapper.find('input') - input.simulate('focus') - input.simulate('change', { target: { value: '100' } }) - - assert.equal(wrapper.state().valueToRender, '100') - assert.equal(wrapper.find('input').prop('value'), '100') - }) - -}) diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js index c548a5695..0268376bf 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper/' import AmountMaxButton from './amount-max-button/' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input' +import UserPreferencedTokenInput from '../../../user-preferenced-token-input' export default class SendAmountRow extends Component { @@ -84,16 +85,25 @@ export default class SendAmountRow extends Component { } } + renderInput () { + const { amount, inError, selectedToken } = this.props + const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput + + return ( + this.validateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} + error={inError} + value={amount} + /> + ) + } + render () { - const { - amount, - amountConversionRate, - convertedCurrency, - gasTotal, - inError, - primaryCurrency, - selectedToken, - } = this.props + const { gasTotal, inError } = this.props return ( {!inError && gasTotal && } - { - this.updateGas(newAmount) - this.updateAmount(newAmount) - }} - onChange={newAmount => this.validateAmount(newAmount)} - inError={inError} - primaryCurrency={primaryCurrency || 'ETH'} - selectedToken={selectedToken} - value={amount} - step="any" - /> + { this.renderInput() } ) } diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js index 8425e076e..e63db4a2d 100644 --- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -6,7 +6,7 @@ import SendAmountRow from '../send-amount-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import AmountMaxButton from '../amount-max-button/amount-max-button.container' -import CurrencyDisplay from '../../../currency-display' +import UserPreferencedTokenInput from '../../../../user-preferenced-token-input' const propsMethodSpies = { setMaxModeTo: sinon.spy(), @@ -150,26 +150,19 @@ describe('SendAmountRow Component', function () { assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) }) - it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay)) + it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => { + console.log('HI', wrapper.find(SendRowWrapper).childAt(1)) + assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput)) }) - it('should render the CurrencyDisplay with the correct props', () => { + it('should render the UserPreferencedTokenInput with the correct props', () => { const { - conversionRate, - convertedCurrency, onBlur, onChange, - inError, - primaryCurrency, - selectedToken, + error, value, } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(conversionRate, 'mockAmountConversionRate') - assert.equal(convertedCurrency, 'mockConvertedCurrency') - assert.equal(inError, false) - assert.equal(primaryCurrency, 'mockPrimaryCurrency') - assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) + assert.equal(error, false) assert.equal(value, 'mockAmount') assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) @@ -192,11 +185,5 @@ describe('SendAmountRow Component', function () { ['mockNewAmount'] ) }) - - it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => { - wrapper.setProps({ primaryCurrency: null }) - const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(primaryCurrency, 'ETH') - }) }) }) diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index bb9a94428..9bbb67506 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' -import CurrencyDisplay from '../../../../send/currency-display' - +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../../constants/common' export default class GasFeeDisplay extends Component { @@ -19,27 +19,24 @@ export default class GasFeeDisplay extends Component { }; render () { - const { - conversionRate, - gasTotal, - onClick, - primaryCurrency = 'ETH', - convertedCurrency, - gasLoadingError, - } = this.props + const { gasTotal, onClick, gasLoadingError } = this.props return (
{gasTotal - ? + ? ( +
+ + +
+ ) : gasLoadingError ?
{this.context.t('setGasPrice')} diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index 7cbe8d0df..9ff01493a 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import {shallow} from 'enzyme' import GasFeeDisplay from '../gas-fee-display.component' -import CurrencyDisplay from '../../../../../send/currency-display' +import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display' import sinon from 'sinon' @@ -29,17 +29,15 @@ describe('SendGasRow Component', function () { describe('render', () => { it('should render a CurrencyDisplay component', () => { - assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) }) it('should render the CurrencyDisplay with the correct props', () => { const { - conversionRate, - convertedCurrency, + type, value, - } = wrapper.find(CurrencyDisplay).props() - assert.equal(conversionRate, 20) - assert.equal(convertedCurrency, 'mockConvertedCurrency') + } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props() + assert.equal(type, 'PRIMARY') assert.equal(value, 'mockGasTotal') }) diff --git a/ui/app/components/token-input/index.js b/ui/app/components/token-input/index.js new file mode 100644 index 000000000..22c06111e --- /dev/null +++ b/ui/app/components/token-input/index.js @@ -0,0 +1 @@ +export { default } from './token-input.container' diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/token-input/tests/token-input.component.test.js new file mode 100644 index 000000000..2131e7705 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.component.test.js @@ -0,0 +1,308 @@ +import React from 'react' +import PropTypes from 'prop-types' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import TokenInput from '../token-input.component' +import UnitInput from '../../unit-input' +import CurrencyDisplay from '../../currency-display' + +describe('TokenInput Component', () => { + const t = key => `translate ${key}` + + describe('rendering', () => { + it('should render properly without a token', () => { + const wrapper = shallow( + , + { context: { t } } + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UnitInput).length, 1) + }) + + it('should render properly with a token', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + , + { context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.currency-input__conversion-component').length, 1) + assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable') + }) + + it('should render properly with a token and selectedTokenExchangeRate', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + , + { context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render properly with a token value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH') + }) + + it('should render properly with a token value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD') + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should call onChange and onBlur on input changes with the hex value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 0) + assert.equal(tokenInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('2710')) + assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH') + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('2710')) + }) + + it('should call onChange and onBlur on input changes with the hex value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + + + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 0) + assert.equal(tokenInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('2710')) + assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD') + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('2710')) + }) + + it('should change the state and pass in a new decimalValue when props.value changes', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = shallow( + + + + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).dive() + assert.equal(tokenInputInstance.state('decimalValue'), 0) + assert.equal(tokenInputInstance.state('hexValue'), undefined) + assert.equal(tokenInputInstance.find(UnitInput).props().value, 0) + + tokenInputInstance.setProps({ value: '2710' }) + tokenInputInstance.update() + assert.equal(tokenInputInstance.state('decimalValue'), 1) + assert.equal(tokenInputInstance.state('hexValue'), '2710') + assert.equal(tokenInputInstance.find(UnitInput).props().value, 1) + }) + }) +}) diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/token-input/tests/token-input.container.test.js new file mode 100644 index 000000000..d73bc9a94 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.container.test.js @@ -0,0 +1,129 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../token-input.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('TokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props when send is empty', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: {}, + send: {}, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 0, + }) + }) + + it('should return the correct props when selectedTokenAddress is not found and send is populated', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x2', + contractExchangeRates: {}, + send: { + token: { address: 'test' }, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: 'test', + }, + selectedTokenExchangeRate: 0, + }) + }) + + it('should return the correct props when contractExchangeRates is populated', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: { + '0x1': 5, + }, + send: {}, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + } + + assert.deepEqual(mergeProps(mockStateProps, {}, {}), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + suffix: 'ABC', + }) + }) + }) +}) diff --git a/ui/app/components/token-input/token-input.component.js b/ui/app/components/token-input/token-input.component.js new file mode 100644 index 000000000..d1388945b --- /dev/null +++ b/ui/app/components/token-input/token-input.component.js @@ -0,0 +1,136 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import UnitInput from '../unit-input' +import CurrencyDisplay from '../currency-display' +import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util' +import ethUtil from 'ethereumjs-util' +import { conversionUtil, multiplyCurrencies } from '../../conversion-util' +import { ETH } from '../../constants/common' + +/** + * Component that allows user to enter token values as a number, and props receive a converted + * hex value. props.value, used as a default or forced value, should be a hex value, which + * gets converted into a decimal value. + */ +export default class TokenInput extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + currentCurrency: PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + value: PropTypes.string, + suffix: PropTypes.string, + showFiat: PropTypes.bool, + selectedToken: PropTypes.object, + selectedTokenExchangeRate: PropTypes.number, + } + + constructor (props) { + super(props) + + const { value: hexValue } = props + const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + + this.state = { + decimalValue, + hexValue, + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsHexValue } = prevProps + const { value: propsHexValue } = this.props + const { hexValue: stateHexValue } = this.state + + if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { + const decimalValue = this.getDecimalValue(this.props) + this.setState({ hexValue: propsHexValue, decimalValue }) + } + } + + getDecimalValue (props) { + const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props + + const multiplier = Math.pow(10, Number(decimals || 0)) + const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + toCurrency: symbol, + conversionRate: multiplier, + invertConversionRate: true, + }) + + return Number(decimalValueString) || 0 + } + + handleChange = decimalValue => { + const { selectedToken: { decimals } = {}, onChange } = this.props + + const multiplier = Math.pow(10, Number(decimals || 0)) + const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' }) + + this.setState({ hexValue, decimalValue }) + onChange(hexValue) + } + + handleBlur = () => { + this.props.onBlur(this.state.hexValue) + } + + renderConversionComponent () { + const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props + const { decimalValue } = this.state + let currency, numberOfDecimals + + if (showFiat) { + // Display Fiat + currency = currentCurrency + numberOfDecimals = 2 + } else { + // Display ETH + currency = ETH + numberOfDecimals = 6 + } + + const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0 + const hexWeiValue = getWeiHexFromDecimalValue({ + value: decimalEthValue, + fromCurrency: ETH, + fromDenomination: ETH, + }) + + return selectedTokenExchangeRate + ? ( + + ) : ( +
+ { this.context.t('noConversionRateAvailable') } +
+ ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + + { this.renderConversionComponent() } + + ) + } +} diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/token-input/token-input.container.js new file mode 100644 index 000000000..ec233b1b8 --- /dev/null +++ b/ui/app/components/token-input/token-input.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import TokenInput from './token-input.component' +import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors' + +const mapStateToProps = state => { + const { metamask: { currentCurrency } } = state + + return { + currentCurrency, + selectedToken: getSelectedToken(state), + selectedTokenExchangeRate: getSelectedTokenExchangeRate(state), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { selectedToken } = stateProps + const suffix = selectedToken && selectedToken.symbol + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + suffix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(TokenInput) diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js index 5a2b4a481..77bedcad7 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js @@ -4,8 +4,9 @@ import classnames from 'classnames' import TransactionBreakdownRow from './transaction-breakdown-row' import Card from '../card' import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import HexToDecimal from '../hex-to-decimal' -import { ETH, GWEI } from '../../constants/common' +import { ETH, GWEI, PRIMARY, SECONDARY } from '../../constants/common' import { getHexGasTotal } from '../../helpers/confirm-transaction/util' import { sumHexes } from '../../helpers/transactions.util' @@ -40,9 +41,9 @@ export default class TransactionBreakdown extends PureComponent { className="transaction-breakdown__card" > - @@ -79,14 +80,14 @@ export default class TransactionBreakdown extends PureComponent {
- -
diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js index 40eef5e15..88573d2d5 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js @@ -4,12 +4,12 @@ import classnames from 'classnames' import Identicon from '../identicon' import TransactionStatus from '../transaction-status' import TransactionAction from '../transaction-action' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import TokenCurrencyDisplay from '../token-currency-display' import TransactionListItemDetails from '../transaction-list-item-details' import { CONFIRM_TRANSACTION_ROUTE } from '../../routes' import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions' -import { ETH } from '../../constants/common' +import { PRIMARY, SECONDARY } from '../../constants/common' import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums' import { getStatusKey } from '../../helpers/transactions.util' @@ -103,12 +103,11 @@ export default class TransactionListItem extends PureComponent { prefix="-" /> ) : ( - ) } @@ -119,10 +118,11 @@ export default class TransactionListItem extends PureComponent { return token ? null : ( - ) } diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js index bb95cb27e..513a8aac9 100644 --- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js +++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js @@ -3,7 +3,7 @@ import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' import TokenBalance from '../../token-balance' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' import { SEND_ROUTE } from '../../../routes' import TransactionViewBalance from '../transaction-view-balance.component' @@ -35,7 +35,7 @@ describe('TransactionViewBalance Component', () => { assert.equal(wrapper.find('.transaction-view-balance').length, 1) assert.equal(wrapper.find('.transaction-view-balance__button').length, 2) - assert.equal(wrapper.find(CurrencyDisplay).length, 2) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) const buttons = wrapper.find('.transaction-view-balance__buttons') assert.equal(propsMethodSpies.showDepositModal.callCount, 0) diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js index 1b7a29c87..273845c47 100644 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import Button from '../button' import Identicon from '../identicon' import TokenBalance from '../token-balance' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import { SEND_ROUTE } from '../../routes' -import { ETH } from '../../constants/common' +import { PRIMARY, SECONDARY } from '../../constants/common' export default class TransactionViewBalance extends PureComponent { static contextTypes = { @@ -33,15 +33,17 @@ export default class TransactionViewBalance extends PureComponent { /> ) : (
- -
) diff --git a/ui/app/components/unit-input/index.js b/ui/app/components/unit-input/index.js new file mode 100644 index 000000000..7c33c9e5c --- /dev/null +++ b/ui/app/components/unit-input/index.js @@ -0,0 +1 @@ +export { default } from './unit-input.component' diff --git a/ui/app/components/unit-input/index.scss b/ui/app/components/unit-input/index.scss new file mode 100644 index 000000000..28c5bf6f0 --- /dev/null +++ b/ui/app/components/unit-input/index.scss @@ -0,0 +1,44 @@ +.unit-input { + min-height: 54px; + border: 1px solid #dedede; + border-radius: 4px; + background-color: #fff; + color: #4d4d4d; + font-size: 1rem; + padding: 8px 10px; + position: relative; + + input[type="number"] { + -moz-appearance: textfield; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + &__input { + color: #4d4d4d; + font-size: 1rem; + font-family: Roboto; + border: none; + outline: 0 !important; + max-width: 22ch; + } + + &__input-container { + display: flex; + align-items: center; + } + + &--error { + border-color: $red; + } +} diff --git a/ui/app/components/unit-input/tests/unit-input.component.test.js b/ui/app/components/unit-input/tests/unit-input.component.test.js new file mode 100644 index 000000000..97d987bc7 --- /dev/null +++ b/ui/app/components/unit-input/tests/unit-input.component.test.js @@ -0,0 +1,146 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import UnitInput from '../unit-input.component' + +describe('UnitInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 0) + }) + + it('should render properly with a suffix', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + }) + + it('should render properly with a child omponent', () => { + const wrapper = shallow( + +
+ TESTCOMPONENT +
+
+ ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.testing').length, 1) + assert.equal(wrapper.find('.testing').text(), 'TESTCOMPONENT') + }) + + it('should render with an error class when props.error === true', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input--error').length, 1) + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should focus the input on component click', () => { + const wrapper = mount( + + ) + + assert.ok(wrapper) + const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus') + wrapper.instance().forceUpdate() + wrapper.update() + assert.equal(handleFocusSpy.callCount, 0) + wrapper.find('.unit-input').simulate('click') + assert.equal(handleFocusSpy.callCount, 1) + }) + + it('should call onChange on input changes with the value', () => { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + }) + + it('should call onBlur on blur with the value', () => { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith(123)) + }) + + it('should set the component state value with props.value', () => { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.state('value'), 123) + }) + + it('should update the component state value with props.value', () => { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(wrapper.state('value'), 123) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + wrapper.setProps({ value: 456 }) + assert.equal(wrapper.state('value'), 456) + assert.equal(handleChangeSpy.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/unit-input/unit-input.component.js b/ui/app/components/unit-input/unit-input.component.js new file mode 100644 index 000000000..f1ebf4d77 --- /dev/null +++ b/ui/app/components/unit-input/unit-input.component.js @@ -0,0 +1,104 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { removeLeadingZeroes } from '../send/send.utils' + +/** + * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also + * allows rendering a child component underneath the input to, for example, display conversions of + * the shown suffix. + */ +export default class UnitInput extends PureComponent { + static propTypes = { + children: PropTypes.node, + error: PropTypes.bool, + onBlur: PropTypes.func, + onChange: PropTypes.func, + placeholder: PropTypes.string, + suffix: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + } + + static defaultProps = { + placeholder: '0', + } + + constructor (props) { + super(props) + + this.state = { + value: props.value || '', + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsValue } = prevProps + const { value: propsValue } = this.props + const { value: stateValue } = this.state + + if (prevPropsValue !== propsValue && propsValue !== stateValue) { + this.setState({ value: propsValue }) + } + } + + handleFocus = () => { + this.unitInput.focus() + } + + handleChange = event => { + const { value: userInput } = event.target + let value = userInput + + if (userInput.length && userInput.length > 1) { + value = removeLeadingZeroes(userInput) + } + + this.setState({ value }) + this.props.onChange(value) + } + + handleBlur = event => { + const { onBlur } = this.props + typeof onBlur === 'function' && onBlur(this.state.value) + } + + getInputWidth (value) { + const valueString = String(value) + const valueLength = valueString.length || 1 + const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 + return (valueLength + decimalPointDeficit + 0.75) + 'ch' + } + + render () { + const { error, placeholder, suffix, children } = this.props + const { value } = this.state + + return ( +
+
+ { this.unitInput = ref }} + /> + { + suffix && ( +
+ { suffix } +
+ ) + } +
+ { children } +
+ ) + } +} diff --git a/ui/app/components/user-preferenced-currency-display/index.js b/ui/app/components/user-preferenced-currency-display/index.js new file mode 100644 index 000000000..0deddaecf --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-currency-display.container' diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js new file mode 100644 index 000000000..ead584c26 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js @@ -0,0 +1,34 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component' +import CurrencyDisplay from '../../currency-display' + +describe('UserPreferencedCurrencyDisplay Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should pass all props to the CurrencyDisplay child component', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true) + assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test') + assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js new file mode 100644 index 000000000..41ad3b73e --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js @@ -0,0 +1,105 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../user-preferenced-currency-display.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('UserPreferencedCurrencyDisplay container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useETHAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useETHAsPrimaryCurrency: true, + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockDispatchProps = {} + + const tests = [ + { + stateProps: { + useETHAsPrimaryCurrency: true, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: 'ETH', + numberOfDecimals: 6, + prefix: undefined, + }, + }, + { + stateProps: { + useETHAsPrimaryCurrency: false, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: undefined, + numberOfDecimals: 2, + prefix: undefined, + }, + }, + { + stateProps: { + useETHAsPrimaryCurrency: true, + }, + ownProps: { + type: 'SECONDARY', + fiatNumberOfDecimals: 4, + fiatPrefix: '-', + }, + result: { + currency: undefined, + numberOfDecimals: 4, + prefix: '-', + }, + }, + { + stateProps: { + useETHAsPrimaryCurrency: false, + }, + ownProps: { + type: 'SECONDARY', + fiatNumberOfDecimals: 4, + numberOfDecimals: 3, + fiatPrefix: 'a', + prefix: 'b', + }, + result: { + currency: 'ETH', + numberOfDecimals: 3, + prefix: 'b', + }, + }, + ] + + tests.forEach(({ stateProps, ownProps, result }) => { + assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), { + ...result, + }) + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js new file mode 100644 index 000000000..4d948ca6a --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { PRIMARY, SECONDARY, ETH } from '../../constants/common' +import CurrencyDisplay from '../currency-display' + +export default class UserPreferencedCurrencyDisplay extends PureComponent { + static propTypes = { + className: PropTypes.string, + prefix: PropTypes.string, + value: PropTypes.string, + numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hideLabel: PropTypes.bool, + style: PropTypes.object, + showEthLogo: PropTypes.bool, + ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + // Used in container + type: PropTypes.oneOf([PRIMARY, SECONDARY]), + ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ethPrefix: PropTypes.string, + fiatPrefix: PropTypes.string, + // From container + currency: PropTypes.string, + } + + renderEthLogo () { + const { currency, showEthLogo, ethLogoHeight = 12 } = this.props + + return currency === ETH && showEthLogo && ( + + ) + } + + render () { + return ( + + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js new file mode 100644 index 000000000..23240c649 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js @@ -0,0 +1,52 @@ +import { connect } from 'react-redux' +import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component' +import { preferencesSelector } from '../../selectors' +import { ETH, PRIMARY, SECONDARY } from '../../constants/common' + +const mapStateToProps = (state, ownProps) => { + const { useETHAsPrimaryCurrency } = preferencesSelector(state) + + return { + useETHAsPrimaryCurrency, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { useETHAsPrimaryCurrency, ...restStateProps } = stateProps + const { + type, + numberOfDecimals: propsNumberOfDecimals, + ethNumberOfDecimals, + fiatNumberOfDecimals, + ethPrefix, + fiatPrefix, + prefix: propsPrefix, + ...restOwnProps + } = ownProps + + let currency, numberOfDecimals, prefix + + if (type === PRIMARY && useETHAsPrimaryCurrency || + type === SECONDARY && !useETHAsPrimaryCurrency) { + // Display ETH + currency = ETH + numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6 + prefix = propsPrefix || ethPrefix + } else if (type === SECONDARY && useETHAsPrimaryCurrency || + type === PRIMARY && !useETHAsPrimaryCurrency) { + // Display Fiat + numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2 + prefix = propsPrefix || fiatPrefix + } + + return { + ...restStateProps, + ...dispatchProps, + ...restOwnProps, + currency, + numberOfDecimals, + prefix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay) diff --git a/ui/app/components/user-preferenced-currency-input/index.js b/ui/app/components/user-preferenced-currency-input/index.js new file mode 100644 index 000000000..4dc70db3d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-currency-input.container' diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js new file mode 100644 index 000000000..0af80a03d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js @@ -0,0 +1,32 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component' +import CurrencyInput from '../../currency-input' + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyInput).length, 1) + }) + + it('should render useFiat for CurrencyInput based on preferences.useETHAsPrimaryCurrency', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyInput).length, 1) + assert.equal(wrapper.find(CurrencyInput).props().useFiat, false) + wrapper.setProps({ useETHAsPrimaryCurrency: false }) + assert.equal(wrapper.find(CurrencyInput).props().useFiat, true) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js new file mode 100644 index 000000000..d860c38da --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js @@ -0,0 +1,31 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../user-preferenced-currency-input.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('UserPreferencedCurrencyInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useETHAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useETHAsPrimaryCurrency: true, + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js new file mode 100644 index 000000000..6e0e00a1d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import CurrencyInput from '../currency-input' + +export default class UserPreferencedCurrencyInput extends PureComponent { + static propTypes = { + useETHAsPrimaryCurrency: PropTypes.bool, + } + + render () { + const { useETHAsPrimaryCurrency, ...restProps } = this.props + + return ( + + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js new file mode 100644 index 000000000..397cdc7cc --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component' +import { preferencesSelector } from '../../selectors' + +const mapStateToProps = state => { + const { useETHAsPrimaryCurrency } = preferencesSelector(state) + + return { + useETHAsPrimaryCurrency, + } +} + +export default connect(mapStateToProps)(UserPreferencedCurrencyInput) diff --git a/ui/app/components/user-preferenced-token-input/index.js b/ui/app/components/user-preferenced-token-input/index.js new file mode 100644 index 000000000..54167e633 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-token-input.container' diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js new file mode 100644 index 000000000..910c7089f --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js @@ -0,0 +1,32 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import UserPreferencedTokenInput from '../user-preferenced-token-input.component' +import TokenInput from '../../token-input' + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(TokenInput).length, 1) + }) + + it('should render showFiat for TokenInput based on preferences.useETHAsPrimaryCurrency', () => { + const wrapper = shallow( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(TokenInput).length, 1) + assert.equal(wrapper.find(TokenInput).props().showFiat, false) + wrapper.setProps({ useETHAsPrimaryCurrency: false }) + assert.equal(wrapper.find(TokenInput).props().showFiat, true) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js new file mode 100644 index 000000000..e3509149a --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js @@ -0,0 +1,31 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../user-preferenced-token-input.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('UserPreferencedTokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useETHAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useETHAsPrimaryCurrency: true, + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js new file mode 100644 index 000000000..f2b537f11 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import TokenInput from '../token-input' + +export default class UserPreferencedTokenInput extends PureComponent { + static propTypes = { + useETHAsPrimaryCurrency: PropTypes.bool, + } + + render () { + const { useETHAsPrimaryCurrency, ...restProps } = this.props + + return ( + + ) + } +} diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js new file mode 100644 index 000000000..416d069dd --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import UserPreferencedTokenInput from './user-preferenced-token-input.component' +import { preferencesSelector } from '../../selectors' + +const mapStateToProps = state => { + const { useETHAsPrimaryCurrency } = preferencesSelector(state) + + return { + useETHAsPrimaryCurrency, + } +} + +export default connect(mapStateToProps)(UserPreferencedTokenInput) diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js index a20f6cc02..4ff4dc837 100644 --- a/ui/app/constants/common.js +++ b/ui/app/constants/common.js @@ -1,3 +1,6 @@ export const ETH = 'ETH' export const GWEI = 'GWEI' export const WEI = 'WEI' + +export const PRIMARY = 'PRIMARY' +export const SECONDARY = 'SECONDARY' diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index 30c32f2bf..2ceafbe08 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -14,7 +14,13 @@ import { hexGreaterThan, } from '../helpers/confirm-transaction/util' -import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util' +import { + getTokenData, + getMethodData, + isSmartContractAddress, + sumHexes, +} from '../helpers/transactions.util' + import { getSymbolAndDecimals } from '../token-util' import { conversionUtil } from '../conversion-util' @@ -31,7 +37,6 @@ const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION') const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS') const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES') const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS') -const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL') const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS') const UPDATE_NONCE = createActionType('UPDATE_NONCE') const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT') @@ -53,7 +58,9 @@ const initState = { ethTransactionAmount: '', ethTransactionFee: '', ethTransactionTotal: '', - hexGasTotal: '', + hexTransactionAmount: '', + hexTransactionFee: '', + hexTransactionTotal: '', nonce: '', toSmartContract: false, fetchingData: false, @@ -99,30 +106,28 @@ export default function reducer ({ confirmTransaction: confirmState = initState methodData: {}, } case UPDATE_TRANSACTION_AMOUNTS: - const { fiatTransactionAmount, ethTransactionAmount } = action.payload + const { fiatTransactionAmount, ethTransactionAmount, hexTransactionAmount } = action.payload return { ...confirmState, fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount, ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount, + hexTransactionAmount: hexTransactionAmount || confirmState.hexTransactionAmount, } case UPDATE_TRANSACTION_FEES: - const { fiatTransactionFee, ethTransactionFee } = action.payload + const { fiatTransactionFee, ethTransactionFee, hexTransactionFee } = action.payload return { ...confirmState, fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee, ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee, + hexTransactionFee: hexTransactionFee || confirmState.hexTransactionFee, } case UPDATE_TRANSACTION_TOTALS: - const { fiatTransactionTotal, ethTransactionTotal } = action.payload + const { fiatTransactionTotal, ethTransactionTotal, hexTransactionTotal } = action.payload return { ...confirmState, fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal, ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal, - } - case UPDATE_HEX_GAS_TOTAL: - return { - ...confirmState, - hexGasTotal: action.payload, + hexTransactionTotal: hexTransactionTotal || confirmState.hexTransactionTotal, } case UPDATE_TOKEN_PROPS: const { tokenSymbol = '', tokenDecimals = '' } = action.payload @@ -222,13 +227,6 @@ export function updateTransactionTotals (totals) { } } -export function updateHexGasTotal (hexGasTotal) { - return { - type: UPDATE_HEX_GAS_TOTAL, - payload: hexGasTotal, - } -} - export function updateTokenProps (tokenProps) { return { type: UPDATE_TOKEN_PROPS, @@ -297,7 +295,7 @@ export function updateTxDataAndCalculate (txData) { dispatch(updateTxData(txData)) - const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData + const { txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData const fiatTransactionAmount = getValueFromWeiHex({ value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, @@ -306,31 +304,39 @@ export function updateTxDataAndCalculate (txData) { value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6, }) - dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount })) + dispatch(updateTransactionAmounts({ + fiatTransactionAmount, + ethTransactionAmount, + hexTransactionAmount: value, + })) - const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice }) - - dispatch(updateHexGasTotal(hexGasTotal)) + const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice }) const fiatTransactionFee = getTransactionFee({ - value: hexGasTotal, + value: hexTransactionFee, toCurrency: currentCurrency, numberOfDecimals: 2, conversionRate, }) const ethTransactionFee = getTransactionFee({ - value: hexGasTotal, + value: hexTransactionFee, toCurrency: 'ETH', numberOfDecimals: 6, conversionRate, }) - dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee })) + dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee, hexTransactionFee })) const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount) const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount) + console.log('HIHIH', value, hexTransactionFee) + const hexTransactionTotal = sumHexes(value, hexTransactionFee) - dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal })) + dispatch(updateTransactionTotals({ + fiatTransactionTotal, + ethTransactionTotal, + hexTransactionTotal, + })) } } diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js index 1bab0add0..eceacd0bd 100644 --- a/ui/app/ducks/tests/confirm-transaction.duck.test.js +++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js @@ -19,7 +19,9 @@ const initialState = { ethTransactionAmount: '', ethTransactionFee: '', ethTransactionTotal: '', - hexGasTotal: '', + hexTransactionAmount: '', + hexTransactionFee: '', + hexTransactionTotal: '', nonce: '', toSmartContract: false, fetchingData: false, @@ -34,7 +36,6 @@ const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA' const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS' const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES' const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS' -const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL' const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS' const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE' const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT' @@ -65,7 +66,9 @@ describe('Confirm Transaction Duck', () => { ethTransactionAmount: '1', ethTransactionFee: '0.000021', ethTransactionTotal: '469.27', - hexGasTotal: '0x1319718a5000', + hexTransactionAmount: '', + hexTransactionFee: '0x1319718a5000', + hexTransactionTotal: '', nonce: '0x0', toSmartContract: false, fetchingData: false, @@ -186,12 +189,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionAmount: '123.45', ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionAmount: '123.45', ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', } ) }) @@ -203,12 +208,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionFee: '123.45', ethTransactionFee: '.5', + hexTransactionFee: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionFee: '123.45', ethTransactionFee: '.5', + hexTransactionFee: '0x1', } ) }) @@ -220,25 +227,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionTotal: '123.45', ethTransactionTotal: '.5', + hexTransactionTotal: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionTotal: '123.45', ethTransactionTotal: '.5', - } - ) - }) - - it('should update hexGasTotal when receiving an UPDATE_HEX_GAS_TOTAL action', () => { - assert.deepEqual( - ConfirmTransactionReducer(mockState, { - type: UPDATE_HEX_GAS_TOTAL, - payload: '0x0', - }), - { - ...mockState.confirmTransaction, - hexGasTotal: '0x0', + hexTransactionTotal: '0x1', } ) }) @@ -435,19 +431,6 @@ describe('Confirm Transaction Duck', () => { ) }) - it('should create an action to update hexGasTotal', () => { - const hexGasTotal = '0x0' - const expectedAction = { - type: UPDATE_HEX_GAS_TOTAL, - payload: hexGasTotal, - } - - assert.deepEqual( - actions.updateHexGasTotal(hexGasTotal), - expectedAction - ) - }) - it('should create an action to update tokenProps', () => { const tokenProps = { tokenDecimals: '1', @@ -568,7 +551,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] @@ -637,7 +619,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] @@ -687,7 +668,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js index 20ef9e35b..777537e1e 100644 --- a/ui/app/helpers/conversions.util.js +++ b/ui/app/helpers/conversions.util.js @@ -61,3 +61,22 @@ export function getValueFromWeiHex ({ conversionRate, }) } + +export function getWeiHexFromDecimalValue ({ + value, + fromCurrency, + conversionRate, + fromDenomination, + invertConversionRate, +}) { + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + toCurrency: ETH, + fromCurrency, + conversionRate, + invertConversionRate, + fromDenomination, + toDenomination: WEI, + }) +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 3f1d3394f..37d8a9187 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -51,6 +51,9 @@ function reduceMetamask (state, action) { isRevealingSeedWords: false, welcomeScreenSeen: false, currentLocale: '', + preferences: { + useETHAsPrimaryCurrency: true, + }, }, state.metamask) switch (action.type) { @@ -365,6 +368,12 @@ function reduceMetamask (state, action) { }) } + case actions.UPDATE_PREFERENCES: { + return extend(metamaskState, { + preferences: { ...action.payload }, + }) + } + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js index fb4517628..9f11551be 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -33,6 +33,7 @@ const selectors = { getSendMaxModeState, getCurrentViewContext, getTotalUnapprovedCount, + preferencesSelector, } module.exports = selectors @@ -195,3 +196,7 @@ function getTotalUnapprovedCount ({ metamask }) { return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedTypedMessagesCount } + +function preferencesSelector ({ metamask }) { + return metamask.preferences +} From 97b914abea62a5e2ced1ecd40285e12325525fb9 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Wed, 17 Oct 2018 15:37:31 -0230 Subject: [PATCH 07/19] Delete add-token integration test --- test/integration/lib/add-token.js | 140 ------------------------------ 1 file changed, 140 deletions(-) delete mode 100644 test/integration/lib/add-token.js diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js deleted file mode 100644 index bb9d0d10f..000000000 --- a/test/integration/lib/add-token.js +++ /dev/null @@ -1,140 +0,0 @@ -const reactTriggerChange = require('react-trigger-change') -const { - timeout, - queryAsync, - findAsync, -} = require('../../lib/util') - -QUnit.module('Add token flow') - -QUnit.test('successful add token flow', (assert) => { - const done = assert.async() - runAddTokenFlowTest(assert) - .then(done) - .catch(err => { - assert.notOk(err, `Error was thrown: ${err.stack}`) - done() - }) -}) - -async function runAddTokenFlowTest (assert, done) { - const selectState = await queryAsync($, 'select') - selectState.val('add token') - reactTriggerChange(selectState[0]) - - // Used to set values on TextField input component - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - ).set - - // Check that no tokens have been added - assert.ok($('.token-list-item').length === 0, 'no tokens added') - - // Go to Add Token screen - let addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') - assert.ok(addTokenButton[0], 'add token button present') - addTokenButton[0].click() - - // Verify Add Token screen - let addTokenWrapper = await queryAsync($, '.page-container') - assert.ok(addTokenWrapper[0], 'add token wrapper renders') - - let addTokenTitle = await queryAsync($, '.page-container__title') - assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') - - // Cancel Add Token - const cancelAddTokenButton = await queryAsync($, 'button.btn-default.btn--large.page-container__footer-button') - assert.ok(cancelAddTokenButton[0], 'cancel add token button present') - cancelAddTokenButton.click() - - assert.ok($('.wallet-view')[0], 'cancelled and returned to account detail wallet view') - - // Return to Add Token Screen - addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') - assert.ok(addTokenButton[0], 'add token button present') - addTokenButton[0].click() - - // Verify Add Token Screen - addTokenWrapper = await queryAsync($, '.page-container') - addTokenTitle = await queryAsync($, '.page-container__title') - assert.ok(addTokenWrapper[0], 'add token wrapper renders') - assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') - - // Search for token - const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0] - searchInput.focus() - await timeout(1000) - nativeInputValueSetter.call(searchInput, 'a') - searchInput.dispatchEvent(new Event('input', { bubbles: true})) - - // Click token to add - const tokenWrapper = await queryAsync($, 'div.token-list__token') - assert.ok(tokenWrapper[0], 'token found') - const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image') - const tokenImageUrl = tokenImageProp.slice(5, -2) - tokenWrapper[0].click() - - // Click Next button - const 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.btn--large')[0], 'confirm add token button found') - $('button.btn-primary.btn--large')[0].click() - - // Verify added token image - let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container') - assert.ok(heroBalance, 'rendered hero balance') - assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added') - - // Return to Add Token Screen - addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') - assert.ok(addTokenButton[0], 'add token button present') - addTokenButton[0].click() - - addTokenWrapper = await queryAsync($, '.page-container') - const addTokenTabs = await queryAsync($, '.page-container__tab') - assert.equal(addTokenTabs.length, 2, 'expected number of tabs') - assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present') - assert.ok(addTokenTabs[1], 'add custom token tab present') - addTokenTabs[1].click() - await timeout(1000) - - // Input token contract address - const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0] - customInput.focus() - await timeout(1000) - nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c') - customInput.dispatchEvent(new Event('input', { bubbles: true})) - - - // Click Next button - // nextButton = await queryAsync($, 'button.btn-primary--lg') - // assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') - // nextButton[0].click() - - // // Verify symbol length error since contract address won't return symbol - const errorMessage = await queryAsync($, '#custom-symbol-helper-text') - assert.ok(errorMessage[0], 'error rendered') - - $('button.btn-default.btn--large')[0].click() - - // await timeout(100000) - - // Confirm Add token - // assert.equal( - // $('.page-container__subtitle')[0].textContent, - // 'Would you like to add these tokens?', - // 'confirm add token rendered' - // ) - // assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found') - // $('button.btn-primary--lg')[0].click() - - // Verify added token image - heroBalance = await queryAsync($, '.transaction-view-balance__balance-container') - assert.ok(heroBalance, 'rendered hero balance') - assert.ok(heroBalance.find('.identicon')[0], 'token added') -} From 42fa54678fbb1a170a8b88e3fec54d7d9d44c303 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 16 Oct 2018 18:24:34 -0230 Subject: [PATCH 08/19] Extract Add Token button into its own component --- app/_locales/en/messages.json | 6 ++++ test/e2e/beta/metamask-beta-ui.spec.js | 4 +-- .../add-token-button.component.js | 34 +++++++++++++++++++ ui/app/components/add-token-button/index.js | 1 + ui/app/components/add-token-button/index.scss | 26 ++++++++++++++ ui/app/components/index.scss | 2 ++ ui/app/components/wallet-view.js | 30 ++++++++++------ .../css/itcss/components/newui-sections.scss | 12 ------- 8 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 ui/app/components/add-token-button/add-token-button.component.js create mode 100644 ui/app/components/add-token-button/index.js create mode 100644 ui/app/components/add-token-button/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 690864ef1..bf5854a31 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -140,6 +140,9 @@ "clickCopy": { "message": "Click to Copy" }, + "clickToAdd": { + "message": "Click on $1 to add them to your account" + }, "close": { "message": "Close" }, @@ -641,6 +644,9 @@ "min": { "message": "Minimum" }, + "missingYourTokens": { + "message": "Don't see your tokens?" + }, "myAccounts": { "message": "My Accounts" }, diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index 12cf91227..f29f242c1 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -662,7 +662,7 @@ describe('MetaMask', function () { }) it('clicks on the Add Token button', async () => { - const addToken = await driver.findElement(By.css('.wallet-view__add-token-button')) + const addToken = await driver.findElement(By.xpath(`//div[contains(text(), 'Add Token')]`)) await addToken.click() await delay(regularDelayMs) }) @@ -1002,7 +1002,7 @@ describe('MetaMask', function () { describe('Add existing token using search', () => { it('clicks on the Add Token button', async () => { - const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) + const addToken = await findElement(driver, By.xpath(`//div[contains(text(), 'Add Token')]`)) await addToken.click() await delay(regularDelayMs) }) diff --git a/ui/app/components/add-token-button/add-token-button.component.js b/ui/app/components/add-token-button/add-token-button.component.js new file mode 100644 index 000000000..10887aed8 --- /dev/null +++ b/ui/app/components/add-token-button/add-token-button.component.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' + +export default class AddTokenButton extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static defaultProps = { + onClick: () => {}, + } + + static propTypes = { + onClick: PropTypes.func, + } + + render () { + const { t } = this.context + const { onClick } = this.props + + return ( +
+

{t('missingYourTokens')}

+

{t('clickToAdd', [t('addToken')])}

+
+ {t('addToken')} +
+
+ ) + } +} diff --git a/ui/app/components/add-token-button/index.js b/ui/app/components/add-token-button/index.js new file mode 100644 index 000000000..15c4fe6ca --- /dev/null +++ b/ui/app/components/add-token-button/index.js @@ -0,0 +1 @@ +export { default } from './add-token-button.component' diff --git a/ui/app/components/add-token-button/index.scss b/ui/app/components/add-token-button/index.scss new file mode 100644 index 000000000..39f404716 --- /dev/null +++ b/ui/app/components/add-token-button/index.scss @@ -0,0 +1,26 @@ +.add-token-button { + display: flex; + flex-direction: column; + color: lighten($scorpion, 25%); + width: 185px; + margin: 36px auto; + text-align: center; + + &__help-header { + font-weight: bold; + font-size: 1rem; + } + + &__help-desc { + font-size: 0.75rem; + margin-top: 1rem; + } + + &__button { + font-size: 0.75rem; + margin: 1rem; + text-transform: uppercase; + color: $curious-blue; + cursor: pointer; + } +} diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index bf34fd732..beffdb221 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,5 +1,7 @@ @import './app-header/index'; +@import './add-token-button/index'; + @import './button-group/index'; @import './card/index'; diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 064a6ab55..8a7cb0f8d 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -17,7 +17,7 @@ const TokenList = require('./token-list') const selectors = require('../selectors') const { ADD_TOKEN_ROUTE } = require('../routes') -import Button from './button' +import AddTokenButton from './add-token-button' module.exports = compose( withRouter, @@ -100,15 +100,30 @@ WalletView.prototype.renderWalletBalance = function () { ]) } +WalletView.prototype.renderAddToken = function () { + const { + sidebarOpen, + hideSidebar, + history, + } = this.props + + return h(AddTokenButton, { + onClick () { + history.push(ADD_TOKEN_ROUTE) + if (sidebarOpen) { + hideSidebar() + } + }, + }) +} + WalletView.prototype.render = function () { const { responsiveDisplayClassname, selectedAddress, keyrings, showAccountDetailModal, - sidebarOpen, hideSidebar, - history, identities, } = this.props // temporary logs + fake extra wallets @@ -201,14 +216,7 @@ WalletView.prototype.render = function () { h(TokenList), - h(Button, { - type: 'primary', - className: 'wallet-view__add-token-button', - onClick: () => { - history.push(ADD_TOKEN_ROUTE) - sidebarOpen && hideSidebar() - }, - }, this.context.t('addToken')), + this.renderAddToken(), ]) } diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 8e963d495..233e781ef 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -120,18 +120,6 @@ $wallet-view-bg: $alabaster; } } } - - &__add-token-button { - flex: 0 0 auto; - margin: 36px auto; - background: none; - transition: border-color .3s ease; - width: 150px; - - &:hover { - border-color: $curious-blue; - } - } } @media screen and (min-width: 576px) { From 2c2ae8f4c789ddb8eef8c8b152acf8ee2b3d5878 Mon Sep 17 00:00:00 2001 From: Bartek Date: Thu, 18 Oct 2018 01:04:03 +0200 Subject: [PATCH 09/19] i18n - add polish (pl) --- app/_locales/index.json | 1 + app/_locales/pl/messages.json | 1213 +++++++++++++++++++++++++++++++++ 2 files changed, 1214 insertions(+) create mode 100644 app/_locales/pl/messages.json diff --git a/app/_locales/index.json b/app/_locales/index.json index 0598aa9ec..46933dc3f 100644 --- a/app/_locales/index.json +++ b/app/_locales/index.json @@ -11,6 +11,7 @@ { "code": "ko", "name": "Korean" }, { "code": "nl", "name": "Dutch" }, { "code": "ph", "name": "Tagalog" }, + { "code": "pl", "name": "Polish" }, { "code": "pt", "name": "Portuguese" }, { "code": "ru", "name": "Russian" }, { "code": "sl", "name": "Slovenian" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json new file mode 100644 index 000000000..c6d797c34 --- /dev/null +++ b/app/_locales/pl/messages.json @@ -0,0 +1,1213 @@ +{ + "accept": { + "message": "Akceptacja" + }, + "accessingYourCamera": { + "message": "Uruchamianie kamery..." + }, + "account": { + "message": "Konto" + }, + "accountDetails": { + "message": "Szczegóły konta" + }, + "accountName": { + "message": "Nazwa konta" + }, + "accountSelectionRequired": { + "message": "Należy wybrać konto!" + }, + "address": { + "message": "Adres" + }, + "addCustomToken": { + "message": "Dodaj token" + }, + "addToken": { + "message": "Dodaj token" + }, + "addTokens": { + "message": "Dodaj tokeny" + }, + "addSuggestedTokens": { + "message": "Dodaj sugerowane tokeny." + }, + "addAcquiredTokens": { + "message": "Dodaj tokeny pozyskane przy pomocy MetaMask" + }, + "amount": { + "message": "Ilość" + }, + "amountPlusGas": { + "message": "Ilość + gaz" + }, + "appDescription": { + "message": "Wtyczka przeglądarki do Ethereum", + "description": "Opis aplikacji" + }, + "appName": { + "message": "MetaMask", + "description": "Nazwa aplikacji" + }, + "approve": { + "message": "Zatwierdź" + }, + "approved": { + "message": "Zatwierdzone" + }, + "attemptingConnect": { + "message": "Próba połączenia z blockchainem." + }, + "attributions": { + "message": "Atrybuty" + }, + "available": { + "message": "Dostępne" + }, + "back": { + "message": "Wstecz" + }, + "balance": { + "message": "Ilość środków" + }, + "balances": { + "message": "Ilość tokenów" + }, + "balanceIsInsufficientGas": { + "message": "Niewystarczająca ilość środków na opłatę za gaz" + }, + "beta": { + "message": "BETA" + }, + "betweenMinAndMax": { + "message": "musi być większe lub równe $1 i mniejsze lub równe $2,", + "description": "pomoc przy wpisywaniu hex jako dane dziesiętne" + }, + "blockiesIdenticon": { + "message": "Użyj Blockies Identicon" + }, + "borrowDharma": { + "message": "Pożycz z Dharma (Beta)" + }, + "browserNotSupported": { + "message": "Twoja przeglądarka nie jest obsługiwana..." + }, + "builtInCalifornia": { + "message": "MetaMask został zaprojektowany i stworzony w Kaliforni." + }, + "buy": { + "message": "Kup" + }, + "buyCoinbase": { + "message": "Kup na Coinbase" + }, + "buyCoinbaseExplainer": { + "message": "Coinbase to najpopularniejszy sposób na kupno i sprzedaż Bitcoin, Ethereum i Litecoin." + }, + "bytes": { + "message": "Bajty" + }, + "ok": { + "message": "Ok" + }, + "cancel": { + "message": "Anuluj" + }, + "classicInterface": { + "message": "Użyj klasycznego interfejsu" + }, + "clickCopy": { + "message": "Kliknij żeby skopiować" + }, + "close": { + "message": "Zamknij" + }, + "chromeRequiredForHardwareWallets": { + "message": "Żeby połączyć się z portfelem sprzętowym, należy uruchomić MetaMask z przeglądarką Google Chrome." + }, + "confirm": { + "message": "Potwierdź" + }, + "confirmed": { + "message": "Potwierdzone" + }, + "confirmContract": { + "message": "Zatwierdź kontrakt" + }, + "confirmPassword": { + "message": "Potwierdź hasło" + }, + "confirmTransaction": { + "message": "Potwierdź transakcję" + }, + "connectHardwareWallet": { + "message": "Podłącz portfel sprzętowy" + }, + "connect": { + "message": "Połącz" + }, + "connecting": { + "message": "Łączenie..." + }, + "connectToLedger": { + "message": "Połącz z Ledger" + }, + "connectToTrezor": { + "message": "Połącz z Trezor" + }, + "continue": { + "message": "Kontynuuj" + }, + "continueToCoinbase": { + "message": "Przejdź do Coinbase" + }, + "contractDeployment": { + "message": "Uruchomienie kontraktu" + }, + "conversionProgress": { + "message": "Przeliczanie w toku" + }, + "copiedButton": { + "message": "Skopiowane" + }, + "copiedClipboard": { + "message": "Skopiowane do schowka" + }, + "copiedExclamation": { + "message": "Skopiowane!" + }, + "copiedSafe": { + "message": "Skopiowałem to w bezpieczne miejsce" + }, + "copy": { + "message": "Skopiuj" + }, + "copyContractAddress": { + "message": "Skopiuj adres kontaktowy" + }, + "copyAddress": { + "message": "Skopiuj adres do schowka" + }, + "copyToClipboard": { + "message": "Skopiuj do schowka" + }, + "copyButton": { + "message": " Skopiuj " + }, + "copyPrivateKey": { + "message": "To jest Twój prywatny klucz (kliknij żeby skopiować)" + }, + "create": { + "message": "Utwórz" + }, + "createAccount": { + "message": "Utwórz konto" + }, + "createDen": { + "message": "Utwórz" + }, + "crypto": { + "message": "Krypto", + "description": "Tym platformy wymiany (kryptowaluty)" + }, + "currentConversion": { + "message": "Obecny kurs" + }, + "currentNetwork": { + "message": "Bieżąca sieć" + }, + "customGas": { + "message": "Ustaw gaz" + }, + "customToken": { + "message": "Własny token" + }, + "customize": { + "message": "Ustaw" + }, + "customRPC": { + "message": "Własne RPC" + }, + "decimalsMustZerotoTen": { + "message": "Liczb po przecinku musi być co najmniej 0 i nie więcej niż 36." + }, + "decimal": { + "message": "Dokładność liczb po przecinku" + }, + "defaultNetwork": { + "message": "Domyślna sieć dla Eteru to Main Net." + }, + "denExplainer": { + "message": "Twój DEN to chroniony hasłem schowek w MetaMasku." + }, + "deposit": { + "message": "Zdeponuj" + }, + "depositBTC": { + "message": "Zdeponuj swoje BTC na poniższy adres:" + }, + "depositCoin": { + "message": "Zdeponuj $1 na poniższy adres", + "description": "Pokazuje użytkownikowi jakie waluty wybrał do zdeponowania w ShapeShift" + }, + "depositEth": { + "message": "Zdeponuj Eth" + }, + "depositEther": { + "message": "Zdeponuj Eter" + }, + "depositFiat": { + "message": "Zdeponuj w Fiat" + }, + "depositFromAccount": { + "message": "Zdeponuj z innego konta" + }, + "depositShapeShift": { + "message": "Zdeponuj przez ShapeShift" + }, + "depositShapeShiftExplainer": { + "message": "Jeśli posiadasz inne kryptowaluty, możesz nimi handlować i deponować Eter bezpośrednio do swojego portfela MetaMask. Nie trzeba żadnego konta." + }, + "details": { + "message": "Szczegóły" + }, + "directDeposit": { + "message": "Bezpośredni depozyt" + }, + "directDepositEther": { + "message": "Zdeponuj Eter bezpośrednio" + }, + "directDepositEtherExplainer": { + "message": "Jeśli już masz Eter, najszybciej umieścisz go w swoim nowym portfelu przy pomocy bezpośredniego depozytu." + }, + "done": { + "message": "Gotowe" + }, + "downloadGoogleChrome": { + "message": "Ściągnij Google Chrome" + }, + "downloadStateLogs": { + "message": "Załaduj logi stanów" + }, + "dontHaveAHardwareWallet": { + "message": "Nie masz portfela sprzętowego?" + }, + "dropped": { + "message": "Odrzucone" + }, + "edit": { + "message": "Edytuj" + }, + "editAccountName": { + "message": "Edytuj nazwę konta" + }, + "editingTransaction": { + "message": "Dokonaj zmian w swojej transakcji" + }, + "emailUs": { + "message": "Napisz do nas!" + }, + "encryptNewDen": { + "message": "Zaszyfruj swój nowy DEN" + }, + "ensNameNotFound": { + "message": "Nie znaleziono nazwy ENS" + }, + "enterPassword": { + "message": "Wpisz hasło" + }, + "enterPasswordConfirm": { + "message": "Wpisz hasło żeby potwierdzić" + }, + "enterPasswordContinue": { + "message": "Podaj hasło żeby kontynuować" + }, + "parameters": { + "message": "Parametry" + }, + "passwordNotLongEnough": { + "message": "Hasło jest za krótkie" + }, + "passwordsDontMatch": { + "message": "Hasła są niezgodne" + }, + "etherscanView": { + "message": "Zobacz konto na Etherscan" + }, + "exchangeRate": { + "message": "Kurs wymiany" + }, + "exportPrivateKey": { + "message": "Eksportuj klucz prywatny" + }, + "exportPrivateKeyWarning": { + "message": "Eksportujesz prywatne klucze na własne ryzyko." + }, + "failed": { + "message": "Nie udało się" + }, + "fiat": { + "message": "FIAT", + "description": "Rodzaj wymiany" + }, + "fileImportFail": { + "message": "Importowanie pliku nie działa? Kliknij tutaj!", + "description": "Wspomaga użytkowników przy importowaniu ich konta z pliku JSON" + }, + "followTwitter": { + "message": "Śledź nas na Twitterze" + }, + "forgetDevice": { + "message": "Usuń to urządzenie." + }, + "from": { + "message": "Z" + }, + "fromToSame": { + "message": "Adresy Z i Do nie mogą być identyczne" + }, + "fromShapeShift": { + "message": "Z ShapeShift" + }, + "functionType": { + "message": "Typ funkcji" + }, + "gas": { + "message": "Gaz", + "description": "Krótkie oznaczenie kosztu gazu" + }, + "gasFee": { + "message": "Opłata za gaz" + }, + "gasLimit": { + "message": "Limit gazu" + }, + "gasLimitCalculation": { + "message": "Obliczamy sugerowany limit gazu na podstawie danych z transakcji w sieci." + }, + "gasLimitRequired": { + "message": "Limit gazu jest wymagany" + }, + "gasLimitTooLow": { + "message": "Limit gazu musi wynosić co najmniej 21000" + }, + "generatingSeed": { + "message": "Generowanie seed..." + }, + "gasPrice": { + "message": "Cena gazu (GWEI)" + }, + "gasPriceCalculation": { + "message": "Obliczamy ceny gazu na podstawie danych z transakcji w sieci." + }, + "gasPriceRequired": { + "message": "Wymagana cena gazu" + }, + "generatingTransaction": { + "message": "Generowanie transakcji" + }, + "getEther": { + "message": "Zdobądź Eter" + }, + "getEtherFromFaucet": { + "message": "Zdobądź Eter ze źródła za $1", + "description": "Wyświetla nazwę sieci dla źródła Eteru" + }, + "getHelp": { + "message": "Po pomoc." + }, + "greaterThanMin": { + "message": "musi być większe lub równe $1.", + "description": "pomoc przy wpisywaniu hex jako dane dziesiętne" + }, + "hardware": { + "message": "sprzęt" + }, + "hardwareWalletConnected": { + "message": "Podłączono sprzętowy portfel" + }, + "hardwareWallets": { + "message": "Podłącz sprzętowy portfel" + }, + "hardwareWalletsMsg": { + "message": "Wybierz portfel sprzętowy, którego chcesz użyć z MetaMaskiem" + }, + "havingTroubleConnecting": { + "message": "Problem z połączeniem?" + }, + "here": { + "message": "tutaj", + "description": "jak w -kliknij tutaj- po więcej informacji (połączone z troubleTokenBalances)" + }, + "hereList": { + "message": "Oto lista!!!" + }, + "hexData": { + "message": "Dane Hex" + }, + "hide": { + "message": "Schowaj" + }, + "hideToken": { + "message": "Schowaj token" + }, + "hideTokenPrompt": { + "message": "Schować token?" + }, + "history": { + "message": "Historia" + }, + "howToDeposit": { + "message": "Jak chcesz zdeponować Eter?" + }, + "holdEther": { + "message": "Umożliwia przechowywanie eteru i tokenów oraz służy jako łącznik do zdecentralizowanych aplikacji." + }, + "import": { + "message": "Importuj", + "description": "Przycisk do importowania konta z wybranego pliku." + }, + "importAccount": { + "message": "Importuj konto" + }, + "importAccountMsg": { + "message": " Importowane konta nie będą powiązane z Twoją pierwotną frazą seed MetaMask. Dowiedz się więcej o importowaniu kont " + }, + "importAnAccount": { + "message": "Importuj konto" + }, + "importDen": { + "message": "Importuj istniejące DEN" + }, + "imported": { + "message": "Zaimportowane", + "description": "status pokazujący, że konto zostało w pełni załadowane na keyring" + }, + "importUsingSeed": { + "message": "Importuj przy pomocy frazy seed konta" + }, + "infoHelp": { + "message": "Info & pomoc" + }, + "initialTransactionConfirmed": { + "message": "Twoja transakcja została potwierdzona w sieci. Kliknij OK żeby wrócić." + }, + "insufficientFunds": { + "message": "Niewystarczające środki." + }, + "insufficientTokens": { + "message": "Niewystarczająca liczba tokenów." + }, + "invalidAddress": { + "message": "Nieprawidłowy adres" + }, + "invalidAddressRecipient": { + "message": "Nieprawidłowy adres odbiorcy" + }, + "invalidGasParams": { + "message": "Nieprawidłowe parametry gazu" + }, + "invalidInput": { + "message": "Nieprawidłowe dane." + }, + "invalidRequest": { + "message": "Nieprawidłowe zapytanie" + }, + "invalidRPC": { + "message": "Nieprawidłowe RPC URI" + }, + "invalidSeedPhrase": { + "message": "Nieprawidłowa fraza seed" + }, + "jsonFail": { + "message": "Coś poszło nie tak. Upewnij się, że plik JSON jest prawidłowo sformatowany." + }, + "jsonFile": { + "message": "Plik JSON", + "description": "formatuj do importowania konta" + }, + "keepTrackTokens": { + "message": "Monitoruj stan tokenów kupionych przy pomocy konta MetaMask." + }, + "kovan": { + "message": "Sieć testowa Kovan" + }, + "knowledgeDataBase": { + "message": "Sprawdź naszą Bazę wiedzy." + }, + "max": { + "message": "Maks." + }, + "learnMore": { + "message": "Dowiedz się więcej" + }, + "ledgerAccountRestriction": { + "message": "Musisz użyć swojego poprzedniego konta zanim dodasz kolejne." + }, + "lessThanMax": { + "message": "musi być mniejsze lub równe $1.", + "description": "pomoc przy wpisywaniu hex jako dane dziesiętne" + }, + "likeToAddTokens": { + "message": "Czy chcesz dodać te tokeny?" + }, + "links": { + "message": "Łącza" + }, + "limit": { + "message": "Limit" + }, + "loading": { + "message": "Ładowanie..." + }, + "loadingTokens": { + "message": "Ładowanie tokenów..." + }, + "localhost": { + "message": "Serwer lokalny 8545" + }, + "login": { + "message": "Zaloguj się" + }, + "logout": { + "message": "Wyloguj się" + }, + "loose": { + "message": "Porzuć" + }, + "loweCaseWords": { + "message": "słowa seed mogą być pisane wyłącznie małymi literami" + }, + "mainnet": { + "message": "Główna sieć Ethereum" + }, + "menu": { + "message": "Menu" + }, + "message": { + "message": "Wiadomość" + }, + "metamaskDescription": { + "message": "MetaMask to bezpieczny portfel dla Ethereum." + }, + "metamaskSeedWords": { + "message": "Słowa Seed MetaMask" + }, + "min": { + "message": "Minimum" + }, + "myAccounts": { + "message": "Moje konta" + }, + "mustSelectOne": { + "message": "Należy wybrać co najmniej 1 token." + }, + "needEtherInWallet": { + "message": "Żeby skorzystać ze zdecentraliowanych aplikacji (dApps) przy pomocy MetaMask, potrzebujesz Eteru w swoim portfelu." + }, + "needImportFile": { + "message": "Musisz wybrać plik do zaimportowania.", + "description": "Użytkownik importuje konto i musi dodać plik, żeby kontynuować" + }, + "needImportPassword": { + "message": "Musisz podać hasło dla wybranego pliku.", + "description": "Hasło i plik niezbędne do zaimportowania konta" + }, + "negativeETH": { + "message": "Nie można wysłać ujemnych ilości ETH." + }, + "networks": { + "message": "Sieci" + }, + "nevermind": { + "message": "Nie ważne" + }, + "newAccount": { + "message": "Nowe konto" + }, + "newAccountNumberName": { + "message": "Konto $1", + "description": "Automatyczna nazwa kolejnego konta utworzonego w widoku Utwórz konto" + }, + "newContract": { + "message": "Nowy kontrakt" + }, + "newPassword": { + "message": "Nowe hasło (min. 8 znaków)" + }, + "newRecipient": { + "message": "Nowy odbiorca" + }, + "newRPC": { + "message": "Nowy RPC URL" + }, + "next": { + "message": "Dalej" + }, + "noAddressForName": { + "message": "Nie wybrano żadnego adresu dla tej nazwy." + }, + "noDeposits": { + "message": "Brak otrzymanych depozytów" + }, + "noConversionRateAvailable": { + "message": "Brak kursu waluty" + }, + "noTransactionHistory": { + "message": "Brak historii transakcji." + }, + "noTransactions": { + "message": "Nie ma transakcji" + }, + "notFound": { + "message": "Nie znaleziono" + }, + "notStarted": { + "message": "Nie rozpoczęto" + }, + "noWebcamFoundTitle": { + "message": "Nie znaleziono kamery" + }, + "noWebcamFound": { + "message": "Twoja kamera nie została znaleziona. Spróbuj ponownie." + }, + "oldUI": { + "message": "Stary interfejs" + }, + "oldUIMessage": { + "message": "Wróciłeś do starego interfejsu. Możesz włączyć nowy interfejs przez opcje w rozwijanym menu w prawym górnym rogu." + }, + "openInTab": { + "message": "Otwórz w zakładce" + }, + "or": { + "message": "lub", + "description": "wybór między tworzeniem i importowaniem nowego konta" + }, + "origin": { + "message": "Pochodzenie" + }, + "password": { + "message": "Hasło" + }, + "passwordCorrect": { + "message": "Upewnij się, że Twoje hasło jest poprawne." + }, + "passwordMismatch": { + "message": "hasła nie są takie same", + "description": "podczas tworzenia hasła, tekst w dwóch polach haseł nie był taki sam" + }, + "passwordShort": { + "message": "hasło za krótkie", + "description": "podczas tworzenia hasła, hasło nie jest bezpieczne, ponieważ nie jest wystarczająco długie" + }, + "pastePrivateKey": { + "message": "Tutaj wklej swój prywatny klucz:", + "description": "Do importowania konta z prywatnego klucza" + }, + "pasteSeed": { + "message": "Tutaj wklej swoją frazę seed!" + }, + "pending": { + "message": "oczekiwanie" + }, + "personalAddressDetected": { + "message": "Wykryto osobisty adres. Wprowadź adres kontraktu tokenów." + }, + "pleaseReviewTransaction": { + "message": "Proszę, sprawdź transakcję." + }, + "popularTokens": { + "message": "Popularne tokeny" + }, + "prev": { + "message": "Poprzednie" + }, + "privacyMsg": { + "message": "Polityka prywatności" + }, + "privateKey": { + "message": "Klucz prywatny", + "description": "wybierz ten typ pliku żeby importować konto" + }, + "privateKeyWarning": { + "message": "Uwaga: Nie ujawniaj nikomu tego klucza. Ktokolwiek posiadający Twoje prywatne klucze może użyć środków znajdujących się na Twoim koncie." + }, + "privateNetwork": { + "message": "Sieć prywatna" + }, + "qrCode": { + "message": "Pokaż kod QR" + }, + "queue": { + "message": "Kolejka" + }, + "readdToken": { + "message": "Możesz później ponownie dodać ten token poprzez \"Dodaj token\" w opcjach menu swojego konta." + }, + "readMore": { + "message": "Dowiedz się więcej tutaj." + }, + "readMore2": { + "message": "Dowiedz się więcej." + }, + "receive": { + "message": "Otrzymaj" + }, + "recipientAddress": { + "message": "Adres odbiorcy" + }, + "refundAddress": { + "message": "Twój adres na zwroty" + }, + "rejected": { + "message": "Odrzucone" + }, + "reset": { + "message": "Reset" + }, + "resetAccount": { + "message": "Resetuj konto" + }, + "resetAccountDescription": { + "message": "Zresetowanie konta wyczyści Twoją historię transakcji." + }, + "restoreFromSeed": { + "message": "Przywrócić konto?" + }, + "restoreVault": { + "message": "Przywróć schowek" + }, + "restoreAccountWithSeed": { + "message": "Przywróć konto frazą seed" + }, + "required": { + "message": "Wymagane" + }, + "retryWithMoreGas": { + "message": "Spróbuj ponownie z większą ceną gazu" + }, + "walletSeed": { + "message": "Seed portfela" + }, + "restore": { + "message": "Przywróć" + }, + "revealSeedWords": { + "message": "Pokaż słowa seed" + }, + "revealSeedWordsTitle": { + "message": "Fraza seed" + }, + "revealSeedWordsDescription": { + "message": "Jeśli kiedyś zmienisz przeglądarkę lub komputer, będziesz potrzebować tej frazy seed, żeby dostać się do swoich kont. Zapisz ją w bezpiecznym miejscu." + }, + "revealSeedWordsWarningTitle": { + "message": "NIE pokazuj tej frazy nikomu!" + }, + "revealSeedWordsWarning": { + "message": "Te słowa mogą być użyte żeby ukraść Twoje konta." + }, + "revert": { + "message": "Wycofaj" + }, + "remove": { + "message": "usuń" + }, + "removeAccount": { + "message": "Usuń konto" + }, + "removeAccountDescription": { + "message": "To konto będzie usunięte z Twojego portfela. Zanim przejdziesz dalej, upewnij się, że masz frazę seed i klucz prywatny do tego importowanego konta. Możesz później importować lub utworzyć nowe konta z rozwijanego menu kont. " + }, + "readyToConnect": { + "message": "Gotowy na połączenie?" + }, + "rinkeby": { + "message": "Sieć testowa Rinkeby" + }, + "ropsten": { + "message": "Sieć testowa Ropsten" + }, + "rpc": { + "message": "Indywidualne RPC" + }, + "currentRpc": { + "message": "Obecne RPC" + }, + "connectingToMainnet": { + "message": "Łączenie z główną siecią Ethereum" + }, + "connectingToRopsten": { + "message": "Łączenie z siecią testową Ropsten" + }, + "connectingToKovan": { + "message": "Łączenie z siecią testową Kovan" + }, + "connectingToRinkeby": { + "message": "Łączenie z siecią testową Rinkeby" + }, + "connectingToUnknown": { + "message": "Łączenie z nieznaną siecią" + }, + "sampleAccountName": { + "message": "Np. Moje nowe konto", + "description": "Umożliwia użytkownikom zrozumieć ideę dodawania własnej nazwy to ich konta" + }, + "save": { + "message": "Zapisz" + }, + "speedUpTitle": { + "message": "Przyspiesz transakcję" + }, + "speedUpSubtitle": { + "message": "Zwiększ cenę gazu żeby nadpisać i przyspieszyć transakcję" + }, + "saveAsCsvFile": { + "message": "Zapisz jako plik CSV" + }, + "saveAsFile": { + "message": "Zapisz jako", + "description": "Proces eksportu konta" + }, + "saveSeedAsFile": { + "message": "Zapisz słowa seed jako plik" + }, + "search": { + "message": "Szukaj" + }, + "searchResults": { + "message": "Wyniki wyszukiwania" + }, + "secretPhrase": { + "message": "Żeby otworzyć schowek, wpisz tutaj swoją frazę dwunastu słów." + }, + "showHexData": { + "message": "Pokaż dane hex" + }, + "showHexDataDescription": { + "message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania" + }, + "newPassword8Chars": { + "message": "Nowe hasło (min. 8 znaków)" + }, + "seedPhraseReq": { + "message": "Frazy seed mają 12 słów" + }, + "select": { + "message": "Wybierz" + }, + "selectCurrency": { + "message": "Wybierz walutę" + }, + "selectService": { + "message": "Wybierz usługę" + }, + "selectType": { + "message": "Wybierz rodzaj" + }, + "send": { + "message": "Wyślij" + }, + "sendETH": { + "message": "Wyślij ETH" + }, + "sendTokens": { + "message": "Wyślij tokeny" + }, + "sentEther": { + "message": "wyślij eter" + }, + "sentTokens": { + "message": "wysłane tokeny" + }, + "separateEachWord": { + "message": "Oddziel słowa pojedynczą spacją" + }, + "onlySendToEtherAddress": { + "message": "Na adres Ethereum wysyłaj tylko ETH." + }, + "onlySendTokensToAccountAddress": { + "message": "Wyślij tylko $1 na adres konta Ethereum.", + "description": "wyświetla symbol tokena" + }, + "orderOneHere": { + "message": "Zamów Trezor lub Ledger i trzymaj swoje środki w portfelu sprzętowym." + }, + "outgoing": { + "message": "Wychodzące" + }, + "searchTokens": { + "message": "Szukaj tokenów" + }, + "selectAnAddress": { + "message": "Wybierz adres" + }, + "selectAnAccount": { + "message": "Wybierz konto" + }, + "selectAnAccountHelp": { + "message": "Wybierz konto do przeglądania w MetaMask" + }, + "selectHdPath": { + "message": "Wybierz ścieżkę HD" + }, + "selectPathHelp": { + "message": "Jeśli nie widzisz poniżej swoich kont Ledger, spróbuj przełączyć się na \"Legacy (MEW / MyCrypto)\"" + }, + "sendTokensAnywhere": { + "message": "Wyślij tokeny do kogoś z adresem Ethereum" + }, + "settings": { + "message": "Ustawienia" + }, + "step1HardwareWallet": { + "message": "1. Podłącz portfel sprzętowy" + }, + "step1HardwareWalletMsg": { + "message": "Połącz swój portfel sprzętowy z komputerem." + }, + "step2HardwareWallet": { + "message": "2. Wybierz konto" + }, + "step2HardwareWalletMsg": { + "message": "Wybierz konto, które chcesz przeglądać. Możesz wybrać tylko jedno konto w danym momencie." + }, + "step3HardwareWallet": { + "message": "3. Zacznij używać dystrybuowanych aplikacji (dApps) i wiele więcej!" + }, + "step3HardwareWalletMsg": { + "message": "Używaj swojego konta sprzętowego tak, jak używasz jakiegokolwiek konta z Ethereum. Loguj się do dystrybuowanych aplikacji (dApps), wysyłaj Eth, kupuj i przechowaj tokeny ERC20 i niewymienne tokeny, jak np. CryptoKitties." + }, + "info": { + "message": "Info" + }, + "scanInstructions": { + "message": "Umieść kod QR na wprost kamery" + }, + "scanQrCode": { + "message": "Skanuj kod QR" + }, + "shapeshiftBuy": { + "message": "Kup w ShapeShift" + }, + "showPrivateKeys": { + "message": "Pokaż prywatne klucze" + }, + "showQRCode": { + "message": "Pokaż kod QR" + }, + "sign": { + "message": "Podpisz" + }, + "signatureRequest": { + "message": "Prośba o podpis" + }, + "signed": { + "message": "Podpisane" + }, + "signMessage": { + "message": "Podpisz wiadomość" + }, + "signNotice": { + "message": "Podpisanie tej wiadomości może mieć \nniebezpieczne skutki uboczne. Podpisuj wiadomości \ntylko ze stron, którym chcesz udostępnić swoje konto.\nTa niebezpieczna metoda będzie usunięta w przyszłych wersjach. " + }, + "sigRequest": { + "message": "Prośba o podpis" + }, + "sigRequested": { + "message": "Podpis wymagany" + }, + "spaceBetween": { + "message": "między słowami może być tylko pojedyncza spacja" + }, + "status": { + "message": "Status" + }, + "stateLogs": { + "message": "Logi stanów" + }, + "stateLogsDescription": { + "message": "Logi stanów zawierają Twoje publiczne adresy kont i wykonanych transakcji." + }, + "stateLogError": { + "message": "Błąd podczas pobierania logów stanów." + }, + "submit": { + "message": "Wyślij" + }, + "submitted": { + "message": "Wysłane" + }, + "supportCenter": { + "message": "Odwiedź nasze Centrum Pomocy" + }, + "symbolBetweenZeroTen": { + "message": "Symbol musi mieć od 0 do 10 znaków." + }, + "takesTooLong": { + "message": "Trwa zbyt długo?" + }, + "terms": { + "message": "Regulamin" + }, + "testFaucet": { + "message": "Źródło testowego ETH" + }, + "to": { + "message": "Do" + }, + "toETHviaShapeShift": { + "message": "$1 na ETH przez ShapeShift", + "description": "system uzupełni typ depozytu na początku wiadomości" + }, + "token": { + "message": "Token" + }, + "tokenAddress": { + "message": "Adres tokena" + }, + "tokenAlreadyAdded": { + "message": "Token jest już dodany." + }, + "tokenBalance": { + "message": "Liczba Twoich tokenów:" + }, + "tokenSelection": { + "message": "Szukaj tokenów lub wybierz z naszej listy popularnych tokenów." + }, + "tokenSymbol": { + "message": "Symbol tokena" + }, + "tokenWarning1": { + "message": "Monitoruj stan tokenów kupionych przy pomocy konta MetaMask. Jeśli masz tokeny kupione przy pomocy innych kont, nie pojawią się tutaj." + }, + "total": { + "message": "Suma" + }, + "transactions": { + "message": "transakcje" + }, + "transactionError": { + "message": "Błąd transakcji. Wyjątek w kodzie kontraktu." + }, + "transactionMemo": { + "message": "Memo transakcji (opcjonalnie)" + }, + "transactionNumber": { + "message": "Numer transakcji" + }, + "transfer": { + "message": "Przelew" + }, + "transfers": { + "message": "Przelewy" + }, + "trezorHardwareWallet": { + "message": "Sprzętowy portfel TREZOR" + }, + "troubleTokenBalances": { + "message": "Wystąpił problem z załadowaniem informacji o Twoich tokenach. Można je zobaczyć ", + "description": "Z linkiem (tutaj) do informacji o stanie tokenów" + }, + "tryAgain": { + "message": "Spróbuj ponownie" + }, + "twelveWords": { + "message": "Tych 12 słów to jedyny sposób, żeby odzyskać konta w MetaMasku. Zapisz je w bezpiecznym miejscu." + }, + "typePassword": { + "message": "Wpisz hasło" + }, + "uiWelcome": { + "message": "Witamy w nowym interfejsie (Beta)" + }, + "uiWelcomeMessage": { + "message": "Używasz teraz nowego interfejsu MetaMask." + }, + "unapproved": { + "message": "Niezatwierdzone" + }, + "unavailable": { + "message": "Niedostępne" + }, + "unknown": { + "message": "Nieznane" + }, + "unknownFunction": { + "message": "Nieznana funkcja" + }, + "unknownNetwork": { + "message": "Nieznana sieć prywatna" + }, + "unknownNetworkId": { + "message": "Nieznane sieciowe ID" + }, + "unknownQrCode": { + "message": "Błąd: nie mogliśmy odczytać tego kodu QR" + }, + "unknownCameraErrorTitle": { + "message": "Ups! Coś poszło nie tak..." + }, + "unknownCameraError": { + "message": "Podczas łączenia się z kamerą wystąpił błąd. Spróbuj ponownie..." + }, + "unlock": { + "message": "Odblokuj" + }, + "unlockMessage": { + "message": "Zdecentralizowana sieć oczekuje" + }, + "uriErrorMsg": { + "message": "URI wymaga prawidłowego prefiksu HTTP/HTTPS." + }, + "usaOnly": { + "message": "Tylko USA", + "description": "Ta platforma wymiany jest dostępna tylko dla osób mieszkających w USA" + }, + "usedByClients": { + "message": "Używany przez różnych klientów" + }, + "useOldUI": { + "message": "Przełącz na stary interfejs" + }, + "validFileImport": { + "message": "Należy wybrać prawidłowy plik do zaimportowania." + }, + "vaultCreated": { + "message": "Schowek utworzony" + }, + "viewAccount": { + "message": "Zobacz konto" + }, + "viewOnEtherscan": { + "message": "Zobacz na Etherscan" + }, + "visitWebSite": { + "message": "Odwiedź naszą stronę" + }, + "warning": { + "message": "Uwaga" + }, + "welcomeBack": { + "message": "Witaj z powrotem!" + }, + "welcomeBeta": { + "message": "Witaj w MetaMask Beta" + }, + "whatsThis": { + "message": "Co to jest?" + }, + "youNeedToAllowCameraAccess": { + "message": "Żeby użyć tej opcji należy podłączyć kamerę" + }, + "yourSigRequested": { + "message": "Twój podpis jest wymagany" + }, + "youSign": { + "message": "Podpisujesz" + }, + "yourPrivateSeedPhrase": { + "message": "Twoja prywatna fraza seed" + } +} \ No newline at end of file From fa36dbf9810fce80e865489a9ac921a0485f45f4 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 17 Oct 2018 19:17:39 -0400 Subject: [PATCH 10/19] inpage - increase provider max listeners to avoid warnings --- app/scripts/inpage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 431702d63..b885a7e05 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -27,6 +27,8 @@ var metamaskStream = new LocalMessageDuplexStream({ // compose the inpage provider var inpageProvider = new MetamaskInpageProvider(metamaskStream) +// set a high max listener count to avoid unnecesary warnings +inpageProvider.setMaxListeners(100) // Augment the provider with its enable method inpageProvider.enable = function (options = {}) { From 09c3611171dfb033f052a9096833a9e6db34c66b Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 17 Oct 2018 19:49:25 -0400 Subject: [PATCH 11/19] 4.16.0 --- CHANGELOG.md | 23 ++++++++++++++++++++--- app/manifest.json | 4 ++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94265257f..fe4db0ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ## Current Develop Branch +## 4.16.0 Thursday October 17 2018 + +- Feature: Add toggle for primary currency (eth/fiat) +- Feature: add tooltip for view etherscan tx +- Feature: add Polish translations +- Feature: improve Korean translations +- Feature: improve Italian translations +- Bug Fix: Fix bug with "pending" block reference +- Bug Fix: Force AccountTracker to update balances on network change +- Bug Fix: Fix document extension check when injecting web3 +- Bug Fix: Fix some support links + +## 4.15.0 Thursday October 17 2018 + +- A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.14.0` is found to have bugs. + ## 4.14.0 Thursday October 11 2018 - Update transaction statuses when switching networks. @@ -9,13 +25,14 @@ - Added rudimentary support for the subscription API to support web3 1.0 and Truffle's Drizzle. - [#5502](https://github.com/MetaMask/metamask-extension/pull/5502) Update Italian translation. -## 4.12.0 Thursday September 27 2018 - -- Reintroduces changes from 4.10.0 ## 4.13.0 - A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.12.0` is found to have bugs. +## 4.12.0 Thursday September 27 2018 + +- Reintroduces changes from 4.10.0 + ## 4.11.1 Tuesday September 25 2018 - Adds Ledger support. diff --git a/app/manifest.json b/app/manifest.json index cd34a586d..aabacd49a 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.14.0", + "version": "4.16.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -77,4 +77,4 @@ "*" ] } -} +} \ No newline at end of file From 7e9e403060530eb6f82038d197b075ae3829867e Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 17 Oct 2018 20:07:10 -0400 Subject: [PATCH 12/19] Changelog - fix release dates --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe4db0ec1..5e2815c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Current Develop Branch -## 4.16.0 Thursday October 17 2018 +## 4.16.0 Thursday October 18 2018 - Feature: Add toggle for primary currency (eth/fiat) - Feature: add tooltip for view etherscan tx @@ -14,7 +14,7 @@ - Bug Fix: Fix document extension check when injecting web3 - Bug Fix: Fix some support links -## 4.15.0 Thursday October 17 2018 +## 4.15.0 Thursday October 11 2018 - A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.14.0` is found to have bugs. From 17372e150d7070e9f4870e65a7d6c1d88568f2f5 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 17 Oct 2018 17:09:46 -0700 Subject: [PATCH 13/19] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2815c76..5a9d7703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Current Develop Branch -## 4.16.0 Thursday October 18 2018 +## 4.16.0 Wednesday October 17 2018 - Feature: Add toggle for primary currency (eth/fiat) - Feature: add tooltip for view etherscan tx From 4d1d4a11595b7024a8233761bd034d6ff579eb71 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 17 Oct 2018 20:40:09 -0700 Subject: [PATCH 14/19] Update Shapeshift logo url and adjust list item contents --- ui/app/components/shift-list-item.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index b87bf959e..0461b615a 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -52,12 +52,12 @@ ShiftListItem.prototype.render = function () { }, }, [ h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', + src: 'https://shapeshift.io/logo.png', style: { height: '35px', width: '132px', position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', + clip: 'rect(0px,30px,34px,0px)', }, }), ]), @@ -132,7 +132,6 @@ ShiftListItem.prototype.renderInfo = function () { case 'no_deposits': return h('.flex-column', { style: { - width: '200px', overflow: 'hidden', }, }, [ From ea214945cf88cef457147bd33a3017c8ea97956a Mon Sep 17 00:00:00 2001 From: William Chong Date: Fri, 19 Oct 2018 00:50:56 +0800 Subject: [PATCH 15/19] Set `NODE_ENV` when generating bundler (#4983) --- gulpfile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 5a468d2f3..0a0e3b3d5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -462,7 +462,9 @@ function generateBundler (opts, performBundle) { bundler.transform(envify({ METAMASK_DEBUG: opts.devMode, NODE_ENV: opts.devMode ? 'development' : 'production', - })) + }), { + global: true, + }) if (opts.watch) { bundler = watchify(bundler) From 75661673e5b2573ee9ab3a378130e4383c4c034f Mon Sep 17 00:00:00 2001 From: Esteban MIno Date: Fri, 19 Oct 2018 13:57:11 -0300 Subject: [PATCH 16/19] add support for wallet_watchAsset --- app/scripts/controllers/preferences.js | 2 +- test/unit/app/controllers/preferences-controller-test.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 8eb2bce0c..16620ca96 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -104,7 +104,7 @@ class PreferencesController { * @param {Function} - end */ async requestWatchAsset (req, res, next, end) { - if (req.method === 'metamask_watchAsset') { + if (req.method === 'metamask_watchAsset' || req.method === 'wallet_watchAsset') { const { type, options } = req.params switch (type) { case 'ERC20': diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index b5ccf3fb5..e81b072d5 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -375,6 +375,11 @@ describe('preferences controller', function () { await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) sandbox.assert.called(stubEnd) sandbox.assert.notCalled(stubNext) + req.method = 'wallet_watchAsset' + req.params.type = 'someasset' + await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) + sandbox.assert.calledTwice(stubEnd) + sandbox.assert.notCalled(stubNext) }) it('should through error if method is supported but asset type is not', async function () { req.method = 'metamask_watchAsset' From 7c4f98ffd6f5d7237d86cb7e1277ec44dec2db22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 19 Oct 2018 17:20:54 -0300 Subject: [PATCH 17/19] specific add and remove methods for frequentRpcList (#5554) --- app/scripts/controllers/preferences.js | 51 +++++++++---------- app/scripts/metamask-controller.js | 4 +- .../preferences-controller-test.js | 19 +++++++ 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 8eb2bce0c..689506a7a 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -374,22 +374,6 @@ class PreferencesController { return Promise.resolve(label) } - /** - * Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list. - * - * @param {string} _url The the new rpc url to add to the updated list - * @param {bool} remove Remove selected url - * @returns {Promise} Promise resolves with undefined - * - */ - updateFrequentRpcList (_url, remove = false) { - return this.addToFrequentRpcList(_url, remove) - .then((rpcList) => { - this.store.updateState({ frequentRpcList: rpcList }) - return Promise.resolve() - }) - } - /** * Setter for the `currentAccountTab` property * @@ -405,24 +389,39 @@ class PreferencesController { } /** - * Returns an updated rpcList based on the passed url and the current list. - * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the - * end of the list. The current list is modified and returned as a promise. + * Adds custom RPC url to state. * - * @param {string} _url The rpc url to add to the frequentRpcList. - * @param {bool} remove Remove selected url - * @returns {Promise} The updated frequentRpcList. + * @param {string} url The RPC url to add to frequentRpcList. + * @returns {Promise} Promise resolving to updated frequentRpcList. * */ - addToFrequentRpcList (_url, remove = false) { + addToFrequentRpcList (url) { const rpcList = this.getFrequentRpcList() - const index = rpcList.findIndex((element) => { return element === _url }) + const index = rpcList.findIndex((element) => { return element === url }) if (index !== -1) { rpcList.splice(index, 1) } - if (!remove && _url !== 'http://localhost:8545') { - rpcList.push(_url) + if (url !== 'http://localhost:8545') { + rpcList.push(url) } + this.store.updateState({ frequentRpcList: rpcList }) + return Promise.resolve(rpcList) + } + + /** + * Removes custom RPC url from state. + * + * @param {string} url The RPC url to remove from frequentRpcList. + * @returns {Promise} Promise resolving to updated frequentRpcList. + * + */ + removeFromFrequentRpcList (url) { + const rpcList = this.getFrequentRpcList() + const index = rpcList.findIndex((element) => { return element === url }) + if (index !== -1) { + rpcList.splice(index, 1) + } + this.store.updateState({ frequentRpcList: rpcList }) return Promise.resolve(rpcList) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 32ceb6790..7913662d4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1458,7 +1458,7 @@ module.exports = class MetamaskController extends EventEmitter { */ async setCustomRpc (rpcTarget) { this.networkController.setRpcTarget(rpcTarget) - await this.preferencesController.updateFrequentRpcList(rpcTarget) + await this.preferencesController.addToFrequentRpcList(rpcTarget) return rpcTarget } @@ -1467,7 +1467,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} rpcTarget - A RPC URL to delete. */ async delCustomRpc (rpcTarget) { - await this.preferencesController.updateFrequentRpcList(rpcTarget, true) + await this.preferencesController.removeFromFrequentRpcList(rpcTarget) } /** diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index b5ccf3fb5..02f421de2 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -479,5 +479,24 @@ describe('preferences controller', function () { assert.equal(preferencesController.store.getState().seedWords, 'foo bar baz') }) }) + + describe('on updateFrequentRpcList', function () { + it('should add custom RPC url to state', function () { + preferencesController.addToFrequentRpcList('rpc_url') + preferencesController.addToFrequentRpcList('http://localhost:8545') + assert.deepEqual(preferencesController.store.getState().frequentRpcList, ['rpc_url']) + preferencesController.addToFrequentRpcList('rpc_url') + assert.deepEqual(preferencesController.store.getState().frequentRpcList, ['rpc_url']) + }) + + it('should remove custom RPC url from state', function () { + preferencesController.addToFrequentRpcList('rpc_url') + assert.deepEqual(preferencesController.store.getState().frequentRpcList, ['rpc_url']) + preferencesController.removeFromFrequentRpcList('other_rpc_url') + preferencesController.removeFromFrequentRpcList('http://localhost:8545') + preferencesController.removeFromFrequentRpcList('rpc_url') + assert.deepEqual(preferencesController.store.getState().frequentRpcList, []) + }) + }) }) From 539597cb13e43ef0479091e17dce48b70833bce0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 20 Oct 2018 04:01:50 -0400 Subject: [PATCH 18/19] deps - fix gulp ref to gulp#v4.0.0 --- package-lock.json | 407 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 384 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09b66d261..e3322d21d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9932,12 +9932,22 @@ "requires": { "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "dev": true, + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "dev": true, "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -9947,7 +9957,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", - "dev": true, "requires": { "bn.js": "^4.11.0", "create-hash": "^1.1.2", @@ -10390,7 +10399,7 @@ "dependencies": { "babelify": { "version": "7.3.0", - "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "requires": { "babel-core": "^6.0.14", @@ -12779,7 +12788,8 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", @@ -12787,7 +12797,8 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -12890,7 +12901,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -12900,6 +12912,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -13017,7 +13030,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -13027,6 +13041,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -13132,6 +13147,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -14249,14 +14265,354 @@ "dev": true }, "glob-watcher": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-4.0.0.tgz", - "integrity": "sha1-nmOo/25h6TLebMLK7OUHGm1zcyk=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.1.tgz", + "integrity": "sha512-fK92r2COMC199WCyGUblrZKhjra3cyVMDiypDdqg1vsSDmexnbYivK1kNR4QItiNXLKmGlqan469ks67RtNa2g==", "requires": { "async-done": "^1.2.0", - "chokidar": "^1.4.3", + "chokidar": "^2.0.0", "just-debounce": "^1.0.0", "object.defaults": "^1.1.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + } + } + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" + } } }, "glob2base": { @@ -14381,10 +14737,10 @@ "dev": true }, "gulp": { - "version": "github:gulpjs/gulp#71c094a51c7972d26f557899ddecab0210ef3776", - "from": "github:gulpjs/gulp#4.0", + "version": "github:gulpjs/gulp#55eb23a268dcc7340bb40808600fd4802848c06f", + "from": "github:gulpjs/gulp#v4.0.0", "requires": { - "glob-watcher": "^4.0.0", + "glob-watcher": "^5.0.0", "gulp-cli": "^2.0.0", "undertaker": "^1.0.0", "vinyl-fs": "^3.0.0" @@ -21494,9 +21850,9 @@ "dev": true }, "mute-stdout": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.0.tgz", - "integrity": "sha1-WzLqB+tDyd7WEwQ0z5JvRrKn/U0=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==" }, "mute-stream": { "version": "0.0.7", @@ -26412,7 +26768,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" }, "printf": { @@ -33471,9 +33827,9 @@ "optional": true }, "v8flags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.0.tgz", - "integrity": "sha512-0m69VIK2dudEf2Ub0xwLQhZkDZu85OmiOpTw+UGDt56ibviYICHziM/3aE+oVg7IjGPp0c83w3eSVqa+lYZ9UQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", + "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", "requires": { "homedir-polyfill": "^1.0.1" } @@ -33700,9 +34056,12 @@ }, "dependencies": { "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "requires": { + "safe-buffer": "~5.1.1" + } } } }, diff --git a/package.json b/package.json index c994acc2f..ef72d7bfb 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "fast-levenshtein": "^2.0.6", "file-loader": "^1.1.11", "fuse.js": "^3.2.0", - "gulp": "github:gulpjs/gulp#4.0", + "gulp": "github:gulpjs/gulp#v4.0.0", "gulp-autoprefixer": "^5.0.0", "gulp-debug": "^3.2.0", "gulp-eslint": "^4.0.0", From b53a1f492c3339ef6fe070ac7d02b63a4393669c Mon Sep 17 00:00:00 2001 From: Brandon Wissmann Date: Sat, 20 Oct 2018 23:55:37 -0400 Subject: [PATCH 19/19] i18n - use localized names in language selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #4250 (#5559) * Added localized names for languages * Capital letter for Čeština * capital letter Русский * i18n - use localized names in language selection * fix build * package-lock.json build fix --- app/_locales/index.json | 42 ++++++++++++++++++++--------------------- package-lock.json | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/_locales/index.json b/app/_locales/index.json index 46933dc3f..234215e39 100644 --- a/app/_locales/index.json +++ b/app/_locales/index.json @@ -1,24 +1,24 @@ [ - { "code": "cs", "name": "Czech" }, - { "code": "de", "name": "German" }, + { "code": "cs", "name": "Čeština" }, + { "code": "de", "name": "Deutsche" }, { "code": "en", "name": "English" }, - { "code": "es", "name": "Spanish" }, - { "code": "fr", "name": "French" }, - { "code": "ht", "name": "Haitian Creole" }, - { "code": "hn", "name": "Hindi" }, - { "code": "it", "name": "Italian" }, - { "code": "ja", "name": "Japanese" }, - { "code": "ko", "name": "Korean" }, - { "code": "nl", "name": "Dutch" }, - { "code": "ph", "name": "Tagalog" }, - { "code": "pl", "name": "Polish" }, - { "code": "pt", "name": "Portuguese" }, - { "code": "ru", "name": "Russian" }, - { "code": "sl", "name": "Slovenian" }, - { "code": "th", "name": "Thai" }, - { "code": "tml", "name": "Tamil" }, - { "code": "tr", "name": "Turkish" }, - { "code": "vi", "name": "Vietnamese" }, - { "code": "zh_CN", "name": "Chinese (Simplified)" }, - { "code": "zh_TW", "name": "Chinese (Traditional)" } + { "code": "es", "name": "Español" }, + { "code": "fr", "name": "Français" }, + { "code": "ht", "name": "Kreyòl ayisyen" }, + { "code": "hn", "name": "हिन्दी" }, + { "code": "it", "name": "Italiano" }, + { "code": "ja", "name": "日本語" }, + { "code": "ko", "name": "한국어" }, + { "code": "nl", "name": "Nederlands" }, + { "code": "ph", "name": "Pilipino" }, + { "code": "pl", "name": "Polskie" }, + { "code": "pt", "name": "Português" }, + { "code": "ru", "name": "Русский" }, + { "code": "sl", "name": "Slovenščina" }, + { "code": "th", "name": "ไทย" }, + { "code": "tml", "name": "தமிழ்" }, + { "code": "tr", "name": "Türkçe" }, + { "code": "vi", "name": "Tiếng Việt" }, + { "code": "zh_CN", "name": "中文(简体)" }, + { "code": "zh_TW", "name": "中文(繁體)" } ] diff --git a/package-lock.json b/package-lock.json index e3322d21d..22f0f3ab9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35267,4 +35267,4 @@ "dev": true } } -} +} \ No newline at end of file