From cd9192f8e5b2a0d7f83fbb3ac64e41c0d6e2b9a7 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 +- src/app/components/balance/btc-balance.tsx | 8 ++- src/app/components/balance/stx-balance.tsx | 7 ++- .../crypto-asset-item.layout.tsx | 15 +++-- .../card/components/available-balance.tsx | 3 +- src/app/components/privacy/private-text.tsx | 25 +++++++++ .../transaction-item.layout.tsx | 8 ++- .../stx-crypto-asset-item.tsx | 11 +++- 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 | 32 +++++++++++ .../ui/components/account/account.card.tsx | 55 +++++++++++-------- .../privacy/private-text.layout.tsx | 35 ++++++++++++ tests/selectors/settings.selectors.ts | 1 + tests/selectors/shared-component.selectors.ts | 3 + tests/specs/settings/settings.spec.ts | 23 ++++++++ 19 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 src/app/components/privacy/private-text.tsx create mode 100644 src/app/ui/components/privacy/private-text.layout.tsx diff --git a/src/app/components/account-total-balance.tsx b/src/app/components/account-total-balance.tsx index 86e2123c93d..e7e4657251d 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 { PrivateText } from '@app/components/privacy/private-text'; 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/btc-balance.tsx b/src/app/components/balance/btc-balance.tsx index 3e90079ae69..ab19a85241c 100644 --- a/src/app/components/balance/btc-balance.tsx +++ b/src/app/components/balance/btc-balance.tsx @@ -1,6 +1,8 @@ import { Caption } from '@leather.io/ui'; import { formatMoney } from '@leather.io/utils'; +import { PrivateText } from '@app/components/privacy/private-text'; + import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader'; import { BtcBalanceLoader } from '../loaders/btc-balance-loader'; @@ -9,7 +11,11 @@ export function BtcBalance() { {signer => ( - {balance => {formatMoney(balance.availableBalance)}} + {balance => ( + + {formatMoney(balance.availableBalance)} + + )} )} diff --git a/src/app/components/balance/stx-balance.tsx b/src/app/components/balance/stx-balance.tsx index 56b090692ee..133783ac913 100644 --- a/src/app/components/balance/stx-balance.tsx +++ b/src/app/components/balance/stx-balance.tsx @@ -4,6 +4,7 @@ import { useStxCryptoAssetBalance } from '@leather.io/query'; import { Caption } from '@leather.io/ui'; import { stacksValue } from '@app/common/stacks-utils'; +import { PrivateText } from '@app/components/privacy/private-text'; interface StxBalanceProps { address: string; @@ -21,5 +22,9 @@ export function StxBalance(props: StxBalanceProps) { [filteredBalanceQuery.data?.unlockedBalance.amount] ); - return {stxBalance}; + return ( + + {stxBalance} + + ); } 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..896f09411b4 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 { PrivateText } from '@app/components/privacy/private-text'; +import { useIsPrivacyMode } 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 isPrivacyMode = useIsPrivacyMode(); const { availableBalanceString, dataTestId, formattedBalance } = parseCryptoAssetBalance(availableBalance); @@ -50,17 +53,19 @@ export function CryptoAssetItemLayout({ - {formattedBalance.value} {balanceSuffix} - + {titleRightBulletInfo} @@ -76,7 +81,7 @@ 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..e78eb682c39 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 { PrivateText } from '@app/components/privacy/private-text'; 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/privacy/private-text.tsx b/src/app/components/privacy/private-text.tsx new file mode 100644 index 00000000000..2482cecc8dc --- /dev/null +++ b/src/app/components/privacy/private-text.tsx @@ -0,0 +1,25 @@ +import { type HTMLStyledProps } from 'leather-styles/jsx'; + +import { useTogglePrivacyMode } from '@app/store/settings/settings.actions'; +import { useIsPrivacyMode } from '@app/store/settings/settings.selectors'; +import { PrivateTextLayout } from '@app/ui/components/privacy/private-text.layout'; + +interface PrivacyAwareProps extends HTMLStyledProps<'span'> { + children: React.ReactNode; + canClickToShow?: boolean; +} + +export function PrivateText({ children, canClickToShow, ...rest }: PrivacyAwareProps) { + const isPrivacyMode = useIsPrivacyMode(); + const togglePrivacyMode = useTogglePrivacyMode(); + + return ( + + {children} + + ); +} diff --git a/src/app/components/transaction-item/transaction-item.layout.tsx b/src/app/components/transaction-item/transaction-item.layout.tsx index 1bff627b015..9b883b1f263 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 { PrivateText } from '@app/components/privacy/private-text'; + interface TransactionItemLayoutProps { openTxLink(): void; rightElement?: ReactNode; @@ -17,9 +19,9 @@ interface TransactionItemLayoutProps { function TxValue({ txValue }: { txValue: ReactNode }) { return ( - + {txValue} - + ); } diff --git a/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx index 74f9c291356..4ba1df7e1d4 100644 --- a/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx +++ b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx @@ -10,6 +10,7 @@ import { } from '@leather.io/utils'; import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; +import { PrivateText } from '@app/components/privacy/private-text'; interface StxCryptoAssetItemProps { balance: StxCryptoAssetBalance; @@ -29,9 +30,15 @@ export function StxCryptoAssetItem({ balance, isLoading, onSelectAsset }: StxCry baseCurrencyAmountInQuote(availableBalance, marketData) ); const titleRightBulletInfo = ( - {formatMoneyWithoutSymbol(lockedBalance)} locked + + {formatMoneyWithoutSymbol(lockedBalance)} locked + + ); + const captionRightBulletInfo = ( + + {fiatLockedBalance} locked + ); - const captionRightBulletInfo = {fiatLockedBalance} locked; return ( + + { + void analytics.track('click_toggle_privacy'); + togglePrivacyMode(); + }} + > + : }> + + Toggle privacy + + + diff --git a/src/app/pages/home/home.tsx b/src/app/pages/home/home.tsx index 787cd57b9cb..97f9332d1d2 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 { useTogglePrivacyMode } from '@app/store/settings/settings.actions'; +import { useIsPrivacyMode } 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 isPrivacyMode = useIsPrivacyMode(); + const togglePrivacyMode = useTogglePrivacyMode(); const { data: name = '', isFetching: isFetchingBnsName } = useAccountDisplayName({ address: account?.address || '', @@ -66,6 +70,8 @@ export function Home() { isFetchingBnsName={isFetchingBnsName} isLoadingBalance={isLoading} isLoadingAdditionalData={isLoadingAdditionalData} + isBalancePrivate={isPrivacyMode} + onShowBalance={togglePrivacyMode} > diff --git a/src/app/store/settings/settings.actions.ts b/src/app/store/settings/settings.actions.ts index eb38a7800b9..b65cadb9666 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 useTogglePrivacyMode() { + const dispatch = useDispatch(); + return () => dispatch(settingsActions.togglePrivacyMode()); +} diff --git a/src/app/store/settings/settings.selectors.ts b/src/app/store/settings/settings.selectors.ts index dbb64f30d64..47a9d637239 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 selectIsPrivacyMode = createSelector(selectSettings, state => state.isPrivacyMode ?? false); + +export function useIsPrivacyMode() { + return useSelector(selectIsPrivacyMode); +} diff --git a/src/app/store/settings/settings.slice.ts b/src/app/store/settings/settings.slice.ts index edce40b7e0c..c1c94c7b78f 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[]; + isPrivacyMode?: boolean; } const initialState: InitialState = { @@ -26,5 +27,8 @@ export const settingsSlice = createSlice({ resetMessages(state) { state.dismissedMessages = []; }, + togglePrivacyMode(state) { + state.isPrivacyMode = !state.isPrivacyMode; + }, }, }); diff --git a/src/app/ui/components/account/account.card.stories.tsx b/src/app/ui/components/account/account.card.stories.tsx index 10b34416e8f..3c9f4631f8f 100644 --- a/src/app/ui/components/account/account.card.stories.tsx +++ b/src/app/ui/components/account/account.card.stories.tsx @@ -1,3 +1,6 @@ +import { useState } from 'react'; + +import { TooltipProvider } from '@radix-ui/react-tooltip'; import type { Meta } from '@storybook/react'; import { Flex } from 'leather-styles/jsx'; @@ -15,6 +18,13 @@ const meta: Meta = { component: Component, tags: ['autodocs'], title: 'Layout/AccountCard', + decorators: [ + Story => ( + + + + ), + ], }; export default meta; @@ -75,3 +85,25 @@ export function AccountCardBnsNameLoading() { ); } + +export function AccountCardPrivateBalance() { + const [isBalanceHidden, setisBalanceHidden] = useState(true); + return ( + null} + isLoadingBalance={false} + isFetchingBnsName={false} + isBalancePrivate={isBalanceHidden} + onShowBalance={() => setisBalanceHidden(false)} + > + + } 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..9f72347bfa3 100644 --- a/src/app/ui/components/account/account.card.tsx +++ b/src/app/ui/components/account/account.card.tsx @@ -1,12 +1,14 @@ 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 { useScaleText } from '@app/common/hooks/use-scale-text'; import { AccountNameLayout } from '@app/components/account/account-name'; +import { PrivateTextLayout } from '@app/ui/components/privacy/private-text.layout'; interface AccountCardProps { name: string; @@ -16,16 +18,20 @@ interface AccountCardProps { isFetchingBnsName: boolean; isLoadingBalance: boolean; isLoadingAdditionalData?: boolean; + isBalancePrivate?: boolean; + onShowBalance?(): void; } export function AccountCard({ name, balance, toggleSwitchAccount, + onShowBalance, children, isFetchingBnsName, isLoadingBalance, isLoadingAdditionalData, + isBalancePrivate, }: AccountCardProps) { const scaleTextRef = useScaleText(); @@ -37,28 +43,30 @@ export function AccountCard({ px={{ base: 'space.05', sm: '0' }} pt={{ base: 'space.05', md: '0' }} > - - - - {name} - + + + + + {name} + - - - - - + + + + + + @@ -66,6 +74,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 +83,9 @@ export function AccountCard({ }} ref={scaleTextRef} > - {balance} + + {balance} + diff --git a/src/app/ui/components/privacy/private-text.layout.tsx b/src/app/ui/components/privacy/private-text.layout.tsx new file mode 100644 index 00000000000..cbe3cb4593b --- /dev/null +++ b/src/app/ui/components/privacy/private-text.layout.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { type HTMLStyledProps, styled } from 'leather-styles/jsx'; + +import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; + +interface PrivateTextLayoutProps extends HTMLStyledProps<'span'> { + children: React.ReactNode; + isPrivate?: boolean; + onShowValue?(): void; +} + +export function PrivateTextLayout({ + isPrivate, + onShowValue, + children, + ...rest +}: PrivateTextLayoutProps) { + const canShowValue = isPrivate && onShowValue; + + return ( + + + {isPrivate ? '***' : children} + + + ); +} diff --git a/tests/selectors/settings.selectors.ts b/tests/selectors/settings.selectors.ts index 2af823d329b..e9e5852ed55 100644 --- a/tests/selectors/settings.selectors.ts +++ b/tests/selectors/settings.selectors.ts @@ -25,4 +25,5 @@ export enum SettingsSelectors { SwitchAccountMenuItem = 'switch-account-menu-item', SwitchAccountItemIndex = 'switch-account-item-[index]', OpenWalletInNewTab = 'open-wallet-in-new-tab', + TogglePrivacy = 'toggle-privacy', } diff --git a/tests/selectors/shared-component.selectors.ts b/tests/selectors/shared-component.selectors.ts index 1f98f53e98f..52543028e21 100644 --- a/tests/selectors/shared-component.selectors.ts +++ b/tests/selectors/shared-component.selectors.ts @@ -3,6 +3,9 @@ export enum SharedComponentsSelectors { AddressDisplayer = 'address-displayer', AddressDisplayerPart = 'address-displayer-part', + // AccountCard + AccountCardBalanceText = 'account-card-balance-text', + // InfoCard InfoCardAssetValue = 'info-card-asset-value', InfoCardRowValue = 'info-card-row-value', diff --git a/tests/specs/settings/settings.spec.ts b/tests/specs/settings/settings.spec.ts index 5afe444075b..f953b840a0c 100644 --- a/tests/specs/settings/settings.spec.ts +++ b/tests/specs/settings/settings.spec.ts @@ -1,6 +1,7 @@ import { TEST_PASSWORD } from '@tests/mocks/constants'; import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; +import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors'; import { test } from '../../fixtures/fixtures'; @@ -70,4 +71,26 @@ test.describe('Settings menu', () => { const networkListItems = await page.getByTestId(SettingsSelectors.NetworkListItem).all(); test.expect(networkListItems).toHaveLength(5); }); + + test('that menu item can toggle privacy', async ({ page, homePage }) => { + const visibleBalanceText = await homePage.page + .getByTestId(SharedComponentsSelectors.AccountCardBalanceText) + .textContent(); + test.expect(visibleBalanceText).toBeTruthy(); + + await homePage.clickSettingsButton(); + await page.getByTestId(SettingsSelectors.TogglePrivacy).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.clickSettingsButton(); + await page.getByTestId(SettingsSelectors.TogglePrivacy).click(); + + await test + .expect(homePage.page.getByTestId(SharedComponentsSelectors.AccountCardBalanceText)) + .toHaveText(visibleBalanceText!); + }); });