diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.constants.ts b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.constants.ts new file mode 100644 index 00000000000..52b0b69960a --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.constants.ts @@ -0,0 +1,2 @@ +export const FORMATTED_VALUE_PRICE_TEST_ID = 'formatted-value-price-test-id'; +export const FORMATTED_PERCENTAGE_TEST_ID = 'formatted-percentage-test-id'; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx index affec361321..ac0bdb30a2e 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx @@ -4,6 +4,10 @@ import AggregatedPercentage from './AggregatedPercentage'; import { mockTheme } from '../../../../util/theme'; import { useSelector } from 'react-redux'; import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; +import { + FORMATTED_VALUE_PRICE_TEST_ID, + FORMATTED_PERCENTAGE_TEST_ID, +} from './AggregatedPercentage.constants'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -65,4 +69,22 @@ describe('AggregatedPercentage', () => { color: mockTheme.colors.error.default, }); }); + + it('renders correctly with privacy mode on', () => { + const { getByTestId } = render( + , + ); + + const formattedPercentage = getByTestId(FORMATTED_PERCENTAGE_TEST_ID); + const formattedValuePrice = getByTestId(FORMATTED_VALUE_PRICE_TEST_ID); + + expect(formattedPercentage.props.children).toBe('••••••••••'); + expect(formattedValuePrice.props.children).toBe('••••••••••'); + }); }); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx index c2a94c1bc1a..715587f6ddc 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx @@ -1,14 +1,19 @@ import React from 'react'; -import Text, { +import { TextColor, TextVariant, } from '../../../../component-library/components/Texts/Text'; +import SensitiveText from '../../../../component-library/components/Texts/SensitiveText'; import { View } from 'react-native'; import { renderFiat } from '../../../../util/number'; import { useSelector } from 'react-redux'; import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; import styleSheet from './AggregatedPercentage.styles'; import { useStyles } from '../../../hooks'; +import { + FORMATTED_VALUE_PRICE_TEST_ID, + FORMATTED_PERCENTAGE_TEST_ID, +} from './AggregatedPercentage.constants'; export interface AggregatedPercentageProps { ethFiat: number; @@ -25,11 +30,13 @@ const AggregatedPercentage = ({ tokenFiat, tokenFiat1dAgo, ethFiat1dAgo, + privacyMode = false, }: { ethFiat: number; tokenFiat: number; tokenFiat1dAgo: number; ethFiat1dAgo: number; + privacyMode?: boolean; }) => { const { styles } = useStyles(styleSheet, {}); @@ -46,12 +53,16 @@ const AggregatedPercentage = ({ let percentageTextColor = TextColor.Default; - if (percentageChange === 0) { - percentageTextColor = TextColor.Default; - } else if (percentageChange > 0) { - percentageTextColor = TextColor.Success; + if (!privacyMode) { + if (percentageChange === 0) { + percentageTextColor = TextColor.Default; + } else if (percentageChange > 0) { + percentageTextColor = TextColor.Success; + } else { + percentageTextColor = TextColor.Error; + } } else { - percentageTextColor = TextColor.Error; + percentageTextColor = TextColor.Alternative; } const formattedPercentage = isValidAmount(percentageChange) @@ -70,12 +81,24 @@ const AggregatedPercentage = ({ return ( - + {formattedValuePrice} - - + + {formattedPercentage} - + ); }; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap index 16b825b0e5d..1066d19a41b 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap +++ b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap @@ -21,6 +21,7 @@ exports[`AggregatedPercentage should render correctly 1`] = ` "lineHeight": 22, } } + testID="formatted-value-price-test-id" > +20 USD @@ -36,6 +37,7 @@ exports[`AggregatedPercentage should render correctly 1`] = ` "lineHeight": 22, } } + testID="formatted-percentage-test-id" > (+11.11%) diff --git a/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts b/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts index 1c6f4688b78..43eca0e2271 100644 --- a/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts +++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.types.ts @@ -1,4 +1,5 @@ // External dependencies. +import React from 'react'; import { TextProps } from '../Text/Text.types'; /** @@ -42,5 +43,5 @@ export interface SensitiveTextProps extends TextProps { /** * The text content to be displayed or hidden. */ - children: string; + children: string | React.ReactNode; } diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts index 90c0ffedf6d..a1103280571 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.styles.ts @@ -13,6 +13,7 @@ const styleSheet = () => StyleSheet.create({ balancesContainer: { alignItems: 'flex-end', + flexDirection: 'column', }, balanceLabel: { textAlign: 'right' }, }); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index 75592d6684b..99a8adeb1aa 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -11,7 +11,11 @@ import Cell, { CellVariant, } from '../../../component-library/components/Cells/Cell'; import { useStyles } from '../../../component-library/hooks'; -import Text from '../../../component-library/components/Texts/Text'; +import { selectPrivacyMode } from '../../../selectors/preferencesController'; +import { TextColor } from '../../../component-library/components/Texts/Text'; +import SensitiveText, { + SensitiveTextLength, +} from '../../../component-library/components/Texts/SensitiveText'; import AvatarGroup from '../../../component-library/components/Avatars/AvatarGroup'; import { formatAddress, @@ -61,30 +65,50 @@ const AccountSelectorList = ({ ? AvatarAccountType.Blockies : AvatarAccountType.JazzIcon, ); - + const privacyMode = useSelector(selectPrivacyMode); const getKeyExtractor = ({ address }: Account) => address; const renderAccountBalances = useCallback( - ({ fiatBalance, tokens }: Assets, address: string) => ( - - {fiatBalance} - {tokens && ( - ({ - ...tokenObj, - variant: AvatarVariant.Token, - }))} - /> - )} - - ), - [styles.balancesContainer, styles.balanceLabel], + ({ fiatBalance, tokens }: Assets, address: string) => { + const fiatBalanceStrSplit = fiatBalance.split('\n'); + const fiatBalanceAmount = fiatBalanceStrSplit[0] || ''; + const tokenTicker = fiatBalanceStrSplit[1] || ''; + + return ( + + + {fiatBalanceAmount} + + + {tokenTicker} + + {tokens && ( + ({ + ...tokenObj, + variant: AvatarVariant.Token, + }))} + /> + )} + + ); + }, + [styles.balancesContainer, styles.balanceLabel, privacyMode], ); const onLongPress = useCallback( diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap index 49c9d6e96b1..036a2be8d53 100644 --- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap +++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap @@ -290,6 +290,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272" @@ -309,7 +310,22 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` } > $3200.00 -1 ETH + + + 1 ETH @@ -583,6 +599,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xd018538C87232FF95acbCe4870629b75640a78E7" @@ -602,7 +619,22 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = ` } > $6400.00 -2 ETH + + + 2 ETH @@ -1453,6 +1485,7 @@ exports[`AccountSelectorList renders correctly 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272" @@ -1472,7 +1505,22 @@ exports[`AccountSelectorList renders correctly 1`] = ` } > $3200.00 -1 ETH + + + 1 ETH @@ -1746,6 +1794,7 @@ exports[`AccountSelectorList renders correctly 1`] = ` style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xd018538C87232FF95acbCe4870629b75640a78E7" @@ -1765,7 +1814,22 @@ exports[`AccountSelectorList renders correctly 1`] = ` } > $6400.00 -2 ETH + + + 2 ETH @@ -1949,6 +2013,7 @@ exports[`AccountSelectorList should render all accounts but only the balance for style={ { "alignItems": "flex-end", + "flexDirection": "column", } } testID="account-balance-by-address-0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272" @@ -1968,7 +2033,22 @@ exports[`AccountSelectorList should render all accounts but only the balance for } > $3200.00 -1 ETH + + + 1 ETH diff --git a/app/components/UI/AssetElement/index.constants.ts b/app/components/UI/AssetElement/index.constants.ts new file mode 100644 index 00000000000..1b14c68f51c --- /dev/null +++ b/app/components/UI/AssetElement/index.constants.ts @@ -0,0 +1,2 @@ +export const FIAT_BALANCE_TEST_ID = 'fiat-balance-test-id'; +export const MAIN_BALANCE_TEST_ID = 'main-balance-test-id'; diff --git a/app/components/UI/AssetElement/index.test.tsx b/app/components/UI/AssetElement/index.test.tsx index 1d178a7f4c3..e664027b309 100644 --- a/app/components/UI/AssetElement/index.test.tsx +++ b/app/components/UI/AssetElement/index.test.tsx @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import { render, fireEvent } from '@testing-library/react-native'; import AssetElement from './'; import { getAssetTestId } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { FIAT_BALANCE_TEST_ID, MAIN_BALANCE_TEST_ID } from './index.constants'; describe('AssetElement', () => { const onPressMock = jest.fn(); @@ -54,4 +55,34 @@ describe('AssetElement', () => { expect(onLongPressMock).toHaveBeenCalledWith(erc20Token); }); + + it('renders the fiat and token balance', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(FIAT_BALANCE_TEST_ID)).toBeDefined(); + expect(getByTestId(MAIN_BALANCE_TEST_ID)).toBeDefined(); + }); + + it('renders the fiat balance with privacy mode', () => { + const { getByTestId } = render( + , + ); + + const fiatBalance = getByTestId(FIAT_BALANCE_TEST_ID); + const mainBalance = getByTestId(MAIN_BALANCE_TEST_ID); + + expect(fiatBalance.props.children).toBe('•••••••••'); + expect(mainBalance.props.children).toBe('••••••'); + }); }); diff --git a/app/components/UI/AssetElement/index.tsx b/app/components/UI/AssetElement/index.tsx index a2810c48db0..ff39a4eac1a 100644 --- a/app/components/UI/AssetElement/index.tsx +++ b/app/components/UI/AssetElement/index.tsx @@ -1,9 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; import { TouchableOpacity, StyleSheet, Platform, View } from 'react-native'; -import Text, { - TextVariant, -} from '../../../component-library/components/Texts/Text'; +import { TextVariant } from '../../../component-library/components/Texts/Text'; import SkeletonText from '../Ramp/components/SkeletonText'; import { TokenI } from '../Tokens/types'; import generateTestId from '../../../../wdio/utils/generateTestId'; @@ -15,6 +13,10 @@ import { import { Colors } from '../../../util/theme/models'; import { fontStyles } from '../../../styles/common'; import { useTheme } from '../../../util/theme'; +import SensitiveText, { + SensitiveTextLength, +} from '../../../component-library/components/Texts/SensitiveText'; +import { FIAT_BALANCE_TEST_ID, MAIN_BALANCE_TEST_ID } from './index.constants'; interface AssetElementProps { children?: React.ReactNode; @@ -23,6 +25,7 @@ interface AssetElementProps { onLongPress?: ((asset: TokenI) => void) | null; balance?: string; mainBalance?: string | null; + privacyMode?: boolean; } const createStyles = (colors: Colors) => @@ -63,6 +66,7 @@ const AssetElement: React.FC = ({ mainBalance = null, onPress, onLongPress, + privacyMode = false, }) => { const { colors } = useTheme(); const styles = createStyles(colors); @@ -75,6 +79,8 @@ const AssetElement: React.FC = ({ onLongPress?.(asset); }; + // TODO: Use the SensitiveText component when it's available + // when privacyMode is true, we should hide the balance and the fiat return ( = ({ {balance && ( - {balance === TOKEN_BALANCE_LOADING ? ( ) : ( balance )} - + )} {mainBalance ? ( - + {mainBalance === TOKEN_BALANCE_LOADING ? ( ) : ( mainBalance )} - + ) : null} diff --git a/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap b/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap index d233e8e44fd..0a66cea0de2 100644 --- a/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap @@ -203,6 +203,7 @@ exports[`Balance should render correctly with a fiat balance 1`] = ` "lineHeight": 24, } } + testID="fiat-balance-test-id" > 456 @@ -220,6 +221,7 @@ exports[`Balance should render correctly with a fiat balance 1`] = ` "textTransform": "uppercase", } } + testID="main-balance-test-id" > 123 @@ -433,6 +435,7 @@ exports[`Balance should render correctly without a fiat balance 1`] = ` "textTransform": "uppercase", } } + testID="main-balance-test-id" > 123 diff --git a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap index 5ec60e73b6f..40327fba228 100644 --- a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap +++ b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap @@ -1060,6 +1060,7 @@ exports[`AssetOverview should render correctly 1`] = ` "textTransform": "uppercase", } } + testID="main-balance-test-id" > 0 ETH diff --git a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.constants.ts b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.constants.ts new file mode 100644 index 00000000000..16f1ef78e5c --- /dev/null +++ b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.constants.ts @@ -0,0 +1,2 @@ +export const EYE_SLASH_ICON_TEST_ID = 'eye-slash-icon'; +export const EYE_ICON_TEST_ID = 'eye-icon'; diff --git a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx index e1e1f694376..31c2beee9cf 100644 --- a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx +++ b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.test.tsx @@ -7,6 +7,10 @@ import AppConstants from '../../../../../../app/core/AppConstants'; import Routes from '../../../../../../app/constants/navigation/Routes'; import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/WalletView.selectors'; import { PortfolioBalance } from '.'; +import Engine from '../../../../../core/Engine'; +import { EYE_SLASH_ICON_TEST_ID, EYE_ICON_TEST_ID } from './index.constants'; + +const { PreferencesController } = Engine.context; jest.mock('../../../../../core/Engine', () => ({ getTotalFiatAccountBalance: jest.fn(), @@ -14,6 +18,9 @@ jest.mock('../../../../../core/Engine', () => ({ TokensController: { ignoreTokens: jest.fn(() => Promise.resolve()), }, + PreferencesController: { + setPrivacyMode: jest.fn(), + }, }, })); @@ -138,4 +145,95 @@ describe('PortfolioBalance', () => { screen: Routes.BROWSER.VIEW, }); }); + + it('renders sensitive text when privacy mode is off', () => { + const { getByTestId } = renderPortfolioBalance({ + ...initialState, + engine: { + backgroundState: { + ...initialState.engine.backgroundState, + PreferencesController: { + privacyMode: false, + }, + }, + }, + }); + const sensitiveText = getByTestId( + WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT, + ); + expect(sensitiveText.props.isHidden).toBeFalsy(); + }); + + it('hides sensitive text when privacy mode is on', () => { + const { getByTestId } = renderPortfolioBalance({ + ...initialState, + engine: { + backgroundState: { + ...initialState.engine.backgroundState, + PreferencesController: { + privacyMode: true, + }, + }, + }, + }); + const sensitiveText = getByTestId( + WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT, + ); + expect(sensitiveText.props.children).toEqual('••••••••••••'); + }); + + it('toggles privacy mode when eye icon is pressed', () => { + const { getByTestId } = renderPortfolioBalance({ + ...initialState, + engine: { + backgroundState: { + ...initialState.engine.backgroundState, + PreferencesController: { + privacyMode: false, + }, + }, + }, + }); + + const balanceContainer = getByTestId('balance-container'); + fireEvent.press(balanceContainer); + + expect(PreferencesController.setPrivacyMode).toHaveBeenCalledWith(true); + }); + + it('renders eye icon when privacy mode is off', () => { + const { getByTestId } = renderPortfolioBalance({ + ...initialState, + engine: { + backgroundState: { + ...initialState.engine.backgroundState, + PreferencesController: { + privacyMode: false, + }, + }, + }, + }); + + const eyeIcon = getByTestId(EYE_ICON_TEST_ID); + expect(eyeIcon).toBeDefined(); + expect(eyeIcon.props.name).toBe('Eye'); + }); + + it('renders eye-slash icon when privacy mode is on', () => { + const { getByTestId } = renderPortfolioBalance({ + ...initialState, + engine: { + backgroundState: { + ...initialState.engine.backgroundState, + PreferencesController: { + privacyMode: true, + }, + }, + }, + }); + + const eyeSlashIcon = getByTestId(EYE_SLASH_ICON_TEST_ID); + expect(eyeSlashIcon).toBeDefined(); + expect(eyeSlashIcon.props.name).toBe('EyeSlash'); + }); }); diff --git a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx index 06729279ac3..e58e013c56e 100644 --- a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx +++ b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View } from 'react-native'; +import { View, TouchableOpacity } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { useSelector } from 'react-redux'; import useIsOriginalNativeTokenSymbol from '../../../../hooks/useIsOriginalNativeTokenSymbol/useIsOriginalNativeTokenSymbol'; @@ -15,6 +15,7 @@ import { selectTicker, } from '../../../../../selectors/networkController'; import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController'; +import { selectPrivacyMode } from '../../../../../selectors/preferencesController'; import { RootState } from '../../../../../reducers'; import { renderFiat } from '../../../../../util/number'; import { isTestNet } from '../../../../../util/networks'; @@ -25,16 +26,22 @@ import Button, { ButtonSize, ButtonWidthTypes, } from '../../../../../component-library/components/Buttons/Button'; -import Text, { - TextVariant, -} from '../../../../../component-library/components/Texts/Text'; +import { TextVariant } from '../../../../../component-library/components/Texts/Text'; +import SensitiveText, { + SensitiveTextLength, +} from '../../../../../component-library/components/Texts/SensitiveText'; import AggregatedPercentage from '../../../../../component-library/components-temp/Price/AggregatedPercentage'; -import { IconName } from '../../../../../component-library/components/Icons/Icon'; +import Icon, { + IconSize, + IconName, +} from '../../../../../component-library/components/Icons/Icon'; import { BrowserTab } from '../../types'; import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/WalletView.selectors'; import { strings } from '../../../../../../locales/i18n'; +import { EYE_SLASH_ICON_TEST_ID, EYE_ICON_TEST_ID } from './index.constants'; export const PortfolioBalance = () => { + const { PreferencesController } = Engine.context; const { colors } = useTheme(); const styles = createStyles(colors); const balance = Engine.getTotalFiatAccountBalance(); @@ -49,6 +56,7 @@ export const PortfolioBalance = () => { ); const currentCurrency = useSelector(selectCurrentCurrency); const browserTabs = useSelector((state: RootState) => state.browser.tabs); + const privacyMode = useSelector(selectPrivacyMode); const isOriginalNativeTokenSymbol = useIsOriginalNativeTokenSymbol( chainId, @@ -108,24 +116,54 @@ export const PortfolioBalance = () => { }); }; + const renderAggregatedPercentage = () => { + if (isTestNet(chainId)) { + return null; + } + + return ( + + ); + }; + + const toggleIsBalanceAndAssetsHidden = (value: boolean) => { + PreferencesController.setPrivacyMode(value); + }; + return ( - toggleIsBalanceAndAssetsHidden(!privacyMode)} + testID="balance-container" > - {fiatBalance} - - - {!isTestNet(chainId) ? ( - - ) : null} + + + {fiatBalance} + + + + + + {renderAggregatedPercentage()} +