diff --git a/src/actions/LocalStorageActions.js b/src/actions/LocalStorageActions.js index 6355c962..ad959790 100644 --- a/src/actions/LocalStorageActions.js +++ b/src/actions/LocalStorageActions.js @@ -57,6 +57,7 @@ const KEY_TOKENS: string = `${STORAGE_PATH}tokens`; const KEY_PENDING: string = `${STORAGE_PATH}pending`; const KEY_BETA_MODAL: string = '/betaModalPrivacy'; // this key needs to be compatible with "parent" (old) wallet const KEY_LANGUAGE: string = `${STORAGE_PATH}language`; +const KEY_LOCAL_CURRENCY: string = `${STORAGE_PATH}localCurrency`; // https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js // or @@ -276,6 +277,11 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { } else { dispatch(WalletActions.fetchLocale(l10nUtils.getInitialLocale())); } + + const localCurrency: ?string = storageUtils.get(TYPE, KEY_LOCAL_CURRENCY); + if (localCurrency) { + dispatch(WalletActions.setLocalCurrency(JSON.parse(localCurrency))); + } }; export const loadData = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { @@ -296,3 +302,11 @@ export const setLanguage = (): ThunkAction => (dispatch: Dispatch, getState: Get const { language } = getState().wallet; storageUtils.set(TYPE, KEY_LANGUAGE, JSON.stringify(language)); }; + +export const setLocalCurrency = (): ThunkAction => ( + dispatch: Dispatch, + getState: GetState +): void => { + const { localCurrency } = getState().wallet; + storageUtils.set(TYPE, KEY_LOCAL_CURRENCY, JSON.stringify(localCurrency)); +}; diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index d44914a2..c43b4cf2 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -58,6 +58,10 @@ export type WalletAction = type: typeof WALLET.SET_LANGUAGE, locale: string, messages: { [string]: string }, + } + | { + type: typeof WALLET.SET_LOCAL_CURRENCY, + localCurrency: string, }; export const init = (): ThunkAction => (dispatch: Dispatch): void => { @@ -104,6 +108,11 @@ export const fetchLocale = (locale: string): ThunkAction => (dispatch: Dispatch) }); }; +export const setLocalCurrency = (localCurrency: string): WalletAction => ({ + type: WALLET.SET_LOCAL_CURRENCY, + localCurrency: localCurrency.toLowerCase(), +}); + // This method will be called after each DEVICE.CONNECT action // if connected device has different "passphrase_protection" settings than saved instances // all saved instances will be removed immediately inside DevicesReducer diff --git a/src/actions/constants/wallet.js b/src/actions/constants/wallet.js index d1d8bb0a..0ccff06d 100644 --- a/src/actions/constants/wallet.js +++ b/src/actions/constants/wallet.js @@ -17,3 +17,4 @@ export const CLEAR_UNAVAILABLE_DEVICE_DATA: 'wallet__clear_unavailable_device_da export const TOGGLE_SIDEBAR: 'wallet__toggle_sidebar' = 'wallet__toggle_sidebar'; export const SET_LANGUAGE: 'wallet__set_language' = 'wallet__set_language'; +export const SET_LOCAL_CURRENCY: 'wallet__set_local_currency' = 'wallet__set_local_currency'; diff --git a/src/actions/ethereum/SendFormActions.js b/src/actions/ethereum/SendFormActions.js index e48a13d0..ad1c3f0e 100644 --- a/src/actions/ethereum/SendFormActions.js +++ b/src/actions/ethereum/SendFormActions.js @@ -10,6 +10,7 @@ import * as WEB3 from 'actions/constants/web3'; import { initialState } from 'reducers/SendFormEthereumReducer'; import * as reducerUtils from 'reducers/utils'; import * as ethUtils from 'utils/ethUtils'; +import { toFiatCurrency, fromFiatCurrency } from 'utils/fiatConverter'; import type { Dispatch, @@ -150,6 +151,9 @@ export const init = (): AsyncAction => async ( initialState.selectedFeeLevel ); + // initial local currency is set according to wallet settings + const { localCurrency } = getState().wallet; + dispatch({ type: SEND.INIT, networkType: 'ethereum', @@ -158,6 +162,7 @@ export const init = (): AsyncAction => async ( networkName: network.shortcut, networkSymbol: network.symbol, currency: network.symbol, + localCurrency, feeLevels, selectedFeeLevel, recommendedGasPrice: gasPrice.toString(), @@ -200,6 +205,9 @@ export const onClear = (): AsyncAction => async ( initialState.selectedFeeLevel ); + // initial local currency is set according to wallet settings + const { localCurrency } = getState().wallet; + dispatch({ type: SEND.CLEAR, networkType: 'ethereum', @@ -208,6 +216,7 @@ export const onClear = (): AsyncAction => async ( networkName: network.shortcut, networkSymbol: network.symbol, currency: network.symbol, + localCurrency, feeLevels, selectedFeeLevel, recommendedGasPrice: gasPrice.toString(), @@ -241,11 +250,12 @@ export const onAddressChange = (address: string): ThunkAction => ( /* * Called from UI on "amount" field change */ -export const onAmountChange = (amount: string): ThunkAction => ( - dispatch: Dispatch, - getState: GetState -): void => { +export const onAmountChange = ( + amount: string, + shouldUpdateLocalAmount: boolean = true +): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const state = getState().sendFormEthereum; + dispatch({ type: SEND.CHANGE, networkType: 'ethereum', @@ -257,6 +267,67 @@ export const onAmountChange = (amount: string): ThunkAction => ( amount, }, }); + + if (shouldUpdateLocalAmount) { + const { localCurrency } = getState().sendFormEthereum; + const fiatRates = getState().fiat.find(f => f.network === state.currency.toLowerCase()); + const localAmount = toFiatCurrency(amount, localCurrency, fiatRates); + dispatch(onLocalAmountChange(localAmount, false)); + } +}; + +/* + * Called from UI on "localAmount" field change + */ +export const onLocalAmountChange = ( + localAmount: string, + shouldUpdateAmount: boolean = true +): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormEthereum; + const { localCurrency } = getState().sendFormEthereum; + const { network } = getState().selectedAccount; + + // updates localAmount + dispatch({ + type: SEND.CHANGE, + networkType: 'ethereum', + state: { + ...state, + untouched: false, + touched: { ...state.touched, localAmount: true }, + setMax: false, + localAmount, + }, + }); + + // updates amount + if (shouldUpdateAmount) { + if (!network) return; + // converts amount in local currency to crypto currency that will be sent + const fiatRates = getState().fiat.find(f => f.network === state.currency.toLowerCase()); + const amount = fromFiatCurrency(localAmount, localCurrency, fiatRates, network.decimals); + dispatch(onAmountChange(amount, false)); + } +}; + +/* + * Called from UI on "localCurrency" selection change + */ +export const onLocalCurrencyChange = (localCurrency: { + value: string, + label: string, +}): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormEthereum; + dispatch({ + type: SEND.CHANGE, + networkType: 'ethereum', + state: { + ...state, + localCurrency: localCurrency.value, + }, + }); + // Recalculates local amount with new currency rates + dispatch(onAmountChange(state.amount, true)); }; /* @@ -298,6 +369,9 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th gasLimit, }, }); + + // Recalculates local amount with new currency rates + dispatch(onAmountChange(state.amount, true)); }; /* @@ -756,7 +830,9 @@ export default { toggleAdvanced, onAddressChange, onAmountChange, + onLocalAmountChange, onCurrencyChange, + onLocalCurrencyChange, onSetMax, onFeeLevelChange, updateFeeLevels, diff --git a/src/actions/ethereum/SendFormValidationActions.js b/src/actions/ethereum/SendFormValidationActions.js index 4ff4a5f6..c3f9a4d0 100644 --- a/src/actions/ethereum/SendFormValidationActions.js +++ b/src/actions/ethereum/SendFormValidationActions.js @@ -4,6 +4,7 @@ import BigNumber from 'bignumber.js'; import EthereumjsUtil from 'ethereumjs-util'; import EthereumjsUnits from 'ethereumjs-units'; import { findDevice, getPendingAmount, findToken } from 'reducers/utils'; +import { toFiatCurrency } from 'utils/fiatConverter'; import * as SEND from 'actions/constants/send'; import * as ethUtils from 'utils/ethUtils'; @@ -131,6 +132,11 @@ export const recalculateTotalAmount = ($state: State): PayloadAction => ( const b = new BigNumber(account.balance).minus(pendingAmount); state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit); } + // calculate amount in local currency + const { localCurrency } = getState().sendFormEthereum; + const fiatRates = getState().fiat.find(f => f.network === state.currency.toLowerCase()); + const localAmount = toFiatCurrency(state.amount, localCurrency, fiatRates); + state.localAmount = localAmount; } state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); diff --git a/src/actions/ripple/SendFormActions.js b/src/actions/ripple/SendFormActions.js index ffe9d54c..14dd94af 100644 --- a/src/actions/ripple/SendFormActions.js +++ b/src/actions/ripple/SendFormActions.js @@ -6,6 +6,7 @@ import * as BLOCKCHAIN from 'actions/constants/blockchain'; import { initialState } from 'reducers/SendFormRippleReducer'; import * as reducerUtils from 'reducers/utils'; import { fromDecimalAmount } from 'utils/formatUtils'; +import { toFiatCurrency, fromFiatCurrency } from 'utils/fiatConverter'; import type { Dispatch, @@ -109,6 +110,9 @@ export const init = (): AsyncAction => async ( initialState.selectedFeeLevel ); + // initial local currency is set according to wallet settings + const { localCurrency } = getState().wallet; + dispatch({ type: SEND.INIT, networkType: 'ripple', @@ -116,6 +120,7 @@ export const init = (): AsyncAction => async ( ...initialState, networkName: network.shortcut, networkSymbol: network.symbol, + localCurrency, feeLevels, selectedFeeLevel, fee: network.fee.defaultFee, @@ -154,6 +159,9 @@ export const onClear = (): AsyncAction => async ( initialState.selectedFeeLevel ); + // initial local currency is set according to wallet settings + const { localCurrency } = getState().wallet; + dispatch({ type: SEND.CLEAR, networkType: 'ripple', @@ -161,6 +169,7 @@ export const onClear = (): AsyncAction => async ( ...initialState, networkName: network.shortcut, networkSymbol: network.symbol, + localCurrency, feeLevels, selectedFeeLevel, fee: network.fee.defaultFee, @@ -193,10 +202,10 @@ export const onAddressChange = (address: string): ThunkAction => ( /* * Called from UI on "amount" field change */ -export const onAmountChange = (amount: string): ThunkAction => ( - dispatch: Dispatch, - getState: GetState -): void => { +export const onAmountChange = ( + amount: string, + shouldUpdateLocalAmount: boolean = true +): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const state = getState().sendFormRipple; dispatch({ type: SEND.CHANGE, @@ -209,6 +218,67 @@ export const onAmountChange = (amount: string): ThunkAction => ( amount, }, }); + + if (shouldUpdateLocalAmount) { + const { localCurrency } = getState().sendFormRipple; + const fiatRates = getState().fiat.find(f => f.network === state.networkName); + const localAmount = toFiatCurrency(amount, localCurrency, fiatRates); + dispatch(onLocalAmountChange(localAmount, false)); + } +}; + +/* + * Called from UI on "localAmount" field change + */ +export const onLocalAmountChange = ( + localAmount: string, + shouldUpdateAmount: boolean = true +): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormRipple; + const { localCurrency } = getState().sendFormRipple; + const fiatRates = getState().fiat.find(f => f.network === state.networkName); + const { network } = getState().selectedAccount; + + // updates localAmount + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + untouched: false, + touched: { ...state.touched, localAmount: true }, + setMax: false, + localAmount, + }, + }); + + // updates amount + if (shouldUpdateAmount) { + if (!network) return; + // converts amount in local currency to crypto currency that will be sent + const amount = fromFiatCurrency(localAmount, localCurrency, fiatRates, network.decimals); + dispatch(onAmountChange(amount, false)); + } +}; + +/* + * Called from UI on "localCurrency" selection change + */ +export const onLocalCurrencyChange = (localCurrency: { + value: string, + label: string, +}): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state = getState().sendFormRipple; + dispatch({ + type: SEND.CHANGE, + networkType: 'ripple', + state: { + ...state, + localCurrency: localCurrency.value, + }, + }); + // Recalculates local amount with new currency rates + dispatch(onAmountChange(state.amount, true)); }; /* @@ -434,6 +504,8 @@ export default { toggleAdvanced, onAddressChange, onAmountChange, + onLocalAmountChange, + onLocalCurrencyChange, onSetMax, onFeeLevelChange, updateFeeLevels, diff --git a/src/actions/ripple/SendFormValidationActions.js b/src/actions/ripple/SendFormValidationActions.js index 4b2055ff..b8a8911a 100644 --- a/src/actions/ripple/SendFormValidationActions.js +++ b/src/actions/ripple/SendFormValidationActions.js @@ -4,6 +4,7 @@ import BigNumber from 'bignumber.js'; import * as SEND from 'actions/constants/send'; import { findDevice, getPendingAmount } from 'reducers/utils'; import { toDecimalAmount } from 'utils/formatUtils'; +import { toFiatCurrency } from 'utils/fiatConverter'; import type { Dispatch, @@ -103,6 +104,12 @@ const recalculateTotalAmount = ($state: State): PayloadAction => ( .minus(account.reserve) .minus(pendingAmount); state.amount = calculateMaxAmount(availableBalance, fee); + + // calculate amount in local currency + const { localCurrency } = getState().sendFormRipple; + const fiatRates = getState().fiat.find(f => f.network === state.networkName); + const localAmount = toFiatCurrency(state.amount, localCurrency, fiatRates); + state.localAmount = localAmount; } state.total = calculateTotal(state.amount, fee); diff --git a/src/components/Header/components/LanguagePicker/index.js b/src/components/Header/components/LanguagePicker/index.js index adf84626..8947373c 100644 --- a/src/components/Header/components/LanguagePicker/index.js +++ b/src/components/Header/components/LanguagePicker/index.js @@ -3,7 +3,8 @@ import * as React from 'react'; import styled from 'styled-components'; import colors from 'config/colors'; import ReactSelect from 'react-select'; -import { LANGUAGE, SCREEN_SIZE } from 'config/variables'; +import { SCREEN_SIZE } from 'config/variables'; +import { LANGUAGE } from 'config/app'; import type { Props } from './Container'; diff --git a/src/components/Select/index.js b/src/components/Select/index.js index 7aa4ad66..4c493b83 100644 --- a/src/components/Select/index.js +++ b/src/components/Select/index.js @@ -4,12 +4,15 @@ import ReactSelect from 'react-select'; import ReactAsyncSelect from 'react-select/lib/Async'; import colors from 'config/colors'; -const styles = isSearchable => ({ +const styles = (isSearchable, withDropdownIndicator = true) => ({ singleValue: base => ({ ...base, maxWidth: 'calc(100% - 10px)', // 8px padding + 2px maring-left width: '100%', color: colors.TEXT_SECONDARY, + '&:hover': { + cursor: isSearchable ? 'text' : 'pointer', + }, }), control: (base, { isDisabled, isFocused }) => ({ ...base, @@ -20,7 +23,7 @@ const styles = isSearchable => ({ boxShadow: isFocused ? `0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW}` : 'none', background: isDisabled ? colors.LANDING : colors.WHITE, '&:hover': { - cursor: isSearchable ? 'text' : 'pointer', + cursor: 'pointer', }, }), indicatorSeparator: () => ({ @@ -28,7 +31,7 @@ const styles = isSearchable => ({ }), dropdownIndicator: (base, { isDisabled }) => ({ ...base, - display: isSearchable || isDisabled ? 'none' : 'block', + display: !withDropdownIndicator || isDisabled ? 'none' : 'block', color: colors.TEXT_SECONDARY, path: '', '&:hover': { diff --git a/src/config/app.js b/src/config/app.js new file mode 100644 index 00000000..b533b8f4 --- /dev/null +++ b/src/config/app.js @@ -0,0 +1,68 @@ +export const LANGUAGE = [ + { code: 'en', name: 'English', en: 'English' }, + { code: 'bn', name: 'Bengali', en: 'Bengali' }, + { code: 'cs', name: 'Česky', en: 'Czech' }, + { code: 'de', name: 'Deutsch', en: 'German' }, + { code: 'el', name: 'Ελληνικά', en: 'Greek' }, + { code: 'es', name: 'Español', en: 'Spanish' }, + { code: 'fr', name: 'Français', en: 'French' }, + { code: 'id', name: 'Bahasa Indonesia', en: 'Indonesian' }, + { code: 'it', name: 'Italiano', en: 'Italian' }, + { code: 'ja', name: '日本語', en: 'Japanese' }, + { code: 'nl', name: 'Nederlands', en: 'Dutch' }, + { code: 'pl', name: 'Polski', en: 'Polish' }, + { code: 'pt', name: 'Português', en: 'Portuguese' }, + { code: 'ru', name: 'Русский', en: 'Russian' }, + { code: 'uk', name: 'Український', en: 'Ukrainian' }, + { code: 'zh', name: '中文(简体)', en: 'Chinese Simplified' }, + { code: 'zh_TW', name: '中文(台灣)', en: 'Chinese Traditional' }, +]; + +export const FIAT_CURRENCIES = [ + 'usd', + 'aed', + 'ars', + 'aud', + 'bdt', + 'bhd', + 'bmd', + 'brl', + 'cad', + 'chf', + 'clp', + 'cny', + 'czk', + 'dkk', + 'eur', + 'gbp', + 'hkd', + 'huf', + 'idr', + 'ils', + 'inr', + 'jpy', + 'krw', + 'kwd', + 'lkr', + 'mmk', + 'mxn', + 'myr', + 'nok', + 'nzd', + 'php', + 'pkr', + 'pln', + 'rub', + 'sar', + 'sek', + 'sgd', + 'thb', + 'try', + 'twd', + 'vef', + 'vnd', + 'zar', + 'xdr', + 'xag', + 'xau', +]; diff --git a/src/config/variables.js b/src/config/variables.js index b657e62d..3a681bf4 100644 --- a/src/config/variables.js +++ b/src/config/variables.js @@ -81,23 +81,3 @@ export const LINE_HEIGHT = { BASE: '1.8', TREZOR_ACTION: '37px', }; - -export const LANGUAGE = [ - { code: 'en', name: 'English', en: 'English' }, - { code: 'bn', name: 'Bengali', en: 'Bengali' }, - { code: 'cs', name: 'Česky', en: 'Czech' }, - { code: 'de', name: 'Deutsch', en: 'German' }, - { code: 'el', name: 'Ελληνικά', en: 'Greek' }, - { code: 'es', name: 'Español', en: 'Spanish' }, - { code: 'fr', name: 'Français', en: 'French' }, - { code: 'id', name: 'Bahasa Indonesia', en: 'Indonesian' }, - { code: 'it', name: 'Italiano', en: 'Italian' }, - { code: 'ja', name: '日本語', en: 'Japanese' }, - { code: 'nl', name: 'Nederlands', en: 'Dutch' }, - { code: 'pl', name: 'Polski', en: 'Polish' }, - { code: 'pt', name: 'Português', en: 'Portuguese' }, - { code: 'ru', name: 'Русский', en: 'Russian' }, - { code: 'uk', name: 'Український', en: 'Ukrainian' }, - { code: 'zh', name: '中文(简体)', en: 'Chinese Simplified' }, - { code: 'zh_TW', name: '中文(台灣)', en: 'Chinese Traditional' }, -]; diff --git a/src/reducers/FiatRateReducer.js b/src/reducers/FiatRateReducer.js index fbccf087..dfd94760 100644 --- a/src/reducers/FiatRateReducer.js +++ b/src/reducers/FiatRateReducer.js @@ -7,7 +7,7 @@ import type { FiatRateAction } from 'services/CoingeckoService'; export type Fiat = { +network: string, - value: string, + rates: { [string]: number }, }; export const initialState: Array = []; @@ -15,12 +15,14 @@ export const initialState: Array = []; const update = (state: Array, action: FiatRateAction): Array => { const affected = state.find(f => f.network === action.network); const otherRates = state.filter(d => d !== affected); - const { network, rate } = action; + const { network, rates } = action; + + Object.keys(rates).map(k => rates[k].toFixed(2)); return otherRates.concat([ { network, - value: rate.toFixed(2), + rates, }, ]); }; diff --git a/src/reducers/SendFormEthereumReducer.js b/src/reducers/SendFormEthereumReducer.js index d2db7401..23234658 100644 --- a/src/reducers/SendFormEthereumReducer.js +++ b/src/reducers/SendFormEthereumReducer.js @@ -17,11 +17,13 @@ export type State = { currency: string, // form fields + localCurrency: string, advanced: boolean, untouched: boolean, // set to true when user made any changes in form touched: { [k: string]: boolean }, address: string, amount: string, + localAmount: string, setMax: boolean, feeLevels: Array, selectedFeeLevel: FeeLevel, @@ -45,6 +47,7 @@ export const initialState: State = { networkName: '', networkSymbol: '', currency: '', + localCurrency: '', advanced: false, untouched: true, @@ -52,6 +55,7 @@ export const initialState: State = { address: '', //address: '0x574BbB36871bA6b78E27f4B4dCFb76eA0091880B', amount: '', + localAmount: '', setMax: false, feeLevels: [], selectedFeeLevel: { diff --git a/src/reducers/SendFormRippleReducer.js b/src/reducers/SendFormRippleReducer.js index b800218f..9b38445a 100644 --- a/src/reducers/SendFormRippleReducer.js +++ b/src/reducers/SendFormRippleReducer.js @@ -16,11 +16,13 @@ export type State = { +networkSymbol: string, // form fields + localCurrency: string, advanced: boolean, untouched: boolean, // set to true when user made any changes in form touched: { [k: string]: boolean }, address: string, amount: string, + localAmount: string, minAmount: string, setMax: boolean, feeLevels: Array, @@ -41,12 +43,14 @@ export type State = { export const initialState: State = { networkName: '', networkSymbol: '', + localCurrency: '', advanced: false, untouched: true, touched: {}, address: '', amount: '', + localAmount: '', minAmount: '0', setMax: false, feeLevels: [], diff --git a/src/reducers/WalletReducer.js b/src/reducers/WalletReducer.js index 7f2e9a22..aa92c230 100644 --- a/src/reducers/WalletReducer.js +++ b/src/reducers/WalletReducer.js @@ -13,6 +13,7 @@ type State = { ready: boolean, online: boolean, language: string, + localCurrency: string, messages: { [string]: string }, dropdownOpened: boolean, showBetaDisclaimer: boolean, @@ -28,6 +29,7 @@ const initialState: State = { ready: false, online: navigator.onLine, language: 'en', + localCurrency: 'usd', messages: {}, dropdownOpened: false, firstLocationChange: true, @@ -129,6 +131,12 @@ export default function wallet(state: State = initialState, action: Action): Sta messages: action.messages ? action.messages : state.messages, }; + case WALLET.SET_LOCAL_CURRENCY: + return { + ...state, + localCurrency: action.localCurrency, + }; + default: return state; } diff --git a/src/services/CoingeckoService.js b/src/services/CoingeckoService.js index c5e6d216..5bdd1097 100644 --- a/src/services/CoingeckoService.js +++ b/src/services/CoingeckoService.js @@ -3,6 +3,8 @@ import { httpRequest } from 'utils/networkUtils'; import { resolveAfter } from 'utils/promiseUtils'; import { READY } from 'actions/constants/localStorage'; +import * as TOKEN from 'actions/constants/token'; +import type { Token } from 'reducers/TokensReducer'; import type { Middleware, @@ -14,14 +16,27 @@ import type { GetState, } from 'flowtype'; +const BASE_URL = 'https://api.coingecko.com/'; + export const RATE_UPDATE: 'rate__update' = 'rate__update'; +export type NetworkRate = { + network: string, + rates: { [string]: number }, +}; + export type FiatRateAction = { type: typeof RATE_UPDATE, network: string, - rate: number, + rates: { [string]: number }, }; +// const getSupportedCurrencies = async () => { +// const url = 'https://api.coingecko.com/api/v3/simple/supported_vs_currencies'; +// const res = await httpRequest(url, 'json'); +// return res; +// }; + const loadRateAction = (): AsyncAction => async ( dispatch: Dispatch, getState: GetState @@ -41,7 +56,7 @@ const loadRateAction = (): AsyncAction => async ( dispatch({ type: RATE_UPDATE, network: response.symbol, - rate: response.market_data.current_price.usd, + rates: response.market_data.current_price, }); } }); @@ -52,6 +67,49 @@ const loadRateAction = (): AsyncAction => async ( await resolveAfter(50000); }; +const fetchCoinRate = async (id: string): Promise => { + const url = new URL(`/api/v3/coins/${id}`, BASE_URL); + url.searchParams.set('tickers', 'false'); + url.searchParams.set('market_data', 'true'); + url.searchParams.set('community_data', 'false'); + url.searchParams.set('developer_data', 'false'); + url.searchParams.set('sparkline', 'false'); + + const response = await fetch(url.toString()); + const rates = await response.json(); + return rates; +}; + +const fetchCoinList = async (): Promise => { + const url = new URL('/api/v3/coins/list', BASE_URL); + + const response = await fetch(url.toString()); + const tokens = await response.json(); + return tokens; +}; + +const loadTokenRateAction = (token: Token): AsyncAction => async ( + dispatch: Dispatch +): Promise => { + const { symbol } = token; + try { + const tokens = await fetchCoinList(); + const tokenData = tokens.find(t => t.symbol === symbol.toLowerCase()); + if (!tokenData) return; + + const res = await fetchCoinRate(tokenData.id); + if (res) { + dispatch({ + type: RATE_UPDATE, + network: res.symbol, + rates: res.market_data.current_price, + }); + } + } catch (err) { + console.log(err); + } +}; + /** * Middleware */ @@ -64,6 +122,10 @@ const CoingeckoService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDi api.dispatch(loadRateAction()); } + if (action.type === TOKEN.ADD) { + api.dispatch(loadTokenRateAction(action.payload)); + } + return action; }; diff --git a/src/services/LocalStorageService.js b/src/services/LocalStorageService.js index 5a258ada..4f79cbe5 100644 --- a/src/services/LocalStorageService.js +++ b/src/services/LocalStorageService.js @@ -25,6 +25,10 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar case WALLET.SET_LANGUAGE: api.dispatch(LocalStorageActions.setLanguage()); break; + + case WALLET.SET_LOCAL_CURRENCY: + api.dispatch(LocalStorageActions.setLocalCurrency()); + break; // first time saving case CONNECT.REMEMBER: api.dispatch(LocalStorageActions.save()); diff --git a/src/utils/__tests__/cryptoUriParser.js b/src/utils/__tests__/cryptoUriParser.js deleted file mode 100644 index 1dde9ad6..00000000 --- a/src/utils/__tests__/cryptoUriParser.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as utils from '../cryptoUriParser'; - -describe('crypto uri parser', () => { - it('parseUri', () => { - expect(utils.parseUri('http://www.trezor.io')).toEqual({ address: '//www.trezor.io' }); // TODO: Error in function - expect(utils.parseUri('www.trezor.io')).toEqual({ address: 'www.trezor.io' }); - expect(utils.parseUri('www.trezor.io/TT')).toEqual({ address: 'www.trezor.io/TT' }); - expect(utils.parseUri('www.trezor.io/TT?param1=aha')).toEqual({ - address: 'www.trezor.io/TT', - param1: 'aha', - }); - expect(utils.parseUri('www.trezor.io/TT?param1=aha¶m2=hah')).toEqual({ - address: 'www.trezor.io/TT', - param1: 'aha', - param2: 'hah', - }); - }); -}); diff --git a/src/utils/__tests__/fiatConverter.test.js b/src/utils/__tests__/fiatConverter.test.js new file mode 100644 index 00000000..19d9cb0e --- /dev/null +++ b/src/utils/__tests__/fiatConverter.test.js @@ -0,0 +1,114 @@ +import BigNumber from 'bignumber.js'; +import * as utils from '../fiatConverter'; + +describe('fiatConverter utils: toFiatCurrency', () => { + const ratesETH = { + network: 'eth', + rates: { + czk: 3007.1079886708517, + eos: 36.852136278995445, + eur: 117.13118845579191, + gbp: 100.43721437661289, + }, + }; + + it('to existing fiat currency', () => { + expect(utils.toFiatCurrency('1', 'czk', ratesETH)).toBe('3007.11'); + expect(utils.toFiatCurrency('0', 'czk', ratesETH)).toBe('0.00'); + expect(utils.toFiatCurrency('1.00000000000', 'czk', ratesETH)).toBe('3007.11'); + expect(utils.toFiatCurrency('0.12345678910111213', 'eur', ratesETH)).toBe('14.46'); + }); + + it('to missing fiat currency', () => { + expect(utils.toFiatCurrency('1', 'usd', ratesETH)).toBe(''); + expect(utils.toFiatCurrency('0', 'usd', ratesETH)).toBe(''); + expect(utils.toFiatCurrency('1.00000000000', 'usd', ratesETH)).toBe(''); + expect(utils.toFiatCurrency('0.12345678910111213', 'usd', ratesETH)).toBe(''); + }); + + it('non-numeric amount to fiat currency', () => { + expect(utils.toFiatCurrency(undefined, 'czk', ratesETH)).toBe(''); + expect(utils.toFiatCurrency(null, 'czk', ratesETH)).toBe(''); + expect(utils.toFiatCurrency('12133.3131.3141.4', 'czk', ratesETH)).toBe(''); + expect(utils.toFiatCurrency(BigNumber('nanbla'), 'czk', ratesETH)).toBe(''); + }); + + it('with null/undefined/empty rates', () => { + expect(utils.toFiatCurrency('1', 'czk', {})).toBe(''); + expect(utils.toFiatCurrency('1', 'czk', null)).toBe(''); + expect(utils.toFiatCurrency('1', 'czk', undefined)).toBe(''); + }); + + it('with null/undefined/empty currency', () => { + expect(utils.toFiatCurrency('1', {}, ratesETH)).toBe(''); + expect(utils.toFiatCurrency('1', null, ratesETH)).toBe(''); + expect(utils.toFiatCurrency('1', undefined, ratesETH)).toBe(''); + }); +}); + +describe('fiatConverter utils: fromFiatCurrency', () => { + const ratesETH = { + network: 'eth', + rates: { + czk: 3007.1079886708517, + eos: 36.852136278995445, + eur: 117.13118845579191, + gbp: 100.43721437661289, + }, + }; + const decimals = 18; + + it('from existing fiat currency', () => { + expect(utils.fromFiatCurrency('3007.1079886708517', 'czk', ratesETH, decimals)).toBe( + '1.000000000000000000' + ); + expect(utils.fromFiatCurrency('0', 'czk', ratesETH, decimals)).toBe('0.000000000000000000'); + expect(utils.fromFiatCurrency('3007.1079886708517', 'czk', ratesETH, decimals)).toBe( + '1.000000000000000000' + ); + expect(utils.fromFiatCurrency('117.13118845579191', 'eur', ratesETH, decimals)).toBe( + '1.000000000000000000' + ); + }); + + it('from missing fiat currency', () => { + expect(utils.fromFiatCurrency('1', 'usd', ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency('0', 'usd', ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency('1.00000000000', 'usd', ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency('0.12345678910111213', 'usd', ratesETH, decimals)).toBe(''); + }); + + it('non-numeric amount to fiat currency', () => { + expect(utils.fromFiatCurrency(undefined, 'czk', ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency(null, 'czk', ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency('12133.3131.3141.4', 'czk', ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency(BigNumber('nanbla'), 'czk', ratesETH, decimals)).toBe(''); + }); + + it('with null/undefined/empty rates', () => { + expect(utils.fromFiatCurrency('1', 'czk', {}, decimals)).toBe(''); + expect(utils.fromFiatCurrency('1', 'czk', null, decimals)).toBe(''); + expect(utils.fromFiatCurrency('1', 'czk', undefined, decimals)).toBe(''); + }); + + it('with null/undefined/empty currency', () => { + expect(utils.fromFiatCurrency('1', {}, ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency('1', null, ratesETH, decimals)).toBe(''); + expect(utils.fromFiatCurrency('1', undefined, ratesETH, decimals)).toBe(''); + }); + + it('different decimals', () => { + expect(utils.fromFiatCurrency('3007.1079886708517', 'czk', ratesETH, 1)).toBe('1.0'); + expect(utils.fromFiatCurrency('0', 'czk', ratesETH, 0)).toBe('0'); + expect(utils.fromFiatCurrency('3007.1079886708517', 'czk', ratesETH, 5)).toBe('1.00000'); + }); + + it('from fiat currency with comma decimal separator', () => { + expect(utils.fromFiatCurrency('3007,1079886708517', 'czk', ratesETH, decimals)).toBe( + '1.000000000000000000' + ); + expect(utils.fromFiatCurrency('117,13118845579191', 'eur', ratesETH, decimals)).toBe( + '1.000000000000000000' + ); + }); +}); diff --git a/src/utils/fiatConverter.js b/src/utils/fiatConverter.js new file mode 100644 index 00000000..b92f8900 --- /dev/null +++ b/src/utils/fiatConverter.js @@ -0,0 +1,44 @@ +import BigNumber from 'bignumber.js'; + +const toFiatCurrency = (amount, fiatCurrency, networkRates) => { + // calculate amount in local currency + if (!networkRates || !networkRates.rates || !amount) { + return ''; + } + + const rate = networkRates.rates[fiatCurrency]; + if (!rate) { + return ''; + } + + let formattedAmount = amount; + if (typeof amount === 'string') { + formattedAmount = amount.replace(',', '.'); + } + + let localAmount = BigNumber(formattedAmount).times(rate); + localAmount = localAmount.isNaN() ? '' : localAmount.toFixed(2); + return localAmount; +}; + +const fromFiatCurrency = (localAmount, fiatCurrency, networkRates, decimals) => { + if (!networkRates || !networkRates.rates || !localAmount) { + return ''; + } + + const rate = networkRates.rates[fiatCurrency]; + if (!rate) { + return ''; + } + + let formattedLocalAmount = localAmount; + if (typeof localAmount === 'string') { + formattedLocalAmount = localAmount.replace(',', '.'); + } + + let amount = BigNumber(formattedLocalAmount).div(rate); + amount = amount.isNaN() ? '' : amount.toFixed(decimals); + return amount; +}; + +export { toFiatCurrency, fromFiatCurrency }; diff --git a/src/utils/l10n.js b/src/utils/l10n.js index 8ced69f6..11170d0f 100644 --- a/src/utils/l10n.js +++ b/src/utils/l10n.js @@ -1,4 +1,4 @@ -import { LANGUAGE } from 'config/variables'; +import { LANGUAGE } from 'config/app'; export const getInitialLocale = (defaultLocale = 'en') => { const browserLocale = navigator.language.split('-')[0]; diff --git a/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js index 3df1c5ed..4ebb7cb7 100644 --- a/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js @@ -8,7 +8,8 @@ import styled, { css } from 'styled-components'; import * as stateUtils from 'reducers/utils'; import Tooltip from 'components/Tooltip'; import ICONS from 'config/icons'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; +import { toFiatCurrency } from 'utils/fiatConverter'; import { NavLink } from 'react-router-dom'; import { findDeviceAccounts } from 'reducers/AccountsReducer'; @@ -104,8 +105,6 @@ const AccountMenu = (props: Props) => { if (!selected || !network) return null; - const fiatRate = props.fiat.find(f => f.network === network.shortcut); - const deviceAccounts: Accounts = findDeviceAccounts(accounts, selected, location.state.network); const selectedAccounts = deviceAccounts.map((account, i) => { @@ -113,6 +112,9 @@ const AccountMenu = (props: Props) => { const url: string = location.pathname.replace(/account+\/([0-9]*)/, `account/${i}`); let balance: ?string = null; + const fiatRates = props.fiat.find(f => f.network === network.shortcut); + const { localCurrency } = props.wallet; + let fiat = ''; if (account.balance !== '') { const pending = stateUtils.getAccountPendingTx(props.pending, account); const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol); @@ -121,10 +123,9 @@ const AccountMenu = (props: Props) => { .toString(10); balance = `${availableBalance} ${network.symbol}`; - if (fiatRate) { - const accountBalance = new BigNumber(availableBalance); - const fiat = accountBalance.times(fiatRate.value).toFixed(2); - balance = `${availableBalance} ${network.symbol} / $${fiat}`; + if (fiatRates) { + fiat = toFiatCurrency(availableBalance, localCurrency, fiatRates); + balance = `${availableBalance} ${network.symbol} / `; } } @@ -140,7 +141,20 @@ const AccountMenu = (props: Props) => { {...l10nCommonMessages.TR_ACCOUNT_HASH} values={{ number: account.index + 1 }} /> - {balance && {balance}} + {balance && ( + + {balance} + {fiatRates && ( + + )} + + )} {!balance && ( diff --git a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js index 2e9dafaa..7bd10306 100644 --- a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js @@ -2,8 +2,10 @@ import React, { PureComponent } from 'react'; import styled from 'styled-components'; import PropTypes from 'prop-types'; import Icon from 'components/Icon'; +import Link from 'components/Link'; import DeviceIcon from 'components/images/DeviceIcon'; import { FormattedMessage } from 'react-intl'; +import { getPattern } from 'support/routes'; import icons from 'config/icons'; import colors from 'config/colors'; @@ -29,6 +31,12 @@ const Item = styled.div` } `; +const Divider = styled.div` + width: 100%; + height: 1px; + background: ${colors.DIVIDER}; +`; + const Label = styled.div` padding-left: 15px; `; @@ -99,6 +107,15 @@ class MenuItems extends PureComponent { + + + + + + + ); } diff --git a/src/views/Wallet/components/LeftNavigation/index.js b/src/views/Wallet/components/LeftNavigation/index.js index b2c9cdca..e5e6bc4d 100644 --- a/src/views/Wallet/components/LeftNavigation/index.js +++ b/src/views/Wallet/components/LeftNavigation/index.js @@ -10,16 +10,19 @@ import icons from 'config/icons'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; import styled from 'styled-components'; import DeviceHeader from 'components/DeviceHeader'; +// import Link from 'components/Link'; import * as deviceUtils from 'utils/device'; import Tooltip from 'components/Tooltip'; import { FormattedMessage } from 'react-intl'; +// import { getPattern } from 'support/routes'; import AccountMenu from './components/AccountMenu'; import CoinMenu from './components/CoinMenu'; import DeviceMenu from './components/DeviceMenu'; import Sidebar from './components/Sidebar'; import type { Props } from './components/common'; +// import l10nCommonMessages from 'views/common.messages'; import l10nMessages from './index.messages'; const Header = styled(DeviceHeader)` @@ -310,6 +313,27 @@ class LeftNavigation extends React.PureComponent { {this.props.devices.length} )} + {/* + } + maxWidth={200} + placement="bottom" + enterDelayMs={0.5} + > + + + + + + */} ( + + + + + +); + +export default TopNavigationWalletSettings; diff --git a/src/views/Wallet/index.js b/src/views/Wallet/index.js index 36ef5cc0..f601fce2 100644 --- a/src/views/Wallet/index.js +++ b/src/views/Wallet/index.js @@ -5,6 +5,7 @@ import colors from 'config/colors'; import styled, { css } from 'styled-components'; import { connect } from 'react-redux'; import { Route, withRouter } from 'react-router-dom'; +import { getPattern } from 'support/routes'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { State } from 'flowtype'; @@ -27,6 +28,7 @@ import Backdrop from 'components/Backdrop'; import LeftNavigation from './components/LeftNavigation/Container'; import TopNavigationAccount from './components/TopNavigationAccount'; import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings'; +import TopNavigationWalletSettings from './components/TopNavigationWalletSettings'; type StateProps = { wallet: $ElementType, @@ -76,6 +78,7 @@ const MainContent = styled.article` flex-direction: column; overflow: auto; border-top-right-radius: 4px; + border-top-left-radius: 4px; @media screen and (max-width: ${SCREEN_SIZE.SM}) { ${props => @@ -89,6 +92,7 @@ const MainContent = styled.article` @media screen and (max-width: 1170px) { border-top-right-radius: 0px; + border-top-left-radius: 0px; } `; @@ -132,13 +136,17 @@ const Wallet = (props: Props) => ( + diff --git a/src/views/Wallet/views/Account/Send/ethereum/index.js b/src/views/Wallet/views/Account/Send/ethereum/index.js index 37fe1860..842cb4ac 100644 --- a/src/views/Wallet/views/Account/Send/ethereum/index.js +++ b/src/views/Wallet/views/Account/Send/ethereum/index.js @@ -9,7 +9,8 @@ import Input from 'components/inputs/Input'; import Icon from 'components/Icon'; import Link from 'components/Link'; import ICONS from 'config/icons'; -import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables'; +import { FONT_SIZE, FONT_WEIGHT, TRANSITION, SCREEN_SIZE } from 'config/variables'; +import { FIAT_CURRENCIES } from 'config/app'; import colors from 'config/colors'; import Title from 'views/Wallet/components/Title'; import P from 'components/Paragraph'; @@ -43,6 +44,44 @@ const InputRow = styled.div` padding-bottom: 28px; `; +const LocalAmountWrapper = styled.div` + display: flex; + align-self: flex-start; + margin-top: 26px; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex: 1 0 100%; + justify-content: flex-end; + margin-top: 0px; + padding-top: 28px; + } +`; + +const AmountRow = styled(InputRow)` + display: flex; + align-items: flex-end; + padding-bottom: 28px; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex-wrap: wrap; + } +`; + +const LocalAmountInput = styled(Input)` + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex: 1 1 100%; + } +`; + +const LocalCurrencySelect = styled(Select)` + min-width: 77px; + height: 40px; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex: 1 1 0; + } +`; + const SetMaxAmountButton = styled(Button)` height: 40px; padding: 0 10px; @@ -187,6 +226,16 @@ const QrButton = styled(Button)` padding: 0 10px; `; +const EqualsSign = styled.div` + align-self: center; + padding: 0 10px; + font-size: ${FONT_SIZE.BIGGER}; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + display: none; + } +`; + // render helpers const getAddressInputState = ( address: string, @@ -230,6 +279,10 @@ const getTokensSelectData = ( return tokensSelectData; }; +const buildCurrencyOption = currency => { + return { value: currency, label: currency.toUpperCase() }; +}; + // stateless component const AccountSend = (props: Props) => { const device = props.wallet.selectedDevice; @@ -237,9 +290,11 @@ const AccountSend = (props: Props) => { const { address, amount, + localAmount, setMax, networkSymbol, currency, + localCurrency, feeLevels, selectedFeeLevel, gasPriceNeedsUpdate, @@ -255,8 +310,10 @@ const AccountSend = (props: Props) => { toggleAdvanced, onAddressChange, onAmountChange, + onLocalAmountChange, onSetMax, onCurrencyChange, + onLocalCurrencyChange, onFeeLevelChange, updateFeeLevels, onSend, @@ -337,7 +394,7 @@ const AccountSend = (props: Props) => { ]} /> - + { />, ]} /> - + + = + onLocalAmountChange(event.target.value)} + sideAddons={[ + onLocalCurrencyChange(option)} + value={buildCurrencyOption(localCurrency)} + options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))} + />, + ]} + /> + + diff --git a/src/views/Wallet/views/Account/Send/ripple/index.js b/src/views/Wallet/views/Account/Send/ripple/index.js index 761747f0..1d705755 100644 --- a/src/views/Wallet/views/Account/Send/ripple/index.js +++ b/src/views/Wallet/views/Account/Send/ripple/index.js @@ -9,7 +9,8 @@ import Input from 'components/inputs/Input'; import Icon from 'components/Icon'; import Link from 'components/Link'; import ICONS from 'config/icons'; -import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables'; +import { FONT_SIZE, FONT_WEIGHT, TRANSITION, SCREEN_SIZE } from 'config/variables'; +import { FIAT_CURRENCIES } from 'config/app'; import colors from 'config/colors'; import Title from 'views/Wallet/components/Title'; import P from 'components/Paragraph'; @@ -185,6 +186,53 @@ const QrButton = styled(Button)` padding: 0 10px; `; +const LocalAmountWrapper = styled.div` + display: flex; + align-self: flex-start; + margin-top: 26px; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex: 1 0 100%; + justify-content: flex-end; + margin-top: 0px; + padding-top: 28px; + } +`; + +const AmountRow = styled(InputRow)` + display: flex; + align-items: flex-end; + padding-bottom: 28px; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex-wrap: wrap; + } +`; + +const LocalAmountInput = styled(Input)` + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex: 1 1 100%; + } +`; + +const LocalCurrencySelect = styled(Select)` + min-width: 77px; + height: 40px; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + flex: 1 1 0; + } +`; + +const EqualsSign = styled.div` + align-self: center; + padding: 0 10px; + font-size: ${FONT_SIZE.BIGGER}; + + @media screen and (max-width: ${SCREEN_SIZE.MD}) { + display: none; + } +`; // render helpers const getAddressInputState = ( address: string, @@ -215,6 +263,10 @@ const getAmountInputState = (amountErrors: string, amountWarnings: string): stri return state; }; +const buildCurrencyOption = currency => { + return { value: currency, label: currency.toUpperCase() }; +}; + // stateless component const AccountSend = (props: Props) => { const device = props.wallet.selectedDevice; @@ -222,6 +274,8 @@ const AccountSend = (props: Props) => { const { address, amount, + localAmount, + localCurrency, setMax, feeLevels, selectedFeeLevel, @@ -238,6 +292,8 @@ const AccountSend = (props: Props) => { toggleAdvanced, onAddressChange, onAmountChange, + onLocalAmountChange, + onLocalCurrencyChange, onSetMax, onFeeLevelChange, updateFeeLevels, @@ -307,7 +363,7 @@ const AccountSend = (props: Props) => { ]} /> - + { />, ]} /> - + + = + onLocalAmountChange(event.target.value)} + sideAddons={[ + onLocalCurrencyChange(option)} + value={buildCurrencyOption(localCurrency)} + options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))} + />, + ]} + /> + + diff --git a/src/views/Wallet/views/Account/Summary/components/Balance/index.js b/src/views/Wallet/views/Account/Summary/components/Balance/index.js index fc1e9233..f20a4d7a 100644 --- a/src/views/Wallet/views/Account/Summary/components/Balance/index.js +++ b/src/views/Wallet/views/Account/Summary/components/Balance/index.js @@ -1,12 +1,13 @@ /* @flow */ import React, { PureComponent } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; import BigNumber from 'bignumber.js'; import styled from 'styled-components'; import Icon from 'components/Icon'; import colors from 'config/colors'; import ICONS from 'config/icons'; import Tooltip from 'components/Tooltip'; +import { toFiatCurrency } from 'utils/fiatConverter'; import { FONT_SIZE, FONT_WEIGHT } from 'config/variables'; import type { Network, State as ReducersState } from 'flowtype'; import l10nMessages from './index.messages'; @@ -15,6 +16,7 @@ type Props = { network: Network, balance: string, fiat: $ElementType, + localCurrency: string, }; type State = { @@ -116,15 +118,13 @@ class AccountBalance extends PureComponent { } render() { - const { network } = this.props; - const fiatRate = this.props.fiat.find(f => f.network === network.shortcut); - let accountBalance = ''; + const { network, localCurrency } = this.props; + const fiatRates = this.props.fiat.find(f => f.network === network.shortcut); let fiatRateValue = ''; let fiat = ''; - if (fiatRate) { - accountBalance = new BigNumber(this.props.balance); - fiatRateValue = new BigNumber(fiatRate.value).toFixed(2); - fiat = accountBalance.times(fiatRateValue).toFixed(2); + if (fiatRates) { + fiatRateValue = new BigNumber(fiatRates.rates[localCurrency]).toFixed(2); + fiat = toFiatCurrency(this.props.balance, localCurrency, fiatRates); } const NoRatesTooltip = ( @@ -155,8 +155,20 @@ class AccountBalance extends PureComponent { - {fiatRate ? `$ ${fiat}` : 'N/A'} - {!fiatRate && NoRatesTooltip} + + {fiatRates ? ( + + ) : ( + 'N/A' + )} + + {!fiatRates && NoRatesTooltip} {this.props.balance} {network.symbol} @@ -168,9 +180,19 @@ class AccountBalance extends PureComponent { - {fiatRate ? `$ ${fiatRateValue}` : 'N/A'} + {fiatRates ? ( + + ) : ( + 'N/A' + )} - {!fiatRate && NoRatesTooltip} + {!fiatRates && NoRatesTooltip} 1 {network.symbol} diff --git a/src/views/Wallet/views/Account/Summary/ethereum/index.js b/src/views/Wallet/views/Account/Summary/ethereum/index.js index 758c6bba..bc0a2e3f 100644 --- a/src/views/Wallet/views/Account/Summary/ethereum/index.js +++ b/src/views/Wallet/views/Account/Summary/ethereum/index.js @@ -100,7 +100,12 @@ const AccountSummary = (props: Props) => { /> - +

@@ -116,6 +121,7 @@ const AccountSummary = (props: Props) => { , + localCurrency: string, }; type State = { @@ -115,14 +116,14 @@ class AccountBalance extends PureComponent { } render() { - const { network } = this.props; - const fiatRate = this.props.fiat.find(f => f.network === network.shortcut); + const { network, localCurrency } = this.props; + const fiatRates = this.props.fiat.find(f => f.network === network.shortcut); let accountBalance = ''; let fiatRateValue = ''; let fiat = ''; - if (fiatRate) { + if (fiatRates) { accountBalance = new BigNumber(this.props.balance); - fiatRateValue = new BigNumber(fiatRate.value).toFixed(2); + fiatRateValue = new BigNumber(fiatRates.rates[localCurrency]).toFixed(2); fiat = accountBalance.times(fiatRateValue).toFixed(2); } @@ -152,8 +153,10 @@ class AccountBalance extends PureComponent { - {fiatRate ? `$ ${fiat}` : 'N/A'} - {!fiatRate && NoRatesTooltip} + + {fiatRates ? `${fiat} ${localCurrency}` : 'N/A'} + + {!fiatRates && NoRatesTooltip} {this.props.balance} {network.symbol} @@ -172,9 +175,9 @@ class AccountBalance extends PureComponent { - {fiatRate ? `$ ${fiatRateValue}` : 'N/A'} + {fiatRates ? `${fiatRateValue} ${localCurrency}` : 'N/A'} - {!fiatRate && NoRatesTooltip} + {!fiatRates && NoRatesTooltip} 1 {network.symbol} diff --git a/src/views/Wallet/views/Account/Summary/ripple/index.js b/src/views/Wallet/views/Account/Summary/ripple/index.js index 4f014c97..7ff6b32b 100644 --- a/src/views/Wallet/views/Account/Summary/ripple/index.js +++ b/src/views/Wallet/views/Account/Summary/ripple/index.js @@ -85,6 +85,7 @@ const AccountSummary = (props: Props) => { balance={balance} reserve={reserve} fiat={props.fiat} + localCurrency={props.wallet.localCurrency} /> {TMP_SHOW_HISTORY && ( diff --git a/src/views/Wallet/views/WalletSettings/Container.js b/src/views/Wallet/views/WalletSettings/Container.js new file mode 100644 index 00000000..24e5c4d0 --- /dev/null +++ b/src/views/Wallet/views/WalletSettings/Container.js @@ -0,0 +1,44 @@ +/* @flow */ +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { injectIntl } from 'react-intl'; + +import * as WalletActions from 'actions/WalletActions'; +import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; +import type { State, Dispatch } from 'flowtype'; +import WalletSettings from './index'; + +type OwnProps = {}; + +type StateProps = { + wallet: $ElementType, + fiat: $ElementType, + localStorage: $ElementType, +}; + +type DispatchProps = { + setLocalCurrency: typeof WalletActions.setLocalCurrency, +}; + +export type Props = StateProps & DispatchProps; + +const mapStateToProps: MapStateToProps = ( + state: State +): StateProps => ({ + wallet: state.wallet, + fiat: state.fiat, + localStorage: state.localStorage, +}); + +const mapDispatchToProps: MapDispatchToProps = ( + dispatch: Dispatch +): DispatchProps => ({ + setLocalCurrency: bindActionCreators(WalletActions.setLocalCurrency, dispatch), +}); + +export default injectIntl( + connect( + mapStateToProps, + mapDispatchToProps + )(WalletSettings) +); diff --git a/src/views/Wallet/views/WalletSettings/index.js b/src/views/Wallet/views/WalletSettings/index.js index ba094339..56fc65eb 100644 --- a/src/views/Wallet/views/WalletSettings/index.js +++ b/src/views/Wallet/views/WalletSettings/index.js @@ -1,47 +1,81 @@ +/* @flow */ import styled from 'styled-components'; import React from 'react'; -import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; -import colors from 'config/colors'; -import icons from 'config/icons'; - -import Content from 'views/Wallet/components/Content'; -import { H1 } from 'components/Heading'; -import Icon from 'components/Icon'; import Link from 'components/Link'; +import Content from 'views/Wallet/components/Content'; +import { Select } from 'components/Select'; import Button from 'components/Button'; -const Section = styled.section` +import colors from 'config/colors'; +import { FIAT_CURRENCIES } from 'config/app'; +import { FONT_SIZE } from 'config/variables'; +import l10nCommonMessages from 'views/common.messages'; +import l10nMessages from './index.messages'; +import type { Props } from './Container'; + +const CurrencySelect = styled(Select)` + min-width: 77px; + /* max-width: 200px; */ +`; + +const CurrencyLabel = styled.div` + color: ${colors.TEXT_SECONDARY}; + padding-bottom: 10px; +`; + +const Section = styled.div` + margin-bottom: 20px; +`; + +const Actions = styled.div` display: flex; - flex-direction: column; `; -const Row = styled.div` +const Buttons = styled.div` display: flex; - flex-direction: column; - align-items: center; - padding: 50px 0; + justify-content: flex-end; `; -const StyledH1 = styled(H1)` - text-align: center; +const Info = styled.div` + flex: 1; + color: ${colors.TEXT_SECONDARY}; + font-size: ${FONT_SIZE.SMALL}; + align-self: center; `; -const WalletSettings = () => ( +const buildCurrencyOption = currency => { + return { value: currency, label: currency.toUpperCase() }; +}; + +const WalletSettings = (props: Props) => (
- - - Wallet settings is under construction + + + + props.setLocalCurrency(option.value)} + value={buildCurrencyOption(props.wallet.localCurrency)} + options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))} + /> +
+ + + + + - + - - + +
); -export default connect( - null, - null -)(WalletSettings); +export default WalletSettings; diff --git a/src/views/Wallet/views/WalletSettings/index.messages.js b/src/views/Wallet/views/WalletSettings/index.messages.js new file mode 100644 index 00000000..abcc4d99 --- /dev/null +++ b/src/views/Wallet/views/WalletSettings/index.messages.js @@ -0,0 +1,16 @@ +/* @flow */ +import { defineMessages } from 'react-intl'; +import type { Messages } from 'flowtype/npm/react-intl'; + +const definedMessages: Messages = defineMessages({ + TR_LOCAL_CURRENCY: { + id: 'TR_LOCAL_CURRENCY', + defaultMessage: 'Local currency', + }, + TR_THE_CHANGES_ARE_SAVED: { + id: 'TR_THE_CHANGES_ARE_SAVED', + defaultMessage: 'The changes are saved automatically as they are made', + }, +}); + +export default definedMessages; diff --git a/src/views/common.messages.js b/src/views/common.messages.js index bb578fb9..f4ee59ba 100644 --- a/src/views/common.messages.js +++ b/src/views/common.messages.js @@ -7,6 +7,10 @@ const definedMessages: Messages = defineMessages({ id: 'TR_DEVICE_SETTINGS', defaultMessage: 'Device settings', }, + TR_APPLICATION_SETTINGS: { + id: 'TR_APPLICATION_SETTINGS', + defaultMessage: 'Application settings', + }, TR_ACCOUNT_HASH: { id: 'TR_ACCOUNT_HASH', defaultMessage: 'Account #{number}', @@ -62,6 +66,10 @@ const definedMessages: Messages = defineMessages({ id: 'TR_FORGET_DEVICE', defaultMessage: 'Forget device', }, + TR_CLOSE: { + id: 'TR_CLOSE', + defaultMessage: 'Close', + }, }); export default definedMessages; diff --git a/src/views/index.js b/src/views/index.js index ffd741c9..fa73d779 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -25,7 +25,7 @@ import AccountSignVerify from 'views/Wallet/views/Account/SignVerify/Container'; import WalletDashboard from 'views/Wallet/views/Dashboard'; import WalletDeviceSettings from 'views/Wallet/views/DeviceSettings'; -import WalletSettings from 'views/Wallet/views/WalletSettings'; +import WalletSettings from 'views/Wallet/views/WalletSettings/Container'; import WalletBootloader from 'views/Wallet/views/Bootloader'; import WalletFirmwareUpdate from 'views/Wallet/views/FirmwareUpdate'; import WalletNoBackup from 'views/Wallet/views/NoBackup';