From f4f8415fd5fa464eac0d010b1fd5389995d1494c Mon Sep 17 00:00:00 2001 From: Adriano Cola Date: Mon, 2 Sep 2024 08:57:10 -0300 Subject: [PATCH] feat: add option to hide balance, closes leather-io#5096 --- src/app/common/hide-balance-provider.tsx | 20 +++++ .../components/balance/hideable-balance.tsx | 13 ++++ .../crypto-asset-item.layout.tsx | 15 ++-- .../transaction-item.layout.tsx | 10 ++- src/app/pages/home/home.tsx | 75 +++++++++++-------- 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 +++++++++ 12 files changed, 218 insertions(+), 63 deletions(-) create mode 100644 src/app/common/hide-balance-provider.tsx create mode 100644 src/app/components/balance/hideable-balance.tsx create mode 100644 tests/specs/home/home.spec.ts diff --git a/src/app/common/hide-balance-provider.tsx b/src/app/common/hide-balance-provider.tsx new file mode 100644 index 00000000000..af8898eea31 --- /dev/null +++ b/src/app/common/hide-balance-provider.tsx @@ -0,0 +1,20 @@ +import { createContext, useContext } from 'react'; + +interface HideBalanceContextProps { + hideBalance?: boolean; +} +const hideBalanceProvider = createContext(null); + +const { Provider } = hideBalanceProvider; + +export function useHideBalanceContext() { + return !!useContext(hideBalanceProvider)?.hideBalance; +} + +interface HideBalanceProviderProps { + children: React.ReactNode; + hideBalance?: boolean; +} +export function HideBalanceProvider({ children, hideBalance }: HideBalanceProviderProps) { + return {children}; +} diff --git a/src/app/components/balance/hideable-balance.tsx b/src/app/components/balance/hideable-balance.tsx new file mode 100644 index 00000000000..988d0320b67 --- /dev/null +++ b/src/app/components/balance/hideable-balance.tsx @@ -0,0 +1,13 @@ +import { type HTMLStyledProps, styled } from 'leather-styles/jsx'; + +import { useHideBalanceContext } from '@app/common/hide-balance-provider'; + +interface HideableBalanceProps extends HTMLStyledProps<'span'> { + children: React.ReactNode; + forceHidden?: boolean; +} +export function HideableBalance({ forceHidden, children, ...rest }: HideableBalanceProps) { + const hideBalance = useHideBalanceContext(); + + 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..6dc3ad7868e 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 { useHideBalanceContext } from '@app/common/hide-balance-provider'; +import { HideableBalance } from '@app/components/balance/hideable-balance'; 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 = useHideBalanceContext(); 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/transaction-item/transaction-item.layout.tsx b/src/app/components/transaction-item/transaction-item.layout.tsx index 326d1352100..280bfe5698c 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; @@ -42,7 +44,11 @@ export function TransactionItemLayout({ } titleRight={ - rightElement ? rightElement : {txValue} + rightElement ? ( + rightElement + ) : ( + {txValue} + ) } /> diff --git a/src/app/pages/home/home.tsx b/src/app/pages/home/home.tsx index 787cd57b9cb..6f94bed7e8b 100644 --- a/src/app/pages/home/home.tsx +++ b/src/app/pages/home/home.tsx @@ -6,6 +6,7 @@ import { Box, Stack } from 'leather-styles/jsx'; import { RouteUrls } from '@shared/route-urls'; import { SwitchAccountOutletContext } from '@shared/switch-account'; +import { HideBalanceProvider } from '@app/common/hide-balance-provider'; import { useAccountDisplayName } from '@app/common/hooks/account/use-account-names'; import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state'; import { useTotalBalance } from '@app/common/hooks/balance/use-total-balance'; @@ -18,6 +19,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 +33,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 || '', @@ -47,39 +52,43 @@ export function Home() { }); return ( - - - setIsShowingSwitchAccount(!isShowingSwitchAccount)} - isFetchingBnsName={isFetchingBnsName} - isLoadingBalance={isLoading} - isLoadingAdditionalData={isLoadingAdditionalData} - > - - - - - - - } /> - }> + + + + setIsShowingSwitchAccount(!isShowingSwitchAccount)} + toggleHideBlance={toggleHideBalance} + isFetchingBnsName={isFetchingBnsName} + isLoadingBalance={isLoading} + isLoadingAdditionalData={isLoadingAdditionalData} + hideBalance={hideBalance} + > + + + + + + + } /> + }> + {homePageModalRoutes} + {homePageModalRoutes} - - {homePageModalRoutes} - - - + + + + ); } 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..ca10c26ab11 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, + Eye1Icon, + Eye1ClosedIcon, + 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!); + }); +});