From 4397a68d3daeaadfb3e2a23a87a988ff7c7e12f9 Mon Sep 17 00:00:00 2001 From: Juligs Date: Mon, 13 Jul 2026 12:21:25 +0200 Subject: [PATCH 1/7] feat: add fiat component --- .../src/components/coin/CoinFiatValue.tsx | 43 +++++++++++++++++++ apps/core/src/components/coin/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 apps/core/src/components/coin/CoinFiatValue.tsx diff --git a/apps/core/src/components/coin/CoinFiatValue.tsx b/apps/core/src/components/coin/CoinFiatValue.tsx new file mode 100644 index 0000000000..c6626b4acc --- /dev/null +++ b/apps/core/src/components/coin/CoinFiatValue.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useIotaClientContext } from '@iota/dapp-kit'; +import { type Network } from '@iota/iota-sdk/client'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { useBalanceInUSD } from '../../hooks'; +import { formatBalanceToUSD } from '../../utils/formatBalanceToUSD'; + +export interface CoinFiatValueProps { + amount: bigint | string | number; + coinType?: string; + className?: string; + withParentheses?: boolean; +} + +/** + * Renders the fiat (USD) value of a coin amount. + * + * Renders nothing when fiat conversion is unavailable (feature-gated to mainnet) + * or when the resulting value is below one cent. + */ +export function CoinFiatValue({ + amount, + coinType = IOTA_TYPE_ARG, + className = 'text-body-md text-iota-neutral-40 dark:text-iota-neutral-60', + withParentheses = true, +}: CoinFiatValueProps): JSX.Element | null { + const { network } = useIotaClientContext(); + const value = useBalanceInUSD(coinType, amount, network as Network); + + if (value === null || value === undefined || Math.abs(value) < 0.005) { + return null; + } + + const formattedValue = formatBalanceToUSD(Math.abs(value)); + + return ( + + {withParentheses ? `(${formattedValue})` : formattedValue} + + ); +} diff --git a/apps/core/src/components/coin/index.ts b/apps/core/src/components/coin/index.ts index 6bbccfc1d7..f994c08ab9 100644 --- a/apps/core/src/components/coin/index.ts +++ b/apps/core/src/components/coin/index.ts @@ -4,3 +4,4 @@ export * from './CoinIcon'; export * from './CoinSelector'; export * from './CoinItem'; +export * from './CoinFiatValue'; From 360748262cc597dabdf06f2fc4297ad8bcf091af Mon Sep 17 00:00:00 2001 From: Juligs Date: Mon, 13 Jul 2026 12:29:30 +0200 Subject: [PATCH 2/7] chore(core): remove some comments --- apps/core/src/components/coin/CoinFiatValue.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/core/src/components/coin/CoinFiatValue.tsx b/apps/core/src/components/coin/CoinFiatValue.tsx index c6626b4acc..8217b0ab65 100644 --- a/apps/core/src/components/coin/CoinFiatValue.tsx +++ b/apps/core/src/components/coin/CoinFiatValue.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2024 IOTA Stiftung +// Copyright (c) 2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 import { useIotaClientContext } from '@iota/dapp-kit'; @@ -14,12 +14,6 @@ export interface CoinFiatValueProps { withParentheses?: boolean; } -/** - * Renders the fiat (USD) value of a coin amount. - * - * Renders nothing when fiat conversion is unavailable (feature-gated to mainnet) - * or when the resulting value is below one cent. - */ export function CoinFiatValue({ amount, coinType = IOTA_TYPE_ARG, From f216490c3deb21f7afc0428b94f2057d5d6701f2 Mon Sep 17 00:00:00 2001 From: evavirseda Date: Tue, 14 Jul 2026 09:34:26 +0200 Subject: [PATCH 3/7] chore: trigger ci From 2c489f8e95c67cfbdae31469d7cd51b3cb3b3c16 Mon Sep 17 00:00:00 2001 From: Juligs Date: Tue, 14 Jul 2026 12:58:26 +0200 Subject: [PATCH 4/7] chore: remove className prop --- apps/core/src/components/coin/CoinFiatValue.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/core/src/components/coin/CoinFiatValue.tsx b/apps/core/src/components/coin/CoinFiatValue.tsx index 8217b0ab65..512c579133 100644 --- a/apps/core/src/components/coin/CoinFiatValue.tsx +++ b/apps/core/src/components/coin/CoinFiatValue.tsx @@ -10,14 +10,12 @@ import { formatBalanceToUSD } from '../../utils/formatBalanceToUSD'; export interface CoinFiatValueProps { amount: bigint | string | number; coinType?: string; - className?: string; withParentheses?: boolean; } export function CoinFiatValue({ amount, coinType = IOTA_TYPE_ARG, - className = 'text-body-md text-iota-neutral-40 dark:text-iota-neutral-60', withParentheses = true, }: CoinFiatValueProps): JSX.Element | null { const { network } = useIotaClientContext(); @@ -30,7 +28,7 @@ export function CoinFiatValue({ const formattedValue = formatBalanceToUSD(Math.abs(value)); return ( - + {withParentheses ? `(${formattedValue})` : formattedValue} ); From e14084d5b1282f38ce179b4444ff3f318838431e Mon Sep 17 00:00:00 2001 From: Juligs Date: Mon, 20 Jul 2026 15:30:50 +0200 Subject: [PATCH 5/7] feat(wallet, wallet-dashboard): show fiat value for Iota balances --- .../src/components/coin/AmountWithFiat.tsx | 41 ++++++++++ apps/core/src/components/coin/index.ts | 1 + apps/core/src/components/stake/StakedCard.tsx | 9 +- .../src/components/stake/UnstakeBreakdown.tsx | 82 +++++++++++++++---- .../hooks/useGetStakingValidatorDetails.ts | 1 + .../molecules/input/InputWrapper.tsx | 3 +- .../app/(protected)/staking/page.tsx | 19 ++++- .../dialogs/staking/views/DetailsView.tsx | 19 ++++- .../staking/views/EnterAmountDialogLayout.tsx | 2 +- .../dialogs/staking/views/EnterAmountView.tsx | 16 +++- .../views/EnterTimelockedAmountView.tsx | 12 ++- .../dialogs/staking/views/StakedInfo.tsx | 39 ++++++--- .../staking-overview/StakingData.tsx | 35 +++++--- .../pages/home/tokens/coin-balance/index.tsx | 37 +++++---- .../DelegationDetailCard.tsx | 19 ++++- .../src/ui/app/staking/stake/StakeForm.tsx | 16 +++- .../app/staking/stake/ValidatorFormDetail.tsx | 21 ++++- .../app/staking/validators/ValidatorsCard.tsx | 19 ++++- 18 files changed, 304 insertions(+), 87 deletions(-) create mode 100644 apps/core/src/components/coin/AmountWithFiat.tsx diff --git a/apps/core/src/components/coin/AmountWithFiat.tsx b/apps/core/src/components/coin/AmountWithFiat.tsx new file mode 100644 index 0000000000..6982a7cf15 --- /dev/null +++ b/apps/core/src/components/coin/AmountWithFiat.tsx @@ -0,0 +1,41 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useIotaClientContext } from '@iota/dapp-kit'; +import { type Network } from '@iota/iota-sdk/client'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { useBalanceInUSD } from '../../hooks'; +import { CoinFiatValue } from './CoinFiatValue'; + +export interface AmountWithFiatProps { + amount: bigint | string | number; + formatted: string; + symbol?: string; + coinType?: string; +} + +export function AmountWithFiat({ + amount, + formatted, + symbol, + coinType = IOTA_TYPE_ARG, +}: AmountWithFiatProps) { + const { network } = useIotaClientContext(); + const value = useBalanceInUSD(coinType, amount, network as Network); + const hasFiatValue = value !== null && value !== undefined && Math.abs(value) >= 0.005; + + return ( + + + {formatted} + {symbol ? ` ${symbol}` : ''} + + {hasFiatValue && ( + + ~ + + + )} + + ); +} diff --git a/apps/core/src/components/coin/index.ts b/apps/core/src/components/coin/index.ts index f994c08ab9..48fd4bc17f 100644 --- a/apps/core/src/components/coin/index.ts +++ b/apps/core/src/components/coin/index.ts @@ -5,3 +5,4 @@ export * from './CoinIcon'; export * from './CoinSelector'; export * from './CoinItem'; export * from './CoinFiatValue'; +export * from './AmountWithFiat'; diff --git a/apps/core/src/components/stake/StakedCard.tsx b/apps/core/src/components/stake/StakedCard.tsx index 743c5a8693..6a166ba8f2 100644 --- a/apps/core/src/components/stake/StakedCard.tsx +++ b/apps/core/src/components/stake/StakedCard.tsx @@ -17,6 +17,7 @@ import { ExtendedDelegatedStake } from '../../utils'; import { useFormatCoin, useStakeRewardStatus, useGetInactiveValidator } from '../../hooks'; import { RewardsOff, Warning } from '@iota/apps-ui-icons'; import { useIotaClientQuery } from '@iota/dapp-kit'; +import { AmountWithFiat } from '../coin'; interface StakedCardProps { extendedStake: ExtendedDelegatedStake; @@ -68,7 +69,13 @@ export function StakedCard({ + } icon={ activeButNotInTheCommittee ? ( diff --git a/apps/core/src/components/stake/UnstakeBreakdown.tsx b/apps/core/src/components/stake/UnstakeBreakdown.tsx index 0f6329c9c0..9beccba404 100644 --- a/apps/core/src/components/stake/UnstakeBreakdown.tsx +++ b/apps/core/src/components/stake/UnstakeBreakdown.tsx @@ -5,6 +5,7 @@ import { Divider, KeyValueInfo } from '@iota/apps-ui-kit'; import { GAS_SYMBOL } from '../../constants'; import { useFormatCoin } from '../../hooks/useFormatCoin'; import type { UnstakeAmounts } from '../../utils/stake/calculateUnstakeAmounts'; +import { AmountWithFiat } from '../coin'; interface UnstakeBreakdownProps { isPartialUnstake: boolean; @@ -32,40 +33,70 @@ export function UnstakeBreakdown({ isPartialUnstake, unstakeAmounts }: UnstakeBr <> + } fullwidth /> + } fullwidth /> + } fullwidth /> + } fullwidth /> + } fullwidth /> + } fullwidth /> @@ -76,21 +107,36 @@ export function UnstakeBreakdown({ isPartialUnstake, unstakeAmounts }: UnstakeBr <> + } fullwidth /> + } fullwidth /> + } fullwidth /> diff --git a/apps/core/src/hooks/useGetStakingValidatorDetails.ts b/apps/core/src/hooks/useGetStakingValidatorDetails.ts index dce7ec33e3..001dcc5d48 100644 --- a/apps/core/src/hooks/useGetStakingValidatorDetails.ts +++ b/apps/core/src/hooks/useGetStakingValidatorDetails.ts @@ -81,6 +81,7 @@ export function useGetStakingValidatorDetails({ totalStake: totalStakeFormatted, totalStakeOriginal: totalStake, totalValidatorsStake: totalValidatorsStakeFormatted, + totalValidatorsStakeOriginal: totalValidatorStake, totalStakePercentage, validatorApy, systemDataResult, diff --git a/apps/ui-kit/src/lib/components/molecules/input/InputWrapper.tsx b/apps/ui-kit/src/lib/components/molecules/input/InputWrapper.tsx index 6c06934ff9..fb3a20a934 100644 --- a/apps/ui-kit/src/lib/components/molecules/input/InputWrapper.tsx +++ b/apps/ui-kit/src/lib/components/molecules/input/InputWrapper.tsx @@ -5,6 +5,7 @@ import cx from 'classnames'; import { SecondaryText } from '@/components/atoms/secondary-text'; import { LABEL_CLASSES } from './input.classes'; import { createElement } from 'react'; +import type { ReactNode } from 'react'; export enum LabelHtmlTag { Label = 'label', @@ -19,7 +20,7 @@ export interface InputWrapperProps { /** * Shows a caption with the text below the input. */ - caption?: string; + caption?: ReactNode; /** * Error Message. Overrides the caption. */ diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index f5b53b2109..50c7f94e69 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -34,6 +34,7 @@ import { useGetAllOwnedObjects, TIMELOCK_IOTA_TYPE, mapTimelockObjects, + AmountWithFiat, } from '@iota/core'; import { useCurrentAccount, useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; import { useMemo } from 'react'; @@ -167,13 +168,23 @@ function StakingDashboardPage(): React.JSX.Element {
+ } /> + } />
diff --git a/apps/wallet-dashboard/components/dialogs/staking/views/DetailsView.tsx b/apps/wallet-dashboard/components/dialogs/staking/views/DetailsView.tsx index a4880b3cee..8177ab4ab6 100644 --- a/apps/wallet-dashboard/components/dialogs/staking/views/DetailsView.tsx +++ b/apps/wallet-dashboard/components/dialogs/staking/views/DetailsView.tsx @@ -14,6 +14,7 @@ import { useIsActiveValidator, useGetNextEpochCommitteeMember, useGetInactiveValidator, + AmountWithFiat, } from '@iota/core'; import { Header, @@ -164,14 +165,24 @@ export function DetailsView({ <div className="flex flex-col gap-y-sm p-md"> <KeyValueInfo keyText="Your Stake" - value={totalStakeFormatted} - supportingLabel={totalStakeSymbol} + value={ + <AmountWithFiat + amount={totalStake} + formatted={totalStakeFormatted} + symbol={totalStakeSymbol} + /> + } fullwidth /> <KeyValueInfo keyText="Earned" - value={iotaEarnedFormatted} - supportingLabel={iotaEarnedSymbol} + value={ + <AmountWithFiat + amount={iotaEarned} + formatted={iotaEarnedFormatted} + symbol={iotaEarnedSymbol} + /> + } fullwidth /> <Divider /> diff --git a/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountDialogLayout.tsx b/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountDialogLayout.tsx index f3abec741d..99ece258c7 100644 --- a/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountDialogLayout.tsx +++ b/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountDialogLayout.tsx @@ -35,7 +35,7 @@ interface FormValues { interface EnterAmountDialogLayoutProps { selectedValidator: string; senderAddress: string; - caption: string; + caption: React.ReactNode; renderInfo?: React.JSX.Element; isLoading: boolean; onBack: () => void; diff --git a/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountView.tsx b/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountView.tsx index 71dfc5ff76..278c3dbbe2 100644 --- a/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountView.tsx +++ b/apps/wallet-dashboard/components/dialogs/staking/views/EnterAmountView.tsx @@ -9,6 +9,7 @@ import { getGasBudgetErrorMessage, NO_BALANCE_GENERIC_MESSAGE, useValidatorInfo, + AmountWithFiat, } from '@iota/core'; import { CoinFormat, IOTA_TYPE_ARG, parseAmount } from '@iota/iota-sdk/utils'; import { useFormikContext } from 'formik'; @@ -73,9 +74,18 @@ export function EnterAmountView({ balance: availableBalance, format: CoinFormat.Full, }); - const caption = availableBalance - ? `${availableBalanceFormatted} ${availableBalanceFormattedSymbol} Available` - : '--'; + const caption = availableBalance ? ( + <span className="flex flex-row items-baseline gap-1"> + <AmountWithFiat + amount={availableBalance} + formatted={availableBalanceFormatted} + symbol={availableBalanceFormattedSymbol} + /> + <span>Available</span> + </span> + ) : ( + '--' + ); const gasUnstakeBuffer = gasSummary?.budget ? BigInt(gasSummary.budget) * BigInt(2) : BigInt(0); const maxSafeAmount = availableBalance - gasUnstakeBuffer; diff --git a/apps/wallet-dashboard/components/dialogs/staking/views/EnterTimelockedAmountView.tsx b/apps/wallet-dashboard/components/dialogs/staking/views/EnterTimelockedAmountView.tsx index 4d2df3cf3c..ec77d874eb 100644 --- a/apps/wallet-dashboard/components/dialogs/staking/views/EnterTimelockedAmountView.tsx +++ b/apps/wallet-dashboard/components/dialogs/staking/views/EnterTimelockedAmountView.tsx @@ -11,6 +11,7 @@ import { toast, getGasBudgetErrorMessage, useCoinMetadata, + AmountWithFiat, } from '@iota/core'; import { CoinFormat, IOTA_TYPE_ARG, parseAmount } from '@iota/iota-sdk/utils'; import { useFormikContext } from 'formik'; @@ -96,7 +97,16 @@ export function EnterTimelockedAmountView({ format: CoinFormat.Full, }); - const caption = `${maxTokenFormatted} ${maxTokenFormattedSymbol} Available`; + const caption = ( + <span className="flex flex-row items-baseline gap-1"> + <AmountWithFiat + amount={maxStakableTimelockedAmount} + formatted={maxTokenFormatted} + symbol={maxTokenFormattedSymbol} + /> + <span>Available</span> + </span> + ); const info = useMemo(() => { if (isSearchingProtocolMaxAmount) { let message = 'The current amount is not valid due to the large number of objects. '; diff --git a/apps/wallet-dashboard/components/dialogs/staking/views/StakedInfo.tsx b/apps/wallet-dashboard/components/dialogs/staking/views/StakedInfo.tsx index 66931587cc..8bb8849f97 100644 --- a/apps/wallet-dashboard/components/dialogs/staking/views/StakedInfo.tsx +++ b/apps/wallet-dashboard/components/dialogs/staking/views/StakedInfo.tsx @@ -5,6 +5,7 @@ import { formatPercentageDisplay, useValidatorInfo, useGetStakingValidatorDetails, + AmountWithFiat, } from '@iota/core'; import { KeyValueInfo, Panel, TooltipPosition } from '@iota/apps-ui-kit'; @@ -18,13 +19,19 @@ export function StakedInfo({ validatorAddress, accountAddress }: StakedInfoProps validatorAddress: validatorAddress, }); - const { totalValidatorsStake, totalStakePercentage, totalStake, effectiveCommission } = - useGetStakingValidatorDetails({ - accountAddress: accountAddress, - stakeId: null, - validatorAddress: validatorAddress, - unstake: false, - }); + const { + totalValidatorsStake, + totalValidatorsStakeOriginal, + totalStakePercentage, + totalStake, + totalStakeOriginal, + effectiveCommission, + } = useGetStakingValidatorDetails({ + accountAddress: accountAddress, + stakeId: null, + validatorAddress: validatorAddress, + unstake: false, + }); const [totalValidatorStakeFormatted, totalValidatorStakeSymbol] = totalValidatorsStake; const [totalStakeFormatted, totalStakeSymbol] = totalStake; @@ -57,16 +64,26 @@ export function StakedInfo({ validatorAddress, accountAddress }: StakedInfoProps keyText="Total Staked" tooltipPosition={TooltipPosition.Right} tooltipText="The full amount of IOTA staked by this validator and delegators for network validation and rewards." - value={totalValidatorStakeFormatted} - supportingLabel={totalValidatorStakeSymbol} + value={ + <AmountWithFiat + amount={totalValidatorsStakeOriginal} + formatted={totalValidatorStakeFormatted} + symbol={totalValidatorStakeSymbol} + /> + } fullwidth /> <KeyValueInfo keyText="Your Staked IOTA" tooltipPosition={TooltipPosition.Right} tooltipText="Your current staked balance." - value={totalStakeFormatted} - supportingLabel={totalStakeSymbol} + value={ + <AmountWithFiat + amount={totalStakeOriginal} + formatted={totalStakeFormatted} + symbol={totalStakeSymbol} + /> + } fullwidth /> </div> diff --git a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx index 0b4893c1de..a6a9194a6a 100644 --- a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx @@ -9,6 +9,7 @@ import { useTotalDelegatedStake, useBalanceVisible, BALANCE_MASK, + AmountWithFiat, } from '@iota/core'; import { DelegatedStake } from '@iota/iota-sdk/client'; interface StakingDataProps { @@ -36,13 +37,18 @@ export function StakingData({ stakingData }: StakingDataProps) { size={LabelTextSize.Large} label="Staked" text={ - stakeResult.isPending - ? '-' - : isBalanceVisible - ? `${formattedDelegatedStake}` - : BALANCE_MASK + stakeResult.isPending ? ( + '-' + ) : isBalanceVisible ? ( + <AmountWithFiat + amount={totalDelegatedStake} + formatted={formattedDelegatedStake} + symbol={stakeSymbol} + /> + ) : ( + `${BALANCE_MASK} ${stakeSymbol}` + ) } - supportingLabel={stakeSymbol} /> </div> <div className="w-1/2"> @@ -50,13 +56,18 @@ export function StakingData({ stakingData }: StakingDataProps) { size={LabelTextSize.Large} label="Earned" text={ - rewardsResult.isPending - ? '-' - : isBalanceVisible - ? `${formattedDelegatedRewards}` - : BALANCE_MASK + rewardsResult.isPending ? ( + '-' + ) : isBalanceVisible ? ( + <AmountWithFiat + amount={totalDelegatedRewards} + formatted={formattedDelegatedRewards} + symbol={rewardsSymbol} + /> + ) : ( + `${BALANCE_MASK} ${rewardsSymbol}` + ) } - supportingLabel={rewardsSymbol} /> </div> </div> diff --git a/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx b/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx index 7e67d8c135..ca5c5fa759 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx @@ -1,9 +1,8 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { formatBalanceToUSD, useBalanceInUSD, useFormatCoin, BALANCE_MASK } from '@iota/core'; -import { IOTA_TYPE_ARG, CoinFormat, formatBalance } from '@iota/iota-sdk/utils'; -import { useMemo } from 'react'; +import { CoinFiatValue, useBalanceInUSD, useFormatCoin, BALANCE_MASK } from '@iota/core'; +import { CoinFormat, formatBalance } from '@iota/iota-sdk/utils'; import { Button, ButtonSize, ButtonType, Tooltip, TooltipPosition } from '@iota/apps-ui-kit'; import BigNumber from 'bignumber.js'; import { useAppSelector, useBalanceVisibility } from '_src/ui/app/hooks'; @@ -16,27 +15,33 @@ export interface CoinProps { interface WalletBalanceUsdProps { amount: bigint; + coinType: string; isVisible: boolean; } -function WalletBalanceUsd({ amount: walletBalance, isVisible }: WalletBalanceUsdProps) { +function WalletBalanceUsd({ amount: walletBalance, coinType, isVisible }: WalletBalanceUsdProps) { const network = useAppSelector((state) => state.app.network); - const formattedWalletBalance = useBalanceInUSD(IOTA_TYPE_ARG, walletBalance, network); + const usdValue = useBalanceInUSD(coinType, walletBalance, network); - const walletBalanceInUsd = useMemo(() => { - if (!formattedWalletBalance) return null; - - return `~${formatBalanceToUSD(formattedWalletBalance)}`; - }, [formattedWalletBalance]); - - if (!walletBalanceInUsd) { + if (usdValue === null || usdValue === undefined || Math.abs(usdValue) < 0.005) { return null; } return ( - <div className="flex items-center gap-1 text-label-md text-iota-neutral-40 dark:text-iota-neutral-60"> - <span>{isVisible ? walletBalanceInUsd : BALANCE_MASK}</span> - <span>USD</span> + <div className="flex items-center gap-1 text-label-md text-iota-neutral-40 dark:text-iota-neutral-60 [&>span]:!text-label-md"> + {isVisible ? ( + <> + <span>~</span> + <CoinFiatValue + amount={walletBalance} + coinType={coinType} + withParentheses={false} + /> + <span>USD</span> + </> + ) : ( + <span>{BALANCE_MASK}</span> + )} </div> ); } @@ -96,7 +101,7 @@ export function CoinBalance({ amount: walletBalance, type }: CoinProps) { /> </div> </div> - <WalletBalanceUsd amount={walletBalance} isVisible={isBalanceVisible} /> + <WalletBalanceUsd amount={walletBalance} coinType={type} isVisible={isBalanceVisible} /> </> ); } diff --git a/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx b/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx index c0be7b8cd5..99fc17ecaa 100644 --- a/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx +++ b/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx @@ -21,6 +21,7 @@ import { useIsValidatorCommitteeMember, useIsActiveValidator, useGetNextEpochCommitteeMember, + AmountWithFiat, } from '@iota/core'; import { Network, type StakeObject } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG, CoinFormat } from '@iota/iota-sdk/utils'; @@ -183,14 +184,24 @@ export function DelegationDetailCard({ validatorAddress, stakedId }: DelegationD <div className="flex flex-col gap-y-sm p-md"> <KeyValueInfo keyText="Your Stake" - value={totalStakeFormatted} - supportingLabel={totalStakeSymbol} + value={ + <AmountWithFiat + amount={totalStake} + formatted={totalStakeFormatted} + symbol={totalStakeSymbol} + /> + } fullwidth /> <KeyValueInfo keyText="Earned" - value={iotaEarnedFormatted} - supportingLabel={iotaEarnedSymbol} + value={ + <AmountWithFiat + amount={iotaEarned} + formatted={iotaEarnedFormatted} + symbol={iotaEarnedSymbol} + /> + } fullwidth /> <Divider /> diff --git a/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx b/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx index 1407bdb1a0..fe25a6f6b9 100644 --- a/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx +++ b/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx @@ -16,6 +16,7 @@ import { NO_BALANCE_GENERIC_MESSAGE, getGasBudgetErrorMessage, useGetValidatorsApy, + AmountWithFiat, } from '@iota/core'; import * as Sentry from '@sentry/react'; import { ampli } from '_src/shared/analytics/ampli'; @@ -242,9 +243,18 @@ export function StakeFormComponent({ validatorAddress, epoch, onSuccess }: Stake placeholder={`0 ${symbol}`} value={amount} caption={ - minAmountTxGasBudget - ? `${availableBalanceFormatted} ${symbol} Available` - : '--' + minAmountTxGasBudget ? ( + <span className="flex flex-row items-baseline gap-1"> + <AmountWithFiat + amount={availableBalance} + formatted={availableBalanceFormatted} + symbol={symbol} + /> + <span>Available</span> + </span> + ) : ( + '--' + ) } suffix={' ' + symbol} errorMessage={amount && meta.error ? meta.error : undefined} diff --git a/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx b/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx index 77082cb3fc..3dabb3904e 100644 --- a/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx +++ b/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx @@ -6,6 +6,7 @@ import { EFFECTIVE_COMMISSION_TOOLTIP, formatPercentageDisplay, useGetStakingValidatorDetails, + AmountWithFiat, } from '@iota/core'; import { useSearchParams } from 'react-router-dom'; import { useActiveAddress } from '_hooks'; @@ -34,7 +35,9 @@ export function ValidatorFormDetail({ validatorAddress, unstake }: ValidatorForm totalStakePercentage, validatorApy: { apy, isApyApproxZero }, totalValidatorsStake: [totalValidatorStakeFormatted, totalValidatorStakeSymbol], + totalValidatorsStakeOriginal, totalStake: [totalStakeFormatted, totalStakeSymbol], + totalStakeOriginal, delegatedStakeDataResult, systemDataResult, effectiveCommission, @@ -102,16 +105,26 @@ export function ValidatorFormDetail({ validatorAddress, unstake }: ValidatorForm keyText="Total Staked" tooltipPosition={TooltipPosition.Bottom} tooltipText="The full amount of IOTA staked by this validator and delegators for network validation and rewards." - value={totalValidatorStakeFormatted} - supportingLabel={totalValidatorStakeSymbol} + value={ + <AmountWithFiat + amount={totalValidatorsStakeOriginal} + formatted={totalValidatorStakeFormatted} + symbol={totalValidatorStakeSymbol} + /> + } fullwidth /> <KeyValueInfo keyText="Your Staked IOTA" tooltipPosition={TooltipPosition.Bottom} tooltipText="Your current staked balance." - value={totalStakeFormatted} - supportingLabel={totalStakeSymbol} + value={ + <AmountWithFiat + amount={totalStakeOriginal} + formatted={totalStakeFormatted} + symbol={totalStakeSymbol} + /> + } fullwidth /> </> diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx index 18ac89f894..c20337500f 100644 --- a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx +++ b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx @@ -12,6 +12,7 @@ import { DELEGATED_STAKES_QUERY_STALE_TIME, useFormatCoin, StakedCard, + AmountWithFiat, } from '@iota/core'; import { useMemo } from 'react'; import { useActiveAddress } from '_hooks'; @@ -115,13 +116,23 @@ export function ValidatorsCard() { <div className="flex gap-xs py-md"> <DisplayStats label="Your stake" - value={totalDelegatedStakeFormatted} - supportingLabel={symbol} + value={ + <AmountWithFiat + amount={totalDelegatedStake} + formatted={totalDelegatedStakeFormatted} + symbol={symbol} + /> + } /> <DisplayStats label="Earned" - value={totalDelegatedRewardsFormatted} - supportingLabel={symbol} + value={ + <AmountWithFiat + amount={totalDelegatedRewards} + formatted={totalDelegatedRewardsFormatted} + symbol={symbol} + /> + } /> </div> <Title title="In progress" size={TitleSize.Small} /> From 71f562bd5426dd7107835aa1aa3501b83623b1bc Mon Sep 17 00:00:00 2001 From: Juligs <sjgomez@boxfish.studio> Date: Wed, 5 Aug 2026 17:10:21 +0200 Subject: [PATCH 6/7] feat(wallet): show fiat value on staking rewards amount --- .../src/components/coin/AmountWithFiat.tsx | 23 +++++++++++++++--- apps/core/src/components/stake/StakedCard.tsx | 24 +++++++++++++++++-- apps/core/src/hooks/useStakeRewardStatus.ts | 5 ++++ .../components/molecules/card/CardAction.tsx | 4 ++-- .../staking-overview/StakingData.tsx | 4 ++++ .../pages/home/tokens/coin-balance/index.tsx | 4 ++-- .../app/staking/validators/ValidatorsCard.tsx | 4 ++++ 7 files changed, 59 insertions(+), 9 deletions(-) diff --git a/apps/core/src/components/coin/AmountWithFiat.tsx b/apps/core/src/components/coin/AmountWithFiat.tsx index 6982a7cf15..31f9753092 100644 --- a/apps/core/src/components/coin/AmountWithFiat.tsx +++ b/apps/core/src/components/coin/AmountWithFiat.tsx @@ -12,6 +12,9 @@ export interface AmountWithFiatProps { formatted: string; symbol?: string; coinType?: string; + direction?: 'row' | 'column'; + showApproxSymbol?: boolean; + align?: 'start' | 'end'; } export function AmountWithFiat({ @@ -19,20 +22,34 @@ export function AmountWithFiat({ formatted, symbol, coinType = IOTA_TYPE_ARG, + direction = 'row', + showApproxSymbol = true, + align = 'start', }: AmountWithFiatProps) { const { network } = useIotaClientContext(); const value = useBalanceInUSD(coinType, amount, network as Network); const hasFiatValue = value !== null && value !== undefined && Math.abs(value) >= 0.005; + const alignClass = align === 'end' ? 'items-end justify-end' : 'items-start justify-start'; return ( - <span className="flex flex-row flex-wrap items-baseline gap-x-1"> + <span + className={ + direction === 'column' + ? `flex flex-col ${alignClass} gap-x-1` + : 'flex flex-row flex-wrap items-baseline gap-x-1' + } + > <span className="whitespace-nowrap"> {formatted} {symbol ? ` ${symbol}` : ''} </span> {hasFiatValue && ( - <span className="flex flex-row items-baseline gap-1 whitespace-nowrap [&>span]:!text-body-sm"> - <span className="key-supporting-text-color text-body-sm">~</span> + <span + className={`flex flex-row items-baseline gap-1 whitespace-nowrap [&>span]:!text-body-sm ${direction === 'column' ? alignClass : ''}`} + > + {showApproxSymbol && ( + <span className="key-supporting-text-color text-body-sm">~</span> + )} <CoinFiatValue amount={amount} coinType={coinType} withParentheses={false} /> </span> )} diff --git a/apps/core/src/components/stake/StakedCard.tsx b/apps/core/src/components/stake/StakedCard.tsx index 6a166ba8f2..c86b9d03b8 100644 --- a/apps/core/src/components/stake/StakedCard.tsx +++ b/apps/core/src/components/stake/StakedCard.tsx @@ -39,7 +39,7 @@ export function StakedCard({ const { principal, stakeRequestEpoch, estimatedReward, validatorAddress } = extendedStake; const { data } = useIotaClientQuery('getLatestIotaSystemState'); - const { title, subtitle } = useStakeRewardStatus({ + const { title, subtitle, rewards, isRewardAmount } = useStakeRewardStatus({ stakeRequestEpoch, currentEpoch, estimatedReward, @@ -50,6 +50,9 @@ export function StakedCard({ const [principalStaked, symbol] = useFormatCoin({ balance: principal, }); + const [rewardsFormatted, rewardsSymbol] = useFormatCoin({ + balance: rewards, + }); const validatorMeta = useMemo(() => { if (!data) return null; @@ -92,7 +95,24 @@ export function StakedCard({ } tooltipPosition={TooltipPosition.Bottom} /> - <CardAction title={title} subtitle={subtitle} type={CardActionType.SupportingText} /> + <CardAction + title={ + isRewardAmount ? ( + <AmountWithFiat + amount={rewards} + formatted={rewardsFormatted} + symbol={rewardsSymbol} + direction="column" + showApproxSymbol={false} + align="end" + /> + ) : ( + title + ) + } + subtitle={subtitle} + type={CardActionType.SupportingText} + /> </Card> ); } diff --git a/apps/core/src/hooks/useStakeRewardStatus.ts b/apps/core/src/hooks/useStakeRewardStatus.ts index c2e5aa2a8b..c57556e305 100644 --- a/apps/core/src/hooks/useStakeRewardStatus.ts +++ b/apps/core/src/hooks/useStakeRewardStatus.ts @@ -70,10 +70,15 @@ export function useStakeRewardStatus({ return statusText[delegationState]; }; + const isRewardAmount = + !Number(epochBeforeRewards) && + (delegationState === StakeState.Earning || delegationState === StakeState.NotEarning); + return { rewards, title: rewardTime(), subtitle: STATUS_COPY[delegationState], + isRewardAmount, }; } export enum StakeState { diff --git a/apps/ui-kit/src/lib/components/molecules/card/CardAction.tsx b/apps/ui-kit/src/lib/components/molecules/card/CardAction.tsx index f6e1aa7050..1468fdd5c9 100644 --- a/apps/ui-kit/src/lib/components/molecules/card/CardAction.tsx +++ b/apps/ui-kit/src/lib/components/molecules/card/CardAction.tsx @@ -6,7 +6,7 @@ import { CardActionType } from './card.enums'; import { Button, ButtonSize, ButtonType } from '@/components/atoms/button'; export type CardActionProps = { - title?: string; + title?: React.ReactNode; subtitle?: string | React.ReactNode; type: CardActionType; onClick?: () => void; @@ -64,7 +64,7 @@ export function CardAction({ <Button type={buttonType || ButtonType.Outlined} size={ButtonSize.Small} - text={title} + text={title as string} onClick={handleActionClick} icon={icon} iconAfterText={iconAfterText} diff --git a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx index a6a9194a6a..09c5757d0d 100644 --- a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx @@ -44,6 +44,8 @@ export function StakingData({ stakingData }: StakingDataProps) { amount={totalDelegatedStake} formatted={formattedDelegatedStake} symbol={stakeSymbol} + direction="column" + showApproxSymbol={false} /> ) : ( `${BALANCE_MASK} ${stakeSymbol}` @@ -63,6 +65,8 @@ export function StakingData({ stakingData }: StakingDataProps) { amount={totalDelegatedRewards} formatted={formattedDelegatedRewards} symbol={rewardsSymbol} + direction="column" + showApproxSymbol={false} /> ) : ( `${BALANCE_MASK} ${rewardsSymbol}` diff --git a/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx b/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx index ca5c5fa759..e60ce94767 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx @@ -67,7 +67,7 @@ export function CoinBalance({ amount: walletBalance, type }: CoinProps) { ); return ( - <> + <div className="flex flex-col gap-xxs"> <div className="flex items-baseline gap-0.5"> {shouldShowTooltip && isBalanceVisible ? ( <Tooltip @@ -102,6 +102,6 @@ export function CoinBalance({ amount: walletBalance, type }: CoinProps) { </div> </div> <WalletBalanceUsd amount={walletBalance} coinType={type} isVisible={isBalanceVisible} /> - </> + </div> ); } diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx index c20337500f..c7a9ed94bc 100644 --- a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx +++ b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx @@ -121,6 +121,8 @@ export function ValidatorsCard() { amount={totalDelegatedStake} formatted={totalDelegatedStakeFormatted} symbol={symbol} + direction="column" + showApproxSymbol={false} /> } /> @@ -131,6 +133,8 @@ export function ValidatorsCard() { amount={totalDelegatedRewards} formatted={totalDelegatedRewardsFormatted} symbol={symbol} + direction="column" + showApproxSymbol={false} /> } /> From 805f97fa27bca24ca27ea9d59fefe254d56eb820 Mon Sep 17 00:00:00 2001 From: Juligs <sjgomez@boxfish.studio> Date: Wed, 5 Aug 2026 18:12:56 +0200 Subject: [PATCH 7/7] fix(wallet-dashboard): hide approx symbol on stake summary cards --- .../app/(protected)/staking/page.tsx | 2 + .../account-balance/AccountBalance.tsx | 62 ++++++++++--------- .../staking-overview/StakingData.tsx | 2 - .../transactions/TransactionTile.tsx | 17 ++++- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/apps/wallet-dashboard/app/(protected)/staking/page.tsx b/apps/wallet-dashboard/app/(protected)/staking/page.tsx index 50c7f94e69..d39c92b611 100644 --- a/apps/wallet-dashboard/app/(protected)/staking/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/staking/page.tsx @@ -173,6 +173,7 @@ function StakingDashboardPage(): React.JSX.Element { amount={totalDelegatedStake} formatted={totalDelegatedStakeFormatted} symbol={symbol} + showApproxSymbol={false} /> } /> @@ -183,6 +184,7 @@ function StakingDashboardPage(): React.JSX.Element { amount={totalDelegatedRewards} formatted={totalDelegatedRewardsFormatted} symbol={symbol} + showApproxSymbol={false} /> } /> diff --git a/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx b/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx index cf7b9c2783..102c937dd7 100644 --- a/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx +++ b/apps/wallet-dashboard/components/account-balance/AccountBalance.tsx @@ -71,38 +71,40 @@ export function AccountBalance() { /> </div> )} - <div className="flex items-baseline gap-xs"> - <span - data-testid="balance-amount" - className="text-headline-lg text-iota-neutral-10 dark:text-iota-neutral-92" - > - {isBalanceVisible ? formatted : BALANCE_MASK} - </span> - <div className="flex items-center gap-xs text-label-md text-iota-neutral-40 dark:text-iota-neutral-60"> - <span>{symbol}</span> - <Button - type={ButtonType.Ghost} - size={ButtonSize.Small} - onClick={toggleBalanceVisible} - className="flex items-center transition-colors hover:text-iota-neutral-10 dark:hover:text-iota-neutral-92" - aria-label={ - isBalanceVisible ? 'Hide balances' : 'Show balances' - } - icon={ - isBalanceVisible ? ( - <VisibilityOn className="h-4 w-4" /> - ) : ( - <VisibilityOff className="h-4 w-4" /> - ) - } - /> + <div className="flex flex-col items-center gap-y-xxs"> + <div className="flex items-baseline gap-xs"> + <span + data-testid="balance-amount" + className="text-headline-lg text-iota-neutral-10 dark:text-iota-neutral-92" + > + {isBalanceVisible ? formatted : BALANCE_MASK} + </span> + <div className="flex items-center gap-xs text-label-md text-iota-neutral-40 dark:text-iota-neutral-60"> + <span>{symbol}</span> + <Button + type={ButtonType.Ghost} + size={ButtonSize.Small} + onClick={toggleBalanceVisible} + className="flex items-center transition-colors hover:text-iota-neutral-10 dark:hover:text-iota-neutral-92" + aria-label={ + isBalanceVisible ? 'Hide balances' : 'Show balances' + } + icon={ + isBalanceVisible ? ( + <VisibilityOn className="h-4 w-4" /> + ) : ( + <VisibilityOff className="h-4 w-4" /> + ) + } + /> + </div> </div> + {fiatBalance && ( + <div className="text-body-md text-iota-neutral-10 dark:text-iota-neutral-92"> + {isBalanceVisible ? fiatBalance : `${BALANCE_MASK} USD`} + </div> + )} </div> - {fiatBalance && ( - <div className="text-body-md text-iota-neutral-10 dark:text-iota-neutral-92"> - {isBalanceVisible ? fiatBalance : `${BALANCE_MASK} USD`} - </div> - )} </div> <div className="flex w-full max-w-56 gap-xs"> <Button diff --git a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx index 09c5757d0d..824e62a0ed 100644 --- a/apps/wallet-dashboard/components/staking-overview/StakingData.tsx +++ b/apps/wallet-dashboard/components/staking-overview/StakingData.tsx @@ -44,7 +44,6 @@ export function StakingData({ stakingData }: StakingDataProps) { amount={totalDelegatedStake} formatted={formattedDelegatedStake} symbol={stakeSymbol} - direction="column" showApproxSymbol={false} /> ) : ( @@ -65,7 +64,6 @@ export function StakingData({ stakingData }: StakingDataProps) { amount={totalDelegatedRewards} formatted={formattedDelegatedRewards} symbol={rewardsSymbol} - direction="column" showApproxSymbol={false} /> ) : ( diff --git a/apps/wallet-dashboard/components/transactions/TransactionTile.tsx b/apps/wallet-dashboard/components/transactions/TransactionTile.tsx index ec5094d675..8a52d46731 100644 --- a/apps/wallet-dashboard/components/transactions/TransactionTile.tsx +++ b/apps/wallet-dashboard/components/transactions/TransactionTile.tsx @@ -25,6 +25,7 @@ import { formatDate, isMigrationTransaction, BALANCE_MASK, + AmountWithFiat, } from '@iota/core'; import { useCurrentAccount } from '@iota/dapp-kit'; import { TransactionDetailsLayout } from '../dialogs/transaction/TransactionDetailsLayout'; @@ -122,7 +123,21 @@ export function TransactionTile({ transaction, hideBalance }: TransactionTilePro <CardAction type={CardActionType.SupportingText} title={ - txnFailed ? '--' : `${hideBalance ? BALANCE_MASK : formatAmount} ${symbol}` + txnFailed ? ( + '--' + ) : hideBalance ? ( + `${BALANCE_MASK} ${symbol}` + ) : ( + <AmountWithFiat + amount={balance} + formatted={formatAmount} + symbol={symbol} + coinType={coinType} + direction="column" + align="end" + showApproxSymbol={false} + /> + ) } /> </Card>