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