1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-27 10:48:22 +00:00

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

Add coins settings
This commit is contained in:
Maroš 2019-04-25 14:07:55 +02:00 committed by GitHub
commit cb9bbaa3c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 506 additions and 8 deletions

View File

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

View File

@ -12,7 +12,6 @@ import * as buildUtils from 'utils/build';
import * as storageUtils from 'utils/storage';
import * as WalletActions from 'actions/WalletActions';
import * as l10nUtils from 'utils/l10n';
import { getAccountTokens } from 'reducers/utils';
import type { Account } from 'reducers/AccountsReducer';
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_LOCAL_CURRENCY: string = `${STORAGE_PATH}localCurrency`;
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
// 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);
if (userTokens) {
dispatch({
@ -350,6 +364,89 @@ export const getImportedAccounts = (): ?Array<Account> => {
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 => (
dispatch: Dispatch
): void => {

View File

@ -25,6 +25,14 @@ export type WalletAction =
state?: RouterLocationState,
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,
opened: boolean,

View File

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

View File

@ -24,6 +24,8 @@ type State = {
firstLocationChange: boolean,
disconnectRequest: ?TrezorDevice,
selectedDevice: ?TrezorDevice,
hiddenCoins: Array<string>,
hiddenCoinsExternal: Array<string>,
};
const initialState: State = {
@ -41,6 +43,8 @@ const initialState: State = {
initialPathname: null,
disconnectRequest: null,
selectedDevice: null,
hiddenCoins: [],
hiddenCoinsExternal: [],
};
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,
};
case WALLET.SET_HIDDEN_COINS:
return {
...state,
hiddenCoins: action.hiddenCoins,
};
case WALLET.SET_HIDDEN_COINS_EXTERNAL:
return {
...state,
hiddenCoinsExternal: action.hiddenCoinsExternal,
};
default:
return state;
}

View File

@ -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> {
getBaseUrl() {
const { selectedDevice } = this.props.wallet;
@ -41,8 +56,11 @@ class CoinMenu extends PureComponent<Props> {
}
getOtherCoins() {
const { hiddenCoinsExternal } = this.props.wallet;
return coins
.sort((a, b) => a.order - b.order)
.filter(item => !item.isHidden) // hide coins globally in config
.filter(item => !hiddenCoinsExternal.includes(item.id))
.map(coin => {
const row = (
<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() {
const { hiddenCoins } = this.props.wallet;
const { config } = this.props.localStorage;
return (
<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
.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)
.map(item => (
<NavLink
@ -95,11 +154,13 @@ class CoinMenu extends PureComponent<Props> {
/>
</NavLink>
))}
<Divider
testId="Main__page__coin__menu__divider"
textLeft={<FormattedMessage {...l10nMessages.TR_OTHER_COINS} />}
hasBorder
/>
{!this.isBottomMenuEmpty() && (
<Divider
testId="Main__page__coin__menu__divider"
textLeft={<FormattedMessage {...l10nMessages.TR_OTHER_COINS} />}
hasBorder
/>
)}
{this.getOtherCoins()}
</Wrapper>
);

View File

@ -7,6 +7,15 @@ const definedMessages: Messages = defineMessages({
id: 'TR_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;

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,19 @@ const definedMessages: Messages = defineMessages({
id: 'TR_THE_CHANGES_ARE_SAVED',
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;