Merge pull request #513 from trezor/feature/coin-settings

Add coins settings
pull/536/head
Maroš 5 years ago committed by GitHub
commit cb9bbaa3c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,8 @@
## next release
__added__
- Coin visibility settings
## 1.2.0-beta ## 1.2.0-beta
__added__ __added__
- Localization - Localization

@ -12,7 +12,6 @@ import * as buildUtils from 'utils/build';
import * as storageUtils from 'utils/storage'; import * as storageUtils from 'utils/storage';
import * as WalletActions from 'actions/WalletActions'; import * as WalletActions from 'actions/WalletActions';
import * as l10nUtils from 'utils/l10n'; import * as l10nUtils from 'utils/l10n';
import { getAccountTokens } from 'reducers/utils'; import { getAccountTokens } from 'reducers/utils';
import type { Account } from 'reducers/AccountsReducer'; import type { Account } from 'reducers/AccountsReducer';
import type { Token } from 'reducers/TokensReducer'; import type { Token } from 'reducers/TokensReducer';
@ -60,6 +59,8 @@ const KEY_BETA_MODAL: string = '/betaModalPrivacy'; // this key needs to be comp
const KEY_LANGUAGE: string = `${STORAGE_PATH}language`; const KEY_LANGUAGE: string = `${STORAGE_PATH}language`;
const KEY_LOCAL_CURRENCY: string = `${STORAGE_PATH}localCurrency`; const KEY_LOCAL_CURRENCY: string = `${STORAGE_PATH}localCurrency`;
const KEY_HIDE_BALANCE: string = `${STORAGE_PATH}hideBalance`; const KEY_HIDE_BALANCE: string = `${STORAGE_PATH}hideBalance`;
const KEY_HIDDEN_COINS: string = `${STORAGE_PATH}hiddenCoins`;
const KEY_HIDDEN_COINS_EXTERNAL: string = `${STORAGE_PATH}hiddenCoinsExternal`;
// https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js // https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js
// or // or
@ -247,6 +248,19 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => {
}); });
} }
const hiddenCoins = getHiddenCoins(false);
dispatch({
type: WALLET.SET_HIDDEN_COINS,
hiddenCoins,
});
const isExternal = true;
const hiddenCoinsExternal = getHiddenCoins(isExternal);
dispatch({
type: WALLET.SET_HIDDEN_COINS_EXTERNAL,
hiddenCoinsExternal,
});
const userTokens: ?string = storageUtils.get(TYPE, KEY_TOKENS); const userTokens: ?string = storageUtils.get(TYPE, KEY_TOKENS);
if (userTokens) { if (userTokens) {
dispatch({ dispatch({
@ -350,6 +364,89 @@ export const getImportedAccounts = (): ?Array<Account> => {
return null; return null;
}; };
export const handleCoinVisibility = (
coinShortcut: string,
shouldBeVisible: boolean,
isExternal: boolean
): ThunkAction => (dispatch: Dispatch): void => {
const configuration: Array<string> = getHiddenCoins(isExternal);
let newConfig: Array<string> = configuration;
const isAlreadyHidden = configuration.find(coin => coin === coinShortcut);
if (shouldBeVisible) {
newConfig = configuration.filter(coin => coin !== coinShortcut);
} else if (!isAlreadyHidden) {
newConfig = [...configuration, coinShortcut];
}
if (isExternal) {
storageUtils.set(TYPE, KEY_HIDDEN_COINS_EXTERNAL, JSON.stringify(newConfig));
dispatch({
type: WALLET.SET_HIDDEN_COINS_EXTERNAL,
hiddenCoinsExternal: newConfig,
});
} else {
storageUtils.set(TYPE, KEY_HIDDEN_COINS, JSON.stringify(newConfig));
dispatch({
type: WALLET.SET_HIDDEN_COINS,
hiddenCoins: newConfig,
});
}
};
export const toggleGroupCoinsVisibility = (
allCoins: Array<string>,
checked: boolean,
isExternal: boolean
): ThunkAction => (dispatch: Dispatch) => {
// supported coins
if (checked && !isExternal) {
dispatch({
type: WALLET.SET_HIDDEN_COINS,
hiddenCoins: [],
});
storageUtils.set(TYPE, KEY_HIDDEN_COINS, JSON.stringify([]));
}
if (!checked && !isExternal) {
dispatch({
type: WALLET.SET_HIDDEN_COINS,
hiddenCoins: allCoins,
});
storageUtils.set(TYPE, KEY_HIDDEN_COINS, JSON.stringify(allCoins));
}
// external coins
if (checked && isExternal) {
dispatch({
type: WALLET.SET_HIDDEN_COINS_EXTERNAL,
hiddenCoinsExternal: [],
});
storageUtils.set(TYPE, KEY_HIDDEN_COINS_EXTERNAL, JSON.stringify([]));
}
if (!checked && isExternal) {
dispatch({
type: WALLET.SET_HIDDEN_COINS_EXTERNAL,
hiddenCoinsExternal: allCoins,
});
storageUtils.set(TYPE, KEY_HIDDEN_COINS_EXTERNAL, JSON.stringify(allCoins));
}
};
export const getHiddenCoins = (isExternal: boolean): Array<string> => {
let coinsConfig: ?string = '';
if (isExternal) {
coinsConfig = storageUtils.get(TYPE, KEY_HIDDEN_COINS_EXTERNAL);
} else {
coinsConfig = storageUtils.get(TYPE, KEY_HIDDEN_COINS);
}
if (coinsConfig) {
return JSON.parse(coinsConfig);
}
return [];
};
export const removeImportedAccounts = (device: TrezorDevice): ThunkAction => ( export const removeImportedAccounts = (device: TrezorDevice): ThunkAction => (
dispatch: Dispatch dispatch: Dispatch
): void => { ): void => {

@ -25,6 +25,14 @@ export type WalletAction =
state?: RouterLocationState, state?: RouterLocationState,
pathname?: string, pathname?: string,
} }
| {
type: typeof WALLET.SET_HIDDEN_COINS,
hiddenCoins: Array<string>,
}
| {
type: typeof WALLET.SET_HIDDEN_COINS_EXTERNAL,
hiddenCoinsExternal: Array<string>,
}
| { | {
type: typeof WALLET.TOGGLE_DEVICE_DROPDOWN, type: typeof WALLET.TOGGLE_DEVICE_DROPDOWN,
opened: boolean, opened: boolean,

@ -19,3 +19,6 @@ export const TOGGLE_SIDEBAR: 'wallet__toggle_sidebar' = 'wallet__toggle_sidebar'
export const SET_LANGUAGE: 'wallet__set_language' = 'wallet__set_language'; export const SET_LANGUAGE: 'wallet__set_language' = 'wallet__set_language';
export const SET_LOCAL_CURRENCY: 'wallet__set_local_currency' = 'wallet__set_local_currency'; export const SET_LOCAL_CURRENCY: 'wallet__set_local_currency' = 'wallet__set_local_currency';
export const SET_HIDE_BALANCE: 'wallet__set_hide_balance' = 'wallet__set_hide_balance'; export const SET_HIDE_BALANCE: 'wallet__set_hide_balance' = 'wallet__set_hide_balance';
export const SET_HIDDEN_COINS: 'wallet__set_hidden_coins' = 'wallet__set_hidden_coins';
export const SET_HIDDEN_COINS_EXTERNAL: 'wallet__set_hidden_coins_external' =
'wallet__set_hidden_coins_external';

@ -24,6 +24,8 @@ type State = {
firstLocationChange: boolean, firstLocationChange: boolean,
disconnectRequest: ?TrezorDevice, disconnectRequest: ?TrezorDevice,
selectedDevice: ?TrezorDevice, selectedDevice: ?TrezorDevice,
hiddenCoins: Array<string>,
hiddenCoinsExternal: Array<string>,
}; };
const initialState: State = { const initialState: State = {
@ -41,6 +43,8 @@ const initialState: State = {
initialPathname: null, initialPathname: null,
disconnectRequest: null, disconnectRequest: null,
selectedDevice: null, selectedDevice: null,
hiddenCoins: [],
hiddenCoinsExternal: [],
}; };
export default function wallet(state: State = initialState, action: Action): State { export default function wallet(state: State = initialState, action: Action): State {
@ -145,6 +149,18 @@ export default function wallet(state: State = initialState, action: Action): Sta
hideBalance: action.toggled, hideBalance: action.toggled,
}; };
case WALLET.SET_HIDDEN_COINS:
return {
...state,
hiddenCoins: action.hiddenCoins,
};
case WALLET.SET_HIDDEN_COINS_EXTERNAL:
return {
...state,
hiddenCoinsExternal: action.hiddenCoinsExternal,
};
default: default:
return state; return state;
} }

@ -26,6 +26,21 @@ const StyledLink = styled(Link)`
} }
`; `;
const Empty = styled.span`
display: flex;
justify-content: center;
align-items: center;
min-height: 50px;
`;
const StyledLinkEmpty = styled(Link)`
padding: 0;
`;
const Gray = styled.span`
color: ${colors.TEXT_SECONDARY};
`;
class CoinMenu extends PureComponent<Props> { class CoinMenu extends PureComponent<Props> {
getBaseUrl() { getBaseUrl() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
@ -41,8 +56,11 @@ class CoinMenu extends PureComponent<Props> {
} }
getOtherCoins() { getOtherCoins() {
const { hiddenCoinsExternal } = this.props.wallet;
return coins return coins
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.filter(item => !item.isHidden) // hide coins globally in config
.filter(item => !hiddenCoinsExternal.includes(item.id))
.map(coin => { .map(coin => {
const row = ( const row = (
<RowCoin <RowCoin
@ -75,12 +93,53 @@ class CoinMenu extends PureComponent<Props> {
}); });
} }
isTopMenuEmpty() {
const numberOfVisibleNetworks = this.props.localStorage.config.networks
.filter(item => !item.isHidden) // hide coins globally in config
.filter(item => !this.props.wallet.hiddenCoins.includes(item.shortcut));
return numberOfVisibleNetworks.length <= 0;
}
isBottomMenuEmpty() {
const { hiddenCoinsExternal } = this.props.wallet;
const numberOfVisibleNetworks = coins
.filter(item => !item.isHidden)
.filter(item => !hiddenCoinsExternal.includes(item.id));
return numberOfVisibleNetworks.length <= 0;
}
isMenuEmpty() {
return this.isTopMenuEmpty() && this.isBottomMenuEmpty();
}
render() { render() {
const { hiddenCoins } = this.props.wallet;
const { config } = this.props.localStorage; const { config } = this.props.localStorage;
return ( return (
<Wrapper data-test="Main__page__coin__menu"> <Wrapper data-test="Main__page__coin__menu">
{this.isMenuEmpty() && (
<Empty>
<Gray>
<FormattedMessage
{...l10nMessages.TR_SELECT_COINS}
values={{
TR_SELECT_COINS_LINK: (
<StyledLinkEmpty to="/settings">
<FormattedMessage
{...l10nMessages.TR_SELECT_COINS_LINK}
/>
</StyledLinkEmpty>
),
}}
/>{' '}
</Gray>
</Empty>
)}
{config.networks {config.networks
.filter(item => !item.isHidden) .filter(item => !item.isHidden) // hide coins globally in config
.filter(item => !hiddenCoins.includes(item.shortcut)) // hide coins by user settings
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map(item => ( .map(item => (
<NavLink <NavLink
@ -95,11 +154,13 @@ class CoinMenu extends PureComponent<Props> {
/> />
</NavLink> </NavLink>
))} ))}
<Divider {!this.isBottomMenuEmpty() && (
testId="Main__page__coin__menu__divider" <Divider
textLeft={<FormattedMessage {...l10nMessages.TR_OTHER_COINS} />} testId="Main__page__coin__menu__divider"
hasBorder textLeft={<FormattedMessage {...l10nMessages.TR_OTHER_COINS} />}
/> hasBorder
/>
)}
{this.getOtherCoins()} {this.getOtherCoins()}
</Wrapper> </Wrapper>
); );

@ -7,6 +7,15 @@ const definedMessages: Messages = defineMessages({
id: 'TR_OTHER_COINS', id: 'TR_OTHER_COINS',
defaultMessage: 'Other coins', defaultMessage: 'Other coins',
}, },
TR_SELECT_COINS: {
id: 'TR_SELECT_COINS',
description: 'COMPLETE SENTENCE: Select a coin in application settings',
defaultMessage: 'Select a coin in {TR_SELECT_COINS_LINK}',
},
TR_SELECT_COINS_LINK: {
id: 'TR_SELECT_COINS_LINK',
defaultMessage: 'application settings',
},
}); });
export default definedMessages; export default definedMessages;

@ -80,6 +80,7 @@ const Dashboard = (props: Props) => (
<Coins> <Coins>
{props.localStorage.config.networks {props.localStorage.config.networks
.filter(item => !item.isHidden) .filter(item => !item.isHidden)
.filter(item => !props.wallet.hiddenCoins.includes(item.shortcut))
.map(network => ( .map(network => (
<StyledNavLink <StyledNavLink
key={network.shortcut} key={network.shortcut}

@ -5,6 +5,7 @@ import { injectIntl } from 'react-intl';
import type { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';
import * as WalletActions from 'actions/WalletActions'; import * as WalletActions from 'actions/WalletActions';
import * as LocalStorageActions from 'actions/LocalStorageActions';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
import WalletSettings from './index'; import WalletSettings from './index';
@ -21,6 +22,8 @@ type StateProps = {|
type DispatchProps = {| type DispatchProps = {|
setLocalCurrency: typeof WalletActions.setLocalCurrency, setLocalCurrency: typeof WalletActions.setLocalCurrency,
setHideBalance: typeof WalletActions.setHideBalance, setHideBalance: typeof WalletActions.setHideBalance,
handleCoinVisibility: typeof LocalStorageActions.handleCoinVisibility,
toggleGroupCoinsVisibility: typeof LocalStorageActions.toggleGroupCoinsVisibility,
|}; |};
export type Props = {| ...OwnProps, ...StateProps, ...DispatchProps |}; export type Props = {| ...OwnProps, ...StateProps, ...DispatchProps |};
@ -34,6 +37,11 @@ const mapStateToProps = (state: State): StateProps => ({
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
setLocalCurrency: bindActionCreators(WalletActions.setLocalCurrency, dispatch), setLocalCurrency: bindActionCreators(WalletActions.setLocalCurrency, dispatch),
setHideBalance: bindActionCreators(WalletActions.setHideBalance, dispatch), setHideBalance: bindActionCreators(WalletActions.setHideBalance, dispatch),
handleCoinVisibility: bindActionCreators(LocalStorageActions.handleCoinVisibility, dispatch),
toggleGroupCoinsVisibility: bindActionCreators(
LocalStorageActions.toggleGroupCoinsVisibility,
dispatch
),
}); });
export default injectIntl<OwnProps>( export default injectIntl<OwnProps>(

@ -0,0 +1,261 @@
/* @flow */
import styled from 'styled-components';
import React, { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import { FONT_SIZE } from 'config/variables';
import coins from 'constants/coins';
import * as LocalStorageActions from 'actions/LocalStorageActions';
import type { Network } from 'flowtype';
import { colors, Switch, CoinLogo, Tooltip, Icon, icons as ICONS } from 'trezor-ui-components';
import l10nMessages from '../../index.messages';
type Props = {
networks: Array<Network>,
hiddenCoins: Array<string>,
hiddenCoinsExternal: Array<string>,
handleCoinVisibility: typeof LocalStorageActions.handleCoinVisibility,
toggleGroupCoinsVisibility: typeof LocalStorageActions.toggleGroupCoinsVisibility,
};
type State = {
showAllCoins: boolean,
showAllCoinsExternal: boolean,
};
const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
const Row = styled.div`
display: flex;
flex-direction: column;
`;
const Content = styled.div`
display: flex;
margin: 20px 0;
flex-direction: column;
`;
const Label = styled.div`
display: flex;
padding: 10px 0;
justify-content: space-between;
color: ${colors.TEXT_SECONDARY};
align-items: center;
`;
const TooltipIcon = styled(Icon)`
margin-left: 6px;
cursor: pointer;
`;
const CoinRow = styled.div`
height: 50px;
align-items: center;
display: flex;
border-bottom: 1px solid ${colors.DIVIDER};
color: ${colors.TEXT_PRIMARY};
justify-content: space-between;
&:first-child {
border-top: 1px solid ${colors.DIVIDER};
}
&:last-child {
border-bottom: 0;
}
`;
const Left = styled.div`
display: flex;
align-items: center;
`;
const Right = styled.div`
display: flex;
align-items: center;
`;
const Name = styled.div`
display: flex;
font-size: ${FONT_SIZE.BIG};
color: ${colors.TEXT_PRIMARY};
`;
const LogoWrapper = styled.div`
display: flex;
width: 45px;
justify-content: center;
align-items: center;
`;
const ToggleAll = styled.div`
cursor: pointer;
`;
class CoinsSettings extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showAllCoins: this.props.hiddenCoins.length === 0,
showAllCoinsExternal: this.props.hiddenCoinsExternal.length === 0,
};
}
render() {
const { props } = this;
return (
<Wrapper>
<Row>
<Content>
<Label>
<Left>
<FormattedMessage {...l10nMessages.TR_VISIBLE_COINS} />
<Tooltip
content={
<FormattedMessage
{...l10nMessages.TR_VISIBLE_COINS_EXPLAINED}
/>
}
maxWidth={210}
placement="right"
>
<TooltipIcon
icon={ICONS.HELP}
color={colors.TEXT_SECONDARY}
size={12}
/>
</Tooltip>
</Left>
<Right>
<ToggleAll
onClick={() => {
const allCoins = props.networks
.filter(x => !x.isHidden)
.map(item => item.shortcut);
props.toggleGroupCoinsVisibility(
allCoins,
!this.state.showAllCoins,
false
);
this.setState(prevState => ({
showAllCoins: !prevState.showAllCoins,
}));
}}
>
{props.hiddenCoins.length > 0 ? 'Show all' : 'Hide all'}
</ToggleAll>
</Right>
</Label>
{props.networks
.filter(network => !network.isHidden)
.map(network => (
<CoinRow key={network.shortcut}>
<Left>
<LogoWrapper>
<CoinLogo height="23" network={network.shortcut} />
</LogoWrapper>
<Name>{network.name}</Name>
</Left>
<Right>
<Switch
isSmall
checkedIcon={false}
uncheckedIcon={false}
onChange={visible => {
props.handleCoinVisibility(
network.shortcut,
visible,
false
);
}}
checked={!props.hiddenCoins.includes(network.shortcut)}
/>
</Right>
</CoinRow>
))}
</Content>
<Content>
<Label>
<Left>
<FormattedMessage {...l10nMessages.TR_VISIBLE_COINS_EXTERNAL} />
<Tooltip
content={
<FormattedMessage
{...l10nMessages.TR_VISIBLE_COINS_EXPLAINED}
/>
}
maxWidth={210}
placement="right"
>
<TooltipIcon
icon={ICONS.HELP}
color={colors.TEXT_SECONDARY}
size={12}
/>
</Tooltip>
</Left>
<Right>
<ToggleAll
onClick={() => {
const allCoins = coins
.filter(x => !x.isHidden)
.map(coin => coin.id);
props.toggleGroupCoinsVisibility(
allCoins,
!this.state.showAllCoinsExternal,
true
);
this.setState(prevState => ({
showAllCoinsExternal: !prevState.showAllCoinsExternal,
}));
}}
>
{props.hiddenCoinsExternal.length > 0 ? 'Show all' : 'Hide all'}
</ToggleAll>
</Right>
</Label>
{coins
.sort((a, b) => a.order - b.order)
.map(network => (
<CoinRow key={network.id}>
<Left>
<LogoWrapper>
<CoinLogo height="23" network={network.id} />
</LogoWrapper>
<Name>{network.coinName}</Name>
</Left>
<Right>
<Switch
isSmall
checkedIcon={false}
uncheckedIcon={false}
onChange={visible => {
props.handleCoinVisibility(
network.id,
visible,
true
);
}}
checked={
!props.hiddenCoinsExternal.includes(network.id)
}
/>
</Right>
</CoinRow>
))}
</Content>
</Row>
</Wrapper>
);
}
}
export default CoinsSettings;

@ -17,6 +17,7 @@ import {
import { FIAT_CURRENCIES } from 'config/app'; import { FIAT_CURRENCIES } from 'config/app';
import { FONT_SIZE } from 'config/variables'; import { FONT_SIZE } from 'config/variables';
import l10nCommonMessages from 'views/common.messages'; import l10nCommonMessages from 'views/common.messages';
import Coins from './components/Coins';
import l10nMessages from './index.messages'; import l10nMessages from './index.messages';
import type { Props } from './Container'; import type { Props } from './Container';
@ -72,7 +73,10 @@ const TooltipIcon = styled(Icon)`
`; `;
const buildCurrencyOption = currency => { const buildCurrencyOption = currency => {
return { value: currency, label: currency.toUpperCase() }; return {
value: currency,
label: currency.toUpperCase(),
};
}; };
const WalletSettings = (props: Props) => ( const WalletSettings = (props: Props) => (
@ -102,6 +106,9 @@ const WalletSettings = (props: Props) => (
</Tooltip> </Tooltip>
</Label> </Label>
<Switch <Switch
isSmall
checkedIcon={false}
uncheckedIcon={false}
onChange={checked => { onChange={checked => {
props.setHideBalance(checked); props.setHideBalance(checked);
}} }}
@ -109,6 +116,15 @@ const WalletSettings = (props: Props) => (
/> />
</Row> </Row>
</Section> </Section>
<Section>
<Coins
networks={props.localStorage.config.networks}
handleCoinVisibility={props.handleCoinVisibility}
toggleGroupCoinsVisibility={props.toggleGroupCoinsVisibility}
hiddenCoins={props.wallet.hiddenCoins}
hiddenCoinsExternal={props.wallet.hiddenCoinsExternal}
/>
</Section>
<Actions> <Actions>
<Info> <Info>
<FormattedMessage {...l10nMessages.TR_THE_CHANGES_ARE_SAVED} /> <FormattedMessage {...l10nMessages.TR_THE_CHANGES_ARE_SAVED} />

@ -16,6 +16,19 @@ const definedMessages: Messages = defineMessages({
id: 'TR_THE_CHANGES_ARE_SAVED', id: 'TR_THE_CHANGES_ARE_SAVED',
defaultMessage: 'The changes are saved automatically as they are made', defaultMessage: 'The changes are saved automatically as they are made',
}, },
TR_VISIBLE_COINS: {
id: 'TR_VISIBLE_COINS',
defaultMessage: 'Visible coins',
},
TR_VISIBLE_COINS_EXTERNAL: {
id: 'TR_VISIBLE_COINS',
defaultMessage: 'Visible external coins',
},
TR_VISIBLE_COINS_EXPLAINED: {
id: 'TR_VISIBLE_COINS_EXPLAINED',
defaultMessage:
'Select the coins you wish to see in the wallet interface. You will be able to change your preferences later.',
},
}); });
export default definedMessages; export default definedMessages;

Loading…
Cancel
Save