From 49e7d003b71f7ca0bd70d845749a34b9666edea0 Mon Sep 17 00:00:00 2001 From: Adriano Cola Date: Mon, 2 Sep 2024 09:04:19 -0300 Subject: [PATCH] feat: add option to hide balance, closes leather-io#5096 --- src/app/components/account-total-balance.tsx | 5 +- .../components/balance/balance-private.tsx | 25 ++++++++ src/app/components/balance/btc-balance.tsx | 9 ++- src/app/components/balance/stx-balance.tsx | 8 ++- .../bitcoin-transaction-item.tsx | 3 + .../crypto-asset-item.layout.tsx | 16 +++-- .../components/layout/card/card.stories.tsx | 17 ++++++ .../card/components/available-balance.tsx | 5 +- .../stacks-transaction-item.tsx | 3 + .../transaction-item.layout.tsx | 14 +++-- src/app/features/asset-list/asset-list.tsx | 25 ++++++-- .../brc20-token-asset-list.tsx | 4 +- .../btc-crypto-asset-item.tsx | 3 + .../runes-asset-list/runes-asset-list.tsx | 4 +- .../src20-token-asset-list.tsx | 4 +- .../sip10-token-asset-item.tsx | 3 + .../sip10-token-asset-list-unsupported.tsx | 3 + .../sip10-token-asset-list.tsx | 3 + .../stx-crypto-asset-item.stories.tsx | 11 ++++ .../stx-crypto-asset-item.tsx | 11 +++- .../stx20-token-asset-list.tsx | 4 +- .../bitcoin-choose-fee/bitcoin-choose-fee.tsx | 4 +- src/app/features/settings/settings.tsx | 21 +++++++ .../choose-asset-to-fund.tsx | 4 ++ src/app/pages/home/home.tsx | 6 ++ .../form/btc/btc-send-form.tsx | 7 ++- .../form/stacks/stacks-common-send-form.tsx | 7 ++- src/app/store/settings/settings.actions.ts | 5 ++ src/app/store/settings/settings.selectors.ts | 6 ++ src/app/store/settings/settings.slice.ts | 4 ++ .../account/account.card.stories.tsx | 32 ++++++++++ .../ui/components/account/account.card.tsx | 60 ++++++++++++------- .../privacy/private-text.layout.tsx | 38 ++++++++++++ tests/selectors/settings.selectors.ts | 1 + tests/selectors/shared-component.selectors.ts | 3 + tests/specs/settings/settings.spec.ts | 23 +++++++ 36 files changed, 351 insertions(+), 50 deletions(-) create mode 100644 src/app/components/balance/balance-private.tsx create mode 100644 src/app/ui/components/privacy/private-text.layout.tsx diff --git a/src/app/components/account-total-balance.tsx b/src/app/components/account-total-balance.tsx index 86e2123c93d..4b4655feed9 100644 --- a/src/app/components/account-total-balance.tsx +++ b/src/app/components/account-total-balance.tsx @@ -5,6 +5,8 @@ import { styled } from 'leather-styles/jsx'; import { SkeletonLoader, shimmerStyles } from '@leather.io/ui'; import { useTotalBalance } from '@app/common/hooks/balance/use-total-balance'; +import { useIsPrivateMode } from '@app/store/settings/settings.selectors'; +import { PrivateTextLayout } from '@app/ui/components/privacy/private-text.layout'; interface AccountTotalBalanceProps { btcAddress: string; @@ -12,6 +14,7 @@ interface AccountTotalBalanceProps { } export const AccountTotalBalance = memo(({ btcAddress, stxAddress }: AccountTotalBalanceProps) => { + const isPrivate = useIsPrivateMode(); const { totalUsdBalance, isFetching, isLoading, isLoadingAdditionalData } = useTotalBalance({ btcAddress, stxAddress, @@ -26,7 +29,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/balance-private.tsx b/src/app/components/balance/balance-private.tsx new file mode 100644 index 00000000000..fed7179cf77 --- /dev/null +++ b/src/app/components/balance/balance-private.tsx @@ -0,0 +1,25 @@ +import { type HTMLStyledProps } from 'leather-styles/jsx'; + +import { useTogglePrivateMode } from '@app/store/settings/settings.actions'; +import { useIsPrivateMode } from '@app/store/settings/settings.selectors'; +import { PrivateTextLayout } from '@app/ui/components/privacy/private-text.layout'; + +interface BalancePrivateProps extends HTMLStyledProps<'span'> { + children: React.ReactNode; + canClickToShow?: boolean; +} + +export function BalancePrivate({ children, canClickToShow, ...rest }: BalancePrivateProps) { + const isPrivateMode = useIsPrivateMode(); + const togglePrivateMode = useTogglePrivateMode(); + + return ( + + {children} + + ); +} diff --git a/src/app/components/balance/btc-balance.tsx b/src/app/components/balance/btc-balance.tsx index 3e90079ae69..33c843b2e54 100644 --- a/src/app/components/balance/btc-balance.tsx +++ b/src/app/components/balance/btc-balance.tsx @@ -3,13 +3,20 @@ import { formatMoney } from '@leather.io/utils'; import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader'; import { BtcBalanceLoader } from '../loaders/btc-balance-loader'; +import { BalancePrivate } from './balance-private'; 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..d056f42e33f 100644 --- a/src/app/components/balance/stx-balance.tsx +++ b/src/app/components/balance/stx-balance.tsx @@ -5,6 +5,8 @@ import { Caption } from '@leather.io/ui'; import { stacksValue } from '@app/common/stacks-utils'; +import { BalancePrivate } from './balance-private'; + interface StxBalanceProps { address: string; } @@ -21,5 +23,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..3f60f62f208 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/ui/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/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..a2f0b5d673e 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/ui/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..896f83eb9c8 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'; @@ -60,6 +62,7 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro isLoading={isLoading} onSelectAsset={onSelectAsset} isLoadingAdditionalData={isLoadingAdditionalData} + isPrivate={isPrivate} /> )} @@ -85,6 +88,7 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro )} @@ -96,6 +100,7 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro {(isLoading, tokens) => ( @@ -103,7 +108,7 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro {isReadOnly && ( - {tokens => } + {tokens => } )} @@ -117,16 +122,22 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro <> {isReadOnly && ( - {tokens => } + {tokens => ( + + )} )} {isReadOnly && ( <> - {tokens => } + {tokens => } - {runes => } + {runes => } )} @@ -141,7 +152,11 @@ export function AssetList({ onSelectAsset, variant = 'read-only' }: AssetListPro {account => ( {(isLoading, tokens) => ( - + )} )} 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..acc3f354271 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 @@ -19,6 +19,7 @@ interface Brc20TokenAssetDetails { interface Brc20TokenAssetListProps { tokens: Brc20TokenAssetDetails[]; variant?: AssetListVariant; + isPrivate?: boolean; } function getBrc20TokenFiatBalance(token: Brc20TokenAssetDetails) { @@ -28,7 +29,7 @@ function getBrc20TokenFiatBalance(token: Brc20TokenAssetDetails) { }); } -export function Brc20TokenAssetList({ tokens }: Brc20TokenAssetListProps) { +export function Brc20TokenAssetList({ tokens, isPrivate }: Brc20TokenAssetListProps) { const { isLoading } = useCurrentBtcCryptoAssetBalanceNativeSegwit(); if (!tokens.length) return null; @@ -40,6 +41,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..b204df8480d 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 @@ -9,6 +9,7 @@ interface BtcCryptoAssetItemProps { balance: BtcCryptoAssetBalance; isLoading: boolean; isLoadingAdditionalData?: boolean; + isPrivate?: boolean; onSelectAsset?(symbol: string): void; } export function BtcCryptoAssetItem({ @@ -16,6 +17,7 @@ export function BtcCryptoAssetItem({ isLoading, onSelectAsset, isLoadingAdditionalData, + isPrivate, }: BtcCryptoAssetItemProps) { const marketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); const fiatAvailableBalance = i18nFormatCurrency( @@ -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..78a6d3a9d95 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 @@ -13,9 +13,10 @@ interface RuneTokenAssetDetails { interface RunesAssetListProps { runes: RuneTokenAssetDetails[]; + isPrivate?: boolean; } -export function RunesAssetList({ runes }: RunesAssetListProps) { +export function RunesAssetList({ runes, isPrivate }: RunesAssetListProps) { 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..15ba3566e94 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 @@ -5,8 +5,9 @@ import type { Src20TokenAssetDetails } from '@app/components/loaders/src20-token interface Src20TokenAssetListProps { tokens: Src20TokenAssetDetails[]; + isPrivate?: boolean; } -export function Src20TokenAssetList({ tokens }: Src20TokenAssetListProps) { +export function Src20TokenAssetList({ tokens, isPrivate }: Src20TokenAssetListProps) { 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..7b83394dd1a 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 @@ -9,6 +9,7 @@ interface Sip10TokenAssetItemProps { balance: CryptoAssetBalance; info: Sip10CryptoAssetInfo; isLoading: boolean; + isPrivate?: boolean; marketData: MarketData; onSelectAsset?(symbol: string, contractId?: string): void; } @@ -16,6 +17,7 @@ export function Sip10TokenAssetItem({ balance, info, isLoading, + isPrivate, marketData, onSelectAsset, }: Sip10TokenAssetItemProps) { @@ -41,6 +43,7 @@ export function Sip10TokenAssetItem({ } isLoading={isLoading} + isPrivate={isPrivate} onSelectAsset={onSelectAsset} titleLeft={name} /> diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx index 28c8b0733a1..4fe8cd77b2c 100644 --- a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx @@ -11,10 +11,12 @@ const accordionValue = 'accordion-unsupported-token-asset-list'; interface Sip10TokenAssetListUnsupportedProps { isLoading: boolean; + isPrivate?: boolean; tokens: Sip10TokenAssetDetails[]; } export function Sip10TokenAssetListUnsupported({ isLoading, + isPrivate, tokens, }: Sip10TokenAssetListUnsupportedProps) { const [isOpen, setIsOpen] = useState(false); @@ -40,6 +42,7 @@ export function Sip10TokenAssetListUnsupported({ key={token.info.name} info={token.info} isLoading={isLoading} + isPrivate={isPrivate} marketData={priceAsMarketData( token.info.contractId, token.balance.availableBalance.symbol diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx index 81a3ebcf260..cc863559baf 100644 --- a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx @@ -8,11 +8,13 @@ import { Sip10TokenAssetItem } from './sip10-token-asset-item'; interface Sip10TokenAssetListProps { isLoading: boolean; + isPrivate?: boolean; tokens: Sip10TokenAssetDetails[]; onSelectAsset?(symbol: string, contractId?: string): void; } export function Sip10TokenAssetList({ isLoading, + isPrivate, tokens, onSelectAsset, }: Sip10TokenAssetListProps) { @@ -28,6 +30,7 @@ export function Sip10TokenAssetList({ key={token.info.name + token.info.contractId} info={token.info} isLoading={isLoading} + isPrivate={isPrivate} marketData={priceAsMarketData( getPrincipalFromContractId(token.info.contractId), token.balance.availableBalance.symbol 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..0c1a54e6f53 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 @@ -59,3 +59,14 @@ export const StxCryptoAssetItemWithLockedBalance: Story = { isLoading: false, }, }; + +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..64043ece248 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 @@ -10,8 +10,9 @@ interface Stx20TokenAssetDetails { interface Stx20TokenAssetListProps { tokens: Stx20TokenAssetDetails[]; + isPrivate?: boolean; } -export function Stx20TokenAssetList({ tokens }: Stx20TokenAssetListProps) { +export function Stx20TokenAssetList({ tokens, isPrivate }: Stx20TokenAssetListProps) { 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..062f5a1a10c 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] @@ -44,6 +46,7 @@ export function ChooseCryptoAssetToFund() { navigateToFund('BTC')} /> )} @@ -58,6 +61,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 d0baf285dcc..2d2e82a95f6 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: CryptoCurrencies = '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..30c8fd059b7 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,14 @@ 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..ea219e12367 --- /dev/null +++ b/src/app/ui/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/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!); + }); });