From 07ecad7ef93265bff8b55266598d539ec91510ca Mon Sep 17 00:00:00 2001 From: Adriano Cola Date: Mon, 2 Sep 2024 09:04:19 -0300 Subject: [PATCH] feat: add option to hide balance, closes leather-io#5096 --- src/app/components/account-total-balance.tsx | 3 +- .../components/balance/hideable-balance.tsx | 14 ++++ .../crypto-asset-item.layout.tsx | 15 ++-- .../card/components/available-balance.tsx | 3 +- .../transaction-item.layout.tsx | 6 +- src/app/features/settings/settings.tsx | 21 ++++++ src/app/pages/home/home.tsx | 6 ++ src/app/store/settings/settings.actions.ts | 5 ++ src/app/store/settings/settings.selectors.ts | 6 ++ src/app/store/settings/settings.slice.ts | 4 ++ .../account/account.card.stories.tsx | 24 +++++++ .../ui/components/account/account.card.tsx | 71 +++++++++++++------ tests/selectors/shared-component.selectors.ts | 4 ++ tests/specs/home/home.spec.ts | 34 +++++++++ 14 files changed, 184 insertions(+), 32 deletions(-) create mode 100644 src/app/components/balance/hideable-balance.tsx create mode 100644 tests/specs/home/home.spec.ts diff --git a/src/app/components/account-total-balance.tsx b/src/app/components/account-total-balance.tsx index 86e2123c93d..99e21880c1a 100644 --- a/src/app/components/account-total-balance.tsx +++ b/src/app/components/account-total-balance.tsx @@ -5,6 +5,7 @@ import { styled } from 'leather-styles/jsx'; import { SkeletonLoader, shimmerStyles } from '@leather.io/ui'; import { useTotalBalance } from '@app/common/hooks/balance/use-total-balance'; +import { HideableBalance } from '@app/components/balance/hideable-balance'; interface AccountTotalBalanceProps { btcAddress: string; @@ -26,7 +27,7 @@ export const AccountTotalBalance = memo(({ btcAddress, stxAddress }: AccountTota textStyle="label.02" data-state={isLoadingAdditionalData || isFetching ? 'loading' : undefined} > - {totalUsdBalance} + {totalUsdBalance} ); diff --git a/src/app/components/balance/hideable-balance.tsx b/src/app/components/balance/hideable-balance.tsx new file mode 100644 index 00000000000..787e1c2ffae --- /dev/null +++ b/src/app/components/balance/hideable-balance.tsx @@ -0,0 +1,14 @@ +import { type HTMLStyledProps, styled } from 'leather-styles/jsx'; + +import { useHideBalance } from '@app/store/settings/settings.selectors'; + +interface HideableBalanceProps extends HTMLStyledProps<'span'> { + children: React.ReactNode; + forceHidden?: boolean; +} + +export function HideableBalance({ forceHidden, children, ...rest }: HideableBalanceProps) { + const hideBalance = useHideBalance(); + + return {hideBalance || forceHidden ? '•••••' : children}; +} diff --git a/src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx b/src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx index 58d4865b988..f4538eb2b49 100644 --- a/src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx +++ b/src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, styled } from 'leather-styles/jsx'; +import { Box, Flex } from 'leather-styles/jsx'; import type { Money } from '@leather.io/models'; import { @@ -11,6 +11,8 @@ import { } from '@leather.io/ui'; import { spamFilter } from '@leather.io/utils'; +import { HideableBalance } from '@app/components/balance/hideable-balance'; +import { useHideBalance } from '@app/store/settings/settings.selectors'; import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; import { parseCryptoAssetBalance } from './crypto-asset-item.layout.utils'; @@ -43,6 +45,7 @@ export function CryptoAssetItemLayout({ titleLeft, titleRightBulletInfo, }: CryptoAssetItemLayoutProps) { + const hideBalance = useHideBalance(); const { availableBalanceString, dataTestId, formattedBalance } = parseCryptoAssetBalance(availableBalance); @@ -50,17 +53,17 @@ export function CryptoAssetItemLayout({ - {formattedBalance.value} {balanceSuffix} - + {titleRightBulletInfo} @@ -76,7 +79,9 @@ export function CryptoAssetItemLayout({ data-state={isLoadingAdditionalData ? 'loading' : undefined} className={shimmerStyles} > - {availableBalance.amount.toNumber() > 0 ? fiatBalance : null} + + {availableBalance.amount.toNumber() > 0 ? fiatBalance : null} + {captionRightBulletInfo} diff --git a/src/app/components/layout/card/components/available-balance.tsx b/src/app/components/layout/card/components/available-balance.tsx index 13e20c508a2..0dfefa57bb7 100644 --- a/src/app/components/layout/card/components/available-balance.tsx +++ b/src/app/components/layout/card/components/available-balance.tsx @@ -2,6 +2,7 @@ import { Box, Flex, HStack, styled } from 'leather-styles/jsx'; import { InfoCircleIcon } from '@leather.io/ui'; +import { HideableBalance } from '@app/components/balance/hideable-balance'; import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; interface AvailableBalanceProps { @@ -26,7 +27,7 @@ export function AvailableBalance({ - {balance} + {balance} ); diff --git a/src/app/components/transaction-item/transaction-item.layout.tsx b/src/app/components/transaction-item/transaction-item.layout.tsx index 1bff627b015..26a5073860c 100644 --- a/src/app/components/transaction-item/transaction-item.layout.tsx +++ b/src/app/components/transaction-item/transaction-item.layout.tsx @@ -1,9 +1,11 @@ import { ReactNode } from 'react'; -import { HStack, styled } from 'leather-styles/jsx'; +import { HStack } from 'leather-styles/jsx'; import { Caption, ItemLayout, Pressable } from '@leather.io/ui'; +import { HideableBalance } from '@app/components/balance/hideable-balance'; + interface TransactionItemLayoutProps { openTxLink(): void; rightElement?: ReactNode; @@ -49,7 +51,7 @@ export function TransactionItemLayout({ {txStatus && txStatus} } - titleRight={} + titleRight={{txValue}} captionRight={rightElement} /> diff --git a/src/app/features/settings/settings.tsx b/src/app/features/settings/settings.tsx index b35ff5bc822..21e65dedc01 100644 --- a/src/app/features/settings/settings.tsx +++ b/src/app/features/settings/settings.tsx @@ -12,6 +12,8 @@ import { ExitIcon, ExpandIcon, ExternalLinkIcon, + Eye1ClosedIcon, + Eye1Icon, Flag, GlobeTiltedIcon, KeyIcon, @@ -37,6 +39,8 @@ import { SignOut } from '@app/features/settings/sign-out/sign-out-confirm'; import { ThemeSheet } from '@app/features/settings/theme/theme-dialog'; import { useLedgerDeviceTargetId } from '@app/store/ledger/ledger.selectors'; import { useCurrentNetworkId } from '@app/store/networks/networks.selectors'; +import { useToggleHideBalance } from '@app/store/settings/settings.actions'; +import { useHideBalance } from '@app/store/settings/settings.selectors'; import { openFeedbackSheet } from '../feedback-button/feedback-button'; import { extractDeviceNameFromKnownTargetIds } from '../ledger/utils/generic-ledger-utils'; @@ -67,6 +71,9 @@ export function Settings({ const { walletType } = useWalletType(); const targetId = useLedgerDeviceTargetId(); + const hideBalance = useHideBalance(); + const toggleHideBalance = useToggleHideBalance(); + const location = useLocation(); const { isPressed: showAdvancedMenuOptions } = useModifierKey('alt', 120); @@ -189,6 +196,20 @@ export function Settings({ + + { + void analytics.track('click_toggle_privacy'); + toggleHideBalance(); + }} + > + : }> + + Toggle privacy + + + diff --git a/src/app/pages/home/home.tsx b/src/app/pages/home/home.tsx index 787cd57b9cb..036bf907916 100644 --- a/src/app/pages/home/home.tsx +++ b/src/app/pages/home/home.tsx @@ -18,6 +18,8 @@ import { ModalBackgroundWrapper } from '@app/routes/components/modal-background- import { useCurrentAccountIndex } from '@app/store/accounts/account'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useToggleHideBalance } from '@app/store/settings/settings.actions'; +import { useHideBalance } from '@app/store/settings/settings.selectors'; import { AccountCard } from '@app/ui/components/account/account.card'; import { AccountActions } from './components/account-actions'; @@ -30,6 +32,8 @@ export function Home() { const navigate = useNavigate(); const account = useCurrentStacksAccount(); const currentAccountIndex = useCurrentAccountIndex(); + const hideBalance = useHideBalance(); + const toggleHideBalance = useToggleHideBalance(); const { data: name = '', isFetching: isFetchingBnsName } = useAccountDisplayName({ address: account?.address || '', @@ -63,9 +67,11 @@ export function Home() { name={name} balance={totalUsdBalance} toggleSwitchAccount={() => setIsShowingSwitchAccount(!isShowingSwitchAccount)} + toggleHideBlance={toggleHideBalance} isFetchingBnsName={isFetchingBnsName} isLoadingBalance={isLoading} isLoadingAdditionalData={isLoadingAdditionalData} + hideBalance={hideBalance} > diff --git a/src/app/store/settings/settings.actions.ts b/src/app/store/settings/settings.actions.ts index eb38a7800b9..b29935fd305 100644 --- a/src/app/store/settings/settings.actions.ts +++ b/src/app/store/settings/settings.actions.ts @@ -8,3 +8,8 @@ export function useDismissMessage() { const dispatch = useDispatch(); return (messageId: string) => dispatch(settingsActions.messageDismissed(messageId)); } + +export function useToggleHideBalance() { + const dispatch = useDispatch(); + return () => dispatch(settingsActions.toggleHideBlance()); +} diff --git a/src/app/store/settings/settings.selectors.ts b/src/app/store/settings/settings.selectors.ts index dbb64f30d64..bb3b44ec1e0 100644 --- a/src/app/store/settings/settings.selectors.ts +++ b/src/app/store/settings/settings.selectors.ts @@ -20,3 +20,9 @@ const selectDismissedMessageIds = createSelector( export function useDismissedMessageIds() { return useSelector(selectDismissedMessageIds); } + +const selectHideBalance = createSelector(selectSettings, state => state.hideBalance ?? false); + +export function useHideBalance() { + return useSelector(selectHideBalance); +} diff --git a/src/app/store/settings/settings.slice.ts b/src/app/store/settings/settings.slice.ts index edce40b7e0c..304f230f798 100644 --- a/src/app/store/settings/settings.slice.ts +++ b/src/app/store/settings/settings.slice.ts @@ -5,6 +5,7 @@ import { UserSelectedTheme } from '@app/common/theme-provider'; interface InitialState { userSelectedTheme: UserSelectedTheme; dismissedMessages: string[]; + hideBalance?: boolean; } const initialState: InitialState = { @@ -26,5 +27,8 @@ export const settingsSlice = createSlice({ resetMessages(state) { state.dismissedMessages = []; }, + toggleHideBlance(state) { + state.hideBalance = !state.hideBalance; + }, }, }); diff --git a/src/app/ui/components/account/account.card.stories.tsx b/src/app/ui/components/account/account.card.stories.tsx index 10b34416e8f..299f39bba7b 100644 --- a/src/app/ui/components/account/account.card.stories.tsx +++ b/src/app/ui/components/account/account.card.stories.tsx @@ -25,6 +25,7 @@ export function AccountCard() { name="leather.btc" balance="$1,000" toggleSwitchAccount={() => null} + toggleHideBlance={() => null} isLoadingBalance={false} isFetchingBnsName={false} > @@ -44,6 +45,7 @@ export function AccountCardLoading() { name="leather.btc" balance="$1,000" toggleSwitchAccount={() => null} + toggleHideBlance={() => null} isLoadingBalance isFetchingBnsName={false} > @@ -63,6 +65,7 @@ export function AccountCardBnsNameLoading() { name="leather.btc" balance="$1,000" toggleSwitchAccount={() => null} + toggleHideBlance={() => null} isLoadingBalance={false} isFetchingBnsName > @@ -75,3 +78,24 @@ export function AccountCardBnsNameLoading() { ); } + +export function AccountCardHiddenBalance() { + return ( + null} + toggleHideBlance={() => null} + isLoadingBalance={false} + isFetchingBnsName={false} + hideBalance + > + + } label="Send" /> + } label="Receive" /> + } label="Buy" /> + } label="Swap" /> + + + ); +} diff --git a/src/app/ui/components/account/account.card.tsx b/src/app/ui/components/account/account.card.tsx index 79725580e04..3c9e90b40ae 100644 --- a/src/app/ui/components/account/account.card.tsx +++ b/src/app/ui/components/account/account.card.tsx @@ -1,31 +1,45 @@ import { ReactNode } from 'react'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; +import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors'; import { Box, Flex, styled } from 'leather-styles/jsx'; -import { ChevronDownIcon, Link, SkeletonLoader, shimmerStyles } from '@leather.io/ui'; +import { + ChevronDownIcon, + Eye1ClosedIcon, + Eye1Icon, + Link, + Pressable, + SkeletonLoader, + shimmerStyles, +} from '@leather.io/ui'; import { useScaleText } from '@app/common/hooks/use-scale-text'; import { AccountNameLayout } from '@app/components/account/account-name'; +import { HideableBalance } from '@app/components/balance/hideable-balance'; interface AccountCardProps { name: string; balance: string; children: ReactNode; toggleSwitchAccount(): void; + toggleHideBlance(): void; isFetchingBnsName: boolean; isLoadingBalance: boolean; isLoadingAdditionalData?: boolean; + hideBalance?: boolean; } export function AccountCard({ name, balance, toggleSwitchAccount, + toggleHideBlance, children, isFetchingBnsName, isLoadingBalance, isLoadingAdditionalData, + hideBalance, }: AccountCardProps) { const scaleTextRef = useScaleText(); @@ -37,28 +51,38 @@ export function AccountCard({ px={{ base: 'space.05', sm: '0' }} pt={{ base: 'space.05', md: '0' }} > - - - - {name} - + + + + + {name} + - - - - - + + + + + + + + {hideBalance ? : } + + + @@ -66,6 +90,7 @@ export function AccountCard({ textStyle="heading.02" data-state={isLoadingAdditionalData ? 'loading' : undefined} className={shimmerStyles} + data-testid={SharedComponentsSelectors.AccountCardBalanceText} style={{ whiteSpace: 'nowrap', display: 'inline-block', @@ -74,7 +99,7 @@ export function AccountCard({ }} ref={scaleTextRef} > - {balance} + {balance} diff --git a/tests/selectors/shared-component.selectors.ts b/tests/selectors/shared-component.selectors.ts index 1f98f53e98f..1852f82516c 100644 --- a/tests/selectors/shared-component.selectors.ts +++ b/tests/selectors/shared-component.selectors.ts @@ -3,6 +3,10 @@ export enum SharedComponentsSelectors { AddressDisplayer = 'address-displayer', AddressDisplayerPart = 'address-displayer-part', + // AccountCard + AccountCardToggleHideBalanceBtn = 'account-card-toggle-hide-balance-btn', + AccountCardBalanceText = 'account-card-balance-text', + // InfoCard InfoCardAssetValue = 'info-card-asset-value', InfoCardRowValue = 'info-card-row-value', diff --git a/tests/specs/home/home.spec.ts b/tests/specs/home/home.spec.ts new file mode 100644 index 00000000000..9439e559a02 --- /dev/null +++ b/tests/specs/home/home.spec.ts @@ -0,0 +1,34 @@ +import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors.js'; + +import { test } from '../../fixtures/fixtures'; + +test.describe('Home', () => { + test.beforeEach(async ({ extensionId, globalPage, onboardingPage }) => { + await globalPage.setupAndUseApiCalls(extensionId); + await onboardingPage.signInWithTestAccount(extensionId); + }); + + test('that clicking the hide button hides account balance', async ({ homePage }) => { + const visibleBalanceText = await homePage.page + .getByTestId(SharedComponentsSelectors.AccountCardBalanceText) + .textContent(); + test.expect(visibleBalanceText).toBeTruthy(); + + await homePage.page + .getByTestId(SharedComponentsSelectors.AccountCardToggleHideBalanceBtn) + .click(); + + // just checks that the balance text changed (don't care about the implementation) + await test + .expect(homePage.page.getByTestId(SharedComponentsSelectors.AccountCardBalanceText)) + .not.toHaveText(visibleBalanceText!); + + await homePage.page + .getByTestId(SharedComponentsSelectors.AccountCardToggleHideBalanceBtn) + .click(); + + await test + .expect(homePage.page.getByTestId(SharedComponentsSelectors.AccountCardBalanceText)) + .toHaveText(visibleBalanceText!); + }); +});