Merge pull request #448 from trezor/feature/currency-switcher

Feature/Fiat currency switcher
pull/453/head
Vladimir Volek 5 years ago committed by GitHub
commit 0ee49b93e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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));
};

@ -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

@ -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';

@ -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,

@ -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<State> => (
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);

@ -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,

@ -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<State> => (
.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);

@ -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';

@ -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': {

@ -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',
];

@ -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' },
];

@ -7,7 +7,7 @@ import type { FiatRateAction } from 'services/CoingeckoService';
export type Fiat = {
+network: string,
value: string,
rates: { [string]: number },
};
export const initialState: Array<Fiat> = [];
@ -15,12 +15,14 @@ export const initialState: Array<Fiat> = [];
const update = (state: Array<Fiat>, action: FiatRateAction): Array<Fiat> => {
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,
},
]);
};

@ -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<FeeLevel>,
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: {

@ -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<FeeLevel>,
@ -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: [],

@ -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;
}

@ -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<any> => {
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<any> => {
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<void> => {
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;
};

@ -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());

@ -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&param2=hah')).toEqual({
address: 'www.trezor.io/TT',
param1: 'aha',
param2: 'hah',
});
});
});

@ -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'
);
});
});

@ -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 };

@ -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];

@ -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 && <Text>{balance}</Text>}
{balance && (
<Text>
{balance}
{fiatRates && (
<FormattedNumber
currency={localCurrency}
value={fiat}
minimumFractionDigits={2}
// eslint-disable-next-line react/style-prop-object
style="currency"
/>
)}
</Text>
)}
{!balance && (
<Text>
<FormattedMessage {...l10nMessages.TR_LOADING_DOT_DOT_DOT} />

@ -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 {
<FormattedMessage {...l10nCommonMessages.TR_FORGET_DEVICE} />
</Label>
</Item>
<Divider />
<Link to={getPattern('wallet-settings')}>
<Item>
<Icon icon={icons.COG} size={25} color={colors.TEXT_SECONDARY} />
<Label>
<FormattedMessage {...l10nCommonMessages.TR_APPLICATION_SETTINGS} />
</Label>
</Item>
</Link>
</Wrapper>
);
}

@ -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<Props, State> {
<Counter>{this.props.devices.length}</Counter>
</Tooltip>
)}
{/* <Tooltip
content={
<FormattedMessage
{...l10nCommonMessages.TR_APPLICATION_SETTINGS}
/>
}
maxWidth={200}
placement="bottom"
enterDelayMs={0.5}
>
<WalletTypeIconWrapper>
<Link to={getPattern('wallet-settings')}>
<Icon
size={25}
color={colors.TEXT_SECONDARY}
hoverColor={colors.TEXT_PRIMARY}
icon={icons.COG}
/>
</Link>
</WalletTypeIconWrapper>
</Tooltip> */}
<Icon
canAnimate={this.state.clicked === true}
isActive={this.props.wallet.dropdownOpened}

@ -0,0 +1,56 @@
import styled from 'styled-components';
import React from 'react';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import { getPattern } from 'support/routes';
import { NavLink } from 'react-router-dom';
import colors from 'config/colors';
import { FormattedMessage } from 'react-intl';
import l10nCommonMessages from 'views/common.messages';
const Wrapper = styled.div`
position: relative;
display: flex;
height: 100%;
flex: 1;
align-items: center;
padding: 0px 30px 0 35px;
overflow-y: hidden;
overflow-x: auto;
`;
const StyledNavLink = styled(NavLink)`
font-weight: ${FONT_WEIGHT.MEDIUM};
font-size: ${FONT_SIZE.TOP_MENU};
color: ${colors.TEXT_SECONDARY};
margin: 0px 4px;
padding: 20px 10px;
white-space: nowrap;
&.active,
&:hover {
transition: all 0.3s ease-in-out;
color: ${colors.TEXT_PRIMARY};
}
&:first-child {
margin-left: 0px;
}
&:last-child {
margin-right: 0px;
}
`;
// TODO: make universal TopNavigation component
const TopNavigationWalletSettings = () => (
<Wrapper>
<StyledNavLink exact to={getPattern('wallet-settings')}>
<FormattedMessage {...l10nCommonMessages.TR_APPLICATION_SETTINGS} />
</StyledNavLink>
</Wrapper>
);
export default TopNavigationWalletSettings;

@ -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<State, 'wallet'>,
@ -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) => (
<MainContent preventBgScroll={props.wallet.showSidebar}>
<Navigation>
<Route
path="/device/:device/network/:network/account/:account"
path={getPattern('wallet-account-summary')}
component={TopNavigationAccount}
/>
<Route
path="/device/:device/device-settings"
path={getPattern('wallet-device-settings')}
component={TopNavigationDeviceSettings}
/>
<Route
path={getPattern('wallet-settings')}
component={TopNavigationWalletSettings}
/>
</Navigation>
<ContextNotifications />
<Log />

@ -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) => {
]}
/>
</InputRow>
<InputRow>
<AmountRow>
<Input
state={getAmountInputState(errors.amount, warnings.amount)}
autoComplete="off"
@ -385,7 +442,29 @@ const AccountSend = (props: Props) => {
/>,
]}
/>
</InputRow>
<LocalAmountWrapper>
<EqualsSign>=</EqualsSign>
<LocalAmountInput
state={getAmountInputState(errors.amount, warnings.amount)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={localAmount}
onChange={event => onLocalAmountChange(event.target.value)}
sideAddons={[
<LocalCurrencySelect
key="local-currency"
isSearchable
isClearable={false}
onChange={option => onLocalCurrencyChange(option)}
value={buildCurrencyOption(localCurrency)}
options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))}
/>,
]}
/>
</LocalAmountWrapper>
</AmountRow>
<InputRow>
<FeeLabelWrapper>

@ -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) => {
]}
/>
</InputRow>
<InputRow>
<AmountRow>
<Input
state={getAmountInputState(errors.amount, warnings.amount)}
autoComplete="off"
@ -350,7 +406,29 @@ const AccountSend = (props: Props) => {
/>,
]}
/>
</InputRow>
<LocalAmountWrapper>
<EqualsSign>=</EqualsSign>
<LocalAmountInput
state={getAmountInputState(errors.amount, warnings.amount)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={localAmount}
onChange={event => onLocalAmountChange(event.target.value)}
sideAddons={[
<LocalCurrencySelect
key="local-currency"
isSearchable={false}
isClearable={false}
onChange={option => onLocalCurrencyChange(option)}
value={buildCurrencyOption(localCurrency)}
options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))}
/>,
]}
/>
</LocalAmountWrapper>
</AmountRow>
<InputRow>
<FeeLabelWrapper>

@ -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<ReducersState, 'fiat'>,
localCurrency: string,
};
type State = {
@ -116,15 +118,13 @@ class AccountBalance extends PureComponent<Props, State> {
}
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<Props, State> {
<FormattedMessage {...l10nMessages.TR_BALANCE} />
</Label>
<TooltipWrapper>
<FiatValue>{fiatRate ? `$ ${fiat}` : 'N/A'}</FiatValue>
{!fiatRate && NoRatesTooltip}
<FiatValue>
{fiatRates ? (
<FormattedNumber
currency={localCurrency}
value={fiat}
minimumFractionDigits={2}
// eslint-disable-next-line react/style-prop-object
style="currency"
/>
) : (
'N/A'
)}
</FiatValue>
{!fiatRates && NoRatesTooltip}
</TooltipWrapper>
<CoinBalance>
{this.props.balance} {network.symbol}
@ -168,9 +180,19 @@ class AccountBalance extends PureComponent<Props, State> {
</Label>
<TooltipWrapper>
<FiatValueRate>
{fiatRate ? `$ ${fiatRateValue}` : 'N/A'}
{fiatRates ? (
<FormattedNumber
currency={localCurrency}
value={fiatRateValue}
minimumFractionDigits={2}
// eslint-disable-next-line react/style-prop-object
style="currency"
/>
) : (
'N/A'
)}
</FiatValueRate>
{!fiatRate && NoRatesTooltip}
{!fiatRates && NoRatesTooltip}
</TooltipWrapper>
<CoinBalance>1 {network.symbol}</CoinBalance>
</BalanceRateWrapper>

@ -100,7 +100,12 @@ const AccountSummary = (props: Props) => {
/>
</Link>
</AccountHeading>
<AccountBalance network={network} balance={balance} fiat={props.fiat} />
<AccountBalance
network={network}
balance={balance}
fiat={props.fiat}
localCurrency={props.wallet.localCurrency}
/>
<H2Wrapper>
<H2>
<FormattedMessage {...l10nSummaryMessages.TR_TOKENS} />
@ -116,6 +121,7 @@ const AccountSummary = (props: Props) => {
<AsyncSelectWrapper>
<AsyncSelect
isSearchable
withDropdownIndicator={false}
defaultOptions
value={null}
isMulti={false}

@ -14,6 +14,7 @@ type Props = {
balance: string,
reserve: string,
fiat: $ElementType<ReducersState, 'fiat'>,
localCurrency: string,
};
type State = {
@ -115,14 +116,14 @@ class AccountBalance extends PureComponent<Props, State> {
}
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<Props, State> {
<BalanceWrapper>
<Label>Balance</Label>
<TooltipWrapper>
<FiatValue>{fiatRate ? `$ ${fiat}` : 'N/A'}</FiatValue>
{!fiatRate && NoRatesTooltip}
<FiatValue>
{fiatRates ? `${fiat} ${localCurrency}` : 'N/A'}
</FiatValue>
{!fiatRates && NoRatesTooltip}
</TooltipWrapper>
<CoinBalance>
{this.props.balance} {network.symbol}
@ -172,9 +175,9 @@ class AccountBalance extends PureComponent<Props, State> {
<Label>Rate</Label>
<TooltipWrapper>
<FiatValueRate>
{fiatRate ? `$ ${fiatRateValue}` : 'N/A'}
{fiatRates ? `${fiatRateValue} ${localCurrency}` : 'N/A'}
</FiatValueRate>
{!fiatRate && NoRatesTooltip}
{!fiatRates && NoRatesTooltip}
</TooltipWrapper>
<CoinBalance>1 {network.symbol}</CoinBalance>
</BalanceRateWrapper>

@ -85,6 +85,7 @@ const AccountSummary = (props: Props) => {
balance={balance}
reserve={reserve}
fiat={props.fiat}
localCurrency={props.wallet.localCurrency}
/>
{TMP_SHOW_HISTORY && (
<H2Wrapper>

@ -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<State, 'wallet'>,
fiat: $ElementType<State, 'fiat'>,
localStorage: $ElementType<State, 'localStorage'>,
};
type DispatchProps = {
setLocalCurrency: typeof WalletActions.setLocalCurrency,
};
export type Props = StateProps & DispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (
state: State
): StateProps => ({
wallet: state.wallet,
fiat: state.fiat,
localStorage: state.localStorage,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (
dispatch: Dispatch
): DispatchProps => ({
setLocalCurrency: bindActionCreators(WalletActions.setLocalCurrency, dispatch),
});
export default injectIntl(
connect(
mapStateToProps,
mapDispatchToProps
)(WalletSettings)
);

@ -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) => (
<Content>
<Section>
<Row>
<Icon size={60} color={colors.WARNING_PRIMARY} icon={icons.WARNING} />
<StyledH1>Wallet settings is under construction</StyledH1>
<CurrencyLabel>
<FormattedMessage {...l10nMessages.TR_LOCAL_CURRENCY} />
</CurrencyLabel>
<CurrencySelect
isSearchable
isClearable={false}
onChange={option => props.setLocalCurrency(option.value)}
value={buildCurrencyOption(props.wallet.localCurrency)}
options={FIAT_CURRENCIES.map(c => buildCurrencyOption(c))}
/>
</Section>
<Actions>
<Info>
<FormattedMessage {...l10nMessages.TR_THE_CHANGES_ARE_SAVED} />
</Info>
<Buttons>
<Link to="/">
<Button>Take me back</Button>
<Button isGreen>
<FormattedMessage {...l10nCommonMessages.TR_CLOSE} />
</Button>
</Link>
</Row>
</Section>
</Buttons>
</Actions>
</Content>
);
export default connect(
null,
null
)(WalletSettings);
export default WalletSettings;

@ -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;

@ -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;

@ -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';

Loading…
Cancel
Save