From 4d6afd7afc9fc67b6f780f1b9d4127b4a84d104a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jun 2026 16:41:06 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tokendetails/model/TokenDetailsModel.kt | 3 + .../state/TokenDetailsBalanceBlockUM.kt | 2 + .../transformer/SetBalanceTransformer.kt | 14 ++ .../SetYieldSupplyBalanceTransformer.kt | 27 ++++ .../ui/components/TokenDetailsBalanceBlock.kt | 55 ++++++- .../transformer/SetBalanceTransformerTest.kt | 83 +++++++++- .../SetYieldSupplyBalanceTransformerTest.kt | 146 ++++++++++++++++++ 7 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index ac85882198..5aa1becb9c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -109,6 +109,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.Q import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetYieldSupplyBalanceTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateActionButtonsTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateAddFundsTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTransferTransformer @@ -470,6 +471,7 @@ internal class TokenDetailsModel @Inject constructor( yieldSupplyGetRewardsBalanceUseCase(status = status, appCurrency = selectedAppCurrencyFlow.value) .onEach { formatted -> uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted) + redesignStateController.update(SetYieldSupplyBalanceTransformer(formatted)) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -479,6 +481,7 @@ internal class TokenDetailsModel @Inject constructor( uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( YieldSupplyRewardBalance.empty(), ) + redesignStateController.update(SetYieldSupplyBalanceTransformer(YieldSupplyRewardBalance.empty())) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt index 406042f571..8a1b6d4902 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt @@ -37,6 +37,8 @@ internal sealed class TokenDetailsBalanceBlockUM { val displayFiatBalanceAvailable: TextReference?, val isBalanceFlickering: Boolean, val isBalanceZero: Boolean, + val displayYieldSupplyFiatBalance: String? = null, + val displayYieldSupplyCryptoBalance: String? = null, ) : TokenDetailsBalanceBlockUM() { val displayCryptoBalance: TextReference diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt index e2f6f7f828..99bb251bdb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt @@ -86,6 +86,9 @@ internal class SetBalanceTransformer( val totalCryptoAmount = computeTotal(status.value.amount, stakingCryptoAmount) + val isYieldSupplyActive = status.value.yieldSupplyStatus?.isActive == true + val prevContent = prev as? TokenDetailsBalanceBlockUM.Content + return TokenDetailsBalanceBlockUM.Content( addFundsButton = prev.addFundsButton, swapButton = prev.swapButton, @@ -108,6 +111,17 @@ internal class SetBalanceTransformer( }, isBalanceFlickering = status.value.sources.total == StatusSource.CACHE, isBalanceZero = totalCryptoAmount.isNullOrZero(), + // Preserve the ticking yield balance produced by SetYieldSupplyBalanceTransformer across status updates + displayYieldSupplyFiatBalance = if (isYieldSupplyActive) { + prevContent?.displayYieldSupplyFiatBalance + } else { + null + }, + displayYieldSupplyCryptoBalance = if (isYieldSupplyActive) { + prevContent?.displayYieldSupplyCryptoBalance + } else { + null + }, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt new file mode 100644 index 0000000000..e924a6a7b6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +/** + * Pushes the ticking yield supply balance (emitted every tick by [YieldSupplyGetRewardsBalanceUseCase]) + * into the redesign [TokenDetailsBalanceBlockUM.Content], so the balance increments in real time. + */ +internal class SetYieldSupplyBalanceTransformer( + private val yieldSupplyRewardBalance: YieldSupplyRewardBalance, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val balanceBlockUM = prevState.balanceBlockUM + if (balanceBlockUM !is TokenDetailsBalanceBlockUM.Content) return prevState + + return prevState.copy( + balanceBlockUM = balanceBlockUM.copy( + displayYieldSupplyFiatBalance = yieldSupplyRewardBalance.fiatBalance, + displayYieldSupplyCryptoBalance = yieldSupplyRewardBalance.cryptoBalance, + ), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 6592a5416a..56957faa3b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -11,7 +11,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -23,10 +25,12 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.text.TextAnimatedCounter import com.tangem.core.ui.ds.button.TangemButtonType import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.ds.button.action.ActionButtons import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resolveReference @@ -42,6 +46,9 @@ import kotlinx.collections.immutable.persistentListOf private val CurrencyIconSize: Dp = 70.dp private val NetworkBadgeSize: Dp = 24.dp +/** OpenType "tabular figures" feature — makes every digit the same width to prevent horizontal jitter. */ +private const val TABULAR_FIGURES_FEATURE = "tnum" + @Composable internal fun TokenDetailsBalanceBlock( balanceBlockUM: TokenDetailsBalanceBlockUM, @@ -124,20 +131,60 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd } } SpacerH(TangemTheme.dimens2.x2) - Text( + AnimatedBalance( modifier = Modifier.testTag(TokenDetailsScreenTestTags.BALANCE_FIAT), - text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + yieldBalance = state.displayYieldSupplyFiatBalance, + fallbackBalance = state.displayFiatBalance, style = TangemTheme.typography2.titleRegular44, color = TangemTheme.colors2.text.neutral.primary, + isBalanceHidden = isBalanceHidden, ) SpacerH(TangemTheme.dimens2.x2_5) - Text( - text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + AnimatedBalance( + yieldBalance = state.displayYieldSupplyCryptoBalance, + fallbackBalance = state.displayCryptoBalance, style = TangemTheme.typography2.bodySemibold16, color = TangemTheme.colors2.text.neutral.secondary, + isBalanceHidden = isBalanceHidden, ) } +/** + * Renders a balance that animates digit-by-digit ([TextAnimatedCounter]) while a ticking yield supply + * value is present, and falls back to a plain [Text] otherwise (or when the balance is hidden). + */ +@Composable +private fun AnimatedBalance( + yieldBalance: String?, + fallbackBalance: TextReference, + style: TextStyle, + color: Color, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens2.x6), + contentAlignment = Alignment.Center, + ) { + if (yieldBalance != null && !isBalanceHidden) { + TextAnimatedCounter( + text = yieldBalance, + // Tabular figures keep every digit the same width, so the centered balance doesn't + // jitter horizontally as digits roll during the increment animation. + style = style.copy(color = color, fontFeatureSettings = TABULAR_FIGURES_FEATURE), + ) + } else { + Text( + text = fallbackBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + style = style, + color = color, + ) + } + } +} + @Composable private fun LoadingBody() { Text( diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt index bba4ce1980..7cd9696901 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt @@ -14,6 +14,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.models.yield.supply.YieldSupplyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM @@ -434,6 +435,59 @@ class SetBalanceTransformerTest { // endregion + // region Yield supply balance + + @Test + fun `GIVEN yield active AND prev has yield balances WHEN transform THEN yield balances preserved`() { + // GIVEN + val status = createStatus(loadedValue(yieldSupplyStatus = activeYieldSupplyStatus())) + val transformer = createTransformer(status) + val prev = contentWithYieldBalances(fiat = "$21,000.12", crypto = "10.500001 ETH") + val state = initialState().copy(balanceBlockUM = prev) + + // WHEN + val result = transformer.transform(state) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayYieldSupplyFiatBalance).isEqualTo("$21,000.12") + assertThat(content.displayYieldSupplyCryptoBalance).isEqualTo("10.500001 ETH") + } + + @Test + fun `GIVEN yield inactive AND prev has yield balances WHEN transform THEN yield balances are cleared`() { + // GIVEN + val status = createStatus(loadedValue(yieldSupplyStatus = null)) + val transformer = createTransformer(status) + val prev = contentWithYieldBalances(fiat = "$21,000.12", crypto = "10.500001 ETH") + val state = initialState().copy(balanceBlockUM = prev) + + // WHEN + val result = transformer.transform(state) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayYieldSupplyFiatBalance).isNull() + assertThat(content.displayYieldSupplyCryptoBalance).isNull() + } + + @Test + fun `GIVEN yield active AND prev is not Content WHEN transform THEN yield balances are null`() { + // GIVEN — prev is the default Loading block, so there are no yield balances to preserve yet + val status = createStatus(loadedValue(yieldSupplyStatus = activeYieldSupplyStatus())) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayYieldSupplyFiatBalance).isNull() + assertThat(content.displayYieldSupplyCryptoBalance).isNull() + } + + // endregion + // region Unrelated fields preserved @Test @@ -473,6 +527,7 @@ class SetBalanceTransformerTest { fiatAmount: BigDecimal = BigDecimal("21000"), fiatRate: BigDecimal = BigDecimal("2000"), stakingBalance: StakingBalance? = null, + yieldSupplyStatus: YieldSupplyStatus? = null, sources: CryptoCurrencyStatus.Sources = CryptoCurrencyStatus.Sources(), ): CryptoCurrencyStatus.Loaded = CryptoCurrencyStatus.Loaded( amount = amount, @@ -480,13 +535,39 @@ class SetBalanceTransformerTest { fiatRate = fiatRate, priceChange = BigDecimal("2.5"), stakingBalance = stakingBalance, - yieldSupplyStatus = null, + yieldSupplyStatus = yieldSupplyStatus, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), networkAddress = mockk(relaxed = true), sources = sources, ) + private fun activeYieldSupplyStatus(): YieldSupplyStatus = YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ) + + private fun contentWithYieldBalances( + fiat: String?, + crypto: String?, + ): TokenDetailsBalanceBlockUM.Content = TokenDetailsBalanceBlockUM.Content( + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference(""), + displayFiatBalanceAll = stringReference(""), + displayCryptoBalanceAvailable = null, + displayFiatBalanceAvailable = null, + isBalanceFlickering = false, + isBalanceZero = false, + displayYieldSupplyFiatBalance = fiat, + displayYieldSupplyCryptoBalance = crypto, + ) + private fun noQuoteValue(): CryptoCurrencyStatus.NoQuote = CryptoCurrencyStatus.NoQuote( amount = BigDecimal("5.0"), stakingBalance = null, diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt new file mode 100644 index 0000000000..c8e7c4a92a --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt @@ -0,0 +1,146 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class SetYieldSupplyBalanceTransformerTest { + + @Test + fun `GIVEN Content state WHEN transform THEN yield balances are set from reward balance`() { + // GIVEN + val state = stateWith(contentBlock()) + val transformer = SetYieldSupplyBalanceTransformer( + YieldSupplyRewardBalance(fiatBalance = "$21,000.12", cryptoBalance = "10.500001 ETH"), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayYieldSupplyFiatBalance).isEqualTo("$21,000.12") + assertThat(content.displayYieldSupplyCryptoBalance).isEqualTo("10.500001 ETH") + } + + @Test + fun `GIVEN empty reward balance WHEN transform on Content THEN yield balances are nulled`() { + // GIVEN + val state = stateWith( + contentBlock().copy( + displayYieldSupplyFiatBalance = "$21,000.12", + displayYieldSupplyCryptoBalance = "10.500001 ETH", + ), + ) + val transformer = SetYieldSupplyBalanceTransformer(YieldSupplyRewardBalance.empty()) + + // WHEN + val result = transformer.transform(state) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayYieldSupplyFiatBalance).isNull() + assertThat(content.displayYieldSupplyCryptoBalance).isNull() + } + + @Test + fun `GIVEN Loading block WHEN transform THEN state is unchanged`() { + // GIVEN + val state = stateWith(loadingBlock()) + val transformer = SetYieldSupplyBalanceTransformer( + YieldSupplyRewardBalance(fiatBalance = "$1", cryptoBalance = "1 ETH"), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + } + + @Test + fun `GIVEN Error block WHEN transform THEN state is unchanged`() { + // GIVEN + val state = stateWith(errorBlock()) + val transformer = SetYieldSupplyBalanceTransformer( + YieldSupplyRewardBalance(fiatBalance = "$1", cryptoBalance = "1 ETH"), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + } + + private fun contentBlock(): TokenDetailsBalanceBlockUM.Content = TokenDetailsBalanceBlockUM.Content( + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference("10.5 ETH"), + displayFiatBalanceAll = stringReference("$21,000.00"), + displayCryptoBalanceAvailable = null, + displayFiatBalanceAvailable = null, + isBalanceFlickering = false, + isBalanceZero = false, + ) + + private fun loadingBlock(): TokenDetailsBalanceBlockUM.Loading = TokenDetailsBalanceBlockUM.Loading( + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ) + + private fun errorBlock(): TokenDetailsBalanceBlockUM.Error = TokenDetailsBalanceBlockUM.Error( + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ) + + private fun stateWith(balanceBlockUM: TokenDetailsBalanceBlockUM): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = balanceBlockUM, + notifications = persistentListOf(), + marketPriceBlockState = mockk(relaxed = true), + earnBlockState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun placeholderButton(): TangemButtonUM = TangemButtonUM( + text = stringReference(""), + type = TangemButtonType.Secondary, + onClick = {}, + ) +} \ No newline at end of file