From 222e62d7f10ffe22dd606aea9c15e1547986c4ab Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Mon, 17 Sep 2018 20:04:10 -0700 Subject: Bug Fix: #1789 and #4525 eth.getCode() with no contract --- .../confirm-transaction-base/confirm-transaction-base.component.js | 2 +- ui/app/components/send/send.utils.js | 2 +- ui/app/constants/error-keys.js | 1 + ui/app/helpers/transactions.util.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) (limited to 'ui') 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..9e6341722 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 @@ -131,7 +131,7 @@ export default class ConfirmTransactionBase extends Component { if (simulationFails) { return { valid: true, - errorKey: TRANSACTION_ERROR_KEY, + errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY, } } diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js index a18a9e4b3..05ba6b88f 100644 --- a/ui/app/components/send/send.utils.js +++ b/ui/app/components/send/send.utils.js @@ -215,7 +215,7 @@ async function estimateGas ({ // if recipient has no code, gas is 21k max: if (!selectedToken && !data) { const code = Boolean(to) && await global.eth.getCode(to) - if (!code || code === '0x') { + if (!code || code === '0x' || code === '0x0') { // Infura will return '0x', and Ganache will return '0x0' return SIMPLE_GAS_COST } } else if (selectedToken && !to) { diff --git a/ui/app/constants/error-keys.js b/ui/app/constants/error-keys.js index f70ed3b19..704064c96 100644 --- a/ui/app/constants/error-keys.js +++ b/ui/app/constants/error-keys.js @@ -1,3 +1,4 @@ export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds' export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow' export const TRANSACTION_ERROR_KEY = 'transactionError' +export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract' diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index f7d249e63..0eb7972d6 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -114,7 +114,7 @@ export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') export async function isSmartContractAddress (address) { const code = await global.eth.getCode(address) - return code && code !== '0x' + return code && code !== '0x' && code !== '0x0'; // Infura will return '0x', and Ganache will return '0x0' } export function sumHexes (...args) { -- cgit v1.2.3 From 4cc0b1ef01573e1541d18bdcd89650e1db32ae9a Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Fri, 28 Sep 2018 11:01:34 -0700 Subject: ganache-core merged my PR, so I changed some comments to clarify that ganache-core v2.2.1 and below will return the non-standard '0x0' --- ui/app/components/send/send.utils.js | 2 +- ui/app/helpers/transactions.util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js index 05ba6b88f..af7b3823f 100644 --- a/ui/app/components/send/send.utils.js +++ b/ui/app/components/send/send.utils.js @@ -215,7 +215,7 @@ async function estimateGas ({ // if recipient has no code, gas is 21k max: if (!selectedToken && !data) { const code = Boolean(to) && await global.eth.getCode(to) - if (!code || code === '0x' || code === '0x0') { // Infura will return '0x', and Ganache will return '0x0' + if (!code || code === '0x' || code === '0x0') { // Infura will return '0x', and ganache-core v2.2.1 will return '0x0' return SIMPLE_GAS_COST } } else if (selectedToken && !to) { diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index 0eb7972d6..b2c617384 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -114,7 +114,7 @@ export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') export async function isSmartContractAddress (address) { const code = await global.eth.getCode(address) - return code && code !== '0x' && code !== '0x0'; // Infura will return '0x', and Ganache will return '0x0' + return code && code !== '0x' && code !== '0x0' // Infura will return '0x', and ganache-core v2.2.1 will return '0x0' } export function sumHexes (...args) { -- cgit v1.2.3 From 093ca07201393930f3efce88ef2283a69177e1d2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 10 Oct 2018 01:12:25 -0700 Subject: Set maxMode and toNickname defaults for clear_send --- ui/app/reducers/metamask.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'ui') diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 3f1d3394f..de19def6d 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -291,8 +291,10 @@ function reduceMetamask (state, action) { amount: '0x0', memo: '', errors: {}, + maxModeOn: false, editingTransactionId: null, forceGasMin: null, + toNickname: '', }, }) @@ -330,9 +332,9 @@ function reduceMetamask (state, action) { }) case actions.SET_USE_BLOCKIE: - return extend(metamaskState, { - useBlockie: action.value, - }) + return extend(metamaskState, { + useBlockie: action.value, + }) case actions.UPDATE_FEATURE_FLAGS: return extend(metamaskState, { -- cgit v1.2.3 From c821a8354c8eba05885ca219f39aedafbd4f8052 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Tue, 16 Oct 2018 06:00:47 +0800 Subject: Add txReceipt data to transaction details (#5513) --- .../transaction-activity-log.util.js | 6 +++- .../transaction-breakdown.component.js | 20 +++++++++++-- .../transaction-list-item.component.js | 3 +- ui/app/components/transaction-status/index.scss | 7 ++++- ui/app/helpers/tests/transactions.util.test.js | 35 ++++++++++++++++++++++ ui/app/helpers/transactions.util.js | 18 +++++++++++ 6 files changed, 84 insertions(+), 5 deletions(-) (limited to 'ui') diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js index 32834ff47..97aa9a8f1 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js @@ -46,11 +46,15 @@ export function getActivities (transaction) { if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) { const { time, txParams: { value } = {} } = base return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value)) + // An entry in the history may be an array of more sub-entries. } else if (Array.isArray(base)) { const events = [] base.forEach(entry => { - const { op, path, value, timestamp } = entry + const { op, path, value, timestamp: entryTimestamp } = entry + // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a + // timestamp, the first sub-entry in a history entry should. + const timestamp = entryTimestamp || base[0] && base[0].timestamp if (path in eventPathsHash && op === REPLACE_OP) { switch (path) { diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js index bb6075e9f..5a2b4a481 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js @@ -26,8 +26,11 @@ export default class TransactionBreakdown extends PureComponent { render () { const { t } = this.context const { transaction, className } = this.props - const { txParams: { gas, gasPrice, value } = {} } = transaction - const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice }) + const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction + + const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas + + const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice }) const totalInHex = sumHexes(hexGasTotal, value) return ( @@ -52,6 +55,19 @@ export default class TransactionBreakdown extends PureComponent { value={gas} /> + { + typeof gasUsed === 'string' && ( + + + + ) + } { assert.doesNotThrow(() => utils.getTokenData()) }) }) + + describe('getStatusKey', () => { + it('should return the correct status', () => { + const tests = [ + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x0', + }, + }, + expected: 'failed', + }, + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x1', + }, + }, + expected: 'confirmed', + }, + { + transaction: { + status: 'pending', + }, + expected: 'pending', + }, + ] + + tests.forEach(({ transaction, expected }) => { + assert.equal(utils.getStatusKey(transaction), expected) + }) + }) + }) }) diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index f7d249e63..e50196196 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -126,3 +126,21 @@ export function sumHexes (...args) { return ethUtil.addHexPrefix(total) } + +/** + * Returns a status key for a transaction. Requires parsing the txMeta.txReceipt on top of + * txMeta.status because txMeta.status does not reflect on-chain errors. + * @param {Object} transaction - The txMeta object of a transaction. + * @param {Object} transaction.txReceipt - The transaction receipt. + * @returns {string} + */ +export function getStatusKey (transaction) { + const { txReceipt: { status } = {} } = transaction + + // There was an on-chain failure + if (status === '0x0') { + return 'failed' + } + + return transaction.status +} -- cgit v1.2.3 From 8c33c018dead721d7ad3b86d75a8a7abcade3938 Mon Sep 17 00:00:00 2001 From: "Michael J. Cohen" Date: Mon, 15 Oct 2018 19:02:57 -0400 Subject: add tooltip for view etherscan tx (#5518) * add tooltip for view etherscan tx * run lint --- .../transaction-list-item-details.component.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'ui') diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js index 13cb51349..a4f28fd63 100644 --- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js @@ -5,6 +5,7 @@ import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.consta import TransactionActivityLog from '../transaction-activity-log' import TransactionBreakdown from '../transaction-breakdown' import Button from '../button' +import Tooltip from '../tooltip' import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' export default class TransactionListItemDetails extends PureComponent { @@ -75,13 +76,15 @@ export default class TransactionListItemDetails extends PureComponent { ) } - + + +
-- cgit v1.2.3 From bdb6e55354ad01fbf0741d0adb99cab8082f6da2 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 15 Oct 2018 20:56:24 -0230 Subject: Decrease click area for AmountMaxButton on send screen --- .../amount-max-button.component.js | 34 +++++++++++++--------- .../tests/amount-max-button-component.test.js | 9 +++--- 2 files changed, 24 insertions(+), 19 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js index 4d0d36ab4..ceb620941 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -34,21 +34,27 @@ export default class AmountMaxButton extends Component { }) } + onMaxClick = (event) => { + const { setMaxModeTo } = this.props + + event.preventDefault() + setMaxModeTo(true) + this.setMaxAmount() + } + render () { - const { setMaxModeTo, maxModeOn } = this.props - - return ( -
{ - event.preventDefault() - setMaxModeTo(true) - this.setMaxAmount() - }} - > - {!maxModeOn ? this.context.t('max') : ''} -
- ) + return this.props.maxModeOn + ? null + : ( +
+ + {this.context.t('max')} + +
+ ) } } diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js index 86a05ff21..b04d3897f 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -56,9 +56,8 @@ describe('AmountMaxButton Component', function () { }) describe('render', () => { - it('should render a div with a send-v2__amount-max class', () => { - assert.equal(wrapper.find('.send-v2__amount-max').length, 1) - assert(wrapper.find('.send-v2__amount-max').is('div')) + it('should render an element with a send-v2__amount-max class', () => { + assert(wrapper.exists('.send-v2__amount-max')) }) it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { @@ -77,9 +76,9 @@ describe('AmountMaxButton Component', function () { ) }) - it('should not render text when maxModeOn is true', () => { + it('should not render anything when maxModeOn is true', () => { wrapper.setProps({ maxModeOn: true }) - assert.equal(wrapper.find('.send-v2__amount-max').text(), '') + assert.ok(!wrapper.exists('.send-v2__amount-max')) }) it('should render the expected text when maxModeOn is false', () => { -- cgit v1.2.3 From bd357280410d026c4839576658a20f9b95b0de88 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 16 Oct 2018 16:36:11 -0230 Subject: Lower i18n-helper#getMessage log level from error to warning --- ui/i18n-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui') 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] -- cgit v1.2.3 From badebe017fe28b58ac742082368484c3a4b1c1bc Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Wed, 17 Oct 2018 07:03:29 +0800 Subject: 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 --- ui/app/actions.js | 36 +++ ui/app/components/account-menu/index.js | 11 +- ui/app/components/balance-component.js | 28 +- .../confirm-detail-row.component.js | 66 +++-- .../confirm-detail-row/index.scss | 12 +- .../tests/confirm-detail-row.component.test.js | 38 +-- .../confirm-page-container-content.component.js | 7 +- .../confirm-page-container-summary.component.js | 19 +- .../confirm-page-container.component.js | 5 +- .../currency-display/currency-display.component.js | 11 +- .../currency-display/currency-display.container.js | 25 +- ui/app/components/currency-display/index.scss | 10 + .../tests/currency-display.container.test.js | 21 +- .../currency-input/currency-input.component.js | 120 ++++++++ .../currency-input/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 +- .../cancel-transaction-gas-fee.component.test.js | 9 +- .../confirm-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 ------------- ui/app/components/send/currency-display/index.js | 1 - .../tests/currency-display.test.js | 91 ------ .../send-amount-row/send-amount-row.component.js | 45 ++- .../tests/send-amount-row-component.test.js | 27 +- .../gas-fee-display/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 +- .../tests/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 +++ .../unit-input/tests/unit-input.component.test.js | 146 ++++++++++ .../components/unit-input/unit-input.component.js | 104 +++++++ .../user-preferenced-currency-display/index.js | 1 + ...-preferenced-currency-display.component.test.js | 34 +++ ...-preferenced-currency-display.container.test.js | 105 +++++++ .../user-preferenced-currency-display.component.js | 45 +++ .../user-preferenced-currency-display.container.js | 52 ++++ .../user-preferenced-currency-input/index.js | 1 + ...er-preferenced-currency-input.component.test.js | 32 +++ ...er-preferenced-currency-input.container.test.js | 31 +++ .../user-preferenced-currency-input.component.js | 20 ++ .../user-preferenced-currency-input.container.js | 13 + .../user-preferenced-token-input/index.js | 1 + .../user-preferenced-token-input.component.test.js | 32 +++ .../user-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 | 62 +++-- .../ducks/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 + 71 files changed, 2370 insertions(+), 585 deletions(-) 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 (limited to 'ui') 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)), + h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, { + value: balanceValue, + type: PRIMARY, + ethNumberOfDecimals: 3, + })), - showFiat && h(CurrencyDisplay, { + 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') } + + ) : ( + + ) + } + + renderPrimaryTotalTextOverride () { + const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props + const tokensText = `${tokenAmount} ${tokenSymbol}` + + return ( +
+ { `${tokensText} + ` } + + { ethTransactionTotal } +
+ ) } - getFiatTotalTextOverride () { + 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) - - dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal })) + console.log('HIHIH', value, hexTransactionFee) + const hexTransactionTotal = sumHexes(value, hexTransactionFee) + + 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 +} -- cgit v1.2.3 From 42fa54678fbb1a170a8b88e3fec54d7d9d44c303 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Tue, 16 Oct 2018 18:24:34 -0230 Subject: Extract Add Token button into its own component --- .../add-token-button/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 ++++++++++++------- ui/app/css/itcss/components/newui-sections.scss | 12 -------- 6 files changed, 82 insertions(+), 23 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 (limited to 'ui') 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) { -- cgit v1.2.3 From 4d1d4a11595b7024a8233761bd034d6ff579eb71 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 17 Oct 2018 20:40:09 -0700 Subject: Update Shapeshift logo url and adjust list item contents --- ui/app/components/shift-list-item.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'ui') 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', }, }, [ -- cgit v1.2.3 From 73ec4e66cb7a476d01371a61692b0d8d9224da04 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 20 Oct 2018 03:14:59 -0400 Subject: sentry - include app state in ui errors --- ui/app/reducers.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) (limited to 'ui') diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 80e76d570..e1a982f93 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -1,3 +1,4 @@ +const clone = require('clone') const extend = require('xtend') const copyToClipboard = require('copy-to-clipboard') @@ -52,19 +53,24 @@ function rootReducer (state, action) { return state } +window.getCleanAppState = function () { + const state = clone(window.METAMASK_CACHED_LOG_STATE) + // append additional information + state.version = global.platform.getVersion() + state.browser = window.navigator.userAgent + // ensure seedWords are not included + if (state.metamask) delete state.metamask.seedWords + if (state.appState.currentView) delete state.appState.currentView.seedWords + return state +} + window.logStateString = function (cb) { - const state = window.METAMASK_CACHED_LOG_STATE - const version = global.platform.getVersion() - const browser = window.navigator.userAgent - return global.platform.getPlatformInfo((err, platform) => { - if (err) { - return cb(err) - } - state.version = version + const state = window.getCleanAppState() + global.platform.getPlatformInfo((err, platform) => { + if (err) return cb(err) state.platform = platform - state.browser = browser const stateString = JSON.stringify(state, removeSeedWords, 2) - return cb(null, stateString) + cb(null, stateString) }) } -- cgit v1.2.3 From fda101912bbb99f7f1adbac9856d34105390c408 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sun, 21 Oct 2018 00:52:41 -0400 Subject: ui - use variable to clarify result of emptiness check --- ui/app/components/send/send.utils.js | 4 +++- ui/app/helpers/transactions.util.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js index af7b3823f..eb1667c63 100644 --- a/ui/app/components/send/send.utils.js +++ b/ui/app/components/send/send.utils.js @@ -215,7 +215,9 @@ async function estimateGas ({ // if recipient has no code, gas is 21k max: if (!selectedToken && !data) { const code = Boolean(to) && await global.eth.getCode(to) - if (!code || code === '0x' || code === '0x0') { // Infura will return '0x', and ganache-core v2.2.1 will return '0x0' + // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + if (codeIsEmpty) { return SIMPLE_GAS_COST } } else if (selectedToken && !to) { diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index 9be77e14f..cfe2c4229 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -114,7 +114,9 @@ export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') export async function isSmartContractAddress (address) { const code = await global.eth.getCode(address) - return code && code !== '0x' && code !== '0x0' // Infura will return '0x', and ganache-core v2.2.1 will return '0x0' + // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + return !codeIsEmpty } export function sumHexes (...args) { -- cgit v1.2.3 From 614995c0e933fcc984126eee20fb7dd4533e8e5b Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Sun, 21 Oct 2018 19:12:40 +0800 Subject: Fix account display width for large currency values --- ui/app/components/balance-component.js | 5 +++-- .../currency-display/currency-display.component.js | 13 +++++++++++-- .../currency-display/currency-display.container.js | 5 +++-- ui/app/components/currency-display/index.scss | 4 ++++ .../tests/currency-display.container.test.js | 13 ++++++++++--- .../currency-input/tests/currency-input.component.test.js | 12 ++++++------ .../token-input/tests/token-input.component.test.js | 12 ++++++------ ui/app/components/transaction-view-balance/index.scss | 5 ++++- ui/app/css/itcss/components/wallet-balance.scss | 5 ++--- 9 files changed, 49 insertions(+), 25 deletions(-) (limited to 'ui') diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index e1fcf08e0..c1b713ccd 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -81,11 +81,12 @@ BalanceComponent.prototype.renderBalance = function () { } return h('div.flex-column.balance-display', {}, [ - h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, { + h(UserPreferencedCurrencyDisplay, { + className: 'token-amount', value: balanceValue, type: PRIMARY, ethNumberOfDecimals: 3, - })), + }), showFiat && h(UserPreferencedCurrencyDisplay, { value: balanceValue, diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js index 5f5717be3..f39e60269 100644 --- a/ui/app/components/currency-display/currency-display.component.js +++ b/ui/app/components/currency-display/currency-display.component.js @@ -10,6 +10,7 @@ export default class CurrencyDisplay extends PureComponent { prefix: PropTypes.string, prefixComponent: PropTypes.node, style: PropTypes.object, + suffix: PropTypes.string, // Used in container currency: PropTypes.oneOf([ETH]), denomination: PropTypes.oneOf([GWEI]), @@ -19,17 +20,25 @@ export default class CurrencyDisplay extends PureComponent { } render () { - const { className, displayValue, prefix, prefixComponent, style } = this.props + const { className, displayValue, prefix, prefixComponent, style, suffix } = this.props const text = `${prefix || ''}${displayValue}` + const title = `${text} ${suffix}` return (
{ prefixComponent} { text } + { + suffix && ( + + { suffix } + + ) + }
) } diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js index b387229b5..1b3fe74da 100644 --- a/ui/app/components/currency-display/currency-display.container.js +++ b/ui/app/components/currency-display/currency-display.container.js @@ -26,14 +26,15 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination, }) - const formattedValue = formatCurrency(convertedValue, toCurrency) - const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}` + const displayValue = formatCurrency(convertedValue, toCurrency) + const suffix = hideLabel ? undefined : toCurrency.toUpperCase() return { ...restStateProps, ...dispatchProps, ...restOwnProps, displayValue, + suffix, } } diff --git a/ui/app/components/currency-display/index.scss b/ui/app/components/currency-display/index.scss index 8c0196102..313c932b8 100644 --- a/ui/app/components/currency-display/index.scss +++ b/ui/app/components/currency-display/index.scss @@ -7,4 +7,8 @@ overflow: hidden; text-overflow: ellipsis; } + + &__suffix { + padding-left: 4px; + } } 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 b9f98c543..fb6678776 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 @@ -45,7 +45,8 @@ describe('CurrencyDisplay container', () => { currency: 'usd', }, result: { - displayValue: '$2.80 USD', + displayValue: '$2.80', + suffix: 'USD', }, }, { @@ -53,7 +54,8 @@ describe('CurrencyDisplay container', () => { value: '0x2386f26fc10000', }, result: { - displayValue: '$2.80 USD', + displayValue: '$2.80', + suffix: 'USD', }, }, { @@ -63,7 +65,8 @@ describe('CurrencyDisplay container', () => { numberOfDecimals: 3, }, result: { - displayValue: '1.266 ETH', + displayValue: '1.266', + suffix: 'ETH', }, }, { @@ -75,6 +78,7 @@ describe('CurrencyDisplay container', () => { }, result: { displayValue: '1.266', + suffix: undefined, }, }, { @@ -86,6 +90,7 @@ describe('CurrencyDisplay container', () => { }, result: { displayValue: '1', + suffix: undefined, }, }, { @@ -97,6 +102,7 @@ describe('CurrencyDisplay container', () => { }, result: { displayValue: '1000000000', + suffix: undefined, }, }, { @@ -108,6 +114,7 @@ describe('CurrencyDisplay container', () => { }, result: { displayValue: '1e-9', + suffix: undefined, }, }, ] 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 index 8de0ef863..5db53066f 100644 --- a/ui/app/components/currency-input/tests/currency-input.component.test.js +++ b/ui/app/components/currency-input/tests/currency-input.component.test.js @@ -69,7 +69,7 @@ describe('CurrencyInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD') }) it('should render properly with a fiat value', () => { @@ -100,7 +100,7 @@ describe('CurrencyInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH') }) }) @@ -140,14 +140,14 @@ describe('CurrencyInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD') 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(wrapper.find('.currency-display-component').text(), '$231.06USD') assert.equal(currencyInputInstance.state.decimalValue, 1) assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') @@ -185,14 +185,14 @@ describe('CurrencyInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '0ETH') 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(wrapper.find('.currency-display-component').text(), '0.004328ETH') assert.equal(currencyInputInstance.state.decimalValue, 1) assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') 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 index 2131e7705..2dacb9bc4 100644 --- a/ui/app/components/token-input/tests/token-input.component.test.js +++ b/ui/app/components/token-input/tests/token-input.component.test.js @@ -122,7 +122,7 @@ describe('TokenInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '2ETH') }) it('should render properly with a token value for fiat', () => { @@ -157,7 +157,7 @@ describe('TokenInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD') }) }) @@ -201,14 +201,14 @@ describe('TokenInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '0ETH') 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(wrapper.find('.currency-display-component').text(), '2ETH') assert.equal(tokenInputInstance.state.decimalValue, 1) assert.equal(tokenInputInstance.state.hexValue, '2710') @@ -250,14 +250,14 @@ describe('TokenInput Component', () => { 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') + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD') 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(wrapper.find('.currency-display-component').text(), '$462.12USD') assert.equal(tokenInputInstance.state.decimalValue, 1) assert.equal(tokenInputInstance.state.hexValue, '2710') diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss index 12045ab6d..190072be0 100644 --- a/ui/app/components/transaction-view-balance/index.scss +++ b/ui/app/components/transaction-view-balance/index.scss @@ -4,11 +4,13 @@ align-items: center; flex: 1; height: 54px; + min-width: 0; &__balance { - margin-left: 12px; + margin: 0 12px; display: flex; flex-direction: column; + min-width: 0; @media screen and (max-width: $break-small) { align-items: center; @@ -45,6 +47,7 @@ display: flex; flex-direction: row; align-items: center; + min-width: 0; @media screen and (max-width: $break-small) { flex-direction: column; diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss index 293771550..3c3349ae0 100644 --- a/ui/app/css/itcss/components/wallet-balance.scss +++ b/ui/app/css/itcss/components/wallet-balance.scss @@ -29,7 +29,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( align-items: center; margin: 20px 24px; flex-direction: row; - flex-grow: 3; + min-width: 0; @media #{$wallet-balance-breakpoint-range} { margin: 10% 4%; @@ -38,8 +38,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( .balance-display { margin-left: 15px; - justify-content: flex-start; - align-items: flex-start; + min-width: 0; .token-amount { font-size: 1.5rem; -- cgit v1.2.3 From 688ef2ed0137cb7f860a182f16860164a4ba90f2 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Tue, 23 Oct 2018 07:15:38 +0800 Subject: Fix signature request styling issues (#5574) --- ui/app/css/itcss/components/request-signature.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss index 445b9ebf5..6c950d846 100644 --- a/ui/app/css/itcss/components/request-signature.scss +++ b/ui/app/css/itcss/components/request-signature.scss @@ -19,7 +19,7 @@ } @media screen and (min-width: $break-large) { - max-height: 620px; + height: 620px; } } -- cgit v1.2.3 From 7852269ed156787eb279e0b55d643c4c13c04020 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Tue, 23 Oct 2018 19:59:04 +0800 Subject: Add Activity Log entry for onchain failures for a transaction. Change scrolling of the transaction list. Remove Transaction Details modal. (#5581) --- ui/app/components/modals/modal.js | 14 ------ .../components/modals/transaction-details/index.js | 1 - .../transaction-details.component.js | 54 ---------------------- .../transaction-details.container.js | 4 -- .../tests/send-amount-row-component.test.js | 1 - .../transaction-activity-log.component.js | 10 ++-- .../transaction-activity-log.util.js | 11 ++++- ui/app/components/transaction-list-item/index.scss | 2 + .../transaction-list-item.component.js | 19 +------- .../transaction-list-item.container.js | 10 ---- ui/app/components/transaction-list/index.scss | 3 -- ui/app/components/transaction-view/index.scss | 1 + ui/app/components/wallet-view.js | 1 - ui/app/ducks/confirm-transaction.duck.js | 1 - 14 files changed, 20 insertions(+), 112 deletions(-) delete mode 100644 ui/app/components/modals/transaction-details/index.js delete mode 100644 ui/app/components/modals/transaction-details/transaction-details.component.js delete mode 100644 ui/app/components/modals/transaction-details/transaction-details.container.js (limited to 'ui') diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 15ca9deaa..338229a28 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -27,7 +27,6 @@ import TransactionConfirmed from './transaction-confirmed' import ConfirmCustomizeGasModal from './customize-gas' import CancelTransaction from './cancel-transaction' import WelcomeBeta from './welcome-beta' -import TransactionDetails from './transaction-details' import RejectTransactions from './reject-transactions' const modalContainerBaseStyle = { @@ -366,19 +365,6 @@ const MODALS = { }, }, - TRANSACTION_DETAILS: { - contents: h(TransactionDetails), - mobileModalStyle: { - ...modalContainerMobileStyle, - }, - laptopModalStyle: { - ...modalContainerLaptopStyle, - }, - contentStyle: { - borderRadius: '8px', - }, - }, - REJECT_TRANSACTIONS: { contents: h(RejectTransactions), mobileModalStyle: { diff --git a/ui/app/components/modals/transaction-details/index.js b/ui/app/components/modals/transaction-details/index.js deleted file mode 100644 index 1fc42c662..000000000 --- a/ui/app/components/modals/transaction-details/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './transaction-details.container' diff --git a/ui/app/components/modals/transaction-details/transaction-details.component.js b/ui/app/components/modals/transaction-details/transaction-details.component.js deleted file mode 100644 index f2fec3409..000000000 --- a/ui/app/components/modals/transaction-details/transaction-details.component.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import Modal from '../../modal' -import TransactionListItemDetails from '../../transaction-list-item-details' -import { hexToDecimal } from '../../../helpers/conversions.util' - -export default class TransactionConfirmed extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - hideModal: PropTypes.func, - transaction: PropTypes.object, - onRetry: PropTypes.func, - showRetry: PropTypes.bool, - onCancel: PropTypes.func, - showCancel: PropTypes.bool, - } - - handleSubmit = () => { - this.props.hideModal() - } - - handleRetry = () => { - const { onRetry, hideModal } = this.props - - Promise.resolve(onRetry()).then(() => hideModal()) - } - - render () { - const { t } = this.context - const { transaction, showRetry, onCancel, showCancel } = this.props - const { txParams: { nonce } = {} } = transaction - const decimalNonce = nonce && hexToDecimal(nonce) - - return ( - - onCancel()} - showCancel={showCancel} - /> - - ) - } -} diff --git a/ui/app/components/modals/transaction-details/transaction-details.container.js b/ui/app/components/modals/transaction-details/transaction-details.container.js deleted file mode 100644 index f212920bb..000000000 --- a/ui/app/components/modals/transaction-details/transaction-details.container.js +++ /dev/null @@ -1,4 +0,0 @@ -import TransactionDetails from './transaction-details.component' -import withModalProps from '../../../higher-order-components/with-modal-props' - -export default withModalProps(TransactionDetails) 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 e63db4a2d..56e80cb83 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 @@ -151,7 +151,6 @@ describe('SendAmountRow Component', function () { }) 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)) }) diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js index c4cf57d14..0e6c2376f 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js @@ -27,10 +27,14 @@ export default class TransactionActivityLog extends PureComponent { } componentDidUpdate (prevProps) { - const { transaction: { history: prevHistory = [] } = {} } = prevProps - const { transaction: { history = [] } = {} } = this.props + const { + transaction: { history: prevHistory = [], txReceipt: { status: prevStatus } = {} } = {}, + } = prevProps + const { + transaction: { history = [], txReceipt: { status } = {} } = {}, + } = this.props - if (prevHistory.length !== history.length) { + if (prevHistory.length !== history.length || prevStatus !== status) { this.setActivites() } } diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js index 97aa9a8f1..16597ae1a 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js @@ -18,6 +18,7 @@ const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted' const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed' const TRANSACTION_DROPPED_EVENT = 'transactionDropped' const TRANSACTION_UPDATED_EVENT = 'transactionUpdated' +const TRANSACTION_ERRORED_EVENT = 'transactionErrored' const eventPathsHash = { [STATUS_PATH]: true, @@ -39,9 +40,9 @@ function eventCreator (eventKey, timestamp, value) { } export function getActivities (transaction) { - const { history = [] } = transaction + const { history = [], txReceipt: { status } = {} } = transaction - return history.reduce((acc, base) => { + const historyActivities = history.reduce((acc, base) => { // First history item should be transaction creation if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) { const { time, txParams: { value } = {} } = base @@ -83,4 +84,10 @@ export function getActivities (transaction) { return acc }, []) + + // If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction, + // so we add an error entry to the Activity Log. + return status === '0x0' + ? historyActivities.concat(eventCreator(TRANSACTION_ERRORED_EVENT)) + : historyActivities } diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss index 9d694546b..ac0e7beeb 100644 --- a/ui/app/components/transaction-list-item/index.scss +++ b/ui/app/components/transaction-list-item/index.scss @@ -85,6 +85,7 @@ text-align: end; grid-area: primary-amount; align-self: end; + justify-self: end; @media screen and (max-width: $break-small) { padding-bottom: 2px; @@ -97,6 +98,7 @@ color: #5e6064; grid-area: secondary-amount; align-self: start; + justify-self: end; } } 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 88573d2d5..696634fe0 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 @@ -10,7 +10,6 @@ import TransactionListItemDetails from '../transaction-list-item-details' import { CONFIRM_TRANSACTION_ROUTE } from '../../routes' import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions' import { PRIMARY, SECONDARY } from '../../constants/common' -import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums' import { getStatusKey } from '../../helpers/transactions.util' export default class TransactionListItem extends PureComponent { @@ -24,7 +23,6 @@ export default class TransactionListItem extends PureComponent { showCancelModal: PropTypes.func, showCancel: PropTypes.bool, showRetry: PropTypes.bool, - showTransactionDetailsModal: PropTypes.func, token: PropTypes.object, tokenData: PropTypes.object, transaction: PropTypes.object, @@ -39,31 +37,16 @@ export default class TransactionListItem extends PureComponent { const { transaction, history, - showTransactionDetailsModal, - methodData, - showCancel, - showRetry, } = this.props const { id, status } = transaction const { showTransactionDetails } = this.state - const windowType = window.METAMASK_UI_TYPE if (status === UNAPPROVED_STATUS) { history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`) return } - if (windowType === ENVIRONMENT_TYPE_FULLSCREEN) { - this.setState({ showTransactionDetails: !showTransactionDetails }) - } else { - showTransactionDetailsModal({ - transaction, - onRetry: this.handleRetry, - showRetry: showRetry && methodData.done, - onCancel: this.handleCancel, - showCancel, - }) - } + this.setState({ showTransactionDetails: !showTransactionDetails }) } handleCancel = () => { diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js index 72f5f5d61..62ed7a73f 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js @@ -28,16 +28,6 @@ const mapDispatchToProps = dispatch => { showCancelModal: (transactionId, originalGasPrice) => { return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice })) }, - showTransactionDetailsModal: ({ transaction, onRetry, showRetry, onCancel, showCancel }) => { - return dispatch(showModal({ - name: 'TRANSACTION_DETAILS', - transaction, - onRetry, - showRetry, - onCancel, - showCancel, - })) - }, } } diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss index 777f701f9..ba7ffd87b 100644 --- a/ui/app/components/transaction-list/index.scss +++ b/ui/app/components/transaction-list/index.scss @@ -2,9 +2,7 @@ display: flex; flex-direction: column; flex: 1; - overflow-y: hidden; margin-top: 8px; - border-top: 1px solid $geyser; &__completed-transactions { display: flex; @@ -26,7 +24,6 @@ &__transactions { flex: 1; - overflow-y: auto; } &__pending-transactions { diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/transaction-view/index.scss index af9771ce0..13187f0e5 100644 --- a/ui/app/components/transaction-view/index.scss +++ b/ui/app/components/transaction-view/index.scss @@ -4,6 +4,7 @@ min-width: 0; display: flex; flex-direction: column; + overflow-y: auto; &__balance-wrapper { @media screen and (max-width: $break-small) { diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 8a7cb0f8d..0a85e41d1 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -127,7 +127,6 @@ WalletView.prototype.render = function () { identities, } = this.props // temporary logs + fake extra wallets - // console.log('walletview, selectedAccount:', selectedAccount) const checksummedAddress = checksumAddress(selectedAddress) diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index 2ceafbe08..328943cd3 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -329,7 +329,6 @@ export function updateTxDataAndCalculate (txData) { const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount) const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount) - console.log('HIHIH', value, hexTransactionFee) const hexTransactionTotal = sumHexes(value, hexTransactionFee) dispatch(updateTransactionTotals({ -- cgit v1.2.3 From 315028ec53654c4ad7817741330627eae742e59d Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Wed, 24 Oct 2018 23:27:16 +0800 Subject: Fix UnknownFunction showing as Confirm (#5590) --- ui/app/helpers/transactions.util.js | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'ui') diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index e50196196..64ec82225 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -27,10 +27,21 @@ export function getTokenData (data = '') { const registry = new MethodRegistry({ provider: global.ethereumProvider }) +/** + * Attempts to return the method data from the MethodRegistry library, if the method exists in the + * registry. Otherwise, returns an empty object. + * @param {string} data - The hex data (@code txParams.data) of a transaction + * @returns {Object} + */ export async function getMethodData (data = '') { const prefixedData = ethUtil.addHexPrefix(data) const fourBytePrefix = prefixedData.slice(0, 10) const sig = await registry.lookup(fourBytePrefix) + + if (!sig) { + return {} + } + const parsedResult = registry.parse(sig) return { -- cgit v1.2.3 From 554f79c0e2ec8f3ddce81ab7aae9dc67ba057ac9 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Thu, 25 Oct 2018 17:21:41 +0800 Subject: Fix blockies icons overriding contract map icons. Refactor Identicon component (#5599) --- ui/app/components/account-dropdowns.js | 3 +- ui/app/components/account-menu/index.js | 2 +- ui/app/components/account-panel.js | 2 +- .../components/app-header/app-header.component.js | 2 +- ui/app/components/balance-component.js | 4 +- .../dropdowns/components/account-dropdowns.js | 2 +- ui/app/components/identicon.js | 124 --------------------- ui/app/components/identicon/identicon.component.js | 99 ++++++++++++++++ ui/app/components/identicon/identicon.container.js | 12 ++ ui/app/components/identicon/index.js | 1 + ui/app/components/identicon/index.scss | 7 ++ .../identicon/tests/identicon.component.test.js | 51 +++++++++ ui/app/components/index.scss | 2 + ui/app/components/jazzicon/index.js | 1 + ui/app/components/jazzicon/jazzicon.component.js | 69 ++++++++++++ .../components/modals/account-modal-container.js | 2 +- .../modals/hide-token-confirmation-modal.js | 2 +- ui/app/components/signature-request.js | 2 +- ui/app/components/token-cell.js | 2 +- .../components/transaction-view-balance/index.scss | 4 +- ui/app/components/wallet-view.js | 2 +- 21 files changed, 257 insertions(+), 138 deletions(-) delete mode 100644 ui/app/components/identicon.js create mode 100644 ui/app/components/identicon/identicon.component.js create mode 100644 ui/app/components/identicon/identicon.container.js create mode 100644 ui/app/components/identicon/index.js create mode 100644 ui/app/components/identicon/index.scss create mode 100644 ui/app/components/identicon/tests/identicon.component.test.js create mode 100644 ui/app/components/jazzicon/index.js create mode 100644 ui/app/components/jazzicon/jazzicon.component.js (limited to 'ui') diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 043008a36..06376e48b 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -6,10 +6,11 @@ const genAccountLink = require('etherscan-link').createAccountLink const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('./identicon') const copyToClipboard = require('copy-to-clipboard') const { checksumAddress } = require('../util') +import Identicon from './identicon' + class AccountDropdowns extends Component { constructor (props) { super(props) diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index c9c5b60e1..94eae8d07 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -7,10 +7,10 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') -const Identicon = require('../identicon') const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') const { getEnvironmentType } = require('../../../../app/scripts/lib/util') const Tooltip = require('../tooltip') +import Identicon from '../identicon' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import { PRIMARY } from '../../constants/common' diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js index abaaf8163..a379ed3ac 100644 --- a/ui/app/components/account-panel.js +++ b/ui/app/components/account-panel.js @@ -1,7 +1,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') -const Identicon = require('./identicon') +import Identicon from './identicon' const formatBalance = require('../util').formatBalance const addressSummary = require('../util').addressSummary diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index b8b002dcc..c82dc1de9 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -2,13 +2,13 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import { matchPath } from 'react-router-dom' +import Identicon from '../identicon' const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_POPUP, } = require('../../../../app/scripts/lib/enums') const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes') -const Identicon = require('../identicon') const NetworkIndicator = require('../network') export default class AppHeader extends PureComponent { diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index c1b713ccd..799ed20db 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -2,8 +2,8 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits -const TokenBalance = require('./token-balance') -const Identicon = require('./identicon') +import TokenBalance from './token-balance' +import Identicon from './identicon' import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display' import { PRIMARY, SECONDARY } from '../constants/common' const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index b497f5c09..9ffcb12cb 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -6,7 +6,7 @@ const genAccountLink = require('../../../../lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('../../identicon') +import Identicon from '../../identicon' const { checksumAddress } = require('../../../util') const copyToClipboard = require('copy-to-clipboard') const { formatBalance } = require('../../../util') diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js deleted file mode 100644 index 7bd921892..000000000 --- a/ui/app/components/identicon.js +++ /dev/null @@ -1,124 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) -const { toDataUrl } = require('../../lib/blockies') - -module.exports = connect(mapStateToProps)(IdenticonComponent) - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -function mapStateToProps (state) { - return { - useBlockie: state.metamask.useBlockie, - } -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - const { className = '', address, image } = props - var diameter = props.diameter || this.defaultDiameter - const style = { - height: diameter, - width: diameter, - borderRadius: diameter / 2, - } - if (image) { - return h('img', { - className: `${className} identicon`, - src: image, - style: { - ...style, - }, - }) - } else if (address) { - return h('div', { - className: `${className} identicon`, - key: 'identicon-' + address, - style: { - display: 'flex', - flexShrink: 0, - alignItems: 'center', - justifyContent: 'center', - ...style, - overflow: 'hidden', - }, - }) - } else { - return h('img.balance-icon', { - className, - src: './images/eth_logo.svg', - style: { - ...style, - }, - }) - } -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address, useBlockie } = props - - if (!address) return - - if (!isNode) { - // eslint-disable-next-line react/no-find-dom-node - var container = findDOMNode(this) - - const diameter = props.diameter || this.defaultDiameter - - if (useBlockie) { - _generateBlockie(container, address, diameter) - } else { - _generateJazzicon(container, address, diameter) - } - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address, useBlockie } = props - - if (!address) return - - if (!isNode) { - // eslint-disable-next-line react/no-find-dom-node - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - const diameter = props.diameter || this.defaultDiameter - - if (useBlockie) { - _generateBlockie(container, address, diameter) - } else { - _generateJazzicon(container, address, diameter) - } - } -} - -function _generateBlockie (container, address, diameter) { - const img = new Image() - img.src = toDataUrl(address) - img.height = diameter - img.width = diameter - container.appendChild(img) -} - -function _generateJazzicon (container, address, diameter) { - const img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) -} diff --git a/ui/app/components/identicon/identicon.component.js b/ui/app/components/identicon/identicon.component.js new file mode 100644 index 000000000..b892e5ae5 --- /dev/null +++ b/ui/app/components/identicon/identicon.component.js @@ -0,0 +1,99 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { toDataUrl } from '../../../lib/blockies' +import contractMap from 'eth-contract-metadata' +import { checksumAddress } from '../../../app/util' +import Jazzicon from '../jazzicon' + +const getStyles = diameter => ( + { + height: diameter, + width: diameter, + borderRadius: diameter / 2, + } +) + +export default class Identicon extends PureComponent { + static propTypes = { + address: PropTypes.string, + className: PropTypes.string, + diameter: PropTypes.number, + image: PropTypes.string, + useBlockie: PropTypes.bool, + } + + static defaultProps = { + diameter: 46, + } + + renderImage () { + const { className, diameter, image } = this.props + + return ( + + ) + } + + renderJazzicon () { + const { address, className, diameter } = this.props + + return ( + + ) + } + + renderBlockie () { + const { address, className, diameter } = this.props + + return ( +
+ +
+ ) + } + + render () { + const { className, address, image, diameter, useBlockie } = this.props + + if (image) { + return this.renderImage() + } + + if (address) { + const checksummedAddress = checksumAddress(address) + + if (contractMap[checksummedAddress] && contractMap[checksummedAddress].logo) { + return this.renderJazzicon() + } + + return useBlockie + ? this.renderBlockie() + : this.renderJazzicon() + } + + return ( + + ) + } +} diff --git a/ui/app/components/identicon/identicon.container.js b/ui/app/components/identicon/identicon.container.js new file mode 100644 index 000000000..bc49bc18e --- /dev/null +++ b/ui/app/components/identicon/identicon.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Identicon from './identicon.component' + +const mapStateToProps = state => { + const { metamask: { useBlockie } } = state + + return { + useBlockie, + } +} + +export default connect(mapStateToProps)(Identicon) diff --git a/ui/app/components/identicon/index.js b/ui/app/components/identicon/index.js new file mode 100644 index 000000000..799c886f2 --- /dev/null +++ b/ui/app/components/identicon/index.js @@ -0,0 +1 @@ +export { default } from './identicon.container' diff --git a/ui/app/components/identicon/index.scss b/ui/app/components/identicon/index.scss new file mode 100644 index 000000000..657afc48f --- /dev/null +++ b/ui/app/components/identicon/index.scss @@ -0,0 +1,7 @@ +.identicon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + overflow: hidden; +} diff --git a/ui/app/components/identicon/tests/identicon.component.test.js b/ui/app/components/identicon/tests/identicon.component.test.js new file mode 100644 index 000000000..2944818f5 --- /dev/null +++ b/ui/app/components/identicon/tests/identicon.component.test.js @@ -0,0 +1,51 @@ +import React from 'react' +import assert from 'assert' +import thunk from 'redux-thunk' +import configureMockStore from 'redux-mock-store' +import { mount } from 'enzyme' +import Identicon from '../identicon.component' + +describe('Identicon', () => { + const state = { + metamask: { + useBlockie: false, + }, + } + + const middlewares = [thunk] + const mockStore = configureMockStore(middlewares) + const store = mockStore(state) + + it('renders default eth_logo identicon with no props', () => { + const wrapper = mount( + + ) + + assert.equal(wrapper.find('img.balance-icon').prop('src'), './images/eth_logo.svg') + }) + + it('renders custom image and add className props', () => { + const wrapper = mount( + + ) + + assert.equal(wrapper.find('img.test-image').prop('className'), 'identicon test-image') + assert.equal(wrapper.find('img.test-image').prop('src'), 'test-image') + }) + + it('renders div with address prop', () => { + const wrapper = mount( + + ) + + assert.equal(wrapper.find('div.test-address').prop('className'), 'identicon test-address') + }) +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index beffdb221..72de6cb93 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -16,6 +16,8 @@ @import './export-text-container/index'; +@import './identicon/index'; + @import './info-box/index'; @import './menu-bar/index'; diff --git a/ui/app/components/jazzicon/index.js b/ui/app/components/jazzicon/index.js new file mode 100644 index 000000000..bea900ab9 --- /dev/null +++ b/ui/app/components/jazzicon/index.js @@ -0,0 +1 @@ +export { default } from './jazzicon.component' diff --git a/ui/app/components/jazzicon/jazzicon.component.js b/ui/app/components/jazzicon/jazzicon.component.js new file mode 100644 index 000000000..fcb1c59b1 --- /dev/null +++ b/ui/app/components/jazzicon/jazzicon.component.js @@ -0,0 +1,69 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import isNode from 'detect-node' +import { findDOMNode } from 'react-dom' +import jazzicon from 'jazzicon' +import iconFactoryGenerator from '../../../lib/icon-factory' +const iconFactory = iconFactoryGenerator(jazzicon) + +/** + * Wrapper around the jazzicon library to return a React component, as the library returns an + * HTMLDivElement which needs to be appended. + */ +export default class Jazzicon extends PureComponent { + static propTypes = { + address: PropTypes.string.isRequired, + className: PropTypes.string, + diameter: PropTypes.number, + style: PropTypes.object, + } + + static defaultProps = { + diameter: 46, + } + + componentDidMount () { + if (!isNode) { + this.appendJazzicon() + } + } + + componentDidUpdate (prevProps) { + const { address: prevAddress } = prevProps + const { address } = this.props + + if (!isNode && address !== prevAddress) { + this.removeExistingChildren() + this.appendJazzicon() + } + } + + removeExistingChildren () { + // eslint-disable-next-line react/no-find-dom-node + const container = findDOMNode(this) + const { children } = container + + for (let i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + } + + appendJazzicon () { + // eslint-disable-next-line react/no-find-dom-node + const container = findDOMNode(this) + const { address, diameter } = this.props + const image = iconFactory.iconForAddress(address, diameter) + container.appendChild(image) + } + + render () { + const { className, style } = this.props + + return ( +
+ ) + } +} diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js index aa0593df8..2a6c655e1 100644 --- a/ui/app/components/modals/account-modal-container.js +++ b/ui/app/components/modals/account-modal-container.js @@ -5,7 +5,7 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') const { getSelectedIdentity } = require('../../selectors') -const Identicon = require('../identicon') +import Identicon from '../identicon' function mapStateToProps (state, ownProps) { return { diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js index fb38516d3..43f3009a5 100644 --- a/ui/app/components/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/modals/hide-token-confirmation-modal.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') -const Identicon = require('../identicon') +import Identicon from '../identicon' function mapStateToProps (state) { return { diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js index d76eb5ef8..85af3b00b 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/signature-request.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits -const Identicon = require('./identicon') +import Identicon from './identicon' const connect = require('react-redux').connect const ethUtil = require('ethereumjs-util') const classnames = require('classnames') diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 477d97597..75ba347fa 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -2,7 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const Identicon = require('./identicon') +import Identicon from './identicon' const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') const selectors = require('../selectors') const actions = require('../actions') diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss index 190072be0..659f896ff 100644 --- a/ui/app/components/transaction-view-balance/index.scss +++ b/ui/app/components/transaction-view-balance/index.scss @@ -23,7 +23,8 @@ font-size: 1.5rem; @media screen and (max-width: $break-small) { - margin-bottom: 12px; + margin: 12px 0; + margin-left: 0; font-size: 1.75rem; } } @@ -32,7 +33,6 @@ font-size: 1.5rem; @media screen and (max-width: $break-small) { - margin-bottom: 12px; font-size: 1.75rem; } } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 0a85e41d1..e050e0ee6 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -7,7 +7,7 @@ const { compose } = require('recompose') const inherits = require('util').inherits const classnames = require('classnames') const { checksumAddress } = require('../util') -const Identicon = require('./identicon') +import Identicon from './identicon' // const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns const Tooltip = require('./tooltip-v2.js').default const copyToClipboard = require('copy-to-clipboard') -- cgit v1.2.3 From 55c66b0d988fac6b2ed5948cf4dcd20fd486070e Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Thu, 25 Oct 2018 22:00:14 +0800 Subject: Fix button styling for overflowing text (#5602) --- ui/app/css/itcss/components/buttons.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ui') diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 655188a3e..e4d379a02 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -3,7 +3,7 @@ */ .button { - height: 44px; + min-height: 44px; background: $white; display: flex; justify-content: center; @@ -87,7 +87,7 @@ } .btn--large { - height: 54px; + min-height: 54px; } .btn-green { -- cgit v1.2.3 From eaca9a0e8a6a8e65a4dd099c8f860a9616b7360e Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 26 Oct 2018 16:04:21 +0800 Subject: Fix raised button min-height (#5613) --- ui/app/css/itcss/components/buttons.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'ui') diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index e4d379a02..5826a8b49 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -82,6 +82,7 @@ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); padding: 6px; height: initial; + min-height: initial; width: initial; min-width: initial; } -- cgit v1.2.3 From 54a8ade2669cb5f8f046509873bc2a9c25425847 Mon Sep 17 00:00:00 2001 From: HackyMiner Date: Fri, 26 Oct 2018 17:26:43 +0900 Subject: Add support for RPC endpoints with custom chain IDs (#5134) --- ui/app/actions.js | 12 +- ui/app/app.js | 8 +- ui/app/components/balance-component.js | 7 +- .../currency-display/currency-display.component.js | 4 +- .../currency-input/currency-input.component.js | 5 +- .../currency-input/currency-input.container.js | 7 +- .../tests/currency-input.component.test.js | 12 ++ .../tests/currency-input.container.test.js | 5 + .../dropdowns/components/account-dropdowns.js | 6 +- ui/app/components/dropdowns/network-dropdown.js | 35 +++--- .../dropdowns/tests/network-dropdown.test.js | 4 +- ui/app/components/eth-balance.js | 12 +- .../network-display/network-display.component.js | 4 +- ui/app/components/network.js | 5 +- .../pages/settings/settings-tab/index.scss | 12 +- .../settings-tab/settings-tab.component.js | 122 ++++++++++++++++----- .../settings-tab/settings-tab.container.js | 14 ++- .../account-list-item.container.js | 2 + .../tests/account-list-item-component.test.js | 1 + .../tests/account-list-item-container.test.js | 2 + ui/app/components/send/send.selectors.js | 5 + .../send/tests/send-selectors-test-data.js | 1 + .../components/send/tests/send-selectors.test.js | 10 ++ .../transaction-activity-log.container.test.js | 3 +- .../transaction-activity-log.component.js | 11 +- .../transaction-activity-log.container.js | 3 +- ui/app/components/transaction-breakdown/index.js | 2 +- .../transaction-breakdown.component.js | 7 +- .../transaction-breakdown.container.js | 11 ++ .../transaction-view-balance.container.js | 3 +- ...-preferenced-currency-display.container.test.js | 22 +++- .../user-preferenced-currency-display.component.js | 1 + .../user-preferenced-currency-display.container.js | 18 +-- ...er-preferenced-currency-input.component.test.js | 6 +- ...er-preferenced-currency-input.container.test.js | 4 +- .../user-preferenced-currency-input.component.js | 6 +- .../user-preferenced-currency-input.container.js | 4 +- .../user-preferenced-token-input.component.test.js | 6 +- .../user-preferenced-token-input.container.test.js | 4 +- .../user-preferenced-token-input.component.js | 6 +- .../user-preferenced-token-input.container.js | 4 +- ui/app/ducks/confirm-transaction.duck.js | 10 +- ui/app/helpers/confirm-transaction/util.js | 8 +- ui/app/helpers/conversions.util.js | 10 +- ui/app/reducers/metamask.js | 2 +- ui/app/selectors.js | 5 + ui/app/selectors/confirm-transaction.js | 1 + ui/app/util.js | 8 +- 48 files changed, 317 insertions(+), 143 deletions(-) create mode 100644 ui/app/components/transaction-breakdown/transaction-breakdown.container.js (limited to 'ui') diff --git a/ui/app/actions.js b/ui/app/actions.js index f8a375e2f..a8b9189e9 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -309,7 +309,7 @@ var actions = { setPreference, updatePreferences, UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', - setUseETHAsPrimaryCurrencyPreference, + setUseNativeCurrencyAsPrimaryCurrencyPreference, setMouseUserState, SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', @@ -1874,10 +1874,10 @@ function updateProviderType (type) { } } -function setRpcTarget (newRpc) { +function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname = '') { return (dispatch) => { - log.debug(`background.setRpcTarget: ${newRpc}`) - background.setCustomRpc(newRpc, (err, result) => { + log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`) + background.setCustomRpc(newRpc, chainId, ticker, nickname, (err, result) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -2330,8 +2330,8 @@ function updatePreferences (value) { } } -function setUseETHAsPrimaryCurrencyPreference (value) { - return setPreference('useETHAsPrimaryCurrency', value) +function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) { + return setPreference('useNativeCurrencyAsPrimaryCurrency', value) } function setNetworkNonce (networkNonce) { diff --git a/ui/app/app.js b/ui/app/app.js index aeb3d05ee..b3aff1f39 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -101,7 +101,7 @@ class App extends Component { network, isMouseUser, provider, - frequentRpcList, + frequentRpcListDetail, currentView, setMouseUserState, sidebar, @@ -147,7 +147,7 @@ class App extends Component { // network dropdown h(NetworkDropdown, { provider, - frequentRpcList, + frequentRpcListDetail, }, []), h(AccountMenu), @@ -230,7 +230,7 @@ App.propTypes = { alertMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, - frequentRpcList: PropTypes.array, + frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebar: PropTypes.object, alertOpen: PropTypes.bool, @@ -322,7 +322,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice, lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, betaUI: state.metamask.featureFlags.betaUI, diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index 799ed20db..0f8514c81 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -6,7 +6,7 @@ import TokenBalance from './token-balance' import Identicon from './identicon' import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display' import { PRIMARY, SECONDARY } from '../constants/common' -const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') +const { getNativeCurrency, getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') const { formatBalance } = require('../util') @@ -21,6 +21,7 @@ function mapStateToProps (state) { return { account, network, + nativeCurrency: getNativeCurrency(state), conversionRate: conversionRateSelector(state), currentCurrency: getCurrentCurrency(state), assetImages: getAssetImages(state), @@ -66,10 +67,10 @@ BalanceComponent.prototype.renderTokenBalance = function () { BalanceComponent.prototype.renderBalance = function () { const props = this.props - const { account } = props + const { account, nativeCurrency } = props const balanceValue = account && account.balance const needsParse = 'needsParse' in props ? props.needsParse : true - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse, nativeCurrency) : '...' const showFiat = 'showFiat' in props ? props.showFiat : true if (formattedBalance === 'None' || formattedBalance === '...') { diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js index f39e60269..2d7413b57 100644 --- a/ui/app/components/currency-display/currency-display.component.js +++ b/ui/app/components/currency-display/currency-display.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { ETH, GWEI } from '../../constants/common' +import { GWEI } from '../../constants/common' export default class CurrencyDisplay extends PureComponent { static propTypes = { @@ -12,7 +12,7 @@ export default class CurrencyDisplay extends PureComponent { style: PropTypes.object, suffix: PropTypes.string, // Used in container - currency: PropTypes.oneOf([ETH]), + currency: PropTypes.string, denomination: PropTypes.oneOf([GWEI]), value: PropTypes.string, numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/currency-input/currency-input.component.js index 54cd0e1ac..0761a75c5 100644 --- a/ui/app/components/currency-input/currency-input.component.js +++ b/ui/app/components/currency-input/currency-input.component.js @@ -14,6 +14,7 @@ export default class CurrencyInput extends PureComponent { static propTypes = { conversionRate: PropTypes.number, currentCurrency: PropTypes.string, + nativeCurrency: PropTypes.string, onChange: PropTypes.func, onBlur: PropTypes.func, suffix: PropTypes.string, @@ -77,13 +78,13 @@ export default class CurrencyInput extends PureComponent { } renderConversionComponent () { - const { useFiat, currentCurrency } = this.props + const { useFiat, currentCurrency, nativeCurrency } = this.props const { hexValue } = this.state let currency, numberOfDecimals if (useFiat) { // Display ETH - currency = ETH + currency = nativeCurrency || ETH numberOfDecimals = 6 } else { // Display Fiat diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js index 18e5533de..1d1ed7b41 100644 --- a/ui/app/components/currency-input/currency-input.container.js +++ b/ui/app/components/currency-input/currency-input.container.js @@ -3,18 +3,19 @@ import CurrencyInput from './currency-input.component' import { ETH } from '../../constants/common' const mapStateToProps = state => { - const { metamask: { currentCurrency, conversionRate } } = state + const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state return { + nativeCurrency, currentCurrency, conversionRate, } } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { currentCurrency } = stateProps + const { nativeCurrency, currentCurrency } = stateProps const { useFiat } = ownProps - const suffix = useFiat ? currentCurrency.toUpperCase() : ETH + const suffix = useFiat ? currentCurrency.toUpperCase() : nativeCurrency || ETH return { ...stateProps, 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 index 5db53066f..a33889f94 100644 --- a/ui/app/components/currency-input/tests/currency-input.component.test.js +++ b/ui/app/components/currency-input/tests/currency-input.component.test.js @@ -22,6 +22,7 @@ describe('CurrencyInput Component', () => { it('should render properly with a suffix', () => { const mockStore = { metamask: { + nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 231.06, }, @@ -32,6 +33,7 @@ describe('CurrencyInput Component', () => { ) @@ -45,6 +47,7 @@ describe('CurrencyInput Component', () => { it('should render properly with an ETH value', () => { const mockStore = { metamask: { + nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 231.06, }, @@ -56,6 +59,7 @@ describe('CurrencyInput Component', () => { @@ -75,6 +79,7 @@ describe('CurrencyInput Component', () => { it('should render properly with a fiat value', () => { const mockStore = { metamask: { + nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 231.06, }, @@ -87,6 +92,7 @@ describe('CurrencyInput Component', () => { value="f602f2234d0ea" suffix="USD" useFiat + nativeCurrency="ETH" currentCurrency="usd" conversionRate={231.06} /> @@ -116,6 +122,7 @@ describe('CurrencyInput Component', () => { it('should call onChange and onBlur on input changes with the hex value for ETH', () => { const mockStore = { metamask: { + nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 231.06, }, @@ -127,6 +134,7 @@ describe('CurrencyInput Component', () => { onChange={handleChangeSpy} onBlur={handleBlurSpy} suffix="ETH" + nativeCurrency="ETH" currentCurrency="usd" conversionRate={231.06} /> @@ -160,6 +168,7 @@ describe('CurrencyInput Component', () => { it('should call onChange and onBlur on input changes with the hex value for fiat', () => { const mockStore = { metamask: { + nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 231.06, }, @@ -171,6 +180,7 @@ describe('CurrencyInput Component', () => { onChange={handleChangeSpy} onBlur={handleBlurSpy} suffix="USD" + nativeCurrency="ETH" currentCurrency="usd" conversionRate={231.06} useFiat @@ -205,6 +215,7 @@ describe('CurrencyInput Component', () => { it('should change the state and pass in a new decimalValue when props.value changes', () => { const mockStore = { metamask: { + nativeCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 231.06, }, @@ -216,6 +227,7 @@ describe('CurrencyInput Component', () => { onChange={handleChangeSpy} onBlur={handleBlurSpy} suffix="USD" + nativeCurrency="ETH" currentCurrency="usd" conversionRate={231.06} useFiat 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 index e77945e4d..5d72958e6 100644 --- a/ui/app/components/currency-input/tests/currency-input.container.test.js +++ b/ui/app/components/currency-input/tests/currency-input.container.test.js @@ -20,12 +20,14 @@ describe('CurrencyInput container', () => { metamask: { conversionRate: 280.45, currentCurrency: 'usd', + nativeCurrency: 'ETH', }, } assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45, currentCurrency: 'usd', + nativeCurrency: 'ETH', }) }) }) @@ -35,12 +37,14 @@ describe('CurrencyInput container', () => { const mockStateProps = { conversionRate: 280.45, currentCurrency: 'usd', + nativeCurrency: 'ETH', } const mockDispatchProps = {} assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), { conversionRate: 280.45, currentCurrency: 'usd', + nativeCurrency: 'ETH', useFiat: true, suffix: 'USD', }) @@ -48,6 +52,7 @@ describe('CurrencyInput container', () => { assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), { conversionRate: 280.45, currentCurrency: 'usd', + nativeCurrency: 'ETH', suffix: 'ETH', }) }) diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index 9ffcb12cb..e6b3e0c0c 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -26,14 +26,14 @@ class AccountDropdowns extends Component { } renderAccounts () { - const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props + const { identities, accounts, selected, menuItemStyles, actions, keyrings, ticker } = this.props return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected const balanceValue = accounts[key].balance - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, true, ticker) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() const keyring = keyrings.find((kr) => { @@ -421,6 +421,7 @@ AccountDropdowns.propTypes = { network: PropTypes.number, // actions.showExportPrivateKeyModal: , style: PropTypes.object, + ticker: PropTypes.string, enableAccountsSelector: PropTypes.bool, enableAccountOption: PropTypes.bool, enableAccountOptions: PropTypes.bool, @@ -458,6 +459,7 @@ const mapDispatchToProps = (dispatch) => { function mapStateToProps (state) { return { + ticker: state.metamask.ticker, keyrings: state.metamask.keyrings, sidebarOpen: state.appState.sidebar.isOpen, } diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index b252b25d9..d4cc695a6 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -24,8 +24,9 @@ const notToggleElementClassnames = [ function mapStateToProps (state) { return { provider: state.metamask.provider, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], networkDropdownOpen: state.appState.networkDropdownOpen, + network: state.metamask.network, } } @@ -40,8 +41,8 @@ function mapDispatchToProps (dispatch) { setDefaultRpcTarget: type => { dispatch(actions.setDefaultRpcTarget(type)) }, - setRpcTarget: (target) => { - dispatch(actions.setRpcTarget(target)) + setRpcTarget: (target, network, ticker, nickname) => { + dispatch(actions.setRpcTarget(target, network, ticker, nickname)) }, delRpcTarget: (target) => { dispatch(actions.delRpcTarget(target)) @@ -71,7 +72,7 @@ module.exports = compose( NetworkDropdown.prototype.render = function () { const props = this.props const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList + const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { fontSize: '16px', @@ -225,7 +226,7 @@ NetworkDropdown.prototype.render = function () { ), this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), + this.renderCommonRpc(rpcListDetail, props.provider), h( DropdownMenuItem, @@ -267,28 +268,33 @@ NetworkDropdown.prototype.getNetworkName = function () { } else if (providerName === 'rinkeby') { name = this.context.t('rinkeby') } else { - name = this.context.t('unknownNetwork') + name = provider.nickname || this.context.t('unknownNetwork') } return name } -NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { +NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { const props = this.props - const reversedRpcList = rpcList.slice().reverse() + const reversedRpcListDetail = rpcListDetail.slice().reverse() + const network = props.network - return reversedRpcList.map((rpc) => { + return reversedRpcListDetail.map((entry) => { + const rpc = entry.rpcUrl + const ticker = entry.ticker || 'ETH' + const nickname = entry.nickname || '' const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget if ((rpc === 'http://localhost:8545') || currentRpcTarget) { return null } else { + const chainId = entry.chainId || network return h( DropdownMenuItem, { key: `common${rpc}`, closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setRpcTarget(rpc), + onClick: () => props.setRpcTarget(rpc, chainId, ticker, nickname), style: { fontSize: '16px', lineHeight: '20px', @@ -302,7 +308,7 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { style: { color: currentRpcTarget ? '#ffffff' : '#9b9b9b', }, - }, rpc), + }, nickname || rpc), h('i.fa.fa-times.delete', { onClick: (e) => { @@ -317,8 +323,9 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { } NetworkDropdown.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider + const { rpcTarget, type, ticker, nickname } = provider const props = this.props + const network = props.network if (type !== 'rpc') return null @@ -332,7 +339,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, - onClick: () => props.setRpcTarget(rpcTarget), + onClick: () => props.setRpcTarget(rpcTarget, network, ticker, nickname), closeMenu: () => this.props.hideNetworkDropdown(), style: { fontSize: '16px', @@ -347,7 +354,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { style: { color: '#ffffff', }, - }, rpcTarget), + }, nickname || rpcTarget), ] ) } diff --git a/ui/app/components/dropdowns/tests/network-dropdown.test.js b/ui/app/components/dropdowns/tests/network-dropdown.test.js index 699b54605..88ad56851 100644 --- a/ui/app/components/dropdowns/tests/network-dropdown.test.js +++ b/ui/app/components/dropdowns/tests/network-dropdown.test.js @@ -45,8 +45,8 @@ describe('Network Dropdown', () => { provider: { 'type': 'test', }, - frequentRpcList: [ - 'http://localhost:7545', + frequentRpcListDetail: [ + { rpcUrl: 'http://localhost:7545' }, ], }, appState: { diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index c3d084bdc..2f6395a2d 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -1,5 +1,6 @@ const { Component } = require('react') const h = require('react-hyperscript') +const connect = require('react-redux').connect const { inherits } = require('util') const { formatBalance, @@ -8,7 +9,12 @@ const { const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -17,9 +23,9 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { const props = this.props - const { value, style, width, needsParse = true } = props + const { ticker, value, style, width, needsParse = true } = props - const formattedValue = value ? formatBalance(value, 6, needsParse) : '...' + const formattedValue = value ? formatBalance(value, 6, needsParse, ticker) : '...' return ( diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js index 38626af20..82f9ff9c3 100644 --- a/ui/app/components/network-display/network-display.component.js +++ b/ui/app/components/network-display/network-display.component.js @@ -41,7 +41,7 @@ export default class NetworkDisplay extends Component { } render () { - const { network, provider: { type } } = this.props + const { network, provider: { type, nickname } } = this.props const networkClass = networkToClassHash[network] return ( @@ -61,7 +61,7 @@ export default class NetworkDisplay extends Component { /> }
- { this.context.t(type) } + { type === 'rpc' && nickname ? nickname : this.context.t(type) }
) diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 83297c4f2..611aadb7b 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -23,9 +23,10 @@ Network.prototype.render = function () { const props = this.props const context = this.context const networkNumber = props.network - let providerName + let providerName, providerNick try { providerName = props.provider.type + providerNick = props.provider.nickname || '' } catch (e) { providerName = null } @@ -131,7 +132,7 @@ Network.prototype.render = function () { }, }), - h('.network-name', context.t('privateNetwork')), + h('.network-name', providerNick || context.t('privateNetwork')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), ]) } diff --git a/ui/app/components/pages/settings/settings-tab/index.scss b/ui/app/components/pages/settings/settings-tab/index.scss index 3bf840c86..ef32b0e4c 100644 --- a/ui/app/components/pages/settings/settings-tab/index.scss +++ b/ui/app/components/pages/settings/settings-tab/index.scss @@ -5,12 +5,9 @@ color: $crimson; } - &__rpc-save-button { - align-self: flex-end; - padding: 5px; - text-transform: uppercase; - color: $dusty-gray; - cursor: pointer; + &__advanced-link { + color: $curious-blue; + padding-left: 5px; } &__rpc-save-button { @@ -19,6 +16,9 @@ text-transform: uppercase; color: $dusty-gray; cursor: pointer; + width: 25%; + min-width: 80px; + height: 33px; } &__button--red { 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 a9e2a723e..cd81491a8 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,12 +55,17 @@ export default class SettingsTab extends PureComponent { sendHexData: PropTypes.bool, currentCurrency: PropTypes.string, conversionDate: PropTypes.number, - useETHAsPrimaryCurrency: PropTypes.bool, - setUseETHAsPrimaryCurrencyPreference: PropTypes.func, + nativeCurrency: PropTypes.string, + useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, + setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, } state = { newRpc: '', + chainId: '', + showOptions: false, + ticker: '', + nickname: '', } renderCurrentConversion () { @@ -121,37 +126,98 @@ export default class SettingsTab extends PureComponent { renderNewRpcUrl () { const { t } = this.context - const { newRpc } = this.state + const { newRpc, chainId, ticker, nickname } = this.state return (
- { t('newRPC') } + { t('newNetwork') }
this.setState({ newRpc: e.target.value })} onKeyPress={e => { if (e.key === 'Enter') { - this.validateRpc(newRpc) + this.validateRpc(newRpc, chainId, ticker, nickname) } }} fullWidth - margin="none" + margin="dense" /> -
{ - e.preventDefault() - this.validateRpc(newRpc) + this.setState({ chainId: e.target.value })} + onKeyPress={e => { + if (e.key === 'Enter') { + this.validateRpc(newRpc, chainId, ticker, nickname) + } }} - > - { t('save') } + style={{ + display: this.state.showOptions ? null : 'none', + }} + fullWidth + margin="dense" + /> + this.setState({ ticker: e.target.value })} + onKeyPress={e => { + if (e.key === 'Enter') { + this.validateRpc(newRpc, chainId, ticker, nickname) + } + }} + style={{ + display: this.state.showOptions ? null : 'none', + }} + fullWidth + margin="dense" + /> + this.setState({ nickname: e.target.value })} + onKeyPress={e => { + if (e.key === 'Enter') { + this.validateRpc(newRpc, chainId, ticker, nickname) + } + }} + style={{ + display: this.state.showOptions ? null : 'none', + }} + fullWidth + margin="dense" + /> +
+ { + e.preventDefault() + this.setState({ showOptions: !this.state.showOptions }) + }} + > + { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') } + +
@@ -159,11 +225,11 @@ export default class SettingsTab extends PureComponent { ) } - validateRpc (newRpc) { + validateRpc (newRpc, chainId, ticker = 'ETH', nickname) { const { setRpcTarget, displayWarning } = this.props if (validUrl.isWebUri(newRpc)) { - setRpcTarget(newRpc) + setRpcTarget(newRpc, chainId, ticker, nickname) } else { const appendedRpc = `http://${newRpc}` @@ -341,9 +407,13 @@ export default class SettingsTab extends PureComponent { ) } - renderUseEthAsPrimaryCurrency () { + renderUsePrimaryCurrencyOptions () { const { t } = this.context - const { useETHAsPrimaryCurrency, setUseETHAsPrimaryCurrencyPreference } = this.props + const { + nativeCurrency, + setUseNativeCurrencyAsPrimaryCurrencyPreference, + useNativeCurrencyAsPrimaryCurrency, + } = this.props return (
@@ -359,23 +429,23 @@ export default class SettingsTab extends PureComponent {
setUseETHAsPrimaryCurrencyPreference(true)} - checked={Boolean(useETHAsPrimaryCurrency)} + id="native-primary-currency" + onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(true)} + checked={Boolean(useNativeCurrencyAsPrimaryCurrency)} />
setUseETHAsPrimaryCurrencyPreference(false)} - checked={!useETHAsPrimaryCurrency} + onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(false)} + checked={!useNativeCurrencyAsPrimaryCurrency} />