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!);
+ });
});