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..c2fea2cf4c2 100644
--- a/src/app/components/balance/btc-balance.tsx
+++ b/src/app/components/balance/btc-balance.tsx
@@ -1,15 +1,20 @@
import { Caption } from '@leather.io/ui';
import { formatMoney } from '@leather.io/utils';
-import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader';
-import { BtcBalanceLoader } from '../loaders/btc-balance-loader';
+import { BitcoinNativeSegwitAccountLoader } from '@app/components/loaders/bitcoin-account-loader';
+import { BtcBalanceLoader } from '@app/components/loaders/btc-balance-loader';
+import { PrivateText } from '@app/components/privacy/private-text';
export function BtcBalance() {
return (
{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/bitcoin-transaction-item/bitcoin-transaction-item.tsx b/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx
index 0348253a62a..c35dc4421ce 100644
--- a/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx
+++ b/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx
@@ -21,6 +21,7 @@ import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { IncreaseFeeButton } from '@app/components/stacks-transaction-item/increase-fee-button';
import { TransactionTitle } from '@app/components/transaction/transaction-title';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';
import { BitcoinTransactionIcon } from './bitcoin-transaction-icon';
@@ -33,6 +34,7 @@ interface BitcoinTransactionItemProps {
export function BitcoinTransactionItem({ transaction }: BitcoinTransactionItemProps) {
const { pathname } = useLocation();
const navigate = useNavigate();
+ const isPrivate = useIsPrivateMode();
const { data: inscriptionData } = useInscriptionByOutput(transaction);
@@ -98,6 +100,7 @@ export function BitcoinTransactionItem({ transaction }: BitcoinTransactionItemPr
txStatus={}
txTitle={}
txValue={value}
+ isPrivate={isPrivate}
/>
);
}
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..ecc31887c68 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,7 @@ import {
} from '@leather.io/ui';
import { spamFilter } from '@leather.io/utils';
+import { PrivateTextLayout } from '@app/components/privacy/private-text.layout';
import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip';
import { parseCryptoAssetBalance } from './crypto-asset-item.layout.utils';
@@ -25,6 +26,7 @@ interface CryptoAssetItemLayoutProps {
icon: React.ReactNode;
isLoading?: boolean;
isLoadingAdditionalData?: boolean;
+ isPrivate?: boolean;
onSelectAsset?(symbol: string, contractId?: string): void;
titleLeft: string;
titleRightBulletInfo?: React.ReactNode;
@@ -39,6 +41,7 @@ export function CryptoAssetItemLayout({
icon,
isLoading = false,
isLoadingAdditionalData = false,
+ isPrivate = false,
onSelectAsset,
titleLeft,
titleRightBulletInfo,
@@ -50,17 +53,18 @@ export function CryptoAssetItemLayout({
-
{formattedBalance.value} {balanceSuffix}
-
+
{titleRightBulletInfo}
@@ -76,7 +80,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/card.stories.tsx b/src/app/components/layout/card/card.stories.tsx
index 3806983bfd2..0b7b182832c 100644
--- a/src/app/components/layout/card/card.stories.tsx
+++ b/src/app/components/layout/card/card.stories.tsx
@@ -79,6 +79,23 @@ export function CardWithBalanceFooter() {
);
}
+export function CardWithPrivateBalanceFooter() {
+ return (
+
+
+
+
+ }
+ >
+ Card content
+
+ );
+}
+
export function CardWithBigHeader() {
return (
- {balance}
+ {balance}
);
diff --git a/src/app/components/privacy/private-text.layout.tsx b/src/app/components/privacy/private-text.layout.tsx
new file mode 100644
index 00000000000..ea219e12367
--- /dev/null
+++ b/src/app/components/privacy/private-text.layout.tsx
@@ -0,0 +1,38 @@
+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,
+ style = {},
+ ...rest
+}: PrivateTextLayoutProps) {
+ const canShowValue = isPrivate && onShowValue;
+
+ return (
+
+
+ {isPrivate ? '***' : children}
+
+
+ );
+}
diff --git a/src/app/components/privacy/private-text.tsx b/src/app/components/privacy/private-text.tsx
new file mode 100644
index 00000000000..e02a549fdfe
--- /dev/null
+++ b/src/app/components/privacy/private-text.tsx
@@ -0,0 +1,25 @@
+import { type HTMLStyledProps } from 'leather-styles/jsx';
+
+import { PrivateTextLayout } from '@app/components/privacy/private-text.layout';
+import { useTogglePrivateMode } from '@app/store/settings/settings.actions';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
+
+interface PrivateTextProps extends HTMLStyledProps<'span'> {
+ children: React.ReactNode;
+ canClickToShow?: boolean;
+}
+
+export function PrivateText({ children, canClickToShow, ...rest }: PrivateTextProps) {
+ const isPrivateMode = useIsPrivateMode();
+ const togglePrivateMode = useTogglePrivateMode();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx b/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx
index 4517b641879..194d4d55c1f 100644
--- a/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx
+++ b/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx
@@ -17,6 +17,7 @@ import { whenPageMode } from '@app/common/utils';
import { openIndexPageInNewTab } from '@app/common/utils/open-in-new-tab';
import { TransactionTitle } from '@app/components/transaction/transaction-title';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout';
import { IncreaseFeeButton } from './increase-fee-button';
@@ -41,6 +42,7 @@ export function StacksTransactionItem({
}: StacksTransactionItemProps) {
const { handleOpenStacksTxLink } = useStacksExplorerLink();
const currentAccount = useCurrentStacksAccount();
+ const isPrivate = useIsPrivateMode();
const { pathname } = useLocation();
const navigate = useNavigate();
@@ -96,6 +98,7 @@ export function StacksTransactionItem({
txStatus={txStatus}
txTitle={}
txValue={txValue}
+ isPrivate={isPrivate}
/>
);
}
diff --git a/src/app/components/transaction-item/transaction-item.layout.tsx b/src/app/components/transaction-item/transaction-item.layout.tsx
index 1bff627b015..9eb7b1a741b 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 { PrivateTextLayout } from '@app/components/privacy/private-text.layout';
+
interface TransactionItemLayoutProps {
openTxLink(): void;
rightElement?: ReactNode;
@@ -13,13 +15,14 @@ interface TransactionItemLayoutProps {
txIcon?: ReactNode;
txStatus?: ReactNode;
children?: ReactNode;
+ isPrivate?: boolean;
}
-function TxValue({ txValue }: { txValue: ReactNode }) {
+function TxValue({ txValue, isPrivate }: { txValue: ReactNode; isPrivate?: boolean }) {
return (
-
+
{txValue}
-
+
);
}
@@ -31,6 +34,7 @@ export function TransactionItemLayout({
txStatus,
txTitle,
txValue,
+ isPrivate,
}: TransactionItemLayoutProps) {
return (
@@ -49,7 +53,7 @@ export function TransactionItemLayout({
{txStatus && txStatus}
}
- titleRight={}
+ titleRight={}
captionRight={rightElement}
/>
diff --git a/src/app/features/asset-list/asset-list.tsx b/src/app/features/asset-list/asset-list.tsx
index 42a201164c9..7c74bb1cc38 100644
--- a/src/app/features/asset-list/asset-list.tsx
+++ b/src/app/features/asset-list/asset-list.tsx
@@ -21,6 +21,7 @@ import { Stx20TokenAssetList } from '@app/features/asset-list/stacks/stx20-token
import { StxCryptoAssetItem } from '@app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useHasLedgerKeys } from '@app/store/ledger/ledger.selectors';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { ConnectLedgerAssetItemFallback } from './_components/connect-ledger-asset-item-fallback';
import { BtcCryptoAssetItem } from './bitcoin/btc-crypto-asset-item/btc-crypto-asset-item';
@@ -36,6 +37,7 @@ interface AssetListProps {
export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListProps) {
const currentAccount = useCurrentStacksAccount();
const isLedger = useHasLedgerKeys();
+ const isPrivate = useIsPrivateMode();
const isReadOnly = variant === 'read-only';
@@ -85,6 +87,7 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro
)}
diff --git a/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx b/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx
index bec2a7ad5c8..231e8593634 100644
--- a/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx
+++ b/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx
@@ -8,6 +8,7 @@ import { convertAssetBalanceToFiat } from '@app/common/asset-utils';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
import type { AssetListVariant } from '@app/features/asset-list/asset-list';
import { useCurrentBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
interface Brc20TokenAssetDetails {
balance: CryptoAssetBalance;
@@ -30,6 +31,7 @@ function getBrc20TokenFiatBalance(token: Brc20TokenAssetDetails) {
export function Brc20TokenAssetList({ tokens }: Brc20TokenAssetListProps) {
const { isLoading } = useCurrentBtcCryptoAssetBalanceNativeSegwit();
+ const isPrivate = useIsPrivateMode();
if (!tokens.length) return null;
return (
@@ -40,6 +42,7 @@ export function Brc20TokenAssetList({ tokens }: Brc20TokenAssetListProps) {
captionLeft={token.info.name.toUpperCase()}
icon={}
isLoading={isLoading}
+ isPrivate={isPrivate}
key={token.info.symbol}
titleLeft={token.info.symbol}
fiatBalance={getBrc20TokenFiatBalance(token)}
diff --git a/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx b/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx
index 033e1219a55..d66d95c3cbd 100644
--- a/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx
+++ b/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx
@@ -4,6 +4,7 @@ import { BtcAvatarIcon } from '@leather.io/ui';
import { baseCurrencyAmountInQuote, i18nFormatCurrency } from '@leather.io/utils';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
interface BtcCryptoAssetItemProps {
balance: BtcCryptoAssetBalance;
@@ -17,6 +18,7 @@ export function BtcCryptoAssetItem({
onSelectAsset,
isLoadingAdditionalData,
}: BtcCryptoAssetItemProps) {
+ const isPrivate = useIsPrivateMode();
const marketData = useCryptoCurrencyMarketDataMeanAverage('BTC');
const fiatAvailableBalance = i18nFormatCurrency(
baseCurrencyAmountInQuote(balance.availableBalance, marketData)
@@ -30,6 +32,7 @@ export function BtcCryptoAssetItem({
icon={}
isLoading={isLoading}
isLoadingAdditionalData={isLoadingAdditionalData}
+ isPrivate={isPrivate}
onSelectAsset={onSelectAsset}
titleLeft="Bitcoin"
/>
diff --git a/src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list.tsx b/src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list.tsx
index 928296a2969..6570372f840 100644
--- a/src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list.tsx
+++ b/src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list.tsx
@@ -4,6 +4,7 @@ import { convertAmountToBaseUnit, createMoneyFromDecimal } from '@leather.io/uti
import { convertAssetBalanceToFiat } from '@app/common/asset-utils';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
interface RuneTokenAssetDetails {
balance: CryptoAssetBalance;
@@ -16,6 +17,8 @@ interface RunesAssetListProps {
}
export function RunesAssetList({ runes }: RunesAssetListProps) {
+ const isPrivate = useIsPrivateMode();
+
return runes.map((rune, i) => (
));
}
diff --git a/src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx b/src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx
index 5152644d27e..48c249ca3a8 100644
--- a/src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx
+++ b/src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx
@@ -2,11 +2,14 @@ import { Src20AvatarIcon } from '@leather.io/ui';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
import type { Src20TokenAssetDetails } from '@app/components/loaders/src20-tokens-loader';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
interface Src20TokenAssetListProps {
tokens: Src20TokenAssetDetails[];
}
export function Src20TokenAssetList({ tokens }: Src20TokenAssetListProps) {
+ const isPrivate = useIsPrivateMode();
+
return tokens.map((token, i) => (
}
titleLeft={token.info.symbol.toUpperCase()}
+ isPrivate={isPrivate}
/>
));
}
diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx
index c78b77f9517..2c81088cecd 100644
--- a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx
+++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx
@@ -4,6 +4,7 @@ import { convertAssetBalanceToFiat } from '@app/common/asset-utils';
import { getSafeImageCanonicalUri } from '@app/common/stacks-utils';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
import { StacksAssetAvatar } from '@app/components/stacks-asset-avatar';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
interface Sip10TokenAssetItemProps {
balance: CryptoAssetBalance;
@@ -19,6 +20,7 @@ export function Sip10TokenAssetItem({
marketData,
onSelectAsset,
}: Sip10TokenAssetItemProps) {
+ const isPrivate = useIsPrivateMode();
const fiatBalance = convertAssetBalanceToFiat({
balance: balance.availableBalance,
marketData,
@@ -41,6 +43,7 @@ export function Sip10TokenAssetItem({
}
isLoading={isLoading}
+ isPrivate={isPrivate}
onSelectAsset={onSelectAsset}
titleLeft={name}
/>
diff --git a/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx
index 5db83074eea..14a33250371 100644
--- a/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx
+++ b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx
@@ -57,5 +57,17 @@ export const StxCryptoAssetItemWithLockedBalance: Story = {
lockedBalance: { amount: new BigNumber(1000000000), decimals: 6, symbol },
},
isLoading: false,
+ isPrivate: true,
+ },
+};
+
+export const StxCryptoAssetItemWithPrivateBalance: Story = {
+ args: {
+ balance: {
+ ...stxCryptoAssetBalance,
+ lockedBalance: { amount: new BigNumber(1000000000), decimals: 6, symbol },
+ },
+ isPrivate: true,
+ isLoading: false,
},
};
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..f4f7a745c02 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
@@ -14,13 +14,19 @@ import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-
interface StxCryptoAssetItemProps {
balance: StxCryptoAssetBalance;
isLoading: boolean;
+ isPrivate?: boolean;
onSelectAsset?(symbol: string): void;
}
-export function StxCryptoAssetItem({ balance, isLoading, onSelectAsset }: StxCryptoAssetItemProps) {
+export function StxCryptoAssetItem({
+ balance,
+ isLoading,
+ isPrivate,
+ onSelectAsset,
+}: StxCryptoAssetItemProps) {
const marketData = useCryptoCurrencyMarketDataMeanAverage('STX');
const { availableBalance, lockedBalance } = balance;
- const showLockedBalance = lockedBalance.amount.isGreaterThan(0);
+ const showLockedBalance = lockedBalance.amount.isGreaterThan(0) && !isPrivate;
const fiatLockedBalance = i18nFormatCurrency(
baseCurrencyAmountInQuote(lockedBalance, marketData)
@@ -41,6 +47,7 @@ export function StxCryptoAssetItem({ balance, isLoading, onSelectAsset }: StxCry
fiatBalance={fiatAvailableBalance}
icon={}
isLoading={isLoading}
+ isPrivate={isPrivate}
onSelectAsset={onSelectAsset}
titleLeft="Stacks"
titleRightBulletInfo={showLockedBalance && titleRightBulletInfo}
diff --git a/src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx b/src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx
index d3e6ddc6af1..1f3d755a197 100644
--- a/src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx
+++ b/src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx
@@ -1,6 +1,7 @@
import type { CryptoAssetBalance, Stx20CryptoAssetInfo } from '@leather.io/models';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { Stx20AvatarIcon } from '@app/ui/components/avatar/stx20-avatar-icon';
interface Stx20TokenAssetDetails {
@@ -12,6 +13,8 @@ interface Stx20TokenAssetListProps {
tokens: Stx20TokenAssetDetails[];
}
export function Stx20TokenAssetList({ tokens }: Stx20TokenAssetListProps) {
+ const isPrivate = useIsPrivateMode();
+
return tokens.map((token, i) => (
}
key={`${token.info.symbol}${i}`}
titleLeft={token.info.symbol}
+ isPrivate={isPrivate}
/>
));
}
diff --git a/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx b/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx
index f0ab3162df9..e0378057dd2 100644
--- a/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx
+++ b/src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx
@@ -13,6 +13,7 @@ import { OnChooseFeeArgs } from '@app/components/bitcoin-fees-list/bitcoin-fees-
import { AvailableBalance, Card } from '@app/components/layout';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { useCurrentBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { ChooseFeeAmount } from './components/choose-fee-amount';
import { ChooseFeeSubtitle } from './components/choose-fee-subtitle';
@@ -51,6 +52,7 @@ export function BitcoinChooseFee({
const { balance } = useCurrentBtcCryptoAssetBalanceNativeSegwit();
const hasAmount = amount.amount.isGreaterThan(0);
const [customFeeInitialValue, setCustomFeeInitialValue] = useState(recommendedFeeRate);
+ const isPrivate = useIsPrivateMode();
if (isLoading) {
return (
@@ -65,7 +67,7 @@ export function BitcoinChooseFee({
border="unset"
footer={
-
+
}
{...rest}
diff --git a/src/app/features/settings/settings.tsx b/src/app/features/settings/settings.tsx
index b35ff5bc822..b7cf75072b3 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 { useTogglePrivateMode } from '@app/store/settings/settings.actions';
+import { useIsPrivateMode } 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 isPrivateMode = useIsPrivateMode();
+ const togglePrivateMode = useTogglePrivateMode();
+
const location = useLocation();
const { isPressed: showAdvancedMenuOptions } = useModifierKey('alt', 120);
@@ -189,6 +196,20 @@ export function Settings({
+
+ {
+ void analytics.track('click_toggle_privacy');
+ togglePrivateMode();
+ }}
+ >
+ : }>
+
+ Toggle privacy
+
+
+
diff --git a/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx b/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx
index de5e4b40e93..466293b1d02 100644
--- a/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx
+++ b/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx
@@ -13,9 +13,11 @@ import { StxAssetItemBalanceLoader } from '@app/components/loaders/stx-balance-l
import { BtcCryptoAssetItem } from '@app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item';
import { StxCryptoAssetItem } from '@app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item';
import { PageHeader } from '@app/features/container/headers/page.header';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
export function ChooseCryptoAssetToFund() {
const navigate = useNavigate();
+ const isPrivate = useIsPrivateMode();
const navigateToFund = useCallback(
(symbol: string) => navigate(RouteUrls.Fund.replace(':currency', symbol)),
[navigate]
@@ -58,6 +60,7 @@ export function ChooseCryptoAssetToFund() {
navigateToFund('STX')}
/>
)}
diff --git a/src/app/pages/home/home.tsx b/src/app/pages/home/home.tsx
index 787cd57b9cb..1364c6af12a 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 { useTogglePrivateMode } from '@app/store/settings/settings.actions';
+import { useIsPrivateMode } 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 isPrivateMode = useIsPrivateMode();
+ const togglePrivateMode = useTogglePrivateMode();
const { data: name = '', isFetching: isFetchingBnsName } = useAccountDisplayName({
address: account?.address || '',
@@ -66,6 +70,8 @@ export function Home() {
isFetchingBnsName={isFetchingBnsName}
isLoadingBalance={isLoading}
isLoadingAdditionalData={isLoadingAdditionalData}
+ isBalancePrivate={isPrivateMode}
+ onShowBalance={togglePrivateMode}
>
diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx
index ce21ac4e949..87d924f7f19 100644
--- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx
+++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx
@@ -11,6 +11,7 @@ import { formatMoney } from '@leather.io/utils';
import { AvailableBalance, ButtonRow, Card, Content, Page } from '@app/components/layout';
import { PageHeader } from '@app/features/container/headers/page.header';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { AmountField } from '../../components/amount-field';
import { SelectedAssetField } from '../../components/selected-asset-field';
@@ -25,6 +26,7 @@ const symbol: CryptoCurrency = 'BTC';
export function BtcSendForm() {
const routeState = useSendFormRouteState();
+ const isPrivate = useIsPrivateMode();
const marketData = useCryptoCurrencyMarketDataMeanAverage('BTC');
const {
balance,
@@ -73,7 +75,10 @@ export function BtcSendForm() {
>
Continue
-
+
}
>
diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx
index b01b74d0b46..2d487b7285c 100644
--- a/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx
+++ b/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx
@@ -18,6 +18,7 @@ import { AvailableBalance, ButtonRow, Card, Page } from '@app/components/layout'
import { NonceSetter } from '@app/components/nonce-setter';
import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values';
import { HighFeeSheet } from '@app/features/stacks-high-fee-warning/stacks-high-fee-dialog';
+import { useIsPrivateMode } from '@app/store/settings/settings.selectors';
import { MemoField } from '../../components/memo-field';
import { StacksRecipientField } from '../../family/stacks/components/stacks-recipient-field';
@@ -48,6 +49,7 @@ export function StacksCommonSendForm({
}: StacksCommonSendFormProps) {
const navigate = useNavigate();
const { onFormStateChange } = useUpdatePersistedSendFormValues();
+ const isPrivate = useIsPrivateMode();
return (
Continue
-
+
}
>
diff --git a/src/app/store/settings/settings.actions.ts b/src/app/store/settings/settings.actions.ts
index eb38a7800b9..52faa037127 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 useTogglePrivateMode() {
+ const dispatch = useDispatch();
+ return () => dispatch(settingsActions.togglePrivateMode());
+}
diff --git a/src/app/store/settings/settings.selectors.ts b/src/app/store/settings/settings.selectors.ts
index dbb64f30d64..cde2ec58676 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 selectIsPrivateMode = createSelector(selectSettings, state => state.isPrivateMode ?? false);
+
+export function useIsPrivateMode() {
+ return useSelector(selectIsPrivateMode);
+}
diff --git a/src/app/store/settings/settings.slice.ts b/src/app/store/settings/settings.slice.ts
index edce40b7e0c..e50bb1e44f6 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[];
+ isPrivateMode?: boolean;
}
const initialState: InitialState = {
@@ -26,5 +27,8 @@ export const settingsSlice = createSlice({
resetMessages(state) {
state.dismissedMessages = [];
},
+ togglePrivateMode(state) {
+ state.isPrivateMode = !state.isPrivateMode;
+ },
},
});
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..d65de1d709b 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/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,14 @@ export function AccountCard({
}}
ref={scaleTextRef}
>
- {balance}
+
+ {balance}
+
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!);
+ });
});